diff options
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | data/README.md | 83 | ||||
| -rw-r--r-- | data/santiago/download-stops.py | 66 | ||||
| -rw-r--r-- | data/vigo/download-stops.py | 66 | ||||
| -rw-r--r-- | data/vigo/overrides/example-new-stops.yaml | 31 | ||||
| -rw-r--r-- | package-lock.json | 6 | ||||
| -rw-r--r-- | src/frontend/app/components/StopAlert.css | 89 | ||||
| -rw-r--r-- | src/frontend/app/components/StopAlert.tsx | 36 | ||||
| -rw-r--r-- | src/frontend/app/components/StopSheet.tsx | 3 | ||||
| -rw-r--r-- | src/frontend/app/data/StopDataProvider.ts | 5 | ||||
| -rw-r--r-- | src/frontend/app/routes/estimates-$id.tsx | 3 |
11 files changed, 378 insertions, 16 deletions
@@ -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" ? ( |
