From de6f38f26cfb7c311fc9e4fb051191df12b8b042 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sat, 22 Nov 2025 18:23:33 +0100 Subject: feat: Implement previous trip shape handling in VigoController and update related components for improved trip tracking --- src/frontend/app/components/StopMapModal.tsx | 184 ++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 32 deletions(-) (limited to 'src/frontend/app/components/StopMapModal.tsx') diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx index ad73fc2..a5a87fd 100644 --- a/src/frontend/app/components/StopMapModal.tsx +++ b/src/frontend/app/components/StopMapModal.tsx @@ -27,6 +27,8 @@ export interface ConsolidatedCirculationForMap { route: string; currentPosition?: Position; stopShapeIndex?: number; + isPreviousTrip?: boolean; + previousTripShapeId?: string; schedule?: { shapeId?: string; }; @@ -54,6 +56,7 @@ export const StopMapModal: React.FC = ({ const mapRef = useRef(null); const hasFitBounds = useRef(false); const [shapeData, setShapeData] = useState(null); + const [previousShapeData, setPreviousShapeData] = useState(null); const regionConfig = getRegionConfig(region); @@ -94,22 +97,29 @@ export const StopMapModal: React.FC = ({ if (!mapRef.current) return; const points: { lat: number; lon: number }[] = []; - if ( - shapeData?.properties?.busPoint && - shapeData?.properties?.stopPoint && - shapeData?.geometry?.coordinates - ) { - const busIdx = shapeData.properties.busPoint.index; - const stopIdx = shapeData.properties.stopPoint.index; - const coords = shapeData.geometry.coordinates; + const addShapePoints = (data: any) => { + if ( + data?.properties?.busPoint && + data?.properties?.stopPoint && + data?.geometry?.coordinates + ) { + const busIdx = data.properties.busPoint.index; + const stopIdx = data.properties.stopPoint.index; + const coords = data.geometry.coordinates; + + const start = Math.min(busIdx, stopIdx); + const end = Math.max(busIdx, stopIdx); + + for (let i = start; i <= end; i++) { + points.push({ lat: coords[i][1], lon: coords[i][0] }); + } + } + }; - const start = Math.min(busIdx, stopIdx); - const end = Math.max(busIdx, stopIdx); + addShapePoints(shapeData); + addShapePoints(previousShapeData); - for (let i = start; i <= end; i++) { - points.push({ lat: coords[i][1], lon: coords[i][0] }); - } - } else { + if (points.length === 0) { if (stop.latitude && stop.longitude) { points.push({ lat: stop.latitude, lon: stop.longitude }); } @@ -153,7 +163,7 @@ export const StopMapModal: React.FC = ({ } as any); } } catch {} - }, [stop, selectedBus, shapeData]); + }, [stop, selectedBus, shapeData, previousShapeData]); // Load style without traffic layers for the stop map useEffect(() => { @@ -221,6 +231,7 @@ export const StopMapModal: React.FC = ({ if (!isOpen) { hasFitBounds.current = false; setShapeData(null); + setPreviousShapeData(null); } }, [isOpen]); @@ -234,6 +245,7 @@ export const StopMapModal: React.FC = ({ !regionConfig.shapeEndpoint ) { setShapeData(null); + setPreviousShapeData(null); return; } @@ -243,25 +255,81 @@ export const StopMapModal: React.FC = ({ const stopLat = stop.latitude; const stopLon = stop.longitude; - let url = `${regionConfig.shapeEndpoint}?shapeId=${shapeId}&busShapeIndex=${shapeIndex}`; - if (stopShapeIndex !== undefined) { - url += `&stopShapeIndex=${stopShapeIndex}`; - } else { - url += `&stopLat=${stopLat}&stopLon=${stopLon}`; - } + const fetchShape = async ( + sId: string, + bIndex?: number, + sIndex?: number, + sLat?: number, + sLon?: number + ) => { + let url = `${regionConfig.shapeEndpoint}?shapeId=${sId}`; + if (bIndex !== undefined) url += `&busShapeIndex=${bIndex}`; + if (sIndex !== undefined) url += `&stopShapeIndex=${sIndex}`; + else if (sLat && sLon) url += `&stopLat=${sLat}&stopLon=${sLon}`; + + const res = await fetch(url); + if (res.ok) return res.json(); + return null; + }; - fetch(url) - .then((res) => { - if (res.ok) return res.json(); - return null; - }) - .then((data) => { - if (data) { - setShapeData(data); - handleCenter(); + const loadShapes = async () => { + if (selectedBus.isPreviousTrip && selectedBus.previousTripShapeId) { + // Bus is on previous trip + // 1. Load previous shape (where bus is) + const prevData = await fetchShape( + selectedBus.previousTripShapeId, + shapeIndex, + stopShapeIndex + ); + + // 2. Load current scheduled shape (where bus is going) + // Bus is not on this shape yet, so no bus index + const currData = await fetchShape( + shapeId, + undefined, + undefined, + stopLat, + stopLon + ); + + if ( + prevData && + prevData.geometry && + prevData.geometry.coordinates && + prevData.properties?.busPoint?.index !== undefined + ) { + const busIdx = prevData.properties.busPoint.index; + const coords = prevData.geometry.coordinates; + // Slice from busIdx - 5 (clamped to 0) to end + const startIdx = Math.max(0, busIdx - 5); + const slicedCoords = coords.slice(startIdx); + + // Join with the first point of the next shape to close the gap + if (currData?.geometry?.coordinates?.length > 0) { + slicedCoords.push(currData.geometry.coordinates[0]); + } + + prevData.geometry.coordinates = slicedCoords; } - }) - .catch((err) => console.error("Failed to load shape", err)); + + setPreviousShapeData(prevData); + setShapeData(currData); + } else { + // Normal case + const data = await fetchShape( + shapeId, + shapeIndex, + stopShapeIndex, + stopLat, + stopLon + ); + setShapeData(data); + setPreviousShapeData(null); + } + handleCenter(); + }; + + loadShapes().catch((err) => console.error("Failed to load shape", err)); }, [isOpen, selectedBus, regionConfig.shapeEndpoint]); if (busesWithPosition.length === 0) { @@ -294,6 +362,58 @@ export const StopMapModal: React.FC = ({ ref={mapRef} interactive={true} > + {/* Previous Shape Layer */} + {previousShapeData && selectedBus && ( + + {/* 1. Black border */} + + {/* 2. White background */} + + {/* 3. Colored dashes */} + + + )} + {/* Shape Layer */} {shapeData && selectedBus && ( -- cgit v1.3