aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/StopMapSheet.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/components/StopMapSheet.tsx')
-rw-r--r--src/frontend/app/components/StopMapSheet.tsx151
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>