From 32574981f659a6c59faf968c8dbfe6eda3c632d6 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Mon, 20 Apr 2026 16:23:00 +0200 Subject: Mostrar procedencia en rutas interurbanas Closes #154 --- .../Services/Processors/NextStopsProcessor.cs | 51 ++++++++++++++++++++++ src/Enmarcha.Backend/Types/Arrivals/Arrival.cs | 5 +++ src/frontend/app/api/schema.ts | 1 + .../app/components/arrivals/ArrivalCard.tsx | 16 ++++++- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/Enmarcha.Backend/Services/Processors/NextStopsProcessor.cs b/src/Enmarcha.Backend/Services/Processors/NextStopsProcessor.cs index 4c0b8ac..0f86be5 100644 --- a/src/Enmarcha.Backend/Services/Processors/NextStopsProcessor.cs +++ b/src/Enmarcha.Backend/Services/Processors/NextStopsProcessor.cs @@ -43,7 +43,31 @@ public class NextStopsProcessor : IArrivalsProcessor .ToList(); } + // Remove last stop since it doesn't make sense to show "via" for the terminus + arrival.NextStops = arrival.NextStops.Take(arrival.NextStops.Count - 1).ToList(); + + if (feedId == "xunta") + { + arrival.OriginStops = otpArrival.Trip.Stoptimes + .Where(s => s.ScheduledDeparture < currentStopDeparture) + .OrderBy(s => s.ScheduledDeparture) + .Take(1) + .Select(s => $"{s.Stop.Name} -- {s.Stop.Description}") + .Distinct() + .ToList(); + } + else if (feedId == "renfe") + { + arrival.OriginStops = otpArrival.Trip.Stoptimes + .Where(s => s.ScheduledDeparture < currentStopDeparture) + .OrderBy(s => s.ScheduledDeparture) + .Take(1) + .Select(s => FeedService.NormalizeStopName(feedId, s.Stop.Name)) + .ToList(); + } + arrival.Headsign.Marquee = GenerateMarquee(feedId, arrival.NextStops); + arrival.Headsign.Origin = GenerateOrigin(feedId, arrival.OriginStops); } return Task.CompletedTask; @@ -122,6 +146,33 @@ public class NextStopsProcessor : IArrivalsProcessor }; } + private static string? GenerateOrigin(string feedId, List originStops) + { + if (originStops.Count == 0) return null; + + if (feedId == "xunta") + { + var points = originStops.Select(SplitXuntaStopDescription).ToList(); + var concellos = points + .Select(p => p.concello) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return concellos.Count > 0 ? string.Join(" > ", concellos) : null; + } + + if (feedId == "renfe") + { + // For trains just show the origin terminus + return originStops.First(); + } + + // For local bus feeds, origin is generally not very informative, + // but return the first stop for completeness + return originStops.First(); + } + private static (string nombre, string parroquia, string concello) SplitXuntaStopDescription(string stopName) { var parts = stopName.Split(" -- ", 3); diff --git a/src/Enmarcha.Backend/Types/Arrivals/Arrival.cs b/src/Enmarcha.Backend/Types/Arrivals/Arrival.cs index 0e74a44..3272a05 100644 --- a/src/Enmarcha.Backend/Types/Arrivals/Arrival.cs +++ b/src/Enmarcha.Backend/Types/Arrivals/Arrival.cs @@ -33,6 +33,9 @@ public class Arrival [JsonIgnore] public List NextStops { get; set; } = []; + [JsonIgnore] + public List OriginStops { get; set; } = []; + [JsonIgnore] public ArrivalsAtStopResponse.Arrival? RawOtpTrip { get; set; } @@ -73,6 +76,8 @@ public class HeadsignInfo [JsonPropertyName("destination")] public required string Destination { get; set; } [JsonPropertyName("marquee")] public string? Marquee { get; set; } + + [JsonPropertyName("origin")] public string? Origin { get; set; } } public class ArrivalDetails diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts index 864ea57..d900055 100644 --- a/src/frontend/app/api/schema.ts +++ b/src/frontend/app/api/schema.ts @@ -11,6 +11,7 @@ export const HeadsignInfoSchema = z.object({ badge: z.string().optional().nullable(), destination: z.string().nullable(), marquee: z.string().optional().nullable(), + origin: z.string().optional().nullable(), }); export const ArrivalPrecisionSchema = z.enum([ diff --git a/src/frontend/app/components/arrivals/ArrivalCard.tsx b/src/frontend/app/components/arrivals/ArrivalCard.tsx index 51e0803..3aae65e 100644 --- a/src/frontend/app/components/arrivals/ArrivalCard.tsx +++ b/src/frontend/app/components/arrivals/ArrivalCard.tsx @@ -34,7 +34,7 @@ const AutoMarquee = ({ text }: { text: string }) => { if (!el) return; const checkScroll = () => { - const charWidth = 8; + const charWidth = 12; const availableWidth = el.offsetWidth; const textWidth = text.length * charWidth; setShouldScroll(textWidth > availableWidth); @@ -252,10 +252,22 @@ export const ArrivalCard: React.FC = ({ {operator && ( {operator} + {headsign.destination || headsign.origin ? ( + <> ·  + ) : ( + "" + )} + + )} + {headsign.origin && ( + + Proc: {headsign.origin}{" "} {headsign.marquee && <> · } )} - {headsign.marquee && } + {headsign.marquee && ( + + )} -- cgit v1.3