From 2a9aca302485bc08f5b2dd2a54987de6f80fc338 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Fri, 19 Dec 2025 13:06:27 +0100 Subject: Implement loading stops as tiles from OTP --- src/frontend/app/AppContext.tsx | 2 +- src/frontend/app/components/LineIcon.tsx | 27 +- src/frontend/app/components/PlannerOverlay.tsx | 3 +- src/frontend/app/components/RegionSelector.tsx | 33 --- src/frontend/app/components/StopMapModal.tsx | 6 +- src/frontend/app/components/StopSummarySheet.css | 309 --------------------- src/frontend/app/components/StopSummarySheet.tsx | 209 -------------- .../app/components/StopSummarySheetSkeleton.tsx | 79 ------ .../app/components/map/StopSummarySheet.css | 309 +++++++++++++++++++++ .../app/components/map/StopSummarySheet.tsx | 220 +++++++++++++++ .../components/map/StopSummarySheetSkeleton.tsx | 79 ++++++ src/frontend/app/config/RegionConfig.ts | 44 --- src/frontend/app/config/constants.ts | 22 ++ src/frontend/app/contexts/MapContext.tsx | 10 +- src/frontend/app/contexts/SettingsContext.tsx | 2 +- src/frontend/app/data/SpecialPlacesProvider.ts | 6 +- src/frontend/app/data/StopDataProvider.ts | 64 +++-- src/frontend/app/routes/home.tsx | 2 +- src/frontend/app/routes/map.tsx | 142 +++++----- src/frontend/app/routes/planner.tsx | 10 +- src/frontend/app/routes/stops-$id.tsx | 7 +- src/frontend/public/maps/spritesheet/sprite.json | 30 +- src/frontend/public/maps/spritesheet/sprite.png | Bin 3426 -> 5926 bytes .../public/maps/spritesheet/sprite@2x.json | 30 +- src/frontend/public/maps/spritesheet/sprite@2x.png | Bin 7465 -> 13514 bytes 25 files changed, 827 insertions(+), 818 deletions(-) delete mode 100644 src/frontend/app/components/RegionSelector.tsx delete mode 100644 src/frontend/app/components/StopSummarySheet.css delete mode 100644 src/frontend/app/components/StopSummarySheet.tsx delete mode 100644 src/frontend/app/components/StopSummarySheetSkeleton.tsx create mode 100644 src/frontend/app/components/map/StopSummarySheet.css create mode 100644 src/frontend/app/components/map/StopSummarySheet.tsx create mode 100644 src/frontend/app/components/map/StopSummarySheetSkeleton.tsx delete mode 100644 src/frontend/app/config/RegionConfig.ts create mode 100644 src/frontend/app/config/constants.ts (limited to 'src/frontend') diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx index 2102ad7..8ff7de2 100644 --- a/src/frontend/app/AppContext.tsx +++ b/src/frontend/app/AppContext.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-refresh/only-export-components */ import { type ReactNode } from "react"; -import { type RegionId } from "./config/RegionConfig"; +import { type RegionId } from "./config/constants"; import { MapProvider, useMap } from "./contexts/MapContext"; import { SettingsProvider, diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx index 8bbeb20..5d85c60 100644 --- a/src/frontend/app/components/LineIcon.tsx +++ b/src/frontend/app/components/LineIcon.tsx @@ -4,9 +4,16 @@ import "./LineIcon.css"; interface LineIconProps { line: string; mode?: "rounded" | "pill" | "default"; + colour?: string; + textColour?: string; } -const LineIcon: React.FC = ({ line, mode = "default" }) => { +const LineIcon: React.FC = ({ + line, + mode = "default", + colour, + textColour, +}) => { const actualLine = useMemo(() => { return line.trim().replace("510", "NAD"); }, [line]); @@ -15,16 +22,26 @@ const LineIcon: React.FC = ({ line, mode = "default" }) => { return /^[a-zA-Z]/.test(actualLine) ? actualLine : `L${actualLine}`; }, [actualLine]); - const cssVarName = `--line-${formattedLine.toLowerCase()}`; - const cssTextVarName = `--line-${formattedLine.toLowerCase()}-text`; + const actualLineColour = useMemo(() => { + const actualColour = colour?.startsWith("#") ? colour : `#${colour}`; + return colour ? actualColour : `var(--line-${formattedLine.toLowerCase()})`; + }, [formattedLine]); + const actualTextColour = useMemo(() => { + const actualTextColour = textColour?.startsWith("#") + ? textColour + : `#${textColour}`; + return textColour + ? actualTextColour + : `var(--line-${formattedLine.toLowerCase()}-text, #000000)`; + }, [formattedLine]); return ( diff --git a/src/frontend/app/components/PlannerOverlay.tsx b/src/frontend/app/components/PlannerOverlay.tsx index 12cfb0f..af71e48 100644 --- a/src/frontend/app/components/PlannerOverlay.tsx +++ b/src/frontend/app/components/PlannerOverlay.tsx @@ -8,7 +8,6 @@ import React, { } from "react"; import { useTranslation } from "react-i18next"; import PlaceListItem from "~/components/PlaceListItem"; -import { REGION_DATA } from "~/config/RegionConfig"; import { reverseGeocode, searchPlaces, @@ -59,7 +58,7 @@ export const PlannerOverlay: React.FC = ({ [] ); const [recentPlaces, setRecentPlaces] = useState([]); - const RECENT_KEY = `recentPlaces_${REGION_DATA.id}`; + const RECENT_KEY = `recentPlaces`; const clearRecentPlaces = useCallback(() => { setRecentPlaces([]); try { diff --git a/src/frontend/app/components/RegionSelector.tsx b/src/frontend/app/components/RegionSelector.tsx deleted file mode 100644 index 124b574..0000000 --- a/src/frontend/app/components/RegionSelector.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useApp } from "../AppContext"; -import { getAvailableRegions } from "../config/RegionConfig"; -import "./RegionSelector.css"; - -export function RegionSelector() { - const { region, setRegion } = useApp(); - const regions = getAvailableRegions(); - - const handleRegionChange = (e: React.ChangeEvent) => { - const newRegion = e.target.value as any; - setRegion(newRegion); - }; - - return ( -
- - -
- ); -} diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx index 1cb6d88..bb6a3fa 100644 --- a/src/frontend/app/components/StopMapModal.tsx +++ b/src/frontend/app/components/StopMapModal.tsx @@ -9,7 +9,7 @@ import React, { import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre"; import { Sheet } from "react-modal-sheet"; import { useApp } from "~/AppContext"; -import { REGION_DATA } from "~/config/RegionConfig"; +import { APP_CONSTANTS } from "~/config/constants"; import { getLineColour } from "~/data/LineColors"; import type { Stop } from "~/data/StopDataProvider"; import { loadStyle } from "~/maps/styleloader"; @@ -243,7 +243,7 @@ export const StopMapModal: React.FC = ({ !selectedBus || !selectedBus.schedule?.shapeId || selectedBus.currentPosition?.shapeIndex === undefined || - !REGION_DATA.shapeEndpoint + !APP_CONSTANTS.shapeEndpoint ) { setShapeData(null); setPreviousShapeData(null); @@ -263,7 +263,7 @@ export const StopMapModal: React.FC = ({ sLat?: number, sLon?: number ) => { - let url = `${REGION_DATA.shapeEndpoint}?shapeId=${sId}`; + let url = `${APP_CONSTANTS.shapeEndpoint}?shapeId=${sId}`; if (bIndex !== undefined) url += `&busShapeIndex=${bIndex}`; if (sIndex !== undefined) url += `&stopShapeIndex=${sIndex}`; else if (sLat && sLon) url += `&stopLat=${sLat}&stopLon=${sLon}`; diff --git a/src/frontend/app/components/StopSummarySheet.css b/src/frontend/app/components/StopSummarySheet.css deleted file mode 100644 index 5869d41..0000000 --- a/src/frontend/app/components/StopSummarySheet.css +++ /dev/null @@ -1,309 +0,0 @@ -/* Stop Sheet Styles */ -.react-modal-sheet-container { - background-color: var(--background-color) !important; - touch-action: none; -} - -/*.react-modal-sheet-content > * > *:not(.stop-sheet-actions){ - interactivity: inert; -}*/ - -.react-modal-sheet-content-scroller { - overscroll-behavior-y: unset !important; - overflow-y: unset !important; -} - -.stop-sheet-content { - padding: 16px; - display: flex; - flex-direction: column; - /* overflow: hidden; */ - touch-action: pan-y; -} - -.stop-sheet-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 16px; -} - -.stop-sheet-title { - font-size: 1.5rem; - font-weight: 600; - color: var(--text-color); - margin: 0; -} - -.stop-sheet-id { - font-size: 1rem; - color: var(--subtitle-color); -} - -.stop-sheet-lines-container { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.stop-sheet-lines-container.scrollable { - display: grid; - grid-template-rows: repeat(2, 1fr); - grid-auto-flow: column; - /* align-content: flex-start; */ - scrollbar-width: thin; - gap: 0.5rem 1rem; - overflow-x: scroll; -} - -.stop-sheet-lines-container.scrollable::-webkit-scrollbar { - height: 6px; -} - -.stop-sheet-lines-container.scrollable::-webkit-scrollbar-thumb { - background-color: var(--border-color); - border-radius: 3px; -} - -.stop-sheet-line-icon { - flex-shrink: 0; -} - -.stop-sheet-loading { - display: flex; - justify-content: center; - align-items: center; - padding: 32px; - color: var(--subtitle-color); - font-size: 1rem; -} - -.stop-sheet-estimates { - flex: 1; - min-height: 0; - margin-block-start: 1.25rem; -} - -.stop-sheet-subtitle { - font-size: 1.1rem; - font-weight: 500; - color: var(--text-color); - margin: 0 0 12px 0; -} - -.stop-sheet-no-estimates { - text-align: center; - padding: 32px 16px; - color: var(--subtitle-color); - font-size: 0.95rem; -} - -.stop-sheet-estimates-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.stop-sheet-estimate-item { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - background-color: var(--message-background-color); - border-radius: 8px; - border: 1px solid var(--border-color); -} - -.stop-sheet-estimate-line { - flex-shrink: 0; -} - -.stop-sheet-estimate-details { - flex: 1; - min-width: 0; -} - -.stop-sheet-estimate-route { - font-weight: 500; - color: var(--text-color); - font-size: 0.95rem; - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.stop-sheet-estimate-arrival { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - flex-shrink: 0; -} - -.stop-sheet-estimate-time { - display: flex; - align-items: center; - gap: 6px; - font-size: 1.05rem; - font-weight: 600; - color: var(--text-color); -} - -.stop-sheet-estimate-time.is-minutes { - color: #22c55e; -} - -.stop-sheet-estimate-time svg { - width: 18px; - height: 18px; - color: var(--subtitle-color); - flex-shrink: 0; -} - -.stop-sheet-estimate-time.is-minutes svg { - color: #22c55e; -} - -.stop-sheet-estimate-distance { - font-size: 0.75rem; - color: var(--subtitle-color); - text-align: right; -} - -.stop-sheet-footer { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin: 0; - padding: 0.75rem 16px 1rem 16px; - border-top: 1px solid var(--border-color); - background-color: var(--background-color); - z-index: 10; -} - -.stop-sheet-timestamp { - font-size: 0.8rem; - color: var(--subtitle-color); - text-align: center; -} - -.stop-sheet-actions { - display: flex; - gap: 0.75rem; -} - -.stop-sheet-reload { - display: inline-flex; - align-items: center; - gap: 0.4rem; - background: transparent; - border: 1px solid var(--border-color); - color: var(--text-color); - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-size: 0.85rem; - cursor: pointer; - transition: all 0.2s ease; - flex: 1; - justify-content: center; -} - -.stop-sheet-reload:hover:not(:disabled) { - background: var(--message-background-color); - border-color: var(--button-background-color); -} - -.stop-sheet-reload:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.reload-icon { - width: 14px; - height: 14px; - transition: transform 0.5s ease; -} - -.reload-icon.spinning { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -.stop-sheet-view-all { - display: block; - padding: 0.5rem 0.75rem; - background-color: var(--button-background-color); - color: white; - text-decoration: none; - text-align: center; - border-radius: 6px; - font-weight: 500; - font-size: 0.85rem; - transition: background-color 0.2s ease; - flex: 2; -} - -.stop-sheet-view-all:hover { - background-color: var(--button-hover-background-color); - text-decoration: none; -} - -/* Error display adjustments for sheet */ -.stop-sheet-content .error-display { - margin: 1rem 0; -} - -.stop-sheet-content .error-display.compact { - min-height: 100px; - padding: 1rem; -} - -.stop-sheet-content .error-display.compact .error-icon { - width: 28px; - height: 28px; -} - -.stop-sheet-content .error-display.compact .error-title { - font-size: 1.1rem; -} - -.stop-sheet-content .error-display.compact .error-message { - font-size: 0.85rem; -} - -[data-rsbs-overlay] { - background-color: rgba(0, 0, 0, 0.3); -} - -[data-rsbs-header] { - background-color: var(--background-color); - border-bottom: 1px solid var(--border-color); - touch-action: none; -} - -[data-rsbs-header]:before { - background-color: var(--subtitle-color); -} - -[data-rsbs-root] [data-rsbs-overlay] { - border-top-left-radius: 16px; - border-top-right-radius: 16px; -} - -[data-rsbs-root] [data-rsbs-content] { - background-color: var(--background-color); - border-top-left-radius: 16px; - border-top-right-radius: 16px; - max-height: 95vh; - overflow: hidden; - touch-action: none; -} diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/StopSummarySheet.tsx deleted file mode 100644 index c2d6ffe..0000000 --- a/src/frontend/app/components/StopSummarySheet.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { RefreshCw } from "lucide-react"; -import React, { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Sheet } from "react-modal-sheet"; -import { Link } from "react-router"; -import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList"; -import { REGION_DATA } from "~/config/RegionConfig"; -import type { Stop } from "~/data/StopDataProvider"; -import { type ConsolidatedCirculation } from "../routes/stops-$id"; -import { ErrorDisplay } from "./ErrorDisplay"; -import LineIcon from "./LineIcon"; -import { StopAlert } from "./StopAlert"; -import "./StopSummarySheet.css"; -import { StopSummarySheetSkeleton } from "./StopSummarySheetSkeleton"; - -interface StopSheetProps { - isOpen: boolean; - onClose: () => void; - stop: Stop; -} - -interface ErrorInfo { - type: "network" | "server" | "unknown"; - status?: number; - message?: string; -} - -const loadConsolidatedData = async ( - stopId: string -): Promise => { - const resp = await fetch( - `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`, - { - headers: { - Accept: "application/json", - }, - } - ); - - if (!resp.ok) { - throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); - } - - return await resp.json(); -}; - -export const StopSheet: React.FC = ({ - isOpen, - onClose, - stop, -}) => { - const { t } = useTranslation(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [lastUpdated, setLastUpdated] = useState(null); - - const parseError = (error: any): ErrorInfo => { - if (!navigator.onLine) { - return { type: "network", message: "No internet connection" }; - } - - if ( - error.message?.includes("Failed to fetch") || - error.message?.includes("NetworkError") - ) { - return { type: "network" }; - } - - if (error.message?.includes("HTTP")) { - const statusMatch = error.message.match(/HTTP (\d+):/); - const status = statusMatch ? parseInt(statusMatch[1]) : undefined; - return { type: "server", status }; - } - - return { type: "unknown", message: error.message }; - }; - - const loadData = async () => { - try { - setLoading(true); - setError(null); - setData(null); - - const stopData = await loadConsolidatedData(stop.stopId); - setData(stopData); - setLastUpdated(new Date()); - } catch (err) { - console.error("Failed to load stop data:", err); - setError(parseError(err)); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (isOpen && stop.stopId) { - loadData(); - } - }, [isOpen, stop.stopId]); - - // Show only the next 4 arrivals - const sortedData = data - ? [...data].sort( - (a, b) => - (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - - (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) - ) - : []; - const limitedEstimates = sortedData.slice(0, 4); - - return ( - - - - -
-
-

{stop.name.original}

- ({stop.stopId}) -
- -
= 10 ? "scrollable" : ""}`} - > - {stop.lines.map((line) => ( -
- -
- ))} -
- - - - {loading ? ( - - ) : error ? ( - - ) : data ? ( - <> -
-

- {t("estimates.next_arrivals", "Next arrivals")} -

- - {limitedEstimates.length === 0 ? ( -
- {t("estimates.none", "No hay estimaciones disponibles")} -
- ) : ( - - )} -
- - ) : null} -
-
- -
- {lastUpdated && ( -
- {t("estimates.last_updated", "Actualizado a las")}{" "} - {lastUpdated.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - })} -
- )} - -
- - - - {t("map.view_all_estimates", "Ver todas las estimaciones")} - -
-
-
- -
- ); -}; diff --git a/src/frontend/app/components/StopSummarySheetSkeleton.tsx b/src/frontend/app/components/StopSummarySheetSkeleton.tsx deleted file mode 100644 index 7697efc..0000000 --- a/src/frontend/app/components/StopSummarySheetSkeleton.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; -import "react-loading-skeleton/dist/skeleton.css"; - -interface StopSheetSkeletonProps { - rows?: number; -} - -export const StopSummarySheetSkeleton: React.FC = ({ - rows = 4, -}) => { - const { t } = useTranslation(); - - return ( - -
-

- {t("estimates.next_arrivals", "Next arrivals")} -

- -
- {Array.from({ length: rows }, (_, index) => ( -
-
- -
- -
-
- -
-
- -
-
-
- ))} -
-
- -
-
- -
- -
-
- -
- -
- -
-
-
-
- ); -}; diff --git a/src/frontend/app/components/map/StopSummarySheet.css b/src/frontend/app/components/map/StopSummarySheet.css new file mode 100644 index 0000000..5869d41 --- /dev/null +++ b/src/frontend/app/components/map/StopSummarySheet.css @@ -0,0 +1,309 @@ +/* Stop Sheet Styles */ +.react-modal-sheet-container { + background-color: var(--background-color) !important; + touch-action: none; +} + +/*.react-modal-sheet-content > * > *:not(.stop-sheet-actions){ + interactivity: inert; +}*/ + +.react-modal-sheet-content-scroller { + overscroll-behavior-y: unset !important; + overflow-y: unset !important; +} + +.stop-sheet-content { + padding: 16px; + display: flex; + flex-direction: column; + /* overflow: hidden; */ + touch-action: pan-y; +} + +.stop-sheet-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.stop-sheet-title { + font-size: 1.5rem; + font-weight: 600; + color: var(--text-color); + margin: 0; +} + +.stop-sheet-id { + font-size: 1rem; + color: var(--subtitle-color); +} + +.stop-sheet-lines-container { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.stop-sheet-lines-container.scrollable { + display: grid; + grid-template-rows: repeat(2, 1fr); + grid-auto-flow: column; + /* align-content: flex-start; */ + scrollbar-width: thin; + gap: 0.5rem 1rem; + overflow-x: scroll; +} + +.stop-sheet-lines-container.scrollable::-webkit-scrollbar { + height: 6px; +} + +.stop-sheet-lines-container.scrollable::-webkit-scrollbar-thumb { + background-color: var(--border-color); + border-radius: 3px; +} + +.stop-sheet-line-icon { + flex-shrink: 0; +} + +.stop-sheet-loading { + display: flex; + justify-content: center; + align-items: center; + padding: 32px; + color: var(--subtitle-color); + font-size: 1rem; +} + +.stop-sheet-estimates { + flex: 1; + min-height: 0; + margin-block-start: 1.25rem; +} + +.stop-sheet-subtitle { + font-size: 1.1rem; + font-weight: 500; + color: var(--text-color); + margin: 0 0 12px 0; +} + +.stop-sheet-no-estimates { + text-align: center; + padding: 32px 16px; + color: var(--subtitle-color); + font-size: 0.95rem; +} + +.stop-sheet-estimates-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.stop-sheet-estimate-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background-color: var(--message-background-color); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.stop-sheet-estimate-line { + flex-shrink: 0; +} + +.stop-sheet-estimate-details { + flex: 1; + min-width: 0; +} + +.stop-sheet-estimate-route { + font-weight: 500; + color: var(--text-color); + font-size: 0.95rem; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stop-sheet-estimate-arrival { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + flex-shrink: 0; +} + +.stop-sheet-estimate-time { + display: flex; + align-items: center; + gap: 6px; + font-size: 1.05rem; + font-weight: 600; + color: var(--text-color); +} + +.stop-sheet-estimate-time.is-minutes { + color: #22c55e; +} + +.stop-sheet-estimate-time svg { + width: 18px; + height: 18px; + color: var(--subtitle-color); + flex-shrink: 0; +} + +.stop-sheet-estimate-time.is-minutes svg { + color: #22c55e; +} + +.stop-sheet-estimate-distance { + font-size: 0.75rem; + color: var(--subtitle-color); + text-align: right; +} + +.stop-sheet-footer { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin: 0; + padding: 0.75rem 16px 1rem 16px; + border-top: 1px solid var(--border-color); + background-color: var(--background-color); + z-index: 10; +} + +.stop-sheet-timestamp { + font-size: 0.8rem; + color: var(--subtitle-color); + text-align: center; +} + +.stop-sheet-actions { + display: flex; + gap: 0.75rem; +} + +.stop-sheet-reload { + display: inline-flex; + align-items: center; + gap: 0.4rem; + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-color); + padding: 0.5rem 0.75rem; + border-radius: 6px; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; + flex: 1; + justify-content: center; +} + +.stop-sheet-reload:hover:not(:disabled) { + background: var(--message-background-color); + border-color: var(--button-background-color); +} + +.stop-sheet-reload:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.reload-icon { + width: 14px; + height: 14px; + transition: transform 0.5s ease; +} + +.reload-icon.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.stop-sheet-view-all { + display: block; + padding: 0.5rem 0.75rem; + background-color: var(--button-background-color); + color: white; + text-decoration: none; + text-align: center; + border-radius: 6px; + font-weight: 500; + font-size: 0.85rem; + transition: background-color 0.2s ease; + flex: 2; +} + +.stop-sheet-view-all:hover { + background-color: var(--button-hover-background-color); + text-decoration: none; +} + +/* Error display adjustments for sheet */ +.stop-sheet-content .error-display { + margin: 1rem 0; +} + +.stop-sheet-content .error-display.compact { + min-height: 100px; + padding: 1rem; +} + +.stop-sheet-content .error-display.compact .error-icon { + width: 28px; + height: 28px; +} + +.stop-sheet-content .error-display.compact .error-title { + font-size: 1.1rem; +} + +.stop-sheet-content .error-display.compact .error-message { + font-size: 0.85rem; +} + +[data-rsbs-overlay] { + background-color: rgba(0, 0, 0, 0.3); +} + +[data-rsbs-header] { + background-color: var(--background-color); + border-bottom: 1px solid var(--border-color); + touch-action: none; +} + +[data-rsbs-header]:before { + background-color: var(--subtitle-color); +} + +[data-rsbs-root] [data-rsbs-overlay] { + border-top-left-radius: 16px; + border-top-right-radius: 16px; +} + +[data-rsbs-root] [data-rsbs-content] { + background-color: var(--background-color); + border-top-left-radius: 16px; + border-top-right-radius: 16px; + max-height: 95vh; + overflow: hidden; + touch-action: none; +} diff --git a/src/frontend/app/components/map/StopSummarySheet.tsx b/src/frontend/app/components/map/StopSummarySheet.tsx new file mode 100644 index 0000000..b24e71c --- /dev/null +++ b/src/frontend/app/components/map/StopSummarySheet.tsx @@ -0,0 +1,220 @@ +import { RefreshCw } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Sheet } from "react-modal-sheet"; +import { Link } from "react-router"; +import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList"; +import { APP_CONSTANTS } from "~/config/constants"; +import { type ConsolidatedCirculation } from "../../routes/stops-$id"; +import { ErrorDisplay } from "../ErrorDisplay"; +import LineIcon from "../LineIcon"; +import "./StopSummarySheet.css"; +import { StopSummarySheetSkeleton } from "./StopSummarySheetSkeleton"; + +export interface StopSheetProps { + isOpen: boolean; + onClose: () => void; + stop: { + stopId: string; + stopCode?: string; + stopFeed?: string; + name: string; + lines: { + line: string; + colour?: string; + textColour?: string; + }[]; + }; +} + +interface ErrorInfo { + type: "network" | "server" | "unknown"; + status?: number; + message?: string; +} + +const loadConsolidatedData = async ( + stopId: string +): Promise => { + const resp = await fetch( + `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`, + { + headers: { + Accept: "application/json", + }, + } + ); + + if (!resp.ok) { + throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); + } + + return await resp.json(); +}; + +export const StopSheet: React.FC = ({ + isOpen, + onClose, + stop, +}) => { + const { t } = useTranslation(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(null); + + const parseError = (error: any): ErrorInfo => { + if (!navigator.onLine) { + return { type: "network", message: "No internet connection" }; + } + + if ( + error.message?.includes("Failed to fetch") || + error.message?.includes("NetworkError") + ) { + return { type: "network" }; + } + + if (error.message?.includes("HTTP")) { + const statusMatch = error.message.match(/HTTP (\d+):/); + const status = statusMatch ? parseInt(statusMatch[1]) : undefined; + return { type: "server", status }; + } + + return { type: "unknown", message: error.message }; + }; + + const loadData = async () => { + try { + setLoading(true); + setError(null); + setData(null); + + const stopData = await loadConsolidatedData(stop.stopId); + setData(stopData); + setLastUpdated(new Date()); + } catch (err) { + console.error("Failed to load stop data:", err); + setError(parseError(err)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isOpen && stop.stopId) { + loadData(); + } + }, [isOpen, stop.stopId]); + + // Show only the next 4 arrivals + const sortedData = data + ? [...data].sort( + (a, b) => + (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) + ) + : []; + const limitedEstimates = sortedData.slice(0, 4); + + return ( + + + + +
+
+

{stop.name}

+ ({stop.stopCode}) +
+ +
+ {stop.lines.map((lineObj) => ( + + ))} +
+ + {/* TODO: Enable stop alerts when available */} + {/**/} + + {loading ? ( + + ) : error ? ( + + ) : data ? ( + <> +
+

+ {t("estimates.next_arrivals", "Next arrivals")} +

+ + {limitedEstimates.length === 0 ? ( +
+ {t("estimates.none", "No hay estimaciones disponibles")} +
+ ) : ( + + )} +
+ + ) : null} +
+
+ +
+ {lastUpdated && ( +
+ {t("estimates.last_updated", "Actualizado a las")}{" "} + {lastUpdated.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + })} +
+ )} + +
+ + + + {t("map.view_all_estimates", "Ver todas las estimaciones")} + +
+
+
+ +
+ ); +}; diff --git a/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx new file mode 100644 index 0000000..7697efc --- /dev/null +++ b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; + +interface StopSheetSkeletonProps { + rows?: number; +} + +export const StopSummarySheetSkeleton: React.FC = ({ + rows = 4, +}) => { + const { t } = useTranslation(); + + return ( + +
+

+ {t("estimates.next_arrivals", "Next arrivals")} +

+ +
+ {Array.from({ length: rows }, (_, index) => ( +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+ ); +}; diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts deleted file mode 100644 index d595b3f..0000000 --- a/src/frontend/app/config/RegionConfig.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { LngLatLike } from "maplibre-gl"; - -export type RegionId = "vigo"; - -export interface RegionData { - id: RegionId; - name: string; - stopsEndpoint: string; - estimatesEndpoint: string; - consolidatedCirculationsEndpoint: string; - timetableEndpoint: string; - shapeEndpoint: string; - defaultCenter: LngLatLike; - bounds: { - sw: LngLatLike; - ne: LngLatLike; - }; - textColour: string; - defaultZoom: number; - showMeters: boolean; -} - -export const REGION_DATA: RegionData = { - id: "vigo", - name: "Vigo", - stopsEndpoint: "/stops/vigo.json", - estimatesEndpoint: "/api/vigo/GetStopEstimates", - consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations", - timetableEndpoint: "/api/vigo/GetStopTimetable", - shapeEndpoint: "/api/vigo/GetShape", - defaultCenter: { - lat: 42.229188855975046, - lng: -8.72246955783102, - } as LngLatLike, - bounds: { - sw: [-8.951059, 42.098923] as LngLatLike, - ne: [-8.447748, 42.3496] as LngLatLike, - }, - textColour: "#e72b37", - defaultZoom: 14, - showMeters: true, -}; - -export const getAvailableRegions = (): RegionData[] => [REGION_DATA]; diff --git a/src/frontend/app/config/constants.ts b/src/frontend/app/config/constants.ts new file mode 100644 index 0000000..9a0fdd1 --- /dev/null +++ b/src/frontend/app/config/constants.ts @@ -0,0 +1,22 @@ +import type { LngLatLike } from "maplibre-gl"; + +export type RegionId = "vigo"; + +export const APP_CONSTANTS = { + id: "vigo", + + stopsEndpoint: "/stops/vigo.json", + consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations", + shapeEndpoint: "/api/vigo/GetShape", + defaultCenter: { + lat: 42.229188855975046, + lng: -8.72246955783102, + } as LngLatLike, + bounds: { + sw: [-9.629517, 41.463312] as LngLatLike, + ne: [-6.289673, 43.711564] as LngLatLike, + }, + textColour: "#e72b37", + defaultZoom: 14, + showMeters: true, +}; diff --git a/src/frontend/app/contexts/MapContext.tsx b/src/frontend/app/contexts/MapContext.tsx index af13bb7..db1392c 100644 --- a/src/frontend/app/contexts/MapContext.tsx +++ b/src/frontend/app/contexts/MapContext.tsx @@ -6,7 +6,7 @@ import { useState, type ReactNode, } from "react"; -import { REGION_DATA } from "~/config/RegionConfig"; +import { APP_CONSTANTS } from "~/config/constants"; interface MapState { center: LngLatLike; @@ -36,8 +36,8 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { // We might want to ensure we have a fallback if the region changed while the app was closed? // But for now, let's stick to the existing logic. return { - center: parsed.center || REGION_DATA.defaultCenter, - zoom: parsed.zoom || REGION_DATA.defaultZoom, + center: parsed.center || APP_CONSTANTS.defaultCenter, + zoom: parsed.zoom || APP_CONSTANTS.defaultZoom, userLocation: parsed.userLocation || null, hasLocationPermission: parsed.hasLocationPermission || false, }; @@ -46,8 +46,8 @@ export const MapProvider = ({ children }: { children: ReactNode }) => { } } return { - center: REGION_DATA.defaultCenter, - zoom: REGION_DATA.defaultZoom, + center: APP_CONSTANTS.defaultCenter, + zoom: APP_CONSTANTS.defaultZoom, userLocation: null, hasLocationPermission: false, }; diff --git a/src/frontend/app/contexts/SettingsContext.tsx b/src/frontend/app/contexts/SettingsContext.tsx index 5f6ff46..d66ee52 100644 --- a/src/frontend/app/contexts/SettingsContext.tsx +++ b/src/frontend/app/contexts/SettingsContext.tsx @@ -5,7 +5,7 @@ import { useState, type ReactNode, } from "react"; -import { APP_CONFIG } from "../config/AppConfig"; +import { APP_CONFIG } from "~/config/AppConfig"; export type Theme = "light" | "dark" | "system"; export type TableStyle = "regular" | "grouped" | "experimental_consolidated"; diff --git a/src/frontend/app/data/SpecialPlacesProvider.ts b/src/frontend/app/data/SpecialPlacesProvider.ts index 2e3be68..d11b119 100644 --- a/src/frontend/app/data/SpecialPlacesProvider.ts +++ b/src/frontend/app/data/SpecialPlacesProvider.ts @@ -1,5 +1,3 @@ -import { REGION_DATA } from "~/config/RegionConfig"; - export interface SpecialPlace { name: string; type: "stop" | "address"; @@ -9,8 +7,8 @@ export interface SpecialPlace { longitude?: number; } -const STORAGE_KEY_HOME = `specialPlace_home_${REGION_DATA.id}`; -const STORAGE_KEY_WORK = `specialPlace_work_${REGION_DATA.id}`; +const STORAGE_KEY_HOME = `specialPlace_home`; +const STORAGE_KEY_WORK = `specialPlace_work`; function getHome(): SpecialPlace | null { try { diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts index e523bd1..7bab10c 100644 --- a/src/frontend/app/data/StopDataProvider.ts +++ b/src/frontend/app/data/StopDataProvider.ts @@ -1,19 +1,13 @@ -import { REGION_DATA } from "~/config/RegionConfig"; +import { APP_CONSTANTS } from "~/config/constants"; export interface CachedStopList { timestamp: number; data: Stop[]; } -export type StopName = { - original: string; - intersect?: string; -}; - export interface Stop { stopId: string; - type?: "bus" | "train"; - name: StopName; + name: string; latitude?: number; longitude?: number; lines: string[]; @@ -41,13 +35,13 @@ function normalizeId(id: number | string): string { // Initialize cachedStops and customNames once per region async function initStops() { - if (!cachedStopsByRegion[REGION_DATA.id]) { - const response = await fetch(REGION_DATA.stopsEndpoint); + if (!cachedStopsByRegion[APP_CONSTANTS.id]) { + const response = await fetch(APP_CONSTANTS.stopsEndpoint); const rawStops = (await response.json()) as any[]; // build array and map - stopsMapByRegion[REGION_DATA.id] = {}; - cachedStopsByRegion[REGION_DATA.id] = rawStops.map((raw) => { + stopsMapByRegion[APP_CONSTANTS.id] = {}; + cachedStopsByRegion[APP_CONSTANTS.id] = rawStops.map((raw) => { const id = normalizeId(raw.stopId); const entry = { ...raw, @@ -55,21 +49,23 @@ async function initStops() { type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"), favourite: false, } as Stop; - stopsMapByRegion[REGION_DATA.id][id] = entry; + stopsMapByRegion[APP_CONSTANTS.id][id] = entry; return entry; }); // load custom names - const rawCustom = localStorage.getItem(`customStopNames_${REGION_DATA.id}`); + const rawCustom = localStorage.getItem( + `customStopNames_${APP_CONSTANTS.id}` + ); if (rawCustom) { const parsed = JSON.parse(rawCustom); const normalized: Record = {}; for (const [key, value] of Object.entries(parsed)) { normalized[normalizeId(key)] = value as string; } - customNamesByRegion[REGION_DATA.id] = normalized; + customNamesByRegion[APP_CONSTANTS.id] = normalized; } else { - customNamesByRegion[REGION_DATA.id] = {}; + customNamesByRegion[APP_CONSTANTS.id] = {}; } } } @@ -92,9 +88,9 @@ async function getStops(): Promise { async function getStopById(stopId: string | number): Promise { await initStops(); const id = normalizeId(stopId); - const stop = stopsMapByRegion[REGION_DATA.id]?.[id]; + const stop = stopsMapByRegion[APP_CONSTANTS.id]?.[id]; if (stop) { - const rawFav = localStorage.getItem(`favouriteStops_${REGION_DATA.id}`); + const rawFav = localStorage.getItem(`favouriteStops_${APP_CONSTANTS.id}`); const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId) : []; @@ -105,32 +101,29 @@ async function getStopById(stopId: string | number): Promise { // Updated display name to include custom names function getDisplayName(stop: Stop): string { - const customNames = customNamesByRegion[REGION_DATA.id] || {}; - if (customNames[stop.stopId]) return customNames[stop.stopId]; - const nameObj = stop.name; - return nameObj.intersect || nameObj.original; + return stop.name; } // New: set or remove custom names function setCustomName(stopId: string | number, label: string) { const id = normalizeId(stopId); - if (!customNamesByRegion[REGION_DATA.id]) { - customNamesByRegion[REGION_DATA.id] = {}; + if (!customNamesByRegion[APP_CONSTANTS.id]) { + customNamesByRegion[APP_CONSTANTS.id] = {}; } - customNamesByRegion[REGION_DATA.id][id] = label; + customNamesByRegion[APP_CONSTANTS.id][id] = label; localStorage.setItem( - `customStopNames_${REGION_DATA.id}`, - JSON.stringify(customNamesByRegion[REGION_DATA.id]) + `customStopNames_${APP_CONSTANTS.id}`, + JSON.stringify(customNamesByRegion[APP_CONSTANTS.id]) ); } function removeCustomName(stopId: string | number) { const id = normalizeId(stopId); - if (customNamesByRegion[REGION_DATA.id]?.[id]) { - delete customNamesByRegion[REGION_DATA.id][id]; + if (customNamesByRegion[APP_CONSTANTS.id]?.[id]) { + delete customNamesByRegion[APP_CONSTANTS.id][id]; localStorage.setItem( - `customStopNames_${REGION_DATA.id}`, - JSON.stringify(customNamesByRegion[REGION_DATA.id]) + `customStopNames_${APP_CONSTANTS.id}`, + JSON.stringify(customNamesByRegion[APP_CONSTANTS.id]) ); } } @@ -138,7 +131,7 @@ function removeCustomName(stopId: string | number) { // New: get custom label for a stop function getCustomName(stopId: string | number): string | undefined { const id = normalizeId(stopId); - return customNamesByRegion[REGION_DATA.id]?.[id]; + return customNamesByRegion[APP_CONSTANTS.id]?.[id]; } function addFavourite(stopId: string | number) { @@ -231,7 +224,7 @@ function getFavouriteIds(): string[] { // New function to load stops from network async function loadStopsFromNetwork(): Promise { - const response = await fetch(REGION_DATA.stopsEndpoint); + const response = await fetch(APP_CONSTANTS.stopsEndpoint); const rawStops = (await response.json()) as any[]; return rawStops.map((raw) => { const id = normalizeId(raw.stopId); @@ -244,6 +237,10 @@ async function loadStopsFromNetwork(): Promise { }); } +function getTileUrlTemplate(): string { + return window.location.origin + "/api/tiles/stops/{z}/{x}/{y}"; +} + export default { getStops, getStopById, @@ -258,4 +255,5 @@ export default { getRecent, getFavouriteIds, loadStopsFromNetwork, + getTileUrlTemplate, }; diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx index e97659a..36565bd 100644 --- a/src/frontend/app/routes/home.tsx +++ b/src/frontend/app/routes/home.tsx @@ -31,7 +31,7 @@ export default function StopList() { () => new Fuse(data || [], { threshold: 0.3, - keys: ["name.original", "name.intersect", "stopId"], + keys: ["name", "stopId"], }), [data] ); diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 39fc062..db9de59 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -1,8 +1,7 @@ -import StopDataProvider, { type Stop } from "../data/StopDataProvider"; +import StopDataProvider from "../data/StopDataProvider"; import "./map.css"; import { DEFAULT_STYLE, loadStyle } from "app/maps/styleloader"; -import type { Feature as GeoJsonFeature, Point } from "geojson"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Map, { @@ -16,8 +15,11 @@ import Map, { } from "react-map-gl/maplibre"; import { useNavigate } from "react-router"; import { PlannerOverlay } from "~/components/PlannerOverlay"; -import { StopSheet } from "~/components/StopSummarySheet"; -import { REGION_DATA } from "~/config/RegionConfig"; +import { + StopSheet, + type StopSheetProps, +} from "~/components/map/StopSummarySheet"; +import { APP_CONSTANTS } from "~/config/constants"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { usePlanner } from "~/hooks/usePlanner"; import { useApp } from "../AppContext"; @@ -28,19 +30,9 @@ export default function StopMap() { const { t } = useTranslation(); const navigate = useNavigate(); usePageTitle(t("navbar.map", "Mapa")); - const [stops, setStops] = useState< - GeoJsonFeature< - Point, - { - stopId: string; - name: string; - lines: string[]; - cancelled?: boolean; - prefix: string; - } - >[] - >([]); - const [selectedStop, setSelectedStop] = useState(null); + const [selectedStop, setSelectedStop] = useState< + StopSheetProps["stop"] | null + >(null); const [isSheetOpen, setIsSheetOpen] = useState(false); const { mapState, updateMapState, theme } = useApp(); const mapRef = useRef(null); @@ -63,45 +55,10 @@ export default function StopMap() { return; } const feature = features[0]; - console.debug("Map click feature:", feature); - const props: any = feature.properties; handlePointClick(feature); }; - useEffect(() => { - StopDataProvider.getStops().then((data) => { - const features: GeoJsonFeature< - Point, - { - stopId: string; - name: string; - lines: string[]; - cancelled?: boolean; - prefix: string; - } - >[] = data.map((s) => ({ - type: "Feature", - geometry: { - type: "Point", - coordinates: [s.longitude as number, s.latitude as number], - }, - properties: { - stopId: s.stopId, - name: s.name.original, - lines: s.lines, - cancelled: s.cancelled ?? false, - prefix: s.stopId.startsWith("renfe:") - ? "stop-renfe" - : s.cancelled - ? "stop-vitrasa-cancelled" - : "stop-vitrasa", - }, - })); - setStops(features); - }); - }, []); - useEffect(() => { //const styleName = "carto"; const styleName = "openfreemap"; @@ -166,26 +123,29 @@ export default function StopMap() { const handlePointClick = (feature: any) => { const props: any = feature.properties; - if (!props || !props.stopId) { + // TODO: Move ID to constant, improve type checking + if (!props || feature.layer.id !== "stops") { console.warn("Invalid feature properties:", props); return; } - const stopId = props.stopId; + const stopId = props.id; - // fetch full stop to get lines array - StopDataProvider.getStopById(stopId) - .then((stop) => { - if (!stop) { - console.warn("Stop not found:", stopId); - return; - } - setSelectedStop(stop); - setIsSheetOpen(true); - }) - .catch((err) => { - console.error("Error fetching stop details:", err); - }); + console.debug("Stop clicked:", stopId, props); + + setSelectedStop({ + stopId: props.id, + stopCode: props.code, + name: props.name || "Unknown Stop", + lines: JSON.parse(props.routes || "[]").map((route) => { + return { + line: route.shortName, + colour: route.colour, + textColour: route.textColour, + }; + }), + }); + setIsSheetOpen(true); }; return ( @@ -203,7 +163,7 @@ export default function StopMap() { style={{ width: "100%", height: "100%" }} interactiveLayerIds={["stops", "stops-label"]} onClick={onMapClick} - minZoom={11} + minZoom={5} scrollZoom pitch={0} roll={0} @@ -214,7 +174,7 @@ export default function StopMap() { zoom: mapState.zoom, }} attributionControl={{ compact: false }} - maxBounds={[REGION_DATA.bounds.sw, REGION_DATA.bounds.ne]} + maxBounds={[APP_CONSTANTS.bounds.sw, APP_CONSTANTS.bounds.ne]} > => { const resp = await fetch( - `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`, + `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`, { headers: { Accept: "application/json", @@ -123,8 +123,7 @@ export default function Estimates() { // Helper function to get the display name for the stop const getStopDisplayName = useCallback(() => { if (customName) return customName; - if (stopData?.name.intersect) return stopData.name.intersect; - if (stopData?.name.original) return stopData.name.original; + if (stopData?.name) return stopData.name; return `Parada ${stopId}`; }, [customName, stopData, stopId]); diff --git a/src/frontend/public/maps/spritesheet/sprite.json b/src/frontend/public/maps/spritesheet/sprite.json index 46a525b..1b3a78b 100644 --- a/src/frontend/public/maps/spritesheet/sprite.json +++ b/src/frontend/public/maps/spritesheet/sprite.json @@ -7,17 +7,41 @@ "height": 32, "pixelRatio": 1 }, - "stop-vitrasa-cancelled": { - "id": "stop-vitrasa-cancelled", + "stop-santiago": { + "id": "stop-santiago", "x": 32, "y": 0, "width": 32, "height": 32, "pixelRatio": 1 }, + "stop-coruna": { + "id": "stop-coruna", + "x": 64, + "y": 0, + "width": 32, + "height": 32, + "pixelRatio": 1 + }, + "stop-xunta": { + "id": "stop-xunta", + "x": 96, + "y": 0, + "width": 32, + "height": 32, + "pixelRatio": 1 + }, "stop-renfe": { "id": "stop-renfe", - "x": 64, + "x": 128, + "y": 0, + "width": 32, + "height": 32, + "pixelRatio": 1 + }, + "stop-feve": { + "id": "stop-feve", + "x": 160, "y": 0, "width": 32, "height": 32, diff --git a/src/frontend/public/maps/spritesheet/sprite.png b/src/frontend/public/maps/spritesheet/sprite.png index a63888a..d8a32ab 100644 Binary files a/src/frontend/public/maps/spritesheet/sprite.png and b/src/frontend/public/maps/spritesheet/sprite.png differ diff --git a/src/frontend/public/maps/spritesheet/sprite@2x.json b/src/frontend/public/maps/spritesheet/sprite@2x.json index 02e05d5..d2e89dd 100644 --- a/src/frontend/public/maps/spritesheet/sprite@2x.json +++ b/src/frontend/public/maps/spritesheet/sprite@2x.json @@ -7,17 +7,41 @@ "height": 64, "pixelRatio": 2 }, - "stop-vitrasa-cancelled": { - "id": "stop-vitrasa-cancelled", + "stop-santiago": { + "id": "stop-santiago", "x": 64, "y": 0, "width": 64, "height": 64, "pixelRatio": 2 }, + "stop-coruna": { + "id": "stop-coruna", + "x": 128, + "y": 0, + "width": 64, + "height": 64, + "pixelRatio": 2 + }, + "stop-xunta": { + "id": "stop-xunta", + "x": 192, + "y": 0, + "width": 64, + "height": 64, + "pixelRatio": 2 + }, "stop-renfe": { "id": "stop-renfe", - "x": 128, + "x": 256, + "y": 0, + "width": 64, + "height": 64, + "pixelRatio": 2 + }, + "stop-feve": { + "id": "stop-feve", + "x": 320, "y": 0, "width": 64, "height": 64, diff --git a/src/frontend/public/maps/spritesheet/sprite@2x.png b/src/frontend/public/maps/spritesheet/sprite@2x.png index 5f4b575..ba873a4 100644 Binary files a/src/frontend/public/maps/spritesheet/sprite@2x.png and b/src/frontend/public/maps/spritesheet/sprite@2x.png differ -- cgit v1.3