From cfbb1625e7873264e2ef435cc76fec2b59cf58d8 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Wed, 24 Dec 2025 19:33:49 +0100 Subject: Refactor map components and improve modal functionality --- src/frontend/app/components/StopMapModal.tsx | 659 --------------------------- 1 file changed, 659 deletions(-) delete mode 100644 src/frontend/app/components/StopMapModal.tsx (limited to 'src/frontend/app/components/StopMapModal.tsx') diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx deleted file mode 100644 index d218af4..0000000 --- a/src/frontend/app/components/StopMapModal.tsx +++ /dev/null @@ -1,659 +0,0 @@ -import maplibregl from "maplibre-gl"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre"; -import { Sheet } from "react-modal-sheet"; -import { useApp } from "~/AppContext"; -import { APP_CONSTANTS } from "~/config/constants"; -import { getLineColour } from "~/data/LineColors"; -import type { Stop } from "~/data/StopDataProvider"; -import { loadStyle } from "~/maps/styleloader"; -import "./StopMapModal.css"; - -export interface Position { - latitude: number; - longitude: number; - orientationDegrees: number; - shapeIndex?: number; -} - -export interface ConsolidatedCirculationForMap { - id: string; - line: string; - route: string; - currentPosition?: Position; - stopShapeIndex?: number; - isPreviousTrip?: boolean; - previousTripShapeId?: string | null; - schedule?: { - shapeId?: string | null; - }; - shape?: any; -} - -interface StopMapModalProps { - stop: Stop; - circulations: ConsolidatedCirculationForMap[]; - isOpen: boolean; - onClose: () => void; - selectedCirculationId?: string; -} - -export const StopMapModal: React.FC = ({ - stop, - circulations, - isOpen, - onClose, - selectedCirculationId, -}) => { - const { theme } = useApp(); - const [styleSpec, setStyleSpec] = useState(null); - const mapRef = useRef(null); - const hasFitBounds = useRef(false); - const userInteracted = useRef(false); - const [shapeData, setShapeData] = useState(null); - const [previousShapeData, setPreviousShapeData] = useState(null); - - // Filter circulations that have GPS coordinates - const busesWithPosition = useMemo( - () => circulations.filter((c) => !!c.currentPosition), - [circulations] - ); - - // Use selectedCirculationId if provided, otherwise use first bus with position - const selectedBus = useMemo(() => { - if (selectedCirculationId !== undefined) { - const circulation = circulations.find( - (c) => c.id === selectedCirculationId - ); - if (circulation) { - return circulation; - } - } - // Fallback to first bus with position - return busesWithPosition.length > 0 ? busesWithPosition[0] : null; - }, [selectedCirculationId, circulations, busesWithPosition]); - - const center = useMemo(() => { - if (selectedBus?.currentPosition) { - return { - latitude: selectedBus.currentPosition.latitude, - longitude: selectedBus.currentPosition.longitude, - }; - } - if (stop.latitude && stop.longitude) { - return { latitude: stop.latitude, longitude: stop.longitude }; - } - return { latitude: 42.2406, longitude: -8.7207 }; // Vigo approx fallback - }, [selectedBus, stop.latitude, stop.longitude]); - - const handleCenter = useCallback(() => { - if (!mapRef.current) return; - if (userInteracted.current) return; - - const points: { lat: number; lon: number }[] = []; - - const getStopsFromFeatureCollection = (data: any) => { - if (!data || data.type !== "FeatureCollection" || !data.features) - return []; - return data.features.filter((f: any) => f.properties?.type === "stop"); - }; - - const findClosestStopIndex = ( - stops: any[], - pos: { lat: number; lon: number } - ) => { - let minDst = Infinity; - let index = -1; - stops.forEach((s: any, idx: number) => { - const [lon, lat] = s.geometry.coordinates; - const dst = Math.pow(lat - pos.lat, 2) + Math.pow(lon - pos.lon, 2); - if (dst < minDst) { - minDst = dst; - index = idx; - } - }); - return index; - }; - - const findClosestPointIndex = ( - coords: number[][], - pos: { lat: number; lon: number } - ) => { - let minDst = Infinity; - let index = -1; - coords.forEach((c, idx) => { - const [lon, lat] = c; - const dst = Math.pow(lat - pos.lat, 2) + Math.pow(lon - pos.lon, 2); - if (dst < minDst) { - minDst = dst; - index = idx; - } - }); - return index; - }; - - const addShapePoints = (data: any, isPrevious: boolean) => { - if (!data) return; - - if (data.type === "FeatureCollection") { - const stops = getStopsFromFeatureCollection(data); - if (stops.length === 0) return; - - let startIdx = 0; - let endIdx = stops.length - 1; - - const currentPos = selectedBus?.currentPosition; - const userStopPos = - stop.latitude && stop.longitude - ? { lat: stop.latitude, lon: stop.longitude } - : null; - - if (isPrevious) { - // Previous trip: Start from Bus, End at last stop - if (currentPos) { - const busIdx = findClosestStopIndex(stops, { - lat: currentPos.latitude, - lon: currentPos.longitude, - }); - if (busIdx !== -1) startIdx = busIdx; - } - } else { - // Current trip: Start from Bus (if not previous), End at User Stop - if (!previousShapeData && currentPos) { - const busIdx = findClosestStopIndex(stops, { - lat: currentPos.latitude, - lon: currentPos.longitude, - }); - if (busIdx !== -1) startIdx = busIdx; - } - - if (userStopPos) { - let userIdx = -1; - // Try name match - if (stop.name) { - userIdx = stops.findIndex( - (s: any) => s.properties?.name === stop.name - ); - } - // Fallback to coords - if (userIdx === -1) { - userIdx = findClosestStopIndex(stops, userStopPos); - } - if (userIdx !== -1) endIdx = userIdx; - } - } - - // Add stops in range - if (startIdx <= endIdx) { - for (let i = startIdx; i <= endIdx; i++) { - const [lon, lat] = stops[i].geometry.coordinates; - points.push({ lat, lon }); - } - } - return; - } - - const coords = data?.geometry?.coordinates; - if (!coords) return; - - let startIdx = 0; - let endIdx = coords.length - 1; - let foundIndices = false; - - if (data.properties?.busPoint && data.properties?.stopPoint) { - startIdx = data.properties.busPoint.index; - endIdx = data.properties.stopPoint.index; - foundIndices = true; - } else { - // Fallback: find closest points on the line - if (selectedBus?.currentPosition) { - const busIdx = findClosestPointIndex(coords, { - lat: selectedBus.currentPosition.latitude, - lon: selectedBus.currentPosition.longitude, - }); - if (busIdx !== -1) startIdx = busIdx; - } - if (stop.latitude && stop.longitude) { - const stopIdx = findClosestPointIndex(coords, { - lat: stop.latitude, - lon: stop.longitude, - }); - if (stopIdx !== -1) endIdx = stopIdx; - } - } - - const start = Math.min(startIdx, endIdx); - const end = Math.max(startIdx, endIdx); - - for (let i = start; i <= end; i++) { - points.push({ lat: coords[i][1], lon: coords[i][0] }); - } - }; - - addShapePoints(previousShapeData, true); - addShapePoints(shapeData, false); - - if (points.length === 0) { - if (stop.latitude && stop.longitude) { - points.push({ lat: stop.latitude, lon: stop.longitude }); - } - - if (selectedBus?.currentPosition) { - points.push({ - lat: selectedBus.currentPosition.latitude, - lon: selectedBus.currentPosition.longitude, - }); - } - } else { - // Ensure bus and stop are always included if available, to prevent cutting them off - if (selectedBus?.currentPosition) { - points.push({ - lat: selectedBus.currentPosition.latitude, - lon: selectedBus.currentPosition.longitude, - }); - } - if (stop.latitude && stop.longitude) { - points.push({ lat: stop.latitude, lon: stop.longitude }); - } - } - - if (points.length === 0) return; - - let minLat = points[0].lat, - maxLat = points[0].lat, - minLon = points[0].lon, - maxLon = points[0].lon; - for (const p of points) { - if (p.lat < minLat) minLat = p.lat; - if (p.lat > maxLat) maxLat = p.lat; - if (p.lon < minLon) minLon = p.lon; - if (p.lon > maxLon) maxLon = p.lon; - } - - const sw = [minLon, minLat] as [number, number]; - const ne = [maxLon, maxLat] as [number, number]; - const bounds = new maplibregl.LngLatBounds(sw, ne); - - try { - if (points.length === 1) { - const only = points[0]; - mapRef.current - .getMap() - .easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 }); - } else { - mapRef.current.getMap().fitBounds(bounds, { - padding: 80, - duration: 500, - maxZoom: 17, - } as any); - } - } catch {} - }, [stop, selectedBus, shapeData, previousShapeData]); - - // Load style without traffic layers for the stop map - useEffect(() => { - let mounted = true; - loadStyle("openfreemap", theme, { includeTraffic: false }) - .then((style) => { - if (mounted) setStyleSpec(style); - }) - .catch((err) => console.error("Failed to load map style", err)); - return () => { - mounted = false; - }; - }, [theme]); - - // Resize map and fit bounds when modal opens - useEffect(() => { - if (isOpen && mapRef.current) { - const timer = setTimeout(() => { - const map = mapRef.current?.getMap(); - if (map) { - map.resize(); - // Trigger fit bounds logic again - hasFitBounds.current = false; - handleCenter(); - } - }, 300); // Wait for sheet animation - return () => clearTimeout(timer); - } - }, [isOpen, handleCenter]); - - // Fit bounds on initial load - useEffect(() => { - if (!styleSpec || !mapRef.current || !isOpen) - return; - - const map = mapRef.current.getMap(); - - // Handle missing sprite images to suppress console warnings - const handleStyleImageMissing = (e: any) => { - if (!map || map.hasImage(e.id)) return; - map.addImage(e.id, { - width: 1, - height: 1, - data: new Uint8Array(4), - }); - }; - - map.on("styleimagemissing", handleStyleImageMissing); - - handleCenter(); - hasFitBounds.current = true; - - return () => { - if (mapRef.current) { - const map = mapRef.current.getMap(); - if (map) { - map.off("styleimagemissing", handleStyleImageMissing); - } - } - }; - }, [styleSpec, stop, selectedBus, isOpen, handleCenter]); - - // Reset bounds when modal opens/closes - useEffect(() => { - if (!isOpen) { - hasFitBounds.current = false; - userInteracted.current = false; - setShapeData(null); - setPreviousShapeData(null); - } - }, [isOpen]); - - // Fetch shape for selected bus - useEffect(() => { - if (!isOpen || !selectedBus) { - setShapeData(null); - setPreviousShapeData(null); - return; - } - - if (selectedBus.shape) { - setShapeData(selectedBus.shape); - setPreviousShapeData(null); - handleCenter(); - return; - } - - setShapeData(null); - setPreviousShapeData(null); - }, [isOpen, selectedBus]); - - if (!selectedBus && busesWithPosition.length === 0) { - return null; // Don't render if no buses with GPS coordinates and no selected bus - } - - return ( - - - - -
- {/* Map Container */} -
- {styleSpec && ( - { - if (e.originalEvent) { - userInteracted.current = true; - } - }} - onDragStart={() => { - userInteracted.current = true; - }} - onZoomStart={() => { - userInteracted.current = true; - }} - onRotateStart={() => { - userInteracted.current = true; - }} - onPitchStart={() => { - userInteracted.current = true; - }} - onLoad={() => { - handleCenter(); - }} - > - {/* Previous Shape Layer */} - {previousShapeData && selectedBus && ( - - {/* 1. Black border */} - - {/* 2. White background */} - - {/* 3. Colored dashes */} - - - )} - - {/* Shape Layer */} - {shapeData && selectedBus && ( - - - - - {/* Stops Layer */} - - - )} - - {/* Stop marker */} - {stop.latitude && stop.longitude && ( - -
- - - - - - - - - - -
-
- )} - - {/* Selected bus marker */} - {selectedBus?.currentPosition && ( - -
- - - -
-
- )} -
- )} - - {/* Floating controls */} -
- -
-
-
-
-
- -
- ); -}; -- cgit v1.3