import { useEffect, useRef } from 'react'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import 'leaflet.markercluster/dist/MarkerCluster.css'; import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; import 'leaflet.markercluster'; import type { Report, CleanupEvent, FilterState } from '@/types'; // Fix for default marker icons in Leaflet with Vite import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; import markerIcon from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow, }); interface MapViewProps { reports: Report[]; events?: CleanupEvent[]; filters?: FilterState; onMarkerClick?: (reportId: string) => void; onMapClick?: (lat: number, lng: number) => void; showHeatmap?: boolean; showClusters?: boolean; center?: [number, number]; zoom?: number; } export function MapView({ reports, events = [], filters, onMarkerClick, onMapClick, showClusters = true, center = [53.3498, -6.2603], // Dublin, Ireland default zoom = 7, }: MapViewProps) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const markersLayerRef = useRef(null); const clusterGroupRef = useRef(null); // Initialize map useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; // Create map instance const map = L.map(mapRef.current).setView(center, zoom); // Add OpenStreetMap tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, }).addTo(map); // Handle map clicks if (onMapClick) { map.on('click', (e: L.LeafletMouseEvent) => { onMapClick(e.latlng.lat, e.latlng.lng); }); } // Create markers layer markersLayerRef.current = L.layerGroup(); // Create cluster group with custom configuration clusterGroupRef.current = L.markerClusterGroup({ maxClusterRadius: 50, // Cluster markers within 50 pixels spiderfyOnMaxZoom: true, showCoverageOnHover: true, zoomToBoundsOnClick: true, iconCreateFunction: (cluster) => { const count = cluster.getChildCount(); let sizeClass = 'marker-cluster-small'; if (count > 10) { sizeClass = 'marker-cluster-large'; } else if (count > 5) { sizeClass = 'marker-cluster-medium'; } return L.divIcon({ html: `
${count}
`, className: `marker-cluster ${sizeClass}`, iconSize: L.point(40, 40), }); }, }); mapInstanceRef.current = map; return () => { map.remove(); mapInstanceRef.current = null; markersLayerRef.current = null; clusterGroupRef.current = null; }; }, []); // Filter reports based on filter state const getFilteredReports = (): Report[] => { if (!filters) return reports; return reports.filter((report) => { // Filter by date range if (filters.dateRange) { const reportDate = new Date(report.createdAt); if (filters.dateRange.start && reportDate < filters.dateRange.start) { return false; } if (filters.dateRange.end && reportDate > filters.dateRange.end) { return false; } } // Filter by litter type if (filters.litterTypes && filters.litterTypes.length > 0) { if (!filters.litterTypes.includes(report.litterType)) { return false; } } // Filter by quantity if (filters.quantities && filters.quantities.length > 0) { if (!filters.quantities.includes(report.quantity)) { return false; } } // Filter by verification status if (filters.verificationStatus && filters.verificationStatus.length > 0) { if (!filters.verificationStatus.includes(report.verificationStatus)) { return false; } } return true; }); }; // Update markers when reports, filters, or clustering changes useEffect(() => { if (!mapInstanceRef.current || !markersLayerRef.current || !clusterGroupRef.current) return; const map = mapInstanceRef.current; const markersLayer = markersLayerRef.current; const clusterGroup = clusterGroupRef.current; // Remove existing layers from map if (map.hasLayer(markersLayer)) { map.removeLayer(markersLayer); } if (map.hasLayer(clusterGroup)) { map.removeLayer(clusterGroup); } // Clear existing markers markersLayer.clearLayers(); clusterGroup.clearLayers(); // Get filtered reports const filteredReports = getFilteredReports(); // Add report markers filteredReports.forEach((report) => { const icon = createCustomMarkerIcon(report.litterType, report.quantity); const marker = L.marker([report.latitude, report.longitude], { icon }); // Add popup with report details marker.bindPopup(`

${getLitterTypeLabel(report.litterType)}

Quantity: ${report.quantity}

Status: ${report.verificationStatus}

Date: ${new Date(report.createdAt).toLocaleDateString()}

${report.description ? `

${report.description}

` : ''}
`); // Handle marker click if (onMarkerClick) { marker.on('click', () => { onMarkerClick(report.id); }); } // Add to appropriate layer if (showClusters) { clusterGroup.addLayer(marker); } else { markersLayer.addLayer(marker); } }); // Add event markers (always to regular layer, not clustered) events.forEach((event) => { const icon = createEventMarkerIcon(event.status); const marker = L.marker([event.latitude, event.longitude], { icon }); marker.bindPopup(`

${event.title}

${event.locationName}

Participants: ${event.participantCount}

Status: ${event.status}

`); markersLayer.addLayer(marker); }); // Add appropriate layer to map if (showClusters) { clusterGroup.addTo(map); } markersLayer.addTo(map); }, [reports, events, filters, showClusters, onMarkerClick]); // Handle geolocation const handleGeolocation = () => { if (!navigator.geolocation) { alert('Geolocation is not supported by your browser'); return; } navigator.geolocation.getCurrentPosition( (position) => { const coords: [number, number] = [ position.coords.latitude, position.coords.longitude, ]; if (mapInstanceRef.current) { mapInstanceRef.current.setView(coords, 13); // Add user location marker L.marker(coords, { icon: L.divIcon({ className: 'user-location-marker', html: '
', iconSize: [16, 16], }), }).addTo(mapInstanceRef.current); } }, (error) => { alert(`Error getting location: ${error.message}`); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0, } ); }; return (
{/* Geolocation button */}
); } // Helper function to create custom marker icons based on litter type function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon { const colors = { plastic: '#3B82F6', // blue metal: '#6B7280', // gray glass: '#10B981', // green organic: '#84CC16', // lime hazardous: '#EF4444', // red other: '#8B5CF6', // purple }; const sizes = { minimal: 24, moderate: 28, significant: 32, severe: 36, }; const color = colors[litterType as keyof typeof colors] || colors.other; const size = sizes[quantity as keyof typeof sizes] || sizes.moderate; return L.divIcon({ className: 'custom-marker', html: `
${getLitterTypeIcon(litterType)}
`, iconSize: [size, size], iconAnchor: [size / 2, size], }); } // Helper function to create event marker icons function createEventMarkerIcon(status: string): L.DivIcon { const colors = { upcoming: '#F59E0B', // amber completed: '#10B981', // green cancelled: '#6B7280', // gray }; const color = colors[status as keyof typeof colors] || colors.upcoming; return L.divIcon({ className: 'event-marker', html: `
📅
`, iconSize: [32, 32], iconAnchor: [16, 16], }); } // Helper function to get litter type icon function getLitterTypeIcon(litterType: string): string { const icons: Record = { plastic: '♻', metal: '⚙', glass: '◆', organic: '🌿', hazardous: '⚠', other: '●', }; const icon = icons[litterType]; return icon !== undefined ? icon : '●'; } // Helper function to get litter type label function getLitterTypeLabel(litterType: string): string { const label = litterType.charAt(0).toUpperCase() + litterType.slice(1); return label; }