diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-19 18:56:34 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-19 18:56:34 +0100 |
| commit | bee85bf92aab84087798ffa9f3f16336acef2fce (patch) | |
| tree | 4fc8e2907e6618940cd9bdeb3da1a81172aab459 /src/Enmarcha.Backend/Services | |
| parent | fed5d57b9e5d3df7c34bccb7a120bfa274b2039a (diff) | |
Basic backoffice for alert management
Diffstat (limited to 'src/Enmarcha.Backend/Services')
| -rw-r--r-- | src/Enmarcha.Backend/Services/BackofficeSelectorService.cs | 117 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Services/OtpService.cs | 2 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Services/Providers/XuntaFareProvider.cs | 2 |
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); |
