From c0e758b1e793159fc86c85916130f8959360c64e Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 15 Mar 2026 20:10:35 +0100 Subject: Implement basic real time information for Renfe --- src/Enmarcha.Backend/Enmarcha.Backend.csproj | 2 +- src/Enmarcha.Backend/Program.cs | 6 +- src/Enmarcha.Backend/Services/FeedService.cs | 1 + .../Services/Processors/RenfeRealTimeProcessor.cs | 72 +++++++++++++++++ .../Processors/SantiagoRealTimeProcessor.cs | 94 ---------------------- .../Services/Processors/TussaRealTimeProcessor.cs | 94 ++++++++++++++++++++++ .../Services/Processors/VigoUsageProcessor.cs | 66 --------------- .../Services/Processors/VitrasaUsageProcessor.cs | 66 +++++++++++++++ 8 files changed, 238 insertions(+), 163 deletions(-) create mode 100644 src/Enmarcha.Backend/Services/Processors/RenfeRealTimeProcessor.cs delete mode 100644 src/Enmarcha.Backend/Services/Processors/SantiagoRealTimeProcessor.cs create mode 100644 src/Enmarcha.Backend/Services/Processors/TussaRealTimeProcessor.cs delete mode 100644 src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs create mode 100644 src/Enmarcha.Backend/Services/Processors/VitrasaUsageProcessor.cs (limited to 'src/Enmarcha.Backend') diff --git a/src/Enmarcha.Backend/Enmarcha.Backend.csproj b/src/Enmarcha.Backend/Enmarcha.Backend.csproj index 3ce20ad..d2c5a28 100644 --- a/src/Enmarcha.Backend/Enmarcha.Backend.csproj +++ b/src/Enmarcha.Backend/Enmarcha.Backend.csproj @@ -12,7 +12,6 @@ - @@ -33,6 +32,7 @@ + diff --git a/src/Enmarcha.Backend/Program.cs b/src/Enmarcha.Backend/Program.cs index e8a7968..46383b0 100644 --- a/src/Enmarcha.Backend/Program.cs +++ b/src/Enmarcha.Backend/Program.cs @@ -126,9 +126,10 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -143,6 +144,7 @@ builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); var app = builder.Build(); diff --git a/src/Enmarcha.Backend/Services/FeedService.cs b/src/Enmarcha.Backend/Services/FeedService.cs index 34bc522..dc016b4 100644 --- a/src/Enmarcha.Backend/Services/FeedService.cs +++ b/src/Enmarcha.Backend/Services/FeedService.cs @@ -207,6 +207,7 @@ public class FeedService return feedId switch { "xunta" => string.Join(" > ", nextStops), + "renfe" => string.Join(" - ", nextStops), _ => string.Join(", ", nextStops.Take(4)) }; } diff --git a/src/Enmarcha.Backend/Services/Processors/RenfeRealTimeProcessor.cs b/src/Enmarcha.Backend/Services/Processors/RenfeRealTimeProcessor.cs new file mode 100644 index 0000000..dcddd5d --- /dev/null +++ b/src/Enmarcha.Backend/Services/Processors/RenfeRealTimeProcessor.cs @@ -0,0 +1,72 @@ +using Enmarcha.Backend.Types; +using Enmarcha.Backend.Types.Arrivals; +using Enmarcha.Sources.GtfsRealtime; +using Arrival = Enmarcha.Backend.Types.Arrivals.Arrival; + +namespace Enmarcha.Backend.Services.Processors; + +public class RenfeRealTimeProcessor : AbstractRealTimeProcessor +{ + private readonly GtfsRealtimeEstimatesProvider _realtime; + private readonly ILogger _logger; + + public RenfeRealTimeProcessor( + GtfsRealtimeEstimatesProvider realtime, + ILogger logger + ) + { + _realtime = realtime; + _logger = logger; + } + + public override async Task ProcessAsync(ArrivalsContext context) + { + if (!context.StopId.StartsWith("renfe:")) return; + + try + { + var delays = await _realtime.GetRenfeDelays(); + var positions = await _realtime.GetRenfePositions(); + System.Diagnostics.Activity.Current?.SetTag("realtime.count", delays.Count); + + foreach (Arrival contextArrival in context.Arrivals) + { + var trainNumber = contextArrival.TripId.Split(":")[1][..5]; + + contextArrival.Headsign.Destination = trainNumber + " - " + contextArrival.Headsign.Destination; + + if (delays.TryGetValue(trainNumber, out var delay)) + { + if (delay is null) + { + // TODO: Indicate train got cancelled + continue; + } + + var delayMinutes = delay.Value / 60; + contextArrival.Delay = new DelayBadge() + { + Minutes = delayMinutes + }; + + contextArrival.Estimate.Minutes += delayMinutes; + contextArrival.Estimate.Precision = ArrivalPrecision.Confident; + } + + if (positions.TryGetValue(trainNumber, out var position)) + { + contextArrival.CurrentPosition = new Position + { + Latitude = position.Latitude, + Longitude = position.Longitude, + OrientationDegrees = 0 // TODO: Set the proper degrees + }; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Renfe real-time data"); + } + } +} diff --git a/src/Enmarcha.Backend/Services/Processors/SantiagoRealTimeProcessor.cs b/src/Enmarcha.Backend/Services/Processors/SantiagoRealTimeProcessor.cs deleted file mode 100644 index a4f7d5b..0000000 --- a/src/Enmarcha.Backend/Services/Processors/SantiagoRealTimeProcessor.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Enmarcha.Backend.Helpers; -using Enmarcha.Backend.Types.Arrivals; -using Enmarcha.Sources.Tussa; -using Arrival = Enmarcha.Backend.Types.Arrivals.Arrival; - -namespace Enmarcha.Backend.Services.Processors; - -public class SantiagoRealTimeProcessor : AbstractRealTimeProcessor -{ - private readonly SantiagoRealtimeEstimatesProvider _realtime; - private readonly FeedService _feedService; - private readonly ILogger _logger; - - public SantiagoRealTimeProcessor( - SantiagoRealtimeEstimatesProvider realtime, - FeedService feedService, - ILogger logger) - { - _realtime = realtime; - _feedService = feedService; - _logger = logger; - } - - public override async Task ProcessAsync(ArrivalsContext context) - { - if (!context.StopId.StartsWith("tussa:")) return; - - var normalizedCode = _feedService.NormalizeStopCode("tussa", context.StopCode); - if (!int.TryParse(normalizedCode, out var numericStopId)) return; - - try - { - var realtime = await _realtime.GetEstimatesForStop(numericStopId); - System.Diagnostics.Activity.Current?.SetTag("realtime.count", realtime.Count); - - var usedTripIds = new HashSet(); - - foreach (var estimate in realtime) - { - var bestMatch = context.Arrivals - .Where(a => !usedTripIds.Contains(a.TripId)) - .Where(a => a.Route.RouteIdInGtfs.Trim() == estimate.Id.ToString()) - .Select(a => new - { - Arrival = a, - TimeDiff = estimate.MinutesToArrive - a.Estimate.Minutes, // RealTime - Schedule - RouteMatch = true - }) - .Where(x => x.RouteMatch) // Strict route matching - .Where(x => x.TimeDiff is >= -5 and <= 35) // Allow 2m early (RealTime < Schedule) or 25m late (RealTime > Schedule) - .OrderBy(x => Math.Abs(x.TimeDiff)) // Best time fit - .FirstOrDefault(); - - if (bestMatch is null) - { - context.Arrivals.Add(new Arrival - { - TripId = $"tussa:rt:{estimate.Id}:{estimate.MinutesToArrive}", - Route = new RouteInfo - { - GtfsId = $"tussa:{estimate.Id}", - ShortName = estimate.Sinoptico, - Colour = estimate.Colour, - TextColour = ContrastHelper.GetBestTextColour(estimate.Colour) - }, - Headsign = new HeadsignInfo - { - Badge = "T.REAL", - Destination = estimate.Name - }, - Estimate = new ArrivalDetails - { - Minutes = estimate.MinutesToArrive, - Precision = ArrivalPrecision.Confident - } - }); - continue; - } - - var arrival = bestMatch.Arrival; - - arrival.Estimate.Minutes = estimate.MinutesToArrive; - arrival.Estimate.Precision = ArrivalPrecision.Confident; - - usedTripIds.Add(arrival.TripId); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching Santiago real-time data for stop {StopId}", context.StopId); - } - } - -} diff --git a/src/Enmarcha.Backend/Services/Processors/TussaRealTimeProcessor.cs b/src/Enmarcha.Backend/Services/Processors/TussaRealTimeProcessor.cs new file mode 100644 index 0000000..7808a3f --- /dev/null +++ b/src/Enmarcha.Backend/Services/Processors/TussaRealTimeProcessor.cs @@ -0,0 +1,94 @@ +using Enmarcha.Backend.Helpers; +using Enmarcha.Backend.Types.Arrivals; +using Enmarcha.Sources.Tussa; +using Arrival = Enmarcha.Backend.Types.Arrivals.Arrival; + +namespace Enmarcha.Backend.Services.Processors; + +public class TussaRealTimeProcessor : AbstractRealTimeProcessor +{ + private readonly SantiagoRealtimeEstimatesProvider _realtime; + private readonly FeedService _feedService; + private readonly ILogger _logger; + + public TussaRealTimeProcessor( + SantiagoRealtimeEstimatesProvider realtime, + FeedService feedService, + ILogger logger) + { + _realtime = realtime; + _feedService = feedService; + _logger = logger; + } + + public override async Task ProcessAsync(ArrivalsContext context) + { + if (!context.StopId.StartsWith("tussa:")) return; + + var normalizedCode = _feedService.NormalizeStopCode("tussa", context.StopCode); + if (!int.TryParse(normalizedCode, out var numericStopId)) return; + + try + { + var realtime = await _realtime.GetEstimatesForStop(numericStopId); + System.Diagnostics.Activity.Current?.SetTag("realtime.count", realtime.Count); + + var usedTripIds = new HashSet(); + + foreach (var estimate in realtime) + { + var bestMatch = context.Arrivals + .Where(a => !usedTripIds.Contains(a.TripId)) + .Where(a => a.Route.RouteIdInGtfs.Trim() == estimate.Id.ToString()) + .Select(a => new + { + Arrival = a, + TimeDiff = estimate.MinutesToArrive - a.Estimate.Minutes, // RealTime - Schedule + RouteMatch = true + }) + .Where(x => x.RouteMatch) // Strict route matching + .Where(x => x.TimeDiff is >= -5 and <= 35) // Allow 2m early (RealTime < Schedule) or 25m late (RealTime > Schedule) + .OrderBy(x => Math.Abs(x.TimeDiff)) // Best time fit + .FirstOrDefault(); + + if (bestMatch is null) + { + context.Arrivals.Add(new Arrival + { + TripId = $"tussa:rt:{estimate.Id}:{estimate.MinutesToArrive}", + Route = new RouteInfo + { + GtfsId = $"tussa:{estimate.Id}", + ShortName = estimate.Sinoptico, + Colour = estimate.Colour, + TextColour = ContrastHelper.GetBestTextColour(estimate.Colour) + }, + Headsign = new HeadsignInfo + { + Badge = "T.REAL", + Destination = estimate.Name + }, + Estimate = new ArrivalDetails + { + Minutes = estimate.MinutesToArrive, + Precision = ArrivalPrecision.Confident + } + }); + continue; + } + + var arrival = bestMatch.Arrival; + + arrival.Estimate.Minutes = estimate.MinutesToArrive; + arrival.Estimate.Precision = ArrivalPrecision.Confident; + + usedTripIds.Add(arrival.TripId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Santiago real-time data for stop {StopId}", context.StopId); + } + } + +} diff --git a/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs b/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs deleted file mode 100644 index 52218d9..0000000 --- a/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Text.Json; -using Enmarcha.Backend.Types.Arrivals; -using Microsoft.Extensions.Caching.Memory; - -namespace Enmarcha.Backend.Services.Processors; - -public class VigoUsageProcessor : IArrivalsProcessor -{ - private readonly HttpClient _httpClient; - private readonly IMemoryCache _cache; - private readonly ILogger _logger; - private readonly FeedService _feedService; - - public VigoUsageProcessor( - HttpClient httpClient, - IMemoryCache cache, - ILogger logger, - FeedService feedService) - { - _httpClient = httpClient; - _cache = cache; - _logger = logger; - _feedService = feedService; - } - - public async Task ProcessAsync(ArrivalsContext context) - { - if (!context.StopId.StartsWith("vitrasa:") || context.IsReduced || context.IsNano) return; - - var normalizedCode = _feedService.NormalizeStopCode("vitrasa", context.StopCode); - - var cacheKey = $"vigo_usage_{normalizedCode}"; - if (_cache.TryGetValue(cacheKey, out List? cachedUsage)) - { - context.Usage = cachedUsage; - return; - } - - try - { - using var activity = Telemetry.Source.StartActivity("FetchVigoUsage"); - var url = $"https://datos.vigo.org/vci_api_app/api2.jsp?tipo=TRANSPORTE_PARADA_HORAS_USO¶da={normalizedCode}"; - var response = await _httpClient.GetAsync(url); - - if (response.IsSuccessStatusCode) - { - var json = await response.Content.ReadAsStringAsync(); - var usage = JsonSerializer.Deserialize>(json); - - if (usage != null) - { - _cache.Set(cacheKey, usage, TimeSpan.FromDays(7)); - context.Usage = usage; - } - } - else - { - _logger.LogWarning("Failed to fetch usage data for stop {StopCode}, status: {Status}", normalizedCode, response.StatusCode); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching usage data for Vigo stop {StopCode}", normalizedCode); - } - } -} diff --git a/src/Enmarcha.Backend/Services/Processors/VitrasaUsageProcessor.cs b/src/Enmarcha.Backend/Services/Processors/VitrasaUsageProcessor.cs new file mode 100644 index 0000000..a2f90d3 --- /dev/null +++ b/src/Enmarcha.Backend/Services/Processors/VitrasaUsageProcessor.cs @@ -0,0 +1,66 @@ +using System.Text.Json; +using Enmarcha.Backend.Types.Arrivals; +using Microsoft.Extensions.Caching.Memory; + +namespace Enmarcha.Backend.Services.Processors; + +public class VitrasaUsageProcessor : IArrivalsProcessor +{ + private readonly HttpClient _httpClient; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + private readonly FeedService _feedService; + + public VitrasaUsageProcessor( + HttpClient httpClient, + IMemoryCache cache, + ILogger logger, + FeedService feedService) + { + _httpClient = httpClient; + _cache = cache; + _logger = logger; + _feedService = feedService; + } + + public async Task ProcessAsync(ArrivalsContext context) + { + if (!context.StopId.StartsWith("vitrasa:") || context.IsReduced || context.IsNano) return; + + var normalizedCode = _feedService.NormalizeStopCode("vitrasa", context.StopCode); + + var cacheKey = $"vigo_usage_{normalizedCode}"; + if (_cache.TryGetValue(cacheKey, out List? cachedUsage)) + { + context.Usage = cachedUsage; + return; + } + + try + { + using var activity = Telemetry.Source.StartActivity("FetchVigoUsage"); + var url = $"https://datos.vigo.org/vci_api_app/api2.jsp?tipo=TRANSPORTE_PARADA_HORAS_USO¶da={normalizedCode}"; + var response = await _httpClient.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + var usage = JsonSerializer.Deserialize>(json); + + if (usage != null) + { + _cache.Set(cacheKey, usage, TimeSpan.FromDays(7)); + context.Usage = usage; + } + } + else + { + _logger.LogWarning("Failed to fetch usage data for stop {StopCode}, status: {Status}", normalizedCode, response.StatusCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching usage data for Vigo stop {StopCode}", normalizedCode); + } + } +} -- cgit v1.3