diff options
| author | Copilot <198982749+Copilot@users.noreply.github.com> | 2025-11-17 23:39:08 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-17 23:39:08 +0100 |
| commit | 276e73412abef28c222c52a84334d49f5e414f3c (patch) | |
| tree | 8b7ae07eafa53f9efc5884e4f0696e6077266f48 /src/frontend/app/components/StopMapSheet.tsx | |
| parent | 36d982fb3b01fd8181b216b57fba2c42e9404d1f (diff) | |
Use consolidated data API in map sheet with shared card component (#100)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Co-authored-by: Ariel Costas Guerrero <ariel@costas.dev>
Diffstat (limited to 'src/frontend/app/components/StopMapSheet.tsx')
| -rw-r--r-- | src/frontend/app/components/StopMapSheet.tsx | 151 |
1 files changed, 122 insertions, 29 deletions
diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx index e87e8c8..b3a1666 100644 --- a/src/frontend/app/components/StopMapSheet.tsx +++ b/src/frontend/app/components/StopMapSheet.tsx @@ -1,6 +1,10 @@ import maplibregl from "maplibre-gl"; import React, { useEffect, useMemo, useRef, useState } from "react"; -import Map, { AttributionControl, Marker, type MapRef } from "react-map-gl/maplibre"; +import Map, { + AttributionControl, + Marker, + type MapRef, +} from "react-map-gl/maplibre"; import { useApp } from "~/AppContext"; import { getLineColor } from "~/data/LineColors"; import type { RegionId } from "~/data/RegionConfig"; @@ -53,24 +57,38 @@ export const StopMap: React.FC<StopMapProps> = ({ const lat2 = (b.lat * Math.PI) / 180; const sinDLat = Math.sin(dLat / 2); const sinDLon = Math.sin(dLon / 2); - const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon; + const h = + sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon; return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))); }; const computeFocusPoints = (): Pt[] => { const buses: Pt[] = []; for (const c of busPositions) { - if (c.currentPosition) buses.push({ lat: c.currentPosition.latitude, lon: c.currentPosition.longitude }); + if (c.currentPosition) + buses.push({ + lat: c.currentPosition.latitude, + lon: c.currentPosition.longitude, + }); } - const stopPt = stop.latitude && stop.longitude ? { lat: stop.latitude, lon: stop.longitude } : null; - const userPt = userPosition ? { lat: userPosition.latitude, lon: userPosition.longitude } : null; + const stopPt = + stop.latitude && stop.longitude + ? { lat: stop.latitude, lon: stop.longitude } + : null; + const userPt = userPosition + ? { lat: userPosition.latitude, lon: userPosition.longitude } + : null; if (buses.length === 0 && !stopPt && !userPt) return []; // Choose anchor for proximity: stop > user > average of buses let anchor: Pt | null = stopPt || userPt || null; if (!anchor && buses.length) { - let lat = 0, lon = 0; - for (const b of buses) { lat += b.lat; lon += b.lon; } + let lat = 0, + lon = 0; + for (const b of buses) { + lat += b.lat; + lon += b.lon; + } anchor = { lat: lat / buses.length, lon: lon / buses.length }; } @@ -122,7 +140,7 @@ export const StopMap: React.FC<StopMapProps> = ({ }); }, () => {}, - { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 }, + { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 } ); geoWatchId.current = navigator.geolocation.watchPosition( (pos) => { @@ -133,7 +151,7 @@ export const StopMap: React.FC<StopMapProps> = ({ }); }, () => {}, - { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 }, + { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 } ); } catch {} return () => { @@ -158,7 +176,7 @@ export const StopMap: React.FC<StopMapProps> = ({ const busPositions = useMemo( () => circulations.filter((c) => !!c.currentPosition), - [circulations], + [circulations] ); // Fit bounds to stop + buses, with ~1km padding each side, with a modest animation @@ -185,13 +203,17 @@ export const StopMap: React.FC<StopMapProps> = ({ const bounds = new maplibregl.LngLatBounds(sw, ne); // Determine predominant bus quadrant relative to stop to bias padding. - const padding: number | { top: number; right: number; bottom: number; left: number } = 24; + const padding: + | number + | { top: number; right: number; bottom: number; left: number } = 24; // If the diagonal is huge (likely outliers sneaked in), clamp via zoom fallback try { if (points.length === 1) { const only = points[0]; - mapRef.current.getMap().jumpTo({ center: [only.lon, only.lat], zoom: 16 }); + mapRef.current + .getMap() + .jumpTo({ center: [only.lon, only.lat], zoom: 16 }); } else { mapRef.current.fitBounds(bounds, { padding: padding as any, @@ -208,7 +230,10 @@ export const StopMap: React.FC<StopMapProps> = ({ const pts = computeFocusPoints(); if (pts.length === 0) return; - let minLat = pts[0].lat, maxLat = pts[0].lat, minLon = pts[0].lon, maxLon = pts[0].lon; + let minLat = pts[0].lat, + maxLat = pts[0].lat, + minLon = pts[0].lon, + maxLon = pts[0].lon; for (const p of pts) { if (p.lat < minLat) minLat = p.lat; if (p.lat > maxLat) maxLat = p.lat; @@ -220,14 +245,22 @@ export const StopMap: React.FC<StopMapProps> = ({ const ne = [maxLon, maxLat] as [number, number]; const bounds = new maplibregl.LngLatBounds(sw, ne); - const padding: number | { top: number; right: number; bottom: number; left: number } = 24; + const padding: + | number + | { top: number; right: number; bottom: number; left: number } = 24; try { if (pts.length === 1) { const only = pts[0]; - mapRef.current.getMap().easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 }); + mapRef.current + .getMap() + .easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 }); } else { - mapRef.current.fitBounds(bounds, { padding: padding as any, duration: 500, maxZoom: 17 } as any); + mapRef.current.fitBounds(bounds, { + padding: padding as any, + duration: 500, + maxZoom: 17, + } as any); } } catch {} }; @@ -256,15 +289,36 @@ export const StopMap: React.FC<StopMapProps> = ({ {/* Stop marker (center) */} {stop.latitude && stop.longitude && ( - <Marker longitude={stop.longitude} latitude={stop.latitude} anchor="bottom"> + <Marker + longitude={stop.longitude} + latitude={stop.latitude} + anchor="bottom" + > <div title={`Stop ${stop.stopId}`}> <svg width="28" height="36" viewBox="0 0 28 36"> <defs> - <filter id="drop" x="-20%" y="-20%" width="140%" height="140%"> - <feDropShadow dx="0" dy="1" stdDeviation="1" flood-opacity="0.35" /> + <filter + id="drop" + x="-20%" + y="-20%" + width="140%" + height="140%" + > + <feDropShadow + dx="0" + dy="1" + stdDeviation="1" + floodOpacity={0.35} + /> </filter> </defs> - <path d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z" fill="#1976d2" stroke="#fff" strokeWidth="2" filter="url(#drop)" /> + <path + d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z" + fill="#1976d2" + stroke="#fff" + strokeWidth="2" + filter="url(#drop)" + /> <circle cx="14" cy="13" r="5" fill="#fff" /> <circle cx="14" cy="13" r="3" fill="#1976d2" /> </svg> @@ -274,7 +328,11 @@ export const StopMap: React.FC<StopMapProps> = ({ {/* User position marker (if available) */} {userPosition && ( - <Marker longitude={userPosition.longitude} latitude={userPosition.latitude} anchor="center"> + <Marker + longitude={userPosition.longitude} + latitude={userPosition.latitude} + anchor="center" + > <div className="user-dot" title="Your location"> <div className="user-dot__pulse" /> <div className="user-dot__core" /> @@ -290,7 +348,12 @@ export const StopMap: React.FC<StopMapProps> = ({ const gaps: number[] = new Array(busPositions.length).fill(baseGap); if (map && zoom >= 14.5 && busPositions.length > 1) { const pts = busPositions.map((c) => - c.currentPosition ? map.project([c.currentPosition.longitude, c.currentPosition.latitude]) : null, + c.currentPosition + ? map.project([ + c.currentPosition.longitude, + c.currentPosition.latitude, + ]) + : null ); for (let i = 0; i < pts.length; i++) { const pi = pts[i]; @@ -314,7 +377,12 @@ export const StopMap: React.FC<StopMapProps> = ({ const showLabel = zoom >= 13; const labelGap = gaps[idx] ?? baseGap; return ( - <Marker key={idx} longitude={p.longitude} latitude={p.latitude} anchor="center"> + <Marker + key={idx} + longitude={p.longitude} + latitude={p.latitude} + anchor="center" + > <div style={{ display: "flex", @@ -329,9 +397,16 @@ export const StopMap: React.FC<StopMapProps> = ({ width="20" height="20" viewBox="0 0 24 24" - style={{ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))" }} + style={{ + filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))", + }} > - <path d="M12 2 L20 22 L12 18 L4 22 Z" fill={lineColor.background} stroke="#fff" strokeWidth="1.5" /> + <path + d="M12 2 L20 22 L12 18 L4 22 Z" + fill={lineColor.background} + stroke="#fff" + strokeWidth="1.5" + /> </svg> {showLabel && ( <div @@ -362,11 +437,29 @@ export const StopMap: React.FC<StopMapProps> = ({ )} {/* Floating controls */} <div className="map-floating-controls"> - <button type="button" aria-label="Center" className="center-btn" onClick={handleCenter} title="Center view"> + <button + type="button" + aria-label="Center" + className="center-btn" + onClick={handleCenter} + title="Center view" + > <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"> - <circle cx="12" cy="12" r="3" fill="currentColor"/> - <path d="M12 2v3M12 19v3M2 12h3M19 12h3" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> - <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="1.5"/> + <circle cx="12" cy="12" r="3" fill="currentColor" /> + <path + d="M12 2v3M12 19v3M2 12h3M19 12h3" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + /> + <circle + cx="12" + cy="12" + r="8" + fill="none" + stroke="currentColor" + strokeWidth="1.5" + /> </svg> </button> </div> |
