From 5ced7f916d94e86e9a7ec164bee56f9a8e3a2a3a Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Mon, 26 May 2025 10:48:43 +0200 Subject: Replace Azure SWA with custom server --- src/AppContext.tsx | 234 - src/Costasdev.Busurbano.Backend/.gitignore | 264 + .../Costasdev.Busurbano.Backend.csproj | 13 + .../GetStopEstimates.cs | 43 + src/Costasdev.Busurbano.Backend/Program.cs | 10 + .../Properties/launchSettings.json | 14 + src/Costasdev.Busurbano.Backend/appsettings.json | 9 + src/ErrorBoundary.tsx | 46 - src/Layout.css | 60 - src/Layout.tsx | 55 - src/components/GroupedTable.tsx | 74 - src/components/LineIcon.css | 239 - src/components/LineIcon.tsx | 17 - src/components/RegularTable.tsx | 70 - src/components/StopItem.css | 54 - src/components/StopItem.tsx | 25 - src/controls/LocateControl.ts | 67 - src/data/StopDataProvider.ts | 160 - src/frontend/frontend.esproj | 22 + src/frontend/index.html | 55 + src/frontend/package-lock.json | 4083 +++++ src/frontend/package.json | 47 + src/frontend/public/favicon.ico | Bin 0 -> 16958 bytes src/frontend/public/logo-256.jpg | Bin 0 -> 7492 bytes src/frontend/public/logo-256.png | Bin 0 -> 37843 bytes src/frontend/public/logo-512.jpg | Bin 0 -> 16746 bytes src/frontend/public/manifest.webmanifest | 84 + src/frontend/public/map-pin-icon.png | Bin 0 -> 4033 bytes .../public/screenshots/estimates-narrow.png | Bin 0 -> 219332 bytes .../public/screenshots/estimates-wide.jpeg | Bin 0 -> 53529 bytes src/frontend/public/screenshots/map-narrow.png | Bin 0 -> 2295265 bytes src/frontend/public/screenshots/map-wide.jpeg | Bin 0 -> 336231 bytes .../public/screenshots/stoplist-narrow.png | Bin 0 -> 297920 bytes src/frontend/public/screenshots/stoplist-wide.jpeg | Bin 0 -> 102303 bytes src/frontend/public/stops.json | 15037 +++++++++++++++++++ src/frontend/public/sw.js | 51 + src/frontend/src/AppContext.tsx | 234 + src/frontend/src/ErrorBoundary.tsx | 46 + src/frontend/src/Layout.css | 60 + src/frontend/src/Layout.tsx | 55 + src/frontend/src/components/GroupedTable.tsx | 74 + src/frontend/src/components/LineIcon.css | 239 + src/frontend/src/components/LineIcon.tsx | 17 + src/frontend/src/components/RegularTable.tsx | 70 + src/frontend/src/components/StopItem.css | 54 + src/frontend/src/components/StopItem.tsx | 25 + src/frontend/src/controls/LocateControl.ts | 67 + src/frontend/src/data/StopDataProvider.ts | 160 + src/frontend/src/main.tsx | 43 + src/frontend/src/pages/Estimates.tsx | 99 + src/frontend/src/pages/Map.tsx | 75 + src/frontend/src/pages/Settings.tsx | 65 + src/frontend/src/pages/StopList.tsx | 135 + src/frontend/src/styles/Estimates.css | 105 + src/frontend/src/styles/Map.css | 86 + src/frontend/src/styles/Pages.css | 364 + src/frontend/src/styles/Settings.css | 94 + src/frontend/src/vite-env.d.ts | 1 + src/frontend/tsconfig.json | 27 + src/frontend/vite.config.ts | 26 + src/main.tsx | 43 - src/pages/Estimates.tsx | 99 - src/pages/Map.tsx | 75 - src/pages/Settings.tsx | 65 - src/pages/StopList.tsx | 135 - src/styles/Estimates.css | 105 - src/styles/Map.css | 86 - src/styles/Pages.css | 364 - src/styles/Settings.css | 94 - src/vite-env.d.ts | 1 - 70 files changed, 21953 insertions(+), 2168 deletions(-) delete mode 100644 src/AppContext.tsx create mode 100644 src/Costasdev.Busurbano.Backend/.gitignore create mode 100644 src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj create mode 100644 src/Costasdev.Busurbano.Backend/GetStopEstimates.cs create mode 100644 src/Costasdev.Busurbano.Backend/Program.cs create mode 100644 src/Costasdev.Busurbano.Backend/Properties/launchSettings.json create mode 100644 src/Costasdev.Busurbano.Backend/appsettings.json delete mode 100644 src/ErrorBoundary.tsx delete mode 100644 src/Layout.css delete mode 100644 src/Layout.tsx delete mode 100644 src/components/GroupedTable.tsx delete mode 100644 src/components/LineIcon.css delete mode 100644 src/components/LineIcon.tsx delete mode 100644 src/components/RegularTable.tsx delete mode 100644 src/components/StopItem.css delete mode 100644 src/components/StopItem.tsx delete mode 100644 src/controls/LocateControl.ts delete mode 100644 src/data/StopDataProvider.ts create mode 100644 src/frontend/frontend.esproj create mode 100644 src/frontend/index.html create mode 100644 src/frontend/package-lock.json create mode 100644 src/frontend/package.json create mode 100644 src/frontend/public/favicon.ico create mode 100644 src/frontend/public/logo-256.jpg create mode 100644 src/frontend/public/logo-256.png create mode 100644 src/frontend/public/logo-512.jpg create mode 100644 src/frontend/public/manifest.webmanifest create mode 100644 src/frontend/public/map-pin-icon.png create mode 100644 src/frontend/public/screenshots/estimates-narrow.png create mode 100644 src/frontend/public/screenshots/estimates-wide.jpeg create mode 100644 src/frontend/public/screenshots/map-narrow.png create mode 100644 src/frontend/public/screenshots/map-wide.jpeg create mode 100644 src/frontend/public/screenshots/stoplist-narrow.png create mode 100644 src/frontend/public/screenshots/stoplist-wide.jpeg create mode 100644 src/frontend/public/stops.json create mode 100644 src/frontend/public/sw.js create mode 100644 src/frontend/src/AppContext.tsx create mode 100644 src/frontend/src/ErrorBoundary.tsx create mode 100644 src/frontend/src/Layout.css create mode 100644 src/frontend/src/Layout.tsx create mode 100644 src/frontend/src/components/GroupedTable.tsx create mode 100644 src/frontend/src/components/LineIcon.css create mode 100644 src/frontend/src/components/LineIcon.tsx create mode 100644 src/frontend/src/components/RegularTable.tsx create mode 100644 src/frontend/src/components/StopItem.css create mode 100644 src/frontend/src/components/StopItem.tsx create mode 100644 src/frontend/src/controls/LocateControl.ts create mode 100644 src/frontend/src/data/StopDataProvider.ts create mode 100644 src/frontend/src/main.tsx create mode 100644 src/frontend/src/pages/Estimates.tsx create mode 100644 src/frontend/src/pages/Map.tsx create mode 100644 src/frontend/src/pages/Settings.tsx create mode 100644 src/frontend/src/pages/StopList.tsx create mode 100644 src/frontend/src/styles/Estimates.css create mode 100644 src/frontend/src/styles/Map.css create mode 100644 src/frontend/src/styles/Pages.css create mode 100644 src/frontend/src/styles/Settings.css create mode 100644 src/frontend/src/vite-env.d.ts create mode 100644 src/frontend/tsconfig.json create mode 100644 src/frontend/vite.config.ts delete mode 100644 src/main.tsx delete mode 100644 src/pages/Estimates.tsx delete mode 100644 src/pages/Map.tsx delete mode 100644 src/pages/Settings.tsx delete mode 100644 src/pages/StopList.tsx delete mode 100644 src/styles/Estimates.css delete mode 100644 src/styles/Map.css delete mode 100644 src/styles/Pages.css delete mode 100644 src/styles/Settings.css delete mode 100644 src/vite-env.d.ts (limited to 'src') diff --git a/src/AppContext.tsx b/src/AppContext.tsx deleted file mode 100644 index 8b4ffe2..0000000 --- a/src/AppContext.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/* eslint-disable react-refresh/only-export-components */ -import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; -import { LatLngTuple } from 'leaflet'; - -type Theme = 'light' | 'dark'; -type TableStyle = 'regular'|'grouped'; -type MapPositionMode = 'gps' | 'last'; - -interface MapState { - center: LatLngTuple; - zoom: number; - userLocation: LatLngTuple | null; - hasLocationPermission: boolean; -} - -interface AppContextProps { - theme: Theme; - setTheme: React.Dispatch>; - toggleTheme: () => void; - - tableStyle: TableStyle; - setTableStyle: React.Dispatch>; - toggleTableStyle: () => void; - - mapState: MapState; - setMapCenter: (center: LatLngTuple) => void; - setMapZoom: (zoom: number) => void; - setUserLocation: (location: LatLngTuple | null) => void; - setLocationPermission: (hasPermission: boolean) => void; - updateMapState: (center: LatLngTuple, zoom: number) => void; - - mapPositionMode: MapPositionMode; - setMapPositionMode: (mode: MapPositionMode) => void; -} - -// Coordenadas por defecto centradas en Vigo -const DEFAULT_CENTER: LatLngTuple = [42.229188855975046, -8.72246955783102]; -const DEFAULT_ZOOM = 14; - -const AppContext = createContext(undefined); - -export const AppProvider = ({ children }: { children: ReactNode }) => { - //#region Theme - const [theme, setTheme] = useState(() => { - const savedTheme = localStorage.getItem('theme'); - if (savedTheme) { - return savedTheme as Theme; - } - const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - return prefersDark ? 'dark' : 'light'; - }); - - const toggleTheme = () => { - setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); - }; - - useEffect(() => { - document.documentElement.setAttribute('data-theme', theme); - localStorage.setItem('theme', theme); - }, [theme]); - //#endregion - - //#region Table Style - const [tableStyle, setTableStyle] = useState(() => { - const savedTableStyle = localStorage.getItem('tableStyle'); - if (savedTableStyle) { - return savedTableStyle as TableStyle; - } - return 'regular'; - }); - - const toggleTableStyle = () => { - setTableStyle((prevTableStyle) => (prevTableStyle === 'regular' ? 'grouped' : 'regular')); - } - - useEffect(() => { - localStorage.setItem('tableStyle', tableStyle); - }, [tableStyle]); - //#endregion - - //#region Map Position Mode - const [mapPositionMode, setMapPositionMode] = useState(() => { - const saved = localStorage.getItem('mapPositionMode'); - return saved === 'last' ? 'last' : 'gps'; - }); - - useEffect(() => { - localStorage.setItem('mapPositionMode', mapPositionMode); - }, [mapPositionMode]); - //#endregion - - //#region Map State - const [mapState, setMapState] = useState(() => { - const savedMapState = localStorage.getItem('mapState'); - if (savedMapState) { - try { - const parsed = JSON.parse(savedMapState); - return { - center: parsed.center || DEFAULT_CENTER, - zoom: parsed.zoom || DEFAULT_ZOOM, - userLocation: parsed.userLocation || null, - hasLocationPermission: parsed.hasLocationPermission || false - }; - } catch (e) { - console.error('Error parsing saved map state', e); - } - } - return { - center: DEFAULT_CENTER, - zoom: DEFAULT_ZOOM, - userLocation: null, - hasLocationPermission: false - }; - }); - - // Helper: check if coordinates are within Vigo bounds - function isWithinVigo([lat, lng]: LatLngTuple): boolean { - // Rough bounding box for Vigo - return lat >= 42.18 && lat <= 42.30 && lng >= -8.78 && lng <= -8.65; - } - - // On app load, if mapPositionMode is 'gps', try to get GPS and set map center - useEffect(() => { - if (mapPositionMode === 'gps') { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - (position) => { - const { latitude, longitude } = position.coords; - const coords: LatLngTuple = [latitude, longitude]; - if (isWithinVigo(coords)) { - setMapState(prev => { - const newState = { ...prev, center: coords, zoom: 16, userLocation: coords }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - } - }, - () => { - // Ignore error, fallback to last - } - ); - } - } - // If 'last', do nothing (already loaded from localStorage) - }, [mapPositionMode]); - - const setMapCenter = (center: LatLngTuple) => { - setMapState(prev => { - const newState = { ...prev, center }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - }; - - const setMapZoom = (zoom: number) => { - setMapState(prev => { - const newState = { ...prev, zoom }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - }; - - const setUserLocation = (userLocation: LatLngTuple | null) => { - setMapState(prev => { - const newState = { ...prev, userLocation }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - }; - - const setLocationPermission = (hasLocationPermission: boolean) => { - setMapState(prev => { - const newState = { ...prev, hasLocationPermission }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - }; - - const updateMapState = (center: LatLngTuple, zoom: number) => { - setMapState(prev => { - const newState = { ...prev, center, zoom }; - localStorage.setItem('mapState', JSON.stringify(newState)); - return newState; - }); - }; - //#endregion - - // Tratar de obtener la ubicación del usuario cuando se carga la aplicación si ya se había concedido permiso antes - useEffect(() => { - if (mapState.hasLocationPermission && !mapState.userLocation) { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - (position) => { - const { latitude, longitude } = position.coords; - setUserLocation([latitude, longitude]); - }, - (error) => { - console.error('Error getting location:', error); - setLocationPermission(false); - } - ); - } - } - }, [mapState.hasLocationPermission, mapState.userLocation]); - - return ( - - {children} - - ); -}; - -export const useApp = () => { - const context = useContext(AppContext); - if (!context) { - throw new Error('useApp must be used within a AppProvider'); - } - return context; -}; \ No newline at end of file diff --git a/src/Costasdev.Busurbano.Backend/.gitignore b/src/Costasdev.Busurbano.Backend/.gitignore new file mode 100644 index 0000000..ff5b00c --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj new file mode 100644 index 0000000..517a253 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj @@ -0,0 +1,13 @@ + + + net9.0 + enable + enable + Costasdev.Busurbano.Backend + 0eff397c-f98e-4c3a-95ec-9d900f1b938c + + + + + + \ No newline at end of file diff --git a/src/Costasdev.Busurbano.Backend/GetStopEstimates.cs b/src/Costasdev.Busurbano.Backend/GetStopEstimates.cs new file mode 100644 index 0000000..7fe77e1 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/GetStopEstimates.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using Costasdev.VigoTransitApi; + +namespace Costasdev.Busurbano.Backend; + +[ApiController] +[Route("api")] +public class ApiController : ControllerBase +{ + private readonly VigoTransitApiClient _api; + + public ApiController(HttpClient http) + { + _api = new VigoTransitApiClient(http); + } + + [HttpGet("GetStopEstimates")] + public async Task Run() + { + var argumentAvailable = Request.Query.TryGetValue("id", out var requestedStopIdString); + if (!argumentAvailable) + { + return new BadRequestObjectResult("Please provide a stop id"); + } + + var argumentNumber = int.TryParse(requestedStopIdString, out var requestedStopId); + if (!argumentNumber) + { + return new BadRequestObjectResult("Please provide a valid stop id"); + } + + try + { + var estimates = await _api.GetStopEstimates(requestedStopId); + return new OkObjectResult(estimates); + } + catch (InvalidOperationException) + { + return new BadRequestObjectResult("Stop not found"); + } + } +} + diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs new file mode 100644 index 0000000..a394282 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Program.cs @@ -0,0 +1,10 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient(); + +var app = builder.Build(); + +app.MapControllers(); + +app.Run(); diff --git a/src/Costasdev.Busurbano.Backend/Properties/launchSettings.json b/src/Costasdev.Busurbano.Backend/Properties/launchSettings.json new file mode 100644 index 0000000..783d5cc --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "urbanovigoWeb": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7240", + "hotReloadEnabled": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + } + } + } +} diff --git a/src/Costasdev.Busurbano.Backend/appsettings.json b/src/Costasdev.Busurbano.Backend/appsettings.json new file mode 100644 index 0000000..23160a4 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx deleted file mode 100644 index 2372f9b..0000000 --- a/src/ErrorBoundary.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { Component, ReactNode } from 'react'; - -interface ErrorBoundaryProps { - children: ReactNode; -} - -interface ErrorBoundaryState { - hasError: boolean; - error: Error | null; -} - -class ErrorBoundary extends Component { - constructor(props: ErrorBoundaryProps) { - super(props); - this.state = { - hasError: false, - error: null - }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { - hasError: true, - error - }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error("Uncaught error:", error, errorInfo); - } - - render() { - if (this.state.hasError) { - return <> -

Something went wrong.

-
-          {this.state.error?.stack}
-        
- ; - } - - return this.props.children; - } -} - -export default ErrorBoundary; \ No newline at end of file diff --git a/src/Layout.css b/src/Layout.css deleted file mode 100644 index 0148da4..0000000 --- a/src/Layout.css +++ /dev/null @@ -1,60 +0,0 @@ -.app-container { - display: flex; - flex-direction: column; - height: 100vh; - width: 100%; - overflow: hidden; -} - -.main-content { - flex: 1; - overflow: auto; - padding-bottom: 60px; /* Extra padding to ensure content isn't hidden behind navbar */ -} - -.nav-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 5; - - background-color: var(--background-color); - display: flex; - justify-content: space-around; - align-items: center; - height: 60px; - border-top: 1px solid var(--border-color); - box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); -} - -.nav-item { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 8px; - color: #616161; - text-decoration: none; - width: 33.3%; - font-size: 14px; -} - -.nav-item.active { - color: var(--button-background-color); -} - -.theme-toggle { - background: none; - border: none; - cursor: pointer; - color: inherit; - display: flex; - align-items: center; - justify-content: center; - padding: 8px; -} - -.theme-toggle:hover { - color: var(--button-hover-background-color); -} \ No newline at end of file diff --git a/src/Layout.tsx b/src/Layout.tsx deleted file mode 100644 index 2a7816c..0000000 --- a/src/Layout.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { ReactNode } from 'react'; -import { Link, useLocation } from 'react-router'; -import { MapPin, Map, Settings } from 'lucide-react'; -import './Layout.css'; - -interface LayoutProps { - children: ReactNode; -} - -export function Layout({ children }: LayoutProps) { - const location = useLocation(); - - const navItems = [ - { - name: 'Paradas', - icon: MapPin, - path: '/stops' - }, - { - name: 'Mapa', - icon: Map, - path: '/map' - }, - { - name: 'Ajustes', - icon: Settings, - path: '/settings' - } - ]; - - return ( -
-
- {children} -
- -
- ); -} \ No newline at end of file diff --git a/src/components/GroupedTable.tsx b/src/components/GroupedTable.tsx deleted file mode 100644 index 58bb5ed..0000000 --- a/src/components/GroupedTable.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { StopDetails } from "../pages/Estimates"; -import LineIcon from "./LineIcon"; - -interface GroupedTable { - data: StopDetails; - dataDate: Date | null; -} - -export const GroupedTable: React.FC = ({ data, dataDate }) => { - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} m`; - } - } - - const groupedEstimates = data.estimates.reduce((acc, estimate) => { - if (!acc[estimate.line]) { - acc[estimate.line] = []; - } - acc[estimate.line].push(estimate); - return acc; - }, {} as Record); - - const sortedLines = Object.keys(groupedEstimates).sort((a, b) => { - const firstArrivalA = groupedEstimates[a][0].minutes; - const firstArrivalB = groupedEstimates[b][0].minutes; - return firstArrivalA - firstArrivalB; - }); - - return - - - - - - - - - - - - - {sortedLines.map((line) => ( - groupedEstimates[line].map((estimate, idx) => ( - - {idx === 0 && ( - - )} - - - - - )) - ))} - - - {data?.estimates.length === 0 && ( - - - - - - )} -
Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}
LíneaRutaLlegadaDistancia
- - {estimate.route}{`${estimate.minutes} min`} - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : "No disponible" - } -
No hay estimaciones disponibles
-} \ No newline at end of file diff --git a/src/components/LineIcon.css b/src/components/LineIcon.css deleted file mode 100644 index e7e8949..0000000 --- a/src/components/LineIcon.css +++ /dev/null @@ -1,239 +0,0 @@ -:root { - --line-c1: rgb(237, 71, 19); - --line-c3d: rgb(255, 204, 0); - --line-c3i: rgb(255, 204, 0); - --line-l4a: rgb(0, 153, 0); - --line-l4c: rgb(0, 153, 0); - --line-l5a: rgb(0, 176, 240); - --line-l5b: rgb(0, 176, 240); - --line-l6: rgb(204, 51, 153); - --line-l7: rgb(150, 220, 153); - --line-l9b: rgb(244, 202, 140); - --line-l10: rgb(153, 51, 0); - --line-l11: rgb(226, 0, 38); - --line-l12a: rgb(106, 150, 190); - --line-l12b: rgb(106, 150, 190); - --line-l13: rgb(0, 176, 240); - --line-l14: rgb(129, 142, 126); - --line-l15a: rgb(216, 168, 206); - --line-l15b: rgb(216, 168, 206); - --line-l15c: rgb(216, 168, 168); - --line-l16: rgb(129, 142, 126); - --line-l17: rgb(214, 245, 31); - --line-l18a: rgb(212, 80, 168); - --line-l18b: rgb(0, 0, 0); - --line-l18h: rgb(0, 0, 0); - --line-l23: rgb(0, 70, 210); - --line-l24: rgb(191, 191, 191); - --line-l25: rgb(172, 100, 4); - --line-l27: rgb(112, 74, 42); - --line-l28: rgb(176, 189, 254); - --line-l29: rgb(248, 184, 90); - --line-l31: rgb(255, 255, 0); - --line-a: rgb(119, 41, 143); - --line-h: rgb(0, 96, 168); - --line-h1: rgb(0, 96, 168); - --line-h2: rgb(0, 96, 168); - --line-h3: rgb(0, 96, 168); - --line-lzd: rgb(61, 78, 167); - --line-n1: rgb(191, 191, 191); - --line-n4: rgb(102, 51, 102); - --line-psa1: rgb(0, 153, 0); - --line-psa4: rgb(0, 153, 0); - --line-ptl: rgb(150, 220, 153); - --line-turistico: rgb(102, 51, 102); - --line-u1: rgb(172, 100, 4); - --line-u2: rgb(172, 100, 4); -} - -.line-icon { - display: inline-block; - padding: 0.25rem 0.5rem; - margin-right: 0.5rem; - border-bottom: 3px solid; - font-size: 0.9rem; - font-weight: 600; - text-transform: uppercase; - color: inherit; - /* Prevent color change on hover */ -} - -.line-c1 { - border-color: var(--line-c1); -} - -.line-c3d { - border-color: var(--line-c3d); -} - -.line-c3i { - border-color: var(--line-c3i); -} - -.line-l4a { - border-color: var(--line-l4a); -} - -.line-l4c { - border-color: var(--line-l4c); -} - -.line-l5a { - border-color: var(--line-l5a); -} - -.line-l5b { - border-color: var(--line-l5b); -} - -.line-l6 { - border-color: var(--line-l6); -} - -.line-l7 { - border-color: var(--line-l7); -} - -.line-l9b { - border-color: var(--line-l9b); -} - -.line-l10 { - border-color: var(--line-l10); -} - -.line-l11 { - border-color: var(--line-l11); -} - -.line-l12a { - border-color: var(--line-l12a); -} - -.line-l12b { - border-color: var(--line-l12b); -} - -.line-l13 { - border-color: var(--line-l13); -} - -.line-l14 { - border-color: var(--line-l14); -} - -.line-l15a { - border-color: var(--line-l15a); -} - -.line-l15b { - border-color: var(--line-l15b); -} - -.line-l15c { - border-color: var(--line-l15c); -} - -.line-l16 { - border-color: var(--line-l16); -} - -.line-l17 { - border-color: var(--line-l17); -} - -.line-l18a { - border-color: var(--line-l18a); -} - -.line-l18b { - border-color: var(--line-l18b); -} - -.line-l18h { - border-color: var(--line-l18h); -} - -.line-l23 { - border-color: var(--line-l23); -} - -.line-l24 { - border-color: var(--line-l24); -} - -.line-l25 { - border-color: var(--line-l25); -} - -.line-l27 { - border-color: var(--line-l27); -} - -.line-l28 { - border-color: var(--line-l28); -} - -.line-l29 { - border-color: var(--line-l29); -} - -.line-l31 { - border-color: var(--line-l31); -} - -.line-a { - border-color: var(--line-a); -} - -.line-h { - border-color: var(--line-h); -} - -.line-h1 { - border-color: var(--line-h1); -} - -.line-h2 { - border-color: var(--line-h2); -} - -.line-h3 { - border-color: var(--line-h3); -} - -.line-lzd { - border-color: var(--line-lzd); -} - -.line-n1 { - border-color: var(--line-n1); -} - -.line-n4 { - border-color: var(--line-n4); -} - -.line-psa1 { - border-color: var(--line-psa1); -} - -.line-psa4 { - border-color: var(--line-psa4); -} - -.line-ptl { - border-color: var(--line-ptl); -} - -.line-turistico { - border-color: var(--line-turistico); -} - -.line-u1 { - border-color: var(--line-u1); -} - -.line-u2 { - border-color: var(--line-u2); -} \ No newline at end of file diff --git a/src/components/LineIcon.tsx b/src/components/LineIcon.tsx deleted file mode 100644 index 50fd1ec..0000000 --- a/src/components/LineIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import './LineIcon.css'; - -interface LineIconProps { - line: string; -} - -const LineIcon: React.FC = ({ line }) => { - const formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; - return ( - - {formattedLine} - - ); -}; - -export default LineIcon; \ No newline at end of file diff --git a/src/components/RegularTable.tsx b/src/components/RegularTable.tsx deleted file mode 100644 index 8f0605f..0000000 --- a/src/components/RegularTable.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { StopDetails } from "../pages/Estimates"; -import LineIcon from "./LineIcon"; - -interface RegularTableProps { - data: StopDetails; - dataDate: Date | null; -} - -export const RegularTable: React.FC = ({ data, dataDate }) => { - - const absoluteArrivalTime = (minutes: number) => { - const now = new Date() - const arrival = new Date(now.getTime() + minutes * 60000) - return Intl.DateTimeFormat(navigator.language, { - hour: '2-digit', - minute: '2-digit' - }).format(arrival) - } - - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} m`; - } - } - - return - - - - - - - - - - - - - {data.estimates - .sort((a, b) => a.minutes - b.minutes) - .map((estimate, idx) => ( - - - - - - - ))} - - - {data?.estimates.length === 0 && ( - - - - - - )} -
Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}
LíneaRutaLlegadaDistancia
{estimate.route} - {estimate.minutes > 15 - ? absoluteArrivalTime(estimate.minutes) - : `${estimate.minutes} min`} - - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : "No disponible" - } -
No hay estimaciones disponibles
-} \ No newline at end of file diff --git a/src/components/StopItem.css b/src/components/StopItem.css deleted file mode 100644 index 9feb2d1..0000000 --- a/src/components/StopItem.css +++ /dev/null @@ -1,54 +0,0 @@ -/* Stop Item Styling */ - -.stop-notes { - font-size: 0.85rem; - font-style: italic; - color: #666; - margin: 2px 0; -} - -.stop-amenities { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 4px; -} - -.amenity-tag { - font-size: 0.75rem; - background-color: #e8f4f8; - color: #0078d4; - border-radius: 4px; - padding: 2px 6px; - display: inline-block; -} - -/* Different colors for different amenity types */ -.amenity-tag[data-amenity="shelter"] { - background-color: #e3f1df; - color: #107c41; -} - -.amenity-tag[data-amenity="bench"] { - background-color: #f0e8fc; - color: #5c2e91; -} - -.amenity-tag[data-amenity="real-time display"] { - background-color: #fff4ce; - color: #986f0b; -} - -/* When there are alternate names available, show an indicator */ -.has-alternate-names { - position: relative; -} - -.has-alternate-names::after { - content: "⋯"; - position: absolute; - right: -15px; - top: 0; - color: #0078d4; - font-weight: bold; -} \ No newline at end of file diff --git a/src/components/StopItem.tsx b/src/components/StopItem.tsx deleted file mode 100644 index cf9ccfc..0000000 --- a/src/components/StopItem.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router'; -import StopDataProvider, { Stop } from '../data/StopDataProvider'; -import LineIcon from './LineIcon'; - -interface StopItemProps { - stop: Stop; -} - -const StopItem: React.FC = ({ stop }) => { - - return ( -
  • - - {stop.favourite && } ({stop.stopId}) {StopDataProvider.getDisplayName(stop)} -
    - {stop.lines?.map(line => )} -
    - - -
  • - ); -}; - -export default StopItem; \ No newline at end of file diff --git a/src/controls/LocateControl.ts b/src/controls/LocateControl.ts deleted file mode 100644 index b8c2d1d..0000000 --- a/src/controls/LocateControl.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createControlComponent } from '@react-leaflet/core'; -import { LocateControl as LeafletLocateControl, LocateOptions } from 'leaflet.locatecontrol'; -import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; -import { useEffect } from 'react'; -import { useMap } from 'react-leaflet'; -import { useApp } from '../AppContext'; - -interface EnhancedLocateControlProps { - options?: LocateOptions; -} - -// Componente que usa el contexto para manejar la localización -export const EnhancedLocateControl = (props: EnhancedLocateControlProps) => { - const map = useMap(); - const { mapState, setUserLocation, setLocationPermission } = useApp(); - - useEffect(() => { - // Configuración por defecto del control de localización - const defaultOptions: LocateOptions = { - position: 'topright', - strings: { - title: 'Mostrar mi ubicación', - }, - flyTo: true, - onLocationError: (err) => { - console.error('Error en la localización:', err); - setLocationPermission(false); - }, - returnToPrevBounds: true, - showPopup: false, - }; - - // Combinamos las opciones por defecto con las personalizadas - const options = { ...defaultOptions, ...props.options }; - - // Creamos la instancia del control - const locateControl = new LeafletLocateControl(options); - - // Añadimos el control al mapa - locateControl.addTo(map); - - // Si tenemos permiso de ubicación y ya conocemos la ubicación del usuario, - // podemos activarla automáticamente - if (mapState.hasLocationPermission && mapState.userLocation) { - // Esperamos a que el mapa esté listo - setTimeout(() => { - try { - locateControl.start(); - } catch (e) { - console.error('Error al iniciar la localización automática', e); - } - }, 1000); - } - - return () => { - // Limpieza al desmontar el componente - locateControl.remove(); - }; - }, [map, mapState.hasLocationPermission, mapState.userLocation, props.options, setLocationPermission, setUserLocation]); - - return null; -}; - -// Exportamos también el control base por compatibilidad -export const LocateControl = createControlComponent( - (props) => new LeafletLocateControl(props) -); \ No newline at end of file diff --git a/src/data/StopDataProvider.ts b/src/data/StopDataProvider.ts deleted file mode 100644 index 0c1e46e..0000000 --- a/src/data/StopDataProvider.ts +++ /dev/null @@ -1,160 +0,0 @@ -export interface CachedStopList { - timestamp: number; - data: Stop[]; -} - -export type StopName = { - original: string; - intersect?: string; -} - -export interface Stop { - stopId: number; - name: StopName; - latitude?: number; - longitude?: number; - lines: string[]; - favourite?: boolean; -} - -// In-memory cache and lookup map -let cachedStops: Stop[] | null = null; -let stopsMap: Record = {}; -// Custom names loaded from localStorage -let customNames: Record = {}; - -// Initialize cachedStops and customNames once -async function initStops() { - if (!cachedStops) { - const response = await fetch('/stops.json'); - const stops = await response.json() as Stop[]; - // build array and map - stopsMap = {}; - cachedStops = stops.map(stop => { - const entry = { ...stop, favourite: false } as Stop; - stopsMap[stop.stopId] = entry; - return entry; - }); - // load custom names - const rawCustom = localStorage.getItem('customStopNames'); - if (rawCustom) customNames = JSON.parse(rawCustom) as Record; - } -} - -async function getStops(): Promise { - await initStops(); - // update favourites - const rawFav = localStorage.getItem('favouriteStops'); - const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : []; - cachedStops!.forEach(stop => stop.favourite = favouriteStops.includes(stop.stopId)); - return cachedStops!; -} - -// New: get single stop by id -async function getStopById(stopId: number): Promise { - await initStops(); - const stop = stopsMap[stopId]; - if (stop) { - const rawFav = localStorage.getItem('favouriteStops'); - const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : []; - stop.favourite = favouriteStops.includes(stopId); - } - return stop; -} - -// Updated display name to include custom names -function getDisplayName(stop: Stop): string { - if (customNames[stop.stopId]) return customNames[stop.stopId]; - const nameObj = stop.name; - return nameObj.intersect || nameObj.original; -} - -// New: set or remove custom names -function setCustomName(stopId: number, label: string) { - customNames[stopId] = label; - localStorage.setItem('customStopNames', JSON.stringify(customNames)); -} - -function removeCustomName(stopId: number) { - delete customNames[stopId]; - localStorage.setItem('customStopNames', JSON.stringify(customNames)); -} - -// New: get custom label for a stop -function getCustomName(stopId: number): string | undefined { - return customNames[stopId]; -} - -function addFavourite(stopId: number) { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - let favouriteStops: number[] = []; - if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - } - - if (!favouriteStops.includes(stopId)) { - favouriteStops.push(stopId); - localStorage.setItem('favouriteStops', JSON.stringify(favouriteStops)); - } -} - -function removeFavourite(stopId: number) { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - let favouriteStops: number[] = []; - if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - } - - const newFavouriteStops = favouriteStops.filter(id => id !== stopId); - localStorage.setItem('favouriteStops', JSON.stringify(newFavouriteStops)); -} - -function isFavourite(stopId: number): boolean { - const rawFavouriteStops = localStorage.getItem('favouriteStops'); - if (rawFavouriteStops) { - const favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - return favouriteStops.includes(stopId); - } - return false; -} - -const RECENT_STOPS_LIMIT = 10; - -function pushRecent(stopId: number) { - const rawRecentStops = localStorage.getItem('recentStops'); - let recentStops: Set = new Set(); - if (rawRecentStops) { - recentStops = new Set(JSON.parse(rawRecentStops) as number[]); - } - - recentStops.add(stopId); - if (recentStops.size > RECENT_STOPS_LIMIT) { - const iterator = recentStops.values(); - const val = iterator.next().value as number; - recentStops.delete(val); - } - - localStorage.setItem('recentStops', JSON.stringify(Array.from(recentStops))); -} - -function getRecent(): number[] { - const rawRecentStops = localStorage.getItem('recentStops'); - if (rawRecentStops) { - return JSON.parse(rawRecentStops) as number[]; - } - return []; -} - -export default { - getStops, - getStopById, - getCustomName, - getDisplayName, - setCustomName, - removeCustomName, - addFavourite, - removeFavourite, - isFavourite, - pushRecent, - getRecent -}; diff --git a/src/frontend/frontend.esproj b/src/frontend/frontend.esproj new file mode 100644 index 0000000..1afb3fb --- /dev/null +++ b/src/frontend/frontend.esproj @@ -0,0 +1,22 @@ + + + npm run dev + npm run build + $(MSBuildProjectDirectory)\build + + + + + + + + + + + + + + + + + diff --git a/src/frontend/index.html b/src/frontend/index.html new file mode 100644 index 0000000..4812ce5 --- /dev/null +++ b/src/frontend/index.html @@ -0,0 +1,55 @@ + + + + + + + + UrbanoVigo Web + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json new file mode 100644 index 0000000..b18eeb4 --- /dev/null +++ b/src/frontend/package-lock.json @@ -0,0 +1,4083 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@fontsource-variable/outfit": "^5.2.5", + "fuse.js": "^7.1.0", + "leaflet": "^1.9.4", + "leaflet.locatecontrol": "^0.84.2", + "leaflet.markercluster": "^1.5.3", + "lucide-react": "^0.510.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-leaflet": "^5.0.0", + "react-leaflet-markercluster": "^5.0.0-rc.0", + "react-router": "^7.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.26.0", + "@types/leaflet": "^1.9.17", + "@types/node": "^22.15.17", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.4", + "@vitejs/plugin-react-swc": "^3.9.0", + "eslint": "^9.26.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.1.0", + "jiti": "^2.4.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.0", + "vite": "^6.3.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fontsource-variable/outfit": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.2.5.tgz", + "integrity": "sha512-MejrIp6Cbmd3u5AZtsot8kmhZiyQM5CATsdcBB2hktYIrv5CVekRSUmFDXL4FpF7K70IvFp2ZMfImm5VhnVf7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", + "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz", + "integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", + "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.4.tgz", + "integrity": "sha512-WxYAszDYgsMV31OVyoG4jbAgJI1Gw0Xq9V19zwhy6+hUUJlJIdZ3r/cbdmTqFv++SktQkZ/X+46yGFxp5XJBEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.11.21" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "zod": "^3.24.2" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet.locatecontrol": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.84.2.tgz", + "integrity": "sha512-Tv0S2bAhpFgZYyyfPgeVhb3hPr9CnlcP15EpMQd9m5vA+aALhM6key1ucfnnD7n09AEeNEmIFe71T4V18Kpu7g==", + "license": "MIT" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.510.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.510.0.tgz", + "integrity": "sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/react-leaflet-markercluster": { + "version": "5.0.0-rc.0", + "resolved": "https://registry.npmjs.org/react-leaflet-markercluster/-/react-leaflet-markercluster-5.0.0-rc.0.tgz", + "integrity": "sha512-jWa4bPD5LfLV3Lid1RWgl+yKUuQtnqeYtJzzLb/fiRjvX+rtwzY8pMoUFuygqyxNrWxMTQlWKBHxkpI7Sxvu4Q==", + "license": "MIT", + "dependencies": { + "@react-leaflet/core": "^3.0.0", + "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", + "react-leaflet": "^5.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", + "react": "^19.0.0", + "react-leaflet": "^5.0.0" + } + }, + "node_modules/react-router": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", + "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.0", + "@typescript-eslint/parser": "8.32.0", + "@typescript-eslint/utils": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/src/frontend/package.json b/src/frontend/package.json new file mode 100644 index 0000000..546b9cf --- /dev/null +++ b/src/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "engines": { + "node": ">=20.0.0" + }, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource-variable/outfit": "^5.2.5", + "fuse.js": "^7.1.0", + "leaflet": "^1.9.4", + "leaflet.locatecontrol": "^0.84.2", + "leaflet.markercluster": "^1.5.3", + "lucide-react": "^0.510.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-leaflet": "^5.0.0", + "react-leaflet-markercluster": "^5.0.0-rc.0", + "react-router": "^7.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.26.0", + "@types/leaflet": "^1.9.17", + "@types/node": "^22.15.17", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.4", + "@vitejs/plugin-react-swc": "^3.9.0", + "eslint": "^9.26.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.1.0", + "jiti": "^2.4.2", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.0", + "vite": "^6.3.5" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "*" + } +} diff --git a/src/frontend/public/favicon.ico b/src/frontend/public/favicon.ico new file mode 100644 index 0000000..b81c323 Binary files /dev/null and b/src/frontend/public/favicon.ico differ diff --git a/src/frontend/public/logo-256.jpg b/src/frontend/public/logo-256.jpg new file mode 100644 index 0000000..c823056 Binary files /dev/null and b/src/frontend/public/logo-256.jpg differ diff --git a/src/frontend/public/logo-256.png b/src/frontend/public/logo-256.png new file mode 100644 index 0000000..a1d6c25 Binary files /dev/null and b/src/frontend/public/logo-256.png differ diff --git a/src/frontend/public/logo-512.jpg b/src/frontend/public/logo-512.jpg new file mode 100644 index 0000000..cf45e80 Binary files /dev/null and b/src/frontend/public/logo-512.jpg differ diff --git a/src/frontend/public/manifest.webmanifest b/src/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..59fbab1 --- /dev/null +++ b/src/frontend/public/manifest.webmanifest @@ -0,0 +1,84 @@ +{ + "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/refs/heads/master/src/schemas/json/web-manifest.json", + "id": "https://busurbano.costas.dev/", + "name": "UrbanoVigo Web", + "description": "Aplicación web para encontrar paradas y tiempos de llegada de los autobuses urbanos de Vigo, España.", + "short_name": "UrbanoVigo", + "start_url": "/", + "display": "standalone", + "orientation": "portrait-primary", + "lang": "es", + "background_color": "#ffffff", + "theme_color": "#007bff", + "icons": [ + { + "src": "/logo-512.jpg", + "sizes": "512x512", + "type": "image/jpg", + "purpose": "any maskable" + }, + { + "src": "/logo-256.jpg", + "sizes": "256x256", + "type": "image/jpg", + "purpose": "any maskable" + }, + { + "src": "/logo-256.png", + "sizes": "256x256", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/favicon.ico", + "sizes": "64x64", + "type": "image/x-icon", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "/screenshots/stoplist-narrow.png", + "sizes": "1440x2960", + "type": "image/png", + "form_factor": "narrow", + "label": "Lista de paradas" + }, + { + "src": "/screenshots/map-narrow.png", + "sizes": "1440x2960", + "type": "image/png", + "form_factor": "narrow", + "label": "Mapa de paradas" + }, + { + "src": "/screenshots/estimates-narrow.png", + "sizes": "1440x2960", + "type": "image/png", + "form_factor": "narrow", + "label": "Estimaciones de llegada a parada" + }, + + { + "src": "/screenshots/stoplist-wide.jpeg", + "sizes": "1788x891", + "type": "image/jpeg", + "form_factor": "wide", + "label": "Lista de paradas" + }, + { + "src": "/screenshots/map-wide.jpeg", + "sizes": "1788x891", + "type": "image/jpeg", + "form_factor": "wide", + "label": "Mapa de paradas" + }, + { + "src": "/screenshots/estimates-wide.jpeg", + "sizes": "1788x891", + "type": "image/jpeg", + "form_factor": "wide", + "label": "Estimaciones de llegada a parada" + } + ] +} \ No newline at end of file diff --git a/src/frontend/public/map-pin-icon.png b/src/frontend/public/map-pin-icon.png new file mode 100644 index 0000000..0015b64 Binary files /dev/null and b/src/frontend/public/map-pin-icon.png differ diff --git a/src/frontend/public/screenshots/estimates-narrow.png b/src/frontend/public/screenshots/estimates-narrow.png new file mode 100644 index 0000000..0337442 Binary files /dev/null and b/src/frontend/public/screenshots/estimates-narrow.png differ diff --git a/src/frontend/public/screenshots/estimates-wide.jpeg b/src/frontend/public/screenshots/estimates-wide.jpeg new file mode 100644 index 0000000..e81f094 Binary files /dev/null and b/src/frontend/public/screenshots/estimates-wide.jpeg differ diff --git a/src/frontend/public/screenshots/map-narrow.png b/src/frontend/public/screenshots/map-narrow.png new file mode 100644 index 0000000..14199c4 Binary files /dev/null and b/src/frontend/public/screenshots/map-narrow.png differ diff --git a/src/frontend/public/screenshots/map-wide.jpeg b/src/frontend/public/screenshots/map-wide.jpeg new file mode 100644 index 0000000..6d3ca64 Binary files /dev/null and b/src/frontend/public/screenshots/map-wide.jpeg differ diff --git a/src/frontend/public/screenshots/stoplist-narrow.png b/src/frontend/public/screenshots/stoplist-narrow.png new file mode 100644 index 0000000..c1d9f25 Binary files /dev/null and b/src/frontend/public/screenshots/stoplist-narrow.png differ diff --git a/src/frontend/public/screenshots/stoplist-wide.jpeg b/src/frontend/public/screenshots/stoplist-wide.jpeg new file mode 100644 index 0000000..264e1a7 Binary files /dev/null and b/src/frontend/public/screenshots/stoplist-wide.jpeg differ diff --git a/src/frontend/public/stops.json b/src/frontend/public/stops.json new file mode 100644 index 0000000..f64dff8 --- /dev/null +++ b/src/frontend/public/stops.json @@ -0,0 +1,15037 @@ +[ + { + "stopId": 20, + "name": { + "original": "Rúa do Abade Juan de Bastos (fronte Asociación Veciños)" + }, + "latitude": 42.187593499, + "longitude": -8.741246641, + "lines": [ + "17" + ] + }, + { + "stopId": 40, + "name": { + "original": "Rúa do Abade Juan de Bastos (cruce Baixada da Moo)" + }, + "latitude": 42.192126677, + "longitude": -8.72901589, + "lines": [ + "17" + ] + }, + { + "stopId": 50, + "name": { + "original": "Rúa do Abade Juan de Bastos 24" + }, + "latitude": 42.19287042, + "longitude": -8.727513924, + "lines": [ + "17" + ] + }, + { + "stopId": 70, + "name": { + "original": "Rúa da Lagoa (cruce Camiño do Casmarcelo)" + }, + "latitude": 42.20020175, + "longitude": -8.700621608, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 80, + "name": { + "original": "Rúa da Lagoa 46" + }, + "latitude": 42.200132216, + "longitude": -8.700535777, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 90, + "name": { + "original": "Aeroporto de Peinador" + }, + "latitude": 42.225956918, + "longitude": -8.63286469, + "lines": [ + "A" + ] + }, + { + "stopId": 100, + "name": { + "original": "Avda. do Alcalde Lavadores 125" + }, + "latitude": 42.219008975, + "longitude": -8.69606935, + "lines": [ + "6" + ] + }, + { + "stopId": 110, + "name": { + "original": "Avda. do Alcalde Lavadores 171" + }, + "latitude": 42.215074591, + "longitude": -8.696738405, + "lines": [ + "6" + ] + }, + { + "stopId": 120, + "name": { + "original": "Avda. do Alcalde Lavadores 8" + }, + "latitude": 42.223288295, + "longitude": -8.700954873, + "lines": [ + "6" + ] + }, + { + "stopId": 130, + "name": { + "original": "Avda. do Alcalde Lavadores 102" + }, + "latitude": 42.219001694, + "longitude": -8.696198267, + "lines": [ + "6" + ] + }, + { + "stopId": 140, + "name": { + "original": "Avda. do Alcalde Lavadores 29" + }, + "latitude": 42.223444913, + "longitude": -8.700801996, + "lines": [ + "6" + ] + }, + { + "stopId": 150, + "name": { + "original": "Avda. do Alcalde Lavadores 48" + }, + "latitude": 42.222636676, + "longitude": -8.697201413, + "lines": [ + "6" + ] + }, + { + "stopId": 160, + "name": { + "original": "Avda. do Alcalde Lavadores 67" + }, + "latitude": 42.222830286, + "longitude": -8.697231476, + "lines": [ + "6" + ] + }, + { + "stopId": 170, + "name": { + "original": "Avda. do Alcalde Lavadores 152" + }, + "latitude": 42.215084316, + "longitude": -8.696854931, + "lines": [ + "6" + ] + }, + { + "stopId": 180, + "name": { + "original": "Estrada de Valadares 451" + }, + "latitude": 42.166144986, + "longitude": -8.720162371, + "lines": [ + "7" + ] + }, + { + "stopId": 190, + "name": { + "original": "Rúa de Ángel de Lema 58" + }, + "latitude": 42.250539537, + "longitude": -8.685179363, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 195, + "name": { + "original": "Rúa de Ángel de Lema 247" + }, + "latitude": 42.256624708, + "longitude": -8.677490797, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 200, + "name": { + "original": "Rúa de Ángel de Lema 100" + }, + "latitude": 42.252115803, + "longitude": -8.683374373, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 210, + "name": { + "original": "Rúa de Ángel de Lema 140" + }, + "latitude": 42.255798748, + "longitude": -8.678507526, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 220, + "name": { + "original": "Rúa de Ángel de Lema 163" + }, + "latitude": 42.252694363, + "longitude": -8.68302903, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 230, + "name": { + "original": "Rúa de Ángel de Lema 14" + }, + "latitude": 42.248041601, + "longitude": -8.691024475, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 240, + "name": { + "original": "Rúa de Ángel de Lema 19" + }, + "latitude": 42.247513476, + "longitude": -8.691874301, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 250, + "name": { + "original": "Rúa de Ángel de Lema 221" + }, + "latitude": 42.255252085, + "longitude": -8.679480662, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 260, + "name": { + "original": "Rúa de Ángel de Lema 91" + }, + "latitude": 42.250421216, + "longitude": -8.685464716, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 270, + "name": { + "original": "Rúa de Desiderio Pernas Arquitecto 1" + }, + "latitude": 42.18920151, + "longitude": -8.810340862, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 280, + "name": { + "original": "Rúa do Arquitecto Antonio Cominges 38" + }, + "latitude": 42.189490674, + "longitude": -8.808107114, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 290, + "name": { + "original": "Rúa do Arquitecto Gómez Román 37" + }, + "latitude": 42.190149471, + "longitude": -8.803788225, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 310, + "name": { + "original": "Rúa do Arquitecto Antonio Cominges 4" + }, + "latitude": 42.190850463, + "longitude": -8.80358845, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 320, + "name": { + "original": "Rúa do Arquitecto Antonio Cominges 70" + }, + "latitude": 42.189221331, + "longitude": -8.811730246, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 330, + "name": { + "original": "Rúa do Arquitecto Antonio Cominges 90" + }, + "latitude": 42.187213169, + "longitude": -8.813069201, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 340, + "name": { + "original": "Rúa de Aragón 116" + }, + "latitude": 42.238036494, + "longitude": -8.700921187, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 350, + "name": { + "original": "Rúa de Aragón 162" + }, + "latitude": 42.240488915, + "longitude": -8.700357923, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 360, + "name": { + "original": "Rúa de Aragón 193" + }, + "latitude": 42.24013184, + "longitude": -8.700947033, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 370, + "name": { + "original": "Rúa de Aragón 212" + }, + "latitude": 42.242101304, + "longitude": -8.698394546, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 380, + "name": { + "original": "Rúa de Aragón 221" + }, + "latitude": 42.242091376, + "longitude": -8.698668131, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 390, + "name": { + "original": "Rúa de Aragón 26" + }, + "latitude": 42.233174046, + "longitude": -8.702380309, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 400, + "name": { + "original": "Rúa de Aragón 91" + }, + "latitude": 42.235598195, + "longitude": -8.701426538, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 410, + "name": { + "original": "Rúa de Aragón 82" + }, + "latitude": 42.235524387, + "longitude": -8.701248417, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 420, + "name": { + "original": "Rúa de Aragón 147" + }, + "latitude": 42.238092485, + "longitude": -8.701156245, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 430, + "name": { + "original": "Rúa do Areal (Aduana)" + }, + "latitude": 42.239341996, + "longitude": -8.720234413, + "lines": [ + "A", + "6", + "9B", + "18A", + "24", + "28", + "H1" + ] + }, + { + "stopId": 530, + "name": { + "original": "Avda. de Ricardo Mella (Estación Coruxo)" + }, + "latitude": 42.193562859, + "longitude": -8.78173994, + "lines": [ + "12A" + ] + }, + { + "stopId": 540, + "name": { + "original": "Avda. de Ricardo Mella (fronte 223)" + }, + "latitude": 42.189424424, + "longitude": -8.790733064, + "lines": [ + "12A" + ] + }, + { + "stopId": 560, + "name": { + "original": "Avda. de Ricardo Mella 518" + }, + "latitude": 42.181015915, + "longitude": -8.807696921, + "lines": [ + "10" + ] + }, + { + "stopId": 570, + "name": { + "original": "Avda. de Ricardo Mella 250" + }, + "latitude": 42.195225102, + "longitude": -8.775226375, + "lines": [ + "12A" + ] + }, + { + "stopId": 572, + "name": { + "original": "Estrada de Madrid 210" + }, + "latitude": 42.214058797, + "longitude": -8.672946954, + "lines": [ + "12B", + "15B", + "15C", + "U2" + ] + }, + { + "stopId": 580, + "name": { + "original": "Avda. de Ricardo Mella 135" + }, + "latitude": 42.195766012, + "longitude": -8.773648966, + "lines": [ + "12A" + ] + }, + { + "stopId": 600, + "name": { + "original": "Avda. de Ricardo Mella 273" + }, + "latitude": 42.189927171, + "longitude": -8.800634184, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 620, + "name": { + "original": "Avda. de Ricado Mella 165" + }, + "latitude": 42.1935678, + "longitude": -8.781529566, + "lines": [ + "12A" + ] + }, + { + "stopId": 630, + "name": { + "original": "Avda. de Ricardo Mella 223" + }, + "latitude": 42.189304527, + "longitude": -8.79068363, + "lines": [ + "12A" + ] + }, + { + "stopId": 650, + "name": { + "original": "Avda. de Ricardo Mella 289" + }, + "latitude": 42.181065441, + "longitude": -8.807509871, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 660, + "name": { + "original": "Avda. do Alcalde Portanet 34" + }, + "latitude": 42.211494566, + "longitude": -8.736022397, + "lines": [ + "7", + "12B", + "17", + "H1" + ] + }, + { + "stopId": 680, + "name": { + "original": "Avda. do Aeroporto (Aeroclub)" + }, + "latitude": 42.229005723, + "longitude": -8.634356866, + "lines": [ + "A" + ] + }, + { + "stopId": 690, + "name": { + "original": "Avda. do Aeroporto 656" + }, + "latitude": 42.233064093, + "longitude": -8.642742935, + "lines": [ + "A", + "25" + ] + }, + { + "stopId": 700, + "name": { + "original": "Avda. do Aeroporto (Colexio)" + }, + "latitude": 42.228674047, + "longitude": -8.633340309, + "lines": [ + "A" + ] + }, + { + "stopId": 710, + "name": { + "original": "Rúa de Aragón (Instituto)" + }, + "latitude": 42.232478958, + "longitude": -8.701988706, + "lines": [ + "A", + "4A", + "9B", + "24", + "27", + "28" + ] + }, + { + "stopId": 720, + "name": { + "original": "Avda. do Aeroporto 215" + }, + "latitude": 42.235739016, + "longitude": -8.684254232, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 730, + "name": { + "original": "Avda. do Aeroporto 130" + }, + "latitude": 42.231109162, + "longitude": -8.690501398, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 740, + "name": { + "original": "Avda. do Aeroporto 181" + }, + "latitude": 42.233560754, + "longitude": -8.686937524, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 750, + "name": { + "original": "Avda. do Aeroporto 184" + }, + "latitude": 42.233103986, + "longitude": -8.68716283, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 760, + "name": { + "original": "Avda. do Aeroporto 240" + }, + "latitude": 42.236775611, + "longitude": -8.683736566, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 770, + "name": { + "original": "Avda. do Aeroporto 273" + }, + "latitude": 42.238939528, + "longitude": -8.681422497, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 780, + "name": { + "original": "Avda. do Aeroporto 298" + }, + "latitude": 42.238554288, + "longitude": -8.680663432, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 790, + "name": { + "original": "Avda. do Aeroporto 325" + }, + "latitude": 42.237426811, + "longitude": -8.675474476, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 800, + "name": { + "original": "Avda. do Aeroporto 328" + }, + "latitude": 42.237801674, + "longitude": -8.676524783, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 810, + "name": { + "original": "Avda. do Aeroporto 350" + }, + "latitude": 42.235521261, + "longitude": -8.67465521, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 820, + "name": { + "original": "Avda. do Aeroporto 377" + }, + "latitude": 42.234766626, + "longitude": -8.671305131, + "lines": [ + "A", + "9B" + ] + }, + { + "stopId": 830, + "name": { + "original": "Avda. do Aeroporto 378" + }, + "latitude": 42.234673289, + "longitude": -8.671348046, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 840, + "name": { + "original": "Avda. do Aeroporto 43" + }, + "latitude": 42.234904325, + "longitude": -8.699245802, + "lines": [ + "A", + "4A", + "9B", + "24", + "27", + "28" + ] + }, + { + "stopId": 850, + "name": { + "original": "Avda. do Aeroporto 423" + }, + "latitude": 42.23630176, + "longitude": -8.665791599, + "lines": [ + "A", + "9B" + ] + }, + { + "stopId": 860, + "name": { + "original": "Avda. do Aeroporto 446" + }, + "latitude": 42.235612667, + "longitude": -8.666529207, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 870, + "name": { + "original": "Avda. do Aeroporto 447" + }, + "latitude": 42.23543058, + "longitude": -8.66197943, + "lines": [ + "A", + "9B" + ] + }, + { + "stopId": 880, + "name": { + "original": "Avda. do Aeroporto 484" + }, + "latitude": 42.23544051, + "longitude": -8.662354939, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 890, + "name": { + "original": "Avda. do Aeroporto 491" + }, + "latitude": 42.232066419, + "longitude": -8.653842977, + "lines": [ + "A" + ] + }, + { + "stopId": 900, + "name": { + "original": "Avda. do Aeroporto 531" + }, + "latitude": 42.233527998, + "longitude": -8.648237616, + "lines": [ + "A" + ] + }, + { + "stopId": 910, + "name": { + "original": "Avda. do Aeroporto 54" + }, + "latitude": 42.234679919, + "longitude": -8.699623994, + "lines": [ + "A", + "4A", + "9B", + "24", + "27", + "28" + ] + }, + { + "stopId": 920, + "name": { + "original": "Avda. do Aeroporto (cruce Camiño das Cereixeiras)" + }, + "latitude": 42.233499069, + "longitude": -8.643325214, + "lines": [ + "A" + ] + }, + { + "stopId": 930, + "name": { + "original": "Avda. do Aeroporto 570" + }, + "latitude": 42.231979036, + "longitude": -8.65372496, + "lines": [ + "A" + ] + }, + { + "stopId": 940, + "name": { + "original": "Avda. do Aeroporto 605" + }, + "latitude": 42.230493878, + "longitude": -8.638023273, + "lines": [ + "A" + ] + }, + { + "stopId": 950, + "name": { + "original": "Avda. do Aeroporto 614" + }, + "latitude": 42.233626818, + "longitude": -8.647811163, + "lines": [ + "A", + "25" + ] + }, + { + "stopId": 960, + "name": { + "original": "Avda. do Aeroporto 686" + }, + "latitude": 42.230918888, + "longitude": -8.638532893, + "lines": [ + "A" + ] + }, + { + "stopId": 970, + "name": { + "original": "Avda. do Aeroporto 91" + }, + "latitude": 42.232787318, + "longitude": -8.693473285, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 980, + "name": { + "original": "Avda. da Atlántida 99" + }, + "latitude": 42.221170087, + "longitude": -8.763656977, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 990, + "name": { + "original": "Avda. da Atlántida (fronte 148)" + }, + "latitude": 42.222451366, + "longitude": -8.769134894, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 1000, + "name": { + "original": "Avda. da Atlántida 109" + }, + "latitude": 42.221220508, + "longitude": -8.767194468, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 1010, + "name": { + "original": "Avda. da Atlántida 136" + }, + "latitude": 42.221479642, + "longitude": -8.767482698, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 1020, + "name": { + "original": "Avda. da Atlántida 150" + }, + "latitude": 42.222764778, + "longitude": -8.769405842, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 1030, + "name": { + "original": "Avda. da Atlántida 25" + }, + "latitude": 42.223219677, + "longitude": -8.754753277, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 1040, + "name": { + "original": "Avda. da Atlántida 32" + }, + "latitude": 42.223237503, + "longitude": -8.755707801, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 1050, + "name": { + "original": "Avda. da Atlántida 71" + }, + "latitude": 42.221875354, + "longitude": -8.760935381, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 1060, + "name": { + "original": "Avda. da Atlántida 84" + }, + "latitude": 42.221789505, + "longitude": -8.759905458, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 1070, + "name": { + "original": "Avda. da Atlántida 114" + }, + "latitude": 42.221148357, + "longitude": -8.764660969, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 1110, + "name": { + "original": "Praza Ribeira do Berbés" + }, + "latitude": 42.237821273, + "longitude": -8.729666379, + "lines": [ + "A", + "5B", + "6" + ] + }, + { + "stopId": 1120, + "name": { + "original": "Avda. de Beiramar (fronte Casa do Mar)" + }, + "latitude": 42.23416729, + "longitude": -8.733331094, + "lines": [ + "6", + "9B", + "15B", + "28" + ] + }, + { + "stopId": 1130, + "name": { + "original": "Avda. de Beiramar (Peiraos auxiliares)" + }, + "latitude": 42.231238831, + "longitude": -8.735255297, + "lines": [ + "6", + "9B", + "15B", + "28" + ] + }, + { + "stopId": 1140, + "name": { + "original": "Avda. de Beiramar (Freire)" + }, + "latitude": 42.225068475, + "longitude": -8.74774586, + "lines": [ + "6", + "9B", + "28" + ] + }, + { + "stopId": 1150, + "name": { + "original": "Rúa da Ribeira do Berbés" + }, + "latitude": 42.237384264, + "longitude": -8.729603006, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "10", + "15B", + "15C", + "28", + "N4" + ] + }, + { + "stopId": 1160, + "name": { + "original": "Avda. de Beiramar (Sto. Domingo)" + }, + "latitude": 42.225759663, + "longitude": -8.743239749, + "lines": [ + "6", + "9B", + "28" + ] + }, + { + "stopId": 1200, + "name": { + "original": "Avda. de Beiramar 51" + }, + "latitude": 42.234233798, + "longitude": -8.73312316, + "lines": [ + "10", + "15B" + ] + }, + { + "stopId": 1210, + "name": { + "original": "Avda. de Beiramar 61" + }, + "latitude": 42.230811976, + "longitude": -8.735364934, + "lines": [ + "10", + "15B" + ] + }, + { + "stopId": 1220, + "name": { + "original": "Avda. de Buenos Aires 46" + }, + "latitude": 42.247097055, + "longitude": -8.693109251, + "lines": [ + "5A", + "10", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 1230, + "name": { + "original": "Avda. de Buenos Aires 49" + }, + "latitude": 42.247251925, + "longitude": -8.693122662, + "lines": [ + "5B", + "10", + "N1", + "H3" + ] + }, + { + "stopId": 1240, + "name": { + "original": "Avda. de Buenos Aires 8" + }, + "latitude": 42.249128205, + "longitude": -8.69514773, + "lines": [ + "5A", + "10", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 1250, + "name": { + "original": "Avda. de Castelao 16" + }, + "latitude": 42.219730396, + "longitude": -8.737456513, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 1260, + "name": { + "original": "Avda. de Castelao 21" + }, + "latitude": 42.219775977, + "longitude": -8.736255523, + "lines": [ + "C3i", + "4A", + "4C", + "5B", + "10", + "11", + "12A", + "15A", + "N1", + "U1" + ] + }, + { + "stopId": 1270, + "name": { + "original": "Avda. de Castelao 50" + }, + "latitude": 42.218704937, + "longitude": -8.74254446, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 1280, + "name": { + "original": "Avda. de Castelao 41" + }, + "latitude": 42.218523315, + "longitude": -8.74223465, + "lines": [ + "C3i", + "4A", + "4C", + "10", + "11", + "12A", + "15A", + "N1", + "U1" + ] + }, + { + "stopId": 1290, + "name": { + "original": "Avda. de Castelao 54" + }, + "latitude": 42.218158263, + "longitude": -8.745797727, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "N4", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 1300, + "name": { + "original": "Avda. de Castelao 68" + }, + "latitude": 42.217466378, + "longitude": -8.751245499, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "N4", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 1310, + "name": { + "original": "Avda. de Castelao 73" + }, + "latitude": 42.217705528, + "longitude": -8.747753325, + "lines": [ + "C3i", + "4A", + "4C", + "10", + "11", + "12A", + "15A", + "N1", + "N4", + "U1" + ] + }, + { + "stopId": 1320, + "name": { + "original": "Avda. de Castelao 87" + }, + "latitude": 42.217302224, + "longitude": -8.751104752, + "lines": [ + "C3i", + "10", + "12A", + "N1", + "U1" + ] + }, + { + "stopId": 1330, + "name": { + "original": "Avda. de Castrelos (Pavillón)" + }, + "latitude": 42.219553947, + "longitude": -8.732509436, + "lines": [ + "A", + "16", + "23", + "27", + "H2" + ] + }, + { + "stopId": 1340, + "name": { + "original": "Avda. de Castrelos (Parque)" + }, + "latitude": 42.212870645, + "longitude": -8.732131792, + "lines": [ + "27", + "H2" + ] + }, + { + "stopId": 1350, + "name": { + "original": "Avda. de Castrelos 121" + }, + "latitude": 42.208026488, + "longitude": -8.7312098, + "lines": [ + "7", + "12B", + "17", + "27" + ] + }, + { + "stopId": 1360, + "name": { + "original": "Avda. de Castrelos 16" + }, + "latitude": 42.219613217, + "longitude": -8.732629194, + "lines": [ + "7", + "12B", + "17", + "27", + "H2", + "PTL" + ] + }, + { + "stopId": 1380, + "name": { + "original": "Avda. de Castrelos 179" + }, + "latitude": 42.20533568, + "longitude": -8.730078621, + "lines": [ + "7", + "12B", + "17", + "U1" + ] + }, + { + "stopId": 1390, + "name": { + "original": "Avda. de Castrelos 186" + }, + "latitude": 42.212735556, + "longitude": -8.732314182, + "lines": [ + "A", + "7", + "12B", + "17", + "27", + "U1", + "H2", + "H", + "PTL" + ] + }, + { + "stopId": 1400, + "name": { + "original": "Avda. de Castrelos 202" + }, + "latitude": 42.210706683, + "longitude": -8.732237372, + "lines": [ + "A", + "7", + "12B", + "17", + "27", + "U1", + "H1", + "H2", + "H", + "PTL" + ] + }, + { + "stopId": 1410, + "name": { + "original": "Avda. de Castrelos 13" + }, + "latitude": 42.218060161, + "longitude": -8.732450427, + "lines": [ + "A", + "16", + "23", + "27", + "H2" + ] + }, + { + "stopId": 1420, + "name": { + "original": "Avda. de Castrelos 297" + }, + "latitude": 42.201440099, + "longitude": -8.726409762, + "lines": [ + "7" + ] + }, + { + "stopId": 1430, + "name": { + "original": "Avda. de Castrelos 318" + }, + "latitude": 42.203408408, + "longitude": -8.728817983, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 1440, + "name": { + "original": "Avda. de Castrelos 339" + }, + "latitude": 42.198480135, + "longitude": -8.723827649, + "lines": [ + "7" + ] + }, + { + "stopId": 1450, + "name": { + "original": "Avda. de Castrelos 366" + }, + "latitude": 42.201044695, + "longitude": -8.726112037, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 1460, + "name": { + "original": "Avda. de Castrelos 396" + }, + "latitude": 42.198867605, + "longitude": -8.72460549, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 1470, + "name": { + "original": "Avda. de Castrelos 399" + }, + "latitude": 42.194996678, + "longitude": -8.72097155, + "lines": [ + "7" + ] + }, + { + "stopId": 1480, + "name": { + "original": "Avda. de Castrelos 526" + }, + "latitude": 42.19015727, + "longitude": -8.72109012, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 1490, + "name": { + "original": "Avda. de Castrelos 67" + }, + "latitude": 42.210613294, + "longitude": -8.732057664, + "lines": [ + "7", + "12B", + "17", + "27", + "H1", + "H2" + ] + }, + { + "stopId": 1500, + "name": { + "original": "Avda. de Castrelos 58" + }, + "latitude": 42.217084821, + "longitude": -8.732530893, + "lines": [ + "7", + "12B", + "17", + "27", + "H2", + "PTL" + ] + }, + { + "stopId": 1510, + "name": { + "original": "Avda. da Ponte 80" + }, + "latitude": 42.215203365, + "longitude": -8.670416197, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 1520, + "name": { + "original": "Avda. da Ponte 83" + }, + "latitude": 42.215400704, + "longitude": -8.671308533, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 1530, + "name": { + "original": "Avda. da Ponte (fronte Grupo S. Gorxal)" + }, + "latitude": 42.212814417, + "longitude": -8.670537674, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 1540, + "name": { + "original": "Avda. da Ponte 15" + }, + "latitude": 42.221677429, + "longitude": -8.66978207, + "lines": [ + "11", + "15A", + "15B", + "15C" + ] + }, + { + "stopId": 1550, + "name": { + "original": "Avda. da Ponte 18" + }, + "latitude": 42.221299207, + "longitude": -8.670013709, + "lines": [ + "11", + "15A", + "15B", + "15C" + ] + }, + { + "stopId": 1560, + "name": { + "original": "Avda. da Ponte 31" + }, + "latitude": 42.219388605, + "longitude": -8.669172606, + "lines": [ + "15A", + "15B" + ] + }, + { + "stopId": 1570, + "name": { + "original": "Avda. da Ponte 47" + }, + "latitude": 42.217957539, + "longitude": -8.669369577, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 1580, + "name": { + "original": "Avda. da Ponte 54" + }, + "latitude": 42.218393587, + "longitude": -8.669480106, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 1590, + "name": { + "original": "Avda. de Galicia (Parque Riouxa)" + }, + "latitude": 42.256667905, + "longitude": -8.682575386, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1600, + "name": { + "original": "Avda. de Galicia 103" + }, + "latitude": 42.251389209, + "longitude": -8.689369833, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1610, + "name": { + "original": "Avda. de Galicia 146" + }, + "latitude": 42.251376198, + "longitude": -8.68920404, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1620, + "name": { + "original": "Avda. de Galicia 139" + }, + "latitude": 42.253987165, + "longitude": -8.686196616, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1630, + "name": { + "original": "Avda. de Galicia 165" + }, + "latitude": 42.255177674, + "longitude": -8.684734482, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1640, + "name": { + "original": "Avda. de Galicia 200" + }, + "latitude": 42.254950083, + "longitude": -8.684862748, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1650, + "name": { + "original": "Avda. de Galicia 238" + }, + "latitude": 42.256910715, + "longitude": -8.68201353, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1660, + "name": { + "original": "Avda. de Galicia 280" + }, + "latitude": 42.259217959, + "longitude": -8.679666503, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1670, + "name": { + "original": "Avda. de Galicia 285" + }, + "latitude": 42.258365967, + "longitude": -8.680508997, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1680, + "name": { + "original": "Avda. de Galicia (Parque Cruce Balbarda)" + }, + "latitude": 42.251327471, + "longitude": -8.69260735, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1690, + "name": { + "original": "Avda. de Galicia 71" + }, + "latitude": 42.251420909, + "longitude": -8.692153216, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1710, + "name": { + "original": "Avda. de Vigo 6" + }, + "latitude": 42.274450823, + "longitude": -8.667138233, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1720, + "name": { + "original": "Avda. de Vigo 95" + }, + "latitude": 42.270480988, + "longitude": -8.667726374, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1730, + "name": { + "original": "Avda. de Vigo 129" + }, + "latitude": 42.267833798, + "longitude": -8.671345739, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1740, + "name": { + "original": "Avda. de Vigo 120" + }, + "latitude": 42.27068743, + "longitude": -8.668057842, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1750, + "name": { + "original": "Avda. de Vigo 161" + }, + "latitude": 42.266305919, + "longitude": -8.672818918, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1760, + "name": { + "original": "Avda. de Vigo 201" + }, + "latitude": 42.26408966, + "longitude": -8.674082239, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1770, + "name": { + "original": "Avda. de Vigo (Alameda de Rosalía de Castro)" + }, + "latitude": 42.26785896, + "longitude": -8.671440263, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1780, + "name": { + "original": "Avda. de Vigo 230" + }, + "latitude": 42.266245291, + "longitude": -8.672965754, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1790, + "name": { + "original": "Avda. de Vigo 261 (Cuatro Puentes)" + }, + "latitude": 42.261621089, + "longitude": -8.677207279, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1800, + "name": { + "original": "Avda. de Vigo 266" + }, + "latitude": 42.263995234, + "longitude": -8.674224503, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1810, + "name": { + "original": "Avda. de Vigo 320" + }, + "latitude": 42.262068498, + "longitude": -8.676736193, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1820, + "name": { + "original": "Avda. de Vigo 49" + }, + "latitude": 42.271878394, + "longitude": -8.666356304, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1830, + "name": { + "original": "Avda. de Vigo 11" + }, + "latitude": 42.274038501, + "longitude": -8.666949932, + "lines": [ + "C3d" + ] + }, + { + "stopId": 1840, + "name": { + "original": "Avda. de Vigo 72" + }, + "latitude": 42.27159436, + "longitude": -8.666389735, + "lines": [ + "C3i" + ] + }, + { + "stopId": 1850, + "name": { + "original": "Avda. de Europa (antes Camiño Freixeiro)" + }, + "latitude": 42.216135691, + "longitude": -8.759632243, + "lines": [ + "C3d", + "C3i", + "4A", + "12A", + "15A" + ] + }, + { + "stopId": 1860, + "name": { + "original": "Avda. de Europa (cruce Rúa da Pardaíña)" + }, + "latitude": 42.216741568, + "longitude": -8.757129742, + "lines": [ + "C3d", + "C3i", + "4A", + "12A", + "15A" + ] + }, + { + "stopId": 1870, + "name": { + "original": "Avda. de Europa 102" + }, + "latitude": 42.211235082, + "longitude": -8.773459294, + "lines": [ + "C3d", + "C3i", + "15A" + ] + }, + { + "stopId": 1880, + "name": { + "original": "Avda. de Europa (cruce Rúa das Teixugueiras)" + }, + "latitude": 42.216687933, + "longitude": -8.756794466, + "lines": [ + "C3d", + "C3i", + "4A", + "4C", + "12A", + "15A", + "N1" + ] + }, + { + "stopId": 1890, + "name": { + "original": "Avda. de Europa 23" + }, + "latitude": 42.215913672, + "longitude": -8.759904797, + "lines": [ + "C3d", + "C3i", + "4A", + "4C", + "12A", + "15A", + "N1" + ] + }, + { + "stopId": 1900, + "name": { + "original": "Avda. de Europa (cruce Rúa do Bravo)" + }, + "latitude": 42.211855119, + "longitude": -8.766755158, + "lines": [ + "C3d", + "C3i", + "4C", + "15A", + "N1" + ] + }, + { + "stopId": 1910, + "name": { + "original": "Avda. de Europa (fronte cruce Rúa do Bravo)" + }, + "latitude": 42.211905694, + "longitude": -8.766999036, + "lines": [ + "C3d", + "C3i", + "15A" + ] + }, + { + "stopId": 1920, + "name": { + "original": "Avda. de Europa 101" + }, + "latitude": 42.211066417, + "longitude": -8.772953743, + "lines": [ + "C3d", + "C3i", + "4C", + "15A", + "N1" + ] + }, + { + "stopId": 1930, + "name": { + "original": "Estrada de Madrid (fronte Seminario)" + }, + "latitude": 42.21474886, + "longitude": -8.69897918, + "lines": [ + "12A", + "12B", + "13", + "U2" + ] + }, + { + "stopId": 1940, + "name": { + "original": "Avda. de Madrid 136" + }, + "latitude": 42.218338954, + "longitude": -8.703817429, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 1950, + "name": { + "original": "Avda. de Madrid 124" + }, + "latitude": 42.220567154, + "longitude": -8.706419628, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 1960, + "name": { + "original": "Avda. de Madrid 62" + }, + "latitude": 42.224676782, + "longitude": -8.711832326, + "lines": [ + "12A", + "12B", + "13", + "U2", + "H2" + ] + }, + { + "stopId": 1970, + "name": { + "original": "Avda. de Madrid 57" + }, + "latitude": 42.223965709, + "longitude": -8.710062068, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 1980, + "name": { + "original": "Estrada de Madrid (Seminario)" + }, + "latitude": 42.214703324, + "longitude": -8.699378397, + "lines": [ + "12A", + "12B", + "13", + "U2" + ] + }, + { + "stopId": 1990, + "name": { + "original": "Avda. de Madrid (cruce Camiño do Raviso)" + }, + "latitude": 42.222130198, + "longitude": -8.708300774, + "lines": [ + "12A", + "12B", + "13", + "U2" + ] + }, + { + "stopId": 2000, + "name": { + "original": "Avda. de Madrid 133" + }, + "latitude": 42.220728012, + "longitude": -8.70612292, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 2010, + "name": { + "original": "Avda. de Madrid 195" + }, + "latitude": 42.218213283, + "longitude": -8.703163426, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 2020, + "name": { + "original": "Avda. de Madrid 2" + }, + "latitude": 42.228518615, + "longitude": -8.719214126, + "lines": [ + "12A", + "12B", + "13", + "U2" + ] + }, + { + "stopId": 2030, + "name": { + "original": "Avda. de Madrid 28" + }, + "latitude": 42.226744428, + "longitude": -8.716268699, + "lines": [ + "12A", + "12B", + "13", + "H2" + ] + }, + { + "stopId": 2040, + "name": { + "original": "Avda. de Madrid (trasera Colexio Hogar)" + }, + "latitude": 42.226835791, + "longitude": -8.715823453, + "lines": [ + "12A", + "12B", + "13" + ] + }, + { + "stopId": 2060, + "name": { + "original": "Avda. de Redondela 122" + }, + "latitude": 42.259945558, + "longitude": -8.672608434, + "lines": [ + "C3d" + ] + }, + { + "stopId": 2070, + "name": { + "original": "Avda. de Redondela 109" + }, + "latitude": 42.259481393, + "longitude": -8.67292487, + "lines": [ + "C3i" + ] + }, + { + "stopId": 2080, + "name": { + "original": "Avda. de Redondela 19" + }, + "latitude": 42.266569717, + "longitude": -8.667160768, + "lines": [ + "C3i" + ] + }, + { + "stopId": 2090, + "name": { + "original": "Avda. de Redondela 32" + }, + "latitude": 42.272074281, + "longitude": -8.664593691, + "lines": [ + "C3d" + ] + }, + { + "stopId": 2100, + "name": { + "original": "Avda. de Redondela 47" + }, + "latitude": 42.263186001, + "longitude": -8.668939094, + "lines": [ + "C3i" + ] + }, + { + "stopId": 2110, + "name": { + "original": "Avda. de Redondela 70" + }, + "latitude": 42.263286688, + "longitude": -8.668985036, + "lines": [ + "C3d" + ] + }, + { + "stopId": 2130, + "name": { + "original": "Avda. de Redondela (Instituto)" + }, + "latitude": 42.266758811, + "longitude": -8.667247828, + "lines": [ + "C3d" + ] + }, + { + "stopId": 2140, + "name": { + "original": "Avda. de Samil (Verbum)" + }, + "latitude": 42.213799707, + "longitude": -8.774374426, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 2150, + "name": { + "original": "Avda. de Samil (fronte Praia da Fonte)" + }, + "latitude": 42.221416498, + "longitude": -8.773724153, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 2160, + "name": { + "original": "Avda. de Samil 15" + }, + "latitude": 42.215907608, + "longitude": -8.774702031, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 2170, + "name": { + "original": "Avda. de Samil 35" + }, + "latitude": 42.210171416, + "longitude": -8.774585056, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 2180, + "name": { + "original": "Avda. de Samil 67" + }, + "latitude": 42.206809895, + "longitude": -8.776206766, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 2190, + "name": { + "original": "Avda. de Samil 81" + }, + "latitude": 42.205147646, + "longitude": -8.77674534, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 2200, + "name": { + "original": "Avda. de Santa Mariña 110" + }, + "latitude": 42.22271748, + "longitude": -8.656176614, + "lines": [ + "11" + ] + }, + { + "stopId": 2210, + "name": { + "original": "Avda. de Santa Mariña 137" + }, + "latitude": 42.222538699, + "longitude": -8.656616496, + "lines": [ + "11" + ] + }, + { + "stopId": 2220, + "name": { + "original": "Avda. de Santa Mariña 17" + }, + "latitude": 42.220338634, + "longitude": -8.668666271, + "lines": [ + "11" + ] + }, + { + "stopId": 2230, + "name": { + "original": "Avda. de Santa Mariña 52" + }, + "latitude": 42.222347963, + "longitude": -8.662841482, + "lines": [ + "11" + ] + }, + { + "stopId": 2240, + "name": { + "original": "Avda. de Santa Mariña 77" + }, + "latitude": 42.222432209, + "longitude": -8.662773458, + "lines": [ + "11" + ] + }, + { + "stopId": 2250, + "name": { + "original": "Avda. de Santa Mariña (cruce Camiño do Narxo)" + }, + "latitude": 42.220826115, + "longitude": -8.659651094, + "lines": [ + "11" + ] + }, + { + "stopId": 2260, + "name": { + "original": "Avda. de Santa Mariña 18" + }, + "latitude": 42.220504663, + "longitude": -8.668053014, + "lines": [ + "11" + ] + }, + { + "stopId": 2270, + "name": { + "original": "Avda. de Santa Mariña 103" + }, + "latitude": 42.22113201, + "longitude": -8.658591621, + "lines": [ + "11" + ] + }, + { + "stopId": 2280, + "name": { + "original": "Baixada á Laxe 31" + }, + "latitude": 42.21650849, + "longitude": -8.719175368, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 2290, + "name": { + "original": "Baixada á Laxe 44" + }, + "latitude": 42.216415126, + "longitude": -8.719355076, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 2300, + "name": { + "original": "Baixada á Ponte Nova 13" + }, + "latitude": 42.22078735, + "longitude": -8.722722261, + "lines": [ + "18A" + ] + }, + { + "stopId": 2310, + "name": { + "original": "Baixada á Praia (fronte 187)" + }, + "latitude": 42.173398721, + "longitude": -8.811050666, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 2320, + "name": { + "original": "Baixada á Praia 121" + }, + "latitude": 42.172541892, + "longitude": -8.809133287, + "lines": [ + "10" + ] + }, + { + "stopId": 2330, + "name": { + "original": "Avda. de Ricardo Mella 357" + }, + "latitude": 42.173316968, + "longitude": -8.81100291, + "lines": [ + "10" + ] + }, + { + "stopId": 2340, + "name": { + "original": "Baixada á Praia 44" + }, + "latitude": 42.167981444, + "longitude": -8.806504239, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 2350, + "name": { + "original": "Baixada á Praia 74" + }, + "latitude": 42.169850316, + "longitude": -8.808861828, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 2360, + "name": { + "original": "Baixada á Praia (Parque C.Cívico)" + }, + "latitude": 42.167825345, + "longitude": -8.806386831, + "lines": [ + "10" + ] + }, + { + "stopId": 2370, + "name": { + "original": "Baixada á Praia 94" + }, + "latitude": 42.172705652, + "longitude": -8.809114415, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 2380, + "name": { + "original": "Baixada á Praia 101" + }, + "latitude": 42.169719111, + "longitude": -8.808832324, + "lines": [ + "10" + ] + }, + { + "stopId": 2390, + "name": { + "original": "Baixada á Salgueira 49" + }, + "latitude": 42.224020371, + "longitude": -8.716787891, + "lines": [ + "18A" + ] + }, + { + "stopId": 2410, + "name": { + "original": "Baixada ao Río 31" + }, + "latitude": 42.209020914, + "longitude": -8.702331689, + "lines": [ + "14" + ] + }, + { + "stopId": 2420, + "name": { + "original": "Rúa do Cacheno (Lavadero)" + }, + "latitude": 42.208468606, + "longitude": -8.702143934, + "lines": [ + "14" + ] + }, + { + "stopId": 2430, + "name": { + "original": "Rúa de Barcelona 64" + }, + "latitude": 42.223917068, + "longitude": -8.726168827, + "lines": [ + "C1" + ] + }, + { + "stopId": 2440, + "name": { + "original": "Rúa de Barcelona 2" + }, + "latitude": 42.228315534, + "longitude": -8.721741958, + "lines": [ + "C1" + ] + }, + { + "stopId": 2450, + "name": { + "original": "Rúa de Barcelona 32" + }, + "latitude": 42.226024692, + "longitude": -8.723390804, + "lines": [ + "C1" + ] + }, + { + "stopId": 2460, + "name": { + "original": "Rúa de Xeme 59" + }, + "latitude": 42.203300943, + "longitude": -8.696320858, + "lines": [ + "14" + ] + }, + { + "stopId": 2490, + "name": { + "original": "Rúa das Coutadas 57" + }, + "latitude": 42.194635725, + "longitude": -8.699504032, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 2500, + "name": { + "original": "Rúa de Ramiro Pascual (Igrexa)" + }, + "latitude": 42.191950062, + "longitude": -8.707193511, + "lines": [ + "27" + ] + }, + { + "stopId": 2510, + "name": { + "original": "Avda. de Castrelos 439" + }, + "latitude": 42.191323803, + "longitude": -8.721049887, + "lines": [ + "7" + ] + }, + { + "stopId": 2520, + "name": { + "original": "Estrada de Bembrive 238" + }, + "latitude": 42.204563665, + "longitude": -8.687025359, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 2540, + "name": { + "original": "Bouzas (Rotonda de Las Anclas)" + }, + "latitude": 42.22513153, + "longitude": -8.751597027, + "lines": [ + "6", + "9B", + "28" + ] + }, + { + "stopId": 2550, + "name": { + "original": "Camiño da Brea 10" + }, + "latitude": 42.204086577, + "longitude": -8.704832445, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 2560, + "name": { + "original": "Camiño da Brea 37" + }, + "latitude": 42.204297185, + "longitude": -8.704942415, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 2570, + "name": { + "original": "Camiño da Brea 54" + }, + "latitude": 42.207146083, + "longitude": -8.704526775, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 2580, + "name": { + "original": "Camiño da Brea 69" + }, + "latitude": 42.207235488, + "longitude": -8.704631381, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 2590, + "name": { + "original": "Rúa da Cabalaría 94" + }, + "latitude": 42.232442621, + "longitude": -8.689558541, + "lines": [ + "28" + ] + }, + { + "stopId": 2600, + "name": { + "original": "Rúa da Cabalaría 153" + }, + "latitude": 42.235437997, + "longitude": -8.689019794, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 2610, + "name": { + "original": "Rúa da Cabalaría 14" + }, + "latitude": 42.233426101, + "longitude": -8.693298639, + "lines": [ + "28" + ] + }, + { + "stopId": 2620, + "name": { + "original": "Rúa da Cabalaría 186" + }, + "latitude": 42.235420737, + "longitude": -8.68894034, + "lines": [ + "28" + ] + }, + { + "stopId": 2630, + "name": { + "original": "Rúa da Cabalaría (cruce Subida ao Rosal Florido)" + }, + "latitude": 42.233483693, + "longitude": -8.693266452, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 2640, + "name": { + "original": "Rúa da Cabalaría 67" + }, + "latitude": 42.232559793, + "longitude": -8.689848219, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 2735, + "name": { + "original": "Rúa da Cachamuíña (Concello)" + }, + "latitude": 42.235033314, + "longitude": -8.727202198, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 2740, + "name": { + "original": "Rúa do Cacheno 75" + }, + "latitude": 42.207451401, + "longitude": -8.701194432, + "lines": [ + "14" + ] + }, + { + "stopId": 2750, + "name": { + "original": "Rúa do Cacheno 28" + }, + "latitude": 42.20671834, + "longitude": -8.699160127, + "lines": [ + "14" + ] + }, + { + "stopId": 2760, + "name": { + "original": "Rúa do Cacheno 66" + }, + "latitude": 42.208194443, + "longitude": -8.701518979, + "lines": [ + "14" + ] + }, + { + "stopId": 2770, + "name": { + "original": "Rúa do Cacheno 49" + }, + "latitude": 42.20648703, + "longitude": -8.698760561, + "lines": [ + "14" + ] + }, + { + "stopId": 2780, + "name": { + "original": "Avda. das Camelias 135" + }, + "latitude": 42.222387178, + "longitude": -8.731207698, + "lines": [ + "4A", + "4C", + "5A", + "5B", + "11", + "12A", + "12B", + "16", + "17", + "27", + "N1", + "LZH" + ] + }, + { + "stopId": 2790, + "name": { + "original": "Avda. das Camelias 37" + }, + "latitude": 42.230566426, + "longitude": -8.730086804, + "lines": [ + "4A", + "4C", + "11", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 2800, + "name": { + "original": "Avda. das Camelias 46" + }, + "latitude": 42.230291959, + "longitude": -8.730279255, + "lines": [ + "4A", + "4C", + "7", + "12B", + "17", + "27", + "PSA 4" + ] + }, + { + "stopId": 2810, + "name": { + "original": "Avda. das Camelias 80" + }, + "latitude": 42.227601839, + "longitude": -8.730236339, + "lines": [ + "4A", + "4C", + "7", + "12B", + "17", + "27", + "PSA 4" + ] + }, + { + "stopId": 2820, + "name": { + "original": "Avda. das Camelias (Praza 8 de Marzo)" + }, + "latitude": 42.227403959, + "longitude": -8.729948584, + "lines": [ + "4A", + "4C", + "11", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 2830, + "name": { + "original": "Rúa de Camilo Veiga 33" + }, + "latitude": 42.22243855, + "longitude": -8.751978552, + "lines": [ + "C3d", + "13", + "15B", + "15C", + "U1", + "H" + ] + }, + { + "stopId": 2840, + "name": { + "original": "Rúa da Goleta 3" + }, + "latitude": 42.241698248, + "longitude": -8.665209215, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 2850, + "name": { + "original": "Rúa da Goleta 2" + }, + "latitude": 42.241676405, + "longitude": -8.665026825, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 2870, + "name": { + "original": "Rúa de Cantabria (Compañía Suministradora de Auga)" + }, + "latitude": 42.237397409, + "longitude": -8.694169154, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 2880, + "name": { + "original": "Rúa de Cantabria 148" + }, + "latitude": 42.239331582, + "longitude": -8.692983618, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 2910, + "name": { + "original": "Rúa de Cantabria 212" + }, + "latitude": 42.241655974, + "longitude": -8.692827561, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 2920, + "name": { + "original": "Rúa de Cantabria 45" + }, + "latitude": 42.235852081, + "longitude": -8.695592417, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 2930, + "name": { + "original": "Rúa de Cantabria 58" + }, + "latitude": 42.235377808, + "longitude": -8.695695331, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 2950, + "name": { + "original": "Rúa dos Canteiros 4" + }, + "latitude": 42.204307169, + "longitude": -8.729719236, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 2960, + "name": { + "original": "Rúa dos Canteiros 101" + }, + "latitude": 42.20073699, + "longitude": -8.738895516, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 2970, + "name": { + "original": "Rúa dos Canteiros 116" + }, + "latitude": 42.201317829, + "longitude": -8.735636365, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 2980, + "name": { + "original": "Rúa dos Canteiros 164" + }, + "latitude": 42.200758846, + "longitude": -8.739043037, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 2990, + "name": { + "original": "Rúa dos Canteiros 9" + }, + "latitude": 42.203792573, + "longitude": -8.729630723, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 3000, + "name": { + "original": "Rúa dos Canteiros 49" + }, + "latitude": 42.201188677, + "longitude": -8.733208966, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 3010, + "name": { + "original": "Rúa dos Canteiros 73" + }, + "latitude": 42.20121252, + "longitude": -8.735711467, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 3020, + "name": { + "original": "Rúa dos Canteiros 76" + }, + "latitude": 42.201218481, + "longitude": -8.733509374, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 3030, + "name": { + "original": "Avda. de Castrelos 458" + }, + "latitude": 42.194702579, + "longitude": -8.721025195, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 3050, + "name": { + "original": "Estrada de Casás (cruce Camiño da Pedra Branca)" + }, + "latitude": 42.192360267, + "longitude": -8.756680959, + "lines": [ + "29" + ] + }, + { + "stopId": 3052, + "name": { + "original": "Avda. Arquitecto Antonio Palacios (cruce Rúa Ricardo Torres)" + }, + "latitude": 42.216966631, + "longitude": -8.729813845, + "lines": [ + "A" + ] + }, + { + "stopId": 3060, + "name": { + "original": "Rúa da Ceboleira 30" + }, + "latitude": 42.224698049, + "longitude": -8.701749123, + "lines": [ + "6" + ] + }, + { + "stopId": 3070, + "name": { + "original": "Rúa da Ceboleira 49" + }, + "latitude": 42.224816342, + "longitude": -8.701565998, + "lines": [ + "6" + ] + }, + { + "stopId": 3080, + "name": { + "original": "Avda. de Cesáreo Vázquez 136" + }, + "latitude": 42.1873653089623, + "longitude": -8.800886236766305, + "lines": [ + "11" + ] + }, + { + "stopId": 3090, + "name": { + "original": "Avda. de Cesáreo Vázquez 182" + }, + "latitude": 42.191019711713736, + "longitude": -8.799628565094565, + "lines": [ + "C3d", + "10", + "11" + ] + }, + { + "stopId": 3100, + "name": { + "original": "Avda. de Cesáreo Vázquez 99" + }, + "latitude": 42.184766843, + "longitude": -8.802180879, + "lines": [ + "11", + "12A" + ] + }, + { + "stopId": 3110, + "name": { + "original": "Avda. de Cesáreo Vázquez 74" + }, + "latitude": 42.184416675, + "longitude": -8.802179713, + "lines": [ + "11" + ] + }, + { + "stopId": 3120, + "name": { + "original": "Avda. de Cesáreo Vázquez 141" + }, + "latitude": 42.187488521491225, + "longitude": -8.801226626055183, + "lines": [ + "11", + "12A" + ] + }, + { + "stopId": 3130, + "name": { + "original": "Avda. de Cesáreo Vázquez 169" + }, + "latitude": 42.191024803868736, + "longitude": -8.799397387002196, + "lines": [ + "11" + ] + }, + { + "stopId": 3140, + "name": { + "original": "Estrada De Zamáns 255" + }, + "latitude": 42.15800346, + "longitude": -8.686134691, + "lines": [ + "7" + ] + }, + { + "stopId": 3150, + "name": { + "original": "Estrada de Zamáns (cruce Igrexa)" + }, + "latitude": 42.157661469, + "longitude": -8.685973759, + "lines": [ + "7" + ] + }, + { + "stopId": 3160, + "name": { + "original": "Camiño da Falcoa 10" + }, + "latitude": 42.206593246, + "longitude": -8.721651793, + "lines": [ + "18B", + "18H" + ] + }, + { + "stopId": 3170, + "name": { + "original": "Rúa das Coutadas 76" + }, + "latitude": 42.220968446, + "longitude": -8.720400826, + "lines": [ + "18A" + ] + }, + { + "stopId": 3180, + "name": { + "original": "Camiño de Quirós 106" + }, + "latitude": 42.21955863, + "longitude": -8.711410119, + "lines": [ + "18B", + "18H" + ] + }, + { + "stopId": 3190, + "name": { + "original": "Camiño de Quirós (cruce Rúa de Dona Cristina)" + }, + "latitude": 42.219681782, + "longitude": -8.711385979, + "lines": [ + "18B", + "18H" + ] + }, + { + "stopId": 3230, + "name": { + "original": "Rúa de Colón 27" + }, + "latitude": 42.236471452, + "longitude": -8.720164905, + "lines": [ + "A", + "5A", + "9B", + "11", + "15B", + "15C", + "16", + "17" + ] + }, + { + "stopId": 3240, + "name": { + "original": "Rúa da Coruña 5" + }, + "latitude": 42.22214729, + "longitude": -8.734167916, + "lines": [ + "A", + "5A", + "5B", + "10", + "11", + "13", + "N4", + "U1", + "H1", + "H" + ] + }, + { + "stopId": 3250, + "name": { + "original": "Rúa da Coruña 26" + }, + "latitude": 42.222378625, + "longitude": -8.734134247, + "lines": [ + "C1", + "A", + "10", + "N4", + "H1" + ] + }, + { + "stopId": 3260, + "name": { + "original": "Rúa da Coruña 37" + }, + "latitude": 42.226287696, + "longitude": -8.737475832, + "lines": [ + "15B" + ] + }, + { + "stopId": 3270, + "name": { + "original": "Rúa da Coruña (fronte 39)" + }, + "latitude": 42.226490285, + "longitude": -8.73744901, + "lines": [ + "10", + "15B" + ] + }, + { + "stopId": 3280, + "name": { + "original": "Rúa de Manuel Lago Lago 1" + }, + "latitude": 42.188898503, + "longitude": -8.776299224, + "lines": [ + "29" + ] + }, + { + "stopId": 3290, + "name": { + "original": "Rúa da Costa 13" + }, + "latitude": 42.213397685, + "longitude": -8.72248211, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 3300, + "name": { + "original": "Rúa da Costa 22" + }, + "latitude": 42.211751348, + "longitude": -8.721691531, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 3310, + "name": { + "original": "Rúa da Costa 74" + }, + "latitude": 42.209233023, + "longitude": -8.720666368, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 3320, + "name": { + "original": "Rúa da Costa 63" + }, + "latitude": 42.210154848, + "longitude": -8.720902403, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 3350, + "name": { + "original": "Rúa do Couto 1" + }, + "latitude": 42.230911796, + "longitude": -8.722711253, + "lines": [ + "12A", + "27" + ] + }, + { + "stopId": 3360, + "name": { + "original": "Rúa do Doutor Canoa 8" + }, + "latitude": 42.237803518, + "longitude": -8.704409031, + "lines": [ + "31" + ] + }, + { + "stopId": 3370, + "name": { + "original": "Estrada de Bembrive (cruce Rúa Eifonso)" + }, + "latitude": 42.205676226, + "longitude": -8.693112047, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3380, + "name": { + "original": "Cruce Eifonso" + }, + "latitude": 42.205623108, + "longitude": -8.693239335, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3390, + "name": { + "original": "Estrada de Bembrive 160" + }, + "latitude": 42.205296644, + "longitude": -8.692558914, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3400, + "name": { + "original": "Estrada da Gándara 22" + }, + "latitude": 42.163584451, + "longitude": -8.716088728, + "lines": [ + "7" + ] + }, + { + "stopId": 3420, + "name": { + "original": "Avda. de Castrelos (Cemiterio de Pereiró)" + }, + "latitude": 42.208071495, + "longitude": -8.731366082, + "lines": [ + "7", + "12B", + "17", + "27", + "U1" + ] + }, + { + "stopId": 3430, + "name": { + "original": "Avda. da Ponte 86 (Cemiterio)" + }, + "latitude": 42.215621206, + "longitude": -8.67221512, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 3450, + "name": { + "original": "Estrada de Camposancos 155" + }, + "latitude": 42.194732286, + "longitude": -8.769245322, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3460, + "name": { + "original": "Estrada de Camposancos 19" + }, + "latitude": 42.203197031, + "longitude": -8.753437723, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3470, + "name": { + "original": "Estrada de Camposancos 214" + }, + "latitude": 42.193581709, + "longitude": -8.772496159, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3480, + "name": { + "original": "Estrada de Camposancos 141" + }, + "latitude": 42.196095467, + "longitude": -8.766230519, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3490, + "name": { + "original": "Estrada de Camposancos 171" + }, + "latitude": 42.193613504, + "longitude": -8.772222574, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3500, + "name": { + "original": "Estrada de Camposancos 190" + }, + "latitude": 42.194553241, + "longitude": -8.770325583, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3510, + "name": { + "original": "Estrada de Camposancos 28" + }, + "latitude": 42.202849227, + "longitude": -8.753870291, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3520, + "name": { + "original": "Estrada de Camposancos 75" + }, + "latitude": 42.200000035, + "longitude": -8.759107913, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3530, + "name": { + "original": "Estrada de Camposancos 88" + }, + "latitude": 42.199773285, + "longitude": -8.759958845, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3540, + "name": { + "original": "Estrada de Camposancos 138" + }, + "latitude": 42.19617384, + "longitude": -8.766295392, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 3550, + "name": { + "original": "Estrada de Bembrive (cruce Camiño Cova)" + }, + "latitude": 42.19747765, + "longitude": -8.688158543, + "lines": [ + "6" + ] + }, + { + "stopId": 3560, + "name": { + "original": "Estrada de Bembrive 104" + }, + "latitude": 42.209126518, + "longitude": -8.695599845, + "lines": [ + "6" + ] + }, + { + "stopId": 3570, + "name": { + "original": "Estrada de Bembrive 109" + }, + "latitude": 42.209576972, + "longitude": -8.695580071, + "lines": [ + "6" + ] + }, + { + "stopId": 3572, + "name": { + "original": "Estrada de Bembrive 110" + }, + "latitude": 42.208457815, + "longitude": -8.695017358, + "lines": [ + "14" + ] + }, + { + "stopId": 3574, + "name": { + "original": "Estrada de Bembrive 129" + }, + "latitude": 42.208533311, + "longitude": -8.694987854, + "lines": [ + "14" + ] + }, + { + "stopId": 3580, + "name": { + "original": "Estrada de Bembrive 180" + }, + "latitude": 42.204440318, + "longitude": -8.690871804, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3590, + "name": { + "original": "Estrada de Bembrive 195" + }, + "latitude": 42.204458432, + "longitude": -8.690765895, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3600, + "name": { + "original": "Estrada de Bembrive 22" + }, + "latitude": 42.21246424, + "longitude": -8.697218847, + "lines": [ + "6" + ] + }, + { + "stopId": 3610, + "name": { + "original": "Estrada de Bembrive 237" + }, + "latitude": 42.204446407, + "longitude": -8.68809021, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3620, + "name": { + "original": "Estrada de Bembrive 278" + }, + "latitude": 42.202562995, + "longitude": -8.685437577, + "lines": [ + "6" + ] + }, + { + "stopId": 3630, + "name": { + "original": "Estrada de Bembrive 269" + }, + "latitude": 42.20274418, + "longitude": -8.684443742, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 3640, + "name": { + "original": "Estrada de Bembrive 315" + }, + "latitude": 42.197359418, + "longitude": -8.688113004, + "lines": [ + "6" + ] + }, + { + "stopId": 3650, + "name": { + "original": "Estrada de Bembrive 346" + }, + "latitude": 42.199275919, + "longitude": -8.689032944, + "lines": [ + "6" + ] + }, + { + "stopId": 3660, + "name": { + "original": "Estrada de Bembrive 39" + }, + "latitude": 42.212227656, + "longitude": -8.697028614, + "lines": [ + "6" + ] + }, + { + "stopId": 3670, + "name": { + "original": "Estrada de Bembrive 398" + }, + "latitude": 42.194968863, + "longitude": -8.689201981, + "lines": [ + "6" + ] + }, + { + "stopId": 3680, + "name": { + "original": "Estrada de Bembrive 64" + }, + "latitude": 42.211144971, + "longitude": -8.69652789, + "lines": [ + "6" + ] + }, + { + "stopId": 3690, + "name": { + "original": "Estrada de Bembrive 73" + }, + "latitude": 42.210947345, + "longitude": -8.69627482, + "lines": [ + "6" + ] + }, + { + "stopId": 3700, + "name": { + "original": "Estrada de Bembrive (cruce Camiño Riomao)" + }, + "latitude": 42.194055385, + "longitude": -8.692079677, + "lines": [ + "6" + ] + }, + { + "stopId": 3710, + "name": { + "original": "Estrada de Bembrive 363" + }, + "latitude": 42.193846109, + "longitude": -8.69200489, + "lines": [ + "6" + ] + }, + { + "stopId": 3720, + "name": { + "original": "Estrada de Bembrive 341" + }, + "latitude": 42.1948986, + "longitude": -8.689213754, + "lines": [ + "6" + ] + }, + { + "stopId": 3730, + "name": { + "original": "Estrada de Bembrive 301" + }, + "latitude": 42.199203404, + "longitude": -8.688880116, + "lines": [ + "6" + ] + }, + { + "stopId": 3740, + "name": { + "original": "Rúa de Canido (Igrexa)" + }, + "latitude": 42.197431882, + "longitude": -8.790144971, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 3750, + "name": { + "original": "Rúa de Canido (Praia de Canido)" + }, + "latitude": 42.193128934, + "longitude": -8.797957944, + "lines": [ + "10", + "11" + ] + }, + { + "stopId": 3760, + "name": { + "original": "Rúa de Canido (fronte 119)" + }, + "latitude": 42.1957321, + "longitude": -8.795211362, + "lines": [ + "10", + "11" + ] + }, + { + "stopId": 3770, + "name": { + "original": "Rúa de Canido 135" + }, + "latitude": 42.1950348, + "longitude": -8.795701844, + "lines": [ + "C3d", + "10", + "11" + ] + }, + { + "stopId": 3780, + "name": { + "original": "Rúa de Canido 217" + }, + "latitude": 42.193041793, + "longitude": -8.797590453, + "lines": [ + "C3d", + "10", + "11" + ] + }, + { + "stopId": 3790, + "name": { + "original": "Rúa de Canido 15" + }, + "latitude": 42.201898697, + "longitude": -8.781706742, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 3800, + "name": { + "original": "Rúa de Canido 26" + }, + "latitude": 42.199639519, + "longitude": -8.785727373, + "lines": [ + "10" + ] + }, + { + "stopId": 3810, + "name": { + "original": "Rúa de Canido 55" + }, + "latitude": 42.200106486, + "longitude": -8.785121194, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 3820, + "name": { + "original": "Rúa do Falcoído 16" + }, + "latitude": 42.182659954, + "longitude": -8.699663442, + "lines": [ + "A" + ] + }, + { + "stopId": 3830, + "name": { + "original": "Rúa do Falcoído (cruce Camiño Goaldino)" + }, + "latitude": 42.180635405, + "longitude": -8.696958243, + "lines": [ + "A" + ] + }, + { + "stopId": 3840, + "name": { + "original": "Rúa do Falcoído (cruce Camiño das Presas)" + }, + "latitude": 42.181019011, + "longitude": -8.696918009, + "lines": [ + "A" + ] + }, + { + "stopId": 3850, + "name": { + "original": "Estrada Clara Campoamor (cruce Estrada Marcosende)" + }, + "latitude": 42.167933917, + "longitude": -8.692758811, + "lines": [ + "A", + "U1" + ] + }, + { + "stopId": 3860, + "name": { + "original": "Estrada Clara Campoamor (cruce Rúa do Falcoído)" + }, + "latitude": 42.168248021, + "longitude": -8.692608608, + "lines": [ + "A", + "U1" + ] + }, + { + "stopId": 3870, + "name": { + "original": "Rúa do Falcoído 13" + }, + "latitude": 42.182749988, + "longitude": -8.699953254, + "lines": [ + "A" + ] + }, + { + "stopId": 3880, + "name": { + "original": "Estrada da Coutada 44" + }, + "latitude": 42.194829735, + "longitude": -8.699040324, + "lines": [ + "6" + ] + }, + { + "stopId": 3890, + "name": { + "original": "Estrada da Coutada 39" + }, + "latitude": 42.193625246, + "longitude": -8.7014263, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 3900, + "name": { + "original": "Estrada da Coutada 19" + }, + "latitude": 42.19232927, + "longitude": -8.704749414, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 3910, + "name": { + "original": "Estrada da Coutada 68" + }, + "latitude": 42.195404022, + "longitude": -8.695794851, + "lines": [ + "6" + ] + }, + { + "stopId": 3920, + "name": { + "original": "Estrada da Coutada 65" + }, + "latitude": 42.195725689, + "longitude": -8.697154293, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 3930, + "name": { + "original": "Estrada da Coutada 12" + }, + "latitude": 42.19287831, + "longitude": -8.703506202, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 3940, + "name": { + "original": "Estrada de Casás 83" + }, + "latitude": 42.196165214, + "longitude": -8.759890011, + "lines": [ + "29" + ] + }, + { + "stopId": 3950, + "name": { + "original": "Estrada de Casás (cruce Camiño do Rial)" + }, + "latitude": 42.196014193, + "longitude": -8.759919516, + "lines": [ + "29" + ] + }, + { + "stopId": 3960, + "name": { + "original": "Baixada do Castelo" + }, + "latitude": 42.213984907, + "longitude": -8.682160511, + "lines": [ + "12A", + "12B", + "13", + "15B", + "15C", + "31" + ] + }, + { + "stopId": 3970, + "name": { + "original": "Estrada Vella de Madrid 190" + }, + "latitude": 42.215872123, + "longitude": -8.679765298, + "lines": [ + "12B", + "15B", + "15C", + "U2" + ] + }, + { + "stopId": 3980, + "name": { + "original": "Estrada Vella de Madrid (cruce Hospital)" + }, + "latitude": 42.21620386, + "longitude": -8.681570425, + "lines": [ + "12A", + "12B", + "13", + "15B", + "15C", + "31", + "U2" + ] + }, + { + "stopId": 4000, + "name": { + "original": "Estrada de Fragoselo 143" + }, + "latitude": 42.182434997, + "longitude": -8.764713761, + "lines": [ + "29" + ] + }, + { + "stopId": 4010, + "name": { + "original": "Estrada de Fragoselo 170" + }, + "latitude": 42.184148566, + "longitude": -8.767237312, + "lines": [ + "29" + ] + }, + { + "stopId": 4020, + "name": { + "original": "Estrada de Fragoselo 79" + }, + "latitude": 42.187056187, + "longitude": -8.769605703, + "lines": [ + "29" + ] + }, + { + "stopId": 4030, + "name": { + "original": "Estrada de Fragoselo 111" + }, + "latitude": 42.184118753, + "longitude": -8.767049558, + "lines": [ + "29" + ] + }, + { + "stopId": 4040, + "name": { + "original": "Estrada de Fragoselo 108" + }, + "latitude": 42.18661248, + "longitude": -8.769574881, + "lines": [ + "29" + ] + }, + { + "stopId": 4050, + "name": { + "original": "Estrada de Fragoselo 196" + }, + "latitude": 42.182111026, + "longitude": -8.76479691, + "lines": [ + "29" + ] + }, + { + "stopId": 4060, + "name": { + "original": "Estrada da Gándara 7" + }, + "latitude": 42.164326088, + "longitude": -8.716892619, + "lines": [ + "7" + ] + }, + { + "stopId": 4070, + "name": { + "original": "Estrada da Gándara 55" + }, + "latitude": 42.162254659, + "longitude": -8.713740793, + "lines": [ + "7" + ] + }, + { + "stopId": 4080, + "name": { + "original": "Estrada da Gándara 48" + }, + "latitude": 42.162252671, + "longitude": -8.71400365, + "lines": [ + "7" + ] + }, + { + "stopId": 4090, + "name": { + "original": "Estrada da Garrida 124" + }, + "latitude": 42.170369621, + "longitude": -8.708506123, + "lines": [ + "7" + ] + }, + { + "stopId": 4100, + "name": { + "original": "Estrada da Garrida 199" + }, + "latitude": 42.16978602, + "longitude": -8.708967287, + "lines": [ + "7" + ] + }, + { + "stopId": 4110, + "name": { + "original": "Estrada da Garrida 40" + }, + "latitude": 42.165331611, + "longitude": -8.714862589, + "lines": [ + "7" + ] + }, + { + "stopId": 4120, + "name": { + "original": "Estrada da Garrida 84" + }, + "latitude": 42.166399713, + "longitude": -8.711237684, + "lines": [ + "7" + ] + }, + { + "stopId": 4130, + "name": { + "original": "Estrada da Garrida 83" + }, + "latitude": 42.16561193, + "longitude": -8.714589004, + "lines": [ + "7" + ] + }, + { + "stopId": 4140, + "name": { + "original": "Estrada da Garrida (frente 80)" + }, + "latitude": 42.166208944, + "longitude": -8.711992518, + "lines": [ + "7" + ] + }, + { + "stopId": 4150, + "name": { + "original": "Estrada de Miraflores 69" + }, + "latitude": 42.217620909, + "longitude": -8.712429612, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 4160, + "name": { + "original": "Estrada de Miraflores 36" + }, + "latitude": 42.217849364, + "longitude": -8.710994711, + "lines": [ + "18A" + ] + }, + { + "stopId": 4170, + "name": { + "original": "Estrada de Miraflores 64" + }, + "latitude": 42.217345186, + "longitude": -8.713011334, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 4200, + "name": { + "original": "Estrada de Moledo 6" + }, + "latitude": 42.215236994, + "longitude": -8.707384971, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4210, + "name": { + "original": "Estrada de Moledo 42" + }, + "latitude": 42.212141078, + "longitude": -8.704563545, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4220, + "name": { + "original": "Estrada de Moledo 22" + }, + "latitude": 42.213737557, + "longitude": -8.706283115, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4230, + "name": { + "original": "Estrada de Moledo 1" + }, + "latitude": 42.215373761, + "longitude": -8.707378349, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4240, + "name": { + "original": "Estrada de Moledo 73" + }, + "latitude": 42.212378349, + "longitude": -8.704729584, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4250, + "name": { + "original": "Estrada de Moledo 25" + }, + "latitude": 42.213377597, + "longitude": -8.70601168, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 4280, + "name": { + "original": "Rúa de Emilia Pardo Bazán 80" + }, + "latitude": 42.224507337, + "longitude": -8.713465538, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4290, + "name": { + "original": "Rúa de Emilia Pardo Bazán 3" + }, + "latitude": 42.227785739, + "longitude": -8.719460889, + "lines": [ + "14", + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 4300, + "name": { + "original": "Rúa de Emilia Pardo Bazán 104" + }, + "latitude": 42.22278924, + "longitude": -8.711754289, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4310, + "name": { + "original": "Rúa de Emilia Pardo Bazán 111" + }, + "latitude": 42.222713762, + "longitude": -8.711555806, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4320, + "name": { + "original": "Rúa de Emilia Pardo Bazán 2" + }, + "latitude": 42.227716225, + "longitude": -8.719750568, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4330, + "name": { + "original": "Rúa de Emilia Pardo Bazán (cruce Baixada á Salgueira)" + }, + "latitude": 42.226463146, + "longitude": -8.71614062, + "lines": [ + "14", + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 4340, + "name": { + "original": "Rúa de Emilia Pardo Bazán 54" + }, + "latitude": 42.226112503, + "longitude": -8.715718376, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4350, + "name": { + "original": "Rúa de Emilia Pardo Bazán 43" + }, + "latitude": 42.224562952, + "longitude": -8.713390437, + "lines": [ + "14", + "18B", + "18H" + ] + }, + { + "stopId": 4440, + "name": { + "original": "Estrada de San Xoán 169" + }, + "latitude": 42.183275038, + "longitude": -8.742437444, + "lines": [ + "17" + ] + }, + { + "stopId": 4450, + "name": { + "original": "Estrada de San Xoán 61" + }, + "latitude": 42.185081179, + "longitude": -8.748029634, + "lines": [ + "17" + ] + }, + { + "stopId": 4460, + "name": { + "original": "Estrada de San Xoán 141" + }, + "latitude": 42.18366712, + "longitude": -8.744402777, + "lines": [ + "17" + ] + }, + { + "stopId": 4490, + "name": { + "original": "Rúa de Manuel Lago Lago (fronte Colexio)" + }, + "latitude": 42.187522648, + "longitude": -8.772977937, + "lines": [ + "29" + ] + }, + { + "stopId": 4500, + "name": { + "original": "Rúa de Manuel Lago Lago 20" + }, + "latitude": 42.188761213, + "longitude": -8.775973652, + "lines": [ + "29" + ] + }, + { + "stopId": 4510, + "name": { + "original": "Rúa de Manuel Lago Lago (Colexio)" + }, + "latitude": 42.187542924, + "longitude": -8.772887753, + "lines": [ + "29" + ] + }, + { + "stopId": 4520, + "name": { + "original": "Estrada das Plantas (Cidade Deportiva)" + }, + "latitude": 42.175884385, + "longitude": -8.670789023, + "lines": [ + "15C" + ] + }, + { + "stopId": 4530, + "name": { + "original": "Estrada das Plantas (fronte cruce Camiño do Pouso)" + }, + "latitude": 42.199681015, + "longitude": -8.668860289, + "lines": [ + "15C" + ] + }, + { + "stopId": 4540, + "name": { + "original": "Estrada das Plantas (fronte Viveiros)" + }, + "latitude": 42.181370416, + "longitude": -8.667861084, + "lines": [ + "15C" + ] + }, + { + "stopId": 4550, + "name": { + "original": "Estrada das Plantas (cruce Camiño do Pouso)" + }, + "latitude": 42.199945182, + "longitude": -8.669085873, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 4560, + "name": { + "original": "Estrada de Valadares 233" + }, + "latitude": 42.177017792, + "longitude": -8.721519832, + "lines": [ + "7" + ] + }, + { + "stopId": 4570, + "name": { + "original": "Estrada de Valadares 100" + }, + "latitude": 42.183987864, + "longitude": -8.724154939, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4580, + "name": { + "original": "Estrada de Valadares 146" + }, + "latitude": 42.180249473, + "longitude": -8.721850007, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4590, + "name": { + "original": "Estrada de Valadares 173" + }, + "latitude": 42.180670846, + "longitude": -8.721533506, + "lines": [ + "7" + ] + }, + { + "stopId": 4600, + "name": { + "original": "Estrada de Valadares 212" + }, + "latitude": 42.176419487, + "longitude": -8.72197849, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4610, + "name": { + "original": "Estrada de Valadares 262" + }, + "latitude": 42.172481228, + "longitude": -8.723905842, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4620, + "name": { + "original": "Estrada de Valadares 329" + }, + "latitude": 42.172330151, + "longitude": -8.723766367, + "lines": [ + "7" + ] + }, + { + "stopId": 4630, + "name": { + "original": "Estrada de Valadares 406" + }, + "latitude": 42.165721529, + "longitude": -8.720146278, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4640, + "name": { + "original": "Estrada de Valadares (cruce Camiño do Canizo)" + }, + "latitude": 42.18659932, + "longitude": -8.719619913, + "lines": [ + "7" + ] + }, + { + "stopId": 4650, + "name": { + "original": "Estrada de Valadares 48" + }, + "latitude": 42.186641056, + "longitude": -8.719853265, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 4660, + "name": { + "original": "Estrada de Valadares 99" + }, + "latitude": 42.183997801, + "longitude": -8.723857214, + "lines": [ + "7" + ] + }, + { + "stopId": 4670, + "name": { + "original": "Estrada do Vao 27" + }, + "latitude": 42.193836069, + "longitude": -8.776441689, + "lines": [ + "11" + ] + }, + { + "stopId": 4680, + "name": { + "original": "Estrada do Vao 116" + }, + "latitude": 42.19429691, + "longitude": -8.786574405, + "lines": [ + "11" + ] + }, + { + "stopId": 4690, + "name": { + "original": "Estrada do Vao 153" + }, + "latitude": 42.196307191, + "longitude": -8.790777973, + "lines": [ + "11" + ] + }, + { + "stopId": 4700, + "name": { + "original": "Estrada do Vao 46" + }, + "latitude": 42.194014721, + "longitude": -8.777055245, + "lines": [ + "11" + ] + }, + { + "stopId": 4710, + "name": { + "original": "Estrada do Vao 65" + }, + "latitude": 42.194064592, + "longitude": -8.781224067, + "lines": [ + "11" + ] + }, + { + "stopId": 4720, + "name": { + "original": "Estrada do Vao 90" + }, + "latitude": 42.193937416, + "longitude": -8.781634173, + "lines": [ + "11" + ] + }, + { + "stopId": 4730, + "name": { + "original": "Estrada do Vao 91" + }, + "latitude": 42.19405266, + "longitude": -8.78551292, + "lines": [ + "11" + ] + }, + { + "stopId": 4740, + "name": { + "original": "Estrada do Vao (Praia do Vao)" + }, + "latitude": 42.196595321, + "longitude": -8.791019372, + "lines": [ + "11" + ] + }, + { + "stopId": 4750, + "name": { + "original": "Estrada Vella de Madrid 6" + }, + "latitude": 42.213983883, + "longitude": -8.697464269, + "lines": [ + "12A", + "12B", + "13", + "U2", + "H3" + ] + }, + { + "stopId": 4760, + "name": { + "original": "Estrada Vella de Madrid 34" + }, + "latitude": 42.216359908, + "longitude": -8.693040769, + "lines": [ + "12A", + "12B", + "13", + "U2", + "H3" + ] + }, + { + "stopId": 4770, + "name": { + "original": "Estrada Vella de Madrid 104" + }, + "latitude": 42.219822923, + "longitude": -8.684198247, + "lines": [ + "12A", + "12B", + "13", + "31", + "U2" + ] + }, + { + "stopId": 4780, + "name": { + "original": "Estrada Vella de Madrid 136" + }, + "latitude": 42.220845201, + "longitude": -8.680581909, + "lines": [ + "12A", + "12B", + "13", + "31", + "U2" + ] + }, + { + "stopId": 4790, + "name": { + "original": "Estrada Vella de Madrid 123" + }, + "latitude": 42.220436017, + "longitude": -8.683690589, + "lines": [ + "12A", + "12B", + "13", + "31" + ] + }, + { + "stopId": 4800, + "name": { + "original": "Estrada Vella de Madrid 160" + }, + "latitude": 42.218049288, + "longitude": -8.679899408, + "lines": [ + "12A", + "12B", + "13", + "31", + "U2" + ] + }, + { + "stopId": 4810, + "name": { + "original": "Estrada Vella de Madrid 177" + }, + "latitude": 42.21827574, + "longitude": -8.679588272, + "lines": [ + "12A", + "12B", + "13", + "31" + ] + }, + { + "stopId": 4820, + "name": { + "original": "Estrada Vella de Madrid 31" + }, + "latitude": 42.216687676, + "longitude": -8.693917852, + "lines": [ + "12A", + "12B", + "13", + "H3" + ] + }, + { + "stopId": 4830, + "name": { + "original": "Estrada Vella de Madrid 81" + }, + "latitude": 42.218719793, + "longitude": -8.690012555, + "lines": [ + "12A", + "12B", + "13", + "H3" + ] + }, + { + "stopId": 4840, + "name": { + "original": "Estrada Vella de Madrid 76" + }, + "latitude": 42.218745617, + "longitude": -8.689009409, + "lines": [ + "12A", + "12B", + "13", + "U2", + "H3" + ] + }, + { + "stopId": 4850, + "name": { + "original": "Estrada Vella de Madrid (frente Centro Comercial)" + }, + "latitude": 42.216448197, + "longitude": -8.68163748, + "lines": [ + "12A", + "13", + "31" + ] + }, + { + "stopId": 4860, + "name": { + "original": "Estrada da Venda (cruce Camiño da Coutadiña)" + }, + "latitude": 42.182744465, + "longitude": -8.70286123, + "lines": [ + "A", + "6" + ] + }, + { + "stopId": 4870, + "name": { + "original": "Estrada da Venda 5" + }, + "latitude": 42.192029809, + "longitude": -8.712623715, + "lines": [ + "A", + "27", + "H3" + ] + }, + { + "stopId": 4880, + "name": { + "original": "Estrada da Venda 240" + }, + "latitude": 42.188021902, + "longitude": -8.711457595, + "lines": [ + "A", + "H3" + ] + }, + { + "stopId": 4890, + "name": { + "original": "Estrada da Venda 238" + }, + "latitude": 42.186610014, + "longitude": -8.710023944, + "lines": [ + "A", + "6" + ] + }, + { + "stopId": 4900, + "name": { + "original": "Estrada da Venda 35" + }, + "latitude": 42.188304845, + "longitude": -8.711474749, + "lines": [ + "A", + "27", + "H3" + ] + }, + { + "stopId": 4910, + "name": { + "original": "Estrada da Venda 4" + }, + "latitude": 42.19205763, + "longitude": -8.712722956, + "lines": [ + "A", + "H3" + ] + }, + { + "stopId": 4920, + "name": { + "original": "Estrada da Venda 49" + }, + "latitude": 42.186610342, + "longitude": -8.709937804, + "lines": [ + "A", + "6" + ] + }, + { + "stopId": 4930, + "name": { + "original": "Travesía da Devesa (Asociación Veciños)" + }, + "latitude": 42.246018558, + "longitude": -8.669287042, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 4940, + "name": { + "original": "Camiño da Devesa (fronte 55)" + }, + "latitude": 42.245450719, + "longitude": -8.672406783, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 4960, + "name": { + "original": "Rúa do Doutor Corbal 135" + }, + "latitude": 42.256210875, + "longitude": -8.696882723, + "lines": [ + "17" + ] + }, + { + "stopId": 4970, + "name": { + "original": "Rúa do Doutor Corbal 126" + }, + "latitude": 42.255883312, + "longitude": -8.696812986, + "lines": [ + "17" + ] + }, + { + "stopId": 4980, + "name": { + "original": "Rúa do Doutor Corbal 149" + }, + "latitude": 42.257539061, + "longitude": -8.697019814, + "lines": [ + "17" + ] + }, + { + "stopId": 5000, + "name": { + "original": "Rúa do Doutor Corbal 6" + }, + "latitude": 42.250121395, + "longitude": -8.696122376, + "lines": [ + "17" + ] + }, + { + "stopId": 5010, + "name": { + "original": "Rúa do Doutor Corbal 95" + }, + "latitude": 42.254169539, + "longitude": -8.697752273, + "lines": [ + "17" + ] + }, + { + "stopId": 5020, + "name": { + "original": "Rúa do Doutor Corbal 94" + }, + "latitude": 42.254104511, + "longitude": -8.697655199, + "lines": [ + "17" + ] + }, + { + "stopId": 5030, + "name": { + "original": "Avda. de Redondela (fronte 28)" + }, + "latitude": 42.272502528, + "longitude": -8.664787252, + "lines": [ + "C3i" + ] + }, + { + "stopId": 5040, + "name": { + "original": "Rúa da Salgueira Entrada 5" + }, + "latitude": 42.222543586, + "longitude": -8.717809812, + "lines": [ + "18A" + ] + }, + { + "stopId": 5060, + "name": { + "original": "Estadio de Balaídos (Avda. do Fragoso)" + }, + "latitude": 42.212676247, + "longitude": -8.739585545, + "lines": [ + "16", + "23", + "N4" + ] + }, + { + "stopId": 5070, + "name": { + "original": "Rúa da Estrada 10-12" + }, + "latitude": 42.220535836, + "longitude": -8.74141599, + "lines": [ + "C3i", + "5B" + ] + }, + { + "stopId": 5090, + "name": { + "original": "Rúa da Estrada 1" + }, + "latitude": 42.220359068, + "longitude": -8.738861999, + "lines": [ + "C3i", + "5B" + ] + }, + { + "stopId": 5120, + "name": { + "original": "Camiño da Falcoa 41" + }, + "latitude": 42.204935234, + "longitude": -8.722453969, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5140, + "name": { + "original": "Camiño da Falcoa 15" + }, + "latitude": 42.207336295, + "longitude": -8.72129506, + "lines": [ + "18B", + "18H" + ] + }, + { + "stopId": 5160, + "name": { + "original": "Camiño da Falcoa 26" + }, + "latitude": 42.204783634, + "longitude": -8.722664062, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5170, + "name": { + "original": "Rúa das Figueiras (Praza da Feira)" + }, + "latitude": 42.227529644, + "longitude": -8.66169041, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5180, + "name": { + "original": "Rúa das Figueiras 96" + }, + "latitude": 42.224562288, + "longitude": -8.66579419, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5190, + "name": { + "original": "Rúa das Figueiras 124" + }, + "latitude": 42.226051942, + "longitude": -8.663629647, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5200, + "name": { + "original": "Rúa das Figueiras 138" + }, + "latitude": 42.227533616, + "longitude": -8.661561664, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5220, + "name": { + "original": "Rúa das Figueiras 190" + }, + "latitude": 42.228984217, + "longitude": -8.658438326, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5230, + "name": { + "original": "Rúa das Figueiras 254" + }, + "latitude": 42.230261894, + "longitude": -8.653683012, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5250, + "name": { + "original": "Rúa das Figueiras 29" + }, + "latitude": 42.223048763, + "longitude": -8.667374011, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5260, + "name": { + "original": "Rúa das Figueiras 22" + }, + "latitude": 42.222838218, + "longitude": -8.667795118, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5270, + "name": { + "original": "Rúa das Figueiras 65" + }, + "latitude": 42.224367639, + "longitude": -8.665989991, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5280, + "name": { + "original": "Rúa das Figueiras 113" + }, + "latitude": 42.225940716, + "longitude": -8.663830813, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 5290, + "name": { + "original": "Avda. da Florida 60" + }, + "latitude": 42.215060936, + "longitude": -8.741309568, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5300, + "name": { + "original": "Avda. da Florida 117" + }, + "latitude": 42.214827343, + "longitude": -8.741319027, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5310, + "name": { + "original": "Avda. da Florida 140" + }, + "latitude": 42.208024308, + "longitude": -8.750412985, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5320, + "name": { + "original": "Avda. da Florida 145" + }, + "latitude": 42.213244058, + "longitude": -8.743657913, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5330, + "name": { + "original": "Avda. da Florida 3" + }, + "latitude": 42.220228436, + "longitude": -8.733426296, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5340, + "name": { + "original": "Avda. da Florida 197" + }, + "latitude": 42.208621114, + "longitude": -8.749572184, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5350, + "name": { + "original": "Avda. da Florida 40" + }, + "latitude": 42.216779248, + "longitude": -8.738705143, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5360, + "name": { + "original": "Avda. da Florida 70" + }, + "latitude": 42.212804215, + "longitude": -8.744506761, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5370, + "name": { + "original": "Avda. da Florida 69" + }, + "latitude": 42.216928527, + "longitude": -8.738268035, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5380, + "name": { + "original": "Avda. da Florida 8" + }, + "latitude": 42.219743053, + "longitude": -8.734322187, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 5390, + "name": { + "original": "Estrada de Fragoselo (cruce Camiño Río da Barxa)" + }, + "latitude": 42.179556959, + "longitude": -8.761854527, + "lines": [ + "29" + ] + }, + { + "stopId": 5400, + "name": { + "original": "Avda. do Fragoso 95" + }, + "latitude": 42.213518168, + "longitude": -8.737719785, + "lines": [ + "7", + "12B", + "17", + "N4", + "H1" + ] + }, + { + "stopId": 5410, + "name": { + "original": "Avda. do Fragoso 12" + }, + "latitude": 42.219689698, + "longitude": -8.733384686, + "lines": [ + "A", + "16", + "23", + "N4", + "U1", + "H1", + "H", + "LZH", + "PSA 1" + ] + }, + { + "stopId": 5420, + "name": { + "original": "Avda. do Fragoso 3" + }, + "latitude": 42.220297406, + "longitude": -8.732924981, + "lines": [ + "7", + "12B", + "17", + "N4", + "H1" + ] + }, + { + "stopId": 5430, + "name": { + "original": "Avda. do Fragoso 36" + }, + "latitude": 42.217381871, + "longitude": -8.734669814, + "lines": [ + "A", + "16", + "23", + "N4", + "U1", + "H1", + "H", + "LZH", + "PSA 1" + ] + }, + { + "stopId": 5440, + "name": { + "original": "Avda. do Fragoso 47" + }, + "latitude": 42.217288508, + "longitude": -8.73454375, + "lines": [ + "7", + "12B", + "17", + "N4", + "H1" + ] + }, + { + "stopId": 5450, + "name": { + "original": "Avda. do Fragoso 54" + }, + "latitude": 42.215348678, + "longitude": -8.736273638, + "lines": [ + "A", + "16", + "23", + "N4", + "U1", + "H1", + "H", + "LZH", + "PSA 1" + ] + }, + { + "stopId": 5460, + "name": { + "original": "Avda. do Fragoso 71" + }, + "latitude": 42.215301002, + "longitude": -8.736123434, + "lines": [ + "7", + "12B", + "17", + "N4", + "H1" + ] + }, + { + "stopId": 5470, + "name": { + "original": "Avda. do Fragoso 86" + }, + "latitude": 42.21315264, + "longitude": -8.738470803, + "lines": [ + "A", + "16", + "23", + "N4", + "U1", + "H1", + "H", + "LZH", + "PSA 1" + ] + }, + { + "stopId": 5480, + "name": { + "original": "Estrada de San Xoán 25" + }, + "latitude": 42.186179757, + "longitude": -8.748888329, + "lines": [ + "17" + ] + }, + { + "stopId": 5490, + "name": { + "original": "Avda. de García Barbón (fronte 120)" + }, + "latitude": 42.241557555, + "longitude": -8.707094861, + "lines": [ + "C3i", + "5B", + "10", + "17", + "N1", + "H3" + ] + }, + { + "stopId": 5500, + "name": { + "original": "Avda. de García Barbón 126" + }, + "latitude": 42.24178025, + "longitude": -8.706720936, + "lines": [ + "C3d", + "5B", + "10", + "17", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 5510, + "name": { + "original": "Avda. de García Barbón 127" + }, + "latitude": 42.238290358, + "longitude": -8.71014103, + "lines": [ + "C3i", + "5B", + "10", + "17", + "N1" + ] + }, + { + "stopId": 5520, + "name": { + "original": "Avda. de García Barbón 7" + }, + "latitude": 42.237485586, + "longitude": -8.719397801, + "lines": [ + "C3i", + "A", + "4A", + "4C", + "5B", + "6", + "7", + "9B", + "10", + "12B", + "14", + "16", + "17", + "18A", + "18B", + "18H", + "28", + "N1", + "N4", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 5530, + "name": { + "original": "Avda. de García Barbón 18" + }, + "latitude": 42.23720456, + "longitude": -8.718680736, + "lines": [ + "C3d", + "5B", + "10", + "17", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5540, + "name": { + "original": "Avda. de García Barbón 28" + }, + "latitude": 42.236759685, + "longitude": -8.716384581, + "lines": [ + "C3d", + "A", + "5B", + "10", + "16", + "17", + "24", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5560, + "name": { + "original": "Avda. de García Barbón 60" + }, + "latitude": 42.236896919, + "longitude": -8.712902905, + "lines": [ + "C3d", + "A", + "5B", + "10", + "16", + "17", + "24", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5570, + "name": { + "original": "Avda. de García Barbón 87" + }, + "latitude": 42.237003397, + "longitude": -8.713586994, + "lines": [ + "C3i", + "5B", + "10", + "17", + "N1" + ] + }, + { + "stopId": 5580, + "name": { + "original": "Avda. de García Barbón 90" + }, + "latitude": 42.238267348, + "longitude": -8.709873166, + "lines": [ + "C3d", + "5B", + "10", + "17", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5590, + "name": { + "original": "Avda. da Ponte (Grupo S. Gorxal)" + }, + "latitude": 42.213057463, + "longitude": -8.670479203, + "lines": [ + "12B", + "15B", + "15C" + ] + }, + { + "stopId": 5600, + "name": { + "original": "Avda. da Gran Vía 107" + }, + "latitude": 42.225140059, + "longitude": -8.72184818, + "lines": [ + "C3i", + "7", + "11", + "13", + "15A", + "16", + "23", + "29", + "H2" + ] + }, + { + "stopId": 5610, + "name": { + "original": "Avda. da Gran Vía 12" + }, + "latitude": 42.233503531, + "longitude": -8.717236739, + "lines": [ + "C1", + "4A", + "4C", + "5B", + "7", + "12B", + "14", + "16", + "17", + "18A", + "18B", + "18H", + "PSA 4" + ] + }, + { + "stopId": 5620, + "name": { + "original": "Avda. da Gran Vía 148" + }, + "latitude": 42.222321607, + "longitude": -8.726440421, + "lines": [ + "C3d", + "13", + "15A", + "23", + "29", + "U1", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5630, + "name": { + "original": "Avda. da Gran Vía 147" + }, + "latitude": 42.222790356, + "longitude": -8.724940076, + "lines": [ + "C3i", + "7", + "11", + "13", + "15A", + "16", + "23", + "29", + "U2", + "H2" + ] + }, + { + "stopId": 5640, + "name": { + "original": "Avda. da Gran Vía 176" + }, + "latitude": 42.220868033, + "longitude": -8.730264447, + "lines": [ + "C3d", + "13", + "15A", + "23", + "29", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5650, + "name": { + "original": "Avda. da Gran Vía 185" + }, + "latitude": 42.220971136, + "longitude": -8.729196245, + "lines": [ + "C3i", + "7", + "11", + "13", + "15A", + "16", + "23", + "29", + "H2" + ] + }, + { + "stopId": 5660, + "name": { + "original": "Avda. da Gran Vía 19" + }, + "latitude": 42.232957918, + "longitude": -8.717215735, + "lines": [ + "4A", + "4C", + "5B", + "7", + "11", + "12B", + "14", + "16", + "17", + "18A", + "18B", + "18H", + "N1" + ] + }, + { + "stopId": 5670, + "name": { + "original": "Avda. da Gran Vía 46" + }, + "latitude": 42.230988893, + "longitude": -8.71867283, + "lines": [ + "C1", + "12A", + "12B", + "14", + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 5680, + "name": { + "original": "Avda. da Gran Vía 66" + }, + "latitude": 42.228173802, + "longitude": -8.720267623, + "lines": [ + "C3d", + "13", + "15A", + "23", + "29", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5690, + "name": { + "original": "Avda. da Gran Vía 85" + }, + "latitude": 42.226868266, + "longitude": -8.720637433, + "lines": [ + "C3i", + "7", + "11", + "13", + "15A", + "16", + "23", + "29", + "H2" + ] + }, + { + "stopId": 5700, + "name": { + "original": "Avda. da Gran Vía 104" + }, + "latitude": 42.225325884, + "longitude": -8.722106624, + "lines": [ + "C3d", + "13", + "15A", + "23", + "29", + "H2", + "PSA 1" + ] + }, + { + "stopId": 5710, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 22" + }, + "latitude": 42.23003666347398, + "longitude": -8.707266671978003, + "lines": [ + "31" + ] + }, + { + "stopId": 5720, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 33" + }, + "latitude": 42.23004933454558, + "longitude": -8.706947409683313, + "lines": [ + "4C", + "23", + "31", + "H2", + "PSA 4" + ] + }, + { + "stopId": 5730, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 44" + }, + "latitude": 42.227850036119314, + "longitude": -8.708105429626789, + "lines": [ + "31", + "H2" + ] + }, + { + "stopId": 5740, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 57" + }, + "latitude": 42.22783722597372, + "longitude": -8.707849091551859, + "lines": [ + "4C", + "23", + "31", + "N4", + "H2", + "PSA 4" + ] + }, + { + "stopId": 5750, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 79" + }, + "latitude": 42.225785485, + "longitude": -8.708786105, + "lines": [ + "4C", + "23", + "31", + "N4", + "H2", + "PSA 4" + ] + }, + { + "stopId": 5760, + "name": { + "original": "Estrada de Valadares 377" + }, + "latitude": 42.16929549, + "longitude": -8.723249687, + "lines": [ + "7" + ] + }, + { + "stopId": 5770, + "name": { + "original": "Estrada de Valadares 310" + }, + "latitude": 42.16911061, + "longitude": -8.723904146, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 5790, + "name": { + "original": "Hospital do Meixoeiro" + }, + "latitude": 42.214534062, + "longitude": -8.684756053, + "lines": [ + "12A", + "12B", + "13", + "15B", + "15C", + "31" + ] + }, + { + "stopId": 5800, + "name": { + "original": "Rúa de Jenaro de la Fuente 29" + }, + "latitude": 42.232202275, + "longitude": -8.703792246, + "lines": [ + "A", + "4A", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "24", + "25", + "27", + "28" + ] + }, + { + "stopId": 5810, + "name": { + "original": "Rúa de Jenaro de la Fuente 10" + }, + "latitude": 42.232258514, + "longitude": -8.705770621, + "lines": [ + "A", + "4A", + "4C", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "23", + "24", + "25", + "27", + "28", + "N4", + "PSA 4" + ] + }, + { + "stopId": 5820, + "name": { + "original": "Rúa de Jenaro de la Fuente 22" + }, + "latitude": 42.232042043, + "longitude": -8.703616807, + "lines": [ + "A", + "4A", + "4C", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "23", + "24", + "25", + "27", + "28", + "N4", + "PSA 4" + ] + }, + { + "stopId": 5830, + "name": { + "original": "Rúa de Jenaro de la Fuente 11" + }, + "latitude": 42.23238699, + "longitude": -8.705610784, + "lines": [ + "A", + "4A", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "24", + "25", + "27", + "28" + ] + }, + { + "stopId": 5840, + "name": { + "original": "Rúa da Lagoa (cruce Rúa do Balde)" + }, + "latitude": 42.201360896, + "longitude": -8.702877394, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 5850, + "name": { + "original": "Rúa da Lagoa (fronte 21)" + }, + "latitude": 42.201154253, + "longitude": -8.702343635, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 5860, + "name": { + "original": "Baixada á Laxe (Centro Saúde)" + }, + "latitude": 42.21669522, + "longitude": -8.715884298, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 5870, + "name": { + "original": "Rúa de López Mora 10" + }, + "latitude": 42.226432697, + "longitude": -8.731262013, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 5880, + "name": { + "original": "Rúa de López Mora 19" + }, + "latitude": 42.226800566, + "longitude": -8.731799623, + "lines": [ + "5A", + "5B", + "12A" + ] + }, + { + "stopId": 5890, + "name": { + "original": "Rúa de López Mora 84" + }, + "latitude": 42.222420533, + "longitude": -8.731996938, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 5900, + "name": { + "original": "Rúa de Macal 8" + }, + "latitude": 42.202071034, + "longitude": -8.723541884, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5910, + "name": { + "original": "Rúa de Macal 90" + }, + "latitude": 42.196213336, + "longitude": -8.721136467, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5920, + "name": { + "original": "Rúa de Macal (cruce Rúa de Ramiro Pascual)" + }, + "latitude": 42.196173594, + "longitude": -8.721023814, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5930, + "name": { + "original": "Rúa de Macal (frente 8)" + }, + "latitude": 42.202050775, + "longitude": -8.723454133, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5940, + "name": { + "original": "Rúa de Macal 39" + }, + "latitude": 42.198347336, + "longitude": -8.721488003, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 5950, + "name": { + "original": "Rúa dos Mestres Goldar 62" + }, + "latitude": 42.207127325, + "longitude": -8.726636888, + "lines": [ + "27" + ] + }, + { + "stopId": 5960, + "name": { + "original": "Rúa dos Mestres Goldar 77" + }, + "latitude": 42.206757786, + "longitude": -8.730056705, + "lines": [ + "27" + ] + }, + { + "stopId": 5970, + "name": { + "original": "Rúa dos Mestres Goldar 96" + }, + "latitude": 42.206880283, + "longitude": -8.729978952, + "lines": [ + "27" + ] + }, + { + "stopId": 5980, + "name": { + "original": "Rúa dos Mestres Goldar 37" + }, + "latitude": 42.207198849, + "longitude": -8.726658346, + "lines": [ + "27" + ] + }, + { + "stopId": 6000, + "name": { + "original": "Rúa de Manuel Álvarez 185" + }, + "latitude": 42.220887532, + "longitude": -8.685059571, + "lines": [ + "31", + "H3" + ] + }, + { + "stopId": 6010, + "name": { + "original": "Rúa de Manuel Álvarez (cruce Camiño Sulevada)" + }, + "latitude": 42.223077231, + "longitude": -8.675499751, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 6020, + "name": { + "original": "Rúa de Manuel Álvarez 58" + }, + "latitude": 42.223195307, + "longitude": -8.681872932, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 6030, + "name": { + "original": "Rúa de Manuel Álvarez 102" + }, + "latitude": 42.220829925, + "longitude": -8.685158812, + "lines": [ + "31", + "H3" + ] + }, + { + "stopId": 6040, + "name": { + "original": "Rúa de Manuel Álvarez (fronte cruce Camiño Sulevada)" + }, + "latitude": 42.22320325, + "longitude": -8.675213007, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 6050, + "name": { + "original": "Rúa de Manuel Cominges 22" + }, + "latitude": 42.19890849, + "longitude": -8.73623433, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 6060, + "name": { + "original": "Rúa de Manuel Cominges 135" + }, + "latitude": 42.196716802, + "longitude": -8.729966982, + "lines": [ + "12B" + ] + }, + { + "stopId": 6070, + "name": { + "original": "Rúa de Manuel Cominges 15" + }, + "latitude": 42.199197066, + "longitude": -8.736588816, + "lines": [ + "12B" + ] + }, + { + "stopId": 6080, + "name": { + "original": "Rúa de Manuel Cominges 64" + }, + "latitude": 42.197736144, + "longitude": -8.732267294, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 6090, + "name": { + "original": "Rúa de Manuel Cominges 77" + }, + "latitude": 42.197626151, + "longitude": -8.732546841, + "lines": [ + "12B" + ] + }, + { + "stopId": 6100, + "name": { + "original": "Rúa de Manuel Cominges 80" + }, + "latitude": 42.197066521, + "longitude": -8.730468609, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 6110, + "name": { + "original": "Camiño da Devesa 6" + }, + "latitude": 42.243941636, + "longitude": -8.669169025, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 6130, + "name": { + "original": "Estrada do Marco 105" + }, + "latitude": 42.207132591, + "longitude": -8.706967295, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 6140, + "name": { + "original": "Estrada do Marco (Colexio)" + }, + "latitude": 42.206925967, + "longitude": -8.707050443, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 6150, + "name": { + "original": "Estrada De Zamáns 233" + }, + "latitude": 42.160616846, + "longitude": -8.691088401, + "lines": [ + "7" + ] + }, + { + "stopId": 6160, + "name": { + "original": "Estrada De Zamáns 150" + }, + "latitude": 42.160635942, + "longitude": -8.69145083, + "lines": [ + "7" + ] + }, + { + "stopId": 6180, + "name": { + "original": "Rúa de Marín (fronte 30)" + }, + "latitude": 42.21850454, + "longitude": -8.753892634, + "lines": [ + "C3i", + "5B" + ] + }, + { + "stopId": 6187, + "name": { + "original": "Rúa de Martín Echegaray (Parque)" + }, + "latitude": 42.215244712, + "longitude": -8.74244225, + "lines": [ + "23", + "N4" + ] + }, + { + "stopId": 6200, + "name": { + "original": "Avda. de E. Martínez Garrido 11" + }, + "latitude": 42.229298043, + "longitude": -8.699760249, + "lines": [ + "6", + "25", + "31" + ] + }, + { + "stopId": 6210, + "name": { + "original": "Avda. de E. Martínez Garrido 16" + }, + "latitude": 42.229259529, + "longitude": -8.699964485, + "lines": [ + "4C", + "6", + "23", + "25", + "31", + "N4", + "PSA 4" + ] + }, + { + "stopId": 6220, + "name": { + "original": "Avda. de E. Martínez Garrido 45" + }, + "latitude": 42.226116269, + "longitude": -8.703346362, + "lines": [ + "31" + ] + }, + { + "stopId": 6230, + "name": { + "original": "Avda. de E. Martínez Garrido 69" + }, + "latitude": 42.224839145, + "longitude": -8.706082215, + "lines": [ + "31" + ] + }, + { + "stopId": 6240, + "name": { + "original": "Avda. de E. Martínez Garrido 77" + }, + "latitude": 42.224493844, + "longitude": -8.707734948, + "lines": [ + "31" + ] + }, + { + "stopId": 6250, + "name": { + "original": "Avda. de E. Martínez Garrido 80" + }, + "latitude": 42.22667463, + "longitude": -8.702581423, + "lines": [ + "4C", + "23", + "31", + "N4", + "PSA 4" + ] + }, + { + "stopId": 6260, + "name": { + "original": "Estrada da Balsa 3" + }, + "latitude": 42.199568643, + "longitude": -8.741030554, + "lines": [ + "12B", + "17" + ] + }, + { + "stopId": 6280, + "name": { + "original": "Rúa Molais (Parque da Grileira)" + }, + "latitude": 42.223843943, + "longitude": -8.65334865, + "lines": [ + "11" + ] + }, + { + "stopId": 6290, + "name": { + "original": "Rúa de San Paio (cruce Rúa Muíños)" + }, + "latitude": 42.202031781, + "longitude": -8.769502424, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 6300, + "name": { + "original": "Rúa de Cánovas del Castillo (Centro Comercial)" + }, + "latitude": 42.240129541, + "longitude": -8.727159759, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "6", + "9B", + "10", + "11", + "15B", + "15C", + "28", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 6360, + "name": { + "original": "Rúa de Pablo Iglesias (Colexio)" + }, + "latitude": 42.211426266, + "longitude": -8.743166543, + "lines": [ + "16" + ] + }, + { + "stopId": 6370, + "name": { + "original": "Rúa de Pablo Iglesias (Río)" + }, + "latitude": 42.210130966, + "longitude": -8.744872428, + "lines": [ + "16" + ] + }, + { + "stopId": 6380, + "name": { + "original": "Rúa de Pablo Iglesias 2" + }, + "latitude": 42.211811672, + "longitude": -8.742420889, + "lines": [ + "16" + ] + }, + { + "stopId": 6390, + "name": { + "original": "Rúa de Pablo Iglesias 20" + }, + "latitude": 42.210204472, + "longitude": -8.74559126, + "lines": [ + "16" + ] + }, + { + "stopId": 6440, + "name": { + "original": "Camiño do Pino Manso 6" + }, + "latitude": 42.222951208, + "longitude": -8.635051581, + "lines": [ + "A" + ] + }, + { + "stopId": 6450, + "name": { + "original": "Praza de Suárez Llanos" + }, + "latitude": 42.224306065, + "longitude": -8.753090783, + "lines": [ + "C3d", + "C3i", + "13", + "U1", + "H" + ] + }, + { + "stopId": 6460, + "name": { + "original": "Rúa dos Pescadores 10" + }, + "latitude": 42.224853579, + "longitude": -8.752608542, + "lines": [ + "C3d", + "C3i" + ] + }, + { + "stopId": 6470, + "name": { + "original": "Rúa de Pi i Margall 108" + }, + "latitude": 42.228458938, + "longitude": -8.732202674, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6480, + "name": { + "original": "Rúa de Pi i Margall 137" + }, + "latitude": 42.229149188, + "longitude": -8.731886908, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6490, + "name": { + "original": "Rúa de Pi i Margall 32" + }, + "latitude": 42.233972514, + "longitude": -8.729963004, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6500, + "name": { + "original": "Rúa de Pi i Margall 51" + }, + "latitude": 42.233690068, + "longitude": -8.730100174, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6510, + "name": { + "original": "Rúa de Pi i Margall 86" + }, + "latitude": 42.230584487, + "longitude": -8.731459155, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6520, + "name": { + "original": "Rúa de Pi i Margall 95" + }, + "latitude": 42.232285981, + "longitude": -8.730816324, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 6530, + "name": { + "original": "Rúa do Pintor Laxeiro 4" + }, + "latitude": 42.222480852, + "longitude": -8.729812967, + "lines": [ + "C1" + ] + }, + { + "stopId": 6550, + "name": { + "original": "Rúa de Pizarro 10" + }, + "latitude": 42.229886416, + "longitude": -8.717463365, + "lines": [ + "C3i", + "6", + "11", + "15A", + "23", + "25", + "28", + "29" + ] + }, + { + "stopId": 6560, + "name": { + "original": "Rúa de Pizarro 34" + }, + "latitude": 42.23113801, + "longitude": -8.711696824, + "lines": [ + "C3i", + "6", + "11", + "15A", + "23", + "25", + "27", + "28" + ] + }, + { + "stopId": 6570, + "name": { + "original": "Rúa de Pizarro 65" + }, + "latitude": 42.231357755, + "longitude": -8.71296778, + "lines": [ + "C3d", + "6", + "15A", + "23", + "25", + "27", + "28", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 6580, + "name": { + "original": "Rúa de Pizarro 7" + }, + "latitude": 42.229860487, + "longitude": -8.717979137, + "lines": [ + "C3d", + "6", + "15A", + "23", + "25", + "27", + "28", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 6620, + "name": { + "original": "Rúa de Policarpo Sanz 40" + }, + "latitude": 42.23757846151978, + "longitude": -8.721031378896738, + "lines": [ + "C1", + "A", + "5A", + "9B", + "15B", + "15C", + "24", + "28", + "N4" + ] + }, + { + "stopId": 6640, + "name": { + "original": "Rúa do Porriño (fronte Instituto)" + }, + "latitude": 42.215506175, + "longitude": -8.753186569, + "lines": [ + "C3d", + "C3i", + "4A", + "4C", + "15A", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 6650, + "name": { + "original": "Rúa do Porriño (Instituto)" + }, + "latitude": 42.215554055, + "longitude": -8.753042462, + "lines": [ + "C3d", + "C3i", + "5B", + "15B", + "U1" + ] + }, + { + "stopId": 6670, + "name": { + "original": "Estrada da Venda 109" + }, + "latitude": 42.183004832, + "longitude": -8.702601056, + "lines": [ + "A", + "6" + ] + }, + { + "stopId": 6680, + "name": { + "original": "Rúa do Portoloureiro 26" + }, + "latitude": 42.207311837, + "longitude": -8.718650635, + "lines": [ + "A" + ] + }, + { + "stopId": 6690, + "name": { + "original": "Rúa do Portoloureiro 52" + }, + "latitude": 42.20495956, + "longitude": -8.715695557, + "lines": [ + "A" + ] + }, + { + "stopId": 6700, + "name": { + "original": "Rúa do Portoloureiro 43" + }, + "latitude": 42.204947406, + "longitude": -8.715589643, + "lines": [ + "A" + ] + }, + { + "stopId": 6720, + "name": { + "original": "Rúa do Portoloureiro (fronte 28)" + }, + "latitude": 42.207385347, + "longitude": -8.718127604, + "lines": [ + "A" + ] + }, + { + "stopId": 6730, + "name": { + "original": "Rúa do Couto 29" + }, + "latitude": 42.199386411, + "longitude": -8.695417016, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 6740, + "name": { + "original": "Rúa da Vista do Mar 95" + }, + "latitude": 42.242957479, + "longitude": -8.691167762, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 6750, + "name": { + "original": "Avda. de Samil (Praia da Fonte)" + }, + "latitude": 42.221783554, + "longitude": -8.773517669, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 6760, + "name": { + "original": "Rúa de Canido (Praia da Calzoa)" + }, + "latitude": 42.201847037, + "longitude": -8.782205633, + "lines": [ + "10" + ] + }, + { + "stopId": 6780, + "name": { + "original": "Avda. de Samil (Dunas)" + }, + "latitude": 42.210256843, + "longitude": -8.774740625, + "lines": [ + "10" + ] + }, + { + "stopId": 6790, + "name": { + "original": "Avda. de Samil (fronte Hotel)" + }, + "latitude": 42.214772655, + "longitude": -8.774772363, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 6810, + "name": { + "original": "Avda. de Samil (Parking)" + }, + "latitude": 42.207405928, + "longitude": -8.776153122, + "lines": [ + "10" + ] + }, + { + "stopId": 6820, + "name": { + "original": "Avda. de Samil (Polideportivo)" + }, + "latitude": 42.20362713, + "longitude": -8.777027535, + "lines": [ + "10" + ] + }, + { + "stopId": 6830, + "name": { + "original": "Praia do Vao" + }, + "latitude": 42.197495083, + "longitude": -8.790235556, + "lines": [ + "10" + ] + }, + { + "stopId": 6860, + "name": { + "original": "Praza de Compostela" + }, + "latitude": 42.239118346, + "longitude": -8.722531274, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "6", + "9B", + "10", + "11", + "15B", + "15C", + "24", + "28", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 6880, + "name": { + "original": "Praza do Cristo da Vitoria" + }, + "latitude": 42.215576278, + "longitude": -8.748885599, + "lines": [ + "4A", + "4C", + "11", + "15A", + "N4" + ] + }, + { + "stopId": 6890, + "name": { + "original": "Rúa da Cruz 36" + }, + "latitude": 42.198959642, + "longitude": -8.68628496, + "lines": [ + "14" + ] + }, + { + "stopId": 6900, + "name": { + "original": "Rúa da Cruz 49" + }, + "latitude": 42.199031443, + "longitude": -8.686165797, + "lines": [ + "14" + ] + }, + { + "stopId": 6930, + "name": { + "original": "Praza de América 1" + }, + "latitude": 42.220997313, + "longitude": -8.732835177, + "lines": [ + "C1", + "N4" + ] + }, + { + "stopId": 6940, + "name": { + "original": "Praza de América 3" + }, + "latitude": 42.220663902, + "longitude": -8.733419892, + "lines": [ + "C3d", + "4A", + "4C", + "5A", + "5B", + "10", + "11", + "12A", + "13", + "15A", + "29", + "N4", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 6950, + "name": { + "original": "Praza de España (cruce Rúa de Pizarro)" + }, + "latitude": 42.229280401, + "longitude": -8.719123549, + "lines": [ + "7", + "12A", + "12B", + "14", + "16", + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 6955, + "name": { + "original": "Praza de España (cruce Rúa de Fernando Conde)" + }, + "latitude": 42.229438377, + "longitude": -8.719781108, + "lines": [ + "6", + "13", + "18B", + "18H", + "25", + "29" + ] + }, + { + "stopId": 6960, + "name": { + "original": "Avda. das Camelias s/n ( Praza do Rei)" + }, + "latitude": 42.235056275, + "longitude": -8.726757514, + "lines": [ + "4A", + "4C", + "7", + "12B", + "16", + "17", + "27", + "PSA 4" + ] + }, + { + "stopId": 6970, + "name": { + "original": "Rúa da Coruña 52" + }, + "latitude": 42.223781999, + "longitude": -8.735258991, + "lines": [ + "C1", + "A", + "10", + "N4", + "H1" + ] + }, + { + "stopId": 6980, + "name": { + "original": "Praza Eugenio Fadrique 6" + }, + "latitude": 42.224471236, + "longitude": -8.73619635, + "lines": [ + "C3i", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 6990, + "name": { + "original": "Praza Eugenio Fadrique 9" + }, + "latitude": 42.224099613, + "longitude": -8.735838344, + "lines": [ + "C3d", + "A", + "9B", + "15B", + "15C" + ] + }, + { + "stopId": 7000, + "name": { + "original": "Praza de Fernando O Católico" + }, + "latitude": 42.232427317, + "longitude": -8.711371073, + "lines": [ + "A", + "4A", + "4C", + "5A", + "9B", + "11", + "15B", + "15C", + "24", + "28", + "N1", + "N4" + ] + }, + { + "stopId": 7030, + "name": { + "original": "Rúa de Manuel Castro 23" + }, + "latitude": 42.212808952, + "longitude": -8.740022994, + "lines": [ + "16", + "23", + "N4" + ] + }, + { + "stopId": 7040, + "name": { + "original": "Estrada de Miraflores 1" + }, + "latitude": 42.218435348, + "longitude": -8.709924429, + "lines": [ + "14", + "18A" + ] + }, + { + "stopId": 7050, + "name": { + "original": "Praza de Miraflores 4" + }, + "latitude": 42.218502886, + "longitude": -8.710133641, + "lines": [ + "14" + ] + }, + { + "stopId": 7060, + "name": { + "original": "Estrada de Moledo 70" + }, + "latitude": 42.210499286, + "longitude": -8.703983192, + "lines": [ + "14", + "18A", + "H3" + ] + }, + { + "stopId": 7070, + "name": { + "original": "Estrada de Moledo 109" + }, + "latitude": 42.210620188, + "longitude": -8.704177049, + "lines": [ + "14", + "18A", + "H3" + ] + }, + { + "stopId": 7080, + "name": { + "original": "Rúa da Rabadeira 95" + }, + "latitude": 42.238797018, + "longitude": -8.650399429, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 7090, + "name": { + "original": "Rúa de Ramiro Pascual 108" + }, + "latitude": 42.191918475, + "longitude": -8.706869692, + "lines": [ + "27" + ] + }, + { + "stopId": 7100, + "name": { + "original": "Rúa de Ramiro Pascual 16" + }, + "latitude": 42.1952442, + "longitude": -8.71956212, + "lines": [ + "12B", + "18B", + "18H", + "27" + ] + }, + { + "stopId": 7110, + "name": { + "original": "Rúa de Ramiro Pascual 30" + }, + "latitude": 42.195709195, + "longitude": -8.716896004, + "lines": [ + "27" + ] + }, + { + "stopId": 7120, + "name": { + "original": "Rúa de Ramiro Pascual 33" + }, + "latitude": 42.195795442, + "longitude": -8.716893641, + "lines": [ + "27" + ] + }, + { + "stopId": 7130, + "name": { + "original": "Rúa de Ramiro Pascual 46" + }, + "latitude": 42.19350247, + "longitude": -8.715219623, + "lines": [ + "27" + ] + }, + { + "stopId": 7140, + "name": { + "original": "Rúa de Ramiro Pascual 71" + }, + "latitude": 42.193601829, + "longitude": -8.715157933, + "lines": [ + "27" + ] + }, + { + "stopId": 7150, + "name": { + "original": "Rúa de Ramiro Pascual 74" + }, + "latitude": 42.192415468, + "longitude": -8.71184004, + "lines": [ + "27" + ] + }, + { + "stopId": 7160, + "name": { + "original": "Rúa de Ramiro Pascual 97" + }, + "latitude": 42.192526527, + "longitude": -8.712588696, + "lines": [ + "27" + ] + }, + { + "stopId": 7170, + "name": { + "original": "Rúa de Ramiro Pascual 9" + }, + "latitude": 42.195322512, + "longitude": -8.719460515, + "lines": [ + "12B", + "18B", + "18H", + "27" + ] + }, + { + "stopId": 7200, + "name": { + "original": "Avda. de Ramón Nieto 125" + }, + "latitude": 42.231023929, + "longitude": -8.69459232, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7210, + "name": { + "original": "Avda. de Ramón Nieto 136" + }, + "latitude": 42.229661504, + "longitude": -8.691416585, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7220, + "name": { + "original": "Avda. de Ramón Nieto 173" + }, + "latitude": 42.229736581, + "longitude": -8.691879077, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7230, + "name": { + "original": "Avda. de Ramón Nieto 198" + }, + "latitude": 42.229560215, + "longitude": -8.687854611, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7240, + "name": { + "original": "Avda. de Ramón Nieto 252" + }, + "latitude": 42.229454954, + "longitude": -8.684539401, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7250, + "name": { + "original": "Avda. de Ramón Nieto 273" + }, + "latitude": 42.229533333, + "longitude": -8.684593198, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7260, + "name": { + "original": "Avda. de Ramón Nieto 308" + }, + "latitude": 42.22953241, + "longitude": -8.681347572, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7270, + "name": { + "original": "Avda. de Ramón Nieto 341" + }, + "latitude": 42.229570145, + "longitude": -8.681232237, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7280, + "name": { + "original": "Avda. de Ramón Nieto 355" + }, + "latitude": 42.228432118, + "longitude": -8.67827376, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7290, + "name": { + "original": "Avda. de Ramón Nieto 360" + }, + "latitude": 42.228411862, + "longitude": -8.678489489, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7300, + "name": { + "original": "Avda. de Ramón Nieto 390" + }, + "latitude": 42.223957681, + "longitude": -8.673451332, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7310, + "name": { + "original": "Avda. de Ramón Nieto 406" + }, + "latitude": 42.223219273, + "longitude": -8.672272983, + "lines": [ + "11", + "15A", + "15B", + "15C", + "25" + ] + }, + { + "stopId": 7320, + "name": { + "original": "Avda. de Ramón Nieto 475" + }, + "latitude": 42.224462184, + "longitude": -8.673842935, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7330, + "name": { + "original": "Avda. de Ramón Nieto 50" + }, + "latitude": 42.231776636, + "longitude": -8.697735869, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7340, + "name": { + "original": "Avda. de Ramón Nieto 503" + }, + "latitude": 42.223272423, + "longitude": -8.67216119, + "lines": [ + "11", + "15A", + "15B", + "15C", + "25" + ] + }, + { + "stopId": 7350, + "name": { + "original": "Avda. de Ramón Nieto 57" + }, + "latitude": 42.231927571, + "longitude": -8.697668814, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7360, + "name": { + "original": "Avda. de Ramón Nieto 96" + }, + "latitude": 42.230432101, + "longitude": -8.694045149, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7370, + "name": { + "original": "Avda. de Ramón Nieto 247" + }, + "latitude": 42.229607881, + "longitude": -8.686980211, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 7380, + "name": { + "original": "Camiño do Raviso 8" + }, + "latitude": 42.220087856, + "longitude": -8.707014826, + "lines": [ + "14" + ] + }, + { + "stopId": 7390, + "name": { + "original": "Camiño do Raviso 35" + }, + "latitude": 42.220167311, + "longitude": -8.706963864, + "lines": [ + "14" + ] + }, + { + "stopId": 7410, + "name": { + "original": "Rúa de Eduardo Cabello (Igrexa)" + }, + "latitude": 42.226366639, + "longitude": -8.752928216, + "lines": [ + "C3d", + "C3i", + "13", + "U1", + "H" + ] + }, + { + "stopId": 7440, + "name": { + "original": "Rúa Da Cruz 18" + }, + "latitude": 42.200640474, + "longitude": -8.684744444, + "lines": [ + "14" + ] + }, + { + "stopId": 7450, + "name": { + "original": "Rúa da Cruz 2" + }, + "latitude": 42.202168422, + "longitude": -8.68473908, + "lines": [ + "14" + ] + }, + { + "stopId": 7460, + "name": { + "original": "Rúa da Cruz 46" + }, + "latitude": 42.19807525, + "longitude": -8.684800771, + "lines": [ + "14" + ] + }, + { + "stopId": 7470, + "name": { + "original": "Rúa Da Cruz 63" + }, + "latitude": 42.198206824, + "longitude": -8.684878336, + "lines": [ + "14" + ] + }, + { + "stopId": 7480, + "name": { + "original": "Rúa Da Cruz 19" + }, + "latitude": 42.200729887, + "longitude": -8.684258964, + "lines": [ + "14" + ] + }, + { + "stopId": 7490, + "name": { + "original": "Rúa do Carballal 52" + }, + "latitude": 42.194091099, + "longitude": -8.683392611, + "lines": [ + "14" + ] + }, + { + "stopId": 7500, + "name": { + "original": "Rúa do Carballal 18" + }, + "latitude": 42.196388225, + "longitude": -8.684151676, + "lines": [ + "14" + ] + }, + { + "stopId": 7540, + "name": { + "original": "Avda. do Tranvía 100" + }, + "latitude": 42.226657624, + "longitude": -8.659447983, + "lines": [ + "11", + "15A" + ] + }, + { + "stopId": 7590, + "name": { + "original": "Avda. do Tranvía 40" + }, + "latitude": 42.225201551, + "longitude": -8.667101684, + "lines": [ + "11", + "15A" + ] + }, + { + "stopId": 7600, + "name": { + "original": "Rúa dos Chans (cruce Subida Chans)" + }, + "latitude": 42.198544554, + "longitude": -8.677824939, + "lines": [ + "14" + ] + }, + { + "stopId": 7610, + "name": { + "original": "Rúa das Chans (Colexio)" + }, + "latitude": 42.196647395, + "longitude": -8.677801904, + "lines": [ + "14" + ] + }, + { + "stopId": 7620, + "name": { + "original": "Rúa das Chans 97" + }, + "latitude": 42.189146758, + "longitude": -8.678708548, + "lines": [ + "14" + ] + }, + { + "stopId": 7630, + "name": { + "original": "Rúa do Salgueiro 6" + }, + "latitude": 42.243135911, + "longitude": -8.66173721, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 7640, + "name": { + "original": "Rúa da Rabadeira 135" + }, + "latitude": 42.241620781, + "longitude": -8.652000054, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 7650, + "name": { + "original": "Rúa da Rabadeira 64" + }, + "latitude": 42.239043121, + "longitude": -8.650247888, + "lines": [ + "9B", + "27", + "28" + ] + }, + { + "stopId": 7660, + "name": { + "original": "Rúa da Rabadeira 104" + }, + "latitude": 42.241610852, + "longitude": -8.651865944, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7662, + "name": { + "original": "Rúa de Recaré 6" + }, + "latitude": 42.211013462, + "longitude": -8.68554295, + "lines": [ + "14" + ] + }, + { + "stopId": 7664, + "name": { + "original": "Rúa de Recaré (cruce Segade)" + }, + "latitude": 42.211201327, + "longitude": -8.688838517, + "lines": [ + "14" + ] + }, + { + "stopId": 7666, + "name": { + "original": "Rúa de Recaré 3" + }, + "latitude": 42.210514541, + "longitude": -8.685349777, + "lines": [ + "14" + ] + }, + { + "stopId": 7668, + "name": { + "original": "Rúa de Recaré 39" + }, + "latitude": 42.211022529, + "longitude": -8.688318168, + "lines": [ + "14" + ] + }, + { + "stopId": 7670, + "name": { + "original": "Rúa San Cristobo 6" + }, + "latitude": 42.235169136, + "longitude": -8.671176685, + "lines": [ + "9B" + ] + }, + { + "stopId": 7680, + "name": { + "original": "Rúa San Cristobo 117" + }, + "latitude": 42.240866745, + "longitude": -8.669273191, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7690, + "name": { + "original": "Rúa San Cristobo 11" + }, + "latitude": 42.235468446, + "longitude": -8.670649153, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7700, + "name": { + "original": "Rúa San Cristobo 30" + }, + "latitude": 42.236478263, + "longitude": -8.669194381, + "lines": [ + "9B" + ] + }, + { + "stopId": 7710, + "name": { + "original": "Rúa San Cristobo 41" + }, + "latitude": 42.236825775, + "longitude": -8.669313413, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7720, + "name": { + "original": "Rúa San Cristobo 80" + }, + "latitude": 42.24099726, + "longitude": -8.669077286, + "lines": [ + "9B" + ] + }, + { + "stopId": 7730, + "name": { + "original": "Rúa do Salgueiro 24" + }, + "latitude": 42.241580812, + "longitude": -8.658065611, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 7740, + "name": { + "original": "Rúa de Salgueiro 23" + }, + "latitude": 42.241658253, + "longitude": -8.658202403, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7750, + "name": { + "original": "Rúa do Salgueiro 1" + }, + "latitude": 42.243330503, + "longitude": -8.661852545, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 7760, + "name": { + "original": "Camiño das Cunchadas (cruce Rúa da Senra)" + }, + "latitude": 42.193300199, + "longitude": -8.681021538, + "lines": [ + "14" + ] + }, + { + "stopId": 7762, + "name": { + "original": "Rúa de Segade 41" + }, + "latitude": 42.209575101, + "longitude": -8.690548833, + "lines": [ + "14" + ] + }, + { + "stopId": 7764, + "name": { + "original": "Rúa de Segade 86" + }, + "latitude": 42.209654568, + "longitude": -8.690495189, + "lines": [ + "14" + ] + }, + { + "stopId": 7810, + "name": { + "original": "Estrada Miraflores (Parque Parróco Xesús Alonso)" + }, + "latitude": 42.217817959, + "longitude": -8.710940668, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 7830, + "name": { + "original": "Rúa de Saa 72" + }, + "latitude": 42.20297089, + "longitude": -8.707945762, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 7840, + "name": { + "original": "Rúa da Saa do Monte 73" + }, + "latitude": 42.203039989, + "longitude": -8.710936623, + "lines": [ + "18B" + ] + }, + { + "stopId": 7850, + "name": { + "original": "Rúa de Saa 57" + }, + "latitude": 42.202923204, + "longitude": -8.708085237, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 7860, + "name": { + "original": "Rúa de Severino Cobas 73" + }, + "latitude": 42.225436283, + "longitude": -8.68893946, + "lines": [ + "25" + ] + }, + { + "stopId": 7870, + "name": { + "original": "Baixada á Praia 4" + }, + "latitude": 42.166897971, + "longitude": -8.802204658, + "lines": [ + "C3d", + "10", + "12A" + ] + }, + { + "stopId": 7880, + "name": { + "original": "Barrio da Salgueira 106" + }, + "latitude": 42.222364236, + "longitude": -8.718898254, + "lines": [ + "18A" + ] + }, + { + "stopId": 7890, + "name": { + "original": "Avda. de Cesáreo Vázquez 61" + }, + "latitude": 42.180764289, + "longitude": -8.80256063, + "lines": [ + "11", + "12A" + ] + }, + { + "stopId": 7900, + "name": { + "original": "Rúa de San Paio (cruce Camiño da Quintela)" + }, + "latitude": 42.20492472, + "longitude": -8.768904292, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7910, + "name": { + "original": "Rúa de San Paio (Igrexa)" + }, + "latitude": 42.208040013, + "longitude": -8.767458581, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7920, + "name": { + "original": "Rúa de San Paio (Torreiro)" + }, + "latitude": 42.208926096, + "longitude": -8.765294038, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7930, + "name": { + "original": "Rúa de San Paio 136" + }, + "latitude": 42.208999211, + "longitude": -8.765346753, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7940, + "name": { + "original": "Rúa de San Paio 220" + }, + "latitude": 42.207987966, + "longitude": -8.767688321, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7950, + "name": { + "original": "Rúa de San Paio 242" + }, + "latitude": 42.206253649, + "longitude": -8.768742472, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7960, + "name": { + "original": "Rúa de San Paio 284" + }, + "latitude": 42.202111343, + "longitude": -8.769560881, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7970, + "name": { + "original": "Rúa de San Paio (fronte 58)" + }, + "latitude": 42.213820465, + "longitude": -8.760651176, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7980, + "name": { + "original": "Rúa de San Paio 54" + }, + "latitude": 42.21430717, + "longitude": -8.760710185, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 7990, + "name": { + "original": "Rúa de San Paio 76" + }, + "latitude": 42.212612622, + "longitude": -8.760868435, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 8000, + "name": { + "original": "Rúa de San Paio 83" + }, + "latitude": 42.212034517, + "longitude": -8.760916715, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 8010, + "name": { + "original": "Rúa de Sanjurjo Badía 106" + }, + "latitude": 42.245018172, + "longitude": -8.703349646, + "lines": [ + "C3d", + "5B", + "10", + "17", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 8020, + "name": { + "original": "Rúa de Sanjurjo Badía 123" + }, + "latitude": 42.246941664, + "longitude": -8.700376378, + "lines": [ + "C3i", + "5B", + "10", + "N1", + "H3" + ] + }, + { + "stopId": 8030, + "name": { + "original": "Rúa de Sanjurjo Badía 136" + }, + "latitude": 42.246056261, + "longitude": -8.701684825, + "lines": [ + "C3d", + "5B", + "10", + "17", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 8040, + "name": { + "original": "Rúa de Sanjurjo Badía 167" + }, + "latitude": 42.24874024, + "longitude": -8.697546209, + "lines": [ + "C3i", + "5B", + "10", + "N1", + "H3" + ] + }, + { + "stopId": 8050, + "name": { + "original": "Rúa de Sanjurjo Badía 202" + }, + "latitude": 42.247765296, + "longitude": -8.698918203, + "lines": [ + "C3d", + "5B", + "10", + "17", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 8060, + "name": { + "original": "Rúa de Sanjurjo Badía 79" + }, + "latitude": 42.244926864, + "longitude": -8.703642393, + "lines": [ + "C3i", + "5B", + "10", + "N1", + "H3" + ] + }, + { + "stopId": 8090, + "name": { + "original": "Estrada Miraflores (Centro Saúde)" + }, + "latitude": 42.216151643, + "longitude": -8.715616477, + "lines": [ + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 8100, + "name": { + "original": "Estrada da Gándara 79" + }, + "latitude": 42.160735669, + "longitude": -8.709771124, + "lines": [ + "7" + ] + }, + { + "stopId": 8110, + "name": { + "original": "Estrada da Gándara (Seoane)" + }, + "latitude": 42.160670051, + "longitude": -8.709878412, + "lines": [ + "7" + ] + }, + { + "stopId": 8120, + "name": { + "original": "Rúa de Severino Cobas (cruce Trav. de Santa Cristina)" + }, + "latitude": 42.22493632, + "longitude": -8.694369092, + "lines": [ + "25" + ] + }, + { + "stopId": 8130, + "name": { + "original": "Rúa de Severino Cobas 51" + }, + "latitude": 42.225485938, + "longitude": -8.692235895, + "lines": [ + "25" + ] + }, + { + "stopId": 8140, + "name": { + "original": "Rúa de Severino Cobas 14" + }, + "latitude": 42.224657696, + "longitude": -8.696532794, + "lines": [ + "25" + ] + }, + { + "stopId": 8150, + "name": { + "original": "Rúa de Severino Cobas 3" + }, + "latitude": 42.225076787, + "longitude": -8.697168477, + "lines": [ + "25" + ] + }, + { + "stopId": 8160, + "name": { + "original": "Rúa de Severino Cobas 88" + }, + "latitude": 42.224884679, + "longitude": -8.694275214, + "lines": [ + "25" + ] + }, + { + "stopId": 8170, + "name": { + "original": "Rúa de Severino Cobas 140" + }, + "latitude": 42.225428901, + "longitude": -8.688744499, + "lines": [ + "25" + ] + }, + { + "stopId": 8180, + "name": { + "original": "Estrada da Garrida 291" + }, + "latitude": 42.173993336, + "longitude": -8.70329684, + "lines": [ + "7" + ] + }, + { + "stopId": 8190, + "name": { + "original": "Subida da Costa (Colina)" + }, + "latitude": 42.214660701, + "longitude": -8.722840401, + "lines": [ + "A" + ] + }, + { + "stopId": 8200, + "name": { + "original": "Subida da Costa 21" + }, + "latitude": 42.214714337, + "longitude": -8.723068388, + "lines": [ + "A" + ] + }, + { + "stopId": 8210, + "name": { + "original": "Subida da Costa 3" + }, + "latitude": 42.214377129, + "longitude": -8.725964538, + "lines": [ + "A" + ] + }, + { + "stopId": 8220, + "name": { + "original": "Camiño da Corredoura 3" + }, + "latitude": 42.213993258, + "longitude": -8.726740824, + "lines": [ + "A" + ] + }, + { + "stopId": 8230, + "name": { + "original": "Rúa dos Chans (cruce Camiño Regada)" + }, + "latitude": 42.199146625, + "longitude": -8.676419461, + "lines": [ + "14" + ] + }, + { + "stopId": 8240, + "name": { + "original": "Subida á Madroa (Urbanización)" + }, + "latitude": 42.243382567, + "longitude": -8.674125307, + "lines": [ + "9B", + "27", + "28" + ] + }, + { + "stopId": 8250, + "name": { + "original": "Subida á Madroa 15" + }, + "latitude": 42.242669731, + "longitude": -8.670096629, + "lines": [ + "9B", + "27", + "28" + ] + }, + { + "stopId": 8282, + "name": { + "original": "Subida á Mouteira 6" + }, + "latitude": 42.206854901, + "longitude": -8.686031058, + "lines": [ + "14" + ] + }, + { + "stopId": 8284, + "name": { + "original": "Subida á Mouteira (Parque Monte Calvario)" + }, + "latitude": 42.206779403, + "longitude": -8.686218813, + "lines": [ + "14" + ] + }, + { + "stopId": 8290, + "name": { + "original": "Subida das Ánimas 31" + }, + "latitude": 42.235646525, + "longitude": -8.685908988, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 8300, + "name": { + "original": "Subida das Ánimas 32" + }, + "latitude": 42.23556225, + "longitude": -8.686044366, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 8330, + "name": { + "original": "Rúa de Tomás A. Alonso 86" + }, + "latitude": 42.223897608, + "longitude": -8.740424721, + "lines": [ + "C3i", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 8340, + "name": { + "original": "Rúa de Tomás A. Alonso 137" + }, + "latitude": 42.223840474, + "longitude": -8.740432891, + "lines": [ + "C3d", + "13", + "15B", + "15C", + "U1", + "H" + ] + }, + { + "stopId": 8370, + "name": { + "original": "Rúa de Tomás A. Alonso 220" + }, + "latitude": 42.223984208, + "longitude": -8.751326546, + "lines": [ + "C3d", + "C3i" + ] + }, + { + "stopId": 8390, + "name": { + "original": "Rúa de Tomás A. Alonso 251" + }, + "latitude": 42.223510532, + "longitude": -8.748903264, + "lines": [ + "C3d", + "13", + "15B", + "15C", + "U1", + "H" + ] + }, + { + "stopId": 8410, + "name": { + "original": "Rúa de Tomás Paredes (fronte 108)" + }, + "latitude": 42.218006346, + "longitude": -8.754154367, + "lines": [ + "C3d", + "10", + "15B" + ] + }, + { + "stopId": 8420, + "name": { + "original": "Rúa de Tomás Paredes 114" + }, + "latitude": 42.218031265, + "longitude": -8.754258995, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 8430, + "name": { + "original": "Rúa de Tomás Paredes 9" + }, + "latitude": 42.221229518, + "longitude": -8.753411657, + "lines": [ + "C3d", + "10", + "15B" + ] + }, + { + "stopId": 8440, + "name": { + "original": "Rúa de Tomás Paredes 86" + }, + "latitude": 42.220192578, + "longitude": -8.754164587, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 8450, + "name": { + "original": "Rúa do Conde de Torrecedeira (Parque)" + }, + "latitude": 42.231511437, + "longitude": -8.732178, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 8460, + "name": { + "original": "Rúa do Conde de Torrecedeira 105" + }, + "latitude": 42.227500225, + "longitude": -8.734096707, + "lines": [ + "C1", + "C3d", + "A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 8470, + "name": { + "original": "Rúa do Conde de Torrecedeira 21" + }, + "latitude": 42.234106639, + "longitude": -8.731302569, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 8480, + "name": { + "original": "Rúa do Conde de Torrecedeira 81" + }, + "latitude": 42.229616766, + "longitude": -8.732861043, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 8490, + "name": { + "original": "Rúa da Travesía de Vigo 202" + }, + "latitude": 42.244366441, + "longitude": -8.695452075, + "lines": [ + "C3i", + "5A", + "N1", + "H3" + ] + }, + { + "stopId": 8500, + "name": { + "original": "Rúa da Travesía de Vigo 105" + }, + "latitude": 42.238455548, + "longitude": -8.703814812, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 8510, + "name": { + "original": "Rúa da Travesía de Vigo 124" + }, + "latitude": 42.238413145, + "longitude": -8.703563202, + "lines": [ + "C3i", + "5A", + "N1" + ] + }, + { + "stopId": 8520, + "name": { + "original": "Rúa da Travesía de Vigo 153" + }, + "latitude": 42.241332883, + "longitude": -8.702059906, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 8530, + "name": { + "original": "Rúa da Travesía de Vigo 158" + }, + "latitude": 42.241101222, + "longitude": -8.701974032, + "lines": [ + "C3i", + "5A", + "N1" + ] + }, + { + "stopId": 8540, + "name": { + "original": "Rúa da Travesía de Vigo 193" + }, + "latitude": 42.242844316, + "longitude": -8.698295825, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 8550, + "name": { + "original": "Rúa da Travesía de Vigo 220" + }, + "latitude": 42.246425568, + "longitude": -8.692950624, + "lines": [ + "C3i", + "5A", + "5B", + "N1", + "H3" + ] + }, + { + "stopId": 8560, + "name": { + "original": "Rúa da Travesía de Vigo 213" + }, + "latitude": 42.24409871, + "longitude": -8.69614733, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 8570, + "name": { + "original": "Rúa da Travesía de Vigo 239" + }, + "latitude": 42.246143319, + "longitude": -8.69359937, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 8580, + "name": { + "original": "Rúa da Travesía de Vigo 32" + }, + "latitude": 42.233828086, + "longitude": -8.706311242, + "lines": [ + "C3i", + "5A", + "31", + "N1" + ] + }, + { + "stopId": 8590, + "name": { + "original": "Rúa da Travesía de Vigo 37" + }, + "latitude": 42.233681224, + "longitude": -8.706702136, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 8600, + "name": { + "original": "Rúa da Travesía de Vigo 71" + }, + "latitude": 42.236350093, + "longitude": -8.70429745, + "lines": [ + "C3d", + "5A", + "31", + "U2", + "H2", + "PSA 1" + ] + }, + { + "stopId": 8610, + "name": { + "original": "Rúa da Travesía de Vigo 8" + }, + "latitude": 42.232028188, + "longitude": -8.708203776, + "lines": [ + "C3i", + "A", + "4A", + "4C", + "5A", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "23", + "24", + "25", + "27", + "28", + "N1", + "N4" + ] + }, + { + "stopId": 8620, + "name": { + "original": "Rúa da Travesía de Vigo 82" + }, + "latitude": 42.236261792, + "longitude": -8.703994979, + "lines": [ + "C3i", + "5A", + "N1" + ] + }, + { + "stopId": 8630, + "name": { + "original": "Rúa da Travesía de Vigo 7" + }, + "latitude": 42.232045931, + "longitude": -8.708603793, + "lines": [ + "C3d", + "A", + "4A", + "4C", + "5A", + "6", + "9B", + "11", + "15A", + "15B", + "15C", + "23", + "25", + "27", + "28", + "N4", + "U2", + "H2", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 8660, + "name": { + "original": "Química (CUVI)" + }, + "latitude": 42.168290977, + "longitude": -8.68342947, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8670, + "name": { + "original": "Bioloxía (CUVI)" + }, + "latitude": 42.167687661, + "longitude": -8.685994335, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8680, + "name": { + "original": "Económicas e Empresariais (CUVI)" + }, + "latitude": 42.169603028, + "longitude": -8.680108895, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8700, + "name": { + "original": "Enxeñeiros (CUVI)" + }, + "latitude": 42.167963445, + "longitude": -8.688421342, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8710, + "name": { + "original": "Universidade." + }, + "latitude": 42.167985106, + "longitude": -8.688425395, + "lines": [ + "A" + ] + }, + { + "stopId": 8720, + "name": { + "original": "Humanidades (CUVI)" + }, + "latitude": 42.169678809, + "longitude": -8.679104749, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8721, + "name": { + "original": "Universidade.." + }, + "latitude": 42.169776602, + "longitude": -8.678942156, + "lines": [ + "A" + ] + }, + { + "stopId": 8730, + "name": { + "original": "Telecomunicacións (CUVI) B" + }, + "latitude": 42.170159671, + "longitude": -8.68735086, + "lines": [ + "A" + ] + }, + { + "stopId": 8740, + "name": { + "original": "Telecomunicacións (CUVI)" + }, + "latitude": 42.170123888, + "longitude": -8.687270393, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 8750, + "name": { + "original": "Rúa de Urzáiz - Est. Intermodal - C.C." + }, + "latitude": 42.233722977, + "longitude": -8.714502762, + "lines": [ + "A", + "4A", + "4C", + "5A", + "6", + "9B", + "11", + "15B", + "15C", + "24", + "28", + "N4", + "PSA 4" + ] + }, + { + "stopId": 8770, + "name": { + "original": "Rúa de Urzáiz 13" + }, + "latitude": 42.235420929, + "longitude": -8.718721877, + "lines": [ + "A", + "4A", + "4C", + "5A", + "6", + "7", + "9B", + "11", + "12B", + "14", + "15B", + "15C", + "16", + "17", + "18A", + "18B", + "18H", + "24", + "28", + "N1", + "N4", + "PSA 4" + ] + }, + { + "stopId": 8820, + "name": { + "original": "Rúa de Urzáiz 28" + }, + "latitude": 42.23516998, + "longitude": -8.718398782, + "lines": [ + "C1", + "A", + "4A", + "4C", + "5A", + "7", + "9B", + "12B", + "14", + "15B", + "15C", + "16", + "17", + "18A", + "18B", + "18H", + "24", + "28", + "N1", + "N4" + ] + }, + { + "stopId": 8840, + "name": { + "original": "Rúa de Urzáiz 60 - Est. Intermodal - C.C." + }, + "latitude": 42.233986283, + "longitude": -8.71541048, + "lines": [ + "A", + "4A", + "4C", + "5A", + "9B", + "11", + "15B", + "15C", + "24", + "28", + "N1", + "N4" + ] + }, + { + "stopId": 8850, + "name": { + "original": "Rúa de Urzáiz 97" + }, + "latitude": 42.232341315, + "longitude": -8.710892054, + "lines": [ + "A", + "4A", + "4C", + "5A", + "6", + "9B", + "11", + "15B", + "15C", + "28", + "N4", + "PSA 4" + ] + }, + { + "stopId": 8870, + "name": { + "original": "Rúa de Venezuela 4" + }, + "latitude": 42.234250043, + "longitude": -8.724361531, + "lines": [ + "4A", + "4C", + "5B", + "11", + "12A", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 8880, + "name": { + "original": "Rúa de Venezuela 20" + }, + "latitude": 42.233188126, + "longitude": -8.72155331, + "lines": [ + "4A", + "4C", + "5B", + "11", + "12A", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 8890, + "name": { + "original": "Rúa de Venezuela 21" + }, + "latitude": 42.233283503, + "longitude": -8.721378959, + "lines": [ + "4A", + "4C", + "5B", + "7", + "12B", + "16", + "17", + "PSA 4" + ] + }, + { + "stopId": 8900, + "name": { + "original": "Rúa de Venezuela 45" + }, + "latitude": 42.232243383, + "longitude": -8.718524158, + "lines": [ + "4A", + "4C", + "5B", + "7", + "12B", + "16", + "17", + "PSA 4" + ] + }, + { + "stopId": 8910, + "name": { + "original": "Rúa de Venezuela 42" + }, + "latitude": 42.232224046, + "longitude": -8.718985824, + "lines": [ + "4A", + "4C", + "5B", + "11", + "12A", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 8916, + "name": { + "original": "Rúa de Venezuela 60" + }, + "latitude": 42.231593651, + "longitude": -8.71714227, + "lines": [ + "27" + ] + }, + { + "stopId": 8930, + "name": { + "original": "Rúa de Vilagarcía de Arousa (cruce Rúa do Grove)" + }, + "latitude": 42.22014115, + "longitude": -8.745082757, + "lines": [ + "C3i", + "5B" + ] + }, + { + "stopId": 8950, + "name": { + "original": "Rúa de Marín 5" + }, + "latitude": 42.218712573, + "longitude": -8.75011435, + "lines": [ + "C3i", + "5B" + ] + }, + { + "stopId": 8970, + "name": { + "original": "Rúa do Seixo 45" + }, + "latitude": 42.197425383, + "longitude": -8.713700535, + "lines": [ + "A", + "H3" + ] + }, + { + "stopId": 8980, + "name": { + "original": "Rúa do Seixo 38" + }, + "latitude": 42.197532685, + "longitude": -8.713614705, + "lines": [ + "A", + "H3" + ] + }, + { + "stopId": 8990, + "name": { + "original": "Rúa do Seixo 75" + }, + "latitude": 42.200673849, + "longitude": -8.714185609, + "lines": [ + "A", + "18B", + "H3" + ] + }, + { + "stopId": 9000, + "name": { + "original": "Rúa do Seixo (Parque)" + }, + "latitude": 42.200719549, + "longitude": -8.714115872, + "lines": [ + "A", + "H3" + ] + }, + { + "stopId": 9010, + "name": { + "original": "Rúa de Xeme 71" + }, + "latitude": 42.203157887, + "longitude": -8.694293108, + "lines": [ + "14" + ] + }, + { + "stopId": 9020, + "name": { + "original": "Xestoso" + }, + "latitude": 42.207584622, + "longitude": -8.670108196, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 9040, + "name": { + "original": "Estrada das Plantas (cruce Camiño Monte Vello)" + }, + "latitude": 42.20831564, + "longitude": -8.670282438, + "lines": [ + "15B", + "15C", + "U2" + ] + }, + { + "stopId": 9050, + "name": { + "original": "Estrada da Igrexa 45" + }, + "latitude": 42.154646971, + "longitude": -8.688349062, + "lines": [ + "7" + ] + }, + { + "stopId": 10061, + "name": { + "original": "Estrada de San Xoán 193" + }, + "latitude": 42.185277472, + "longitude": -8.741558953, + "lines": [ + "17" + ] + }, + { + "stopId": 14101, + "name": { + "original": "Estrada da Garrida 165" + }, + "latitude": 42.168008539, + "longitude": -8.710415438, + "lines": [ + "7" + ] + }, + { + "stopId": 14102, + "name": { + "original": "Estrada da Garrida 108" + }, + "latitude": 42.168282882, + "longitude": -8.710066751, + "lines": [ + "7" + ] + }, + { + "stopId": 14105, + "name": { + "original": "Ciencias Xurídicas (CUVI)" + }, + "latitude": 42.167237978, + "longitude": -8.681135704, + "lines": [ + "A", + "15C", + "U1", + "U2" + ] + }, + { + "stopId": 14106, + "name": { + "original": "Avda. do Aeroporto 92" + }, + "latitude": 42.234161582, + "longitude": -8.695074564, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 14107, + "name": { + "original": "Camiño Padín (Rotonda Autoestrada)" + }, + "latitude": 42.257847205, + "longitude": -8.677696507, + "lines": [ + "10" + ] + }, + { + "stopId": 14108, + "name": { + "original": "Avda. da Ponte (antes desvío Autovía)" + }, + "latitude": 42.21401741, + "longitude": -8.67133083, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 14111, + "name": { + "original": "Estrada de Bembrive (Centro Saúde)" + }, + "latitude": 42.204262657, + "longitude": -8.684801801, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 14112, + "name": { + "original": "Estrada de Bembrive (Alameda)" + }, + "latitude": 42.204047198, + "longitude": -8.684697288, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 14113, + "name": { + "original": "Estrada da Coutada 20" + }, + "latitude": 42.193458577, + "longitude": -8.702065856, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 14117, + "name": { + "original": "Rúa do Couto de San Honorato 26" + }, + "latitude": 42.228574702, + "longitude": -8.712864548, + "lines": [ + "H2" + ] + }, + { + "stopId": 14119, + "name": { + "original": "Rúa do Couto de San Honorato 80" + }, + "latitude": 42.229320789, + "longitude": -8.710390551, + "lines": [ + "H2" + ] + }, + { + "stopId": 14121, + "name": { + "original": "Rúa da Reconquista 2" + }, + "latitude": 42.238625474, + "longitude": -8.723242095, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "10", + "15B", + "15C", + "24", + "28", + "N4" + ] + }, + { + "stopId": 14122, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 9" + }, + "latitude": 42.231584097, + "longitude": -8.706968521, + "lines": [ + "4C", + "23", + "31", + "H2", + "PSA 4" + ] + }, + { + "stopId": 14123, + "name": { + "original": "Rúa do Porriño 9" + }, + "latitude": 42.214127819, + "longitude": -8.752027594, + "lines": [ + "C3d", + "C3i", + "4A", + "4C", + "5B", + "11", + "15A", + "15B", + "N4", + "U1" + ] + }, + { + "stopId": 14124, + "name": { + "original": "Rúa de Eduardo Cabello (fronte Igrexa)" + }, + "latitude": 42.226569499, + "longitude": -8.752773946, + "lines": [ + "C3d", + "C3i", + "6" + ] + }, + { + "stopId": 14125, + "name": { + "original": "Rúa do Porriño (fronte 9)" + }, + "latitude": 42.213869651, + "longitude": -8.751990789, + "lines": [ + "C3d", + "C3i", + "4A", + "4C", + "15A", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14126, + "name": { + "original": "Rúa da Travesía de Vigo 194" + }, + "latitude": 42.242494425, + "longitude": -8.699249038, + "lines": [ + "C3i", + "5A", + "N1" + ] + }, + { + "stopId": 14127, + "name": { + "original": "Avda. de Buenos Aires 13" + }, + "latitude": 42.249306896, + "longitude": -8.695179916, + "lines": [ + "5B", + "10", + "N1", + "H3" + ] + }, + { + "stopId": 14128, + "name": { + "original": "Camiño do Caramuxo (fronte 9)" + }, + "latitude": 42.20733292, + "longitude": -8.752159103, + "lines": [ + "5A" + ] + }, + { + "stopId": 14129, + "name": { + "original": "Camiño do Caramuxo 11" + }, + "latitude": 42.20723039, + "longitude": -8.752592351, + "lines": [ + "5A" + ] + }, + { + "stopId": 14131, + "name": { + "original": "Rúa de Tomás Paredes 4" + }, + "latitude": 42.221948768, + "longitude": -8.753171211, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 14132, + "name": { + "original": "Rúa de Sanjurjo Badía 252" + }, + "latitude": 42.249307631, + "longitude": -8.696542008, + "lines": [ + "C3d", + "5A", + "5B", + "10", + "17", + "31", + "U2", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 14133, + "name": { + "original": "Avda. de Galicia 37" + }, + "latitude": 42.250977575, + "longitude": -8.694471881, + "lines": [ + "C3i", + "17" + ] + }, + { + "stopId": 14134, + "name": { + "original": "Avda. de Galicia 182" + }, + "latitude": 42.253208793, + "longitude": -8.686995591, + "lines": [ + "C3d" + ] + }, + { + "stopId": 14135, + "name": { + "original": "Rúa de Santo Amaro (Praza de España)" + }, + "latitude": 42.229174145, + "longitude": -8.720143055, + "lines": [ + "C1" + ] + }, + { + "stopId": 14136, + "name": { + "original": "Avda. de Galicia 18" + }, + "latitude": 42.250484372, + "longitude": -8.694878804, + "lines": [ + "C3d", + "17" + ] + }, + { + "stopId": 14137, + "name": { + "original": "Estrada Matamá Pazo (Igrexa)" + }, + "latitude": 42.200003406, + "longitude": -8.753169, + "lines": [ + "29" + ] + }, + { + "stopId": 14138, + "name": { + "original": "Estrada de Madrid (Campo de Fútbol)" + }, + "latitude": 42.216459201, + "longitude": -8.678591709, + "lines": [ + "12B", + "15B", + "15C" + ] + }, + { + "stopId": 14139, + "name": { + "original": "Avda. de E. Martínez Garrido 27" + }, + "latitude": 42.227492758, + "longitude": -8.700413366, + "lines": [ + "6", + "25", + "31" + ] + }, + { + "stopId": 14140, + "name": { + "original": "Avda. de E. Martínez Garrido 30" + }, + "latitude": 42.228210877, + "longitude": -8.699999354, + "lines": [ + "4C", + "6", + "23", + "25", + "31", + "N4", + "PSA 4" + ] + }, + { + "stopId": 14141, + "name": { + "original": "Rúa de Jenaro de la Fuente 43" + }, + "latitude": 42.231379202, + "longitude": -8.699876213, + "lines": [ + "11", + "15A", + "15B", + "15C", + "H3" + ] + }, + { + "stopId": 14142, + "name": { + "original": "Avda. da Hispanidade 22" + }, + "latitude": 42.231463434, + "longitude": -8.728844425, + "lines": [ + "16" + ] + }, + { + "stopId": 14143, + "name": { + "original": "Avda. da Hispanidade 38" + }, + "latitude": 42.229753483, + "longitude": -8.729002675, + "lines": [ + "16" + ] + }, + { + "stopId": 14144, + "name": { + "original": "Avda. da Hispanidade 82" + }, + "latitude": 42.226760436, + "longitude": -8.727385303, + "lines": [ + "16" + ] + }, + { + "stopId": 14150, + "name": { + "original": "Rúa do Padre Don Rúa 1" + }, + "latitude": 42.232076561, + "longitude": -8.719055236, + "lines": [ + "14" + ] + }, + { + "stopId": 14152, + "name": { + "original": "Rúa do Monte Calvario 4" + }, + "latitude": 42.204815402, + "longitude": -8.687168969, + "lines": [ + "14" + ] + }, + { + "stopId": 14153, + "name": { + "original": "Estrada de Bembrive 173" + }, + "latitude": 42.205357233, + "longitude": -8.692495739, + "lines": [ + "6", + "14" + ] + }, + { + "stopId": 14154, + "name": { + "original": "Rúa das Chans (fronte 56)" + }, + "latitude": 42.19360258, + "longitude": -8.677258993, + "lines": [ + "14" + ] + }, + { + "stopId": 14156, + "name": { + "original": "Rúa de Xeme (cruce Camiño da Carballeira)" + }, + "latitude": 42.203378431, + "longitude": -8.696666863, + "lines": [ + "14" + ] + }, + { + "stopId": 14157, + "name": { + "original": "Rúa do Xeme (cruce Rúa de Eifonso)" + }, + "latitude": 42.202979066, + "longitude": -8.694065121, + "lines": [ + "14" + ] + }, + { + "stopId": 14161, + "name": { + "original": "Rúa de López Mora 62" + }, + "latitude": 42.224130699, + "longitude": -8.732568248, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 14162, + "name": { + "original": "Avda. da Florida 82" + }, + "latitude": 42.211371871, + "longitude": -8.746523782, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 14163, + "name": { + "original": "Avda. da Florida (fronte 82)" + }, + "latitude": 42.211442202, + "longitude": -8.746227469, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 14164, + "name": { + "original": "Rúa de Tomás A. Alonso 136" + }, + "latitude": 42.225172437, + "longitude": -8.744777354, + "lines": [ + "C3i", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 14165, + "name": { + "original": "Rúa de Tomás A. Alonso 193" + }, + "latitude": 42.224905029, + "longitude": -8.745285775, + "lines": [ + "C3d", + "13", + "15B", + "15C", + "U1", + "H" + ] + }, + { + "stopId": 14166, + "name": { + "original": "Avda. das Camelias 114" + }, + "latitude": 42.225142981, + "longitude": -8.729707944, + "lines": [ + "4A", + "4C", + "7", + "12B", + "17", + "27", + "PSA 4" + ] + }, + { + "stopId": 14167, + "name": { + "original": "Beiramar - Pescadores" + }, + "latitude": 42.225279021, + "longitude": -8.751908648, + "lines": [ + "6", + "9B", + "28" + ] + }, + { + "stopId": 14168, + "name": { + "original": "Avda. das Camelias 113" + }, + "latitude": 42.224928285, + "longitude": -8.729631509, + "lines": [ + "4A", + "4C", + "11", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 14169, + "name": { + "original": "Avda. das Camelias 136" + }, + "latitude": 42.22244224, + "longitude": -8.731271052, + "lines": [ + "C1", + "4A", + "4C", + "7", + "12B", + "16", + "17", + "27", + "LZH", + "PSA 4" + ] + }, + { + "stopId": 14170, + "name": { + "original": "Avda. de Samil (Praia da Punta)" + }, + "latitude": 42.218831744, + "longitude": -8.77571001, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 14171, + "name": { + "original": "Avda. de Samil (fronte Praia da Punta)" + }, + "latitude": 42.218844713, + "longitude": -8.775459221, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 14173, + "name": { + "original": "Rúa do Gaiteiro de Ricardo Portela (fronte Pavillón)" + }, + "latitude": 42.235900754, + "longitude": -8.731391435, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 14174, + "name": { + "original": "Rúa do Padre Seixas (Parque da Bouza)" + }, + "latitude": 42.211844516, + "longitude": -8.749287921, + "lines": [ + "11", + "16" + ] + }, + { + "stopId": 14175, + "name": { + "original": "Rúa do Padre Seixas 32" + }, + "latitude": 42.211792864, + "longitude": -8.749617832, + "lines": [ + "16" + ] + }, + { + "stopId": 14177, + "name": { + "original": "Rúa de Fernando Conde (cruce Avda. da Gran Vía)" + }, + "latitude": 42.22985125, + "longitude": -8.71972059, + "lines": [ + "12A", + "14", + "27" + ] + }, + { + "stopId": 14178, + "name": { + "original": "Rúa do Marqués de Alcedo (Parque)" + }, + "latitude": 42.233009005, + "longitude": -8.724497604, + "lines": [ + "12A", + "27" + ] + }, + { + "stopId": 14179, + "name": { + "original": "Rúa da Costa 4" + }, + "latitude": 42.213260612, + "longitude": -8.722562576, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 14180, + "name": { + "original": "Avda. de E. Martínez Garrido 108" + }, + "latitude": 42.224749197, + "longitude": -8.707320585, + "lines": [ + "4C", + "23", + "31", + "N4", + "PSA 4" + ] + }, + { + "stopId": 14181, + "name": { + "original": "Camiño da Corredoura (Igrexa)" + }, + "latitude": 42.210954716, + "longitude": -8.727776522, + "lines": [ + "A" + ] + }, + { + "stopId": 14182, + "name": { + "original": "Rúa da Costa 39" + }, + "latitude": 42.211618245, + "longitude": -8.72147159, + "lines": [ + "A", + "18B", + "18H" + ] + }, + { + "stopId": 14183, + "name": { + "original": "Rúa do Xalón (Colexio)" + }, + "latitude": 42.220622235, + "longitude": -8.654888024, + "lines": [ + "11" + ] + }, + { + "stopId": 14184, + "name": { + "original": "Rúa do Xalón 5" + }, + "latitude": 42.217384986, + "longitude": -8.657082399, + "lines": [ + "11" + ] + }, + { + "stopId": 14185, + "name": { + "original": "Rúa da Becerreira 81" + }, + "latitude": 42.219667098, + "longitude": -8.659470523, + "lines": [ + "11" + ] + }, + { + "stopId": 14186, + "name": { + "original": "Rúa da Becerreira (fronte 64)" + }, + "latitude": 42.218044517, + "longitude": -8.662618478, + "lines": [ + "11" + ] + }, + { + "stopId": 14187, + "name": { + "original": "Rúa da Becerreira 1" + }, + "latitude": 42.221588029, + "longitude": -8.662035851, + "lines": [ + "11" + ] + }, + { + "stopId": 14188, + "name": { + "original": "Rúa da Becerreira 41" + }, + "latitude": 42.217595578, + "longitude": -8.661414166, + "lines": [ + "11" + ] + }, + { + "stopId": 14189, + "name": { + "original": "Rúa Molais 84" + }, + "latitude": 42.226081487, + "longitude": -8.654133203, + "lines": [ + "11" + ] + }, + { + "stopId": 14190, + "name": { + "original": "Rúa de Severino Cobas 196" + }, + "latitude": 42.225080876, + "longitude": -8.683314171, + "lines": [ + "25" + ] + }, + { + "stopId": 14191, + "name": { + "original": "Camiño da Bouciña 76" + }, + "latitude": 42.223737557, + "longitude": -8.682141153, + "lines": [ + "25" + ] + }, + { + "stopId": 14192, + "name": { + "original": "Rúa das Figueiras 282" + }, + "latitude": 42.231625599, + "longitude": -8.652046516, + "lines": [ + "25" + ] + }, + { + "stopId": 14193, + "name": { + "original": "Avda. de Santa Mariña 443" + }, + "latitude": 42.230916628, + "longitude": -8.641628816, + "lines": [ + "25" + ] + }, + { + "stopId": 14194, + "name": { + "original": "Avda. de Santa Mariña 425" + }, + "latitude": 42.228639377, + "longitude": -8.640978361, + "lines": [ + "25" + ] + }, + { + "stopId": 14195, + "name": { + "original": "Avda. de Santa Mariña 249" + }, + "latitude": 42.226263256, + "longitude": -8.644091084, + "lines": [ + "25" + ] + }, + { + "stopId": 14196, + "name": { + "original": "Avda. de Santa Mariña 229" + }, + "latitude": 42.225296, + "longitude": -8.649527921, + "lines": [ + "25" + ] + }, + { + "stopId": 14197, + "name": { + "original": "Rúa Molais (cruce Rúa das Carballas)" + }, + "latitude": 42.225912491, + "longitude": -8.653698801, + "lines": [ + "25" + ] + }, + { + "stopId": 14198, + "name": { + "original": "Rúa do Riomao 21" + }, + "latitude": 42.227231301, + "longitude": -8.659997969, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 14199, + "name": { + "original": "Camiño da Bouciña 79" + }, + "latitude": 42.223755464, + "longitude": -8.682041911, + "lines": [ + "25" + ] + }, + { + "stopId": 14200, + "name": { + "original": "Rúa de Severino Cobas 119" + }, + "latitude": 42.225112655, + "longitude": -8.683402684, + "lines": [ + "25" + ] + }, + { + "stopId": 14201, + "name": { + "original": "Avda. de Santa Mariña (cruce Avda. do Tranvía)" + }, + "latitude": 42.226291056, + "longitude": -8.641647591, + "lines": [ + "25" + ] + }, + { + "stopId": 14202, + "name": { + "original": "Rúa de Jenaro de la Fuente 58" + }, + "latitude": 42.231273786, + "longitude": -8.700145645, + "lines": [ + "11", + "15A", + "15B", + "15C", + "H3" + ] + }, + { + "stopId": 14203, + "name": { + "original": "Avda. do Tranvía S/N (despois Camiño Lugar)" + }, + "latitude": 42.226524401, + "longitude": -8.661251786, + "lines": [ + "11", + "15A" + ] + }, + { + "stopId": 14204, + "name": { + "original": "Rúa de Manuel Álvarez 151" + }, + "latitude": 42.22312688, + "longitude": -8.681864633, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 14205, + "name": { + "original": "Estrada do Freixo (Cemiterio)" + }, + "latitude": 42.178408629, + "longitude": -8.733198549, + "lines": [ + "7" + ] + }, + { + "stopId": 14206, + "name": { + "original": "Avda. da Gran Vía (Instituto)" + }, + "latitude": 42.220514043, + "longitude": -8.731700217, + "lines": [ + "C3i", + "7", + "11", + "13", + "15A", + "16", + "23", + "29", + "H2" + ] + }, + { + "stopId": 14207, + "name": { + "original": "Camiño do Pinal 19" + }, + "latitude": 42.161212162, + "longitude": -8.716377433, + "lines": [ + "7" + ] + }, + { + "stopId": 14208, + "name": { + "original": "Estrada de Valadares 452" + }, + "latitude": 42.162237207, + "longitude": -8.71885531, + "lines": [ + "7" + ] + }, + { + "stopId": 14209, + "name": { + "original": "Estrada da Garrida 263" + }, + "latitude": 42.173333822, + "longitude": -8.705439803, + "lines": [ + "7" + ] + }, + { + "stopId": 14210, + "name": { + "original": "Estrada da Garrida (frente 243)" + }, + "latitude": 42.173077394, + "longitude": -8.705659744, + "lines": [ + "7" + ] + }, + { + "stopId": 14211, + "name": { + "original": "Estrada do Monte Alba 32" + }, + "latitude": 42.165479162, + "longitude": -8.721775005, + "lines": [ + "7" + ] + }, + { + "stopId": 14212, + "name": { + "original": "Estrada do Monte Alba 54" + }, + "latitude": 42.164694101, + "longitude": -8.724472962, + "lines": [ + "7" + ] + }, + { + "stopId": 14213, + "name": { + "original": "Estrada do Freixo (Campo Fútbol)" + }, + "latitude": 42.169153241, + "longitude": -8.729001464, + "lines": [ + "7" + ] + }, + { + "stopId": 14214, + "name": { + "original": "Estrada do Freixo 191" + }, + "latitude": 42.175760907, + "longitude": -8.734516924, + "lines": [ + "7" + ] + }, + { + "stopId": 14215, + "name": { + "original": "Estrada do Freixo 90" + }, + "latitude": 42.175592169, + "longitude": -8.734477788, + "lines": [ + "7" + ] + }, + { + "stopId": 14216, + "name": { + "original": "Estrada do Freixo (fronte Campo Fútbol)" + }, + "latitude": 42.169143301, + "longitude": -8.729076566, + "lines": [ + "7" + ] + }, + { + "stopId": 14217, + "name": { + "original": "Estrada do Monte Alba (frente 54)" + }, + "latitude": 42.164570839, + "longitude": -8.724561475, + "lines": [ + "7" + ] + }, + { + "stopId": 14218, + "name": { + "original": "Estrada do Monte Alba 21" + }, + "latitude": 42.165288305, + "longitude": -8.721367309, + "lines": [ + "7" + ] + }, + { + "stopId": 14219, + "name": { + "original": "Camiño do Pinal 59" + }, + "latitude": 42.159500512, + "longitude": -8.718247279, + "lines": [ + "7" + ] + }, + { + "stopId": 14220, + "name": { + "original": "Camiño da Bouciña 14" + }, + "latitude": 42.225657321, + "longitude": -8.681467666, + "lines": [ + "25" + ] + }, + { + "stopId": 14221, + "name": { + "original": "Camiño da Bouciña 3" + }, + "latitude": 42.225809218, + "longitude": -8.681652991, + "lines": [ + "25" + ] + }, + { + "stopId": 14222, + "name": { + "original": "Camiño do Pinal 5" + }, + "latitude": 42.162959525, + "longitude": -8.716541365, + "lines": [ + "7" + ] + }, + { + "stopId": 14223, + "name": { + "original": "Avda. Beiramar \"Porto Pesqueiro Berbés\"" + }, + "latitude": 42.236650106, + "longitude": -8.73161427, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 14224, + "name": { + "original": "Rúa do Conde de Torrecedeira 16" + }, + "latitude": 42.234285927, + "longitude": -8.731266507, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 14225, + "name": { + "original": "Rúa do Conde de Torrecedeira 50" + }, + "latitude": 42.231716574, + "longitude": -8.732308737, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 14226, + "name": { + "original": "Rúa do Conde de Torrecedeira 70" + }, + "latitude": 42.229574859, + "longitude": -8.733073973, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 14227, + "name": { + "original": "Rúa do Conde de Torrecedeira 86" + }, + "latitude": 42.22696657, + "longitude": -8.734559706, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 14228, + "name": { + "original": "Avda. de Peinador 100" + }, + "latitude": 42.221696342, + "longitude": -8.632840997, + "lines": [ + "A" + ] + }, + { + "stopId": 14231, + "name": { + "original": "Rúa da Rabadeira 39" + }, + "latitude": 42.235542066, + "longitude": -8.652196565, + "lines": [ + "9B" + ] + }, + { + "stopId": 14232, + "name": { + "original": "Rúa da Rabadeira (fronte 33)" + }, + "latitude": 42.235317662, + "longitude": -8.652094641, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 14233, + "name": { + "original": "Rúa San Cristobo 90" + }, + "latitude": 42.241037275, + "longitude": -8.668947597, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 14236, + "name": { + "original": "Rúa de Manuel Cominges (fronte 112)" + }, + "latitude": 42.196308523, + "longitude": -8.723526935, + "lines": [ + "12B" + ] + }, + { + "stopId": 14237, + "name": { + "original": "Rúa de Manuel Cominges 134" + }, + "latitude": 42.196119748, + "longitude": -8.723457198, + "lines": [ + "12B" + ] + }, + { + "stopId": 14238, + "name": { + "original": "Rúa da Saa do Monte 5" + }, + "latitude": 42.20458802, + "longitude": -8.714617309, + "lines": [ + "18B" + ] + }, + { + "stopId": 14240, + "name": { + "original": "Rúa das Chabarras 21" + }, + "latitude": 42.197985091, + "longitude": -8.714523201, + "lines": [ + "18B" + ] + }, + { + "stopId": 14241, + "name": { + "original": "Rúa das Chabarras 60" + }, + "latitude": 42.197842023, + "longitude": -8.71471632, + "lines": [ + "18B" + ] + }, + { + "stopId": 14242, + "name": { + "original": "Rúa das Chabarras (cruce Camiño dos Pasais)" + }, + "latitude": 42.196378259, + "longitude": -8.716979043, + "lines": [ + "18B" + ] + }, + { + "stopId": 14243, + "name": { + "original": "Rúa das Chabarras 24" + }, + "latitude": 42.196539214, + "longitude": -8.716874437, + "lines": [ + "18B" + ] + }, + { + "stopId": 14244, + "name": { + "original": "Rúa de Macal 60" + }, + "latitude": 42.198216234, + "longitude": -8.721498041, + "lines": [ + "18B", + "18H", + "27" + ] + }, + { + "stopId": 14245, + "name": { + "original": "Avda. de García Barbón 43" + }, + "latitude": 42.23691728, + "longitude": -8.716743143, + "lines": [ + "C3i", + "5B", + "10", + "16", + "17", + "N1" + ] + }, + { + "stopId": 14247, + "name": { + "original": "Camiño dos Muíños 69" + }, + "latitude": 42.200511179, + "longitude": -8.769110573, + "lines": [ + "12A" + ] + }, + { + "stopId": 14248, + "name": { + "original": "Camiño dos Muíños 74" + }, + "latitude": 42.200580723, + "longitude": -8.76911862, + "lines": [ + "12A" + ] + }, + { + "stopId": 14249, + "name": { + "original": "Avda. de Cesáreo Vázquez 5" + }, + "latitude": 42.177662554, + "longitude": -8.800157923, + "lines": [ + "12A" + ] + }, + { + "stopId": 14250, + "name": { + "original": "Avda. do Aeroporto 463" + }, + "latitude": 42.234914814, + "longitude": -8.658983411, + "lines": [ + "A", + "9B" + ] + }, + { + "stopId": 14251, + "name": { + "original": "Avda. do Aeroporto (fronte 463)" + }, + "latitude": 42.234817602, + "longitude": -8.65882027, + "lines": [ + "A", + "9B", + "27" + ] + }, + { + "stopId": 14252, + "name": { + "original": "Estrada Clara Campoamor (Instituto)" + }, + "latitude": 42.165934208, + "longitude": -8.707243001, + "lines": [ + "U1" + ] + }, + { + "stopId": 14253, + "name": { + "original": "Estrada Clara Campoamor (cruce Rúa do Padrón do Couto)" + }, + "latitude": 42.164455564, + "longitude": -8.707223843, + "lines": [ + "U1" + ] + }, + { + "stopId": 14255, + "name": { + "original": "Rúa do Pintor Colmeiro (Parque do Pintor Colmeiro)" + }, + "latitude": 42.225111918, + "longitude": -8.726733526, + "lines": [ + "16" + ] + }, + { + "stopId": 14256, + "name": { + "original": "Rúa de Zamora 89" + }, + "latitude": 42.222198901, + "longitude": -8.728317834, + "lines": [ + "16" + ] + }, + { + "stopId": 14257, + "name": { + "original": "Rúa de Zamora 71" + }, + "latitude": 42.223448271, + "longitude": -8.725547112, + "lines": [ + "16" + ] + }, + { + "stopId": 14258, + "name": { + "original": "Rúa de Zamora 51" + }, + "latitude": 42.224870416, + "longitude": -8.723632015, + "lines": [ + "16" + ] + }, + { + "stopId": 14259, + "name": { + "original": "Rúa de Zamora 31" + }, + "latitude": 42.227088982, + "longitude": -8.721545256, + "lines": [ + "16" + ] + }, + { + "stopId": 14260, + "name": { + "original": "Avda. da Gran Vía (fronte Avda. de Madrid)" + }, + "latitude": 42.228741057, + "longitude": -8.71961914, + "lines": [ + "7", + "14", + "15A", + "16", + "18A", + "18B", + "18H", + "H2" + ] + }, + { + "stopId": 14261, + "name": { + "original": "Rúa de Zamora 1" + }, + "latitude": 42.228644118, + "longitude": -8.720692314, + "lines": [ + "16" + ] + }, + { + "stopId": 14264, + "name": { + "original": "Rúa de Urzáiz - Príncipe" + }, + "latitude": 42.235873545, + "longitude": -8.720083317, + "lines": [ + "C1", + "A", + "4A", + "4C", + "5A", + "7", + "9B", + "12B", + "14", + "15B", + "15C", + "16", + "17", + "18A", + "18B", + "18H", + "24", + "28", + "N1", + "N4" + ] + }, + { + "stopId": 14267, + "name": { + "original": "Avda. da Atlántida 64" + }, + "latitude": 42.221892792, + "longitude": -8.758191526, + "lines": [ + "10", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 14268, + "name": { + "original": "Avda. da Atlántida 49" + }, + "latitude": 42.221731945, + "longitude": -8.758417175, + "lines": [ + "10", + "15B", + "15C" + ] + }, + { + "stopId": 14270, + "name": { + "original": "Estrada da Balsa 67" + }, + "latitude": 42.196619218, + "longitude": -8.743240048, + "lines": [ + "17" + ] + }, + { + "stopId": 14271, + "name": { + "original": "Estrada da Balsa 103" + }, + "latitude": 42.196050474, + "longitude": -8.745105715, + "lines": [ + "17" + ] + }, + { + "stopId": 14273, + "name": { + "original": "Rúa do Xalón 41" + }, + "latitude": 42.219274062, + "longitude": -8.656419893, + "lines": [ + "11" + ] + }, + { + "stopId": 14277, + "name": { + "original": "Avda. da Mariña Española 8" + }, + "latitude": 42.251776399, + "longitude": -8.69414009, + "lines": [ + "17" + ] + }, + { + "stopId": 14278, + "name": { + "original": "Avda. da Mariña Española 44" + }, + "latitude": 42.25430173, + "longitude": -8.692915616, + "lines": [ + "17" + ] + }, + { + "stopId": 14279, + "name": { + "original": "Riós (Rotonda)" + }, + "latitude": 42.257069093, + "longitude": -8.690786611, + "lines": [ + "17" + ] + }, + { + "stopId": 14280, + "name": { + "original": "Avda. da Mariña Española (ETEA)" + }, + "latitude": 42.254604716, + "longitude": -8.692539681, + "lines": [ + "17" + ] + }, + { + "stopId": 14281, + "name": { + "original": "Avda. da Mariña Española (Praia de Ríos)" + }, + "latitude": 42.251596707, + "longitude": -8.69420171, + "lines": [ + "17" + ] + }, + { + "stopId": 14287, + "name": { + "original": "Rúa Santa Tegra 67" + }, + "latitude": 42.25020334, + "longitude": -8.701924083, + "lines": [ + "17" + ] + }, + { + "stopId": 14288, + "name": { + "original": "Avda. de Guixar (fronte 28)" + }, + "latitude": 42.249218849, + "longitude": -8.704807605, + "lines": [ + "17" + ] + }, + { + "stopId": 14289, + "name": { + "original": "Rúa de Xulián Estévez (fronte 58)" + }, + "latitude": 42.246484972, + "longitude": -8.705864005, + "lines": [ + "17" + ] + }, + { + "stopId": 14290, + "name": { + "original": "Rúa de Xulián Estévez (fronte 18)" + }, + "latitude": 42.244107542, + "longitude": -8.706343638, + "lines": [ + "17" + ] + }, + { + "stopId": 14291, + "name": { + "original": "Avda. da Ponte (Vigo Memorial)" + }, + "latitude": 42.209935219, + "longitude": -8.671464542, + "lines": [ + "12B", + "15B", + "15C", + "U2" + ] + }, + { + "stopId": 14294, + "name": { + "original": "Avda. de Ricardo Mella 406" + }, + "latitude": 42.190684424876565, + "longitude": -8.799308812770041, + "lines": [ + "12A" + ] + }, + { + "stopId": 14295, + "name": { + "original": "Rúa de Pi i Margall 121" + }, + "latitude": 42.230436358, + "longitude": -8.731437473, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 14296, + "name": { + "original": "Praza dos Leóns (Vigozoo)" + }, + "latitude": 42.248375604, + "longitude": -8.675578666, + "lines": [ + "28" + ] + }, + { + "stopId": 14299, + "name": { + "original": "Avda. de Samil (frente Verbum)" + }, + "latitude": 42.213644883, + "longitude": -8.774567214, + "lines": [ + "C3i", + "15A", + "N1" + ] + }, + { + "stopId": 14300, + "name": { + "original": "Avda. da Florida 30" + }, + "latitude": 42.217907548, + "longitude": -8.73707436, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 14301, + "name": { + "original": "Avda. da Florida 47" + }, + "latitude": 42.218257459, + "longitude": -8.736328798, + "lines": [ + "5A", + "11", + "29" + ] + }, + { + "stopId": 14302, + "name": { + "original": "Estrada Vella de Madrid 7" + }, + "latitude": 42.214542094, + "longitude": -8.696431619, + "lines": [ + "12A", + "12B", + "13", + "H3" + ] + }, + { + "stopId": 14304, + "name": { + "original": "Estrada Vella de Madrid 145" + }, + "latitude": 42.221313975, + "longitude": -8.681944471, + "lines": [ + "12A", + "12B", + "13", + "31" + ] + }, + { + "stopId": 14307, + "name": { + "original": "Rúa do Pintor Colmeiro 11" + }, + "latitude": 42.224464416, + "longitude": -8.727967343, + "lines": [ + "16" + ] + }, + { + "stopId": 14308, + "name": { + "original": "Camiño do Pinal 6" + }, + "latitude": 42.16328558, + "longitude": -8.716707662, + "lines": [ + "7" + ] + }, + { + "stopId": 14309, + "name": { + "original": "Camiño do Pinal 28" + }, + "latitude": 42.161067029, + "longitude": -8.716468628, + "lines": [ + "7" + ] + }, + { + "stopId": 14310, + "name": { + "original": "Camiño do Pinal (fronte 57)" + }, + "latitude": 42.159564137, + "longitude": -8.718295559, + "lines": [ + "7" + ] + }, + { + "stopId": 14311, + "name": { + "original": "Estrada de Valadares 505" + }, + "latitude": 42.162720337, + "longitude": -8.718900908, + "lines": [ + "7" + ] + }, + { + "stopId": 14314, + "name": { + "original": "Rúa das Mantelas (cruce Avda. da Gran Vía)" + }, + "latitude": 42.227212568, + "longitude": -8.720183032, + "lines": [ + "18A" + ] + }, + { + "stopId": 14315, + "name": { + "original": "Rúa das Mantelas 92" + }, + "latitude": 42.22393338, + "longitude": -8.716924148, + "lines": [ + "18A" + ] + }, + { + "stopId": 14317, + "name": { + "original": "Rúa da Salgueira Entrada (Igrexa)" + }, + "latitude": 42.222723933, + "longitude": -8.719150283, + "lines": [ + "18A" + ] + }, + { + "stopId": 14318, + "name": { + "original": "Rúa da Salguera Entrada (fronte 5)" + }, + "latitude": 42.222591256, + "longitude": -8.717753486, + "lines": [ + "18A" + ] + }, + { + "stopId": 14319, + "name": { + "original": "Rúa das Coutadas (Fonte)" + }, + "latitude": 42.221002214, + "longitude": -8.72027208, + "lines": [ + "18A" + ] + }, + { + "stopId": 14320, + "name": { + "original": "Rúa do Miradoiro (Rotonda Centro Comercial)" + }, + "latitude": 42.220799025, + "longitude": -8.723345356, + "lines": [ + "18A" + ] + }, + { + "stopId": 14321, + "name": { + "original": "Camiño do Freixeiro 74" + }, + "latitude": 42.218131641, + "longitude": -8.723120057, + "lines": [ + "18A" + ] + }, + { + "stopId": 14322, + "name": { + "original": "Rúa da Fonte Santa 4" + }, + "latitude": 42.217813814, + "longitude": -8.721352482, + "lines": [ + "18A" + ] + }, + { + "stopId": 14323, + "name": { + "original": "Rúa Finca dos Aires (cruce Rúa da Fonte Santa)" + }, + "latitude": 42.217059742, + "longitude": -8.720340235, + "lines": [ + "18A" + ] + }, + { + "stopId": 14324, + "name": { + "original": "Rúa Finca dos Aires (Urbanización)" + }, + "latitude": 42.217256401, + "longitude": -8.720101519, + "lines": [ + "18A" + ] + }, + { + "stopId": 14325, + "name": { + "original": "Rúa da Fonte Santa (fronte 4)" + }, + "latitude": 42.217848286, + "longitude": -8.7214811, + "lines": [ + "18A" + ] + }, + { + "stopId": 14326, + "name": { + "original": "Baixada á Ponte Nova 61" + }, + "latitude": 42.218038279, + "longitude": -8.722489738, + "lines": [ + "18A" + ] + }, + { + "stopId": 14328, + "name": { + "original": "Citroën - PSA" + }, + "latitude": 42.208988415, + "longitude": -8.746151897, + "lines": [ + "LZH", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14329, + "name": { + "original": "Citroën (Puerta Principal)" + }, + "latitude": 42.210124372, + "longitude": -8.741139991, + "lines": [ + "LZH", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14330, + "name": { + "original": "Subida ás Chans (fronte cruce Rúa Senra)" + }, + "latitude": 42.198072667, + "longitude": -8.682624653, + "lines": [ + "14" + ] + }, + { + "stopId": 14331, + "name": { + "original": "Rúa de Álvaro Cunqueiro 30" + }, + "latitude": 42.223769828, + "longitude": -8.728938728, + "lines": [ + "4A", + "4C", + "5A", + "5B", + "11", + "12A", + "12B", + "17", + "27", + "N1", + "LZH" + ] + }, + { + "stopId": 14333, + "name": { + "original": "Rúa de Cánovas del Castillo 18" + }, + "latitude": 42.240189011, + "longitude": -8.726765331, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "10", + "15B", + "15C", + "28", + "N4" + ] + }, + { + "stopId": 14335, + "name": { + "original": "Camiño do Arieiro (Residencia de Maiores)" + }, + "latitude": 42.212692269, + "longitude": -8.675661599, + "lines": [ + "31" + ] + }, + { + "stopId": 14336, + "name": { + "original": "Rúa das Teixugueiras 8" + }, + "latitude": 42.21447626, + "longitude": -8.75600551, + "lines": [ + "13", + "15A" + ] + }, + { + "stopId": 14337, + "name": { + "original": "Rúa do Limpiño (Rotonda Rúa Teixugueiras)" + }, + "latitude": 42.213080218, + "longitude": -8.754660224, + "lines": [ + "5A", + "5B", + "13", + "15A", + "15B", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14345, + "name": { + "original": "Rúa do Areiro 49" + }, + "latitude": 42.237269816, + "longitude": -8.685138009, + "lines": [ + "28" + ] + }, + { + "stopId": 14346, + "name": { + "original": "Rúa do Areiro 52" + }, + "latitude": 42.237287688, + "longitude": -8.685019992, + "lines": [ + "28" + ] + }, + { + "stopId": 14347, + "name": { + "original": "Rúa do Areiro (cruce Salcides)" + }, + "latitude": 42.23913765, + "longitude": -8.683873934, + "lines": [ + "28" + ] + }, + { + "stopId": 14348, + "name": { + "original": "Rúa do Areiro 76" + }, + "latitude": 42.238950988, + "longitude": -8.683793467, + "lines": [ + "28" + ] + }, + { + "stopId": 14349, + "name": { + "original": "Rúa do Areiro (Campo de Fútbol)" + }, + "latitude": 42.244717312, + "longitude": -8.678473607, + "lines": [ + "28" + ] + }, + { + "stopId": 14350, + "name": { + "original": "Rúa do Areiro (fronte Campo de Fútbol)" + }, + "latitude": 42.244849353, + "longitude": -8.678366319, + "lines": [ + "28" + ] + }, + { + "stopId": 14353, + "name": { + "original": "Praza dos Leóns (fronte Vigozoo)" + }, + "latitude": 42.248151648, + "longitude": -8.675976097, + "lines": [ + "28" + ] + }, + { + "stopId": 14354, + "name": { + "original": "Avda. de Ramón Nieto (fronte Igrexa)" + }, + "latitude": 42.225911433, + "longitude": -8.675526243, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 14355, + "name": { + "original": "Avda. de Ramón Nieto 409" + }, + "latitude": 42.226638363, + "longitude": -8.676135104, + "lines": [ + "11", + "15A", + "15B", + "15C", + "31", + "H3" + ] + }, + { + "stopId": 14356, + "name": { + "original": "Avda. de Ricardo Mella 314" + }, + "latitude": 42.192009114, + "longitude": -8.783993123, + "lines": [ + "12A" + ] + }, + { + "stopId": 14357, + "name": { + "original": "Avda. de Ricardo Mella (cruce Camiño do Río)" + }, + "latitude": 42.191796473, + "longitude": -8.784014088, + "lines": [ + "12A" + ] + }, + { + "stopId": 14358, + "name": { + "original": "Rúa das Teixugueiras 28" + }, + "latitude": 42.209054557, + "longitude": -8.75715865, + "lines": [ + "5A", + "5B", + "13", + "N4" + ] + }, + { + "stopId": 14359, + "name": { + "original": "Rúa das Teixugueiras 16-Portal 2" + }, + "latitude": 42.212235738, + "longitude": -8.755011746, + "lines": [ + "5A", + "5B", + "13", + "N4" + ] + }, + { + "stopId": 14360, + "name": { + "original": "Rúa das Teixugueiras 19-Portal 5" + }, + "latitude": 42.208965857, + "longitude": -8.757020567, + "lines": [ + "5A", + "5B", + "13", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14361, + "name": { + "original": "Rúa das Teixugueiras 17" + }, + "latitude": 42.209770472, + "longitude": -8.755295907, + "lines": [ + "5A", + "5B", + "13", + "15A", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14362, + "name": { + "original": "Avda. de Samil 101" + }, + "latitude": 42.202937872, + "longitude": -8.776830486, + "lines": [ + "C3d", + "4C", + "10" + ] + }, + { + "stopId": 14364, + "name": { + "original": "Estrada das Plantas (fronte Cidade Deportiva)" + }, + "latitude": 42.175757186, + "longitude": -8.671074371, + "lines": [ + "15C" + ] + }, + { + "stopId": 14365, + "name": { + "original": "Estrada das Plantas (Viveiros)" + }, + "latitude": 42.181650197, + "longitude": -8.667515723, + "lines": [ + "15C" + ] + }, + { + "stopId": 14372, + "name": { + "original": "Barrio da Salgueira 22" + }, + "latitude": 42.221887526, + "longitude": -8.720011371, + "lines": [ + "18A" + ] + }, + { + "stopId": 14376, + "name": { + "original": "Rúa da Pateira 20" + }, + "latitude": 42.226612651, + "longitude": -8.699658408, + "lines": [ + "25" + ] + }, + { + "stopId": 14377, + "name": { + "original": "Rúa da Pateira 5" + }, + "latitude": 42.226582661, + "longitude": -8.700385762, + "lines": [ + "25" + ] + }, + { + "stopId": 14378, + "name": { + "original": "Rúa Molais 83" + }, + "latitude": 42.22396201, + "longitude": -8.653340726, + "lines": [ + "25" + ] + }, + { + "stopId": 14381, + "name": { + "original": "Rúa do Abade Juan de Bastos 6" + }, + "latitude": 42.195647685, + "longitude": -8.728974153, + "lines": [ + "17" + ] + }, + { + "stopId": 14383, + "name": { + "original": "Estrada Clara Campoamor (cruce Estrada do Portal)" + }, + "latitude": 42.174718265, + "longitude": -8.713684656, + "lines": [ + "U1" + ] + }, + { + "stopId": 14384, + "name": { + "original": "Estrada Clara Campoamor (Parque Tecnolóxico)" + }, + "latitude": 42.175073486, + "longitude": -8.713494654, + "lines": [ + "U1" + ] + }, + { + "stopId": 14385, + "name": { + "original": "Rúa da Vista do Mar 45" + }, + "latitude": 42.240712912, + "longitude": -8.6919418, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 14386, + "name": { + "original": "Rúa da Vista do Mar (Embalse)" + }, + "latitude": 42.238020208, + "longitude": -8.691543884, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 14387, + "name": { + "original": "Rúa da Vista do Mar 1" + }, + "latitude": 42.237167043, + "longitude": -8.693243792, + "lines": [ + "4A", + "24" + ] + }, + { + "stopId": 14388, + "name": { + "original": "Rúa das Teixugueiras 11" + }, + "latitude": 42.211610124, + "longitude": -8.754550253, + "lines": [ + "5A", + "5B", + "13", + "15A", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14389, + "name": { + "original": "Rúa das Teixugueiras 22" + }, + "latitude": 42.210212603, + "longitude": -8.755079989, + "lines": [ + "5A", + "5B", + "13", + "N4" + ] + }, + { + "stopId": 14390, + "name": { + "original": "Rúa do Salgueiro (cruce Camiño Sanatorio)" + }, + "latitude": 42.241570883, + "longitude": -8.655380719, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 14391, + "name": { + "original": "Rúa do Salgueiro (fronte 38)" + }, + "latitude": 42.241652296, + "longitude": -8.655302935, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 14392, + "name": { + "original": "Rúa da Pedra Seixa (Colexio)" + }, + "latitude": 42.209458591, + "longitude": -8.760561083, + "lines": [ + "5A" + ] + }, + { + "stopId": 14393, + "name": { + "original": "Rúa da Pedra Seixa (fronte Colexio)" + }, + "latitude": 42.209568936, + "longitude": -8.760777001, + "lines": [ + "5A" + ] + }, + { + "stopId": 14395, + "name": { + "original": "Estrada de Madrid 217" + }, + "latitude": 42.215601037, + "longitude": -8.675477665, + "lines": [ + "12B", + "15B", + "15C" + ] + }, + { + "stopId": 14396, + "name": { + "original": "Avda. de García Barbón 106" + }, + "latitude": 42.239965365, + "longitude": -8.708024282, + "lines": [ + "C3d", + "5B", + "10", + "17", + "31", + "H2", + "H3", + "PSA 1" + ] + }, + { + "stopId": 14397, + "name": { + "original": "Avda. de García Barbón (fronte 104)" + }, + "latitude": 42.23973713, + "longitude": -8.708397682, + "lines": [ + "C3i", + "5B", + "10", + "17", + "N1", + "H3" + ] + }, + { + "stopId": 14398, + "name": { + "original": "Avda. da Gran Vía 116" + }, + "latitude": 42.22406594, + "longitude": -8.723691036, + "lines": [ + "C3d", + "13", + "15A", + "23", + "29", + "H2", + "PSA 1" + ] + }, + { + "stopId": 14401, + "name": { + "original": "Rúa de San Paio (cruce Camiño Barroca)" + }, + "latitude": 42.21072167, + "longitude": -8.76212542, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 14402, + "name": { + "original": "Rúa de San Paio 111" + }, + "latitude": 42.210791203, + "longitude": -8.761940347, + "lines": [ + "4A", + "12A" + ] + }, + { + "stopId": 14403, + "name": { + "original": "Rúa de Pedro Alvarado (cruce Camiño das Maceiras)" + }, + "latitude": 42.25049654, + "longitude": -8.698390035, + "lines": [ + "17" + ] + }, + { + "stopId": 14404, + "name": { + "original": "Rúa do Doutor Corbal 58" + }, + "latitude": 42.251785642, + "longitude": -8.696871994, + "lines": [ + "17" + ] + }, + { + "stopId": 14406, + "name": { + "original": "Rúa de Enrique Lorenzo 32" + }, + "latitude": 42.249462772, + "longitude": -8.699772952, + "lines": [ + "17" + ] + }, + { + "stopId": 14408, + "name": { + "original": "Rúa de Pedro Alvarado 5" + }, + "latitude": 42.248885006, + "longitude": -8.698128758, + "lines": [ + "17" + ] + }, + { + "stopId": 14409, + "name": { + "original": "Estrada das Plantas (fronte cruce Avda. do Rebullón)" + }, + "latitude": 42.204188441, + "longitude": -8.670257126, + "lines": [ + "15B", + "15C" + ] + }, + { + "stopId": 14410, + "name": { + "original": "Estrada das Plantas (cruce Avda. do Rebullón)" + }, + "latitude": 42.204639457, + "longitude": -8.670329545, + "lines": [ + "15C" + ] + }, + { + "stopId": 14411, + "name": { + "original": "Rúa de Xeme 1" + }, + "latitude": 42.205174543, + "longitude": -8.698209134, + "lines": [ + "14" + ] + }, + { + "stopId": 14412, + "name": { + "original": "Rúa de Xeme 6" + }, + "latitude": 42.20497586, + "longitude": -8.697957006, + "lines": [ + "14" + ] + }, + { + "stopId": 14413, + "name": { + "original": "Estrada da Garrida (cruce Camiño Fabas)" + }, + "latitude": 42.173955568, + "longitude": -8.703050076, + "lines": [ + "7" + ] + }, + { + "stopId": 14414, + "name": { + "original": "Avda. de Santa Mariña 40" + }, + "latitude": 42.221587211, + "longitude": -8.665078444, + "lines": [ + "11" + ] + }, + { + "stopId": 14415, + "name": { + "original": "Avda. de Santa Mariña (antes 49)" + }, + "latitude": 42.221557416, + "longitude": -8.665381534, + "lines": [ + "11" + ] + }, + { + "stopId": 14416, + "name": { + "original": "Rúa de Severino Cobas 118" + }, + "latitude": 42.225468623, + "longitude": -8.691491081, + "lines": [ + "25" + ] + }, + { + "stopId": 14419, + "name": { + "original": "Estrada de Bembrive (cruce Camiño dos Rapadouros)" + }, + "latitude": 42.201138734, + "longitude": -8.688585073, + "lines": [ + "6" + ] + }, + { + "stopId": 14420, + "name": { + "original": "Estrada de Bembrive 318" + }, + "latitude": 42.201419233, + "longitude": -8.688526069, + "lines": [ + "6" + ] + }, + { + "stopId": 14421, + "name": { + "original": "Estrada das Prantas (fronte Campo de Béisbol)" + }, + "latitude": 42.186683264, + "longitude": -8.669320703, + "lines": [ + "15C" + ] + }, + { + "stopId": 14422, + "name": { + "original": "Estrada das Plantas (Campo de Béisbol)" + }, + "latitude": 42.18695315, + "longitude": -8.669514066, + "lines": [ + "15C" + ] + }, + { + "stopId": 14425, + "name": { + "original": "Avda. do Alcalde Gregorio Espino 2" + }, + "latitude": 42.232253792, + "longitude": -8.707208575, + "lines": [ + "31" + ] + }, + { + "stopId": 14475, + "name": { + "original": "Rúa de Barcelona 78" + }, + "latitude": 42.222992354, + "longitude": -8.728300382, + "lines": [ + "C1" + ] + }, + { + "stopId": 14890, + "name": { + "original": "Rúa das Teixugueiras 25" + }, + "latitude": 42.207545331, + "longitude": -8.758718406, + "lines": [ + "5B", + "13", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 14892, + "name": { + "original": "Rúa do Conde de Torrecedeira 123" + }, + "latitude": 42.224929414, + "longitude": -8.735414067, + "lines": [ + "C1", + "C3d", + "A", + "9B", + "15C", + "N4", + "H1" + ] + }, + { + "stopId": 14893, + "name": { + "original": "Rúa de Manuel Costas Bastos 26" + }, + "latitude": 42.243157956, + "longitude": -8.666962176, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 14894, + "name": { + "original": "Avda. do Alcalde Portanet 8" + }, + "latitude": 42.211736934, + "longitude": -8.733337505, + "lines": [ + "7", + "12B", + "17", + "H1" + ] + }, + { + "stopId": 14895, + "name": { + "original": "Rúa do Areiro (cruce Camiño das Laxes)" + }, + "latitude": 42.241392275, + "longitude": -8.681203235, + "lines": [ + "28" + ] + }, + { + "stopId": 14896, + "name": { + "original": "Rúa do Areiro 93" + }, + "latitude": 42.241385532, + "longitude": -8.681400937, + "lines": [ + "28" + ] + }, + { + "stopId": 14897, + "name": { + "original": "Camiño do Arieiro (fronte 13)" + }, + "latitude": 42.213239161, + "longitude": -8.67854147, + "lines": [ + "31" + ] + }, + { + "stopId": 14898, + "name": { + "original": "Camiño do Arieiro 13" + }, + "latitude": 42.213239161, + "longitude": -8.678369808, + "lines": [ + "31" + ] + }, + { + "stopId": 14899, + "name": { + "original": "Rúa de López Mora 33" + }, + "latitude": 42.225485719, + "longitude": -8.730501434, + "lines": [ + "5A", + "5B", + "12A" + ] + }, + { + "stopId": 14900, + "name": { + "original": "Rúa de Martín Echegaray 24" + }, + "latitude": 42.217196117, + "longitude": -8.743726669, + "lines": [ + "23", + "N4" + ] + }, + { + "stopId": 14901, + "name": { + "original": "Avda. de Castelao 1" + }, + "latitude": 42.220211003, + "longitude": -8.734183023, + "lines": [ + "C3i", + "10", + "11", + "15A", + "U1" + ] + }, + { + "stopId": 14903, + "name": { + "original": "Rúa de Pi i Margall 66" + }, + "latitude": 42.23174719, + "longitude": -8.731081308, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 14905, + "name": { + "original": "Camiño da Devesa (Cemiterio)" + }, + "latitude": 42.249981353, + "longitude": -8.667186504, + "lines": [ + "9B" + ] + }, + { + "stopId": 14906, + "name": { + "original": "Rúa da Rabadeira 6" + }, + "latitude": 42.232479787, + "longitude": -8.654890792, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 14907, + "name": { + "original": "Rúa da Rabadeira 24" + }, + "latitude": 42.233655479, + "longitude": -8.653300242, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 14908, + "name": { + "original": "Rúa da Rabadeira 17" + }, + "latitude": 42.233829075, + "longitude": -8.653458259, + "lines": [ + "9B" + ] + }, + { + "stopId": 14909, + "name": { + "original": "Rúa da Rabadeira 11" + }, + "latitude": 42.232663198, + "longitude": -8.655097059, + "lines": [ + "9B" + ] + }, + { + "stopId": 14910, + "name": { + "original": "Estrada do Marco 4" + }, + "latitude": 42.21025095, + "longitude": -8.704036986, + "lines": [ + "18A", + "18B", + "H3" + ] + }, + { + "stopId": 14911, + "name": { + "original": "Estrada do Marco 16" + }, + "latitude": 42.208830737, + "longitude": -8.706971174, + "lines": [ + "18A", + "18B", + "H3" + ] + }, + { + "stopId": 15001, + "name": { + "original": "Rúa Regueiro do Forno (Vial C) Centro de Servicios" + }, + "latitude": 42.176053629, + "longitude": -8.709460132, + "lines": [ + "PTL" + ] + }, + { + "stopId": 15002, + "name": { + "original": "PTL 2" + }, + "latitude": 42.177194637, + "longitude": -8.707850807, + "lines": [ + "PTL" + ] + }, + { + "stopId": 15003, + "name": { + "original": "PTL 3" + }, + "latitude": 42.178124939, + "longitude": -8.706606262, + "lines": [ + "PTL" + ] + }, + { + "stopId": 15004, + "name": { + "original": "PTL 4" + }, + "latitude": 42.176503017, + "longitude": -8.710007303, + "lines": [ + "PTL" + ] + }, + { + "stopId": 20009, + "name": { + "original": "Estrada Herville 16" + }, + "latitude": 42.154843231, + "longitude": -8.67357438, + "lines": [ + "7" + ] + }, + { + "stopId": 20010, + "name": { + "original": "Avda. de Balaídos 69" + }, + "latitude": 42.212824845, + "longitude": -8.737161077, + "lines": [ + "16", + "23", + "H" + ] + }, + { + "stopId": 20011, + "name": { + "original": "Avda. de Balaídos 11" + }, + "latitude": 42.213089061, + "longitude": -8.733392573, + "lines": [ + "16", + "23", + "H" + ] + }, + { + "stopId": 20012, + "name": { + "original": "Avda. de Castrelos 33" + }, + "latitude": 42.215888032, + "longitude": -8.732331627, + "lines": [ + "A", + "16", + "23", + "27", + "H2" + ] + }, + { + "stopId": 20013, + "name": { + "original": "Avda. de Castrelos 116" + }, + "latitude": 42.215905917, + "longitude": -8.732471102, + "lines": [ + "7", + "12B", + "17", + "27", + "H2", + "PTL" + ] + }, + { + "stopId": 20018, + "name": { + "original": "Estrada Herville 70" + }, + "latitude": 42.151451604, + "longitude": -8.673803367, + "lines": [ + "7" + ] + }, + { + "stopId": 20019, + "name": { + "original": "Subida aos Padróns 165" + }, + "latitude": 42.149222193, + "longitude": -8.679363987, + "lines": [ + "7" + ] + }, + { + "stopId": 20020, + "name": { + "original": "Subida aos Padróns (cruce Parque Forestal)" + }, + "latitude": 42.151606055, + "longitude": -8.679299082, + "lines": [ + "7" + ] + }, + { + "stopId": 20021, + "name": { + "original": "Subida aos Padróns (fronte 34)" + }, + "latitude": 42.152770176, + "longitude": -8.686251828, + "lines": [ + "7" + ] + }, + { + "stopId": 20022, + "name": { + "original": "Rúa da Vía Norte (Hospital)" + }, + "latitude": 42.234622237, + "longitude": -8.707758443, + "lines": [ + "24" + ] + }, + { + "stopId": 20023, + "name": { + "original": "Rúa da Vía Norte - Est. Intermodal - C.C." + }, + "latitude": 42.234062973, + "longitude": -8.712195759, + "lines": [ + "24" + ] + }, + { + "stopId": 20024, + "name": { + "original": "Rúa das Teixugueiras 34" + }, + "latitude": 42.207634066, + "longitude": -8.758920861, + "lines": [ + "5B", + "13", + "N4" + ] + }, + { + "stopId": 20025, + "name": { + "original": "Rúa das Teixugueiras 38" + }, + "latitude": 42.206553268, + "longitude": -8.760122491, + "lines": [ + "5B", + "13", + "N4" + ] + }, + { + "stopId": 20026, + "name": { + "original": "Rúa das Teixugueiras 29" + }, + "latitude": 42.206488366, + "longitude": -8.759906624, + "lines": [ + "5B", + "13", + "N4", + "U1", + "H", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 20027, + "name": { + "original": "Avda. de Castelao 64" + }, + "latitude": 42.217691983, + "longitude": -8.749585877, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "N4", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 20029, + "name": { + "original": "Subida á Madroa (fronte Campo Fútbol)" + }, + "latitude": 42.245921506, + "longitude": -8.673014474, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 20030, + "name": { + "original": "Subida á Madroa (Campo Fútbol)" + }, + "latitude": 42.247859379, + "longitude": -8.674363625, + "lines": [ + "9B", + "28" + ] + }, + { + "stopId": 20041, + "name": { + "original": "Rúa da Cabalaría 91" + }, + "latitude": 42.233622103, + "longitude": -8.689209566, + "lines": [ + "27", + "28" + ] + }, + { + "stopId": 20042, + "name": { + "original": "Rúa da Cabalaría 148" + }, + "latitude": 42.233723398, + "longitude": -8.689094231, + "lines": [ + "28" + ] + }, + { + "stopId": 20043, + "name": { + "original": "Rúa do Areiro 20" + }, + "latitude": 42.236036786, + "longitude": -8.686656768, + "lines": [ + "28" + ] + }, + { + "stopId": 20044, + "name": { + "original": "Rúa de Martín Echegaray 7" + }, + "latitude": 42.215220874, + "longitude": -8.742680967, + "lines": [ + "23", + "N4" + ] + }, + { + "stopId": 20045, + "name": { + "original": "Rúa de Xestoso 4" + }, + "latitude": 42.200532989, + "longitude": -8.674075447, + "lines": [ + "15B" + ] + }, + { + "stopId": 20046, + "name": { + "original": "Rúa de Xestoso 12" + }, + "latitude": 42.201968444, + "longitude": -8.67477879, + "lines": [ + "15B" + ] + }, + { + "stopId": 20047, + "name": { + "original": "Rúa do Xestoso 72" + }, + "latitude": 42.204330306, + "longitude": -8.674670483, + "lines": [ + "15B" + ] + }, + { + "stopId": 20048, + "name": { + "original": "Avda. de Cesáreo Vázquez (cruce Camiño Amariz Lourenzo)" + }, + "latitude": 42.182684406, + "longitude": -8.802402364, + "lines": [ + "11", + "12A" + ] + }, + { + "stopId": 20049, + "name": { + "original": "Avda. de Cesáreo Vázquez 62" + }, + "latitude": 42.18238342, + "longitude": -8.802126069, + "lines": [ + "11" + ] + }, + { + "stopId": 20050, + "name": { + "original": "Rúa de Severino Cobas 186" + }, + "latitude": 42.225550059, + "longitude": -8.686684563, + "lines": [ + "25" + ] + }, + { + "stopId": 20051, + "name": { + "original": "Rúa de Severino Cobas 89" + }, + "latitude": 42.225652904, + "longitude": -8.686624017, + "lines": [ + "25" + ] + }, + { + "stopId": 20052, + "name": { + "original": "Rúa de Aragón 21" + }, + "latitude": 42.232748414, + "longitude": -8.702539655, + "lines": [ + "4A", + "H3" + ] + }, + { + "stopId": 20053, + "name": { + "original": "Rúa de Ángel de Lema (cruce Paraixal)" + }, + "latitude": 42.248962858, + "longitude": -8.688272303, + "lines": [ + "C3i", + "10" + ] + }, + { + "stopId": 20054, + "name": { + "original": "Rúa de Ángel de Lema 33" + }, + "latitude": 42.248897377, + "longitude": -8.689150714, + "lines": [ + "C3d", + "10" + ] + }, + { + "stopId": 20057, + "name": { + "original": "Estación Ferrocarril Guixar" + }, + "latitude": 42.238843911, + "longitude": -8.713008504, + "lines": [ + "A", + "5B", + "16", + "24" + ] + }, + { + "stopId": 20058, + "name": { + "original": "Rúa do Canceleiro 6" + }, + "latitude": 42.238435471, + "longitude": -8.714413687, + "lines": [ + "5B", + "16" + ] + }, + { + "stopId": 20059, + "name": { + "original": "Rúa de Manuel Álvarez (fronte 10)" + }, + "latitude": 42.222745522, + "longitude": -8.677932515, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 20060, + "name": { + "original": "Rúa de Manuel Álvarez 10" + }, + "latitude": 42.22282586, + "longitude": -8.678077606, + "lines": [ + "25", + "31", + "H3" + ] + }, + { + "stopId": 20061, + "name": { + "original": "Rúa de Martín Echegaray (Colexio)" + }, + "latitude": 42.217568173, + "longitude": -8.744018511, + "lines": [ + "23", + "N4" + ] + }, + { + "stopId": 20062, + "name": { + "original": "Avda. de Beiramar 1" + }, + "latitude": 42.236143706, + "longitude": -8.73180718, + "lines": [ + "10", + "15B" + ] + }, + { + "stopId": 20071, + "name": { + "original": "Rúa de Xestoso (fronte 105)" + }, + "latitude": 42.205511653, + "longitude": -8.672824803, + "lines": [ + "15B" + ] + }, + { + "stopId": 20072, + "name": { + "original": "Camiño do Pouso" + }, + "latitude": 42.196643694, + "longitude": -8.671663218, + "lines": [ + "15B" + ] + }, + { + "stopId": 20075, + "name": { + "original": "Avda. de Castelao 65" + }, + "latitude": 42.218011215, + "longitude": -8.745369728, + "lines": [ + "C3i", + "4A", + "4C", + "10", + "11", + "12A", + "15A", + "N1", + "N4", + "U1" + ] + }, + { + "stopId": 20076, + "name": { + "original": "Avda. de Castelao 25" + }, + "latitude": 42.21901679, + "longitude": -8.739919147, + "lines": [ + "C3i", + "4A", + "4C", + "10", + "11", + "12A", + "15A", + "N1", + "U1" + ] + }, + { + "stopId": 20077, + "name": { + "original": "Avda. de Castelao 40" + }, + "latitude": 42.219259727, + "longitude": -8.739809435, + "lines": [ + "C3d", + "4A", + "4C", + "5B", + "10", + "12A", + "13", + "15A", + "PSA 1", + "PSA 4" + ] + }, + { + "stopId": 20078, + "name": { + "original": "Avda. das Camelias 3" + }, + "latitude": 42.233341329, + "longitude": -8.728967219, + "lines": [ + "4A", + "4C", + "11", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 20079, + "name": { + "original": "Avda. das Camelias 8" + }, + "latitude": 42.23341294, + "longitude": -8.729045156, + "lines": [ + "4A", + "4C", + "7", + "12B", + "17", + "27", + "PSA 4" + ] + }, + { + "stopId": 20080, + "name": { + "original": "Avda. de Santa Mariña 68" + }, + "latitude": 42.221674556, + "longitude": -8.660937347, + "lines": [ + "11" + ] + }, + { + "stopId": 20081, + "name": { + "original": "Subida aos Padróns (cruce Camiño da Chan da Rabicha)" + }, + "latitude": 42.151852858, + "longitude": -8.684956786, + "lines": [ + "7" + ] + }, + { + "stopId": 20082, + "name": { + "original": "Avda. de Santa Mariña (fronte 66)" + }, + "latitude": 42.221758032, + "longitude": -8.661135597, + "lines": [ + "11" + ] + }, + { + "stopId": 20083, + "name": { + "original": "Rúa Castañal 6" + }, + "latitude": 42.188074669, + "longitude": -8.701928367, + "lines": [ + "27" + ] + }, + { + "stopId": 20084, + "name": { + "original": "Rúa Castañal 26" + }, + "latitude": 42.18711079, + "longitude": -8.699519743, + "lines": [ + "27" + ] + }, + { + "stopId": 20085, + "name": { + "original": "Rúa Castañal (cruce Camiño das Presas)" + }, + "latitude": 42.185852445, + "longitude": -8.696410892, + "lines": [ + "27" + ] + }, + { + "stopId": 20086, + "name": { + "original": "Estrada dos Seixiños 67" + }, + "latitude": 42.190645281, + "longitude": -8.696150583, + "lines": [ + "27" + ] + }, + { + "stopId": 20087, + "name": { + "original": "Estrada dos Seixiños 23" + }, + "latitude": 42.194639373, + "longitude": -8.696795357, + "lines": [ + "27" + ] + }, + { + "stopId": 20089, + "name": { + "original": "Porriño - Padre Seixas ©" + }, + "latitude": 42.213044566, + "longitude": -8.751396835, + "lines": [ + "16" + ] + }, + { + "stopId": 20091, + "name": { + "original": "Camiño da Miragaia 11-13" + }, + "latitude": 42.238164803, + "longitude": -8.711212761, + "lines": [ + "A", + "5B", + "16", + "24" + ] + }, + { + "stopId": 20094, + "name": { + "original": "Rúa das Mantelas (fronte 63)" + }, + "latitude": 42.22518736, + "longitude": -8.717399288, + "lines": [ + "18A" + ] + }, + { + "stopId": 20095, + "name": { + "original": "Estrada Vella de Madrid 107A" + }, + "latitude": 42.219212419, + "longitude": -8.685836356, + "lines": [ + "12A", + "12B", + "13", + "H3" + ] + }, + { + "stopId": 20096, + "name": { + "original": "Estrada Vella de Madrid (fronte 107A)" + }, + "latitude": 42.219128991, + "longitude": -8.685753208, + "lines": [ + "12A", + "12B", + "13", + "U2", + "H3" + ] + }, + { + "stopId": 20099, + "name": { + "original": "Rúa de Camilo Veiga 48" + }, + "latitude": 42.222390674, + "longitude": -8.752507356, + "lines": [ + "C3i", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 20100, + "name": { + "original": "Rúa de Camilo Veiga 6" + }, + "latitude": 42.223195763, + "longitude": -8.749650702, + "lines": [ + "C3i", + "15B", + "15C", + "N1" + ] + }, + { + "stopId": 20102, + "name": { + "original": "H. A. Cunqueiro (Porta Principal)" + }, + "latitude": 42.191034002, + "longitude": -8.714303116, + "lines": [ + "6", + "12B", + "18H", + "27", + "H1", + "H2", + "H3", + "H" + ] + }, + { + "stopId": 20103, + "name": { + "original": "Avda. do Fragoso 21" + }, + "latitude": 42.218946899, + "longitude": -8.733670293, + "lines": [ + "7", + "12B", + "17", + "N4", + "H1" + ] + }, + { + "stopId": 20104, + "name": { + "original": "Rúa de Emilia Pardo Bazán 134" + }, + "latitude": 42.220938435, + "longitude": -8.709621883, + "lines": [ + "14" + ] + }, + { + "stopId": 20105, + "name": { + "original": "Rúa de Emilia Pardo Bazán 121" + }, + "latitude": 42.221232035, + "longitude": -8.709808647, + "lines": [ + "14" + ] + }, + { + "stopId": 20107, + "name": { + "original": "Estrada do Porto (Lavadero)" + }, + "latitude": 42.188244696, + "longitude": -8.703164368, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 20110, + "name": { + "original": "Rúa de Manuel Castro 10" + }, + "latitude": 42.213797254, + "longitude": -8.741472696, + "lines": [ + "23", + "N4" + ] + }, + { + "stopId": 20111, + "name": { + "original": "H. A. Cunqueiro (Hospital de Día)" + }, + "latitude": 42.187585838, + "longitude": -8.716278919, + "lines": [ + "A", + "6", + "12B", + "18H", + "27", + "H1", + "H2", + "H3", + "H" + ] + }, + { + "stopId": 20112, + "name": { + "original": "H. A. Cunqueiro (Urxencias)" + }, + "latitude": 42.188578188, + "longitude": -8.713087125, + "lines": [ + "6", + "12B", + "18H", + "H1", + "H3", + "H" + ] + }, + { + "stopId": 20113, + "name": { + "original": "Praza de América 3 (Dirección Hospital)" + }, + "latitude": 42.220876566, + "longitude": -8.733367644, + "lines": [ + "12B", + "H1", + "H2", + "PTL" + ] + }, + { + "stopId": 20114, + "name": { + "original": "Estrada do Porto (fronte Lavadero)" + }, + "latitude": 42.18846205, + "longitude": -8.703352711, + "lines": [ + "6" + ] + }, + { + "stopId": 20115, + "name": { + "original": "Estrada do Porto (fronte cruce Rúa das Sueiras)" + }, + "latitude": 42.190100441, + "longitude": -8.705453204, + "lines": [ + "6" + ] + }, + { + "stopId": 20116, + "name": { + "original": "Estrada da Coutada-Beade 2" + }, + "latitude": 42.19202547, + "longitude": -8.705712064, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 20117, + "name": { + "original": "Estrada do Porto (cruce Camiño do Frascuelo)" + }, + "latitude": 42.191616209, + "longitude": -8.706277831, + "lines": [ + "6", + "27" + ] + }, + { + "stopId": 20118, + "name": { + "original": "Rúa Conde de Gondomar" + }, + "latitude": 42.228358488, + "longitude": -8.719490904, + "lines": [ + "H2" + ] + }, + { + "stopId": 20119, + "name": { + "original": "H. A. Cunqueiro (chegada)" + }, + "latitude": 42.190930878, + "longitude": -8.71409354, + "lines": [ + "6", + "12B", + "H3" + ] + }, + { + "stopId": 20124, + "name": { + "original": "Estrada Clara Campoamor 6" + }, + "latitude": 42.208989468, + "longitude": -8.729330619, + "lines": [ + "A", + "12B", + "U1", + "H1", + "H2", + "H", + "PTL" + ] + }, + { + "stopId": 20125, + "name": { + "original": "Estrada Clara Campoamor (fronte 6)" + }, + "latitude": 42.209126911, + "longitude": -8.729344197, + "lines": [ + "12B", + "H1", + "H2" + ] + }, + { + "stopId": 20126, + "name": { + "original": "Estrada Clara Campoamor (Rotonda HAC)" + }, + "latitude": 42.190252452, + "longitude": -8.717998617, + "lines": [ + "12B", + "18H", + "H1", + "H2" + ] + }, + { + "stopId": 20127, + "name": { + "original": "Estrada Clara Campoamor (fronte Rotonda HAC)" + }, + "latitude": 42.19007538, + "longitude": -8.718125045, + "lines": [ + "A", + "12B", + "18H", + "27", + "U1", + "H1", + "H2", + "H", + "PTL" + ] + }, + { + "stopId": 20130, + "name": { + "original": "Parque Forestal de Zamáns (Proba Andaina)" + }, + "latitude": 42.152788309, + "longitude": -8.681902684, + "lines": [ + "7" + ] + }, + { + "stopId": 20132, + "name": { + "original": "Avda. de Galicia 341" + }, + "latitude": 42.260473187, + "longitude": -8.67881466, + "lines": [ + "C3i" + ] + }, + { + "stopId": 20136, + "name": { + "original": "Avda. de E. Martínez Garrido 98" + }, + "latitude": 42.225764699, + "longitude": -8.704499864, + "lines": [ + "4C", + "23", + "31", + "N4", + "PSA 4" + ] + }, + { + "stopId": 20137, + "name": { + "original": "Camiño da Devesa (Asociación Veciños)" + }, + "latitude": 42.246563041, + "longitude": -8.669395817, + "lines": [ + "9B" + ] + }, + { + "stopId": 20139, + "name": { + "original": "Estrada Matamá Pazo (fronte 162)" + }, + "latitude": 42.199144892, + "longitude": -8.758506717, + "lines": [ + "29" + ] + }, + { + "stopId": 20141, + "name": { + "original": "Avda. da Ponte (fronte Vigo Memorial)" + }, + "latitude": 42.21057897, + "longitude": -8.671171189, + "lines": [ + "12B", + "15B", + "15C" + ] + }, + { + "stopId": 20142, + "name": { + "original": "Camiño do Outeiro 3" + }, + "latitude": 42.200738188, + "longitude": -8.714882876, + "lines": [ + "18B" + ] + }, + { + "stopId": 20143, + "name": { + "original": "Rúa das Teixugueiras (fronte 1)" + }, + "latitude": 42.215448094, + "longitude": -8.756474306, + "lines": [ + "15A" + ] + }, + { + "stopId": 20154, + "name": { + "original": "Rúa de Ramiro Pascual (fronte 127)" + }, + "latitude": 42.192089689, + "longitude": -8.709245389, + "lines": [ + "27" + ] + }, + { + "stopId": 20155, + "name": { + "original": "Rúa de Ramiro Pascual 131" + }, + "latitude": 42.19217626, + "longitude": -8.708899009, + "lines": [ + "27" + ] + }, + { + "stopId": 20156, + "name": { + "original": "Económicas e Empresariais (CUVI 2)" + }, + "latitude": 42.169602007, + "longitude": -8.680122554, + "lines": [ + "A" + ] + }, + { + "stopId": 20157, + "name": { + "original": "Estrada do Porto 88" + }, + "latitude": 42.185615419, + "longitude": -8.702424678, + "lines": [ + "6" + ] + }, + { + "stopId": 20158, + "name": { + "original": "Estrada do Porto 81" + }, + "latitude": 42.185593055, + "longitude": -8.702377974, + "lines": [ + "6" + ] + }, + { + "stopId": 20159, + "name": { + "original": "Estrada de Valadares 571" + }, + "latitude": 42.160348044, + "longitude": -8.718706355, + "lines": [ + "7" + ] + }, + { + "stopId": 20160, + "name": { + "original": "Estrada de Valadares 522" + }, + "latitude": 42.160066796, + "longitude": -8.718938239, + "lines": [ + "7" + ] + }, + { + "stopId": 20166, + "name": { + "original": "Camiño da Brea 2" + }, + "latitude": 42.202134841, + "longitude": -8.70572793, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 20167, + "name": { + "original": "Camiño da Brea 3" + }, + "latitude": 42.202095058, + "longitude": -8.705814233, + "lines": [ + "18A", + "18B" + ] + }, + { + "stopId": 20168, + "name": { + "original": "Estrada do Freixo (despois 118)" + }, + "latitude": 42.173596087, + "longitude": -8.730918928, + "lines": [ + "7" + ] + }, + { + "stopId": 20169, + "name": { + "original": "Estrada do Freixo (despois 235)" + }, + "latitude": 42.173616782, + "longitude": -8.730810863, + "lines": [ + "7" + ] + }, + { + "stopId": 20170, + "name": { + "original": "Rúa de Álvaro Cunqueiro 4" + }, + "latitude": 42.224544805, + "longitude": -8.730413561, + "lines": [ + "5A", + "5B", + "12A" + ] + }, + { + "stopId": 20171, + "name": { + "original": "Estrada Clara Campoamor (cruce Camiño da Pousa)" + }, + "latitude": 42.204380762, + "longitude": -8.726688445, + "lines": [ + "12B", + "H1", + "H2" + ] + }, + { + "stopId": 20172, + "name": { + "original": "Estrada Clara Campoamor (cruce Camiño da Nogueira)" + }, + "latitude": 42.203736336, + "longitude": -8.726617869, + "lines": [ + "A", + "12B", + "U1", + "H1", + "H2", + "H", + "PTL" + ] + }, + { + "stopId": 20173, + "name": { + "original": "Avda. de Castrelos 502" + }, + "latitude": 42.192504056, + "longitude": -8.721215121, + "lines": [ + "7", + "U1" + ] + }, + { + "stopId": 20174, + "name": { + "original": "Baixada ao Pontillón S/N" + }, + "latitude": 42.21519917, + "longitude": -8.726793773, + "lines": [ + "A" + ] + }, + { + "stopId": 20177, + "name": { + "original": "Rúa de Pizarro 16" + }, + "latitude": 42.230767817, + "longitude": -8.715105964, + "lines": [ + "C3i", + "6", + "11", + "15A", + "23", + "25", + "28" + ] + }, + { + "stopId": 20178, + "name": { + "original": "Estrada de Camposancos (cruce Camiño da Estea)" + }, + "latitude": 42.172412443, + "longitude": -8.799591567, + "lines": [ + "12A" + ] + }, + { + "stopId": 20180, + "name": { + "original": "Rúa do Reiseñor 10" + }, + "latitude": 42.229527407, + "longitude": -8.70843784, + "lines": [ + "H2" + ] + }, + { + "stopId": 20186, + "name": { + "original": "Rúa da Rabadeira 71" + }, + "latitude": 42.23755404, + "longitude": -8.651558138, + "lines": [ + "9B" + ] + }, + { + "stopId": 20187, + "name": { + "original": "Rúa da Rabadeira 46" + }, + "latitude": 42.237422128, + "longitude": -8.65153195, + "lines": [ + "9B", + "27" + ] + }, + { + "stopId": 20188, + "name": { + "original": "Rúa da Saa (fronte 43)" + }, + "latitude": 42.201670402, + "longitude": -8.708928464, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 20189, + "name": { + "original": "Rúa da Saa 10" + }, + "latitude": 42.201625853, + "longitude": -8.712945043, + "lines": [ + "18B", + "H3" + ] + }, + { + "stopId": 20190, + "name": { + "original": "Avda. das Camelias (fronte Praza do Rei)" + }, + "latitude": 42.234906013, + "longitude": -8.72662052, + "lines": [ + "4A", + "4C", + "11", + "12B", + "17", + "27", + "N1" + ] + }, + { + "stopId": 20191, + "name": { + "original": "Rúa das Figueiras 200" + }, + "latitude": 42.229676205, + "longitude": -8.657383392, + "lines": [ + "15A", + "25" + ] + }, + { + "stopId": 20192, + "name": { + "original": "Rúa de Colón 26" + }, + "latitude": 42.237168511, + "longitude": -8.720373767, + "lines": [ + "4A", + "4C", + "5B", + "7", + "12B", + "16", + "17", + "PSA 4" + ] + }, + { + "stopId": 20193, + "name": { + "original": "Rúa de Policarpo Sanz 25" + }, + "latitude": 42.23767601188501, + "longitude": -8.721582630122455, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "10", + "11", + "15B", + "15C", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 20194, + "name": { + "original": "Rúa de Cánovas del Castillo 28" + }, + "latitude": 42.240364985, + "longitude": -8.724530974, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "10", + "15B", + "15C", + "28", + "N4" + ] + }, + { + "stopId": 20195, + "name": { + "original": "Praza de Compostela (fronte 35)" + }, + "latitude": 42.2393606, + "longitude": -8.724131464, + "lines": [ + "C3i", + "A", + "5A", + "5B", + "6", + "9B", + "10", + "11", + "15B", + "15C", + "28", + "N1", + "N4", + "H1" + ] + }, + { + "stopId": 20196, + "name": { + "original": "Estrada de Camposancos 498" + }, + "latitude": 42.175325155, + "longitude": -8.799594139, + "lines": [ + "12A" + ] + }, + { + "stopId": 20197, + "name": { + "original": "Rúa de Pi i Margall 3-5" + }, + "latitude": 42.23558703, + "longitude": -8.728830897, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 20198, + "name": { + "original": "Rúa de Policarpo Sanz 26" + }, + "latitude": 42.237533428, + "longitude": -8.722195046, + "lines": [ + "C1", + "C3d", + "A", + "5A", + "9B", + "10", + "15B", + "15C", + "24", + "28", + "N4" + ] + }, + { + "stopId": 20199, + "name": { + "original": "Rúa de Puerto Rico 12" + }, + "latitude": 42.228802205, + "longitude": -8.718136653, + "lines": [ + "H2" + ] + }, + { + "stopId": 20200, + "name": { + "original": "Rúa de Pi i Margall (fronte 5)" + }, + "latitude": 42.235482452, + "longitude": -8.728981431, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 20201, + "name": { + "original": "Paseo de Granada S/N" + }, + "latitude": 42.235701104, + "longitude": -8.726054911, + "lines": [ + "5B", + "12A" + ] + }, + { + "stopId": 20203, + "name": { + "original": "Avda. da Gran Vía 47" + }, + "latitude": 42.230881062, + "longitude": -8.718397577, + "lines": [ + "7", + "12B", + "14", + "16", + "18A", + "18B", + "18H" + ] + }, + { + "stopId": 20209, + "name": { + "original": "Avda. do Alcalde Portanet 23" + }, + "latitude": 42.211481651, + "longitude": -8.734440746, + "lines": [ + "H1" + ] + }, + { + "stopId": 20210, + "name": { + "original": "Estrada de Camposancos 108" + }, + "latitude": 42.19824056, + "longitude": -8.763182189, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 20211, + "name": { + "original": "Estrada de Camposancos 109" + }, + "latitude": 42.198422825, + "longitude": -8.762538026, + "lines": [ + "11", + "29" + ] + }, + { + "stopId": 20212, + "name": { + "original": "Rúa do Canabido 18" + }, + "latitude": 42.188388732, + "longitude": -8.805956864, + "lines": [ + "10" + ] + }, + { + "stopId": 20215, + "name": { + "original": "Rúa da Coruña 21" + }, + "latitude": 42.223880296, + "longitude": -8.735520196, + "lines": [ + "A", + "5A", + "5B", + "10", + "11", + "13", + "N4", + "U1", + "H1", + "H" + ] + }, + { + "stopId": 20216, + "name": { + "original": "Avda. de Cesáreo Vázquez (fronte 43)" + }, + "latitude": 42.179747589, + "longitude": -8.802157388, + "lines": [ + "11" + ] + }, + { + "stopId": 20219, + "name": { + "original": "Avda. do Aeroporto (fronte 90)" + }, + "latitude": 42.234830699, + "longitude": -8.695443515, + "lines": [ + "A", + "9B", + "27", + "28" + ] + } +] diff --git a/src/frontend/public/sw.js b/src/frontend/public/sw.js new file mode 100644 index 0000000..70ca169 --- /dev/null +++ b/src/frontend/public/sw.js @@ -0,0 +1,51 @@ +const API_CACHE_NAME = 'api-cache-v1' +const API_URL_PATTERN = /\/api\/(GetStopList)/; +const API_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours + +self.addEventListener('install', (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('fetch', async (event) => { + const url = new URL(event.request.url); + + if (event.request.method !== "GET" || !API_URL_PATTERN.test(url.pathname)) { + return; + } + + event.respondWith(apiCacheFirst(event.request)); +}); + +async function apiCacheFirst(request) { + const cache = await caches.open(API_CACHE_NAME); + const cachedResponse = await cache.match(request); + + if (cachedResponse) { + const age = Date.now() - new Date(cachedResponse.headers.get('date')).getTime(); + if (age < API_MAX_AGE) { + console.debug(`SW: Cache HIT for ${request.url}`); + return cachedResponse; + } + + // Cache is too old, fetch a fresh copy + cache.delete(request); + } + + try { + const netResponse = await fetch(request); + + const responseToCache = netResponse.clone(); + + cache.put(request, responseToCache); + + console.debug(`SW: Cache MISS for ${request.url}`); + + return netResponse; + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/src/frontend/src/AppContext.tsx b/src/frontend/src/AppContext.tsx new file mode 100644 index 0000000..ecba9e2 --- /dev/null +++ b/src/frontend/src/AppContext.tsx @@ -0,0 +1,234 @@ +/* eslint-disable react-refresh/only-export-components */ +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; +import { type LatLngTuple } from 'leaflet'; + +type Theme = 'light' | 'dark'; +type TableStyle = 'regular'|'grouped'; +type MapPositionMode = 'gps' | 'last'; + +interface MapState { + center: LatLngTuple; + zoom: number; + userLocation: LatLngTuple | null; + hasLocationPermission: boolean; +} + +interface AppContextProps { + theme: Theme; + setTheme: React.Dispatch>; + toggleTheme: () => void; + + tableStyle: TableStyle; + setTableStyle: React.Dispatch>; + toggleTableStyle: () => void; + + mapState: MapState; + setMapCenter: (center: LatLngTuple) => void; + setMapZoom: (zoom: number) => void; + setUserLocation: (location: LatLngTuple | null) => void; + setLocationPermission: (hasPermission: boolean) => void; + updateMapState: (center: LatLngTuple, zoom: number) => void; + + mapPositionMode: MapPositionMode; + setMapPositionMode: (mode: MapPositionMode) => void; +} + +// Coordenadas por defecto centradas en Vigo +const DEFAULT_CENTER: LatLngTuple = [42.229188855975046, -8.72246955783102]; +const DEFAULT_ZOOM = 14; + +const AppContext = createContext(undefined); + +export const AppProvider = ({ children }: { children: ReactNode }) => { + //#region Theme + const [theme, setTheme] = useState(() => { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + return savedTheme as Theme; + } + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'light'; + }); + + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); + }; + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); + }, [theme]); + //#endregion + + //#region Table Style + const [tableStyle, setTableStyle] = useState(() => { + const savedTableStyle = localStorage.getItem('tableStyle'); + if (savedTableStyle) { + return savedTableStyle as TableStyle; + } + return 'regular'; + }); + + const toggleTableStyle = () => { + setTableStyle((prevTableStyle) => (prevTableStyle === 'regular' ? 'grouped' : 'regular')); + } + + useEffect(() => { + localStorage.setItem('tableStyle', tableStyle); + }, [tableStyle]); + //#endregion + + //#region Map Position Mode + const [mapPositionMode, setMapPositionMode] = useState(() => { + const saved = localStorage.getItem('mapPositionMode'); + return saved === 'last' ? 'last' : 'gps'; + }); + + useEffect(() => { + localStorage.setItem('mapPositionMode', mapPositionMode); + }, [mapPositionMode]); + //#endregion + + //#region Map State + const [mapState, setMapState] = useState(() => { + const savedMapState = localStorage.getItem('mapState'); + if (savedMapState) { + try { + const parsed = JSON.parse(savedMapState); + return { + center: parsed.center || DEFAULT_CENTER, + zoom: parsed.zoom || DEFAULT_ZOOM, + userLocation: parsed.userLocation || null, + hasLocationPermission: parsed.hasLocationPermission || false + }; + } catch (e) { + console.error('Error parsing saved map state', e); + } + } + return { + center: DEFAULT_CENTER, + zoom: DEFAULT_ZOOM, + userLocation: null, + hasLocationPermission: false + }; + }); + + // Helper: check if coordinates are within Vigo bounds + function isWithinVigo([lat, lng]: LatLngTuple): boolean { + // Rough bounding box for Vigo + return lat >= 42.18 && lat <= 42.30 && lng >= -8.78 && lng <= -8.65; + } + + // On app load, if mapPositionMode is 'gps', try to get GPS and set map center + useEffect(() => { + if (mapPositionMode === 'gps') { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + const coords: LatLngTuple = [latitude, longitude]; + if (isWithinVigo(coords)) { + setMapState(prev => { + const newState = { ...prev, center: coords, zoom: 16, userLocation: coords }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + } + }, + () => { + // Ignore error, fallback to last + } + ); + } + } + // If 'last', do nothing (already loaded from localStorage) + }, [mapPositionMode]); + + const setMapCenter = (center: LatLngTuple) => { + setMapState(prev => { + const newState = { ...prev, center }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setMapZoom = (zoom: number) => { + setMapState(prev => { + const newState = { ...prev, zoom }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setUserLocation = (userLocation: LatLngTuple | null) => { + setMapState(prev => { + const newState = { ...prev, userLocation }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const setLocationPermission = (hasLocationPermission: boolean) => { + setMapState(prev => { + const newState = { ...prev, hasLocationPermission }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + + const updateMapState = (center: LatLngTuple, zoom: number) => { + setMapState(prev => { + const newState = { ...prev, center, zoom }; + localStorage.setItem('mapState', JSON.stringify(newState)); + return newState; + }); + }; + //#endregion + + // Tratar de obtener la ubicación del usuario cuando se carga la aplicación si ya se había concedido permiso antes + useEffect(() => { + if (mapState.hasLocationPermission && !mapState.userLocation) { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + setUserLocation([latitude, longitude]); + }, + (error) => { + console.error('Error getting location:', error); + setLocationPermission(false); + } + ); + } + } + }, [mapState.hasLocationPermission, mapState.userLocation]); + + return ( + + {children} + + ); +}; + +export const useApp = () => { + const context = useContext(AppContext); + if (!context) { + throw new Error('useApp must be used within a AppProvider'); + } + return context; +}; diff --git a/src/frontend/src/ErrorBoundary.tsx b/src/frontend/src/ErrorBoundary.tsx new file mode 100644 index 0000000..5c877b7 --- /dev/null +++ b/src/frontend/src/ErrorBoundary.tsx @@ -0,0 +1,46 @@ +import React, { Component, type ReactNode } from 'react'; + +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null + }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { + hasError: true, + error + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + return <> +

    Something went wrong.

    +
    +          {this.state.error?.stack}
    +        
    + ; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/frontend/src/Layout.css b/src/frontend/src/Layout.css new file mode 100644 index 0000000..601794b --- /dev/null +++ b/src/frontend/src/Layout.css @@ -0,0 +1,60 @@ +#root { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; + overflow: hidden; +} + +.main-content { + flex: 1; + overflow: auto; + padding-bottom: 60px; /* Extra padding to ensure content isn't hidden behind navbar */ +} + +.nav-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 5; + + background-color: var(--background-color); + display: flex; + justify-content: space-around; + align-items: center; + height: 60px; + border-top: 1px solid var(--border-color); + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 8px; + color: #616161; + text-decoration: none; + width: 33.3%; + font-size: 14px; +} + +.nav-item.active { + color: var(--button-background-color); +} + +.theme-toggle { + background: none; + border: none; + cursor: pointer; + color: inherit; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; +} + +.theme-toggle:hover { + color: var(--button-hover-background-color); +} diff --git a/src/frontend/src/Layout.tsx b/src/frontend/src/Layout.tsx new file mode 100644 index 0000000..f933ddc --- /dev/null +++ b/src/frontend/src/Layout.tsx @@ -0,0 +1,55 @@ +import { type ReactNode } from 'react'; +import { Link, useLocation } from 'react-router'; +import { MapPin, Map, Settings } from 'lucide-react'; +import './Layout.css'; + +interface LayoutProps { + children: ReactNode; +} + +export function Layout({ children }: LayoutProps) { + const location = useLocation(); + + const navItems = [ + { + name: 'Paradas', + icon: MapPin, + path: '/stops' + }, + { + name: 'Mapa', + icon: Map, + path: '/map' + }, + { + name: 'Ajustes', + icon: Settings, + path: '/settings' + } + ]; + + return ( + <> +
    + {children} +
    + + + ); +} diff --git a/src/frontend/src/components/GroupedTable.tsx b/src/frontend/src/components/GroupedTable.tsx new file mode 100644 index 0000000..b7f990d --- /dev/null +++ b/src/frontend/src/components/GroupedTable.tsx @@ -0,0 +1,74 @@ +import { type StopDetails } from "../pages/Estimates"; +import LineIcon from "./LineIcon"; + +interface GroupedTable { + data: StopDetails; + dataDate: Date | null; +} + +export const GroupedTable: React.FC = ({ data, dataDate }) => { + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} m`; + } + } + + const groupedEstimates = data.estimates.reduce((acc, estimate) => { + if (!acc[estimate.line]) { + acc[estimate.line] = []; + } + acc[estimate.line].push(estimate); + return acc; + }, {} as Record); + + const sortedLines = Object.keys(groupedEstimates).sort((a, b) => { + const firstArrivalA = groupedEstimates[a][0].minutes; + const firstArrivalB = groupedEstimates[b][0].minutes; + return firstArrivalA - firstArrivalB; + }); + + return + + + + + + + + + + + + + {sortedLines.map((line) => ( + groupedEstimates[line].map((estimate, idx) => ( + + {idx === 0 && ( + + )} + + + + + )) + ))} + + + {data?.estimates.length === 0 && ( + + + + + + )} +
    Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}
    LíneaRutaLlegadaDistancia
    + + {estimate.route}{`${estimate.minutes} min`} + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } +
    No hay estimaciones disponibles
    +} diff --git a/src/frontend/src/components/LineIcon.css b/src/frontend/src/components/LineIcon.css new file mode 100644 index 0000000..e7e8949 --- /dev/null +++ b/src/frontend/src/components/LineIcon.css @@ -0,0 +1,239 @@ +:root { + --line-c1: rgb(237, 71, 19); + --line-c3d: rgb(255, 204, 0); + --line-c3i: rgb(255, 204, 0); + --line-l4a: rgb(0, 153, 0); + --line-l4c: rgb(0, 153, 0); + --line-l5a: rgb(0, 176, 240); + --line-l5b: rgb(0, 176, 240); + --line-l6: rgb(204, 51, 153); + --line-l7: rgb(150, 220, 153); + --line-l9b: rgb(244, 202, 140); + --line-l10: rgb(153, 51, 0); + --line-l11: rgb(226, 0, 38); + --line-l12a: rgb(106, 150, 190); + --line-l12b: rgb(106, 150, 190); + --line-l13: rgb(0, 176, 240); + --line-l14: rgb(129, 142, 126); + --line-l15a: rgb(216, 168, 206); + --line-l15b: rgb(216, 168, 206); + --line-l15c: rgb(216, 168, 168); + --line-l16: rgb(129, 142, 126); + --line-l17: rgb(214, 245, 31); + --line-l18a: rgb(212, 80, 168); + --line-l18b: rgb(0, 0, 0); + --line-l18h: rgb(0, 0, 0); + --line-l23: rgb(0, 70, 210); + --line-l24: rgb(191, 191, 191); + --line-l25: rgb(172, 100, 4); + --line-l27: rgb(112, 74, 42); + --line-l28: rgb(176, 189, 254); + --line-l29: rgb(248, 184, 90); + --line-l31: rgb(255, 255, 0); + --line-a: rgb(119, 41, 143); + --line-h: rgb(0, 96, 168); + --line-h1: rgb(0, 96, 168); + --line-h2: rgb(0, 96, 168); + --line-h3: rgb(0, 96, 168); + --line-lzd: rgb(61, 78, 167); + --line-n1: rgb(191, 191, 191); + --line-n4: rgb(102, 51, 102); + --line-psa1: rgb(0, 153, 0); + --line-psa4: rgb(0, 153, 0); + --line-ptl: rgb(150, 220, 153); + --line-turistico: rgb(102, 51, 102); + --line-u1: rgb(172, 100, 4); + --line-u2: rgb(172, 100, 4); +} + +.line-icon { + display: inline-block; + padding: 0.25rem 0.5rem; + margin-right: 0.5rem; + border-bottom: 3px solid; + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + color: inherit; + /* Prevent color change on hover */ +} + +.line-c1 { + border-color: var(--line-c1); +} + +.line-c3d { + border-color: var(--line-c3d); +} + +.line-c3i { + border-color: var(--line-c3i); +} + +.line-l4a { + border-color: var(--line-l4a); +} + +.line-l4c { + border-color: var(--line-l4c); +} + +.line-l5a { + border-color: var(--line-l5a); +} + +.line-l5b { + border-color: var(--line-l5b); +} + +.line-l6 { + border-color: var(--line-l6); +} + +.line-l7 { + border-color: var(--line-l7); +} + +.line-l9b { + border-color: var(--line-l9b); +} + +.line-l10 { + border-color: var(--line-l10); +} + +.line-l11 { + border-color: var(--line-l11); +} + +.line-l12a { + border-color: var(--line-l12a); +} + +.line-l12b { + border-color: var(--line-l12b); +} + +.line-l13 { + border-color: var(--line-l13); +} + +.line-l14 { + border-color: var(--line-l14); +} + +.line-l15a { + border-color: var(--line-l15a); +} + +.line-l15b { + border-color: var(--line-l15b); +} + +.line-l15c { + border-color: var(--line-l15c); +} + +.line-l16 { + border-color: var(--line-l16); +} + +.line-l17 { + border-color: var(--line-l17); +} + +.line-l18a { + border-color: var(--line-l18a); +} + +.line-l18b { + border-color: var(--line-l18b); +} + +.line-l18h { + border-color: var(--line-l18h); +} + +.line-l23 { + border-color: var(--line-l23); +} + +.line-l24 { + border-color: var(--line-l24); +} + +.line-l25 { + border-color: var(--line-l25); +} + +.line-l27 { + border-color: var(--line-l27); +} + +.line-l28 { + border-color: var(--line-l28); +} + +.line-l29 { + border-color: var(--line-l29); +} + +.line-l31 { + border-color: var(--line-l31); +} + +.line-a { + border-color: var(--line-a); +} + +.line-h { + border-color: var(--line-h); +} + +.line-h1 { + border-color: var(--line-h1); +} + +.line-h2 { + border-color: var(--line-h2); +} + +.line-h3 { + border-color: var(--line-h3); +} + +.line-lzd { + border-color: var(--line-lzd); +} + +.line-n1 { + border-color: var(--line-n1); +} + +.line-n4 { + border-color: var(--line-n4); +} + +.line-psa1 { + border-color: var(--line-psa1); +} + +.line-psa4 { + border-color: var(--line-psa4); +} + +.line-ptl { + border-color: var(--line-ptl); +} + +.line-turistico { + border-color: var(--line-turistico); +} + +.line-u1 { + border-color: var(--line-u1); +} + +.line-u2 { + border-color: var(--line-u2); +} \ No newline at end of file diff --git a/src/frontend/src/components/LineIcon.tsx b/src/frontend/src/components/LineIcon.tsx new file mode 100644 index 0000000..50fd1ec --- /dev/null +++ b/src/frontend/src/components/LineIcon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import './LineIcon.css'; + +interface LineIconProps { + line: string; +} + +const LineIcon: React.FC = ({ line }) => { + const formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; + return ( + + {formattedLine} + + ); +}; + +export default LineIcon; \ No newline at end of file diff --git a/src/frontend/src/components/RegularTable.tsx b/src/frontend/src/components/RegularTable.tsx new file mode 100644 index 0000000..211a47c --- /dev/null +++ b/src/frontend/src/components/RegularTable.tsx @@ -0,0 +1,70 @@ +import { type StopDetails } from "../pages/Estimates"; +import LineIcon from "./LineIcon"; + +interface RegularTableProps { + data: StopDetails; + dataDate: Date | null; +} + +export const RegularTable: React.FC = ({ data, dataDate }) => { + + const absoluteArrivalTime = (minutes: number) => { + const now = new Date() + const arrival = new Date(now.getTime() + minutes * 60000) + return Intl.DateTimeFormat(navigator.language, { + hour: '2-digit', + minute: '2-digit' + }).format(arrival) + } + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} m`; + } + } + + return + + + + + + + + + + + + + {data.estimates + .sort((a, b) => a.minutes - b.minutes) + .map((estimate, idx) => ( + + + + + + + ))} + + + {data?.estimates.length === 0 && ( + + + + + + )} +
    Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}
    LíneaRutaLlegadaDistancia
    {estimate.route} + {estimate.minutes > 15 + ? absoluteArrivalTime(estimate.minutes) + : `${estimate.minutes} min`} + + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible" + } +
    No hay estimaciones disponibles
    +} diff --git a/src/frontend/src/components/StopItem.css b/src/frontend/src/components/StopItem.css new file mode 100644 index 0000000..9feb2d1 --- /dev/null +++ b/src/frontend/src/components/StopItem.css @@ -0,0 +1,54 @@ +/* Stop Item Styling */ + +.stop-notes { + font-size: 0.85rem; + font-style: italic; + color: #666; + margin: 2px 0; +} + +.stop-amenities { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} + +.amenity-tag { + font-size: 0.75rem; + background-color: #e8f4f8; + color: #0078d4; + border-radius: 4px; + padding: 2px 6px; + display: inline-block; +} + +/* Different colors for different amenity types */ +.amenity-tag[data-amenity="shelter"] { + background-color: #e3f1df; + color: #107c41; +} + +.amenity-tag[data-amenity="bench"] { + background-color: #f0e8fc; + color: #5c2e91; +} + +.amenity-tag[data-amenity="real-time display"] { + background-color: #fff4ce; + color: #986f0b; +} + +/* When there are alternate names available, show an indicator */ +.has-alternate-names { + position: relative; +} + +.has-alternate-names::after { + content: "⋯"; + position: absolute; + right: -15px; + top: 0; + color: #0078d4; + font-weight: bold; +} \ No newline at end of file diff --git a/src/frontend/src/components/StopItem.tsx b/src/frontend/src/components/StopItem.tsx new file mode 100644 index 0000000..29370b7 --- /dev/null +++ b/src/frontend/src/components/StopItem.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Link } from 'react-router'; +import StopDataProvider, { type Stop } from '../data/StopDataProvider'; +import LineIcon from './LineIcon'; + +interface StopItemProps { + stop: Stop; +} + +const StopItem: React.FC = ({ stop }) => { + + return ( +
  • + + {stop.favourite && } ({stop.stopId}) {StopDataProvider.getDisplayName(stop)} +
    + {stop.lines?.map(line => )} +
    + + +
  • + ); +}; + +export default StopItem; diff --git a/src/frontend/src/controls/LocateControl.ts b/src/frontend/src/controls/LocateControl.ts new file mode 100644 index 0000000..26effa5 --- /dev/null +++ b/src/frontend/src/controls/LocateControl.ts @@ -0,0 +1,67 @@ +import { createControlComponent } from '@react-leaflet/core'; +import { LocateControl as LeafletLocateControl, type LocateOptions } from 'leaflet.locatecontrol'; +import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import { useApp } from '../AppContext'; + +interface EnhancedLocateControlProps { + options?: LocateOptions; +} + +// Componente que usa el contexto para manejar la localización +export const EnhancedLocateControl = (props: EnhancedLocateControlProps) => { + const map = useMap(); + const { mapState, setUserLocation, setLocationPermission } = useApp(); + + useEffect(() => { + // Configuración por defecto del control de localización + const defaultOptions: LocateOptions = { + position: 'topright', + strings: { + title: 'Mostrar mi ubicación', + }, + flyTo: true, + onLocationError: (err) => { + console.error('Error en la localización:', err); + setLocationPermission(false); + }, + returnToPrevBounds: true, + showPopup: false, + }; + + // Combinamos las opciones por defecto con las personalizadas + const options = { ...defaultOptions, ...props.options }; + + // Creamos la instancia del control + const locateControl = new LeafletLocateControl(options); + + // Añadimos el control al mapa + locateControl.addTo(map); + + // Si tenemos permiso de ubicación y ya conocemos la ubicación del usuario, + // podemos activarla automáticamente + if (mapState.hasLocationPermission && mapState.userLocation) { + // Esperamos a que el mapa esté listo + setTimeout(() => { + try { + locateControl.start(); + } catch (e) { + console.error('Error al iniciar la localización automática', e); + } + }, 1000); + } + + return () => { + // Limpieza al desmontar el componente + locateControl.remove(); + }; + }, [map, mapState.hasLocationPermission, mapState.userLocation, props.options, setLocationPermission, setUserLocation]); + + return null; +}; + +// Exportamos también el control base por compatibilidad +export const LocateControl = createControlComponent( + (props) => new LeafletLocateControl(props) +); diff --git a/src/frontend/src/data/StopDataProvider.ts b/src/frontend/src/data/StopDataProvider.ts new file mode 100644 index 0000000..0c1e46e --- /dev/null +++ b/src/frontend/src/data/StopDataProvider.ts @@ -0,0 +1,160 @@ +export interface CachedStopList { + timestamp: number; + data: Stop[]; +} + +export type StopName = { + original: string; + intersect?: string; +} + +export interface Stop { + stopId: number; + name: StopName; + latitude?: number; + longitude?: number; + lines: string[]; + favourite?: boolean; +} + +// In-memory cache and lookup map +let cachedStops: Stop[] | null = null; +let stopsMap: Record = {}; +// Custom names loaded from localStorage +let customNames: Record = {}; + +// Initialize cachedStops and customNames once +async function initStops() { + if (!cachedStops) { + const response = await fetch('/stops.json'); + const stops = await response.json() as Stop[]; + // build array and map + stopsMap = {}; + cachedStops = stops.map(stop => { + const entry = { ...stop, favourite: false } as Stop; + stopsMap[stop.stopId] = entry; + return entry; + }); + // load custom names + const rawCustom = localStorage.getItem('customStopNames'); + if (rawCustom) customNames = JSON.parse(rawCustom) as Record; + } +} + +async function getStops(): Promise { + await initStops(); + // update favourites + const rawFav = localStorage.getItem('favouriteStops'); + const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : []; + cachedStops!.forEach(stop => stop.favourite = favouriteStops.includes(stop.stopId)); + return cachedStops!; +} + +// New: get single stop by id +async function getStopById(stopId: number): Promise { + await initStops(); + const stop = stopsMap[stopId]; + if (stop) { + const rawFav = localStorage.getItem('favouriteStops'); + const favouriteStops = rawFav ? JSON.parse(rawFav) as number[] : []; + stop.favourite = favouriteStops.includes(stopId); + } + return stop; +} + +// Updated display name to include custom names +function getDisplayName(stop: Stop): string { + if (customNames[stop.stopId]) return customNames[stop.stopId]; + const nameObj = stop.name; + return nameObj.intersect || nameObj.original; +} + +// New: set or remove custom names +function setCustomName(stopId: number, label: string) { + customNames[stopId] = label; + localStorage.setItem('customStopNames', JSON.stringify(customNames)); +} + +function removeCustomName(stopId: number) { + delete customNames[stopId]; + localStorage.setItem('customStopNames', JSON.stringify(customNames)); +} + +// New: get custom label for a stop +function getCustomName(stopId: number): string | undefined { + return customNames[stopId]; +} + +function addFavourite(stopId: number) { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + let favouriteStops: number[] = []; + if (rawFavouriteStops) { + favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + } + + if (!favouriteStops.includes(stopId)) { + favouriteStops.push(stopId); + localStorage.setItem('favouriteStops', JSON.stringify(favouriteStops)); + } +} + +function removeFavourite(stopId: number) { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + let favouriteStops: number[] = []; + if (rawFavouriteStops) { + favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + } + + const newFavouriteStops = favouriteStops.filter(id => id !== stopId); + localStorage.setItem('favouriteStops', JSON.stringify(newFavouriteStops)); +} + +function isFavourite(stopId: number): boolean { + const rawFavouriteStops = localStorage.getItem('favouriteStops'); + if (rawFavouriteStops) { + const favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + return favouriteStops.includes(stopId); + } + return false; +} + +const RECENT_STOPS_LIMIT = 10; + +function pushRecent(stopId: number) { + const rawRecentStops = localStorage.getItem('recentStops'); + let recentStops: Set = new Set(); + if (rawRecentStops) { + recentStops = new Set(JSON.parse(rawRecentStops) as number[]); + } + + recentStops.add(stopId); + if (recentStops.size > RECENT_STOPS_LIMIT) { + const iterator = recentStops.values(); + const val = iterator.next().value as number; + recentStops.delete(val); + } + + localStorage.setItem('recentStops', JSON.stringify(Array.from(recentStops))); +} + +function getRecent(): number[] { + const rawRecentStops = localStorage.getItem('recentStops'); + if (rawRecentStops) { + return JSON.parse(rawRecentStops) as number[]; + } + return []; +} + +export default { + getStops, + getStopById, + getCustomName, + getDisplayName, + setCustomName, + removeCustomName, + addFavourite, + removeFavourite, + isFavourite, + pushRecent, + getRecent +}; diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx new file mode 100644 index 0000000..48ff63c --- /dev/null +++ b/src/frontend/src/main.tsx @@ -0,0 +1,43 @@ +import '@fontsource-variable/outfit' +import './styles/Pages.css' + +import { createRoot } from 'react-dom/client' +import { createBrowserRouter, Navigate, RouterProvider } from 'react-router' +import { StopList } from './pages/StopList' +import { Estimates } from './pages/Estimates' +import { StopMap } from './pages/Map' +import { Layout } from './Layout' +import { Settings } from './pages/Settings' +import { AppProvider } from './AppContext' +import ErrorBoundary from './ErrorBoundary' + +const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/stops', + element: , + }, + { + path: '/map', + element: , + }, + { + path: '/estimates/:stopId', + element: + }, + { + path: '/settings', + element: + } +]) + +createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/src/frontend/src/pages/Estimates.tsx b/src/frontend/src/pages/Estimates.tsx new file mode 100644 index 0000000..6a98731 --- /dev/null +++ b/src/frontend/src/pages/Estimates.tsx @@ -0,0 +1,99 @@ +import { type JSX, useEffect, useState } from "react"; +import { useParams } from "react-router"; +import StopDataProvider from "../data/StopDataProvider"; +import { Star, Edit2 } from 'lucide-react'; +import "../styles/Estimates.css"; +import { RegularTable } from "../components/RegularTable"; +import { useApp } from "../AppContext"; +import { GroupedTable } from "../components/GroupedTable"; + +export interface StopDetails { + stop: { + id: number; + name: string; + latitude: number; + longitude: number; + } + estimates: { + line: string; + route: string; + minutes: number; + meters: number; + }[] +} + +const loadData = async (stopId: string) => { + const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`); + return await resp.json(); +}; + +export function Estimates() { + const params = useParams(); + const stopIdNum = parseInt(params.stopId ?? ""); + const [customName, setCustomName] = useState(undefined); + const [data, setData] = useState(null); + const [dataDate, setDataDate] = useState(null); + const [favourited, setFavourited] = useState(false); + const { tableStyle } = useApp(); + + useEffect(() => { + loadData(params.stopId!) + .then((body: StopDetails) => { + setData(body); + setDataDate(new Date()); + setCustomName(StopDataProvider.getCustomName(stopIdNum)); + }) + + + StopDataProvider.pushRecent(parseInt(params.stopId ?? "")); + + setFavourited( + StopDataProvider.isFavourite(parseInt(params.stopId ?? "")) + ); + }, [params.stopId]); + + + const toggleFavourite = () => { + if (favourited) { + StopDataProvider.removeFavourite(stopIdNum); + setFavourited(false); + } else { + StopDataProvider.addFavourite(stopIdNum); + setFavourited(true); + } + } + + const handleRename = () => { + const current = customName ?? data?.stop.name; + const input = window.prompt('Custom name for this stop:', current); + if (input === null) return; // cancelled + const trimmed = input.trim(); + if (trimmed === '') { + StopDataProvider.removeCustomName(stopIdNum); + setCustomName(undefined); + } else { + StopDataProvider.setCustomName(stopIdNum, trimmed); + setCustomName(trimmed); + } + }; + + if (data === null) return

    Cargando datos en tiempo real...

    + + return ( +
    +
    +

    + + + {(customName ?? data.stop.name)} ({data.stop.id}) +

    +
    + +
    + {tableStyle === 'grouped' ? + : + } +
    +
    + ) +} diff --git a/src/frontend/src/pages/Map.tsx b/src/frontend/src/pages/Map.tsx new file mode 100644 index 0000000..52c73f8 --- /dev/null +++ b/src/frontend/src/pages/Map.tsx @@ -0,0 +1,75 @@ +import StopDataProvider, { type Stop } from "../data/StopDataProvider"; + +import 'leaflet/dist/leaflet.css' +import 'react-leaflet-markercluster/styles' + +import { useEffect, useState } from 'react'; +import LineIcon from '../components/LineIcon'; +import { Link } from 'react-router'; +import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; +import MarkerClusterGroup from "react-leaflet-markercluster"; +import { Icon, type LatLngTuple } from "leaflet"; +import { EnhancedLocateControl } from "../controls/LocateControl"; +import { useApp } from "../AppContext"; + +const icon = new Icon({ + iconUrl: '/map-pin-icon.png', + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41] +}); + +// Componente auxiliar para detectar cambios en el mapa +const MapEventHandler = () => { + const { updateMapState } = useApp(); + + const map = useMapEvents({ + moveend: () => { + const center = map.getCenter(); + const zoom = map.getZoom(); + updateMapState([center.lat, center.lng], zoom); + } + }); + + return null; +}; + +// Componente principal del mapa +export function StopMap() { + const [stops, setStops] = useState([]); + const { mapState } = useApp(); + + useEffect(() => { + StopDataProvider.getStops().then(setStops); + }, []); + + return ( + + + + + + {stops.map(stop => ( + + + {StopDataProvider.getDisplayName(stop)} +
    + {stop.lines.map((line) => ( + + ))} +
    +
    + ))} +
    +
    + ); +} diff --git a/src/frontend/src/pages/Settings.tsx b/src/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..1ad15ab --- /dev/null +++ b/src/frontend/src/pages/Settings.tsx @@ -0,0 +1,65 @@ +import { useApp } from "../AppContext"; +import "../styles/Settings.css"; + +export function Settings() { + const { theme, setTheme, tableStyle, setTableStyle, mapPositionMode, setMapPositionMode } = useApp(); + + return ( +
    +

    Sobre UrbanoVigo Web

    +

    + Aplicación web para encontrar paradas y tiempos de llegada de los autobuses + urbanos de Vigo, España. +

    +
    +

    Ajustes

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + ¿Qué significa esto? +

    + La tabla de horarios puede mostrarse de dos formas: +

    +
    +
    Mostrar por orden
    +
    Las paradas se muestran en el orden en que se visitan. Aplicaciones como Infobus (Vitrasa) usan este estilo.
    +
    Agrupar por línea
    +
    Las paradas se agrupan por la línea de autobús. Aplicaciones como iTranvias (A Coruña) o Moovit (más o menos) usan este estilo.
    +
    +
    +
    +

    Créditos

    +

    + + Código en GitHub + - + Desarrollado por + Ariel Costas + +

    +

    + Datos obtenidos de datos.vigo.org bajo + licencia Open Data Commons Attribution License +

    +
    + ) +} \ No newline at end of file diff --git a/src/frontend/src/pages/StopList.tsx b/src/frontend/src/pages/StopList.tsx new file mode 100644 index 0000000..59a1942 --- /dev/null +++ b/src/frontend/src/pages/StopList.tsx @@ -0,0 +1,135 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import StopDataProvider, { type Stop } from "../data/StopDataProvider"; +import StopItem from "../components/StopItem"; +import Fuse from "fuse.js"; + +const placeholders = [ + "Urzaiz", + "Gran Vía", + "Castelao", + "García Barbón", + "Valladares", + "Florida", + "Pizarro", + "Estrada Madrid", + "Sanjurjo Badía" +]; + +export function StopList() { + const [data, setData] = useState(null) + const [searchResults, setSearchResults] = useState(null); + const searchTimeout = useRef(null); + + const randomPlaceholder = useMemo(() => placeholders[Math.floor(Math.random() * placeholders.length)], []); + const fuse = useMemo(() => new Fuse(data || [], { threshold: 0.3, keys: ['name.original'] }), [data]); + + useEffect(() => { + StopDataProvider.getStops().then((stops: Stop[]) => setData(stops)) + }, []); + + const handleStopSearch = (event: React.ChangeEvent) => { + const stopName = event.target.value || ""; + + if (searchTimeout.current) { + clearTimeout(searchTimeout.current); + } + + searchTimeout.current = setTimeout(() => { + if (stopName.length === 0) { + setSearchResults(null); + return; + } + + if (!data) { + console.error("No data available for search"); + return; + } + + const results = fuse.search(stopName); + const items = results.map(result => result.item); + setSearchResults(items); + }, 300); + } + + const favouritedStops = useMemo(() => { + return data?.filter(stop => stop.favourite) ?? [] + }, [data]) + + const recentStops = useMemo(() => { + // no recent items if data not loaded + if (!data) return null; + const recentIds = StopDataProvider.getRecent(); + if (recentIds.length === 0) return null; + // map and filter out missing entries + const stopsList = recentIds + .map(id => data.find(stop => stop.stopId === id)) + .filter((s): s is Stop => Boolean(s)); + return stopsList.reverse(); + }, [data]); + + if (data === null) return

    Loading...

    + + return ( +
    +

    UrbanoVigo Web

    + +
    +
    + + +
    +
    + + {searchResults && searchResults.length > 0 && ( +
    +

    Resultados de la búsqueda

    +
      + {searchResults.map((stop: Stop) => ( + + ))} +
    +
    + )} + +
    +

    Paradas favoritas

    + + {favouritedStops?.length === 0 && ( +

    + Accede a una parada y márcala como favorita para verla aquí. +

    + )} + +
      + {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
    +
    + + {recentStops && recentStops.length > 0 && ( +
    +

    Recientes

    + +
      + {recentStops.map((stop: Stop) => ( + + ))} +
    +
    + )} + +
    +

    Paradas

    + +
      + {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( + + ))} +
    +
    +
    + ) +} diff --git a/src/frontend/src/styles/Estimates.css b/src/frontend/src/styles/Estimates.css new file mode 100644 index 0000000..86ca09b --- /dev/null +++ b/src/frontend/src/styles/Estimates.css @@ -0,0 +1,105 @@ +.table-responsive { + overflow-x: auto; + margin-bottom: 1.5rem; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table caption { + margin-bottom: 0.5rem; + font-weight: 500; +} + +.table th, +.table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #eee; +} + +.table th { + border-bottom: 2px solid #ddd; +} + +.table tfoot td { + text-align: center; +} + +/* Estimates page specific styles */ +.estimates-header { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.estimates-stop-id { + font-size: 1rem; + color: var(--subtitle-color); + margin-left: 0.5rem; +} + +.estimates-arrival { + color: #28a745; + font-weight: 500; +} + +.estimates-delayed { + color: #dc3545; +} + +.button-group { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.button { + padding: 0.75rem 1rem; + background-color: var(--button-background-color); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + text-align: center; + text-decoration: none; + display: inline-block; +} + +.button:hover { + background-color: var(--button-hover-background-color); +} + +.button:disabled { + background-color: var(--button-disabled-background-color); + cursor: not-allowed; +} + +.star-icon { + margin-right: 0.5rem; + color: #ccc; + fill: none; +} + +.star-icon.active { + color: var(--star-color); + /* Yellow color for active star */ + fill: var(--star-color); +} + +/* Pencil (edit) icon next to header */ +.edit-icon { + margin-right: 0.5rem; + color: #ccc; + cursor: pointer; + stroke-width: 2px; +} + +.edit-icon:hover { + color: var(--star-color); +} \ No newline at end of file diff --git a/src/frontend/src/styles/Map.css b/src/frontend/src/styles/Map.css new file mode 100644 index 0000000..3af112a --- /dev/null +++ b/src/frontend/src/styles/Map.css @@ -0,0 +1,86 @@ +/* Map page specific styles */ +.map-container { + height: calc(100vh - 140px); + margin: -16px; + margin-bottom: 1rem; + position: relative; +} + +/* Fullscreen map styles */ +.fullscreen-container { + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + padding: 0; + margin: 0; + max-width: none; + overflow: hidden; +} + +.fullscreen-map { + width: 100%; + height: 100%; +} + +.fullscreen-loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + font-size: 1.8rem; + font-weight: 600; + color: var(--text-color); +} + +/* Map marker and popup styles */ +.stop-marker { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + transition: all 0.2s ease-in-out; +} + +.stop-marker:hover { + transform: scale(1.2); +} + +.maplibregl-popup { + max-width: 250px; +} + +.maplibregl-popup-content { + padding: 12px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.popup-line-icons { + display: flex; + flex-wrap: wrap; + margin: 6px 0; + gap: 5px; +} + +.popup-line { + display: inline-block; + background-color: var(--button-background-color); + color: white; + padding: 2px 6px; + margin-right: 4px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; +} + +.popup-link { + display: block; + margin-top: 8px; + color: var(--button-background-color); + text-decoration: none; + font-weight: 500; +} + +.popup-link:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/frontend/src/styles/Pages.css b/src/frontend/src/styles/Pages.css new file mode 100644 index 0000000..90ffad2 --- /dev/null +++ b/src/frontend/src/styles/Pages.css @@ -0,0 +1,364 @@ +:root { + --background-color: #ffffff; + --text-color: #333333; + --subtitle-color: #444444; + --border-color: #eeeeee; + --button-background-color: #007bff; + --button-hover-background-color: #0069d9; + --button-disabled-background-color: #cccccc; + --star-color: #ffcc00; + --message-background-color: #f8f9fa; + + font-family: 'Outfit Variable', Roboto, Arial, sans-serif; +} + +[data-theme='dark'] { + --background-color: #121212; + --text-color: #ffffff; + --subtitle-color: #bbbbbb; + --border-color: #444444; + --button-background-color: #1e88e5; + --button-hover-background-color: #1565c0; + --button-disabled-background-color: #555555; + --star-color: #ffcc00; + --message-background-color: #333333; +} + +body { + background-color: var(--background-color); + color: var(--text-color); +} + +/* Mobile-first page styles */ + +/* Common page styles */ +.page-container { + max-width: 100%; + padding: 0 16px; + background-color: var(--background-color); + color: var(--text-color); +} + +.page-title { + font-size: 1.8rem; + margin-bottom: 1rem; + font-weight: 600; + color: var(--text-color); +} + +.page-subtitle { + font-size: 1.4rem; + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-weight: 500; + color: var(--subtitle-color); +} + +/* Form styles */ +.search-form { + margin-bottom: 1.5rem; +} + +.form-group { + margin-bottom: 1rem; + display: flex; + flex-direction: column; +} + +.form-label { + font-size: 0.9rem; + margin-bottom: 0.25rem; + font-weight: 500; +} + +.form-input { + padding: 0.75rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.form-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 1rem; + + padding: 0.75rem 1rem; + background-color: var(--button-background-color); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + width: 100%; + margin-top: 0.5rem; +} + +.form-button:hover { + background-color: var(--button-hover-background-color); +} + +/* List styles */ +.list-container { + margin-bottom: 1.5rem; +} + +.list { + list-style: none; + padding: 0; + margin: 0; +} + +.list-item { + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.list-item-link { + display: block; + color: var(--text-color); + text-decoration: none; + font-size: 1.1rem; /* Increased font size for stop name */ +} + +.list-item-link:hover { + color: var(--button-background-color); +} + +.list-item-link:hover .line-icon { + color: var(--text-color); +} + +.distance-info { + font-size: 0.9rem; + color: var(--subtitle-color); +} + +/* Message styles */ +.message { + padding: 1rem; + background-color: var(--message-background-color); + border-radius: 8px; + margin-bottom: 1rem; +} + +/* About page specific styles */ +.about-page { + text-align: center; + padding: 1rem; +} + +.about-version { + color: var(--subtitle-color); + font-size: 0.9rem; + margin-top: 2rem; +} + +.about-description { + margin-top: 1rem; + line-height: 1.6; +} + +/* Map page specific styles */ +.map-container { + height: calc(100vh - 140px); + margin: -16px; + margin-bottom: 1rem; + position: relative; +} + +/* Fullscreen map styles */ +.fullscreen-container { + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + padding: 0; + margin: 0; + max-width: none; + overflow: hidden; +} + +.fullscreen-map { + width: 100%; + height: 100%; +} + +.fullscreen-loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + font-size: 1.8rem; + font-weight: 600; + color: var(--text-color); +} + +/* Map marker and popup styles */ +.stop-marker { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); + transition: all 0.2s ease-in-out; +} + +.stop-marker:hover { + transform: scale(1.2); +} + +.maplibregl-popup { + max-width: 250px; +} + +.maplibregl-popup-content { + padding: 12px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.popup-line-icons { + display: flex; + flex-wrap: wrap; + margin: 6px 0; + gap: 5px; +} + +.popup-line { + display: inline-block; + background-color: var(--button-background-color); + color: white; + padding: 2px 6px; + margin-right: 4px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; +} + +.popup-link { + display: block; + margin-top: 8px; + color: var(--button-background-color); + text-decoration: none; + font-weight: 500; +} + +.popup-link:hover { + text-decoration: underline; +} + +/* Estimates page specific styles */ +.estimates-header { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +.estimates-stop-id { + font-size: 1rem; + color: var(--subtitle-color); + margin-left: 0.5rem; +} + +.estimates-arrival { + color: #28a745; + font-weight: 500; +} + +.estimates-delayed { + color: #dc3545; +} + +.button-group { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.button { + padding: 0.75rem 1rem; + background-color: var(--button-background-color); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + text-align: center; + text-decoration: none; + display: inline-block; +} + +.button:hover { + background-color: var(--button-hover-background-color); +} + +.button:disabled { + background-color: var(--button-disabled-background-color); + cursor: not-allowed; +} + +.star-icon { + margin-right: 0.5rem; + color: #ccc; + fill: none; +} + +.star-icon.active { + color: var(--star-color); /* Yellow color for active star */ + fill: var(--star-color); +} + +/* Tablet and larger breakpoint */ +@media (min-width: 768px) { + .page-container { + width: 90%; + max-width: 768px; + margin: 0 auto; + } + + .page-title { + font-size: 2.2rem; + } + + .search-form { + display: flex; + align-items: flex-end; + gap: 1rem; + } + + .form-group { + flex: 1; + margin-bottom: 0; + } + + .form-button { + width: auto; + margin-top: 0; + } + + .list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + } + + .list-item { + border: 1px solid var(--border-color); + border-radius: 8px; + margin-bottom: 0; + } +} + +/* Desktop breakpoint */ +@media (min-width: 1024px) { + .page-container { + max-width: 1024px; + } + + .list { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + } +} \ No newline at end of file diff --git a/src/frontend/src/styles/Settings.css b/src/frontend/src/styles/Settings.css new file mode 100644 index 0000000..934577d --- /dev/null +++ b/src/frontend/src/styles/Settings.css @@ -0,0 +1,94 @@ +/* About page specific styles */ +.about-page { + text-align: center; + padding: 1rem; +} + +.about-version { + color: var(--subtitle-color); + font-size: 0.9rem; + margin-top: 2rem; +} + +.about-description { + margin-top: 1rem; + line-height: 1.6; +} + +.settings-section { + margin-bottom: 2em; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; + background-color: var(--message-background-color); + text-align: left; +} + +.settings-section h2 { + margin-bottom: 1em; +} + +.settings-content { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 1em; +} + +.settings-content-inline { + display: flex; + align-items: center; + margin-bottom: 1em; +} + +.settings-section .form-button { + margin-bottom: 1em; + padding: 0.75rem 1.5rem; + font-size: 1.1rem; +} + +.settings-section .form-select-inline { + margin-left: 0.5em; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.settings-section .form-label-inline { + font-weight: 500; +} + +.settings-section .form-label { + display: block; + margin-bottom: 0.5em; + font-weight: 500; +} + +.settings-section .form-description { + margin-top: 0.5em; + font-size: 0.9rem; + color: var(--subtitle-color); +} + +.settings-section .form-details { + margin-top: 0.5em; + font-size: 0.9rem; + color: var(--subtitle-color); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 0.5rem; +} + +.settings-section .form-details summary { + cursor: pointer; + font-weight: 500; +} + +.settings-section .form-details p { + margin-top: 0.5em; +} + +.settings-section p { + margin-top: 0.5em; +} \ No newline at end of file diff --git a/src/frontend/src/vite-env.d.ts b/src/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/frontend/tsconfig.json b/src/frontend/tsconfig.json new file mode 100644 index 0000000..dc391a4 --- /dev/null +++ b/src/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts new file mode 100644 index 0000000..e7b5a95 --- /dev/null +++ b/src/frontend/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '^/api': { + target: 'https://localhost:7240', + secure: false + } + } + }, + build: { + rollupOptions: { + output: { + manualChunks: { + react: ['react', 'react-dom'], + router: ['react-router'], + leaflet: ['leaflet', 'react-leaflet', 'leaflet.locatecontrol', 'leaflet.markercluster'] + } + } + } + } +}) diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index c7a4db9..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import '@fontsource-variable/outfit' -import './styles/Pages.css' - -import { createRoot } from 'react-dom/client' -import { createBrowserRouter, Navigate, RouterProvider } from 'react-router' -import { StopList } from './pages/StopList.tsx' -import { Estimates } from './pages/Estimates.tsx' -import { StopMap } from './pages/Map.tsx' -import { Layout } from './Layout.tsx' -import { Settings } from './pages/Settings.tsx' -import { AppProvider } from './AppContext.tsx' -import ErrorBoundary from './ErrorBoundary' - -const router = createBrowserRouter([ - { - path: '/', - element: , - }, - { - path: '/stops', - element: , - }, - { - path: '/map', - element: , - }, - { - path: '/estimates/:stopId', - element: - }, - { - path: '/settings', - element: - } -]) - -createRoot(document.getElementById('root')!).render( - - - - - -) diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx deleted file mode 100644 index 7cf941a..0000000 --- a/src/pages/Estimates.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { JSX, useEffect, useState } from "react"; -import { useParams } from "react-router"; -import StopDataProvider from "../data/StopDataProvider"; -import { Star, Edit2 } from 'lucide-react'; -import "../styles/Estimates.css"; -import { RegularTable } from "../components/RegularTable"; -import { useApp } from "../AppContext"; -import { GroupedTable } from "../components/GroupedTable"; - -export interface StopDetails { - stop: { - id: number; - name: string; - latitude: number; - longitude: number; - } - estimates: { - line: string; - route: string; - minutes: number; - meters: number; - }[] -} - -const loadData = async (stopId: string) => { - const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`); - return await resp.json(); -}; - -export function Estimates(): JSX.Element { - const params = useParams(); - const stopIdNum = parseInt(params.stopId ?? ""); - const [customName, setCustomName] = useState(undefined); - const [data, setData] = useState(null); - const [dataDate, setDataDate] = useState(null); - const [favourited, setFavourited] = useState(false); - const { tableStyle } = useApp(); - - useEffect(() => { - loadData(params.stopId!) - .then((body: StopDetails) => { - setData(body); - setDataDate(new Date()); - setCustomName(StopDataProvider.getCustomName(stopIdNum)); - }) - - - StopDataProvider.pushRecent(parseInt(params.stopId ?? "")); - - setFavourited( - StopDataProvider.isFavourite(parseInt(params.stopId ?? "")) - ); - }, [params.stopId]); - - - const toggleFavourite = () => { - if (favourited) { - StopDataProvider.removeFavourite(stopIdNum); - setFavourited(false); - } else { - StopDataProvider.addFavourite(stopIdNum); - setFavourited(true); - } - } - - const handleRename = () => { - const current = customName ?? data?.stop.name; - const input = window.prompt('Custom name for this stop:', current); - if (input === null) return; // cancelled - const trimmed = input.trim(); - if (trimmed === '') { - StopDataProvider.removeCustomName(stopIdNum); - setCustomName(undefined); - } else { - StopDataProvider.setCustomName(stopIdNum, trimmed); - setCustomName(trimmed); - } - }; - - if (data === null) return

    Cargando datos en tiempo real...

    - - return ( -
    -
    -

    - - - {(customName ?? data.stop.name)} ({data.stop.id}) -

    -
    - -
    - {tableStyle === 'grouped' ? - : - } -
    -
    - ) -} diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx deleted file mode 100644 index 1f0a9e0..0000000 --- a/src/pages/Map.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import StopDataProvider, { Stop } from "../data/StopDataProvider"; - -import 'leaflet/dist/leaflet.css' -import 'react-leaflet-markercluster/styles' - -import { useEffect, useState } from 'react'; -import LineIcon from '../components/LineIcon'; -import { Link } from 'react-router'; -import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; -import MarkerClusterGroup from "react-leaflet-markercluster"; -import { Icon, LatLngTuple } from "leaflet"; -import { EnhancedLocateControl } from "../controls/LocateControl"; -import { useApp } from "../AppContext"; - -const icon = new Icon({ - iconUrl: '/map-pin-icon.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - shadowSize: [41, 41] -}); - -// Componente auxiliar para detectar cambios en el mapa -const MapEventHandler = () => { - const { updateMapState } = useApp(); - - const map = useMapEvents({ - moveend: () => { - const center = map.getCenter(); - const zoom = map.getZoom(); - updateMapState([center.lat, center.lng], zoom); - } - }); - - return null; -}; - -// Componente principal del mapa -export function StopMap() { - const [stops, setStops] = useState([]); - const { mapState } = useApp(); - - useEffect(() => { - StopDataProvider.getStops().then(setStops); - }, []); - - return ( - - - - - - {stops.map(stop => ( - - - {StopDataProvider.getDisplayName(stop)} -
    - {stop.lines.map((line) => ( - - ))} -
    -
    - ))} -
    -
    - ); -} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx deleted file mode 100644 index 1ad15ab..0000000 --- a/src/pages/Settings.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useApp } from "../AppContext"; -import "../styles/Settings.css"; - -export function Settings() { - const { theme, setTheme, tableStyle, setTableStyle, mapPositionMode, setMapPositionMode } = useApp(); - - return ( -
    -

    Sobre UrbanoVigo Web

    -

    - Aplicación web para encontrar paradas y tiempos de llegada de los autobuses - urbanos de Vigo, España. -

    -
    -

    Ajustes

    -
    - - -
    -
    - - -
    -
    - - -
    -
    - ¿Qué significa esto? -

    - La tabla de horarios puede mostrarse de dos formas: -

    -
    -
    Mostrar por orden
    -
    Las paradas se muestran en el orden en que se visitan. Aplicaciones como Infobus (Vitrasa) usan este estilo.
    -
    Agrupar por línea
    -
    Las paradas se agrupan por la línea de autobús. Aplicaciones como iTranvias (A Coruña) o Moovit (más o menos) usan este estilo.
    -
    -
    -
    -

    Créditos

    -

    - - Código en GitHub - - - Desarrollado por - Ariel Costas - -

    -

    - Datos obtenidos de datos.vigo.org bajo - licencia Open Data Commons Attribution License -

    -
    - ) -} \ No newline at end of file diff --git a/src/pages/StopList.tsx b/src/pages/StopList.tsx deleted file mode 100644 index b965456..0000000 --- a/src/pages/StopList.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import StopDataProvider, { Stop } from "../data/StopDataProvider"; -import StopItem from "../components/StopItem"; -import Fuse from "fuse.js"; - -const placeholders = [ - "Urzaiz", - "Gran Vía", - "Castelao", - "García Barbón", - "Valladares", - "Florida", - "Pizarro", - "Estrada Madrid", - "Sanjurjo Badía" -]; - -export function StopList() { - const [data, setData] = useState(null) - const [searchResults, setSearchResults] = useState(null); - const searchTimeout = useRef(null); - - const randomPlaceholder = useMemo(() => placeholders[Math.floor(Math.random() * placeholders.length)], []); - const fuse = useMemo(() => new Fuse(data || [], { threshold: 0.3, keys: ['name.original'] }), [data]); - - useEffect(() => { - StopDataProvider.getStops().then((stops: Stop[]) => setData(stops)) - }, []); - - const handleStopSearch = (event: React.ChangeEvent) => { - const stopName = event.target.value || ""; - - if (searchTimeout.current) { - clearTimeout(searchTimeout.current); - } - - searchTimeout.current = setTimeout(() => { - if (stopName.length === 0) { - setSearchResults(null); - return; - } - - if (!data) { - console.error("No data available for search"); - return; - } - - const results = fuse.search(stopName); - const items = results.map(result => result.item); - setSearchResults(items); - }, 300); - } - - const favouritedStops = useMemo(() => { - return data?.filter(stop => stop.favourite) ?? [] - }, [data]) - - const recentStops = useMemo(() => { - // no recent items if data not loaded - if (!data) return null; - const recentIds = StopDataProvider.getRecent(); - if (recentIds.length === 0) return null; - // map and filter out missing entries - const stopsList = recentIds - .map(id => data.find(stop => stop.stopId === id)) - .filter((s): s is Stop => Boolean(s)); - return stopsList.reverse(); - }, [data]); - - if (data === null) return

    Loading...

    - - return ( -
    -

    UrbanoVigo Web

    - -
    -
    - - -
    -
    - - {searchResults && searchResults.length > 0 && ( -
    -

    Resultados de la búsqueda

    -
      - {searchResults.map((stop: Stop) => ( - - ))} -
    -
    - )} - -
    -

    Paradas favoritas

    - - {favouritedStops?.length === 0 && ( -

    - Accede a una parada y márcala como favorita para verla aquí. -

    - )} - -
      - {favouritedStops?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( - - ))} -
    -
    - - {recentStops && recentStops.length > 0 && ( -
    -

    Recientes

    - -
      - {recentStops.map((stop: Stop) => ( - - ))} -
    -
    - )} - -
    -

    Paradas

    - -
      - {data?.sort((a, b) => a.stopId - b.stopId).map((stop: Stop) => ( - - ))} -
    -
    -
    - ) -} diff --git a/src/styles/Estimates.css b/src/styles/Estimates.css deleted file mode 100644 index 86ca09b..0000000 --- a/src/styles/Estimates.css +++ /dev/null @@ -1,105 +0,0 @@ -.table-responsive { - overflow-x: auto; - margin-bottom: 1.5rem; -} - -.table { - width: 100%; - border-collapse: collapse; -} - -.table caption { - margin-bottom: 0.5rem; - font-weight: 500; -} - -.table th, -.table td { - padding: 0.75rem; - text-align: left; - border-bottom: 1px solid #eee; -} - -.table th { - border-bottom: 2px solid #ddd; -} - -.table tfoot td { - text-align: center; -} - -/* Estimates page specific styles */ -.estimates-header { - display: flex; - align-items: center; - margin-bottom: 1rem; -} - -.estimates-stop-id { - font-size: 1rem; - color: var(--subtitle-color); - margin-left: 0.5rem; -} - -.estimates-arrival { - color: #28a745; - font-weight: 500; -} - -.estimates-delayed { - color: #dc3545; -} - -.button-group { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - flex-wrap: wrap; -} - -.button { - padding: 0.75rem 1rem; - background-color: var(--button-background-color); - color: white; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - text-align: center; - text-decoration: none; - display: inline-block; -} - -.button:hover { - background-color: var(--button-hover-background-color); -} - -.button:disabled { - background-color: var(--button-disabled-background-color); - cursor: not-allowed; -} - -.star-icon { - margin-right: 0.5rem; - color: #ccc; - fill: none; -} - -.star-icon.active { - color: var(--star-color); - /* Yellow color for active star */ - fill: var(--star-color); -} - -/* Pencil (edit) icon next to header */ -.edit-icon { - margin-right: 0.5rem; - color: #ccc; - cursor: pointer; - stroke-width: 2px; -} - -.edit-icon:hover { - color: var(--star-color); -} \ No newline at end of file diff --git a/src/styles/Map.css b/src/styles/Map.css deleted file mode 100644 index 3af112a..0000000 --- a/src/styles/Map.css +++ /dev/null @@ -1,86 +0,0 @@ -/* Map page specific styles */ -.map-container { - height: calc(100vh - 140px); - margin: -16px; - margin-bottom: 1rem; - position: relative; -} - -/* Fullscreen map styles */ -.fullscreen-container { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - padding: 0; - margin: 0; - max-width: none; - overflow: hidden; -} - -.fullscreen-map { - width: 100%; - height: 100%; -} - -.fullscreen-loading { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - width: 100vw; - font-size: 1.8rem; - font-weight: 600; - color: var(--text-color); -} - -/* Map marker and popup styles */ -.stop-marker { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease-in-out; -} - -.stop-marker:hover { - transform: scale(1.2); -} - -.maplibregl-popup { - max-width: 250px; -} - -.maplibregl-popup-content { - padding: 12px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.popup-line-icons { - display: flex; - flex-wrap: wrap; - margin: 6px 0; - gap: 5px; -} - -.popup-line { - display: inline-block; - background-color: var(--button-background-color); - color: white; - padding: 2px 6px; - margin-right: 4px; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; -} - -.popup-link { - display: block; - margin-top: 8px; - color: var(--button-background-color); - text-decoration: none; - font-weight: 500; -} - -.popup-link:hover { - text-decoration: underline; -} \ No newline at end of file diff --git a/src/styles/Pages.css b/src/styles/Pages.css deleted file mode 100644 index 90ffad2..0000000 --- a/src/styles/Pages.css +++ /dev/null @@ -1,364 +0,0 @@ -:root { - --background-color: #ffffff; - --text-color: #333333; - --subtitle-color: #444444; - --border-color: #eeeeee; - --button-background-color: #007bff; - --button-hover-background-color: #0069d9; - --button-disabled-background-color: #cccccc; - --star-color: #ffcc00; - --message-background-color: #f8f9fa; - - font-family: 'Outfit Variable', Roboto, Arial, sans-serif; -} - -[data-theme='dark'] { - --background-color: #121212; - --text-color: #ffffff; - --subtitle-color: #bbbbbb; - --border-color: #444444; - --button-background-color: #1e88e5; - --button-hover-background-color: #1565c0; - --button-disabled-background-color: #555555; - --star-color: #ffcc00; - --message-background-color: #333333; -} - -body { - background-color: var(--background-color); - color: var(--text-color); -} - -/* Mobile-first page styles */ - -/* Common page styles */ -.page-container { - max-width: 100%; - padding: 0 16px; - background-color: var(--background-color); - color: var(--text-color); -} - -.page-title { - font-size: 1.8rem; - margin-bottom: 1rem; - font-weight: 600; - color: var(--text-color); -} - -.page-subtitle { - font-size: 1.4rem; - margin-top: 1.5rem; - margin-bottom: 0.75rem; - font-weight: 500; - color: var(--subtitle-color); -} - -/* Form styles */ -.search-form { - margin-bottom: 1.5rem; -} - -.form-group { - margin-bottom: 1rem; - display: flex; - flex-direction: column; -} - -.form-label { - font-size: 0.9rem; - margin-bottom: 0.25rem; - font-weight: 500; -} - -.form-input { - padding: 0.75rem; - font-size: 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; -} - -.form-button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 1rem; - - padding: 0.75rem 1rem; - background-color: var(--button-background-color); - color: white; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - width: 100%; - margin-top: 0.5rem; -} - -.form-button:hover { - background-color: var(--button-hover-background-color); -} - -/* List styles */ -.list-container { - margin-bottom: 1.5rem; -} - -.list { - list-style: none; - padding: 0; - margin: 0; -} - -.list-item { - padding: 1rem; - border-bottom: 1px solid var(--border-color); -} - -.list-item-link { - display: block; - color: var(--text-color); - text-decoration: none; - font-size: 1.1rem; /* Increased font size for stop name */ -} - -.list-item-link:hover { - color: var(--button-background-color); -} - -.list-item-link:hover .line-icon { - color: var(--text-color); -} - -.distance-info { - font-size: 0.9rem; - color: var(--subtitle-color); -} - -/* Message styles */ -.message { - padding: 1rem; - background-color: var(--message-background-color); - border-radius: 8px; - margin-bottom: 1rem; -} - -/* About page specific styles */ -.about-page { - text-align: center; - padding: 1rem; -} - -.about-version { - color: var(--subtitle-color); - font-size: 0.9rem; - margin-top: 2rem; -} - -.about-description { - margin-top: 1rem; - line-height: 1.6; -} - -/* Map page specific styles */ -.map-container { - height: calc(100vh - 140px); - margin: -16px; - margin-bottom: 1rem; - position: relative; -} - -/* Fullscreen map styles */ -.fullscreen-container { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - padding: 0; - margin: 0; - max-width: none; - overflow: hidden; -} - -.fullscreen-map { - width: 100%; - height: 100%; -} - -.fullscreen-loading { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - width: 100vw; - font-size: 1.8rem; - font-weight: 600; - color: var(--text-color); -} - -/* Map marker and popup styles */ -.stop-marker { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); - transition: all 0.2s ease-in-out; -} - -.stop-marker:hover { - transform: scale(1.2); -} - -.maplibregl-popup { - max-width: 250px; -} - -.maplibregl-popup-content { - padding: 12px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.popup-line-icons { - display: flex; - flex-wrap: wrap; - margin: 6px 0; - gap: 5px; -} - -.popup-line { - display: inline-block; - background-color: var(--button-background-color); - color: white; - padding: 2px 6px; - margin-right: 4px; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; -} - -.popup-link { - display: block; - margin-top: 8px; - color: var(--button-background-color); - text-decoration: none; - font-weight: 500; -} - -.popup-link:hover { - text-decoration: underline; -} - -/* Estimates page specific styles */ -.estimates-header { - display: flex; - align-items: center; - margin-bottom: 1rem; -} - -.estimates-stop-id { - font-size: 1rem; - color: var(--subtitle-color); - margin-left: 0.5rem; -} - -.estimates-arrival { - color: #28a745; - font-weight: 500; -} - -.estimates-delayed { - color: #dc3545; -} - -.button-group { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - flex-wrap: wrap; -} - -.button { - padding: 0.75rem 1rem; - background-color: var(--button-background-color); - color: white; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - text-align: center; - text-decoration: none; - display: inline-block; -} - -.button:hover { - background-color: var(--button-hover-background-color); -} - -.button:disabled { - background-color: var(--button-disabled-background-color); - cursor: not-allowed; -} - -.star-icon { - margin-right: 0.5rem; - color: #ccc; - fill: none; -} - -.star-icon.active { - color: var(--star-color); /* Yellow color for active star */ - fill: var(--star-color); -} - -/* Tablet and larger breakpoint */ -@media (min-width: 768px) { - .page-container { - width: 90%; - max-width: 768px; - margin: 0 auto; - } - - .page-title { - font-size: 2.2rem; - } - - .search-form { - display: flex; - align-items: flex-end; - gap: 1rem; - } - - .form-group { - flex: 1; - margin-bottom: 0; - } - - .form-button { - width: auto; - margin-top: 0; - } - - .list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1rem; - } - - .list-item { - border: 1px solid var(--border-color); - border-radius: 8px; - margin-bottom: 0; - } -} - -/* Desktop breakpoint */ -@media (min-width: 1024px) { - .page-container { - max-width: 1024px; - } - - .list { - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - } -} \ No newline at end of file diff --git a/src/styles/Settings.css b/src/styles/Settings.css deleted file mode 100644 index 934577d..0000000 --- a/src/styles/Settings.css +++ /dev/null @@ -1,94 +0,0 @@ -/* About page specific styles */ -.about-page { - text-align: center; - padding: 1rem; -} - -.about-version { - color: var(--subtitle-color); - font-size: 0.9rem; - margin-top: 2rem; -} - -.about-description { - margin-top: 1rem; - line-height: 1.6; -} - -.settings-section { - margin-bottom: 2em; - padding: 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - background-color: var(--message-background-color); - text-align: left; -} - -.settings-section h2 { - margin-bottom: 1em; -} - -.settings-content { - display: flex; - flex-direction: column; - align-items: flex-start; - margin-bottom: 1em; -} - -.settings-content-inline { - display: flex; - align-items: center; - margin-bottom: 1em; -} - -.settings-section .form-button { - margin-bottom: 1em; - padding: 0.75rem 1.5rem; - font-size: 1.1rem; -} - -.settings-section .form-select-inline { - margin-left: 0.5em; - padding: 0.5rem; - font-size: 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; -} - -.settings-section .form-label-inline { - font-weight: 500; -} - -.settings-section .form-label { - display: block; - margin-bottom: 0.5em; - font-weight: 500; -} - -.settings-section .form-description { - margin-top: 0.5em; - font-size: 0.9rem; - color: var(--subtitle-color); -} - -.settings-section .form-details { - margin-top: 0.5em; - font-size: 0.9rem; - color: var(--subtitle-color); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.5rem; -} - -.settings-section .form-details summary { - cursor: pointer; - font-weight: 500; -} - -.settings-section .form-details p { - margin-top: 0.5em; -} - -.settings-section p { - margin-top: 0.5em; -} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// -- cgit v1.3