aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-02-14 01:35:54 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-02-14 01:36:09 +0100
commitac366d04cd54869c9a2b090aae24a276c32a85a6 (patch)
tree64a5c5903b07646a5c58b1b7e4c9704022549245 /src/Enmarcha.Backend
parent3f8fb6fda07f97c9fd676cff67c637c0df0f7029 (diff)
feat: Implemen experimental bus stop usage display
Diffstat (limited to 'src/Enmarcha.Backend')
-rw-r--r--src/Enmarcha.Backend/Controllers/ArrivalsController.cs9
-rw-r--r--src/Enmarcha.Backend/Data/vitrasa_stops_p95.csv60
-rw-r--r--src/Enmarcha.Backend/Enmarcha.Backend.csproj2
-rw-r--r--src/Enmarcha.Backend/Program.cs1
-rw-r--r--src/Enmarcha.Backend/Services/ArrivalsPipeline.cs1
-rw-r--r--src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs121
-rw-r--r--src/Enmarcha.Backend/Types/Arrivals/BusStopUsagePoint.cs15
-rw-r--r--src/Enmarcha.Backend/Types/Arrivals/StopArrivalsResponse.cs3
8 files changed, 208 insertions, 4 deletions
diff --git a/src/Enmarcha.Backend/Controllers/ArrivalsController.cs b/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
index 13fb430..a887c89 100644
--- a/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
+++ b/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
@@ -130,7 +130,7 @@ public partial class ArrivalsController : ControllerBase
arrivals.Add(arrival);
}
- await _pipeline.ExecuteAsync(new ArrivalsContext
+ var context = new ArrivalsContext
{
StopId = id,
StopCode = stop.Code,
@@ -138,7 +138,9 @@ public partial class ArrivalsController : ControllerBase
Arrivals = arrivals,
NowLocal = nowLocal,
StopLocation = new Position { Latitude = stop.Lat, Longitude = stop.Lon }
- });
+ };
+
+ await _pipeline.ExecuteAsync(context);
var feedId = id.Split(':')[0];
@@ -167,7 +169,8 @@ public partial class ArrivalsController : ControllerBase
ContrastHelper.GetBestTextColour(r.Color ?? fallbackColor) :
r.TextColor
})],
- Arrivals = [.. arrivals.Where(a => a.Estimate.Minutes >= timeThreshold)]
+ Arrivals = [.. arrivals.Where(a => a.Estimate.Minutes >= timeThreshold)],
+ Usage = context.Usage
});
}
diff --git a/src/Enmarcha.Backend/Data/vitrasa_stops_p95.csv b/src/Enmarcha.Backend/Data/vitrasa_stops_p95.csv
new file mode 100644
index 0000000..8091fd9
--- /dev/null
+++ b/src/Enmarcha.Backend/Data/vitrasa_stops_p95.csv
@@ -0,0 +1,60 @@
+codigo
+14264
+8630
+6940
+14206
+14333
+8750
+5610
+6930
+8610
+8840
+5800
+1310
+5820
+5630
+6300
+2780
+8820
+8460
+8520
+8540
+8470
+14892
+8040
+8480
+6570
+8450
+6960
+5300
+6860
+5790
+5540
+20102
+6620
+14337
+5650
+1360
+20193
+8500
+1920
+20111
+20075
+5560
+5570
+7000
+5530
+2820
+5620
+5680
+5520
+6550
+1260
+1280
+8770
+2830
+14123
+14163
+14168
+2020
+8060
diff --git a/src/Enmarcha.Backend/Enmarcha.Backend.csproj b/src/Enmarcha.Backend/Enmarcha.Backend.csproj
index 1591e7c..de6489e 100644
--- a/src/Enmarcha.Backend/Enmarcha.Backend.csproj
+++ b/src/Enmarcha.Backend/Enmarcha.Backend.csproj
@@ -35,7 +35,7 @@
</ItemGroup>
<ItemGroup>
- <None Update="Data\xunta_fares.csv">
+ <None Update="Data\*.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
diff --git a/src/Enmarcha.Backend/Program.cs b/src/Enmarcha.Backend/Program.cs
index 8599a5c..f4b39ff 100644
--- a/src/Enmarcha.Backend/Program.cs
+++ b/src/Enmarcha.Backend/Program.cs
@@ -128,6 +128,7 @@ builder.Services.AddSingleton<LineFormatterService>();
builder.Services.AddScoped<IArrivalsProcessor, VitrasaRealTimeProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, CorunaRealTimeProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, SantiagoRealTimeProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, VigoUsageProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, FilterAndSortProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, NextStopsProcessor>();
diff --git a/src/Enmarcha.Backend/Services/ArrivalsPipeline.cs b/src/Enmarcha.Backend/Services/ArrivalsPipeline.cs
index 4f49afe..6d8c2c0 100644
--- a/src/Enmarcha.Backend/Services/ArrivalsPipeline.cs
+++ b/src/Enmarcha.Backend/Services/ArrivalsPipeline.cs
@@ -23,6 +23,7 @@ public class ArrivalsContext
public Position? StopLocation { get; set; }
public required List<Arrival> Arrivals { get; set; }
+ public List<BusStopUsagePoint>? Usage { get; set; }
public required DateTime NowLocal { get; set; }
}
diff --git a/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs b/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs
new file mode 100644
index 0000000..f5c7664
--- /dev/null
+++ b/src/Enmarcha.Backend/Services/Processors/VigoUsageProcessor.cs
@@ -0,0 +1,121 @@
+using System.Text.Json;
+using CsvHelper;
+using CsvHelper.Configuration;
+using Enmarcha.Backend.Types.Arrivals;
+using Microsoft.Extensions.Caching.Memory;
+using System.Globalization;
+
+namespace Enmarcha.Backend.Services.Processors;
+
+public class VigoUsageProcessor : IArrivalsProcessor
+{
+ private readonly HttpClient _httpClient;
+ private readonly IMemoryCache _cache;
+ private readonly ILogger<VigoUsageProcessor> _logger;
+ private readonly IHostEnvironment _environment;
+ private readonly FeedService _feedService;
+ private static readonly HashSet<string> _vigoStopsWhitelist = [];
+ private static bool _whitelistLoaded = false;
+ private static readonly object _lock = new();
+
+ public VigoUsageProcessor(
+ HttpClient httpClient,
+ IMemoryCache cache,
+ ILogger<VigoUsageProcessor> logger,
+ IHostEnvironment environment,
+ FeedService feedService)
+ {
+ _httpClient = httpClient;
+ _cache = cache;
+ _logger = logger;
+ _environment = environment;
+ _feedService = feedService;
+
+ LoadWhitelist();
+ }
+
+ private void LoadWhitelist()
+ {
+ if (_whitelistLoaded) return;
+
+ lock (_lock)
+ {
+ if (_whitelistLoaded) return;
+
+ try
+ {
+ var path = Path.Combine(_environment.ContentRootPath, "Data", "vitrasa_stops_p95.csv");
+ if (File.Exists(path))
+ {
+ using var reader = new StreamReader(path);
+ using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
+ csv.Read();
+ csv.ReadHeader();
+ while (csv.Read())
+ {
+ var code = csv.GetField("codigo");
+ if (!string.IsNullOrWhiteSpace(code))
+ {
+ _vigoStopsWhitelist.Add(code.Trim());
+ }
+ }
+ _logger.LogInformation("Loaded {Count} Vigo stops for usage data whitelist", _vigoStopsWhitelist.Count);
+ }
+ else
+ {
+ _logger.LogWarning("Vigo stops whitelist CSV not found at {Path}", path);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error loading Vigo stops whitelist");
+ }
+ finally
+ {
+ _whitelistLoaded = true;
+ }
+ }
+ }
+
+ public async Task ProcessAsync(ArrivalsContext context)
+ {
+ if (!context.StopId.StartsWith("vitrasa:")) return;
+
+ var normalizedCode = _feedService.NormalizeStopCode("vitrasa", context.StopCode);
+ if (!_vigoStopsWhitelist.Contains(normalizedCode)) return;
+
+ var cacheKey = $"vigo_usage_{normalizedCode}";
+ if (_cache.TryGetValue(cacheKey, out List<BusStopUsagePoint>? 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&parada={normalizedCode}";
+ var response = await _httpClient.GetAsync(url);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var json = await response.Content.ReadAsStringAsync();
+ var usage = JsonSerializer.Deserialize<List<BusStopUsagePoint>>(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/Types/Arrivals/BusStopUsagePoint.cs b/src/Enmarcha.Backend/Types/Arrivals/BusStopUsagePoint.cs
new file mode 100644
index 0000000..edb08f4
--- /dev/null
+++ b/src/Enmarcha.Backend/Types/Arrivals/BusStopUsagePoint.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Enmarcha.Backend.Types.Arrivals;
+
+public class BusStopUsagePoint
+{
+ [JsonPropertyName("h")]
+ public required int Hour { get; set; }
+
+ [JsonPropertyName("t")]
+ public required int Total { get; set; }
+
+ [JsonPropertyName("d")]
+ public required int DayOfWeek { get; set; }
+}
diff --git a/src/Enmarcha.Backend/Types/Arrivals/StopArrivalsResponse.cs b/src/Enmarcha.Backend/Types/Arrivals/StopArrivalsResponse.cs
index 4d2f481..ddc4535 100644
--- a/src/Enmarcha.Backend/Types/Arrivals/StopArrivalsResponse.cs
+++ b/src/Enmarcha.Backend/Types/Arrivals/StopArrivalsResponse.cs
@@ -18,4 +18,7 @@ public class StopArrivalsResponse
[JsonPropertyName("arrivals")]
public List<Arrival> Arrivals { get; set; } = [];
+
+ [JsonPropertyName("usage")]
+ public List<BusStopUsagePoint>? Usage { get; set; }
}