diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-19 13:06:27 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-19 13:06:27 +0100 |
| commit | 2a9aca302485bc08f5b2dd2a54987de6f80fc338 (patch) | |
| tree | 38171abad21b2952eca6ff9e8534545b4c28ed12 /src | |
| parent | 37cdb0c418a7f2b47e40ae9db7ad86e1fddc86fe (diff) | |
Implement loading stops as tiles from OTP
Diffstat (limited to 'src')
29 files changed, 646 insertions, 321 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs b/src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs new file mode 100644 index 0000000..56f836e --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs @@ -0,0 +1,198 @@ +using Costasdev.Busurbano.Backend.GraphClient; +using Costasdev.Busurbano.Backend.GraphClient.App; +using NetTopologySuite.IO.VectorTiles; +using NetTopologySuite.IO.VectorTiles.Mapbox; + +using Microsoft.AspNetCore.Mvc; + +using Microsoft.Extensions.Caching.Memory; +using NetTopologySuite.Features; +using System.Text.Json; +using Costasdev.Busurbano.Backend.Helpers; + +[ApiController] +[Route("api/tiles")] +public class TileController : ControllerBase +{ + private readonly ILogger<TileController> _logger; + private readonly IMemoryCache _cache; + private readonly HttpClient _httpClient; + + public TileController( + ILogger<TileController> logger, + IMemoryCache cache, + HttpClient httpClient + ) + { + _logger = logger; + _cache = cache; + _httpClient = httpClient; + } + + /* + vitrasa:20223: # Castrelos (Pavillón) - Final U1 + hide: true +vitrasa:20146: # García Barbón 7 - final líneas A y 18A + hide: true +vitrasa:20220: # (Samil) COIA-SAMIL - Final L15A + hide: true +vitrasa:20001: # (Samil) Samil por Beiramar - Final L15B + hide: true +vitrasa:20002: # (Samil) Samil por Torrecedeira - Final L15C + hide: true +vitrasa:20144: # (Samil) Samil por Coia - Final C3D+C3i + hide: true +vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i + hide: true + */ + private static readonly string[] HiddenStops = + [ + "vitrasa:20223", + "vitrasa:20146", + "vitrasa:20220", + "vitrasa:20001", + "vitrasa:20002", + "vitrasa:20144", + "vitrasa:20145" + ]; + + [HttpGet("stops/{z}/{x}/{y}")] + public async Task<IActionResult> GetTrafficTile(int z, int x, int y) + { + var cacheHit = _cache.TryGetValue($"stops-tile-{z}-{x}-{y}", out byte[]? cachedTile); + if (cacheHit && cachedTile != null) + { + Response.Headers.Append("X-Cache-Hit", "true"); + return File(cachedTile, "application/x-protobuf"); + } + + // Calculate bounding box in EPSG:4326 + double n = Math.Pow(2, z); + double lonMin = x / n * 360.0 - 180.0; + double lonMax = (x + 1) / n * 360.0 - 180.0; + + double latMaxRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n))); + double latMax = latMaxRad * 180.0 / Math.PI; + + double latMinRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n))); + double latMin = latMinRad * 180.0 / Math.PI; + + var requestContent = StopTileRequestContent.Query(new StopTileRequestContent.Bbox(lonMin, latMin, lonMax, latMax)); + var request = new HttpRequestMessage(HttpMethod.Post, "http://100.67.54.115:3957/otp/gtfs/v1"); + request.Content = JsonContent.Create(new GraphClientRequest + { + Query = requestContent + }); + + var response = await _httpClient.SendAsync(request); + var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>(); + + if (responseBody == null || !responseBody.IsSuccess) + { + _logger.LogError("Error fetching stop data: {StatusCode}", response.StatusCode); + _logger.LogError("Sent request: {RequestContent}", await request.Content.ReadAsStringAsync()); + _logger.LogError("Response body: {ResponseBody}", await response.Content.ReadAsStringAsync()); + return StatusCode(500, "Error fetching stop data"); + } + + var tileDef = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, z); + VectorTile vt = new() { TileId = tileDef.Id }; + var lyr = new Layer { Name = "stops" }; + + responseBody.Data?.StopsByBbox?.ForEach(stop => + { + var idParts = stop.GtfsId.Split(':', 2); + string codeWithinFeed = stop.Code ?? string.Empty; + + // TODO: Refactor this, maybe do it client-side or smth + if (idParts[0] == "vitrasa") + { + var digits = new string(codeWithinFeed.Where(char.IsDigit).ToArray()); + if (int.TryParse(digits, out int code)) + { + codeWithinFeed = code.ToString(); + } + } + + if (HiddenStops.Contains($"{idParts[0]}:{codeWithinFeed}")) + { + return; + } + + var fallbackColours = GetFallbackColourForFeed(idParts[0]); + + Feature feature = new() + { + Geometry = new NetTopologySuite.Geometries.Point(stop.Lon, stop.Lat), + Attributes = new AttributesTable + { + // The ID will be used to request the arrivals + { "id", stop.GtfsId }, + // The feed is the first part of the GTFS ID, corresponding to the feed where the info comes from, used for icons probably + { "feed", idParts[0] }, + // The public identifier, usually feed:code or feed:id, recognisable by users and in other systems + { "code", $"{idParts[0]}:{codeWithinFeed}" }, + // The name of the stop + { "name", stop.Name }, + // Routes + { "routes", JsonSerializer.Serialize(stop.Routes?.OrderBy( + r => r.ShortName, + Comparer<string?>.Create(SortingHelper.SortRouteShortNames) + ).Select(r => { + var colour = r.Color ?? fallbackColours.Color; + string textColour; + + if (r.Color is null) // None is present, use fallback + { + textColour = fallbackColours.TextColor; + } + else if (r.TextColor is null || r.TextColor.EndsWith("000000")) + { + // Text colour not provided, or default-black; check the better contrasting + textColour = ContrastHelper.GetBestTextColour(colour); + } + else + { + // Use provided text colour + textColour = r.TextColor; + } + + return new { + shortName = r.ShortName, + colour, + textColour + }; + })) } + } + }; + + lyr.Features.Add(feature); + }); + + vt.Layers.Add(lyr); + + using var ms = new MemoryStream(); + vt.Write(ms, minLinealExtent: 1, minPolygonalExtent: 2); + + _cache.Set($"stops-tile-{z}-{x}-{y}", ms.ToArray(), TimeSpan.FromMinutes(15)); + Response.Headers.Append("X-Cache-Hit", "false"); + + return File(ms.ToArray(), "application/x-protobuf"); + } + + private static (string Color, string TextColor) GetFallbackColourForFeed(string feed) + { + return feed switch + { + "vitrasa" => ("#95D516", "#000000"), + "santiago" => ("#508096", "#FFFFFF"), + "coruna" => ("#E61C29", "#FFFFFF"), + "xunta" => ("#007BC4", "#FFFFFF"), + "renfe" => ("#870164", "#FFFFFF"), + "feve" => ("#EE3D32", "#FFFFFF"), + _ => ("#000000", "#FFFFFF"), + + }; + } + +} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.Legacy.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.Legacy.cs deleted file mode 100644 index d006e38..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.Legacy.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using Costasdev.Busurbano.Backend.Types; -using Microsoft.AspNetCore.Mvc; -using SysFile = System.IO.File; - -namespace Costasdev.Busurbano.Backend.Controllers; - -public partial class VigoController : ControllerBase -{ - [HttpGet("GetStopEstimates")] - public async Task<IActionResult> Run( - [FromQuery] int id - ) - { - try - { - var response = await _api.GetStopEstimates(id); - // Return only the estimates array, not the stop metadata - return new OkObjectResult(response.Estimates); - } - catch (InvalidOperationException) - { - return BadRequest("Stop not found"); - } - } - - [HttpGet("GetStopTimetable")] - public async Task<IActionResult> GetStopTimetable( - [FromQuery] int stopId, - [FromQuery] string? date = null -) - { - // Use Europe/Madrid timezone to determine the correct date - var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); - var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz); - - // If no date provided or date is "today", use Madrid timezone's current date - string effectiveDate; - if (string.IsNullOrEmpty(date) || date == "today") - { - effectiveDate = nowLocal.Date.ToString("yyyy-MM-dd"); - } - else - { - // Validate provided date format - if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out _)) - { - return BadRequest("Invalid date format. Please use yyyy-MM-dd format."); - } - effectiveDate = date; - } - - try - { - var file = Path.Combine(_configuration.VitrasaScheduleBasePath, effectiveDate, stopId + ".json"); - if (!SysFile.Exists(file)) - { - throw new FileNotFoundException(); - } - - var contents = await SysFile.ReadAllTextAsync(file); - - return new OkObjectResult(JsonSerializer.Deserialize<List<ScheduledStop>>(contents)!); - } - catch (FileNotFoundException ex) - { - _logger.LogError(ex, "Stop data not found for stop {StopId} on date {Date}", stopId, effectiveDate); - return StatusCode(404, $"Stop data not found for stop {stopId} on date {effectiveDate}"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error loading stop data"); - return StatusCode(500, "Error loading timetable"); - } - } - -} diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj index abacd68..4b556da 100644 --- a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj +++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj @@ -16,8 +16,9 @@ <PackageReference Include="Google.Protobuf" Version="3.33.1" /> <PackageReference Include="ProjNet" Version="2.1.0" /> - <PackageReference Include="NetTopologySuite" Version="2.6.0" /> - <PackageReference Include="NetTopologySuite.IO.GeoJSON" Version="4.0.0" /> + <PackageReference Include="NetTopologySuite" Version="2.6.0" /> + <PackageReference Include="NetTopologySuite.IO.GeoJSON" Version="4.0.0" /> + <PackageReference Include="NetTopologySuite.IO.VectorTiles.Mapbox" Version="1.1.0" /> </ItemGroup> <ItemGroup> diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs new file mode 100644 index 0000000..8a271f2 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using System.Text.Json.Serialization; + +namespace Costasdev.Busurbano.Backend.GraphClient.App; + +public class StopTileRequestContent : IGraphRequest<StopTileRequestContent.Bbox> +{ + public record Bbox(double MinLon, double MinLat, double MaxLon, double MaxLat); + + public static string Query(Bbox bbox) + { + return string.Create(CultureInfo.InvariantCulture, $@" + query Query {{ + stopsByBbox( + minLat: {bbox.MinLat:F6} + minLon: {bbox.MinLon:F6} + maxLon: {bbox.MaxLon:F6} + maxLat: {bbox.MaxLat:F6} + ) {{ + gtfsId + code + name + lat + lon + routes {{ + gtfsId + shortName + color + textColor + }} + }} + }} + "); + } +} + +public class StopTileResponse : AbstractGraphResponse +{ + [JsonPropertyName("stopsByBbox")] + public List<Stop>? StopsByBbox { get; set; } + + public record Stop + { + [JsonPropertyName("gtfsId")] + public required string GtfsId { get; init; } + + [JsonPropertyName("code")] + public string? Code { get; init; } + + [JsonPropertyName("name")] + public required string Name { get; init; } + + [JsonPropertyName("lat")] + public required double Lat { get; init; } + + [JsonPropertyName("lon")] + public required double Lon { get; init; } + + [JsonPropertyName("routes")] + public List<Route>? Routes { get; init; } + } + + public record Route + { + [JsonPropertyName("gtfsId")] + public required string GtfsId { get; init; } + [JsonPropertyName("shortName")] + public required string ShortName { get; init; } + + [JsonPropertyName("color")] + public string? Color { get; init; } + + [JsonPropertyName("textColor")] + public string? TextColor { get; init; } + } +} diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/ResponseTypes.cs b/src/Costasdev.Busurbano.Backend/GraphClient/ResponseTypes.cs new file mode 100644 index 0000000..2d4d5df --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/GraphClient/ResponseTypes.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace Costasdev.Busurbano.Backend.GraphClient; + +public class GraphClientRequest +{ + public string OperationName { get; set; } = "Query"; + public required string Query { get; set; } +} + +public class GraphClientResponse<T> where T : AbstractGraphResponse +{ + [JsonPropertyName("data")] + public T? Data { get; set; } + + [JsonPropertyName("errors")] + public List<GraphClientError>? Errors { get; set; } + + public bool IsSuccess => Errors == null || Errors.Count == 0; +} + +public interface IGraphRequest<T> +{ + static abstract string Query(T parameters); +} + +public class AbstractGraphResponse +{ +} + +public class GraphClientError +{ + [JsonPropertyName("message")] + public required string Message { get; set; } +} + diff --git a/src/Costasdev.Busurbano.Backend/Helpers/ContrastHelper.cs b/src/Costasdev.Busurbano.Backend/Helpers/ContrastHelper.cs new file mode 100644 index 0000000..e48660b --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Helpers/ContrastHelper.cs @@ -0,0 +1,48 @@ +namespace Costasdev.Busurbano.Backend.Helpers; + +using System; +using System.Globalization; + +public static class ContrastHelper +{ + public static string GetBestTextColour(string backgroundHex) + { + // Strip # + backgroundHex = backgroundHex.TrimStart('#'); + + if (backgroundHex.Length != 6) + throw new ArgumentException("Hex colour must be 6 characters (RRGGBB)"); + + // Parse RGB + int r = int.Parse(backgroundHex.Substring(0, 2), NumberStyles.HexNumber); + int g = int.Parse(backgroundHex.Substring(2, 2), NumberStyles.HexNumber); + int b = int.Parse(backgroundHex.Substring(4, 2), NumberStyles.HexNumber); + + // Convert to relative luminance + double luminance = GetRelativeLuminance(r, g, b); + + // Contrast ratios + double contrastWithWhite = (1.0 + 0.05) / (luminance + 0.05); + double contrastWithBlack = (luminance + 0.05) / 0.05; + + if (contrastWithWhite > 3) + { + return "#FFFFFF"; + } + + return "#000000"; + } + + private static double GetRelativeLuminance(int r, int g, int b) + { + double rs = r / 255.0; + double gs = g / 255.0; + double bs = b / 255.0; + + rs = rs <= 0.03928 ? rs / 12.92 : Math.Pow((rs + 0.055) / 1.055, 2.4); + gs = gs <= 0.03928 ? gs / 12.92 : Math.Pow((gs + 0.055) / 1.055, 2.4); + bs = bs <= 0.03928 ? bs / 12.92 : Math.Pow((bs + 0.055) / 1.055, 2.4); + + return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; + } +} diff --git a/src/Costasdev.Busurbano.Backend/Helpers/SortingHelper.cs b/src/Costasdev.Busurbano.Backend/Helpers/SortingHelper.cs new file mode 100644 index 0000000..472a56f --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Helpers/SortingHelper.cs @@ -0,0 +1,35 @@ +namespace Costasdev.Busurbano.Backend.Helpers; + +public class SortingHelper +{ + public static int SortRouteShortNames(string? a, string? b) + { + if (a == null && b == null) return 0; + if (a == null) return 1; + if (b == null) return -1; + + var aDigits = new string(a.Where(char.IsDigit).ToArray()); + var bDigits = new string(b.Where(char.IsDigit).ToArray()); + + bool aHasDigits = int.TryParse(aDigits, out int aNumber); + bool bHasDigits = int.TryParse(bDigits, out int bNumber); + + if (aHasDigits != bHasDigits) + { + // Non-numeric routes (like "A" or "-") go to the beginning + return aHasDigits ? 1 : -1; + } + + if (aHasDigits && bHasDigits) + { + if (aNumber != bNumber) + { + return aNumber.CompareTo(bNumber); + } + } + + // If both are non-numeric, or numeric parts are equal, use alphabetical + return string.Compare(a, b, StringComparison.OrdinalIgnoreCase); + } + +} 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<LineIconProps> = ({ line, mode = "default" }) => { +const LineIcon: React.FC<LineIconProps> = ({ + line, + mode = "default", + colour, + textColour, +}) => { const actualLine = useMemo(() => { return line.trim().replace("510", "NAD"); }, [line]); @@ -15,16 +22,26 @@ const LineIcon: React.FC<LineIconProps> = ({ 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 ( <span className={`line-icon-${mode}`} style={ { - "--line-colour": `var(${cssVarName})`, - "--line-text-colour": `var(${cssTextVarName}, #000000)`, + "--line-colour": actualLineColour, + "--line-text-colour": actualTextColour, } as React.CSSProperties } > 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<PlannerOverlayProps> = ({ [] ); const [recentPlaces, setRecentPlaces] = useState<PlannerSearchResult[]>([]); - 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<HTMLSelectElement>) => { - const newRegion = e.target.value as any; - setRegion(newRegion); - }; - - return ( - <div className="region-selector"> - <label htmlFor="region-select" className="region-label"> - Región: - </label> - <select - id="region-select" - className="region-select" - value={region} - onChange={handleRegionChange} - > - {regions.map((r) => ( - <option key={r.id} value={r.id}> - {r.name} - </option> - ))} - </select> - </div> - ); -} 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<StopMapModalProps> = ({ !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<StopMapModalProps> = ({ 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/map/StopSummarySheet.css index 5869d41..5869d41 100644 --- a/src/frontend/app/components/StopSummarySheet.css +++ b/src/frontend/app/components/map/StopSummarySheet.css diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/map/StopSummarySheet.tsx index c2d6ffe..b24e71c 100644 --- a/src/frontend/app/components/StopSummarySheet.tsx +++ b/src/frontend/app/components/map/StopSummarySheet.tsx @@ -4,19 +4,27 @@ 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 { 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"; -interface StopSheetProps { +export interface StopSheetProps { isOpen: boolean; onClose: () => void; - stop: Stop; + stop: { + stopId: string; + stopCode?: string; + stopFeed?: string; + name: string; + lines: { + line: string; + colour?: string; + textColour?: string; + }[]; + }; } interface ErrorInfo { @@ -29,7 +37,7 @@ const loadConsolidatedData = async ( stopId: string ): Promise<ConsolidatedCirculation[]> => { const resp = await fetch( - `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`, + `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`, { headers: { Accept: "application/json", @@ -116,21 +124,24 @@ export const StopSheet: React.FC<StopSheetProps> = ({ <Sheet.Content drag="y"> <div className="stop-sheet-content"> <div className="stop-sheet-header"> - <h2 className="stop-sheet-title">{stop.name.original}</h2> - <span className="stop-sheet-id">({stop.stopId})</span> + <h2 className="stop-sheet-title">{stop.name}</h2> + <span className="stop-sheet-id">({stop.stopCode})</span> </div> - <div - className={`stop-sheet-lines-container ${stop.lines.length >= 10 ? "scrollable" : ""}`} - > - {stop.lines.map((line) => ( - <div key={line} className="stop-sheet-line-icon"> - <LineIcon line={line} mode="rounded" /> - </div> + <div className={`d-flex flex-wrap flex-row gap-2`}> + {stop.lines.map((lineObj) => ( + <LineIcon + key={lineObj.line} + line={lineObj.line} + mode="pill" + colour={lineObj.colour} + textColour={lineObj.textColour} + /> ))} </div> - <StopAlert stop={stop} compact /> + {/* TODO: Enable stop alerts when available */} + {/*<StopAlert stop={stop} compact />*/} {loading ? ( <StopSummarySheetSkeleton /> @@ -158,7 +169,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({ ) : ( <ConsolidatedCirculationList data={data.slice(0, 4)} - driver={stop.stopId.split(":")[0]} + driver={stop.stopFeed} reduced /> )} diff --git a/src/frontend/app/components/StopSummarySheetSkeleton.tsx b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx index 7697efc..7697efc 100644 --- a/src/frontend/app/components/StopSummarySheetSkeleton.tsx +++ b/src/frontend/app/components/map/StopSummarySheetSkeleton.tsx 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<string, string> = {}; 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<Stop[]> { async function getStopById(stopId: string | number): Promise<Stop | undefined> { 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<Stop | undefined> { // 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<Stop[]> { - 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<Stop[]> { }); } +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<Stop | null>(null); + const [selectedStop, setSelectedStop] = useState< + StopSheetProps["stop"] | null + >(null); const [isSheetOpen, setIsSheetOpen] = useState(false); const { mapState, updateMapState, theme } = useApp(); const mapRef = useRef<MapRef>(null); @@ -63,46 +55,11 @@ 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"; loadStyle(styleName, theme) @@ -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]} > <NavigationControl position="bottom-right" /> <GeolocateControl @@ -225,8 +185,10 @@ export default function StopMap() { <Source id="stops-source" - type="geojson" - data={{ type: "FeatureCollection", features: stops }} + type="vector" + tiles={[StopDataProvider.getTileUrlTemplate()]} + minzoom={11} + maxzoom={20} /> <Layer @@ -234,8 +196,26 @@ export default function StopMap() { type="symbol" minzoom={11} source="stops-source" + source-layer="stops" layout={{ - "icon-image": ["get", "prefix"], + // TODO: Fix ñapa by maybe including this from the server side? + "icon-image": [ + "match", + ["get", "feed"], + "vitrasa", + "stop-vitrasa", + "santiago", + "stop-santiago", + "coruna", + "stop-coruna", + "xunta", + "stop-xunta", + "renfe", + "stop-renfe", + "feve", + "stop-feve", + "#stop-generic", + ], "icon-size": [ "interpolate", ["linear"], @@ -256,6 +236,7 @@ export default function StopMap() { id="stops-label" type="symbol" source="stops-source" + source-layer="stops" minzoom={16} layout={{ "text-field": ["get", "name"], @@ -267,10 +248,21 @@ export default function StopMap() { }} paint={{ "text-color": [ - "case", - ["==", ["get", "prefix"], "stop-renfe"], + "match", + ["get", "feed"], + "vitrasa", + "#95D516", + "santiago", + "#508096", + "coruna", + "#E61C29", + "xunta", + "#007BC4", + "renfe", "#870164", - "#e72b37", + "feve", + "#EE3D32", + "#333333", ], "text-halo-color": "#FFF", "text-halo-width": 1, diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx index b1a9813..9e44425 100644 --- a/src/frontend/app/routes/planner.tsx +++ b/src/frontend/app/routes/planner.tsx @@ -9,7 +9,7 @@ import { useLocation } from "react-router"; import { useApp } from "~/AppContext"; import LineIcon from "~/components/LineIcon"; import { PlannerOverlay } from "~/components/PlannerOverlay"; -import { REGION_DATA } from "~/config/RegionConfig"; +import { APP_CONSTANTS } from "~/config/constants"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { type Itinerary } from "~/data/PlannerApi"; import { usePlanner } from "~/hooks/usePlanner"; @@ -392,7 +392,7 @@ const ItineraryDetail = ({ if (!arrivalsByStop[stopKey]) { try { const resp = await fetch( - `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${encodeURIComponent(leg.from.stopCode || leg.from.stopId)}`, + `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${encodeURIComponent(leg.from.stopCode || leg.from.stopId)}`, { headers: { Accept: "application/json" } } ); @@ -424,9 +424,11 @@ const ItineraryDetail = ({ ref={mapRef} initialViewState={{ longitude: - origin?.lon || (REGION_DATA.defaultCenter as [number, number])[0], + origin?.lon || + (APP_CONSTANTS.defaultCenter as [number, number])[0], latitude: - origin?.lat || (REGION_DATA.defaultCenter as [number, number])[1], + origin?.lat || + (APP_CONSTANTS.defaultCenter as [number, number])[1], zoom: 13, }} mapStyle={mapStyle} diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx index 6d06215..46358dc 100644 --- a/src/frontend/app/routes/stops-$id.tsx +++ b/src/frontend/app/routes/stops-$id.tsx @@ -10,7 +10,7 @@ import { StopHelpModal } from "~/components/StopHelpModal"; import { StopMapModal } from "~/components/StopMapModal"; import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList"; import { ConsolidatedCirculationListSkeleton } from "~/components/Stops/ConsolidatedCirculationListSkeleton"; -import { REGION_DATA } from "~/config/RegionConfig"; +import { APP_CONSTANTS } from "~/config/constants"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { useAutoRefresh } from "~/hooks/useAutoRefresh"; import StopDataProvider, { type Stop } from "../data/StopDataProvider"; @@ -58,7 +58,7 @@ const loadConsolidatedData = async ( stopId: string ): Promise<ConsolidatedCirculation[]> => { 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 Binary files differindex a63888a..d8a32ab 100644 --- a/src/frontend/public/maps/spritesheet/sprite.png +++ b/src/frontend/public/maps/spritesheet/sprite.png 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 Binary files differindex 5f4b575..ba873a4 100644 --- a/src/frontend/public/maps/spritesheet/sprite@2x.png +++ b/src/frontend/public/maps/spritesheet/sprite@2x.png |
