aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-25 02:37:21 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-25 02:37:21 +0100
commit0197a19973940d40a373b8aa68b2791391149cef (patch)
tree36ac440484dabaebd8b17089c56f984e64601f45 /src
parent843cfb208849d652da16e943247057cf5a251254 (diff)
Implement selecting stop layers to display
Diffstat (limited to 'src')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TileController.cs36
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/FeedService.cs2
-rw-r--r--src/frontend/app/components/stop/StopMapModal.tsx335
-rw-r--r--src/frontend/app/contexts/SettingsContext.tsx64
-rw-r--r--src/frontend/app/i18n/locales/en-GB.json3
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json3
-rw-r--r--src/frontend/app/i18n/locales/gl-ES.json3
-rw-r--r--src/frontend/app/routes/map.tsx50
-rw-r--r--src/frontend/app/routes/settings.tsx44
9 files changed, 343 insertions, 197 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
index f3fe51c..52d919f 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
@@ -84,7 +84,7 @@ public class TileController : ControllerBase
var tileDef = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, z);
VectorTile vt = new() { TileId = tileDef.Id };
- var lyr = new Layer { Name = "stops" };
+ var stopsLayer = new Layer { Name = "stops" };
responseBody.Data?.StopsByBbox?.ForEach(stop =>
{
@@ -108,12 +108,13 @@ public class TileController : ControllerBase
{
// The ID will be used to request the arrivals
{ "id", stop.GtfsId },
- // The feed is the first part of the GTFS ID, corresponding to the feed where the info comes from, used for icons probably
+ // The feed is the first part of the GTFS ID
{ "feed", idParts[0] },
// The public identifier, usually feed:code or feed:id, recognisable by users and in other systems
{ "code", $"{idParts[0]}:{codeWithinFeed}" },
- // The name of the stop
{ "name", _feedService.NormalizeStopName(feedId, stop.Name) },
+ { "icon", GetIconNameForFeed(feedId) },
+ { "transitKind", GetTransitKind(feedId) },
// Routes
{ "routes", JsonSerializer
.Serialize(
@@ -146,10 +147,10 @@ public class TileController : ControllerBase
}
};
- lyr.Features.Add(feature);
+ stopsLayer.Features.Add(feature);
});
- vt.Layers.Add(lyr);
+ vt.Layers.Add(stopsLayer);
using var ms = new MemoryStream();
vt.Write(ms, minLinealExtent: 1, minPolygonalExtent: 2);
@@ -160,6 +161,31 @@ public class TileController : ControllerBase
return File(ms.ToArray(), "application/x-protobuf");
}
+ private string GetIconNameForFeed(string feedId)
+ {
+ return feedId switch
+ {
+ "vitrasa" => "stop-vitrasa",
+ "santiago" => "stop-santiago",
+ "coruna" => "stop-coruna",
+ "xunta" => "stop-xunta",
+ "renfe" => "stop-renfe",
+ "feve" => "stop-feve",
+ _ => "stop-generic",
+ };
+ }
+
+ private string GetTransitKind(string feedId)
+ {
+ return feedId switch
+ {
+ "vitrasa" or "santiago" or "coruna" => "bus",
+ "xunta" => "coach",
+ "renfe" or "feve" => "train",
+ _ => "unknown",
+ };
+ }
+
private List<StopTileResponse.Route> GetDistinctRoutes(string feedId, List<StopTileResponse.Route> routes)
{
List<StopTileResponse.Route> distinctRoutes = [];
diff --git a/src/Costasdev.Busurbano.Backend/Services/FeedService.cs b/src/Costasdev.Busurbano.Backend/Services/FeedService.cs
index 6cebcf2..a8710b5 100644
--- a/src/Costasdev.Busurbano.Backend/Services/FeedService.cs
+++ b/src/Costasdev.Busurbano.Backend/Services/FeedService.cs
@@ -37,7 +37,7 @@ public class FeedService
{
return feed switch
{
- "vitrasa" => ("#95D516", "#000000"),
+ "vitrasa" => ("#81D002", "#000000"),
"santiago" => ("#508096", "#FFFFFF"),
"coruna" => ("#E61C29", "#FFFFFF"),
"xunta" => ("#007BC4", "#FFFFFF"),
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>