From d285093900ff6f8e3d5dba394999bb413f5d00f3 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Fri, 14 Nov 2025 18:24:43 +0100 Subject: Enhance stop map functionality with new styles and components for better user experience --- src/frontend/app/components/StopMapSheet.tsx | 218 +++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/frontend/app/components/StopMapSheet.tsx (limited to 'src/frontend/app/components/StopMapSheet.tsx') diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx new file mode 100644 index 0000000..a0d30f4 --- /dev/null +++ b/src/frontend/app/components/StopMapSheet.tsx @@ -0,0 +1,218 @@ +import maplibregl from "maplibre-gl"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import Map, { Marker, NavigationControl, type MapRef } from "react-map-gl/maplibre"; +import { useApp } from "~/AppContext"; +import { getLineColor } from "~/data/LineColors"; +import type { RegionId } from "~/data/RegionConfig"; +import type { Stop } from "~/data/StopDataProvider"; +import { loadStyle } from "~/maps/styleloader"; +import "./StopMapSheet.css"; + +export interface Position { + latitude: number; + longitude: number; + orientationDegrees: number; +} + +export interface ConsolidatedCirculationForMap { + line: string; + route: string; + currentPosition?: Position; +} + +interface StopMapProps { + stop: Stop; + circulations: ConsolidatedCirculationForMap[]; + region: RegionId; +} + +export const StopMap: React.FC = ({ + stop, + circulations, + region, +}) => { + const { theme } = useApp(); + const [styleSpec, setStyleSpec] = useState(null); + const mapRef = useRef(null); + const hasFitBounds = useRef(false); + + useEffect(() => { + let mounted = true; + loadStyle("openfreemap", theme) + .then((style) => { + if (mounted) setStyleSpec(style); + }) + .catch((err) => console.error("Failed to load map style", err)); + return () => { + mounted = false; + }; + }, [theme]); + + const center = useMemo(() => { + if (stop.latitude && stop.longitude) { + return { latitude: stop.latitude, longitude: stop.longitude }; + } + // fallback to first available bus position + const pos = circulations.find((c) => c.currentPosition)?.currentPosition; + return pos + ? { latitude: pos.latitude, longitude: pos.longitude } + : { latitude: 42.2406, longitude: -8.7207 }; // Vigo approx fallback + }, [stop.latitude, stop.longitude, circulations]); + + const busPositions = useMemo( + () => circulations.filter((c) => !!c.currentPosition), + [circulations], + ); + + // Fit bounds to stop + buses, with ~1km padding each side, with a modest animation + // Only fit bounds on the first load, not on subsequent updates + useEffect(() => { + if (!styleSpec || !mapRef.current || hasFitBounds.current) return; + + const points: { lat: number; lon: number }[] = []; + if (stop.latitude && stop.longitude) { + points.push({ lat: stop.latitude, lon: stop.longitude }); + } + for (const c of busPositions) { + if (c.currentPosition) { + points.push({ + lat: c.currentPosition.latitude, + lon: c.currentPosition.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; + } + + // ~1km in degrees + const kmToDegLat = 1.0 / 111.32; // ≈0.008983 + const centerLat = (minLat + maxLat) / 2; + const kmToDegLon = kmToDegLat / Math.max(Math.cos((centerLat * Math.PI) / 180), 0.1); + const padLat = kmToDegLat; + const padLon = kmToDegLon; + + const sw = [minLon - padLon, minLat - padLat] as [number, number]; + const ne = [maxLon + padLon, maxLat + padLat] as [number, number]; + const bounds = new maplibregl.LngLatBounds(sw, ne); + + try { + mapRef.current.fitBounds(bounds, { + padding: 32, + duration: 700, + maxZoom: 17, + } as any); + hasFitBounds.current = true; + } catch {} + }, [styleSpec, stop.latitude, stop.longitude, busPositions]); + + return ( +
+ {styleSpec && ( + + + + {/* Stop marker (center) */} + {stop.latitude && stop.longitude && ( + +
+ + )} + + {/* Bus markers with heading */} + {busPositions.map((c, idx) => { + const p = c.currentPosition!; + const lineColor = getLineColor(region, c.line); + return ( + +
+ {/* Line number above */} +
+ {c.line} +
+ {/* Arrow pointing direction */} + + + +
+
+ ); + })} + + )} +
+ ); +}; -- cgit v1.3