diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-25 02:37:21 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-25 02:37:21 +0100 |
| commit | 0197a19973940d40a373b8aa68b2791391149cef (patch) | |
| tree | 36ac440484dabaebd8b17089c56f984e64601f45 /src/frontend | |
| parent | 843cfb208849d652da16e943247057cf5a251254 (diff) | |
Implement selecting stop layers to display
Diffstat (limited to 'src/frontend')
| -rw-r--r-- | src/frontend/app/components/stop/StopMapModal.tsx | 335 | ||||
| -rw-r--r-- | src/frontend/app/contexts/SettingsContext.tsx | 64 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/en-GB.json | 3 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/es-ES.json | 3 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/gl-ES.json | 3 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 50 | ||||
| -rw-r--r-- | src/frontend/app/routes/settings.tsx | 44 |
7 files changed, 311 insertions, 191 deletions
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<StopMapModalProps> = ({ 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<StopMapModalProps> = ({ }} > {/* Previous Shape Layer */} - {previousShapeData && selectedBus && ( - <Source - id="prev-route-shape" - type="geojson" - data={previousShapeData} - > - {/* 1. Black border */} - <Layer - id="prev-route-shape-border" - type="line" - paint={{ - "line-color": "#000000", - "line-width": 6, - "line-opacity": 0.8, - }} - layout={{ - "line-cap": "round", - "line-join": "round", - }} - /> - {/* 2. White background */} - <Layer - id="prev-route-shape-white" - type="line" - paint={{ - "line-color": "#FFFFFF", - "line-width": 4, - }} - layout={{ - "line-cap": "round", - "line-join": "round", - }} - /> - {/* 3. Colored dashes */} - <Layer - id="prev-route-shape-inner" - type="line" - paint={{ - "line-color": getLineColour(selectedBus.line) - .background, - "line-width": 4, - "line-dasharray": [2, 2], - }} - layout={{ - "line-cap": "round", - "line-join": "round", - }} - /> - </Source> - )} + {previousShapeData && selectedBus && ( + <Source + id="prev-route-shape" + type="geojson" + data={previousShapeData} + > + {/* 1. Black border */} + <Layer + id="prev-route-shape-border" + type="line" + paint={{ + "line-color": "#000000", + "line-width": 6, + "line-opacity": 0.8, + }} + layout={{ + "line-cap": "round", + "line-join": "round", + }} + /> + {/* 2. White background */} + <Layer + id="prev-route-shape-white" + type="line" + paint={{ + "line-color": "#FFFFFF", + "line-width": 4, + }} + layout={{ + "line-cap": "round", + "line-join": "round", + }} + /> + {/* 3. Colored dashes */} + <Layer + id="prev-route-shape-inner" + type="line" + paint={{ + "line-color": getLineColour(selectedBus.line) + .background, + "line-width": 4, + "line-dasharray": [2, 2], + }} + layout={{ + "line-cap": "round", + "line-join": "round", + }} + /> + </Source> + )} - {/* 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": getLineColour(selectedBus.line) - .background, - "line-width": 3, - "line-opacity": 0.7, - }} - layout={{ - "line-cap": "round", - "line-join": "round", - }} - /> + {/* 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": getLineColour(selectedBus.line) + .background, + "line-width": 3, + "line-opacity": 0.7, + }} + layout={{ + "line-cap": "round", + "line-join": "round", + }} + /> - {/* Stops Layer */} - <Layer - id="route-stops" - type="circle" - filter={["==", "type", "stop"]} - paint={{ - "circle-color": "#FFFFFF", - "circle-radius": 4, - "circle-stroke-width": 2, - "circle-stroke-color": getLineColour(selectedBus.line) - .background, - }} - /> - </Source> - )} + {/* Stops Layer */} + <Layer + id="route-stops" + type="circle" + filter={["==", "type", "stop"]} + paint={{ + "circle-color": "#FFFFFF", + "circle-radius": 4, + "circle-stroke-width": 2, + "circle-stroke-color": getLineColour(selectedBus.line) + .background, + }} + /> + </Source> + )} - {/* Stop marker */} - {stop.latitude && stop.longitude && ( - <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-stop" - 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-stop)" - /> - <circle cx="14" cy="13" r="5" fill="#fff" /> - <circle cx="14" cy="13" r="3" fill="#1976d2" /> - </svg> - </div> - </Marker> - )} + {/* Stop marker */} + {stop.latitude && stop.longitude && ( + <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-stop" + 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-stop)" + /> + <circle cx="14" cy="13" r="5" fill="#fff" /> + <circle cx="14" cy="13" r="3" fill="#1976d2" /> + </svg> + </div> + </Marker> + )} - {/* Selected bus marker */} - {selectedBus?.currentPosition && ( - <Marker - longitude={selectedBus.currentPosition.longitude} - latitude={selectedBus.currentPosition.latitude} - anchor="center" + {/* Selected bus marker */} + {selectedBus?.currentPosition && ( + <Marker + longitude={selectedBus.currentPosition.longitude} + latitude={selectedBus.currentPosition.latitude} + anchor="center" + > + <div + style={{ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 6, + transform: `rotate(${selectedBus.currentPosition.orientationDegrees}deg)`, + transformOrigin: "center center", + }} > - <div + <svg + width="24" + height="24" + viewBox="0 0 24 24" style={{ - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: 6, - transform: `rotate(${selectedBus.currentPosition.orientationDegrees}deg)`, - transformOrigin: "center center", + filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.3))", }} > - <svg - width="24" - height="24" - viewBox="0 0 24 24" - style={{ - filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.3))", - }} - > - <path - d="M12 2 L22 22 L12 17 L2 22 Z" - fill={getLineColour(selectedBus.line).background} - stroke="#000" - strokeWidth="2" - strokeLinejoin="round" - /> - </svg> - </div> - </Marker> - )} - </AppMap> + <path + d="M12 2 L22 22 L12 17 L2 22 Z" + fill={getLineColour(selectedBus.line).background} + stroke="#000" + strokeWidth="2" + strokeLinejoin="round" + /> + </svg> + </div> + </Marker> + )} + </AppMap> {/* Floating controls */} <div className="map-modal-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<SettingsContextProps | undefined>( @@ -141,6 +148,56 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { localStorage.setItem("showCameras", showCameras.toString()); }, [showCameras]); + + const [showBusStops, setShowBusStops] = useState<boolean>(() => { + 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<boolean>(() => { + 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<boolean>(() => { + 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" /> </label> + + <hr className="border-border" /> + <label className="flex items-center justify-between p-4 rounded-lg border border-border bg-surface cursor-pointer hover:bg-surface/50 transition-colors"> + <span className="text-text font-medium"> + {t("about.show_stops_bus", "Mostrar paradas de autobús")} + </span> + <input + type="checkbox" + checked={showBusStops} + onChange={(e) => setShowBusStops(e.target.checked)} + className="w-5 h-5 rounded border-border text-primary focus:ring-primary/50" + /> + </label> + <label className="flex items-center justify-between p-4 rounded-lg border border-border bg-surface cursor-pointer hover:bg-surface/50 transition-colors"> + <span className="text-text font-medium"> + {t( + "about.show_stops_coach", + "Mostrar paradas de autobús interurbano" + )} + </span> + <input + type="checkbox" + checked={showCoachStops} + onChange={(e) => setShowCoachStops(e.target.checked)} + className="w-5 h-5 rounded border-border text-primary focus:ring-primary/50" + /> + </label> + <label className="flex items-center justify-between p-4 rounded-lg border border-border bg-surface cursor-pointer hover:bg-surface/50 transition-colors"> + <span className="text-text font-medium"> + {t("about.show_stops_train", "Mostrar paradas de tren")} + </span> + <input + type="checkbox" + checked={showTrainStops} + onChange={(e) => setShowTrainStops(e.target.checked)} + className="w-5 h-5 rounded border-border text-primary focus:ring-primary/50" + /> + </label> </div> </section> |
