aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend
diff options
context:
space:
mode:
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs127
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/OtpService.cs59
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/Transit/RouteDtos.cs45
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; } = [];
+}