diff options
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
3 files changed, 231 insertions, 0 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs new file mode 100644 index 0000000..b519ea7 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs @@ -0,0 +1,127 @@ +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/Services/OtpService.cs b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs index 704139d..37f7e91 100644 --- a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs @@ -3,6 +3,7 @@ using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Helpers; using Costasdev.Busurbano.Backend.Types.Otp; using Costasdev.Busurbano.Backend.Types.Planner; +using Costasdev.Busurbano.Backend.Types.Transit; using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -30,6 +31,64 @@ public class OtpService _feedService = feedService; } + public RouteDto MapRoute(RoutesListResponse.RouteItem route) + { + var feedId = route.GtfsId.Split(':')[0]; + return new RouteDto + { + Id = route.GtfsId, + ShortName = _feedService.NormalizeRouteShortName(feedId, route.ShortName ?? string.Empty), + LongName = route.LongName, + Color = route.Color, + TextColor = route.TextColor, + SortOrder = route.SortOrder, + AgencyName = route.Agency?.Name, + TripCount = route.Patterns.Sum(p => p.TripsForDate.Count) + }; + } + + public RouteDetailsDto MapRouteDetails(RouteDetailsResponse.RouteItem route) + { + var feedId = route.GtfsId?.Split(':')[0] ?? "unknown"; + return new RouteDetailsDto + { + ShortName = _feedService.NormalizeRouteShortName(feedId, route.ShortName ?? string.Empty), + LongName = route.LongName, + Color = route.Color, + TextColor = route.TextColor, + Patterns = route.Patterns.Select(MapPattern).ToList() + }; + } + + private PatternDto MapPattern(RouteDetailsResponse.PatternItem pattern) + { + var feedId = pattern.Id.Split(':')[0]; + return new PatternDto + { + Id = pattern.Id, + Name = pattern.Name, + Headsign = pattern.Headsign, + DirectionId = pattern.DirectionId, + Code = pattern.Code, + SemanticHash = pattern.SemanticHash, + TripCount = pattern.TripsForDate.Count, + Geometry = DecodePolyline(pattern.PatternGeometry?.Points)?.Coordinates, + Stops = pattern.Stops.Select((s, i) => new PatternStopDto + { + Id = s.GtfsId, + Code = _feedService.NormalizeStopCode(feedId, s.Code ?? string.Empty), + Name = _feedService.NormalizeStopName(feedId, s.Name), + Lat = s.Lat, + Lon = s.Lon, + ScheduledDepartures = pattern.TripsForDate + .Select(t => t.Stoptimes.ElementAtOrDefault(i)?.ScheduledDeparture ?? -1) + .Where(d => d != -1) + .OrderBy(d => d) + .ToList() + }).ToList() + }; + } + private Leg MapLeg(OtpLeg otpLeg) { return new Leg diff --git a/src/Costasdev.Busurbano.Backend/Types/Transit/RouteDtos.cs b/src/Costasdev.Busurbano.Backend/Types/Transit/RouteDtos.cs new file mode 100644 index 0000000..f647b5b --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Types/Transit/RouteDtos.cs @@ -0,0 +1,45 @@ +namespace Costasdev.Busurbano.Backend.Types.Transit; + +public class RouteDto +{ + public required string Id { get; set; } + public string? ShortName { get; set; } + public string? LongName { get; set; } + public string? Color { get; set; } + public string? TextColor { get; set; } + public int? SortOrder { get; set; } + public string? AgencyName { get; set; } + public int TripCount { get; set; } +} + +public class RouteDetailsDto +{ + public string? ShortName { get; set; } + public string? LongName { get; set; } + public string? Color { get; set; } + public string? TextColor { get; set; } + public List<PatternDto> Patterns { get; set; } = []; +} + +public class PatternDto +{ + public required string Id { get; set; } + public string? Name { get; set; } + public string? Headsign { get; set; } + public int DirectionId { get; set; } + public string? Code { get; set; } + public string? SemanticHash { get; set; } + public int TripCount { get; set; } + public List<List<double>>? Geometry { get; set; } + public List<PatternStopDto> Stops { get; set; } = []; +} + +public class PatternStopDto +{ + public required string Id { get; set; } + public string? Code { get; set; } + public required string Name { get; set; } + public double Lat { get; set; } + public double Lon { get; set; } + public List<int> ScheduledDepartures { get; set; } = []; +} |
