diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-29 00:41:52 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-29 00:41:52 +0100 |
| commit | a304c24b32c0327436bbd8c2853e60668e161b42 (patch) | |
| tree | 08f65c05daca134cf4d2e4f779bd15d98fd66370 /src/Costasdev.Busurbano.Backend/Controllers | |
| parent | 120a3c6bddd0fb8d9fa05df4763596956554c025 (diff) | |
Rename a lot of stuff, add Santiago real time
Diffstat (limited to 'src/Costasdev.Busurbano.Backend/Controllers')
6 files changed, 0 insertions, 952 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs deleted file mode 100644 index a0e8505..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.Net; -using Costasdev.Busurbano.Backend.Configuration; -using Costasdev.Busurbano.Backend.Helpers; -using Costasdev.Busurbano.Backend.Services; -using Costasdev.Busurbano.Backend.Types; -using Costasdev.Busurbano.Backend.Types.Arrivals; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api/stops")] -public partial class ArrivalsController : ControllerBase -{ - private readonly ILogger<ArrivalsController> _logger; - private readonly IMemoryCache _cache; - private readonly HttpClient _httpClient; - private readonly ArrivalsPipeline _pipeline; - private readonly FeedService _feedService; - private readonly AppConfiguration _config; - - public ArrivalsController( - ILogger<ArrivalsController> logger, - IMemoryCache cache, - HttpClient httpClient, - ArrivalsPipeline pipeline, - FeedService feedService, - IOptions<AppConfiguration> configOptions - ) - { - _logger = logger; - _cache = cache; - _httpClient = httpClient; - _pipeline = pipeline; - _feedService = feedService; - _config = configOptions.Value; - } - - [HttpGet("arrivals")] - public async Task<IActionResult> GetArrivals( - [FromQuery] string id, - [FromQuery] bool reduced - ) - { - var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); - var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz); - var todayLocal = nowLocal.Date; - - var requestContent = ArrivalsAtStopContent.Query( - new ArrivalsAtStopContent.Args(id, reduced) - ); - - var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1"); - request.Content = JsonContent.Create(new GraphClientRequest - { - Query = requestContent - }); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<ArrivalsAtStopResponse>>(); - - if (responseBody is not { IsSuccess: true } || responseBody.Data?.Stop == null) - { - LogErrorFetchingStopData(response.StatusCode, await response.Content.ReadAsStringAsync()); - return StatusCode(500, "Error fetching stop data"); - } - - var stop = responseBody.Data.Stop; - _logger.LogInformation("Fetched {Count} arrivals for stop {StopName} ({StopId})", stop.Arrivals.Count, stop.Name, id); - - List<Arrival> arrivals = []; - foreach (var item in stop.Arrivals) - { - // Discard trip without pickup at stop - if (item.PickupTypeParsed.Equals(ArrivalsAtStopResponse.PickupType.None)) - { - continue; - } - - // Discard on last stop - if (item.Trip.ArrivalStoptime.Stop.GtfsId == id) - { - continue; - } - - if (item.Trip.Geometry?.Points != null) - { - _logger.LogDebug("Trip {TripId} has geometry", item.Trip.GtfsId); - } - - // Calculate departure time using the service day in the feed's timezone (Europe/Madrid) - // This ensures we treat ScheduledDepartureSeconds as relative to the local midnight of the service day - var serviceDayLocal = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(item.ServiceDay), tz); - var departureTime = serviceDayLocal.Date.AddSeconds(item.ScheduledDepartureSeconds); - - var minutesToArrive = (int)(departureTime - nowLocal).TotalMinutes; - //var isRunning = departureTime < nowLocal; - - Arrival arrival = new() - { - TripId = item.Trip.GtfsId, - Route = new RouteInfo - { - GtfsId = item.Trip.Route.GtfsId, - ShortName = item.Trip.RouteShortName, - Colour = item.Trip.Route.Color ?? "FFFFFF", - TextColour = item.Trip.Route.TextColor ?? "000000" - }, - Headsign = new HeadsignInfo - { - Destination = item.Headsign - }, - Estimate = new ArrivalDetails - { - Minutes = minutesToArrive, - Precision = departureTime < nowLocal.AddMinutes(-1) ? ArrivalPrecision.Past : ArrivalPrecision.Scheduled - }, - RawOtpTrip = item - }; - - arrivals.Add(arrival); - } - - await _pipeline.ExecuteAsync(new ArrivalsContext - { - StopId = id, - StopCode = stop.Code, - IsReduced = reduced, - Arrivals = arrivals, - NowLocal = nowLocal, - StopLocation = new Position { Latitude = stop.Lat, Longitude = stop.Lon } - }); - - var feedId = id.Split(':')[0]; - - // Time after an arrival's time to still include it in the response. This is useful without real-time data, for delayed buses. - var timeThreshold = GetThresholdForFeed(id); - - var (fallbackColor, fallbackTextColor) = _feedService.GetFallbackColourForFeed(feedId); - - return Ok(new StopArrivalsResponse - { - StopCode = _feedService.NormalizeStopCode(feedId, stop.Code), - StopName = _feedService.NormalizeStopName(feedId, stop.Name), - StopLocation = new Position - { - Latitude = stop.Lat, - Longitude = stop.Lon - }, - Routes = [.. stop.Routes - .OrderBy( - r => r.ShortName, - Comparer<string?>.Create(SortingHelper.SortRouteShortNames) - ) - .Select(r => new RouteInfo - { - GtfsId = r.GtfsId, - ShortName = _feedService.NormalizeRouteShortName(feedId, r.ShortName ?? ""), - Colour = r.Color ?? fallbackColor, - TextColour = r.TextColor is null or "000000" ? - ContrastHelper.GetBestTextColour(r.Color ?? fallbackColor) : - r.TextColor - })], - Arrivals = [.. arrivals.Where(a => a.Estimate.Minutes >= timeThreshold)] - }); - - } - - private static int GetThresholdForFeed(string id) - { - string feedId = id.Split(':', 2)[0]; - - if (feedId is "vitrasa" or "coruna") - { - return 0; - } - - return -30; - } - - [HttpGet] - public async Task<IActionResult> GetStops([FromQuery] string ids) - { - if (string.IsNullOrWhiteSpace(ids)) - { - return BadRequest("Ids parameter is required"); - } - - var stopIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var requestContent = StopsInfoContent.Query(new StopsInfoContent.Args(stopIds)); - - var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1"); - request.Content = JsonContent.Create(new GraphClientRequest - { - Query = requestContent - }); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopsInfoResponse>>(); - - if (responseBody is not { IsSuccess: true } || responseBody.Data?.Stops == null) - { - return StatusCode(500, "Error fetching stops data"); - } - - var result = responseBody.Data.Stops.ToDictionary( - s => s.GtfsId, - s => - { - var feedId = s.GtfsId.Split(':', 2)[0]; - var (fallbackColor, _) = _feedService.GetFallbackColourForFeed(feedId); - - return new - { - id = s.GtfsId, - code = _feedService.NormalizeStopCode(feedId, s.Code ?? ""), - name = s.Name, - routes = s.Routes - .OrderBy(r => r.ShortName, Comparer<string?>.Create(SortingHelper.SortRouteShortNames)) - .Select(r => new - { - shortName = _feedService.NormalizeRouteShortName(feedId, r.ShortName ?? ""), - colour = r.Color ?? fallbackColor, - textColour = r.TextColor is null or "000000" ? - ContrastHelper.GetBestTextColour(r.Color ?? fallbackColor) : - r.TextColor - }) - .ToList() - }; - } - ); - - return Ok(result); - } - - [HttpGet("search")] - public IActionResult SearchStops([FromQuery] string q) - { - // Placeholder for future implementation with Postgres and fuzzy searching - return Ok(new List<object>()); - } - - [LoggerMessage(LogLevel.Error, "Error fetching stop data, received {statusCode} {responseBody}")] - partial void LogErrorFetchingStopData(HttpStatusCode statusCode, string responseBody); -} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs deleted file mode 100644 index a7faf44..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Net; -using Costasdev.Busurbano.Backend.Configuration; -using Costasdev.Busurbano.Backend.Services; -using Costasdev.Busurbano.Backend.Types.Planner; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api/planner")] -public partial class RoutePlannerController : ControllerBase -{ - private readonly ILogger<RoutePlannerController> _logger; - private readonly OtpService _otpService; - private readonly IGeocodingService _geocodingService; - private readonly AppConfiguration _config; - private readonly HttpClient _httpClient; - - public RoutePlannerController( - ILogger<RoutePlannerController> logger, - OtpService otpService, - IGeocodingService geocodingService, - IOptions<AppConfiguration> config, - HttpClient httpClient - ) - { - _logger = logger; - _otpService = otpService; - _geocodingService = geocodingService; - _config = config.Value; - _httpClient = httpClient; - } - - [HttpGet("autocomplete")] - public async Task<ActionResult<List<PlannerSearchResult>>> Autocomplete([FromQuery] string query) - { - if (string.IsNullOrWhiteSpace(query)) - { - return BadRequest("Query cannot be empty"); - } - - var results = await _geocodingService.GetAutocompleteAsync(query); - return Ok(results); - } - - [HttpGet("reverse")] - public async Task<ActionResult<PlannerSearchResult>> Reverse([FromQuery] double lat, [FromQuery] double lon) - { - var result = await _geocodingService.GetReverseGeocodeAsync(lat, lon); - if (result == null) - { - return NotFound(); - } - return Ok(result); - } - - [HttpGet("plan")] - public async Task<ActionResult<RoutePlan>> Plan( - [FromQuery] double fromLat, - [FromQuery] double fromLon, - [FromQuery] double toLat, - [FromQuery] double toLon, - [FromQuery] DateTimeOffset? time, - [FromQuery] bool arriveBy = false) - { - try - { - var requestContent = PlanConnectionContent.Query( - new PlanConnectionContent.Args(fromLat, fromLon, toLat, toLon, time ?? DateTimeOffset.Now, arriveBy) - ); - - var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1"); - request.Content = JsonContent.Create(new GraphClientRequest - { - Query = requestContent - }); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<PlanConnectionResponse>>(); - - if (responseBody is not { IsSuccess: true }) - { - LogErrorFetchingRoutes(response.StatusCode, await response.Content.ReadAsStringAsync()); - return StatusCode(500, "An error occurred while planning the route."); - } - - var plan = _otpService.MapPlanResponse(responseBody.Data!); - return Ok(plan); - } - catch (Exception e) - { - _logger.LogError("Exception planning route: {e}", e); - return StatusCode(500, "An error occurred while planning the route."); - } - } - - [LoggerMessage(LogLevel.Error, "Error fetching route planning, received {statusCode} {responseBody}")] - partial void LogErrorFetchingRoutes(HttpStatusCode? statusCode, string responseBody); -} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs deleted file mode 100644 index 0e550a3..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs +++ /dev/null @@ -1,217 +0,0 @@ -using NetTopologySuite.Features; -using NetTopologySuite.IO.VectorTiles; -using NetTopologySuite.IO.VectorTiles.Mapbox; - -using Microsoft.AspNetCore.Mvc; - -using Microsoft.Extensions.Caching.Memory; -using System.Text.Json; -using Costasdev.Busurbano.Backend.Helpers; -using Costasdev.Busurbano.Backend.Services; -using Costasdev.Busurbano.Backend.Configuration; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; -using Microsoft.Extensions.Options; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api/tiles")] -public class TileController : ControllerBase -{ - private readonly ILogger<TileController> _logger; - private readonly IMemoryCache _cache; - private readonly HttpClient _httpClient; - private readonly FeedService _feedService; - private readonly AppConfiguration _config; - - public TileController( - ILogger<TileController> logger, - IMemoryCache cache, - HttpClient httpClient, - FeedService feedService, - IOptions<AppConfiguration> configOptions - ) - { - _logger = logger; - _cache = cache; - _httpClient = httpClient; - _feedService = feedService; - _config = configOptions.Value; - } - - [HttpGet("stops/{z:int}/{x:int}/{y:int}")] - public async Task<IActionResult> Stops(int z, int x, int y) - { - if (z is < 9 or > 20) - { - return BadRequest("Zoom level out of range (9-20)"); - } - - var cacheHit = _cache.TryGetValue($"stops-tile-{z}-{x}-{y}", out byte[]? cachedTile); - if (cacheHit && cachedTile != null) - { - Response.Headers.Append("X-Cache-Hit", "true"); - return File(cachedTile, "application/x-protobuf"); - } - - // Calculate bounding box in EPSG:4326 - var n = Math.Pow(2, z); - var lonMin = x / n * 360.0 - 180.0; - var lonMax = (x + 1) / n * 360.0 - 180.0; - - var latMaxRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n))); - var latMax = latMaxRad * 180.0 / Math.PI; - - var latMinRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n))); - var latMin = latMinRad * 180.0 / Math.PI; - - var requestContent = StopTileRequestContent.Query(new StopTileRequestContent.Bbox(lonMin, latMin, lonMax, latMax)); - var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1"); - request.Content = JsonContent.Create(new GraphClientRequest - { - Query = requestContent - }); - - var response = await _httpClient.SendAsync(request); - var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>(); - - if (responseBody is not { IsSuccess: true }) - { - _logger.LogError( - "Error fetching stop data, received {StatusCode} {ResponseBody}", - response.StatusCode, - await response.Content.ReadAsStringAsync() - ); - return StatusCode(500, "Error fetching stop data"); - } - - var tileDef = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, z); - VectorTile vt = new() { TileId = tileDef.Id }; - var stopsLayer = new Layer { Name = "stops" }; - - responseBody.Data?.StopsByBbox?.ForEach(stop => - { - var idParts = stop.GtfsId.Split(':', 2); - string feedId = idParts[0]; - string codeWithinFeed = _feedService.NormalizeStopCode(feedId, stop.Code ?? string.Empty); - - if (_feedService.IsStopHidden($"{feedId}:{codeWithinFeed}")) - { - return; - } - - // TODO: Duplicate from ArrivalsController - var (Color, TextColor) = _feedService.GetFallbackColourForFeed(idParts[0]); - var distinctRoutes = GetDistinctRoutes(feedId, stop.Routes ?? []); - - Feature feature = new() - { - Geometry = new NetTopologySuite.Geometries.Point(stop.Lon, stop.Lat), - Attributes = new AttributesTable - { - // The ID will be used to request the arrivals - { "id", stop.GtfsId }, - // The feed is the first part of the GTFS ID - { "feed", idParts[0] }, - // The public identifier, usually feed:code or feed:id, recognisable by users and in other systems - { "code", $"{idParts[0]}:{codeWithinFeed}" }, - { "name", _feedService.NormalizeStopName(feedId, stop.Name) }, - { "icon", GetIconNameForFeed(feedId) }, - { "transitKind", GetTransitKind(feedId) }, - // Routes - { "routes", JsonSerializer - .Serialize( - distinctRoutes.Select(r => { - var colour = r.Color ?? Color; - string textColour; - - if (r.Color is null) // None is present, use fallback - { - textColour = TextColor; - } - else if (r.TextColor is null || r.TextColor.EndsWith("000000")) - { - // Text colour not provided, or default-black; check the better contrasting - textColour = ContrastHelper.GetBestTextColour(colour); - } - else - { - // Use provided text colour - textColour = r.TextColor; - } - - return new { - shortName = r.ShortName, - colour, - textColour - }; - })) - } - } - }; - - stopsLayer.Features.Add(feature); - }); - - vt.Layers.Add(stopsLayer); - - using var ms = new MemoryStream(); - vt.Write(ms, minLinealExtent: 1, minPolygonalExtent: 2); - - _cache.Set($"stops-tile-{z}-{x}-{y}", ms.ToArray(), TimeSpan.FromMinutes(15)); - Response.Headers.Append("X-Cache-Hit", "false"); - - return File(ms.ToArray(), "application/x-protobuf"); - } - - private string GetIconNameForFeed(string feedId) - { - return feedId switch - { - "vitrasa" => "stop-vitrasa", - "santiago" => "stop-santiago", - "coruna" => "stop-coruna", - "xunta" => "stop-xunta", - "renfe" => "stop-renfe", - "feve" => "stop-feve", - _ => "stop-generic", - }; - } - - private string GetTransitKind(string feedId) - { - return feedId switch - { - "vitrasa" or "santiago" or "coruna" => "bus", - "xunta" => "coach", - "renfe" or "feve" => "train", - _ => "unknown", - }; - } - - private List<StopTileResponse.Route> GetDistinctRoutes(string feedId, List<StopTileResponse.Route> routes) - { - List<StopTileResponse.Route> distinctRoutes = []; - HashSet<string> seen = new(); - - foreach (var route in routes) - { - var seenId = _feedService.GetUniqueRouteShortName(feedId, route.ShortName ?? string.Empty); - route.ShortName = seenId; - - if (seen.Contains(seenId)) - { - continue; - } - - seen.Add(seenId); - distinctRoutes.Add(route); - } - - return [.. distinctRoutes.OrderBy( - r => r.ShortName, - Comparer<string?>.Create(SortingHelper.SortRouteShortNames) - )]; - } -} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs deleted file mode 100644 index 0542054..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Globalization; -using System.Text; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using NetTopologySuite.Features; -using NetTopologySuite.IO; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api")] -public class TrafficDataController : ControllerBase -{ - private readonly ILogger<TrafficDataController> _logger; - private readonly IMemoryCache _cache; - private readonly HttpClient _httpClient; - - public TrafficDataController( - ILogger<TrafficDataController> logger, - IMemoryCache cache, - HttpClient httpClient - ) - { - _logger = logger; - _cache = cache; - _httpClient = httpClient; - } - - [HttpGet("traffic")] - public async Task<IActionResult> Get() - { - var trafficData = _cache.GetOrCreate("vigo-traffic-geojson", entry => - { - var data = GetTrafficData(); - - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); - - return data.Result; - }); - - if (string.IsNullOrEmpty(trafficData)) - { - return StatusCode(404); - } - - return Content(trafficData, "application/json", Encoding.UTF8); - } - - private async Task<string> GetTrafficData() - { - var resp = await _httpClient.GetAsync("https://datos.vigo.org/data/trafico/treal.geojson"); - var body = resp.Content.ReadAsStringAsync().Result; - - var reader = new GeoJsonReader(); - var featureCollection = reader.Read<FeatureCollection>(body); - - var filteredFeatures = new FeatureCollection(); - foreach (var kvp in featureCollection) - { - var newAttributes = new AttributesTable(); - - if ( - !kvp.Attributes.Exists("actualizacion") || - !kvp.Attributes.Exists("style") - ) - { - continue; - } - - var updateParsed = DateTime.TryParseExact( - kvp.Attributes["actualizacion"].ToString(), - "dd/MM/yyyy H:mm:ss", - null, - DateTimeStyles.None, - out var updatedAt - ); - - if (!updateParsed || updatedAt < DateTime.Today) - { - continue; - } - - var style = kvp.Attributes["style"].ToString(); - - if (style == "#SINDATOS") - { - continue; - } - - var vehiculosAttribute = (kvp.Attributes["vehiculos"] ?? "0").ToString(); - - var vehiclesParsed = int.TryParse(vehiculosAttribute, out var vehicles); - if (!vehiclesParsed || vehicles <= 0) - { - continue; - } - - newAttributes.Add("updatedAt", updatedAt.ToString("O")); - newAttributes.Add("style", style); - newAttributes.Add("vehicles", vehicles); - - kvp.Attributes = newAttributes; - filteredFeatures.Add(kvp); - } - - var writer = new GeoJsonWriter(); - - return writer.Write(filteredFeatures); - } -} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs deleted file mode 100644 index b519ea7..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Costasdev.Busurbano.Backend.Configuration; -using Costasdev.Busurbano.Backend.Helpers; -using Costasdev.Busurbano.Backend.Services; -using Costasdev.Busurbano.Backend.Types.Transit; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql; -using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api/transit")] -public class TransitController : ControllerBase -{ - private readonly ILogger<TransitController> _logger; - private readonly OtpService _otpService; - private readonly AppConfiguration _config; - private readonly HttpClient _httpClient; - private readonly IMemoryCache _cache; - - public TransitController( - ILogger<TransitController> logger, - OtpService otpService, - IOptions<AppConfiguration> config, - HttpClient httpClient, - IMemoryCache cache - ) - { - _logger = logger; - _otpService = otpService; - _config = config.Value; - _httpClient = httpClient; - _cache = cache; - } - - [HttpGet("routes")] - public async Task<ActionResult<List<RouteDto>>> GetRoutes([FromQuery] string[] feeds) - { - if (feeds.Length == 0) - { - feeds = ["santiago", "vitrasa", "coruna", "feve"]; - } - - var serviceDate = DateTime.Now.ToString("yyyy-MM-dd"); - var cacheKey = $"routes_{string.Join("_", feeds)}_{serviceDate}"; - if (_cache.TryGetValue(cacheKey, out List<RouteDto>? cachedRoutes)) - { - return Ok(cachedRoutes); - } - - try - { - var query = RoutesListContent.Query(new RoutesListContent.Args(feeds, serviceDate)); - var response = await SendOtpQueryAsync<RoutesListResponse>(query); - - if (response?.Data == null) - { - return StatusCode(500, "Failed to fetch routes from OTP."); - } - - var routes = response.Data.Routes - .Select(_otpService.MapRoute) - .Where(r => r.TripCount > 0) - .OrderBy(r => r.ShortName, Comparer<string?>.Create(SortingHelper.SortRouteShortNames)) - .ToList(); - - _cache.Set(cacheKey, routes, TimeSpan.FromHours(1)); - - return Ok(routes); - } - catch (Exception e) - { - _logger.LogError(e, "Error fetching routes"); - return StatusCode(500, "An error occurred while fetching routes."); - } - } - - [HttpGet("routes/{id}")] - public async Task<ActionResult<RouteDetailsDto>> GetRouteDetails(string id) - { - var serviceDate = DateTime.Now.ToString("yyyy-MM-dd"); - var cacheKey = $"route_details_{id}_{serviceDate}"; - - if (_cache.TryGetValue(cacheKey, out RouteDetailsDto? cachedDetails)) - { - return Ok(cachedDetails); - } - - try - { - var query = RouteDetailsContent.Query(new RouteDetailsContent.Args(id, serviceDate)); - var response = await SendOtpQueryAsync<RouteDetailsResponse>(query); - - if (response?.Data?.Route == null) - { - return NotFound(); - } - - var details = _otpService.MapRouteDetails(response.Data.Route); - _cache.Set(cacheKey, details, TimeSpan.FromHours(1)); - - return Ok(details); - } - catch (Exception e) - { - _logger.LogError(e, "Error fetching route details for {Id}", id); - return StatusCode(500, "An error occurred while fetching route details."); - } - } - - private async Task<GraphClientResponse<T>?> SendOtpQueryAsync<T>(string query) where T : AbstractGraphResponse - { - var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1"); - request.Content = JsonContent.Create(new GraphClientRequest { Query = query }); - - var response = await _httpClient.SendAsync(request); - if (!response.IsSuccessStatusCode) - { - _logger.LogError("OTP query failed with status {StatusCode}", response.StatusCode); - return null; - } - - return await response.Content.ReadFromJsonAsync<GraphClientResponse<T>>(); - } -} diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs deleted file mode 100644 index 642cccb..0000000 --- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Costasdev.Busurbano.Backend.Configuration; -using Costasdev.Busurbano.Backend.Services; -using Costasdev.Busurbano.Backend.Services.Providers; -using Costasdev.VigoTransitApi; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Costasdev.Busurbano.Backend.Controllers; - -[ApiController] -[Route("api/vigo")] -public partial class VigoController : ControllerBase -{ - private readonly ILogger<VigoController> _logger; - private readonly VigoTransitApiClient _api; - private readonly AppConfiguration _configuration; - private readonly ShapeTraversalService _shapeService; - private readonly VitrasaTransitProvider _vitrasaProvider; - private readonly RenfeTransitProvider _renfeProvider; - - public VigoController( - HttpClient http, - IOptions<AppConfiguration> options, - ILogger<VigoController> logger, - ShapeTraversalService shapeService, - VitrasaTransitProvider vitrasaProvider, - RenfeTransitProvider renfeProvider) - { - _logger = logger; - _api = new VigoTransitApiClient(http); - _configuration = options.Value; - _shapeService = shapeService; - _vitrasaProvider = vitrasaProvider; - _renfeProvider = renfeProvider; - } - - [HttpGet("GetShape")] - public async Task<IActionResult> GetShape( - [FromQuery] string shapeId, - [FromQuery] int? startPointIndex = null, - [FromQuery] double? busLat = null, - [FromQuery] double? busLon = null, - [FromQuery] int? busShapeIndex = null, - [FromQuery] double? stopLat = null, - [FromQuery] double? stopLon = null, - [FromQuery] int? stopShapeIndex = null - ) - { - var path = await _shapeService.GetShapePathAsync(shapeId, 0); - if (path == null) - { - return NotFound(); - } - - // Determine bus point - object? busPoint = null; - if (busShapeIndex.HasValue && busShapeIndex.Value >= 0 && busShapeIndex.Value < path.Count) - { - var p = path[busShapeIndex.Value]; - busPoint = new { lat = p.Latitude, lon = p.Longitude, index = busShapeIndex.Value }; - } - else if (busLat.HasValue && busLon.HasValue) - { - var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, busLat.Value, busLon.Value); - if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count) - { - var p = path[idx.Value]; - busPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value }; - } - } - else if (startPointIndex.HasValue && startPointIndex.Value >= 0 && startPointIndex.Value < path.Count) - { - var p = path[startPointIndex.Value]; - busPoint = new { lat = p.Latitude, lon = p.Longitude, index = startPointIndex.Value }; - } - - // Determine stop point - object? stopPoint = null; - if (stopShapeIndex.HasValue && stopShapeIndex.Value >= 0 && stopShapeIndex.Value < path.Count) - { - var p = path[stopShapeIndex.Value]; - stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = stopShapeIndex.Value }; - } - else if (stopLat.HasValue && stopLon.HasValue) - { - var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, stopLat.Value, stopLon.Value); - if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count) - { - var p = path[idx.Value]; - stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value }; - } - } - - // Convert to GeoJSON LineString - var coordinates = path.Select(p => new[] { p.Longitude, p.Latitude }).ToList(); - - var geoJson = new - { - type = "Feature", - geometry = new - { - type = "LineString", - coordinates = coordinates - }, - properties = new - { - busPoint, - stopPoint - } - }; - - return Ok(geoJson); - } - - [HttpGet("GetConsolidatedCirculations")] - public async Task<IActionResult> GetConsolidatedCirculations( - [FromQuery] string stopId - ) - { - // Use Europe/Madrid timezone consistently to avoid UTC/local skew - var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); - var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz); - - ITransitProvider provider; - string effectiveStopId; - - if (stopId.StartsWith("renfe:")) - { - provider = _renfeProvider; - effectiveStopId = stopId.Substring("renfe:".Length); - } - else if (stopId.StartsWith("vitrasa:")) - { - provider = _vitrasaProvider; - effectiveStopId = stopId.Substring("vitrasa:".Length); - } - else - { - // Legacy/Default - provider = _vitrasaProvider; - effectiveStopId = stopId; - } - - var result = await provider.GetCirculationsAsync(effectiveStopId, nowLocal); - return Ok(result); - } -} |
