aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Taskfile.yml11
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/LineFormatterService.cs36
-rw-r--r--src/frontend/app/AppContext.tsx4
-rw-r--r--src/frontend/app/components/LineIcon.tsx9
-rw-r--r--src/frontend/app/components/StopGallery.tsx21
-rw-r--r--src/frontend/app/components/StopGalleryItem.tsx10
-rw-r--r--src/frontend/app/components/StopHelpModal.tsx4
-rw-r--r--src/frontend/app/components/StopMapModal.tsx29
-rw-r--r--src/frontend/app/components/StopSummarySheet.tsx8
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.css3
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx139
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx14
-rw-r--r--src/frontend/app/components/ThemeColorManager.tsx6
-rw-r--r--src/frontend/app/components/layout/AppShell.tsx5
-rw-r--r--src/frontend/app/components/layout/Drawer.css4
-rw-r--r--src/frontend/app/components/layout/NavBar.tsx9
-rw-r--r--src/frontend/app/config/RegionConfig.ts6
-rw-r--r--src/frontend/app/contexts/PageTitleContext.tsx4
-rw-r--r--src/frontend/app/data/LineColors.ts5
-rw-r--r--src/frontend/app/data/LinesData.ts472
-rw-r--r--src/frontend/app/data/StopDataProvider.ts63
-rw-r--r--src/frontend/app/root.css4
-rw-r--r--src/frontend/app/root.tsx2
-rw-r--r--src/frontend/app/routes/about.tsx23
-rw-r--r--src/frontend/app/routes/home.tsx36
-rw-r--r--src/frontend/app/routes/lines.tsx57
-rw-r--r--src/frontend/app/routes/map.tsx62
-rw-r--r--src/frontend/app/routes/settings.tsx34
-rw-r--r--src/frontend/app/routes/stops-$id.tsx44
-rw-r--r--src/frontend/vite.config.ts6
-rw-r--r--src/gtfs_perstop_report/src/common.py1
-rw-r--r--src/gtfs_perstop_report/src/download.py96
-rw-r--r--src/gtfs_perstop_report/src/logger.py12
-rw-r--r--src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py30
-rw-r--r--src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi69
-rw-r--r--src/gtfs_perstop_report/src/routes.py24
-rw-r--r--src/gtfs_perstop_report/src/services.py61
-rw-r--r--src/gtfs_perstop_report/src/shapes.py31
-rw-r--r--src/gtfs_perstop_report/src/stop_schedule_pb2.py35
-rw-r--r--src/gtfs_perstop_report/src/stop_schedule_pb2.pyi34
-rw-r--r--src/gtfs_perstop_report/src/stop_times.py71
-rw-r--r--src/gtfs_perstop_report/src/stops.py4
-rw-r--r--src/gtfs_perstop_report/src/street_name.py17
-rw-r--r--src/gtfs_perstop_report/src/trips.py75
-rw-r--r--src/gtfs_perstop_report/stop_report.py69
-rw-r--r--src/stop_downloader/vigo/download-stops.py59
46 files changed, 1058 insertions, 760 deletions
diff --git a/Taskfile.yml b/Taskfile.yml
index 583004e..baf4a95 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -29,15 +29,12 @@ tasks:
- mkdir dist/frontend
- mv src/frontend/build/client/ dist/frontend/
- format-backend:
+ format:
desc: Format backend solution.
cmds:
- - dotnet format --verbosity diagnostic
-
- format-frontend:
- desc: Format frontend sources.
- cmds:
- - 'prettier --write src/frontend/**/*.{ts,tsx,css}'
+ - dotnet format --verbosity diagnostic src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
+ - npx prettier --write "src/frontend/**/*.{ts,tsx,css}"
+ - uvx ruff format ./src/gtfs_perstop_report ./src/stop_downloader
gen-stop-report:
desc: Generate stop-based JSON reports for specified dates or date ranges.
diff --git a/src/Costasdev.Busurbano.Backend/Services/LineFormatterService.cs b/src/Costasdev.Busurbano.Backend/Services/LineFormatterService.cs
index 2108b2f..6ec40b1 100644
--- a/src/Costasdev.Busurbano.Backend/Services/LineFormatterService.cs
+++ b/src/Costasdev.Busurbano.Backend/Services/LineFormatterService.cs
@@ -1,4 +1,4 @@
-using Costasdev.Busurbano.Backend.Types;
+using Costasdev.Busurbano.Backend.Types;
namespace Costasdev.Busurbano.Backend.Services;
@@ -25,27 +25,27 @@ public class LineFormatterService
.Replace("\"", "");
return circulation;
case "FUT":
- {
- if (circulation.Route == "CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO")
{
- circulation.Line = "MAR";
- circulation.Route = "MARCADOR ⚽: CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO";
- }
+ if (circulation.Route == "CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO")
+ {
+ circulation.Line = "MAR";
+ circulation.Route = "MARCADOR ⚽: CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO";
+ }
- if (circulation.Route == "P. ESPAÑA-T.VIGO-S.BADÍA")
- {
- circulation.Line = "RIO";
- circulation.Route = "RÍO ⚽: P. ESPAÑA-T.VIGO-S.BADÍA";
- }
+ if (circulation.Route == "P. ESPAÑA-T.VIGO-S.BADÍA")
+ {
+ circulation.Line = "RIO";
+ circulation.Route = "RÍO ⚽: P. ESPAÑA-T.VIGO-S.BADÍA";
+ }
- if (circulation.Route == "NAVIA-BOUZAS-URZAIZ-G. ESPINO")
- {
- circulation.Line = "GOL";
- circulation.Route = "GOL ⚽: NAVIA-BOUZAS-URZAIZ-G. ESPINO";
- }
+ if (circulation.Route == "NAVIA-BOUZAS-URZAIZ-G. ESPINO")
+ {
+ circulation.Line = "GOL";
+ circulation.Route = "GOL ⚽: NAVIA-BOUZAS-URZAIZ-G. ESPINO";
+ }
- return circulation;
- }
+ return circulation;
+ }
default:
return circulation;
}
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 12a54da..2102ad7 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -23,7 +23,9 @@ export const useApp = () => {
...map,
// Mock region support for now since we only have one region
region: "vigo" as RegionId,
- setRegion: (region: RegionId) => { console.log("Set region", region); },
+ setRegion: (region: RegionId) => {
+ console.log("Set region", region);
+ },
};
};
diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx
index fc40824..8bbeb20 100644
--- a/src/frontend/app/components/LineIcon.tsx
+++ b/src/frontend/app/components/LineIcon.tsx
@@ -6,13 +6,10 @@ interface LineIconProps {
mode?: "rounded" | "pill" | "default";
}
-const LineIcon: React.FC<LineIconProps> = ({
- line,
- mode = "default",
-}) => {
+const LineIcon: React.FC<LineIconProps> = ({ line, mode = "default" }) => {
const actualLine = useMemo(() => {
- return line.trim().replace('510', 'NAD');
- }, [line])
+ return line.trim().replace("510", "NAD");
+ }, [line]);
const formattedLine = useMemo(() => {
return /^[a-zA-Z]/.test(actualLine) ? actualLine : `L${actualLine}`;
diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx
index a45bfca..8c13aa1 100644
--- a/src/frontend/app/components/StopGallery.tsx
+++ b/src/frontend/app/components/StopGallery.tsx
@@ -36,7 +36,9 @@ const StopGallery: React.FC<StopGalleryProps> = ({
if (stops.length === 0 && emptyMessage) {
return (
<div className="w-full px-4 flex flex-col gap-2">
- <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
+ {title}
+ </h3>
<div className="text-center">
<p className="text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 rounded-lg">
{emptyMessage}
@@ -52,15 +54,17 @@ const StopGallery: React.FC<StopGalleryProps> = ({
return (
<div className="w-full px-4 flex flex-col gap-2">
- <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
+ {title}
+ </h3>
<div
ref={scrollRef}
className="overflow-x-auto overflow-y-hidden snap-x snap-mandatory scrollbar-hide pb-2"
style={{
- WebkitOverflowScrolling: 'touch',
- scrollbarWidth: 'none',
- msOverflowStyle: 'none'
+ WebkitOverflowScrolling: "touch",
+ scrollbarWidth: "none",
+ msOverflowStyle: "none",
}}
>
<div className="flex gap-3">
@@ -73,8 +77,11 @@ const StopGallery: React.FC<StopGalleryProps> = ({
{stops.map((_, index) => (
<span
key={index}
- className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${index === activeIndex ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-700"
- }`}
+ className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${
+ index === activeIndex
+ ? "bg-blue-600"
+ : "bg-gray-300 dark:bg-gray-700"
+ }`}
></span>
))}
</div>
diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx
index 6c80362..bf60697 100644
--- a/src/frontend/app/components/StopGalleryItem.tsx
+++ b/src/frontend/app/components/StopGalleryItem.tsx
@@ -22,7 +22,9 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => {
to={`/stops/${stop.stopId}`}
>
<div className="flex items-center gap-2 mb-1">
- {stop.favourite && <span className="text-yellow-500 text-base">★</span>}
+ {stop.favourite && (
+ <span className="text-yellow-500 text-base">★</span>
+ )}
<span className="text-xs text-gray-600 dark:text-gray-400 font-medium">
({stop.stopId})
</span>
@@ -30,10 +32,10 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => {
<div
className="text-[0.95rem] font-semibold mb-2 leading-snug line-clamp-2 min-h-[2.5em]"
style={{
- display: '-webkit-box',
+ display: "-webkit-box",
WebkitLineClamp: 2,
- WebkitBoxOrient: 'vertical',
- overflow: 'hidden'
+ WebkitBoxOrient: "vertical",
+ overflow: "hidden",
}}
>
{StopDataProvider.getDisplayName(stop)}
diff --git a/src/frontend/app/components/StopHelpModal.tsx b/src/frontend/app/components/StopHelpModal.tsx
index 87e02f9..e8157ab 100644
--- a/src/frontend/app/components/StopHelpModal.tsx
+++ b/src/frontend/app/components/StopHelpModal.tsx
@@ -21,9 +21,7 @@ export const StopHelpModal: React.FC<StopHelpModalProps> = ({
<Sheet.Content>
<div className="p-6 pb-10 flex flex-col gap-8 overflow-y-auto max-h-[80vh] text-slate-900 dark:text-slate-100">
<div>
- <h2 className="text-xl font-bold mb-4">
- {t("stop_help.title")}
- </h2>
+ <h2 className="text-xl font-bold mb-4">{t("stop_help.title")}</h2>
<div className="space-y-5">
<div className="flex gap-4 items-start">
diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx
index 55ad848..1cb6d88 100644
--- a/src/frontend/app/components/StopMapModal.tsx
+++ b/src/frontend/app/components/StopMapModal.tsx
@@ -1,11 +1,12 @@
import maplibregl from "maplibre-gl";
-import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import Map, {
- Layer,
- Marker,
- Source,
- type MapRef
-} from "react-map-gl/maplibre";
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre";
import { Sheet } from "react-modal-sheet";
import { useApp } from "~/AppContext";
import { REGION_DATA } from "~/config/RegionConfig";
@@ -161,7 +162,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
maxZoom: 17,
} as any);
}
- } catch { }
+ } catch {}
}, [stop, selectedBus, shapeData, previousShapeData]);
// Load style without traffic layers for the stop map
@@ -337,11 +338,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
}
return (
- <Sheet
- isOpen={isOpen}
- onClose={onClose}
- detent="content"
- >
+ <Sheet isOpen={isOpen} onClose={onClose} detent="content">
<Sheet.Container style={{ backgroundColor: "var(--background-color)" }}>
<Sheet.Header />
<Sheet.Content disableDrag={true}>
@@ -358,7 +355,11 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({
}}
style={{ width: "100%", height: "50vh" }}
mapStyle={styleSpec}
- attributionControl={{ compact: false, customAttribution: "Concello de Vigo & Viguesa de Transportes SL" }}
+ attributionControl={{
+ compact: false,
+ customAttribution:
+ "Concello de Vigo & Viguesa de Transportes SL",
+ }}
ref={mapRef}
interactive={true}
onMove={(e) => {
diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/StopSummarySheet.tsx
index e85dda3..55cbbd8 100644
--- a/src/frontend/app/components/StopSummarySheet.tsx
+++ b/src/frontend/app/components/StopSummarySheet.tsx
@@ -102,10 +102,10 @@ export const StopSheet: React.FC<StopSheetProps> = ({
// Show only the next 4 arrivals
const sortedData = data
? [...data].sort(
- (a, b) =>
- (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) -
- (b.realTime?.minutes ?? b.schedule?.minutes ?? 999)
- )
+ (a, b) =>
+ (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) -
+ (b.realTime?.minutes ?? b.schedule?.minutes ?? 999)
+ )
: [];
const limitedEstimates = sortedData.slice(0, 4);
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
index 9922b03..935c06d 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css
@@ -1,4 +1,4 @@
-@import '../../tailwind.css';
+@import "../../tailwind.css";
.consolidated-circulation-card {
all: unset;
@@ -40,7 +40,6 @@
pointer-events: none;
}
-
.consolidated-circulation-card .card-row {
display: flex;
align-items: center;
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
index 70a9355..3fa984b 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
-import Marquee from 'react-fast-marquee';
+import Marquee from "react-fast-marquee";
import { useTranslation } from "react-i18next";
import LineIcon from "~components/LineIcon";
import { type ConsolidatedCirculation } from "~routes/stops-$id";
@@ -109,7 +109,10 @@ const AutoMarquee = ({ text }: { text: string }) => {
}
return (
- <div ref={containerRef} className="w-full overflow-hidden text-sm font-mono truncate">
+ <div
+ ref={containerRef}
+ className="w-full overflow-hidden text-sm font-mono truncate"
+ >
{text}
</div>
);
@@ -175,9 +178,11 @@ export const ConsolidatedCirculationCard: React.FC<
const tone =
delta <= 2 ? "delay-ok" : delta <= 10 ? "delay-warn" : "delay-critical";
return {
- label: reduced ? `R${delta}` : t("estimates.delay_positive", {
- minutes: delta,
- }),
+ label: reduced
+ ? `R${delta}`
+ : t("estimates.delay_positive", {
+ minutes: delta,
+ }),
tone,
kind: "delay",
} as const;
@@ -186,22 +191,28 @@ export const ConsolidatedCirculationCard: React.FC<
// Early
const tone = absDelta <= 2 ? "delay-ok" : "delay-early";
return {
- label: reduced ? `A${absDelta}` : t("estimates.delay_negative", {
- minutes: absDelta,
- }),
+ label: reduced
+ ? `A${absDelta}`
+ : t("estimates.delay_negative", {
+ minutes: absDelta,
+ }),
tone,
kind: "delay",
} as const;
}, [estimate.schedule, estimate.realTime, t, reduced]);
const metaChips = useMemo(() => {
- const chips: Array<{ label: string; tone?: string, kind?: "regular" | "gps" | "delay" | "warning" }> = [];
+ const chips: Array<{
+ label: string;
+ tone?: string;
+ kind?: "regular" | "gps" | "delay" | "warning";
+ }> = [];
if (delayChip) {
chips.push(delayChip);
}
- if (estimate.schedule && driver !== 'renfe') {
+ if (estimate.schedule && driver !== "renfe") {
chips.push({
label: `${parseServiceId(estimate.schedule.serviceId)} · ${getTripIdDisplay(
estimate.schedule.tripId
@@ -211,7 +222,10 @@ export const ConsolidatedCirculationCard: React.FC<
}
if (estimate.realTime && estimate.realTime.distance >= 0) {
- chips.push({ label: formatDistance(estimate.realTime.distance), kind: "regular" });
+ chips.push({
+ label: formatDistance(estimate.realTime.distance),
+ kind: "regular",
+ });
}
if (estimate.currentPosition) {
@@ -243,7 +257,7 @@ export const ConsolidatedCirculationCard: React.FC<
// Check if bus has GPS position (live tracking)
const hasGpsPosition = !!estimate.currentPosition;
- const isRenfe = driver === 'renfe';
+ const isRenfe = driver === "renfe";
const isClickable = hasGpsPosition;
const looksDisabled = !isClickable && !isRenfe;
@@ -251,10 +265,10 @@ export const ConsolidatedCirculationCard: React.FC<
const interactiveProps = readonly
? {}
: {
- onClick: isClickable ? onMapClick : undefined,
- type: "button" as const,
- disabled: !isClickable,
- };
+ onClick: isClickable ? onMapClick : undefined,
+ type: "button" as const,
+ disabled: !isClickable,
+ };
if (reduced) {
return (
@@ -263,15 +277,16 @@ export const ConsolidatedCirculationCard: React.FC<
flex-none flex items-center gap-2.5 min-h-12
bg-(--message-background-color) border border-(--border-color)
rounded-xl px-3 py-2.5 transition-all
- ${readonly
- ? looksDisabled
- ? "opacity-70 cursor-not-allowed"
- : ""
- : isClickable
- ? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]"
- : looksDisabled
+ ${
+ readonly
+ ? looksDisabled
? "opacity-70 cursor-not-allowed"
: ""
+ : isClickable
+ ? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]"
+ : looksDisabled
+ ? "opacity-70 cursor-not-allowed"
+ : ""
}
`.trim()}
{...interactiveProps}
@@ -281,8 +296,10 @@ export const ConsolidatedCirculationCard: React.FC<
</div>
<div className="flex-1 min-w-0 flex flex-col gap-1">
<strong className="text-base text-(--text-color) overflow-hidden text-ellipsis line-clamp-2 leading-tight">
- {driver === 'renfe' && estimate.schedule?.tripId && (
- <span className="font-mono text-slate-500 mr-1.5 text-sm">{estimate.schedule.tripId}</span>
+ {driver === "renfe" && estimate.schedule?.tripId && (
+ <span className="font-mono text-slate-500 mr-1.5 text-sm">
+ {estimate.schedule.tripId}
+ </span>
)}
{estimate.route}
</strong>
@@ -292,22 +309,28 @@ export const ConsolidatedCirculationCard: React.FC<
let chipColourClasses = "";
switch (chip.tone) {
case "delay-ok":
- chipColourClasses = "bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300";
+ chipColourClasses =
+ "bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300";
break;
case "delay-warn":
- chipColourClasses = "bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300";
+ chipColourClasses =
+ "bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300";
break;
case "delay-critical":
- chipColourClasses = "bg-red-400/20 dark:bg-red-600/30 text-red-600 dark:text-red-300";
+ chipColourClasses =
+ "bg-red-400/20 dark:bg-red-600/30 text-red-600 dark:text-red-300";
break;
case "delay-early":
- chipColourClasses = "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300";
+ chipColourClasses =
+ "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300";
break;
case "warning":
- chipColourClasses = "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300";
+ chipColourClasses =
+ "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300";
break;
default:
- chipColourClasses = "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]";
+ chipColourClasses =
+ "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]";
}
return (
@@ -315,8 +338,12 @@ export const ConsolidatedCirculationCard: React.FC<
key={`${chip.label}-${idx}`}
className={`text-xs px-2 py-0.5 rounded-full flex items-center justify-center gap-1 shrink-0 ${chipColourClasses}`}
>
- {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)}
- {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)}
+ {chip.kind === "gps" && (
+ <LocateIcon className="w-3 h-3 inline-block" />
+ )}
+ {chip.kind === "warning" && (
+ <AlertTriangle className="w-3 h-3 inline-block" />
+ )}
{chip.label}
</span>
);
@@ -327,17 +354,20 @@ export const ConsolidatedCirculationCard: React.FC<
<div
className={`
inline-flex items-center justify-center px-2 py-1.5 rounded-xl shrink-0
- ${timeClass === "time-running"
- ? "bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]"
- : timeClass === "time-delayed"
- ? "bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]"
- : "bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]"
+ ${
+ timeClass === "time-running"
+ ? "bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]"
+ : timeClass === "time-delayed"
+ ? "bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]"
+ : "bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]"
}
`.trim()}
>
<div className="flex flex-col items-center leading-none">
<span className="text-lg font-bold leading-none">{etaValue}</span>
- <span className="text-[0.65rem] uppercase tracking-wider mt-0.5 opacity-90">{etaUnit}</span>
+ <span className="text-[0.65rem] uppercase tracking-wider mt-0.5 opacity-90">
+ {etaUnit}
+ </span>
</div>
</div>
</Tag>
@@ -346,16 +376,17 @@ export const ConsolidatedCirculationCard: React.FC<
return (
<Tag
- className={`consolidated-circulation-card ${readonly
- ? looksDisabled
- ? "no-gps"
- : ""
- : isClickable
- ? "has-gps"
- : looksDisabled
+ className={`consolidated-circulation-card ${
+ readonly
+ ? looksDisabled
? "no-gps"
: ""
- }`}
+ : isClickable
+ ? "has-gps"
+ : looksDisabled
+ ? "no-gps"
+ : ""
+ }`}
{...interactiveProps}
>
<>
@@ -365,8 +396,10 @@ export const ConsolidatedCirculationCard: React.FC<
</div>
<div className="route-info">
<strong className="uppercase">
- {driver === 'renfe' && estimate.schedule?.tripId && (
- <span className="font-mono text-slate-500 mr-2 text-[0.9em]">{estimate.schedule.tripId}</span>
+ {driver === "renfe" && estimate.schedule?.tripId && (
+ <span className="font-mono text-slate-500 mr-2 text-[0.9em]">
+ {estimate.schedule.tripId}
+ </span>
)}
{estimate.route}
</strong>
@@ -389,8 +422,12 @@ export const ConsolidatedCirculationCard: React.FC<
key={`${chip.label}-${idx}`}
className={`meta-chip ${chip.tone ?? ""}`.trim()}
>
- {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)}
- {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)}
+ {chip.kind === "gps" && (
+ <LocateIcon className="w-3 h-3 inline-block" />
+ )}
+ {chip.kind === "warning" && (
+ <AlertTriangle className="w-3 h-3 inline-block" />
+ )}
{chip.label}
</span>
))}
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
index ec79f1c..eea4582 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
@@ -7,17 +7,17 @@ import "./ConsolidatedCirculationList.css";
interface ConsolidatedCirculationListProps {
data: ConsolidatedCirculation[];
- onCirculationClick?: (estimate: ConsolidatedCirculation, index: number) => void;
+ onCirculationClick?: (
+ estimate: ConsolidatedCirculation,
+ index: number
+ ) => void;
reduced?: boolean;
driver?: string;
}
-export const ConsolidatedCirculationList: React.FC<ConsolidatedCirculationListProps> = ({
- data,
- onCirculationClick,
- reduced,
- driver,
-}) => {
+export const ConsolidatedCirculationList: React.FC<
+ ConsolidatedCirculationListProps
+> = ({ data, onCirculationClick, reduced, driver }) => {
const { t } = useTranslation();
const sortedData = [...data].sort(
diff --git a/src/frontend/app/components/ThemeColorManager.tsx b/src/frontend/app/components/ThemeColorManager.tsx
index c138dc9..eba0471 100644
--- a/src/frontend/app/components/ThemeColorManager.tsx
+++ b/src/frontend/app/components/ThemeColorManager.tsx
@@ -9,11 +9,11 @@ export const ThemeColorManager = () => {
let meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
- meta = document.createElement('meta');
- meta.setAttribute('name', 'theme-color');
+ meta = document.createElement("meta");
+ meta.setAttribute("name", "theme-color");
document.head.appendChild(meta);
}
- meta.setAttribute('content', color);
+ meta.setAttribute("content", color);
}, [resolvedTheme]);
return null;
diff --git a/src/frontend/app/components/layout/AppShell.tsx b/src/frontend/app/components/layout/AppShell.tsx
index 91f6c0d..08aee59 100644
--- a/src/frontend/app/components/layout/AppShell.tsx
+++ b/src/frontend/app/components/layout/AppShell.tsx
@@ -1,6 +1,9 @@
import React, { useState } from "react";
import { Outlet } from "react-router";
-import { PageTitleProvider, usePageTitleContext } from "~/contexts/PageTitleContext";
+import {
+ PageTitleProvider,
+ usePageTitleContext,
+} from "~/contexts/PageTitleContext";
import { ThemeColorManager } from "../ThemeColorManager";
import "./AppShell.css";
import { Drawer } from "./Drawer";
diff --git a/src/frontend/app/components/layout/Drawer.css b/src/frontend/app/components/layout/Drawer.css
index 27ccce6..4f6bd5f 100644
--- a/src/frontend/app/components/layout/Drawer.css
+++ b/src/frontend/app/components/layout/Drawer.css
@@ -8,7 +8,9 @@
z-index: 99;
opacity: 0;
visibility: hidden;
- transition: opacity 0.3s ease, visibility 0.3s ease;
+ transition:
+ opacity 0.3s ease,
+ visibility 0.3s ease;
}
.drawer-overlay.open {
diff --git a/src/frontend/app/components/layout/NavBar.tsx b/src/frontend/app/components/layout/NavBar.tsx
index 0ac6a71..40591c4 100644
--- a/src/frontend/app/components/layout/NavBar.tsx
+++ b/src/frontend/app/components/layout/NavBar.tsx
@@ -57,7 +57,7 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) {
updateMapState(coords, 16);
}
},
- () => { },
+ () => {},
{
enableHighAccuracy: false,
maximumAge: 5 * 60 * 1000,
@@ -70,13 +70,14 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) {
name: t("navbar.lines", "Líneas"),
icon: Route,
path: "/lines",
- }
+ },
];
return (
<nav
- className={`${styles.navBar} ${orientation === "vertical" ? styles.vertical : ""
- }`}
+ className={`${styles.navBar} ${
+ orientation === "vertical" ? styles.vertical : ""
+ }`}
>
{navItems.map((item) => {
const Icon = item.icon;
diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts
index 43fe70a..75da06d 100644
--- a/src/frontend/app/config/RegionConfig.ts
+++ b/src/frontend/app/config/RegionConfig.ts
@@ -28,10 +28,7 @@ export const REGION_DATA: RegionData = {
consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations",
timetableEndpoint: "/api/vigo/GetStopTimetable",
shapeEndpoint: "/api/vigo/GetShape",
- defaultCenter: [
- 42.229188855975046,
- -8.72246955783102
- ] as LngLatLike,
+ defaultCenter: [42.229188855975046, -8.72246955783102] as LngLatLike,
bounds: {
sw: [-8.951059, 42.098923] as LngLatLike,
ne: [-8.447748, 42.3496] as LngLatLike,
@@ -42,4 +39,3 @@ export const REGION_DATA: RegionData = {
};
export const getAvailableRegions = (): RegionData[] => [REGION_DATA];
-
diff --git a/src/frontend/app/contexts/PageTitleContext.tsx b/src/frontend/app/contexts/PageTitleContext.tsx
index 396e409..4a13a8a 100644
--- a/src/frontend/app/contexts/PageTitleContext.tsx
+++ b/src/frontend/app/contexts/PageTitleContext.tsx
@@ -24,7 +24,9 @@ export const PageTitleProvider: React.FC<{ children: React.ReactNode }> = ({
export const usePageTitleContext = () => {
const context = useContext(PageTitleContext);
if (!context) {
- throw new Error("usePageTitleContext must be used within a PageTitleProvider");
+ throw new Error(
+ "usePageTitleContext must be used within a PageTitleProvider"
+ );
}
return context;
};
diff --git a/src/frontend/app/data/LineColors.ts b/src/frontend/app/data/LineColors.ts
index 4e5fe8f..d24d870 100644
--- a/src/frontend/app/data/LineColors.ts
+++ b/src/frontend/app/data/LineColors.ts
@@ -1,4 +1,3 @@
-
interface LineColorInfo {
background: string;
text: string;
@@ -61,7 +60,5 @@ export function getLineColour(line: string): LineColorInfo {
let formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`;
formattedLine = formattedLine.toLowerCase().trim();
- return (
- vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor
- );
+ return vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor;
}
diff --git a/src/frontend/app/data/LinesData.ts b/src/frontend/app/data/LinesData.ts
index 13224e6..cd661b3 100644
--- a/src/frontend/app/data/LinesData.ts
+++ b/src/frontend/app/data/LinesData.ts
@@ -1,7 +1,7 @@
export interface LineInfo {
- lineNumber: string;
- routeName: string;
- scheduleUrl: string;
+ lineNumber: string;
+ routeName: string;
+ scheduleUrl: string;
}
/**
@@ -17,236 +17,240 @@ export interface LineInfo {
*/
-
export const VIGO_LINES: LineInfo[] = [
- {
- "lineNumber": "C1",
- "routeName": "P.América - C. Castillo - P.Sanz - G.Via - P.América",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1.pdf"
- },
- {
- "lineNumber": "C3d",
- "routeName": "Bouzas/Coia - E.Fadrique - Encarnación (dereita) - Pza España - Bouzas/Coia",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3001.pdf"
- },
- {
- "lineNumber": "C3i",
- "routeName": "Bouzas/Coia - Pza España - Encarnación (esquerda) - E.Fadrique - Bouzas/Coia",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3002.pdf"
- },
- {
- "lineNumber": "4A",
- "routeName": "Coia - Camelias - Centro - Aragón",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4001.pdf"
- },
- {
- "lineNumber": "4C",
- "routeName": "Coia - Camelias - Centro - M.Garrido - Gregorio Espino",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4003.pdf"
- },
- {
- "lineNumber": "5A",
- "routeName": "Navia - Florida - L.Mora - Urzaiz - T.Vigo - Teis",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/5001.pdf"
- },
- {
- "lineNumber": "5B",
- "routeName": "Navia - Coia - L.Mora - Pi Margall - G.Barbón - Teis",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/5004.pdf"
- },
- {
- "lineNumber": "6",
- "routeName": "H.Cunqueiro - Beade - Bembrive - Pza. España",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/6.pdf"
- },
- {
- "lineNumber": "7",
- "routeName": "Zamans/Valladares - Fragoso - P.América - P.España - Centro",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/7.pdf"
- },
- {
- "lineNumber": "9B",
- "routeName": "Centro - Choróns - San Cristovo - Rabadeira",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/9002.pdf"
- },
- {
- "lineNumber": "10",
- "routeName": "Teis - G.Barbón - Torrecedeira - Av. Atlántida - Samil - Vao - Saiáns",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/10.pdf"
- },
- {
- "lineNumber": "11",
- "routeName": "San Miguel - Vao - P. América - Urzaiz - Ramón Nieto - Grileira",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/11.pdf"
- },
- {
- "lineNumber": "12A",
- "routeName": "Saiáns - Muiños - Castelao - Pi Margall - P.España - H.Meixoeiro",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1201.pdf"
- },
- {
- "lineNumber": "12B",
- "routeName": "H.Cunqueiro - Castrelos - Camelias - P.España - H.Meixoeiro",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1202.pdf"
- },
- {
- "lineNumber": "13",
- "routeName": "Navia - Bouzas - Gran Vía - P.España - H.Meixoeiro",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/13.pdf"
- },
- {
- "lineNumber": "14",
- "routeName": "Gran Vía - Miraflores - Moledo - Chans",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/14.pdf"
- },
- {
- "lineNumber": "15A",
- "routeName": "Av. Ponte - Choróns - Gran Vía - Castelao - Navia - Samil",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1501.pdf"
- },
- {
- "lineNumber": "15B",
- "routeName": "Xestoso - Choróns - P.Sanz - Beiramar - Bouzas - Samil",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1506.pdf"
- },
- {
- "lineNumber": "15C",
- "routeName": "CUVI - Choróns - P.Sanz - Torrecedeira - Bouzas - Samil",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1507.pdf"
- },
- {
- "lineNumber": "16",
- "routeName": "Coia - Balaídos - Zamora - P.España - Colón - Guixar",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/16.pdf"
- },
- {
- "lineNumber": "17",
- "routeName": "Matamá/Freixo - Fragoso - Camelias - G.Barbón - Ríos/A Guía",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/17.pdf"
- },
- {
- "lineNumber": "18A",
- "routeName": "AREAL/COLÓN - SÁRDOMA/POULEIRA",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/18.pdf"
- },
- {
- "lineNumber": "18B",
- "routeName": "URZAIZ / P.ESPAÑA - POULEIRA",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1801.pdf"
- },
- {
- "lineNumber": "18H",
- "routeName": "URZAIZ / P. ESPAÑA - H. ALV. CUNQUEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1802.pdf"
- },
- {
- "lineNumber": "23",
- "routeName": "M. ECHEGARAY - Balaídos - Gran Vía - Choróns - Gregorio Espino",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/23.pdf"
- },
- {
- "lineNumber": "24",
- "routeName": "Poulo - Vía Norte - Colón - Guixar",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/24.pdf"
- },
- {
- "lineNumber": "25",
- "routeName": "PZA. ESPAÑA – SABAXÁNS / CAEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/25.pdf"
- },
- {
- "lineNumber": "27",
- "routeName": "BEADE (C. CULTURAL) – RABADEIRA",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/27.pdf"
- },
- {
- "lineNumber": "28",
- "routeName": "VIGOZOO - SAN PAIO - BOUZAS",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/28.pdf"
- },
- {
- "lineNumber": "29",
- "routeName": "FRAGOSELO / S. ANDRÉS – PZA. ESPAÑA",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/29.pdf"
- },
- {
- "lineNumber": "31",
- "routeName": "SAN LOURENZO – HOSP. MEIXOEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/31.pdf"
- },
- {
- "lineNumber": "A",
- "routeName": "ARENAL – PORTO / UNIVERSIDADE",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/8.pdf"
- },
- {
- "lineNumber": "H",
- "routeName": "NAVIA - BOUZAS - HOSPITAL ALVARO CUNQUEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/104.pdf"
- },
- {
- "lineNumber": "H1",
- "routeName": "POLICARPO SANZ – HOSPITAL ÁLVARO CUNQUEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/101.pdf"
- },
- {
- "lineNumber": "H2",
- "routeName": "GREGORIO ESPINO – HOSPITAL ÁLVARO CUNQU",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/102.pdf"
- },
- {
- "lineNumber": "H3",
- "routeName": "GARCÍA BARBÓN – HOSPITAL ÁLVARO CUNQUEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/105.pdf"
- },
- {
- "lineNumber": "LZD",
- "routeName": "STELLANTIS - ALV. CUNQUEIRO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/751.pdf"
- },
- {
- "lineNumber": "N1",
- "routeName": "SAMIL – BUENOS AIRES",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/30.pdf"
- },
- {
- "lineNumber": "N4",
- "routeName": "NAVIA - G. ESPINO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3305.pdf"
- },
- {
- "lineNumber": "PSA1",
- "routeName": "STELLANTIS - G.BARBON",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/301.pdf"
- },
- {
- "lineNumber": "PSA4",
- "routeName": "STELLANTIS - G. BARBON",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4004.pdf"
- },
- {
- "lineNumber": "PTL",
- "routeName": "PARQUE TECNOLÓXICO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/304.pdf"
- },
- {
- "lineNumber": "TUR",
- "routeName": "TURISTICO",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/500.pdf"
- },
- {
- "lineNumber": "U1",
- "routeName": "LANZADEIRA PZA. AMÉRICA – UNIVERSIDADE",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/201.pdf"
- },
- {
- "lineNumber": "U2",
- "routeName": "LANZADEIRA PZA. DE ESPAÑA – UNIVERSIDADE",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/202.pdf"
- },
- {
- "lineNumber": "VTS",
- "routeName": "CABRAL - BASE",
- "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3010.pdf"
- }
+ {
+ lineNumber: "C1",
+ routeName: "P.América - C. Castillo - P.Sanz - G.Via - P.América",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1.pdf",
+ },
+ {
+ lineNumber: "C3d",
+ routeName:
+ "Bouzas/Coia - E.Fadrique - Encarnación (dereita) - Pza España - Bouzas/Coia",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3001.pdf",
+ },
+ {
+ lineNumber: "C3i",
+ routeName:
+ "Bouzas/Coia - Pza España - Encarnación (esquerda) - E.Fadrique - Bouzas/Coia",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3002.pdf",
+ },
+ {
+ lineNumber: "4A",
+ routeName: "Coia - Camelias - Centro - Aragón",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4001.pdf",
+ },
+ {
+ lineNumber: "4C",
+ routeName: "Coia - Camelias - Centro - M.Garrido - Gregorio Espino",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4003.pdf",
+ },
+ {
+ lineNumber: "5A",
+ routeName: "Navia - Florida - L.Mora - Urzaiz - T.Vigo - Teis",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/5001.pdf",
+ },
+ {
+ lineNumber: "5B",
+ routeName: "Navia - Coia - L.Mora - Pi Margall - G.Barbón - Teis",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/5004.pdf",
+ },
+ {
+ lineNumber: "6",
+ routeName: "H.Cunqueiro - Beade - Bembrive - Pza. España",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/6.pdf",
+ },
+ {
+ lineNumber: "7",
+ routeName: "Zamans/Valladares - Fragoso - P.América - P.España - Centro",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/7.pdf",
+ },
+ {
+ lineNumber: "9B",
+ routeName: "Centro - Choróns - San Cristovo - Rabadeira",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/9002.pdf",
+ },
+ {
+ lineNumber: "10",
+ routeName:
+ "Teis - G.Barbón - Torrecedeira - Av. Atlántida - Samil - Vao - Saiáns",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/10.pdf",
+ },
+ {
+ lineNumber: "11",
+ routeName:
+ "San Miguel - Vao - P. América - Urzaiz - Ramón Nieto - Grileira",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/11.pdf",
+ },
+ {
+ lineNumber: "12A",
+ routeName:
+ "Saiáns - Muiños - Castelao - Pi Margall - P.España - H.Meixoeiro",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1201.pdf",
+ },
+ {
+ lineNumber: "12B",
+ routeName: "H.Cunqueiro - Castrelos - Camelias - P.España - H.Meixoeiro",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1202.pdf",
+ },
+ {
+ lineNumber: "13",
+ routeName: "Navia - Bouzas - Gran Vía - P.España - H.Meixoeiro",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/13.pdf",
+ },
+ {
+ lineNumber: "14",
+ routeName: "Gran Vía - Miraflores - Moledo - Chans",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/14.pdf",
+ },
+ {
+ lineNumber: "15A",
+ routeName: "Av. Ponte - Choróns - Gran Vía - Castelao - Navia - Samil",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1501.pdf",
+ },
+ {
+ lineNumber: "15B",
+ routeName: "Xestoso - Choróns - P.Sanz - Beiramar - Bouzas - Samil",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1506.pdf",
+ },
+ {
+ lineNumber: "15C",
+ routeName: "CUVI - Choróns - P.Sanz - Torrecedeira - Bouzas - Samil",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1507.pdf",
+ },
+ {
+ lineNumber: "16",
+ routeName: "Coia - Balaídos - Zamora - P.España - Colón - Guixar",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/16.pdf",
+ },
+ {
+ lineNumber: "17",
+ routeName: "Matamá/Freixo - Fragoso - Camelias - G.Barbón - Ríos/A Guía",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/17.pdf",
+ },
+ {
+ lineNumber: "18A",
+ routeName: "AREAL/COLÓN - SÁRDOMA/POULEIRA",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/18.pdf",
+ },
+ {
+ lineNumber: "18B",
+ routeName: "URZAIZ / P.ESPAÑA - POULEIRA",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1801.pdf",
+ },
+ {
+ lineNumber: "18H",
+ routeName: "URZAIZ / P. ESPAÑA - H. ALV. CUNQUEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1802.pdf",
+ },
+ {
+ lineNumber: "23",
+ routeName: "M. ECHEGARAY - Balaídos - Gran Vía - Choróns - Gregorio Espino",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/23.pdf",
+ },
+ {
+ lineNumber: "24",
+ routeName: "Poulo - Vía Norte - Colón - Guixar",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/24.pdf",
+ },
+ {
+ lineNumber: "25",
+ routeName: "PZA. ESPAÑA – SABAXÁNS / CAEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/25.pdf",
+ },
+ {
+ lineNumber: "27",
+ routeName: "BEADE (C. CULTURAL) – RABADEIRA",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/27.pdf",
+ },
+ {
+ lineNumber: "28",
+ routeName: "VIGOZOO - SAN PAIO - BOUZAS",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/28.pdf",
+ },
+ {
+ lineNumber: "29",
+ routeName: "FRAGOSELO / S. ANDRÉS – PZA. ESPAÑA",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/29.pdf",
+ },
+ {
+ lineNumber: "31",
+ routeName: "SAN LOURENZO – HOSP. MEIXOEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/31.pdf",
+ },
+ {
+ lineNumber: "A",
+ routeName: "ARENAL – PORTO / UNIVERSIDADE",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/8.pdf",
+ },
+ {
+ lineNumber: "H",
+ routeName: "NAVIA - BOUZAS - HOSPITAL ALVARO CUNQUEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/104.pdf",
+ },
+ {
+ lineNumber: "H1",
+ routeName: "POLICARPO SANZ – HOSPITAL ÁLVARO CUNQUEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/101.pdf",
+ },
+ {
+ lineNumber: "H2",
+ routeName: "GREGORIO ESPINO – HOSPITAL ÁLVARO CUNQU",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/102.pdf",
+ },
+ {
+ lineNumber: "H3",
+ routeName: "GARCÍA BARBÓN – HOSPITAL ÁLVARO CUNQUEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/105.pdf",
+ },
+ {
+ lineNumber: "LZD",
+ routeName: "STELLANTIS - ALV. CUNQUEIRO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/751.pdf",
+ },
+ {
+ lineNumber: "N1",
+ routeName: "SAMIL – BUENOS AIRES",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/30.pdf",
+ },
+ {
+ lineNumber: "N4",
+ routeName: "NAVIA - G. ESPINO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3305.pdf",
+ },
+ {
+ lineNumber: "PSA1",
+ routeName: "STELLANTIS - G.BARBON",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/301.pdf",
+ },
+ {
+ lineNumber: "PSA4",
+ routeName: "STELLANTIS - G. BARBON",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4004.pdf",
+ },
+ {
+ lineNumber: "PTL",
+ routeName: "PARQUE TECNOLÓXICO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/304.pdf",
+ },
+ {
+ lineNumber: "TUR",
+ routeName: "TURISTICO",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/500.pdf",
+ },
+ {
+ lineNumber: "U1",
+ routeName: "LANZADEIRA PZA. AMÉRICA – UNIVERSIDADE",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/201.pdf",
+ },
+ {
+ lineNumber: "U2",
+ routeName: "LANZADEIRA PZA. DE ESPAÑA – UNIVERSIDADE",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/202.pdf",
+ },
+ {
+ lineNumber: "VTS",
+ routeName: "CABRAL - BASE",
+ scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3010.pdf",
+ },
];
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index 920c7e1..e523bd1 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -12,7 +12,7 @@ export type StopName = {
export interface Stop {
stopId: string;
- type?: 'bus' | 'train';
+ type?: "bus" | "train";
name: StopName;
latitude?: number;
longitude?: number;
@@ -35,7 +35,7 @@ const customNamesByRegion: Record<string, Record<string, string>> = {};
// Helper to normalize ID
function normalizeId(id: number | string): string {
const s = String(id);
- if (s.includes(':')) return s;
+ if (s.includes(":")) return s;
return `vitrasa:${s}`;
}
@@ -52,8 +52,8 @@ async function initStops() {
const entry = {
...raw,
stopId: id,
- type: raw.type || (id.startsWith('renfe:') ? 'train' : 'bus'),
- favourite: false
+ type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"),
+ favourite: false,
} as Stop;
stopsMapByRegion[REGION_DATA.id][id] = entry;
return entry;
@@ -65,7 +65,7 @@ async function initStops() {
const parsed = JSON.parse(rawCustom);
const normalized: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed)) {
- normalized[normalizeId(key)] = value as string;
+ normalized[normalizeId(key)] = value as string;
}
customNamesByRegion[REGION_DATA.id] = normalized;
} else {
@@ -78,7 +78,9 @@ async function getStops(): Promise<Stop[]> {
await initStops();
// update favourites
const rawFav = localStorage.getItem("favouriteStops_vigo");
- const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : [];
+ const favouriteStops = rawFav
+ ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId)
+ : [];
cachedStopsByRegion["vigo"]!.forEach(
(stop) => (stop.favourite = favouriteStops.includes(stop.stopId))
@@ -87,15 +89,15 @@ async function getStops(): Promise<Stop[]> {
}
// New: get single stop by id
-async function getStopById(
- stopId: string | number
-): Promise<Stop | undefined> {
+async function getStopById(stopId: string | number): Promise<Stop | undefined> {
await initStops();
const id = normalizeId(stopId);
const stop = stopsMapByRegion[REGION_DATA.id]?.[id];
if (stop) {
const rawFav = localStorage.getItem(`favouriteStops_${REGION_DATA.id}`);
- const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : [];
+ const favouriteStops = rawFav
+ ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId)
+ : [];
stop.favourite = favouriteStops.includes(id);
}
return stop;
@@ -144,15 +146,14 @@ function addFavourite(stopId: string | number) {
const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`);
let favouriteStops: string[] = [];
if (rawFavouriteStops) {
- favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId);
+ favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
+ normalizeId
+ );
}
if (!favouriteStops.includes(id)) {
favouriteStops.push(id);
- localStorage.setItem(
- `favouriteStops_vigo`,
- JSON.stringify(favouriteStops)
- );
+ localStorage.setItem(`favouriteStops_vigo`, JSON.stringify(favouriteStops));
}
}
@@ -161,7 +162,9 @@ function removeFavourite(stopId: string | number) {
const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`);
let favouriteStops: string[] = [];
if (rawFavouriteStops) {
- favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId);
+ favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
+ normalizeId
+ );
}
const newFavouriteStops = favouriteStops.filter((sid) => sid !== id);
@@ -175,7 +178,9 @@ function isFavourite(stopId: string | number): boolean {
const id = normalizeId(stopId);
const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`);
if (rawFavouriteStops) {
- const favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId);
+ const favouriteStops = (
+ JSON.parse(rawFavouriteStops) as (number | string)[]
+ ).map(normalizeId);
return favouriteStops.includes(id);
}
return false;
@@ -188,7 +193,9 @@ function pushRecent(stopId: string | number) {
const rawRecentStops = localStorage.getItem(`recentStops_vigo`);
let recentStops: Set<string> = new Set();
if (rawRecentStops) {
- recentStops = new Set((JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId));
+ recentStops = new Set(
+ (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId)
+ );
}
recentStops.add(id);
@@ -207,7 +214,7 @@ function pushRecent(stopId: string | number) {
function getRecent(): string[] {
const rawRecentStops = localStorage.getItem(`recentStops_vigo`);
if (rawRecentStops) {
- return (JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId);
+ return (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId);
}
return [];
}
@@ -215,7 +222,9 @@ function getRecent(): string[] {
function getFavouriteIds(): string[] {
const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`);
if (rawFavouriteStops) {
- return (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId);
+ return (JSON.parse(rawFavouriteStops) as (number | string)[]).map(
+ normalizeId
+ );
}
return [];
}
@@ -225,13 +234,13 @@ async function loadStopsFromNetwork(): Promise<Stop[]> {
const response = await fetch(REGION_DATA.stopsEndpoint);
const rawStops = (await response.json()) as any[];
return rawStops.map((raw) => {
- const id = normalizeId(raw.stopId);
- return {
- ...raw,
- stopId: id,
- type: raw.type || (id.startsWith('renfe:') ? 'train' : 'bus'),
- favourite: false
- } as Stop;
+ const id = normalizeId(raw.stopId);
+ return {
+ ...raw,
+ stopId: id,
+ type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"),
+ favourite: false,
+ } as Stop;
});
}
diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css
index 72eecff..367fa29 100644
--- a/src/frontend/app/root.css
+++ b/src/frontend/app/root.css
@@ -45,7 +45,9 @@
--error-message-color: #7f8c8d;
color-scheme: light;
- font-family: ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ font-family:
+ ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
+ "Segoe UI Symbol", "Noto Color Emoji";
}
[data-theme="dark"] {
diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx
index 7bf07a0..9c105f0 100644
--- a/src/frontend/app/root.tsx
+++ b/src/frontend/app/root.tsx
@@ -3,7 +3,7 @@ import {
Links,
Meta,
Scripts,
- ScrollRestoration
+ ScrollRestoration,
} from "react-router";
import "@fontsource-variable/roboto";
diff --git a/src/frontend/app/routes/about.tsx b/src/frontend/app/routes/about.tsx
index 1354e8f..5158330 100644
--- a/src/frontend/app/routes/about.tsx
+++ b/src/frontend/app/routes/about.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { usePageTitle } from "~/contexts/PageTitleContext";
-import '../tailwind-full.css';
+import "../tailwind-full.css";
export default function About() {
const { t } = useTranslation();
@@ -37,7 +37,9 @@ export default function About() {
<strong className="text-[--text-color] min-w-fit">
{t("about.data_realtime")}:
</strong>
- <span className="opacity-80">{t("about.data_realtime_source")}</span>
+ <span className="opacity-80">
+ {t("about.data_realtime_source")}
+ </span>
</li>
<li className="flex flex-col sm:flex-row sm:items-start gap-1">
<strong className="text-[--text-color] min-w-fit">
@@ -89,9 +91,7 @@ export default function About() {
</div>
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg">
- <p className="text-sm leading-relaxed">
- {t("about.thanks_council")}
- </p>
+ <p className="text-sm leading-relaxed">{t("about.thanks_council")}</p>
</div>
</section>
@@ -119,8 +119,17 @@ export default function About() {
rel="nofollow noreferrer noopener"
target="_blank"
>
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
- <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
+ <svg
+ className="w-5 h-5"
+ fill="currentColor"
+ viewBox="0 0 24 24"
+ aria-hidden="true"
+ >
+ <path
+ fillRule="evenodd"
+ d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
+ clipRule="evenodd"
+ />
</svg>
GitHub
</a>
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index f97fdf7..7c13da6 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -217,7 +217,9 @@ export default function StopList() {
if (isNumericSearch) {
// Direct match for stop codes
const stopId = searchQuery.trim();
- const exactMatch = data.filter((stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`));
+ const exactMatch = data.filter(
+ (stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`)
+ );
if (exactMatch.length > 0) {
items = exactMatch;
} else {
@@ -281,7 +283,9 @@ export default function StopList() {
{/* Favourites Gallery */}
{!loading && (
<StopGallery
- stops={favouriteStops.sort((a, b) => a.stopId.localeCompare(b.stopId))}
+ stops={favouriteStops.sort((a, b) =>
+ a.stopId.localeCompare(b.stopId)
+ )}
title={t("stoplist.favourites")}
emptyMessage={t("stoplist.no_favourites")}
/>
@@ -301,9 +305,24 @@ export default function StopList() {
<div className="w-full px-4 flex flex-col gap-2">
<div className="flex items-center gap-2">
{userLocation && (
- <svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
+ <svg
+ className="w-5 h-5 text-blue-600 dark:text-blue-400"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
+ />
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth={2}
+ d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
+ />
</svg>
)}
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
@@ -322,9 +341,10 @@ export default function StopList() {
</>
)}
{!loading && data
- ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map(
- (stop) => <StopItem key={stop.stopId} stop={stop} />
- )
+ ? (userLocation
+ ? sortedAllStops.slice(0, 6)
+ : sortedAllStops
+ ).map((stop) => <StopItem key={stop.stopId} stop={stop} />)
: null}
</ul>
</div>
diff --git a/src/frontend/app/routes/lines.tsx b/src/frontend/app/routes/lines.tsx
index 658716f..acf8a7f 100644
--- a/src/frontend/app/routes/lines.tsx
+++ b/src/frontend/app/routes/lines.tsx
@@ -2,36 +2,39 @@ import { useTranslation } from "react-i18next";
import LineIcon from "~/components/LineIcon";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { VIGO_LINES } from "~/data/LinesData";
-import '../tailwind-full.css';
+import "../tailwind-full.css";
export default function LinesPage() {
- const { t } = useTranslation();
- usePageTitle(t("navbar.lines", "Líneas"));
+ const { t } = useTranslation();
+ usePageTitle(t("navbar.lines", "Líneas"));
- return (
- <div className="container mx-auto px-4 py-6">
- <p className="mb-6 text-gray-700 dark:text-gray-300">
- {t("lines.description", "A continuación se muestra una lista de las líneas de autobús urbano de Vigo con sus respectivas rutas y enlaces a los horarios oficiales.")}
- </p>
+ return (
+ <div className="container mx-auto px-4 py-6">
+ <p className="mb-6 text-gray-700 dark:text-gray-300">
+ {t(
+ "lines.description",
+ "A continuación se muestra una lista de las líneas de autobús urbano de Vigo con sus respectivas rutas y enlaces a los horarios oficiales."
+ )}
+ </p>
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {VIGO_LINES.map((line) => (
- <a
- key={line.lineNumber}
- href={line.scheduleUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700"
- >
- <LineIcon line={line.lineNumber} mode="rounded" />
- <div className="flex-1 min-w-0">
- <p className="text-sm md:text-md font-semibold text-gray-900 dark:text-gray-100">
- {line.routeName}
- </p>
- </div>
- </a>
- ))}
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {VIGO_LINES.map((line) => (
+ <a
+ key={line.lineNumber}
+ href={line.scheduleUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700"
+ >
+ <LineIcon line={line.lineNumber} mode="rounded" />
+ <div className="flex-1 min-w-0">
+ <p className="text-sm md:text-md font-semibold text-gray-900 dark:text-gray-100">
+ {line.routeName}
+ </p>
</div>
- </div>
- );
+ </a>
+ ))}
+ </div>
+ </div>
+ );
}
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index 402bf60..187e9f2 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -12,7 +12,7 @@ import Map, {
Source,
type MapLayerMouseEvent,
type MapRef,
- type StyleSpecification
+ type StyleSpecification,
} from "react-map-gl/maplibre";
import { StopSheet } from "~/components/StopSummarySheet";
import { REGION_DATA } from "~/config/RegionConfig";
@@ -35,7 +35,13 @@ export default function StopMap() {
const [stops, setStops] = useState<
GeoJsonFeature<
Point,
- { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string }
+ {
+ stopId: string;
+ name: string;
+ lines: string[];
+ cancelled?: boolean;
+ prefix: string;
+ }
>[]
>([]);
const [selectedStop, setSelectedStop] = useState<Stop | null>(null);
@@ -51,7 +57,12 @@ export default function StopMap() {
const onMapClick = (e: MapLayerMouseEvent) => {
const features = e.features;
if (!features || features.length === 0) {
- console.debug("No features found on map click. Position:", e.lngLat, "Point:", e.point);
+ console.debug(
+ "No features found on map click. Position:",
+ e.lngLat,
+ "Point:",
+ e.point
+ );
return;
}
const feature = features[0];
@@ -65,7 +76,13 @@ export default function StopMap() {
StopDataProvider.getStops().then((data) => {
const features: GeoJsonFeature<
Point,
- { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string }
+ {
+ stopId: string;
+ name: string;
+ lines: string[];
+ cancelled?: boolean;
+ prefix: string;
+ }
>[] = data.map((s) => ({
type: "Feature",
geometry: {
@@ -77,7 +94,11 @@ export default function StopMap() {
name: s.name.original,
lines: s.lines,
cancelled: s.cancelled ?? false,
- prefix: s.stopId.startsWith("renfe:") ? "stop-renfe" : (s.cancelled ? "stop-vitrasa-cancelled" : "stop-vitrasa"),
+ prefix: s.stopId.startsWith("renfe:")
+ ? "stop-renfe"
+ : s.cancelled
+ ? "stop-vitrasa-cancelled"
+ : "stop-vitrasa",
},
}));
setStops(features);
@@ -190,7 +211,11 @@ export default function StopMap() {
maxBounds={[REGION_DATA.bounds.sw, REGION_DATA.bounds.ne]}
>
<NavigationControl position="top-right" />
- <GeolocateControl position="top-right" trackUserLocation={true} positionOptions={{ enableHighAccuracy: false }} />
+ <GeolocateControl
+ position="top-right"
+ trackUserLocation={true}
+ positionOptions={{ enableHighAccuracy: false }}
+ />
<Source
id="stops-source"
@@ -204,10 +229,7 @@ export default function StopMap() {
minzoom={11}
source="stops-source"
layout={{
- "icon-image": [
- "get",
- "prefix"
- ],
+ "icon-image": ["get", "prefix"],
"icon-size": [
"interpolate",
["linear"],
@@ -242,22 +264,20 @@ export default function StopMap() {
"case",
["==", ["get", "prefix"], "stop-renfe"],
"#870164",
- "#e72b37"
+ "#e72b37",
],
"text-halo-color": "#FFF",
"text-halo-width": 1,
}}
/>
- {
- selectedStop && (
- <StopSheet
- isOpen={isSheetOpen}
- onClose={() => setIsSheetOpen(false)}
- stop={selectedStop}
- />
- )
- }
- </Map >
+ {selectedStop && (
+ <StopSheet
+ isOpen={isSheetOpen}
+ onClose={() => setIsSheetOpen(false)}
+ stop={selectedStop}
+ />
+ )}
+ </Map>
);
}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index 9b4625f..56df777 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -2,22 +2,29 @@ import { Computer, Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { useApp, type Theme } from "../AppContext";
-import '../tailwind-full.css';
+import "../tailwind-full.css";
export default function Settings() {
const { t, i18n } = useTranslation();
usePageTitle(t("navbar.settings", "Ajustes"));
- const {
- theme,
- setTheme,
- mapPositionMode,
- setMapPositionMode
- } = useApp();
+ const { theme, setTheme, mapPositionMode, setMapPositionMode } = useApp();
const THEMES = [
- { value: "light" as Theme, label: t("about.theme_light", "Claro"), icon: Sun },
- { value: "dark" as Theme, label: t("about.theme_dark", "Oscuro"), icon: Moon },
- { value: "system" as Theme, label: t("about.theme_system", "Sistema"), icon: Computer },
+ {
+ value: "light" as Theme,
+ label: t("about.theme_light", "Claro"),
+ icon: Sun,
+ },
+ {
+ value: "dark" as Theme,
+ label: t("about.theme_dark", "Oscuro"),
+ icon: Moon,
+ },
+ {
+ value: "system" as Theme,
+ label: t("about.theme_system", "Sistema"),
+ icon: Computer,
+ },
];
return (
@@ -37,9 +44,10 @@ export default function Settings() {
rounded-lg border-2 transition-all duration-200
hover:bg-gray-50 dark:hover:bg-gray-800
focus:outline-none focus:ring focus:ring-blue-500 dark:focus:ring-offset-gray-900
- ${value === theme
- ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 font-semibold"
- : "border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300"
+ ${
+ value === theme
+ ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 font-semibold"
+ : "border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300"
}
`}
>
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 553b8e7..6d06215 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -198,9 +198,7 @@ export default function Estimates() {
loadData();
StopDataProvider.pushRecent(stopId);
- setFavourited(
- StopDataProvider.isFavourite(stopId)
- );
+ setFavourited(StopDataProvider.isFavourite(stopId));
setDataLoading(false);
}, [stopId, loadData]);
@@ -246,34 +244,48 @@ export default function Estimates() {
<div className="flex items-center justify-between py-2">
<div className="flex items-center gap-8">
<Star
- className={`cursor-pointer transition-colors ${favourited
- ? "fill-[var(--star-color)] text-[var(--star-color)]"
- : "text-slate-500"
- }`}
+ className={`cursor-pointer transition-colors ${
+ favourited
+ ? "fill-[var(--star-color)] text-[var(--star-color)]"
+ : "text-slate-500"
+ }`}
onClick={toggleFavourite}
/>
- <CircleHelp className="text-slate-500 cursor-pointer" onClick={() => setIsHelpModalOpen(true)} />
+ <CircleHelp
+ className="text-slate-500 cursor-pointer"
+ onClick={() => setIsHelpModalOpen(true)}
+ />
</div>
<div className="consolidated-circulation-caption">
- {t("estimates.caption", "Estimaciones de llegadas a las {{time}}", {
- time: dataDate?.toLocaleTimeString(),
- })}
+ {t(
+ "estimates.caption",
+ "Estimaciones de llegadas a las {{time}}",
+ {
+ time: dataDate?.toLocaleTimeString(),
+ }
+ )}
</div>
<div>
{isReducedView ? (
- <EyeClosed className="text-slate-500" onClick={() => setIsReducedView(false)} />
+ <EyeClosed
+ className="text-slate-500"
+ onClick={() => setIsReducedView(false)}
+ />
) : (
- <Eye className="text-slate-500" onClick={() => setIsReducedView(true)} />
+ <Eye
+ className="text-slate-500"
+ onClick={() => setIsReducedView(true)}
+ />
)}
</div>
</div>
<ConsolidatedCirculationList
data={data}
reduced={isReducedView}
- driver={stopData?.stopId.split(':')[0]}
+ driver={stopData?.stopId.split(":")[0]}
onCirculationClick={(estimate, idx) => {
setSelectedCirculationId(getCirculationId(estimate));
setIsMapModalOpen(true);
@@ -295,8 +307,8 @@ export default function Estimates() {
previousTripShapeId: c.previousTripShapeId,
schedule: c.schedule
? {
- shapeId: c.schedule.shapeId,
- }
+ shapeId: c.schedule.shapeId,
+ }
: undefined,
}))}
isOpen={isMapModalOpen}
diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts
index b827847..042177d 100644
--- a/src/frontend/vite.config.ts
+++ b/src/frontend/vite.config.ts
@@ -11,11 +11,7 @@ export default defineConfig({
define: {
__COMMIT_HASH__: JSON.stringify(commitHash),
},
- plugins: [
- reactRouter(),
- tsconfigPaths(),
- tailwindcss()
- ],
+ plugins: [reactRouter(), tsconfigPaths(), tailwindcss()],
server: {
proxy: {
"^/api": {
diff --git a/src/gtfs_perstop_report/src/common.py b/src/gtfs_perstop_report/src/common.py
index 22769e4..c2df785 100644
--- a/src/gtfs_perstop_report/src/common.py
+++ b/src/gtfs_perstop_report/src/common.py
@@ -40,7 +40,6 @@ def get_all_feed_dates(feed_dir: str) -> List[str]:
if len(result) > 0:
return result
-
# Fallback: use calendar_dates.txt
if os.path.exists(calendar_dates_path):
with open(calendar_dates_path, encoding="utf-8") as f:
diff --git a/src/gtfs_perstop_report/src/download.py b/src/gtfs_perstop_report/src/download.py
index 19125bc..4d0c620 100644
--- a/src/gtfs_perstop_report/src/download.py
+++ b/src/gtfs_perstop_report/src/download.py
@@ -9,39 +9,44 @@ from src.logger import get_logger
logger = get_logger("download")
+
def _get_metadata_path(output_dir: str) -> str:
"""Get the path to the metadata file for storing ETag and Last-Modified info."""
- return os.path.join(output_dir, '.gtfsmetadata')
+ return os.path.join(output_dir, ".gtfsmetadata")
+
def _load_metadata(output_dir: str) -> Optional[dict]:
"""Load existing metadata from the output directory."""
metadata_path = _get_metadata_path(output_dir)
if os.path.exists(metadata_path):
try:
- with open(metadata_path, 'r', encoding='utf-8') as f:
+ with open(metadata_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to load metadata from {metadata_path}: {e}")
return None
-def _save_metadata(output_dir: str, etag: Optional[str], last_modified: Optional[str]) -> None:
+
+def _save_metadata(
+ output_dir: str, etag: Optional[str], last_modified: Optional[str]
+) -> None:
"""Save ETag and Last-Modified metadata to the output directory."""
metadata_path = _get_metadata_path(output_dir)
- metadata = {
- 'etag': etag,
- 'last_modified': last_modified
- }
-
+ metadata = {"etag": etag, "last_modified": last_modified}
+
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
-
+
try:
- with open(metadata_path, 'w', encoding='utf-8') as f:
+ with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, indent=2)
except IOError as e:
logger.warning(f"Failed to save metadata to {metadata_path}: {e}")
-def _check_if_modified(feed_url: str, output_dir: str) -> Tuple[bool, Optional[str], Optional[str]]:
+
+def _check_if_modified(
+ feed_url: str, output_dir: str
+) -> Tuple[bool, Optional[str], Optional[str]]:
"""
Check if the feed has been modified using conditional headers.
Returns (is_modified, etag, last_modified)
@@ -49,58 +54,69 @@ def _check_if_modified(feed_url: str, output_dir: str) -> Tuple[bool, Optional[s
metadata = _load_metadata(output_dir)
if not metadata:
return True, None, None
-
+
headers = {}
- if metadata.get('etag'):
- headers['If-None-Match'] = metadata['etag']
- if metadata.get('last_modified'):
- headers['If-Modified-Since'] = metadata['last_modified']
-
+ if metadata.get("etag"):
+ headers["If-None-Match"] = metadata["etag"]
+ if metadata.get("last_modified"):
+ headers["If-Modified-Since"] = metadata["last_modified"]
+
if not headers:
return True, None, None
-
+
try:
response = requests.head(feed_url, headers=headers)
-
+
if response.status_code == 304:
- logger.info("Feed has not been modified (304 Not Modified), skipping download")
- return False, metadata.get('etag'), metadata.get('last_modified')
+ logger.info(
+ "Feed has not been modified (304 Not Modified), skipping download"
+ )
+ return False, metadata.get("etag"), metadata.get("last_modified")
elif response.status_code == 200:
- etag = response.headers.get('ETag')
- last_modified = response.headers.get('Last-Modified')
+ etag = response.headers.get("ETag")
+ last_modified = response.headers.get("Last-Modified")
return True, etag, last_modified
else:
- logger.warning(f"Unexpected response status {response.status_code} when checking for modifications, proceeding with download")
+ logger.warning(
+ f"Unexpected response status {response.status_code} when checking for modifications, proceeding with download"
+ )
return True, None, None
except requests.RequestException as e:
- logger.warning(f"Failed to check if feed has been modified: {e}, proceeding with download")
+ logger.warning(
+ f"Failed to check if feed has been modified: {e}, proceeding with download"
+ )
return True, None, None
-def download_feed_from_url(feed_url: str, output_dir: str = None, force_download: bool = False) -> Optional[str]:
+
+def download_feed_from_url(
+ feed_url: str, output_dir: str = None, force_download: bool = False
+) -> Optional[str]:
"""
Download GTFS feed from URL.
-
+
Args:
feed_url: URL to download the GTFS feed from
output_dir: Directory where reports will be written (used for metadata storage)
force_download: If True, skip conditional download checks
-
+
Returns:
Path to the directory containing the extracted GTFS files, or None if download was skipped
"""
-
+
# Check if we need to download the feed
if not force_download and output_dir:
- is_modified, cached_etag, cached_last_modified = _check_if_modified(feed_url, output_dir)
+ is_modified, cached_etag, cached_last_modified = _check_if_modified(
+ feed_url, output_dir
+ )
if not is_modified:
logger.info("Feed has not been modified, skipping download")
return None
-
+
# Create a directory in the system temporary directory
- temp_dir = tempfile.mkdtemp(prefix='gtfs_vigo_')
+ temp_dir = tempfile.mkdtemp(prefix="gtfs_vigo_")
# Create a temporary zip file in the temporary directory
- zip_filename = os.path.join(temp_dir, 'gtfs_vigo.zip')
+ zip_filename = os.path.join(temp_dir, "gtfs_vigo.zip")
headers = {}
response = requests.get(feed_url, headers=headers)
@@ -108,23 +124,23 @@ def download_feed_from_url(feed_url: str, output_dir: str = None, force_download
if response.status_code != 200:
raise Exception(f"Failed to download GTFS data: {response.status_code}")
- with open(zip_filename, 'wb') as file:
+ with open(zip_filename, "wb") as file:
file.write(response.content)
-
+
# Extract and save metadata if output_dir is provided
if output_dir:
- etag = response.headers.get('ETag')
- last_modified = response.headers.get('Last-Modified')
+ etag = response.headers.get("ETag")
+ last_modified = response.headers.get("Last-Modified")
if etag or last_modified:
_save_metadata(output_dir, etag, last_modified)
# Extract the zip file
- with zipfile.ZipFile(zip_filename, 'r') as zip_ref:
+ with zipfile.ZipFile(zip_filename, "r") as zip_ref:
zip_ref.extractall(temp_dir)
-
+
# Clean up the downloaded zip file
os.remove(zip_filename)
logger.info(f"GTFS feed downloaded from {feed_url} and extracted to {temp_dir}")
- return temp_dir \ No newline at end of file
+ return temp_dir
diff --git a/src/gtfs_perstop_report/src/logger.py b/src/gtfs_perstop_report/src/logger.py
index 9488076..6c56787 100644
--- a/src/gtfs_perstop_report/src/logger.py
+++ b/src/gtfs_perstop_report/src/logger.py
@@ -1,12 +1,14 @@
"""
Logging configuration for the GTFS application.
"""
+
import logging
from colorama import init, Fore, Style
# Initialize Colorama (required on Windows)
init(autoreset=True)
+
class ColorFormatter(logging.Formatter):
def format(self, record: logging.LogRecord):
# Base format
@@ -28,16 +30,18 @@ class ColorFormatter(logging.Formatter):
# Add color to the entire line
formatter = logging.Formatter(
- prefix + log_format + Style.RESET_ALL, "%Y-%m-%d %H:%M:%S")
+ prefix + log_format + Style.RESET_ALL, "%Y-%m-%d %H:%M:%S"
+ )
return formatter.format(record)
+
def get_logger(name: str) -> logging.Logger:
"""
Create and return a logger with the given name.
-
+
Args:
name (str): The name of the logger.
-
+
Returns:
logging.Logger: Configured logger instance.
"""
@@ -50,5 +54,5 @@ def get_logger(name: str) -> logging.Logger:
console_handler.setLevel(logging.DEBUG)
console_handler.setFormatter(ColorFormatter())
logger.addHandler(console_handler)
-
+
return logger
diff --git a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py
index cb4f336..c7279c5 100644
--- a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py
+++ b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.py
@@ -2,6 +2,7 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: stop_schedule.proto
"""Generated protocol buffer code."""
+
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,22 +12,21 @@ from google.protobuf import symbol_database as _symbol_database
_sym_db = _symbol_database.Default()
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13stop_schedule.proto\x12\x05proto\"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\x83\x04\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12\"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\x85\x03\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18\" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\t\x12\x1e\n\x16previous_trip_shape_id\x18\x33 \x01(\t\";\n\x05Shape\x12\x10\n\x08shape_id\x18\x01 \x01(\t\x12 \n\x06points\x18\x03 \x03(\x0b\x32\x10.proto.Epsg25829B$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
+ b'\n\x13stop_schedule.proto\x12\x05proto"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01"\x83\x04\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\x85\x03\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\t\x12\x1e\n\x16previous_trip_shape_id\x18\x33 \x01(\t";\n\x05Shape\x12\x10\n\x08shape_id\x18\x01 \x01(\t\x12 \n\x06points\x18\x03 \x03(\x0b\x32\x10.proto.Epsg25829B$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3'
+)
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'stop_schedule_pb2', globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "stop_schedule_pb2", globals())
if _descriptor._USE_C_DESCRIPTORS == False:
-
- DESCRIPTOR._options = None
- DESCRIPTOR._serialized_options = b'\252\002!Costasdev.Busurbano.Backend.Types'
- _EPSG25829._serialized_start=30
- _EPSG25829._serialized_end=63
- _STOPARRIVALS._serialized_start=66
- _STOPARRIVALS._serialized_end=581
- _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_start=192
- _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_end=581
- _SHAPE._serialized_start=583
- _SHAPE._serialized_end=642
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b"\252\002!Costasdev.Busurbano.Backend.Types"
+ _EPSG25829._serialized_start = 30
+ _EPSG25829._serialized_end = 63
+ _STOPARRIVALS._serialized_start = 66
+ _STOPARRIVALS._serialized_end = 581
+ _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_start = 192
+ _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_end = 581
+ _SHAPE._serialized_start = 583
+ _SHAPE._serialized_end = 642
# @@protoc_insertion_point(module_scope)
diff --git a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi
index 355798f..fc55f4e 100644
--- a/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi
+++ b/src/gtfs_perstop_report/src/proto/stop_schedule_pb2.pyi
@@ -1,7 +1,13 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
-from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
+from typing import (
+ ClassVar as _ClassVar,
+ Iterable as _Iterable,
+ Mapping as _Mapping,
+ Optional as _Optional,
+ Union as _Union,
+)
DESCRIPTOR: _descriptor.FileDescriptor
@@ -11,7 +17,9 @@ class Epsg25829(_message.Message):
Y_FIELD_NUMBER: _ClassVar[int]
x: float
y: float
- def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ...) -> None: ...
+ def __init__(
+ self, x: _Optional[float] = ..., y: _Optional[float] = ...
+ ) -> None: ...
class Shape(_message.Message):
__slots__ = ["points", "shape_id"]
@@ -19,12 +27,34 @@ class Shape(_message.Message):
SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
points: _containers.RepeatedCompositeFieldContainer[Epsg25829]
shape_id: str
- def __init__(self, shape_id: _Optional[str] = ..., points: _Optional[_Iterable[_Union[Epsg25829, _Mapping]]] = ...) -> None: ...
+ def __init__(
+ self,
+ shape_id: _Optional[str] = ...,
+ points: _Optional[_Iterable[_Union[Epsg25829, _Mapping]]] = ...,
+ ) -> None: ...
class StopArrivals(_message.Message):
__slots__ = ["arrivals", "location", "stop_id"]
class ScheduledArrival(_message.Message):
- __slots__ = ["calling_ssm", "calling_time", "line", "next_streets", "previous_trip_shape_id", "route", "service_id", "shape_dist_traveled", "shape_id", "starting_code", "starting_name", "starting_time", "stop_sequence", "terminus_code", "terminus_name", "terminus_time", "trip_id"]
+ __slots__ = [
+ "calling_ssm",
+ "calling_time",
+ "line",
+ "next_streets",
+ "previous_trip_shape_id",
+ "route",
+ "service_id",
+ "shape_dist_traveled",
+ "shape_id",
+ "starting_code",
+ "starting_name",
+ "starting_time",
+ "stop_sequence",
+ "terminus_code",
+ "terminus_name",
+ "terminus_time",
+ "trip_id",
+ ]
CALLING_SSM_FIELD_NUMBER: _ClassVar[int]
CALLING_TIME_FIELD_NUMBER: _ClassVar[int]
LINE_FIELD_NUMBER: _ClassVar[int]
@@ -59,11 +89,38 @@ class StopArrivals(_message.Message):
terminus_name: str
terminus_time: str
trip_id: str
- def __init__(self, service_id: _Optional[str] = ..., trip_id: _Optional[str] = ..., line: _Optional[str] = ..., route: _Optional[str] = ..., shape_id: _Optional[str] = ..., shape_dist_traveled: _Optional[float] = ..., stop_sequence: _Optional[int] = ..., next_streets: _Optional[_Iterable[str]] = ..., starting_code: _Optional[str] = ..., starting_name: _Optional[str] = ..., starting_time: _Optional[str] = ..., calling_time: _Optional[str] = ..., calling_ssm: _Optional[int] = ..., terminus_code: _Optional[str] = ..., terminus_name: _Optional[str] = ..., terminus_time: _Optional[str] = ..., previous_trip_shape_id: _Optional[str] = ...) -> None: ...
+ def __init__(
+ self,
+ service_id: _Optional[str] = ...,
+ trip_id: _Optional[str] = ...,
+ line: _Optional[str] = ...,
+ route: _Optional[str] = ...,
+ shape_id: _Optional[str] = ...,
+ shape_dist_traveled: _Optional[float] = ...,
+ stop_sequence: _Optional[int] = ...,
+ next_streets: _Optional[_Iterable[str]] = ...,
+ starting_code: _Optional[str] = ...,
+ starting_name: _Optional[str] = ...,
+ starting_time: _Optional[str] = ...,
+ calling_time: _Optional[str] = ...,
+ calling_ssm: _Optional[int] = ...,
+ terminus_code: _Optional[str] = ...,
+ terminus_name: _Optional[str] = ...,
+ terminus_time: _Optional[str] = ...,
+ previous_trip_shape_id: _Optional[str] = ...,
+ ) -> None: ...
+
ARRIVALS_FIELD_NUMBER: _ClassVar[int]
LOCATION_FIELD_NUMBER: _ClassVar[int]
STOP_ID_FIELD_NUMBER: _ClassVar[int]
arrivals: _containers.RepeatedCompositeFieldContainer[StopArrivals.ScheduledArrival]
location: Epsg25829
stop_id: str
- def __init__(self, stop_id: _Optional[str] = ..., location: _Optional[_Union[Epsg25829, _Mapping]] = ..., arrivals: _Optional[_Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]] = ...) -> None: ...
+ def __init__(
+ self,
+ stop_id: _Optional[str] = ...,
+ location: _Optional[_Union[Epsg25829, _Mapping]] = ...,
+ arrivals: _Optional[
+ _Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]
+ ] = ...,
+ ) -> None: ...
diff --git a/src/gtfs_perstop_report/src/routes.py b/src/gtfs_perstop_report/src/routes.py
index e67a1a4..06cf0e5 100644
--- a/src/gtfs_perstop_report/src/routes.py
+++ b/src/gtfs_perstop_report/src/routes.py
@@ -1,12 +1,14 @@
"""
Module for loading and querying GTFS routes data.
"""
+
import os
import csv
from src.logger import get_logger
logger = get_logger("routes")
+
def load_routes(feed_dir: str) -> dict[str, dict[str, str]]:
"""
Load routes data from the GTFS feed.
@@ -16,24 +18,26 @@ def load_routes(feed_dir: str) -> dict[str, dict[str, str]]:
containing route_short_name and route_color.
"""
routes: dict[str, dict[str, str]] = {}
- routes_file_path = os.path.join(feed_dir, 'routes.txt')
+ routes_file_path = os.path.join(feed_dir, "routes.txt")
try:
- with open(routes_file_path, 'r', encoding='utf-8') as routes_file:
+ with open(routes_file_path, "r", encoding="utf-8") as routes_file:
reader = csv.DictReader(routes_file)
header = reader.fieldnames or []
- if 'route_color' not in header:
- logger.warning("Column 'route_color' not found in routes.txt. Defaulting to black (#000000).")
+ if "route_color" not in header:
+ logger.warning(
+ "Column 'route_color' not found in routes.txt. Defaulting to black (#000000)."
+ )
for row in reader:
- route_id = row['route_id']
- if 'route_color' in row and row['route_color']:
- route_color = row['route_color']
+ route_id = row["route_id"]
+ if "route_color" in row and row["route_color"]:
+ route_color = row["route_color"]
else:
- route_color = '000000'
+ route_color = "000000"
routes[route_id] = {
- 'route_short_name': row['route_short_name'],
- 'route_color': route_color
+ "route_short_name": row["route_short_name"],
+ "route_color": route_color,
}
except FileNotFoundError:
raise FileNotFoundError(f"Routes file not found at {routes_file_path}")
diff --git a/src/gtfs_perstop_report/src/services.py b/src/gtfs_perstop_report/src/services.py
index fb1110d..d456e43 100644
--- a/src/gtfs_perstop_report/src/services.py
+++ b/src/gtfs_perstop_report/src/services.py
@@ -19,26 +19,28 @@ def get_active_services(feed_dir: str, date: str) -> list[str]:
ValueError: If the date format is incorrect.
"""
search_date = date.replace("-", "").replace(":", "").replace("/", "")
- weekday = datetime.datetime.strptime(date, '%Y-%m-%d').weekday()
+ weekday = datetime.datetime.strptime(date, "%Y-%m-%d").weekday()
active_services: list[str] = []
try:
- with open(os.path.join(feed_dir, 'calendar.txt'), 'r', encoding="utf-8") as calendar_file:
+ with open(
+ os.path.join(feed_dir, "calendar.txt"), "r", encoding="utf-8"
+ ) as calendar_file:
lines = calendar_file.readlines()
if len(lines) > 1:
# First parse the header, get each column's index
- header = lines[0].strip().split(',')
+ header = lines[0].strip().split(",")
try:
- service_id_index = header.index('service_id')
- monday_index = header.index('monday')
- tuesday_index = header.index('tuesday')
- wednesday_index = header.index('wednesday')
- thursday_index = header.index('thursday')
- friday_index = header.index('friday')
- saturday_index = header.index('saturday')
- sunday_index = header.index('sunday')
- start_date_index = header.index('start_date')
- end_date_index = header.index('end_date')
+ service_id_index = header.index("service_id")
+ monday_index = header.index("monday")
+ tuesday_index = header.index("tuesday")
+ wednesday_index = header.index("wednesday")
+ thursday_index = header.index("thursday")
+ friday_index = header.index("friday")
+ saturday_index = header.index("saturday")
+ sunday_index = header.index("sunday")
+ start_date_index = header.index("start_date")
+ end_date_index = header.index("end_date")
except ValueError as e:
logger.error(f"Required column not found in header: {e}")
return active_services
@@ -50,14 +52,15 @@ def get_active_services(feed_dir: str, date: str) -> list[str]:
3: thursday_index,
4: friday_index,
5: saturday_index,
- 6: sunday_index
+ 6: sunday_index,
}
for idx, line in enumerate(lines[1:], 1):
- parts = line.strip().split(',')
+ parts = line.strip().split(",")
if len(parts) < len(header):
logger.warning(
- f"Skipping malformed line in calendar.txt line {idx+1}: {line.strip()}")
+ f"Skipping malformed line in calendar.txt line {idx + 1}: {line.strip()}"
+ )
continue
service_id = parts[service_id_index]
@@ -66,24 +69,27 @@ def get_active_services(feed_dir: str, date: str) -> list[str]:
end_date = parts[end_date_index]
# Check if day of week is active AND date is within the service range
- if day_value == '1' and start_date <= search_date <= end_date:
+ if day_value == "1" and start_date <= search_date <= end_date:
active_services.append(service_id)
except FileNotFoundError:
logger.warning("calendar.txt file not found.")
try:
- with open(os.path.join(feed_dir, 'calendar_dates.txt'), 'r', encoding="utf-8") as calendar_dates_file:
+ with open(
+ os.path.join(feed_dir, "calendar_dates.txt"), "r", encoding="utf-8"
+ ) as calendar_dates_file:
lines = calendar_dates_file.readlines()
if len(lines) <= 1:
logger.warning(
- "calendar_dates.txt file is empty or has only header line, not processing.")
+ "calendar_dates.txt file is empty or has only header line, not processing."
+ )
return active_services
- header = lines[0].strip().split(',')
+ header = lines[0].strip().split(",")
try:
- service_id_index = header.index('service_id')
- date_index = header.index('date')
- exception_type_index = header.index('exception_type')
+ service_id_index = header.index("service_id")
+ date_index = header.index("date")
+ exception_type_index = header.index("exception_type")
except ValueError as e:
logger.error(f"Required column not found in header: {e}")
return active_services
@@ -91,20 +97,21 @@ def get_active_services(feed_dir: str, date: str) -> list[str]:
# Now read the rest of the file, find all services where 'date' matches the search_date
# Start from 1 to skip header
for idx, line in enumerate(lines[1:], 1):
- parts = line.strip().split(',')
+ parts = line.strip().split(",")
if len(parts) < len(header):
logger.warning(
- f"Skipping malformed line in calendar_dates.txt line {idx+1}: {line.strip()}")
+ f"Skipping malformed line in calendar_dates.txt line {idx + 1}: {line.strip()}"
+ )
continue
service_id = parts[service_id_index]
date_value = parts[date_index]
exception_type = parts[exception_type_index]
- if date_value == search_date and exception_type == '1':
+ if date_value == search_date and exception_type == "1":
active_services.append(service_id)
- if date_value == search_date and exception_type == '2':
+ if date_value == search_date and exception_type == "2":
if service_id in active_services:
active_services.remove(service_id)
except FileNotFoundError:
diff --git a/src/gtfs_perstop_report/src/shapes.py b/src/gtfs_perstop_report/src/shapes.py
index f49832a..a308999 100644
--- a/src/gtfs_perstop_report/src/shapes.py
+++ b/src/gtfs_perstop_report/src/shapes.py
@@ -36,13 +36,24 @@ def process_shapes(feed_dir: str, out_dir: str) -> None:
try:
shape = Shape(
shape_id=row["shape_id"],
- shape_pt_lat=float(row["shape_pt_lat"]) if row.get("shape_pt_lat") else None,
- shape_pt_lon=float(row["shape_pt_lon"]) if row.get("shape_pt_lon") else None,
- shape_pt_position=int(row["shape_pt_position"]) if row.get("shape_pt_position") else None,
- shape_dist_traveled=float(row["shape_dist_traveled"]) if row.get("shape_dist_traveled") else None,
+ shape_pt_lat=float(row["shape_pt_lat"])
+ if row.get("shape_pt_lat")
+ else None,
+ shape_pt_lon=float(row["shape_pt_lon"])
+ if row.get("shape_pt_lon")
+ else None,
+ shape_pt_position=int(row["shape_pt_position"])
+ if row.get("shape_pt_position")
+ else None,
+ shape_dist_traveled=float(row["shape_dist_traveled"])
+ if row.get("shape_dist_traveled")
+ else None,
)
- if shape.shape_pt_lat is not None and shape.shape_pt_lon is not None:
+ if (
+ shape.shape_pt_lat is not None
+ and shape.shape_pt_lon is not None
+ ):
shape_pt_25829_x, shape_pt_25829_y = transformer.transform(
shape.shape_pt_lon, shape.shape_pt_lat
)
@@ -55,18 +66,22 @@ def process_shapes(feed_dir: str, out_dir: str) -> None:
except Exception as e:
logger.warning(
f"Error parsing stops.txt line {row_num}: {e} - line data: {row}"
- )
+ )
except FileNotFoundError:
logger.error(f"File not found: {file_path}")
except Exception as e:
logger.error(f"Error reading stops.txt: {e}")
-
# Write shapes to Protobuf files
from src.proto.stop_schedule_pb2 import Epsg25829, Shape as PbShape
for shape_id, shape_points in shapes.items():
- points = sorted(shape_points, key=lambda sp: sp.shape_pt_position if sp.shape_pt_position is not None else 0)
+ points = sorted(
+ shape_points,
+ key=lambda sp: sp.shape_pt_position
+ if sp.shape_pt_position is not None
+ else 0,
+ )
pb_shape = PbShape(
shape_id=shape_id,
diff --git a/src/gtfs_perstop_report/src/stop_schedule_pb2.py b/src/gtfs_perstop_report/src/stop_schedule_pb2.py
index 285b057..76a1da4 100644
--- a/src/gtfs_perstop_report/src/stop_schedule_pb2.py
+++ b/src/gtfs_perstop_report/src/stop_schedule_pb2.py
@@ -4,38 +4,37 @@
# source: stop_schedule.proto
# Protobuf Python Version: 6.33.0
"""Generated protocol buffer code."""
+
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
+
_runtime_version.ValidateProtobufRuntimeVersion(
- _runtime_version.Domain.PUBLIC,
- 6,
- 33,
- 0,
- '',
- 'stop_schedule.proto'
+ _runtime_version.Domain.PUBLIC, 6, 33, 0, "", "stop_schedule.proto"
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13stop_schedule.proto\x12\x05proto\"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\xe3\x03\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12\"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\xe5\x02\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18\" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\tB$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
+ b'\n\x13stop_schedule.proto\x12\x05proto"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01"\xe3\x03\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\xe5\x02\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\tB$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3'
+)
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'stop_schedule_pb2', _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "stop_schedule_pb2", _globals)
if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\252\002!Costasdev.Busurbano.Backend.Types'
- _globals['_EPSG25829']._serialized_start=30
- _globals['_EPSG25829']._serialized_end=63
- _globals['_STOPARRIVALS']._serialized_start=66
- _globals['_STOPARRIVALS']._serialized_end=549
- _globals['_STOPARRIVALS_SCHEDULEDARRIVAL']._serialized_start=192
- _globals['_STOPARRIVALS_SCHEDULEDARRIVAL']._serialized_end=549
+ _globals["DESCRIPTOR"]._loaded_options = None
+ _globals[
+ "DESCRIPTOR"
+ ]._serialized_options = b"\252\002!Costasdev.Busurbano.Backend.Types"
+ _globals["_EPSG25829"]._serialized_start = 30
+ _globals["_EPSG25829"]._serialized_end = 63
+ _globals["_STOPARRIVALS"]._serialized_start = 66
+ _globals["_STOPARRIVALS"]._serialized_end = 549
+ _globals["_STOPARRIVALS_SCHEDULEDARRIVAL"]._serialized_start = 192
+ _globals["_STOPARRIVALS_SCHEDULEDARRIVAL"]._serialized_end = 549
# @@protoc_insertion_point(module_scope)
diff --git a/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi b/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi
index aa42cdb..c8d7f36 100644
--- a/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi
+++ b/src/gtfs_perstop_report/src/stop_schedule_pb2.pyi
@@ -12,7 +12,9 @@ class Epsg25829(_message.Message):
Y_FIELD_NUMBER: _ClassVar[int]
x: float
y: float
- def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ...) -> None: ...
+ def __init__(
+ self, x: _Optional[float] = ..., y: _Optional[float] = ...
+ ) -> None: ...
class StopArrivals(_message.Message):
__slots__ = ()
@@ -50,11 +52,37 @@ class StopArrivals(_message.Message):
terminus_code: str
terminus_name: str
terminus_time: str
- def __init__(self, service_id: _Optional[str] = ..., trip_id: _Optional[str] = ..., line: _Optional[str] = ..., route: _Optional[str] = ..., shape_id: _Optional[str] = ..., shape_dist_traveled: _Optional[float] = ..., stop_sequence: _Optional[int] = ..., next_streets: _Optional[_Iterable[str]] = ..., starting_code: _Optional[str] = ..., starting_name: _Optional[str] = ..., starting_time: _Optional[str] = ..., calling_time: _Optional[str] = ..., calling_ssm: _Optional[int] = ..., terminus_code: _Optional[str] = ..., terminus_name: _Optional[str] = ..., terminus_time: _Optional[str] = ...) -> None: ...
+ def __init__(
+ self,
+ service_id: _Optional[str] = ...,
+ trip_id: _Optional[str] = ...,
+ line: _Optional[str] = ...,
+ route: _Optional[str] = ...,
+ shape_id: _Optional[str] = ...,
+ shape_dist_traveled: _Optional[float] = ...,
+ stop_sequence: _Optional[int] = ...,
+ next_streets: _Optional[_Iterable[str]] = ...,
+ starting_code: _Optional[str] = ...,
+ starting_name: _Optional[str] = ...,
+ starting_time: _Optional[str] = ...,
+ calling_time: _Optional[str] = ...,
+ calling_ssm: _Optional[int] = ...,
+ terminus_code: _Optional[str] = ...,
+ terminus_name: _Optional[str] = ...,
+ terminus_time: _Optional[str] = ...,
+ ) -> None: ...
+
STOP_ID_FIELD_NUMBER: _ClassVar[int]
LOCATION_FIELD_NUMBER: _ClassVar[int]
ARRIVALS_FIELD_NUMBER: _ClassVar[int]
stop_id: str
location: Epsg25829
arrivals: _containers.RepeatedCompositeFieldContainer[StopArrivals.ScheduledArrival]
- def __init__(self, stop_id: _Optional[str] = ..., location: _Optional[_Union[Epsg25829, _Mapping]] = ..., arrivals: _Optional[_Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]] = ...) -> None: ...
+ def __init__(
+ self,
+ stop_id: _Optional[str] = ...,
+ location: _Optional[_Union[Epsg25829, _Mapping]] = ...,
+ arrivals: _Optional[
+ _Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]
+ ] = ...,
+ ) -> None: ...
diff --git a/src/gtfs_perstop_report/src/stop_times.py b/src/gtfs_perstop_report/src/stop_times.py
index f3c3f25..c48f505 100644
--- a/src/gtfs_perstop_report/src/stop_times.py
+++ b/src/gtfs_perstop_report/src/stop_times.py
@@ -1,6 +1,7 @@
"""
Functions for handling GTFS stop_times data.
"""
+
import csv
import os
from src.logger import get_logger
@@ -9,13 +10,25 @@ logger = get_logger("stop_times")
STOP_TIMES_BY_FEED: dict[str, dict[str, list["StopTime"]]] = {}
-STOP_TIMES_BY_REQUEST: dict[tuple[str, frozenset[str]], dict[str, list["StopTime"]]] = {}
+STOP_TIMES_BY_REQUEST: dict[
+ tuple[str, frozenset[str]], dict[str, list["StopTime"]]
+] = {}
+
class StopTime:
"""
Class representing a stop time entry in the GTFS data.
"""
- def __init__(self, trip_id: str, arrival_time: str, departure_time: str, stop_id: str, stop_sequence: int, shape_dist_traveled: float | None):
+
+ def __init__(
+ self,
+ trip_id: str,
+ arrival_time: str,
+ departure_time: str,
+ stop_id: str,
+ stop_sequence: int,
+ shape_dist_traveled: float | None,
+ ):
self.trip_id = trip_id
self.arrival_time = arrival_time
self.departure_time = departure_time
@@ -36,47 +49,63 @@ def _load_stop_times_for_feed(feed_dir: str) -> dict[str, list[StopTime]]:
stops: dict[str, list[StopTime]] = {}
try:
- with open(os.path.join(feed_dir, 'stop_times.txt'), 'r', encoding="utf-8", newline='') as stop_times_file:
+ with open(
+ os.path.join(feed_dir, "stop_times.txt"), "r", encoding="utf-8", newline=""
+ ) as stop_times_file:
reader = csv.DictReader(stop_times_file)
if reader.fieldnames is None:
logger.error("stop_times.txt missing header row.")
STOP_TIMES_BY_FEED[feed_dir] = {}
return STOP_TIMES_BY_FEED[feed_dir]
- required_columns = ['trip_id', 'arrival_time', 'departure_time', 'stop_id', 'stop_sequence']
- missing_columns = [col for col in required_columns if col not in reader.fieldnames]
+ required_columns = [
+ "trip_id",
+ "arrival_time",
+ "departure_time",
+ "stop_id",
+ "stop_sequence",
+ ]
+ missing_columns = [
+ col for col in required_columns if col not in reader.fieldnames
+ ]
if missing_columns:
logger.error(f"Required columns not found in header: {missing_columns}")
STOP_TIMES_BY_FEED[feed_dir] = {}
return STOP_TIMES_BY_FEED[feed_dir]
- has_shape_dist = 'shape_dist_traveled' in reader.fieldnames
+ has_shape_dist = "shape_dist_traveled" in reader.fieldnames
if not has_shape_dist:
- logger.warning("Column 'shape_dist_traveled' not found in stop_times.txt. Distances will be set to None.")
+ logger.warning(
+ "Column 'shape_dist_traveled' not found in stop_times.txt. Distances will be set to None."
+ )
for row in reader:
- trip_id = row['trip_id']
+ trip_id = row["trip_id"]
if trip_id not in stops:
stops[trip_id] = []
dist = None
- if has_shape_dist and row['shape_dist_traveled']:
+ if has_shape_dist and row["shape_dist_traveled"]:
try:
- dist = float(row['shape_dist_traveled'])
+ dist = float(row["shape_dist_traveled"])
except ValueError:
pass
try:
- stops[trip_id].append(StopTime(
- trip_id=trip_id,
- arrival_time=row['arrival_time'],
- departure_time=row['departure_time'],
- stop_id=row['stop_id'],
- stop_sequence=int(row['stop_sequence']),
- shape_dist_traveled=dist
- ))
+ stops[trip_id].append(
+ StopTime(
+ trip_id=trip_id,
+ arrival_time=row["arrival_time"],
+ departure_time=row["departure_time"],
+ stop_id=row["stop_id"],
+ stop_sequence=int(row["stop_sequence"]),
+ shape_dist_traveled=dist,
+ )
+ )
except ValueError as e:
- logger.warning(f"Error parsing stop_sequence for trip {trip_id}: {e}")
+ logger.warning(
+ f"Error parsing stop_sequence for trip {trip_id}: {e}"
+ )
for trip_stop_times in stops.values():
trip_stop_times.sort(key=lambda st: st.stop_sequence)
@@ -89,7 +118,9 @@ def _load_stop_times_for_feed(feed_dir: str) -> dict[str, list[StopTime]]:
return stops
-def get_stops_for_trips(feed_dir: str, trip_ids: list[str]) -> dict[str, list[StopTime]]:
+def get_stops_for_trips(
+ feed_dir: str, trip_ids: list[str]
+) -> dict[str, list[StopTime]]:
"""
Get stops for a list of trip IDs based on the cached 'stop_times.txt' data.
"""
diff --git a/src/gtfs_perstop_report/src/stops.py b/src/gtfs_perstop_report/src/stops.py
index bb54fa4..fb95cf2 100644
--- a/src/gtfs_perstop_report/src/stops.py
+++ b/src/gtfs_perstop_report/src/stops.py
@@ -36,9 +36,7 @@ def get_all_stops_by_code(feed_dir: str) -> Dict[str, Stop]:
all_stops = get_all_stops(feed_dir)
for stop in all_stops.values():
- stop_25829_x, stop_25829_y = transformer.transform(
- stop.stop_lon, stop.stop_lat
- )
+ stop_25829_x, stop_25829_y = transformer.transform(stop.stop_lon, stop.stop_lat)
stop.stop_25829_x = stop_25829_x
stop.stop_25829_y = stop_25829_y
diff --git a/src/gtfs_perstop_report/src/street_name.py b/src/gtfs_perstop_report/src/street_name.py
index ec6b5b6..81d419b 100644
--- a/src/gtfs_perstop_report/src/street_name.py
+++ b/src/gtfs_perstop_report/src/street_name.py
@@ -3,7 +3,8 @@ import re
re_remove_quotation_marks = re.compile(r'[""”]', re.IGNORECASE)
re_anything_before_stopcharacters_with_parentheses = re.compile(
- r'^(.*?)(?:,|\s\s|\s-\s| \d| S\/N|\s\()', re.IGNORECASE)
+ r"^(.*?)(?:,|\s\s|\s-\s| \d| S\/N|\s\()", re.IGNORECASE
+)
NAME_REPLACEMENTS = {
@@ -17,15 +18,13 @@ NAME_REPLACEMENTS = {
" do ": " ",
" da ": " ",
" das ": " ",
- "Riós": "Ríos"
+ "Riós": "Ríos",
}
def get_street_name(original_name: str) -> str:
- original_name = re.sub(re_remove_quotation_marks,
- '', original_name).strip()
- match = re.match(
- re_anything_before_stopcharacters_with_parentheses, original_name)
+ original_name = re.sub(re_remove_quotation_marks, "", original_name).strip()
+ match = re.match(re_anything_before_stopcharacters_with_parentheses, original_name)
if match:
street_name = match.group(1)
else:
@@ -41,9 +40,9 @@ def get_street_name(original_name: str) -> str:
def normalise_stop_name(original_name: str | None) -> str:
if original_name is None:
- return ''
- stop_name = re.sub(re_remove_quotation_marks, '', original_name).strip()
+ return ""
+ stop_name = re.sub(re_remove_quotation_marks, "", original_name).strip()
- stop_name = stop_name.replace(' ', ', ')
+ stop_name = stop_name.replace(" ", ", ")
return stop_name
diff --git a/src/gtfs_perstop_report/src/trips.py b/src/gtfs_perstop_report/src/trips.py
index 0cedd26..0de632a 100644
--- a/src/gtfs_perstop_report/src/trips.py
+++ b/src/gtfs_perstop_report/src/trips.py
@@ -1,16 +1,28 @@
"""
Functions for handling GTFS trip data.
"""
+
import os
from src.logger import get_logger
logger = get_logger("trips")
+
class TripLine:
"""
Class representing a trip line in the GTFS data.
"""
- def __init__(self, route_id: str, service_id: str, trip_id: str, headsign: str, direction_id: int, shape_id: str|None = None, block_id: str|None = None):
+
+ def __init__(
+ self,
+ route_id: str,
+ service_id: str,
+ trip_id: str,
+ headsign: str,
+ direction_id: int,
+ shape_id: str | None = None,
+ block_id: str | None = None,
+ ):
self.route_id = route_id
self.service_id = service_id
self.trip_id = trip_id
@@ -28,15 +40,17 @@ class TripLine:
TRIPS_BY_SERVICE_ID: dict[str, dict[str, list[TripLine]]] = {}
-def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, list[TripLine]]:
+def get_trips_for_services(
+ feed_dir: str, service_ids: list[str]
+) -> dict[str, list[TripLine]]:
"""
Get trips for a list of service IDs based on the 'trips.txt' file.
Uses caching to avoid reading and parsing the file multiple times.
-
+
Args:
feed_dir (str): Directory containing the GTFS feed files.
service_ids (list[str]): List of service IDs to find trips for.
-
+
Returns:
dict[str, list[TripLine]]: Dictionary mapping service IDs to lists of trip objects.
"""
@@ -44,52 +58,58 @@ def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, l
if feed_dir in TRIPS_BY_SERVICE_ID:
logger.debug(f"Using cached trips data for {feed_dir}")
# Return only the trips for the requested service IDs
- return {service_id: TRIPS_BY_SERVICE_ID[feed_dir].get(service_id, [])
- for service_id in service_ids}
-
+ return {
+ service_id: TRIPS_BY_SERVICE_ID[feed_dir].get(service_id, [])
+ for service_id in service_ids
+ }
+
trips: dict[str, list[TripLine]] = {}
try:
- with open(os.path.join(feed_dir, 'trips.txt'), 'r', encoding="utf-8") as trips_file:
+ with open(
+ os.path.join(feed_dir, "trips.txt"), "r", encoding="utf-8"
+ ) as trips_file:
lines = trips_file.readlines()
if len(lines) <= 1:
logger.warning(
- "trips.txt file is empty or has only header line, not processing.")
+ "trips.txt file is empty or has only header line, not processing."
+ )
return trips
- header = lines[0].strip().split(',')
+ header = lines[0].strip().split(",")
try:
- service_id_index = header.index('service_id')
- trip_id_index = header.index('trip_id')
- route_id_index = header.index('route_id')
- headsign_index = header.index('trip_headsign')
- direction_id_index = header.index('direction_id')
+ service_id_index = header.index("service_id")
+ trip_id_index = header.index("trip_id")
+ route_id_index = header.index("route_id")
+ headsign_index = header.index("trip_headsign")
+ direction_id_index = header.index("direction_id")
except ValueError as e:
logger.error(f"Required column not found in header: {e}")
return trips
# Check if shape_id column exists
shape_id_index = None
- if 'shape_id' in header:
- shape_id_index = header.index('shape_id')
+ if "shape_id" in header:
+ shape_id_index = header.index("shape_id")
else:
logger.warning("shape_id column not found in trips.txt")
# Check if block_id column exists
block_id_index = None
- if 'block_id' in header:
- block_id_index = header.index('block_id')
+ if "block_id" in header:
+ block_id_index = header.index("block_id")
else:
logger.info("block_id column not found in trips.txt")
# Initialize cache for this feed directory
TRIPS_BY_SERVICE_ID[feed_dir] = {}
-
+
for line in lines[1:]:
- parts = line.strip().split(',')
+ parts = line.strip().split(",")
if len(parts) < len(header):
logger.warning(
- f"Skipping malformed line in trips.txt: {line.strip()}")
+ f"Skipping malformed line in trips.txt: {line.strip()}"
+ )
continue
service_id = parts[service_id_index]
@@ -115,19 +135,20 @@ def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, l
trip_id=trip_id,
headsign=parts[headsign_index],
direction_id=int(
- parts[direction_id_index] if parts[direction_id_index] else -1),
+ parts[direction_id_index] if parts[direction_id_index] else -1
+ ),
shape_id=shape_id,
- block_id=block_id
+ block_id=block_id,
)
-
+
TRIPS_BY_SERVICE_ID[feed_dir][service_id].append(trip_line)
-
+
# Also build the result for the requested service IDs
if service_id in service_ids:
if service_id not in trips:
trips[service_id] = []
trips[service_id].append(trip_line)
-
+
except FileNotFoundError:
logger.warning("trips.txt file not found.")
diff --git a/src/gtfs_perstop_report/stop_report.py b/src/gtfs_perstop_report/stop_report.py
index f8fdc64..3bbdf11 100644
--- a/src/gtfs_perstop_report/stop_report.py
+++ b/src/gtfs_perstop_report/stop_report.py
@@ -32,8 +32,7 @@ def parse_args():
default="./output/",
help="Directory to write reports to (default: ./output/)",
)
- parser.add_argument("--feed-dir", type=str,
- help="Path to the feed directory")
+ parser.add_argument("--feed-dir", type=str, help="Path to the feed directory")
parser.add_argument(
"--feed-url",
type=str,
@@ -244,12 +243,9 @@ def build_trip_previous_shape_map(
if shift_key not in trips_by_shift:
trips_by_shift[shift_key] = []
- trips_by_shift[shift_key].append((
- trip,
- trip_number,
- first_stop.stop_id,
- last_stop.stop_id
- ))
+ trips_by_shift[shift_key].append(
+ (trip, trip_number, first_stop.stop_id, last_stop.stop_id)
+ )
# For each shift, sort trips by trip number and link consecutive trips
for shift_key, shift_trips in trips_by_shift.items():
# Sort by trip number
@@ -262,16 +258,20 @@ def build_trip_previous_shape_map(
# Check if trips are consecutive (trip numbers differ by 1),
# if previous trip's terminus matches current trip's start,
# and if both trips have valid shape IDs
- if (current_num == prev_num + 1 and
- prev_end_stop == current_start_stop and
- prev_trip.shape_id and
- current_trip.shape_id):
+ if (
+ current_num == prev_num + 1
+ and prev_end_stop == current_start_stop
+ and prev_trip.shape_id
+ and current_trip.shape_id
+ ):
trip_previous_shape[current_trip.trip_id] = prev_trip.shape_id
return trip_previous_shape
-def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict[str, Any]]]:
+def get_stop_arrivals(
+ feed_dir: str, date: str, provider
+) -> Dict[str, List[Dict[str, Any]]]:
"""
Process trips for the given date and organize stop arrivals.
Also includes night services from the previous day (times >= 24:00:00).
@@ -293,15 +293,16 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
if not active_services:
logger.info("No active services found for the given date.")
- logger.info(
- f"Found {len(active_services)} active services for date {date}.")
+ logger.info(f"Found {len(active_services)} active services for date {date}.")
# Also get services from the previous day to include night services (times >= 24:00)
- prev_date = (datetime.strptime(date, "%Y-%m-%d") -
- timedelta(days=1)).strftime("%Y-%m-%d")
+ prev_date = (datetime.strptime(date, "%Y-%m-%d") - timedelta(days=1)).strftime(
+ "%Y-%m-%d"
+ )
prev_services = get_active_services(feed_dir, prev_date)
logger.info(
- f"Found {len(prev_services)} active services for previous date {prev_date} (for night services).")
+ f"Found {len(prev_services)} active services for previous date {prev_date} (for night services)."
+ )
all_services = list(set(active_services + prev_services))
@@ -314,18 +315,17 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
logger.info(f"Found {total_trip_count} trips for active services.")
# Get all trip IDs
- all_trip_ids = [trip.trip_id for trip_list in trips.values()
- for trip in trip_list]
+ all_trip_ids = [trip.trip_id for trip_list in trips.values() for trip in trip_list]
# Get stops for all trips
stops_for_all_trips = get_stops_for_trips(feed_dir, all_trip_ids)
logger.info(f"Precomputed stops for {len(stops_for_all_trips)} trips.")
# Build mapping from trip_id to previous trip's shape_id
- trip_previous_shape_map = build_trip_previous_shape_map(
- trips, stops_for_all_trips)
+ trip_previous_shape_map = build_trip_previous_shape_map(trips, stops_for_all_trips)
logger.info(
- f"Built previous trip shape mapping for {len(trip_previous_shape_map)} trips.")
+ f"Built previous trip shape mapping for {len(trip_previous_shape_map)} trips."
+ )
# Load routes information
routes = load_routes(feed_dir)
@@ -389,8 +389,7 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
stop_to_segment_idx.append(len(segment_names) - 1)
# Precompute future street transitions per segment
- future_suffix_by_segment: list[tuple[str, ...]] = [
- ()] * len(segment_names)
+ future_suffix_by_segment: list[tuple[str, ...]] = [()] * len(segment_names)
future_tuple: tuple[str, ...] = ()
for idx in range(len(segment_names) - 1, -1, -1):
future_suffix_by_segment[idx] = future_tuple
@@ -437,7 +436,7 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
passes.append("previous")
for mode in passes:
- is_current_mode = (mode == "current")
+ is_current_mode = mode == "current"
for i, (stop_time, _) in enumerate(trip_stop_pairs):
# Skip the last stop of the trip (terminus) to avoid duplication
@@ -457,11 +456,9 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
continue
# Normalize times for display on current day (e.g. 25:30 -> 01:30)
- final_starting_time = normalize_gtfs_time(
- starting_time)
+ final_starting_time = normalize_gtfs_time(starting_time)
final_calling_time = normalize_gtfs_time(dep_time)
- final_terminus_time = normalize_gtfs_time(
- terminus_time)
+ final_terminus_time = normalize_gtfs_time(terminus_time)
# SSM should be small (early morning)
final_calling_ssm = time_to_seconds(final_calling_time)
else:
@@ -489,12 +486,10 @@ def get_stop_arrivals(feed_dir: str, date: str, provider) -> Dict[str, List[Dict
# Format IDs and route using provider-specific logic
service_id_fmt = provider.format_service_id(service_id)
trip_id_fmt = provider.format_trip_id(trip_id)
- route_fmt = provider.format_route(
- trip_headsign, terminus_name)
+ route_fmt = provider.format_route(trip_headsign, terminus_name)
# Get previous trip shape_id if available
- previous_trip_shape_id = trip_previous_shape_map.get(
- trip_id, "")
+ previous_trip_shape_id = trip_previous_shape_map.get(trip_id, "")
stop_arrivals[stop_code].append(
{
@@ -616,8 +611,7 @@ def main():
feed_dir = args.feed_dir
else:
logger.info(f"Downloading GTFS feed from {feed_url}...")
- feed_dir = download_feed_from_url(
- feed_url, output_dir, args.force_download)
+ feed_dir = download_feed_from_url(feed_url, output_dir, args.force_download)
if feed_dir is None:
logger.info("Download was skipped (feed not modified). Exiting.")
return
@@ -642,8 +636,7 @@ def main():
_, stop_summary = process_date(feed_dir, date, output_dir, provider)
all_stops_summary[date] = stop_summary
- logger.info(
- "Finished processing all dates. Beginning with shape transformation.")
+ logger.info("Finished processing all dates. Beginning with shape transformation.")
# Process shapes, converting each coordinate to EPSG:25829 and saving as Protobuf
process_shapes(feed_dir, output_dir)
diff --git a/src/stop_downloader/vigo/download-stops.py b/src/stop_downloader/vigo/download-stops.py
index eda5bde..5d039dc 100644
--- a/src/stop_downloader/vigo/download-stops.py
+++ b/src/stop_downloader/vigo/download-stops.py
@@ -24,10 +24,9 @@ def load_stop_overrides(file_path):
return {}
try:
- with open(file_path, 'r', encoding='utf-8') as f:
+ with open(file_path, "r", encoding="utf-8") as f:
overrides = yaml.safe_load(f)
- print(
- f"Loaded {len(overrides) if overrides else 0} stop overrides")
+ print(f"Loaded {len(overrides) if overrides else 0} stop overrides")
return overrides or {}
except Exception as e:
print(f"Error loading overrides: {e}", file=sys.stderr)
@@ -93,12 +92,10 @@ def apply_overrides(stops, overrides):
# Create the new stop
new_stop = {
"stopId": stop_id_int,
- "name": {
- "original": override.get("name", f"Stop {stop_id_int}")
- },
+ "name": {"original": override.get("name", f"Stop {stop_id_int}")},
"latitude": override.get("location", {}).get("latitude"),
"longitude": override.get("location", {}).get("longitude"),
- "lines": override.get("lines", [])
+ "lines": override.get("lines", []),
}
# Add optional fields (excluding the 'new' parameter)
@@ -132,7 +129,7 @@ def download_stops_vitrasa() -> list[dict]:
try:
with urllib.request.urlopen(req) as response:
# Read the response and decode from ISO-8859-1 to UTF-8
- content = response.read().decode('iso-8859-1')
+ content = response.read().decode("iso-8859-1")
data = json.loads(content)
print(f"Downloaded {len(data)} stops")
@@ -142,16 +139,16 @@ def download_stops_vitrasa() -> list[dict]:
for stop in data:
name = stop.get("nombre", "").strip()
# Fix double space equals comma-space: "Castrelos 202" -> "Castrelos, 202"; and remove quotes
- name = name.replace(" ", ", ").replace('"', '').replace("'", "")
+ name = name.replace(" ", ", ").replace('"', "").replace("'", "")
processed_stop = {
"stopId": "vitrasa:" + str(stop.get("id")),
- "name": {
- "original": name
- },
+ "name": {"original": name},
"latitude": stop.get("lat"),
"longitude": stop.get("lon"),
- "lines": [line.strip() for line in stop.get("lineas", "").split(",")] if stop.get("lineas") else []
+ "lines": [line.strip() for line in stop.get("lineas", "").split(",")]
+ if stop.get("lineas")
+ else [],
}
processed_stops.append(processed_stop)
@@ -171,10 +168,19 @@ def download_stops_renfe() -> list[dict]:
with urllib.request.urlopen(req) as response:
content = response.read()
data = csv.DictReader(
- content.decode('utf-8').splitlines(),
- delimiter=';',
- fieldnames=["CODE", "NAME", "LAT", "LNG",
- "ADDRESS", "ZIP", "CITY", "PROVINCE", "COUNTRY"]
+ content.decode("utf-8").splitlines(),
+ delimiter=";",
+ fieldnames=[
+ "CODE",
+ "NAME",
+ "LAT",
+ "LNG",
+ "ADDRESS",
+ "ZIP",
+ "CITY",
+ "PROVINCE",
+ "COUNTRY",
+ ],
)
stops = [row for row in data]
@@ -191,12 +197,10 @@ def download_stops_renfe() -> list[dict]:
processed_stop = {
"stopId": "renfe:" + str(stop.get("CODE", 0)),
- "name": {
- "original": name
- },
- "latitude": float(stop.get("LAT", 0).replace(',', '.')),
- "longitude": float(stop.get("LNG", 0).replace(',', '.')),
- "lines": []
+ "name": {"original": name},
+ "latitude": float(stop.get("LAT", 0).replace(",", ".")),
+ "longitude": float(stop.get("LNG", 0).replace(",", ".")),
+ "lines": [],
}
processed_stops.append(processed_stop)
@@ -229,17 +233,15 @@ def main():
all_stops = apply_overrides(all_stops, overrides)
# Filter out hidden stops
- visible_stops = [
- stop for stop in all_stops if not stop.get("hide")]
- print(
- f"Removed {len(all_stops) - len(visible_stops)} hidden stops")
+ visible_stops = [stop for stop in all_stops if not stop.get("hide")]
+ print(f"Removed {len(all_stops) - len(visible_stops)} hidden stops")
# Sort stops by ID ascending
visible_stops.sort(key=lambda x: x["stopId"])
output_file = os.path.join(SCRIPT_DIR, OUTPUT_FILE)
- with open(output_file, 'w', encoding='utf-8') as f:
+ with open(output_file, "w", encoding="utf-8") as f:
json.dump(visible_stops, f, ensure_ascii=False, indent=2)
print(f"Saved processed stops data to {output_file}")
@@ -249,6 +251,7 @@ def main():
print(f"Error processing stops data: {e}", file=sys.stderr)
# Print full exception traceback
import traceback
+
traceback.print_exc()
return 1