import { AlertTriangle, Construction, Crosshair, MapPin, Navigation } from 'lucide-react'; import 'maplibre-gl/dist/maplibre-gl.css'; import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useState } from 'react'; import Map, { FullscreenControl, GeolocateControl, Layer, Marker, NavigationControl, Popup, Source } from 'react-map-gl/maplibre'; import FloatingSearchBar from './FloatingSearchBar'; import { MapLayersMenu } from './MapLayersMenu'; interface MapViewProps { routeData: any; alternativeRoutes?: any[]; riskData: any; previousRouteData?: any; previousRiskData?: any; incidentsData: any; startLoc: [number, number] | null; endLoc: [number, number] | null; setStartLoc: (loc: [number, number] | null) => void; setEndLoc: (loc: [number, number] | null) => void; setStartQuery: (query: string) => void; setEndQuery: (query: string) => void; theme: 'dark' | 'light'; showIncidents: boolean; showRoutePlayback?: boolean; playbackSpeed?: number; mapStyleType?: string; setMapStyleType?: (type: string) => void; activeLayers?: string[]; setActiveLayers?: (layers: string[]) => void; } export default function MapView({ routeData, alternativeRoutes, riskData, previousRouteData, previousRiskData, incidentsData, startLoc, endLoc, setStartLoc, setEndLoc, setStartQuery, setEndQuery, theme, showIncidents, showRoutePlayback, playbackSpeed = 1, mapStyleType = 'default', setMapStyleType, activeLayers = [], setActiveLayers }: MapViewProps) { const [viewState, setViewState] = useState({ longitude: 77.4126, // Default to India center roughly latitude: 23.2599, zoom: 5 }); const [progress, setProgress] = useState(1); const [contextMenu, setContextMenu] = useState<{lng: number, lat: number, x: number, y: number} | null>(null); const [hoveredIncident, setHoveredIncident] = useState(null); const [hoveredSegment, setHoveredSegment] = useState(null); const [poiResults, setPoiResults] = useState([]); // Map styles based on theme and mapStyleType using completely free providers const getMapStyle = () => { if (mapStyleType === 'satellite') { return { version: 8, sources: { 'esri-satellite': { type: 'raster', tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], tileSize: 256, attribution: '© Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' } }, layers: [ { id: 'satellite', type: 'raster', source: 'esri-satellite', minzoom: 0, maxzoom: 22 } ] }; } if (mapStyleType === 'navigation') { return "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"; } // Default / Fallback to Carto return theme === 'dark' ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" : "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"; }; useEffect(() => { if (startLoc) { setViewState(prev => ({ ...prev, longitude: startLoc[0], latitude: startLoc[1], zoom: 12 })); } }, [startLoc]); useEffect(() => { if (!riskData || riskData.length === 0) { setProgress(1); return; } setProgress(0); let start: number; let animationFrameId: number; // Base duration is 2 seconds for initial load, 10 seconds for playback const baseDuration = showRoutePlayback ? 10000 : 2000; const duration = baseDuration / playbackSpeed; const animate = (timestamp: number) => { if (!start) start = timestamp; const elapsed = timestamp - start; const currentProgress = Math.min(elapsed / duration, 1); setProgress(currentProgress); if (currentProgress < 1) { animationFrameId = requestAnimationFrame(animate); } else if (showRoutePlayback) { // Loop playback start = timestamp; animationFrameId = requestAnimationFrame(animate); } }; animationFrameId = requestAnimationFrame(animate); return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } }; }, [riskData, showRoutePlayback, playbackSpeed]); const geojson = useMemo(() => { if (!riskData || riskData.length === 0) return null; const visibleCount = Math.max(1, Math.floor(riskData.length * progress)); const visibleSegments = riskData.slice(0, visibleCount); return { type: 'FeatureCollection', features: visibleSegments.map((segment: any) => ({ type: 'Feature', properties: { risk: segment.risk_probability || segment.risk_score || 0, traffic: segment.traffic_level || 0, explanation: segment.explanation || 'No specific risk factors' }, geometry: { type: 'LineString', coordinates: [segment.start, segment.end] } })) }; }, [riskData, progress]); const previousGeojson = useMemo(() => { if (!previousRouteData || !previousRouteData.geometry) return null; return { type: 'Feature', properties: {}, geometry: previousRouteData.geometry }; }, [previousRouteData]); const alternativeGeojson = useMemo(() => { if (!alternativeRoutes || alternativeRoutes.length === 0) return null; const validRoutes = alternativeRoutes.filter((r: any) => r && r.geometry); if (validRoutes.length === 0) return null; return { type: 'FeatureCollection', features: validRoutes.map((route: any) => ({ type: 'Feature', properties: {}, geometry: route.geometry })) }; }, [alternativeRoutes]); const [measurePoints, setMeasurePoints] = useState<[number, number][]>([]); // Calculate distance for measure tool const measureDistance = useMemo(() => { if (measurePoints.length < 2) return 0; let dist = 0; for (let i = 1; i < measurePoints.length; i++) { const p1 = measurePoints[i - 1]; const p2 = measurePoints[i]; // Haversine formula const R = 6371e3; // metres const φ1 = p1[1] * Math.PI/180; // φ, λ in radians const φ2 = p2[1] * Math.PI/180; const Δφ = (p2[1]-p1[1]) * Math.PI/180; const Δλ = (p2[0]-p1[0]) * Math.PI/180; const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); dist += R * c; } return dist; }, [measurePoints]); const [bearing, setBearing] = useState(0); return (
e.preventDefault()}>
{ if (setActiveLayers) setActiveLayers(layers); if (!layers.includes('measure')) setMeasurePoints([]); }} />
{ setViewState(evt.viewState); setBearing(evt.viewState.bearing || 0); }} mapStyle={getMapStyle() as any} interactiveLayerIds={geojson ? ['route-layer'] : []} onMouseMove={(e) => { if (e.features && e.features.length > 0) { const feature = e.features[0]; if (feature.layer.id === 'route-layer') { setHoveredSegment({ lng: e.lngLat.lng, lat: e.lngLat.lat, properties: feature.properties }); } } else { setHoveredSegment(null); } }} onMouseLeave={() => setHoveredSegment(null)} onContextMenu={(e) => { e.originalEvent.preventDefault(); setContextMenu({ lng: e.lngLat.lng, lat: e.lngLat.lat, x: e.point.x, y: e.point.y }); }} onClick={(e) => { if (activeLayers.includes('measure')) { setMeasurePoints([...measurePoints, [e.lngLat.lng, e.lngLat.lat]]); return; } setContextMenu(null); setPoiResults([]); }} > {startLoc && ( )} {endLoc && ( )} {Array.isArray(poiResults) && poiResults.map((poi: any, idx: number) => ( { e.originalEvent.stopPropagation(); if (setEndLoc && setEndQuery) { setEndLoc([parseFloat(poi.lon || poi.center?.[0]), parseFloat(poi.lat || poi.center?.[1])]); setEndQuery(poi.display_name || poi.place_name); setPoiResults([]); } }} >
{poi.name || (poi.display_name || poi.place_name || '').split(',')[0]}
))}
{showIncidents && incidentsData && incidentsData.map((incident: any, idx: number) => ( setHoveredIncident(incident)} onMouseLeave={() => setHoveredIncident(null)} className={`p-2 rounded-full shadow-lg cursor-pointer transition-transform ${ incident.type.toLowerCase() === 'accident' ? (incident.severity.toLowerCase() === 'high' ? 'bg-rose-600 text-white ring-4 ring-rose-600/30 scale-125 z-10' : 'bg-rose-400 text-white') : 'bg-amber-500 text-white' }`} > {incident.type.toLowerCase() === 'accident' ? : } ))} {hoveredIncident && (
{hoveredIncident.type.toLowerCase() === 'accident' ? : } {hoveredIncident.type.toUpperCase()}

Severity: {hoveredIncident.severity}

)} {hoveredSegment && (
Route Segment

Risk Score: 0.7 ? 'text-rose-500' : (hoveredSegment.properties?.risk || 0) > 0.4 ? 'text-amber-500' : 'text-emerald-500'}`}>{((hoveredSegment.properties?.risk || 0) * 100).toFixed(0)}%

Traffic: {((hoveredSegment.properties?.traffic || 0) * 100).toFixed(0)}%

{hoveredSegment.properties?.explanation || 'No specific risk factors'}

)} {activeLayers.includes('transit') && ( )} {activeLayers.includes('biking') && ( )} {activeLayers.includes('terrain') && ( )} {activeLayers.includes('traffic') && ( )} {activeLayers.includes('streetview') && ( )} {activeLayers.includes('wildfires') && ( )} {activeLayers.includes('airquality') && ( )} {activeLayers.includes('traveltime') && startLoc && ( { const angle = (i * 10 * Math.PI) / 180; const radius = 0.05; // approx 5km in degrees return [ startLoc[0] + radius * Math.cos(angle), startLoc[1] + radius * Math.sin(angle) ]; }).concat([[startLoc[0] + 0.05, startLoc[1]]]) ] } }}> )} {activeLayers.includes('measure') && measurePoints.length > 0 && ( <> {measurePoints.map((pt, i) => (
))} {measurePoints.length > 1 && (
{measureDistance > 1000 ? `${(measureDistance / 1000).toFixed(2)} km` : `${Math.round(measureDistance)} m`}
)} )} {previousGeojson && ( )} {alternativeGeojson && ( )} {geojson && ( = 0.7) Rose 1.0, '#9f1239' // Severe risk Dark Rose ] }} /> )} {contextMenu && (
)}
); }