aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2025-11-06 20:34:36 +0100
committerGitHub <noreply@github.com>2025-11-06 20:34:36 +0100
commita24639e17b63c5ebb9b2bb26af18e17302e9360b (patch)
tree3301a4f8f278cab61696b2cd0212cb71e0676363
parent9a2e46b6e8f0decbd1995c14d34b0b8c0d663b49 (diff)
Add manual stop creation and override alerts with alternate stop codes (#74)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> Co-authored-by: Ariel Costas Guerrero <ariel@costas.dev>
-rw-r--r--.gitignore6
-rw-r--r--data/README.md83
-rw-r--r--data/santiago/download-stops.py66
-rw-r--r--data/vigo/download-stops.py66
-rw-r--r--data/vigo/overrides/example-new-stops.yaml31
-rw-r--r--package-lock.json6
-rw-r--r--src/frontend/app/components/StopAlert.css89
-rw-r--r--src/frontend/app/components/StopAlert.tsx36
-rw-r--r--src/frontend/app/components/StopSheet.tsx3
-rw-r--r--src/frontend/app/data/StopDataProvider.ts5
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx3
11 files changed, 378 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index f71b400..531de93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,12 @@
.tsbuildinfo
*.tsbuildinfo
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+
# Logs
logs
*.log
diff --git a/data/README.md b/data/README.md
index 6aa2e10..2cf8151 100644
--- a/data/README.md
+++ b/data/README.md
@@ -1,8 +1,10 @@
-# Bus Stop Overrides
+# Bus Stop Overrides and Manual Stops
-This file defines custom overrides for specific bus stops in YAML format.
+This directory contains YAML files for overriding properties of existing bus stops and manually adding new stops.
-## Format
+## Overrides Format
+
+Overrides modify or extend properties of existing stops from the transit API.
```yaml
stopId: # Numeric ID of the stop to override
@@ -16,20 +18,56 @@ stopId: # Numeric ID of the stop to override
amenities: # List of amenities available at this stop (list)
- shelter
- display
+ cancelled: # Mark stop as cancelled/out of service (boolean)
+ title: # Alert title shown to users (string)
+ message: # Alert message shown to users (string)
+ alternateCodes: # Alternative stop codes (list of strings)
+ - "ALT-123"
+```
+
+## Adding New Stops
+
+New stops that don't exist in the transit API can be added directly in override files using the `new: true` parameter. The `new` parameter is automatically removed after the stop is added to the list.
+
+```yaml
+stopId: # Numeric ID for the new stop (must not conflict with existing stops)
+ new: true # Mark this as a new stop (required, will be removed after processing)
+ name: # Name of the stop (string)
+ location: # Location coordinates (required for new stops)
+ latitude: # Latitude coordinate (float)
+ longitude: # Longitude coordinate (float)
+ lines: # List of lines serving this stop (list of strings)
+ - "1"
+ - "2"
+ amenities: # Optional: List of amenities (list)
+ - shelter
+ title: # Optional: Alert title (string)
+ message: # Optional: Alert message (string)
+ cancelled: # Optional: Mark as cancelled (boolean)
+ alternateCodes: # Optional: Alternative stop codes (list)
```
## Field Descriptions
- **stopId** (integer): Unique identifier of the bus stop.
+- **new** (boolean): Set to `true` to add a new stop that doesn't exist in the API. This parameter is removed after processing.
+- **name** (string): Override or set the stop name.
- **alternateNames** (object): Other names used in different contexts.
- **key** (string): Name used in a specific context, such as `metro`.
- **location** (object):
- - **latitude** (float): Override latitude coordinate.
- - **longitude** (float): Override longitude coordinate.
+ - **latitude** (float): Override/set latitude coordinate.
+ - **longitude** (float): Override/set longitude coordinate.
+- **lines** (array of strings): List of line numbers serving this stop (required for new stops).
- **hide** (boolean): Set to `true` to exclude the stop from maps and listings.
-- **amenities** (array of strings): Amenities available at this stop, such as `shelter` or `display`. For now, only those two will be supported in the app.
+- **cancelled** (boolean): Set to `true` to mark the stop as cancelled or out of service.
+- **title** (string): Alert title displayed to users (e.g., "Stop Temporarily Closed").
+- **message** (string): Detailed message about the stop status or alert.
+- **alternateCodes** (array of strings): Alternative stop codes or identifiers.
+- **amenities** (array of strings): Amenities available at this stop, such as `shelter` or `display`.
+
+## Examples
-## Example
+### Override Example
```yaml
12345:
@@ -42,5 +80,34 @@ stopId: # Numeric ID of the stop to override
hide: false
amenities:
- shelter
- - real-time display
+ - display
+ title: "Stop Relocated"
+ message: "This stop has been temporarily moved 50 meters north."
+```
+
+### New Stop Example
+
+```yaml
+99999:
+ new: true
+ name: "New Development Stop"
+ location:
+ latitude: 42.229188
+ longitude: -8.722469
+ lines:
+ - "5"
+ - "12"
+ amenities:
+ - shelter
+```
+
+### Cancelled Stop Example
+
+```yaml
+54321:
+ cancelled: true
+ title: "Stop Out of Service"
+ message: "This stop is temporarily closed for construction. Use stop 54322 (100m south) instead."
+ alternateCodes:
+ - "54322"
```
diff --git a/data/santiago/download-stops.py b/data/santiago/download-stops.py
index f673be5..4900c41 100644
--- a/data/santiago/download-stops.py
+++ b/data/santiago/download-stops.py
@@ -29,12 +29,20 @@ def load_stop_overrides(file_path):
return {}
def apply_overrides(stops, overrides):
- """Apply overrides to the stop data"""
+ """Apply overrides to the stop data and add new stops"""
+ # Track existing stop IDs
+ existing_stop_ids = {stop.get("stopId") for stop in stops}
+
+ # Apply overrides to existing stops
for stop in stops:
stop_id = stop.get("stopId")
if stop_id in overrides:
override = overrides[stop_id]
+ # Override name if provided
+ if "name" in override:
+ stop["name"]["original"] = override["name"]
+
# Apply or add alternate names
if "alternateNames" in override:
for key, value in override["alternateNames"].items():
@@ -55,6 +63,62 @@ def apply_overrides(stops, overrides):
if "hide" in override:
stop["hide"] = override["hide"]
+ # Mark stop as cancelled
+ if "cancelled" in override:
+ stop["cancelled"] = override["cancelled"]
+
+ # Add alert title
+ if "title" in override:
+ stop["title"] = override["title"]
+
+ # Add alert message
+ if "message" in override:
+ stop["message"] = override["message"]
+
+ # Add alternate codes
+ if "alternateCodes" in override:
+ stop["alternateCodes"] = override["alternateCodes"]
+
+ # Add new stops (those with "new: true" parameter)
+ new_stops_added = 0
+ for stop_id, override in overrides.items():
+ # Check if this is a new stop
+ if override.get("new") and stop_id not in existing_stop_ids:
+ # Ensure stop_id is an integer for consistency
+ stop_id_int = int(stop_id) if isinstance(stop_id, str) else stop_id
+
+ # Create the new stop
+ new_stop = {
+ "stopId": 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", [])
+ }
+
+ # Add optional fields (excluding the 'new' parameter)
+ if "alternateNames" in override:
+ for key, value in override["alternateNames"].items():
+ new_stop["name"][key] = value
+ if "amenities" in override:
+ new_stop["amenities"] = override["amenities"]
+ if "cancelled" in override:
+ new_stop["cancelled"] = override["cancelled"]
+ if "title" in override:
+ new_stop["title"] = override["title"]
+ if "message" in override:
+ new_stop["message"] = override["message"]
+ if "alternateCodes" in override:
+ new_stop["alternateCodes"] = override["alternateCodes"]
+
+ stops.append(new_stop)
+ new_stops_added += 1
+
+ if new_stops_added > 0:
+ print(f"Added {new_stops_added} new stops from overrides")
+
return stops
def main():
diff --git a/data/vigo/download-stops.py b/data/vigo/download-stops.py
index a57d30f..f332f5b 100644
--- a/data/vigo/download-stops.py
+++ b/data/vigo/download-stops.py
@@ -29,12 +29,20 @@ def load_stop_overrides(file_path):
return {}
def apply_overrides(stops, overrides):
- """Apply overrides to the stop data"""
+ """Apply overrides to the stop data and add new stops"""
+ # Track existing stop IDs
+ existing_stop_ids = {stop.get("stopId") for stop in stops}
+
+ # Apply overrides to existing stops
for stop in stops:
stop_id = stop.get("stopId")
if stop_id in overrides:
override = overrides[stop_id]
+ # Override name if provided
+ if "name" in override:
+ stop["name"]["original"] = override["name"]
+
# Apply or add alternate names
if "alternateNames" in override:
for key, value in override["alternateNames"].items():
@@ -55,6 +63,62 @@ def apply_overrides(stops, overrides):
if "hide" in override:
stop["hide"] = override["hide"]
+ # Mark stop as cancelled
+ if "cancelled" in override:
+ stop["cancelled"] = override["cancelled"]
+
+ # Add alert title
+ if "title" in override:
+ stop["title"] = override["title"]
+
+ # Add alert message
+ if "message" in override:
+ stop["message"] = override["message"]
+
+ # Add alternate codes
+ if "alternateCodes" in override:
+ stop["alternateCodes"] = override["alternateCodes"]
+
+ # Add new stops (those with "new: true" parameter)
+ new_stops_added = 0
+ for stop_id, override in overrides.items():
+ # Check if this is a new stop
+ if override.get("new") and stop_id not in existing_stop_ids:
+ # Ensure stop_id is an integer for consistency
+ stop_id_int = int(stop_id) if isinstance(stop_id, str) else stop_id
+
+ # Create the new stop
+ new_stop = {
+ "stopId": 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", [])
+ }
+
+ # Add optional fields (excluding the 'new' parameter)
+ if "alternateNames" in override:
+ for key, value in override["alternateNames"].items():
+ new_stop["name"][key] = value
+ if "amenities" in override:
+ new_stop["amenities"] = override["amenities"]
+ if "cancelled" in override:
+ new_stop["cancelled"] = override["cancelled"]
+ if "title" in override:
+ new_stop["title"] = override["title"]
+ if "message" in override:
+ new_stop["message"] = override["message"]
+ if "alternateCodes" in override:
+ new_stop["alternateCodes"] = override["alternateCodes"]
+
+ stops.append(new_stop)
+ new_stops_added += 1
+
+ if new_stops_added > 0:
+ print(f"Added {new_stops_added} new stops from overrides")
+
return stops
def main():
diff --git a/data/vigo/overrides/example-new-stops.yaml b/data/vigo/overrides/example-new-stops.yaml
new file mode 100644
index 0000000..6937471
--- /dev/null
+++ b/data/vigo/overrides/example-new-stops.yaml
@@ -0,0 +1,31 @@
+# Example: Adding a new stop using the 'new' parameter
+# New stops are added directly in override files with new: true
+
+# Example 1: New stop with basic information (commented out to avoid affecting production)
+# 99001:
+# new: true
+# name: "New Development Stop"
+# location:
+# latitude: 42.229188
+# longitude: -8.722469
+# lines:
+# - "5"
+# - "12"
+# amenities:
+# - shelter
+
+# Example 2: New stop with alert information
+# 99002:
+# new: true
+# name: "Temporary Event Stop"
+# location:
+# latitude: 42.230000
+# longitude: -8.723000
+# lines:
+# - "EVENT"
+# title: "Special Event Stop"
+# message: "This stop is active during special events only."
+
+# Note: The 'new: true' parameter tells the system to create a new stop.
+# This parameter is automatically removed after the stop is added to the dataset.
+# Choose stop IDs in the 90000+ range to avoid conflicts with existing stops.
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index f5e7798..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Costasdev.Busurbano",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {}
-}
diff --git a/src/frontend/app/components/StopAlert.css b/src/frontend/app/components/StopAlert.css
new file mode 100644
index 0000000..0032d09
--- /dev/null
+++ b/src/frontend/app/components/StopAlert.css
@@ -0,0 +1,89 @@
+.stop-alert {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ padding: 0.75rem;
+ border-radius: 0.5rem;
+ margin: 0.75rem 0;
+ border: 1px solid;
+}
+
+.stop-alert-info {
+ background-color: rgba(59, 130, 246, 0.1);
+ border-color: rgba(59, 130, 246, 0.3);
+ color: #1e40af;
+}
+
+.stop-alert-error {
+ background-color: rgba(239, 68, 68, 0.1);
+ border-color: rgba(239, 68, 68, 0.3);
+ color: #991b1b;
+}
+
+.stop-alert-compact {
+ padding: 0.5rem;
+ margin: 0.5rem 0;
+ font-size: 0.875rem;
+}
+
+.stop-alert-icon {
+ flex-shrink: 0;
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+.stop-alert-compact .stop-alert-icon {
+ width: 1rem;
+ height: 1rem;
+}
+
+.stop-alert-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.stop-alert-title {
+ font-weight: 600;
+ font-size: 0.95rem;
+}
+
+.stop-alert-compact .stop-alert-title {
+ font-size: 0.85rem;
+}
+
+.stop-alert-message {
+ font-size: 0.9rem;
+ opacity: 0.9;
+}
+
+.stop-alert-compact .stop-alert-message {
+ font-size: 0.8rem;
+}
+
+.stop-alert-alternate-codes {
+ font-size: 0.85rem;
+ margin-top: 0.25rem;
+ font-style: italic;
+ opacity: 0.8;
+}
+
+.stop-alert-compact .stop-alert-alternate-codes {
+ font-size: 0.75rem;
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .stop-alert-info {
+ background-color: rgba(59, 130, 246, 0.15);
+ border-color: rgba(59, 130, 246, 0.4);
+ color: #93c5fd;
+ }
+
+ .stop-alert-error {
+ background-color: rgba(239, 68, 68, 0.15);
+ border-color: rgba(239, 68, 68, 0.4);
+ color: #fca5a5;
+ }
+}
diff --git a/src/frontend/app/components/StopAlert.tsx b/src/frontend/app/components/StopAlert.tsx
new file mode 100644
index 0000000..69ecc22
--- /dev/null
+++ b/src/frontend/app/components/StopAlert.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { AlertCircle, Info } from "lucide-react";
+import type { Stop } from "~/data/StopDataProvider";
+import "./StopAlert.css";
+
+interface StopAlertProps {
+ stop: Stop;
+ compact?: boolean;
+}
+
+export const StopAlert: React.FC<StopAlertProps> = ({ stop, compact = false }) => {
+ // Don't render anything if there's no alert content
+ const hasContent = stop.title || stop.message;
+ if (!hasContent) {
+ return null;
+ }
+
+ const isError = stop.cancelled === true;
+
+ return (
+ <div className={`stop-alert ${isError ? 'stop-alert-error' : 'stop-alert-info'} ${compact ? 'stop-alert-compact' : ''}`}>
+ <div className="stop-alert-icon">
+ {isError ? <AlertCircle /> : <Info />}
+ </div>
+ <div className="stop-alert-content">
+ {stop.title && <div className="stop-alert-title">{stop.title}</div>}
+ {stop.message && <div className="stop-alert-message">{stop.message}</div>}
+ {stop.alternateCodes && stop.alternateCodes.length > 0 && (
+ <div className="stop-alert-alternate-codes">
+ Alternative stops: {stop.alternateCodes.join(", ")}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+};
diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx
index afa530b..695b18e 100644
--- a/src/frontend/app/components/StopSheet.tsx
+++ b/src/frontend/app/components/StopSheet.tsx
@@ -6,6 +6,7 @@ import { Clock, RefreshCw } from "lucide-react";
import LineIcon from "./LineIcon";
import { StopSheetSkeleton } from "./StopSheetSkeleton";
import { ErrorDisplay } from "./ErrorDisplay";
+import { StopAlert } from "./StopAlert";
import { type Estimate } from "../routes/estimates-$id";
import { REGIONS, type RegionId, getRegionConfig } from "../data/RegionConfig";
import { useApp } from "../AppContext";
@@ -144,6 +145,8 @@ export const StopSheet: React.FC<StopSheetProps> = ({
))}
</div>
+ <StopAlert stop={stop} compact />
+
{loading ? (
<StopSheetSkeleton />
) : error ? (
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index e49faaa..25a617b 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -17,6 +17,11 @@ export interface Stop {
longitude?: number;
lines: string[];
favourite?: boolean;
+ amenities?: string[];
+ cancelled?: boolean;
+ title?: string;
+ message?: string;
+ alternateCodes?: string[];
}
// In-memory cache and lookup map per region
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index f213105..21186fb 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -14,6 +14,7 @@ import { ErrorDisplay } from "~/components/ErrorDisplay";
import { PullToRefresh } from "~/components/PullToRefresh";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import { type RegionId, getRegionConfig } from "~/data/RegionConfig";
+import { StopAlert } from "~/components/StopAlert";
export interface Estimate {
line: string;
@@ -276,6 +277,8 @@ export default function Estimates() {
</button>
</div>
+ {stopData && <StopAlert stop={stopData} />}
+
<div className="table-responsive">
{estimatesLoading ? (
tableStyle === "grouped" ? (