From 0197a19973940d40a373b8aa68b2791391149cef Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 25 Dec 2025 02:37:21 +0100 Subject: Implement selecting stop layers to display --- src/frontend/app/components/stop/StopMapModal.tsx | 343 +++++++++++----------- src/frontend/app/contexts/SettingsContext.tsx | 64 ++++ src/frontend/app/i18n/locales/en-GB.json | 3 + src/frontend/app/i18n/locales/es-ES.json | 3 + src/frontend/app/i18n/locales/gl-ES.json | 3 + src/frontend/app/routes/map.tsx | 50 ++-- src/frontend/app/routes/settings.tsx | 44 +++ 7 files changed, 315 insertions(+), 195 deletions(-) (limited to 'src/frontend') diff --git a/src/frontend/app/components/stop/StopMapModal.tsx b/src/frontend/app/components/stop/StopMapModal.tsx index 2e091b1..757411e 100644 --- a/src/frontend/app/components/stop/StopMapModal.tsx +++ b/src/frontend/app/components/stop/StopMapModal.tsx @@ -9,7 +9,6 @@ import { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre"; import { Sheet } from "react-modal-sheet"; import { useApp } from "~/AppContext"; import { AppMap } from "~/components/shared/AppMap"; -import { APP_CONSTANTS } from "~/config/constants"; import { getLineColour } from "~/data/LineColors"; import type { Stop } from "~/data/StopDataProvider"; import "./StopMapModal.css"; @@ -370,8 +369,6 @@ export const StopMapModal: React.FC = ({ showTraffic={false} attributionControl={{ compact: false, - customAttribution: - "Concello de Vigo & Viguesa de Transportes SL", }} onMove={(e) => { if (e.originalEvent) { @@ -395,180 +392,180 @@ export const StopMapModal: React.FC = ({ }} > {/* Previous Shape Layer */} - {previousShapeData && selectedBus && ( - - {/* 1. Black border */} - - {/* 2. White background */} - - {/* 3. Colored dashes */} - - - )} - - {/* Shape Layer */} - {shapeData && selectedBus && ( - - - - - {/* Stops Layer */} - - - )} - - {/* Stop marker */} - {stop.latitude && stop.longitude && ( - -
- - - - - - - - - - -
-
- )} - - {/* Selected bus marker */} - {selectedBus?.currentPosition && ( - + {/* 1. Black border */} + + {/* 2. White background */} + + {/* 3. Colored dashes */} + + + )} + + {/* Shape Layer */} + {shapeData && selectedBus && ( + + + + + {/* Stops Layer */} + + + )} + + {/* Stop marker */} + {stop.latitude && stop.longitude && ( + +
+ + + + + + + + + + +
+
+ )} + + {/* Selected bus marker */} + {selectedBus?.currentPosition && ( + +
-
- - - -
- - )} - + + +
+
+ )} + {/* Floating controls */}
diff --git a/src/frontend/app/contexts/SettingsContext.tsx b/src/frontend/app/contexts/SettingsContext.tsx index 6a64b67..1833818 100644 --- a/src/frontend/app/contexts/SettingsContext.tsx +++ b/src/frontend/app/contexts/SettingsContext.tsx @@ -23,6 +23,13 @@ interface SettingsContextProps { setShowTraffic: (show: boolean) => void; showCameras: boolean; setShowCameras: (show: boolean) => void; + + showBusStops: boolean; + setShowBusStops: (show: boolean) => void; + showCoachStops: boolean; + setShowCoachStops: (show: boolean) => void; + showTrainStops: boolean; + setShowTrainStops: (show: boolean) => void; } const SettingsContext = createContext( @@ -141,6 +148,56 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { localStorage.setItem("showCameras", showCameras.toString()); }, [showCameras]); + + const [showBusStops, setShowBusStops] = useState(() => { + const saved = localStorage.getItem("stopsLayers"); + if (saved) { + try { + const parsed = JSON.parse(saved); + return parsed.bus ?? true; + } catch { + return true; + } + } + return true; + }); + + const [showCoachStops, setShowCoachStops] = useState(() => { + const saved = localStorage.getItem("stopsLayers"); + if (saved) { + try { + const parsed = JSON.parse(saved); + return parsed.coach ?? true; + } catch { + return true; + } + } + return true; + }); + + const [showTrainStops, setShowTrainStops] = useState(() => { + const saved = localStorage.getItem("stopsLayers"); + if (saved) { + try { + const parsed = JSON.parse(saved); + return parsed.train ?? true; + } catch { + return true; + } + } + return true; + }); + + useEffect(() => { + localStorage.setItem( + "stopsLayers", + JSON.stringify({ + bus: showBusStops, + coach: showCoachStops, + train: showTrainStops, + }) + ); + }, [showBusStops, showCoachStops, showTrainStops]); //#endregion return ( @@ -156,6 +213,13 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { setShowTraffic, showCameras, setShowCameras, + + showBusStops, + setShowBusStops, + showCoachStops, + setShowCoachStops, + showTrainStops, + setShowTrainStops, }} > {children} diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json index bd51aa4..a8f3f52 100644 --- a/src/frontend/app/i18n/locales/en-GB.json +++ b/src/frontend/app/i18n/locales/en-GB.json @@ -29,6 +29,9 @@ "map_layers": "Map layers", "show_traffic": "Show traffic", "show_cameras": "Show cameras", + "show_stops_bus": "Show bus stops", + "show_stops_coach": "Show coach stops", + "show_stops_train": "Show train stops", "language": "Language" }, "stoplist": { diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json index c20d660..2bffac9 100644 --- a/src/frontend/app/i18n/locales/es-ES.json +++ b/src/frontend/app/i18n/locales/es-ES.json @@ -29,6 +29,9 @@ "map_layers": "Capas del mapa", "show_traffic": "Mostrar tráfico", "show_cameras": "Mostrar cámaras", + "show_stops_bus": "Mostrar paradas de autobús", + "show_stops_coach": "Mostrar paradas de autobús interurbano", + "show_stops_train": "Mostrar paradas de tren", "language": "Idioma" }, "stoplist": { diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json index e7068e8..5086feb 100644 --- a/src/frontend/app/i18n/locales/gl-ES.json +++ b/src/frontend/app/i18n/locales/gl-ES.json @@ -29,6 +29,9 @@ "map_layers": "Capas do mapa", "show_traffic": "Amosar tráfico", "show_cameras": "Amosar cámaras", + "show_stops_bus": "Amosar paradas de autobús", + "show_stops_coach": "Amosar paradas de autobús interurbano", + "show_stops_train": "Amosar paradas de tren", "language": "Idioma" }, "stoplist": { diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index a8c74b4..cccdaa3 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -1,7 +1,8 @@ import StopDataProvider from "../data/StopDataProvider"; import "./map.css"; -import { useRef, useState } from "react"; +import type { FilterSpecification } from "maplibre-gl"; +import { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Layer, @@ -10,6 +11,7 @@ import { type MapRef, } from "react-map-gl/maplibre"; import { useNavigate } from "react-router"; +import { useApp } from "~/AppContext"; import { StopSummarySheet, type StopSheetProps, @@ -23,6 +25,11 @@ import "../tailwind-full.css"; // Componente principal del mapa export default function StopMap() { const { t } = useTranslation(); + const { + showBusStops: showCitybusStops, + showCoachStops: showIntercityBusStops, + showTrainStops, + } = useApp(); const navigate = useNavigate(); usePageTitle(t("navbar.map", "Mapa")); const [selectedStop, setSelectedStop] = useState< @@ -50,6 +57,20 @@ export default function StopMap() { handlePointClick(feature); }; + const stopLayerFilter = useMemo(() => { + const filter: FilterSpecification = ["any"]; + if (showCitybusStops) { + filter.push(["==", ["get", "transitKind"], "bus"]); + } + if (showIntercityBusStops) { + filter.push(["==", ["get", "transitKind"], "coach"]); + } + if (showTrainStops) { + filter.push(["==", ["get", "transitKind"], "train"]); + } + return filter; + }, [showCitybusStops, showIntercityBusStops, showTrainStops]); + const getLatitude = (center: any) => Array.isArray(center) ? center[0] : center.lat; const getLongitude = (center: any) => @@ -63,7 +84,7 @@ export default function StopMap() { routes: string; } = feature.properties; // TODO: Move ID to constant, improve type checking - if (!props || feature.layer.id !== "stops") { + if (!props || feature.layer.id.startsWith("stops") === false) { console.warn("Invalid feature properties:", props); return; } @@ -123,25 +144,9 @@ export default function StopMap() { minzoom={11} source="stops-source" source-layer="stops" + filter={stopLayerFilter} layout={{ - // TODO: Fix ñapa by maybe including this from the server side? - "icon-image": [ - "match", - ["get", "feed"], - "vitrasa", - "stop-vitrasa", - "santiago", - "stop-santiago", - "coruna", - "stop-coruna", - "xunta", - "stop-xunta", - "renfe", - "stop-renfe", - "feve", - "stop-feve", - "stop-generic", - ], + "icon-image": ["get", "icon"], "icon-size": [ "interpolate", ["linear"], @@ -164,6 +169,7 @@ export default function StopMap() { source="stops-source" source-layer="stops" minzoom={16} + filter={stopLayerFilter} layout={{ "text-field": ["get", "name"], "text-font": ["Noto Sans Bold"], @@ -177,7 +183,7 @@ export default function StopMap() { "match", ["get", "feed"], "vitrasa", - "#95D516", + "#81D002", "santiago", "#508096", "coruna", @@ -188,7 +194,7 @@ export default function StopMap() { "#870164", "feve", "#EE3D32", - "#333333", + "#27187D", ], "text-halo-color": "#FFF", "text-halo-width": 1, diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index f51b2e9..e7fdffa 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -16,6 +16,12 @@ export default function Settings() { setShowTraffic, showCameras, setShowCameras, + showBusStops, + setShowBusStops, + showCoachStops, + setShowCoachStops, + showTrainStops, + setShowTrainStops, } = useApp(); const THEMES = [ @@ -120,6 +126,44 @@ export default function Settings() { className="w-5 h-5 rounded border-border text-primary focus:ring-primary/50" /> + +
+ + +
-- cgit v1.3