aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Services
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-03-19 18:56:34 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-03-19 18:56:34 +0100
commitbee85bf92aab84087798ffa9f3f16336acef2fce (patch)
tree4fc8e2907e6618940cd9bdeb3da1a81172aab459 /src/Enmarcha.Backend/Services
parentfed5d57b9e5d3df7c34bccb7a120bfa274b2039a (diff)
Basic backoffice for alert management
Diffstat (limited to 'src/Enmarcha.Backend/Services')
-rw-r--r--src/Enmarcha.Backend/Services/BackofficeSelectorService.cs117
-rw-r--r--src/Enmarcha.Backend/Services/OtpService.cs2
-rw-r--r--src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs2
3 files changed, 119 insertions, 2 deletions
diff --git a/src/Enmarcha.Backend/Services/BackofficeSelectorService.cs b/src/Enmarcha.Backend/Services/BackofficeSelectorService.cs
new file mode 100644
index 0000000..d09e207
--- /dev/null
+++ b/src/Enmarcha.Backend/Services/BackofficeSelectorService.cs
@@ -0,0 +1,117 @@
+using Enmarcha.Backend.Configuration;
+using Enmarcha.Sources.OpenTripPlannerGql;
+using Enmarcha.Sources.OpenTripPlannerGql.Queries;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+
+namespace Enmarcha.Backend.Services;
+
+public class BackofficeSelectorService(
+ HttpClient httpClient,
+ IOptions<AppConfiguration> config,
+ IMemoryCache cache,
+ ILogger<BackofficeSelectorService> logger)
+{
+ public async Task<SelectorTransitData> GetTransitDataAsync()
+ {
+ const string cacheKey = "backoffice_transit";
+ if (cache.TryGetValue(cacheKey, out SelectorTransitData? cached) && cached is not null)
+ return cached;
+
+ var feeds = config.Value.OtpFeeds;
+ var today = DateTime.Today.ToString("yyyy-MM-dd");
+ var query = RoutesListContent.Query(new RoutesListContent.Args(feeds, today));
+
+ List<RoutesListResponse.RouteItem> routes = [];
+ try
+ {
+ var req = new HttpRequestMessage(HttpMethod.Post, $"{config.Value.OpenTripPlannerBaseUrl}/gtfs/v1");
+ req.Content = JsonContent.Create(new GraphClientRequest { Query = query });
+ var resp = await httpClient.SendAsync(req);
+ resp.EnsureSuccessStatusCode();
+ var body = await resp.Content.ReadFromJsonAsync<GraphClientResponse<RoutesListResponse>>();
+ routes = body?.Data?.Routes ?? [];
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to fetch routes from OTP");
+ }
+
+ var routeDtos = routes
+ .Select(r =>
+ {
+ var (feedId, routeId) = SplitGtfsId(r.GtfsId);
+ var color = NormalizeColor(r.Color);
+ return new SelectorRouteItem(feedId, r.GtfsId, $"route#{feedId}:{routeId}", r.ShortName, r.LongName, r.Agency?.Name, color);
+ })
+ .OrderBy(r => r.ShortName)
+ .ToList();
+
+ var agencyDtos = routeDtos
+ .Where(r => r.AgencyName is not null)
+ .GroupBy(r => r.FeedId)
+ .Select(g => new SelectorAgencyItem(g.Key, $"agency#{g.Key}", g.First().AgencyName!))
+ .ToList();
+
+ var result = new SelectorTransitData(agencyDtos, routeDtos);
+ cache.Set(cacheKey, result, TimeSpan.FromHours(1));
+ return result;
+ }
+
+ public async Task<List<SelectorStopItem>> GetStopsByBboxAsync(
+ double minLon, double minLat, double maxLon, double maxLat)
+ {
+ // Cache per coarse grid (~0.1° cells, roughly 8 km) to reuse across small pans
+ var cacheKey = $"stops_{minLon:F1}_{minLat:F1}_{maxLon:F1}_{maxLat:F1}";
+ if (cache.TryGetValue(cacheKey, out List<SelectorStopItem>? cached) && cached is not null)
+ return cached;
+
+ var query = StopTileRequestContent.Query(
+ new StopTileRequestContent.TileRequestParams(minLon, minLat, maxLon, maxLat));
+ try
+ {
+ var req = new HttpRequestMessage(HttpMethod.Post, $"{config.Value.OpenTripPlannerBaseUrl}/gtfs/v1");
+ req.Content = JsonContent.Create(new GraphClientRequest { Query = query });
+ var resp = await httpClient.SendAsync(req);
+ resp.EnsureSuccessStatusCode();
+ var body = await resp.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>();
+ var stops = (body?.Data?.StopsByBbox ?? [])
+ .Select(s =>
+ {
+ var (feedId, stopId) = SplitGtfsId(s.GtfsId);
+ var routeItems = (s.Routes ?? []).Select(r =>
+ {
+ var (rf, ri) = SplitGtfsId(r.GtfsId);
+ return new SelectorRouteItem(rf, r.GtfsId, $"route#{rf}:{ri}", r.ShortName, null, null, NormalizeColor(r.Color));
+ }).ToList();
+ return new SelectorStopItem(s.GtfsId, $"stop#{feedId}:{stopId}", s.Name, s.Code, s.Lat, s.Lon, routeItems);
+ })
+ .ToList();
+ cache.Set(cacheKey, stops, TimeSpan.FromMinutes(30));
+ return stops;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to fetch stops from OTP for bbox {MinLon},{MinLat} to {MaxLon},{MaxLat}",
+ minLon, minLat, maxLon, maxLat);
+ return [];
+ }
+ }
+
+ private static (string FeedId, string EntityId) SplitGtfsId(string gtfsId)
+ {
+ var parts = gtfsId.Split(':', 2);
+ return (parts[0], parts.Length > 1 ? parts[1] : gtfsId);
+ }
+
+ private static string? NormalizeColor(string? color)
+ {
+ if (string.IsNullOrWhiteSpace(color)) return null;
+ return color.StartsWith('#') ? color : '#' + color;
+ }
+}
+
+public record SelectorTransitData(List<SelectorAgencyItem> Agencies, List<SelectorRouteItem> Routes);
+public record SelectorAgencyItem(string FeedId, string Selector, string Name);
+public record SelectorRouteItem(string FeedId, string GtfsId, string Selector, string? ShortName, string? LongName, string? AgencyName, string? Color);
+public record SelectorStopItem(string GtfsId, string Selector, string Name, string? Code, double Lat, double Lon, List<SelectorRouteItem> Routes);
diff --git a/src/Enmarcha.Backend/Services/OtpService.cs b/src/Enmarcha.Backend/Services/OtpService.cs
index 8724cda..16ba029 100644
--- a/src/Enmarcha.Backend/Services/OtpService.cs
+++ b/src/Enmarcha.Backend/Services/OtpService.cs
@@ -125,7 +125,7 @@ public class OtpService
try
{
- var bbox = new StopTileRequestContent.Bbox(minLon, minLat, maxLon, maxLat);
+ var bbox = new StopTileRequestContent.TileRequestParams(minLon, minLat, maxLon, maxLat);
var query = StopTileRequestContent.Query(bbox);
var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
diff --git a/src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs b/src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs
index 3e62264..733be92 100644
--- a/src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs
+++ b/src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs
@@ -20,7 +20,7 @@ public class XuntaFareProvider
public XuntaFareProvider(IWebHostEnvironment env)
{
- var filePath = Path.Combine(env.ContentRootPath, "Data", "xunta_fares.csv");
+ var filePath = Path.Combine(env.ContentRootPath, "Content", "xunta_fares.csv");
using var reader = new StreamReader(filePath);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);