aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/app/components/StopMapModal.tsx89
-rw-r--r--src/frontend/app/components/StopMapSheet.css2
-rw-r--r--src/frontend/app/components/StopMapSheet.tsx76
-rw-r--r--src/frontend/app/config/RegionConfig.ts2
-rw-r--r--src/frontend/app/routes/stops-$id.tsx7
5 files changed, 166 insertions, 10 deletions
diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx
index 1799f74..67435b4 100644
--- a/src/frontend/app/components/StopMapModal.tsx
+++ b/src/frontend/app/components/StopMapModal.tsx
@@ -1,12 +1,14 @@
import maplibregl from "maplibre-gl";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Map, {
- Marker,
- type MapRef
+ Layer,
+ Marker,
+ Source,
+ type MapRef
} from "react-map-gl/maplibre";
import { Sheet } from "react-modal-sheet";
import { useApp } from "~/AppContext";
-import type { RegionId } from "~/config/RegionConfig";
+import { getRegionConfig, type RegionId } from "~/config/RegionConfig";
import { getLineColor } from "~/data/LineColors";
import type { Stop } from "~/data/StopDataProvider";
import { loadStyle } from "~/maps/styleloader";
@@ -16,12 +18,16 @@ export interface Position {
latitude: number;
longitude: number;
orientationDegrees: number;
+ shapeIndex?: number;
}
export interface ConsolidatedCirculationForMap {
line: string;
route: string;
currentPosition?: Position;
+ schedule?: {
+ shapeId?: string;
+ };
}
interface StopMapModalProps {
@@ -45,6 +51,9 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
const [styleSpec, setStyleSpec] = useState<any | null>(null);
const mapRef = useRef<MapRef | null>(null);
const hasFitBounds = useRef(false);
+ const [shapeData, setShapeData] = useState<any | null>(null);
+
+ const regionConfig = getRegionConfig(region);
// Filter circulations that have GPS coordinates
const busesWithPosition = useMemo(
@@ -117,7 +126,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
.easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 });
} else {
mapRef.current.fitBounds(bounds, {
- padding: 24,
+ padding: 80,
duration: 500,
maxZoom: 17,
} as any);
@@ -190,9 +199,42 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
useEffect(() => {
if (!isOpen) {
hasFitBounds.current = false;
+ setShapeData(null);
}
}, [isOpen]);
+ // Fetch shape for selected bus
+ useEffect(() => {
+ if (
+ !isOpen ||
+ !selectedBus ||
+ !selectedBus.schedule?.shapeId ||
+ selectedBus.currentPosition?.shapeIndex === undefined ||
+ !regionConfig.shapeEndpoint
+ ) {
+ setShapeData(null);
+ return;
+ }
+
+ const shapeId = selectedBus.schedule.shapeId;
+ const shapeIndex = selectedBus.currentPosition.shapeIndex;
+
+ fetch(
+ `${regionConfig.shapeEndpoint}?shapeId=${shapeId}&startPointIndex=${shapeIndex}`
+ )
+ .then((res) => {
+ if (res.ok) return res.json();
+ return null;
+ })
+ .then((data) => {
+ if (data) {
+ setShapeData(data);
+ handleCenter();
+ }
+ })
+ .catch((err) => console.error("Failed to load shape", err));
+ }, [isOpen, selectedBus, regionConfig.shapeEndpoint]);
+
if (busesWithPosition.length === 0) {
return null; // Don't render if no buses with GPS coordinates
}
@@ -217,12 +259,45 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
longitude: center.longitude,
zoom: 16,
}}
- style={{ width: "100%", height: "320px" }}
+ style={{ width: "100%", height: "50vh" }}
mapStyle={styleSpec}
- attributionControl={false}
+ attributionControl={{compact: false, customAttribution: "Concello de Vigo & Viguesa de Transportes SL"}}
ref={mapRef}
interactive={true}
>
+ {/* Shape Layer */}
+ {shapeData && selectedBus && (
+ <Source id="route-shape" type="geojson" data={shapeData}>
+ <Layer
+ id="route-shape-border"
+ type="line"
+ paint={{
+ "line-color": "#000000",
+ "line-width": 5,
+ "line-opacity": 0.6,
+ }}
+ layout={{
+ "line-cap": "round",
+ "line-join": "round",
+ }}
+ />
+ <Layer
+ id="route-shape-inner"
+ type="line"
+ paint={{
+ "line-color": getLineColor(region, selectedBus.line)
+ .background,
+ "line-width": 3,
+ "line-opacity": 0.7,
+ }}
+ layout={{
+ "line-cap": "round",
+ "line-join": "round",
+ }}
+ />
+ </Source>
+ )}
+
{/* Stop marker */}
{stop.latitude && stop.longitude && (
<Marker
@@ -290,7 +365,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
<path
d="M12 2 L22 22 L12 17 L2 22 Z"
fill={getLineColor(region, selectedBus.line).background}
- stroke="#fff"
+ stroke="#000"
strokeWidth="2"
strokeLinejoin="round"
/>
diff --git a/src/frontend/app/components/StopMapSheet.css b/src/frontend/app/components/StopMapSheet.css
index 6b2e8ed..7c96c2b 100644
--- a/src/frontend/app/components/StopMapSheet.css
+++ b/src/frontend/app/components/StopMapSheet.css
@@ -1,7 +1,7 @@
/* Stop map container */
.stop-map-container {
width: 100%;
- height: 300px;
+ height: 50vh;
overflow: hidden;
border: 1px solid var(--border-color);
margin-block-start: 0;
diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx
index 71a1095..7dab82b 100644
--- a/src/frontend/app/components/StopMapSheet.tsx
+++ b/src/frontend/app/components/StopMapSheet.tsx
@@ -1,8 +1,8 @@
import maplibregl from "maplibre-gl";
import React, { useEffect, useMemo, useRef, useState } from "react";
-import Map, { Marker, type MapRef } from "react-map-gl/maplibre";
+import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre";
import { useApp } from "~/AppContext";
-import type { RegionId } from "~/config/RegionConfig";
+import { getRegionConfig, type RegionId } from "~/config/RegionConfig";
import { getLineColor } from "~/data/LineColors";
import type { Stop } from "~/data/StopDataProvider";
import { loadStyle } from "~/maps/styleloader";
@@ -12,12 +12,16 @@ export interface Position {
latitude: number;
longitude: number;
orientationDegrees: number;
+ shapeIndex?: number;
}
export interface ConsolidatedCirculationForMap {
line: string;
route: string;
currentPosition?: Position;
+ schedule?: {
+ shapeId?: string;
+ };
}
interface StopMapProps {
@@ -44,6 +48,36 @@ export const StopMap: React.FC<StopMapProps> = ({
const [zoom, setZoom] = useState<number>(16);
const [moveTick, setMoveTick] = useState<number>(0);
const [showAttribution, setShowAttribution] = useState(false);
+ const [shapes, setShapes] = useState<Record<string, any>>({});
+
+ const regionConfig = getRegionConfig(region);
+
+ useEffect(() => {
+ circulations.forEach((c) => {
+ if (
+ c.schedule?.shapeId &&
+ c.currentPosition?.shapeIndex !== undefined &&
+ regionConfig.shapeEndpoint
+ ) {
+ const key = `${c.schedule.shapeId}_${c.currentPosition.shapeIndex}`;
+ if (!shapes[key]) {
+ fetch(
+ `${regionConfig.shapeEndpoint}?shapeId=${c.schedule.shapeId}&startPointIndex=${c.currentPosition.shapeIndex}`
+ )
+ .then((res) => {
+ if (res.ok) return res.json();
+ return null;
+ })
+ .then((data) => {
+ if (data) {
+ setShapes((prev) => ({ ...prev, [key]: data }));
+ }
+ })
+ .catch((err) => console.error("Failed to load shape", err));
+ }
+ }
+ });
+ }, [circulations, regionConfig.shapeEndpoint, shapes]);
type Pt = { lat: number; lon: number };
const haversineKm = (a: Pt, b: Pt) => {
@@ -308,6 +342,44 @@ export const StopMap: React.FC<StopMapProps> = ({
setMoveTick((t) => (t + 1) % 1000000);
}}
>
+ {/* Shapes */}
+ {circulations.map((c, idx) => {
+ if (
+ !c.schedule?.shapeId ||
+ c.currentPosition?.shapeIndex === undefined
+ )
+ return null;
+ const key = `${c.schedule.shapeId}_${c.currentPosition.shapeIndex}`;
+ const shapeData = shapes[key];
+ if (!shapeData) return null;
+ const lineColor = getLineColor(region, c.line);
+
+ return (
+ <Source
+ key={idx}
+ id={`shape-${idx}`}
+ type="geojson"
+ data={shapeData}
+ >
+ <Layer
+ id={`layer-border-${idx}`}
+ type="line"
+ paint={{
+ "line-color": "#000000",
+ "line-width": 6,
+ }}
+ />
+ <Layer
+ id={`layer-inner-${idx}`}
+ type="line"
+ paint={{
+ "line-color": lineColor.background,
+ "line-width": 4,
+ }}
+ />
+ </Source>
+ );
+ })}
{/* Stop marker (center) */}
{stop.latitude && stop.longitude && (
diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts
index 8acfbbf..a6ffdf8 100644
--- a/src/frontend/app/config/RegionConfig.ts
+++ b/src/frontend/app/config/RegionConfig.ts
@@ -7,6 +7,7 @@ export interface RegionConfig {
estimatesEndpoint: string;
consolidatedCirculationsEndpoint: string | null;
timetableEndpoint: string | null;
+ shapeEndpoint: string | null;
defaultCenter: [number, number]; // [lat, lng]
bounds?: {
sw: [number, number];
@@ -25,6 +26,7 @@ export const REGIONS: Record<RegionId, RegionConfig> = {
estimatesEndpoint: "/api/vigo/GetStopEstimates",
consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations",
timetableEndpoint: "/api/vigo/GetStopTimetable",
+ shapeEndpoint: "/api/vigo/GetShape",
defaultCenter: [42.229188855975046, -8.72246955783102],
bounds: {
sw: [-8.951059, 42.098923],
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index a2b2da3..024ea73 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -23,6 +23,7 @@ export interface ConsolidatedCirculation {
minutes: number;
serviceId: string;
tripId: string;
+ shapeId?: string;
};
realTime?: {
minutes: number;
@@ -32,6 +33,7 @@ export interface ConsolidatedCirculation {
latitude: number;
longitude: number;
orientationDegrees: number;
+ shapeIndex?: number;
};
}
@@ -269,6 +271,11 @@ export default function Estimates() {
line: c.line,
route: c.route,
currentPosition: c.currentPosition,
+ schedule: c.schedule
+ ? {
+ shapeId: c.schedule.shapeId,
+ }
+ : undefined,
}))}
isOpen={isMapModalOpen}
onClose={() => setIsMapModalOpen(false)}