diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-22 18:16:57 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-22 18:16:57 +0100 |
| commit | 4b7eaa318f22d7cc768491c421cb7aeac477f95d (patch) | |
| tree | 0b39abce444679396475e4f48885479e2ae0650f /src/Costasdev.Busurbano.Backend | |
| parent | 91f7d7dd5a4ca8453cfdbc9a3beeb216b6638ef7 (diff) | |
Implement retrieving next arrivals for a stop (scheduled only)
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs | 68 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs | 110 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Program.cs | 9 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs | 82 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs | 15 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs | 4 | ||||
| -rw-r--r-- | src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs (renamed from src/Costasdev.Busurbano.Backend/Types/Planner/PlannerModels.cs) | 0 |
7 files changed, 245 insertions, 43 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs index eb81784..5dee48d 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs @@ -1,13 +1,16 @@ -using Costasdev.Busurbano.Backend.GraphClient; +using System.Net; +using Costasdev.Busurbano.Backend.GraphClient; using Costasdev.Busurbano.Backend.GraphClient.App; +using Costasdev.Busurbano.Backend.Types; +using Costasdev.Busurbano.Backend.Types.Arrivals; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; namespace Costasdev.Busurbano.Backend.Controllers; [ApiController] -[Route("api")] -public class ArrivalsController : ControllerBase +[Route("api/stops")] +public partial class ArrivalsController : ControllerBase { private readonly ILogger<ArrivalsController> _logger; private readonly IMemoryCache _cache; @@ -25,9 +28,16 @@ public class ArrivalsController : ControllerBase } [HttpGet("arrivals")] - public async Task<IActionResult> GetArrivals(string id) + public async Task<IActionResult> GetArrivals( + [FromQuery] string id, + [FromQuery] bool reduced + ) { - var requestContent = ArrivalsAtStopContent.Query(id); + var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); + var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz); + var todayLocal = nowLocal.Date; + + var requestContent = ArrivalsAtStopContent.Query(new(id, reduced ? 4 : 10)); var request = new HttpRequestMessage(HttpMethod.Post, "http://100.67.54.115:3957/otp/gtfs/v1"); request.Content = JsonContent.Create(new GraphClientRequest { @@ -37,16 +47,50 @@ public class ArrivalsController : ControllerBase var response = await _httpClient.SendAsync(request); var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<ArrivalsAtStopResponse>>(); - if (responseBody is not { IsSuccess: true }) + if (responseBody is not { IsSuccess: true } || responseBody.Data?.Stop == null) { - _logger.LogError( - "Error fetching stop data, received {StatusCode} {ResponseBody}", - response.StatusCode, - await response.Content.ReadAsStringAsync() - ); + LogErrorFetchingStopData(response.StatusCode, await response.Content.ReadAsStringAsync()); return StatusCode(500, "Error fetching stop data"); } - return Ok(responseBody.Data?.Stop); + var stop = responseBody.Data.Stop; + List<Arrival> arrivals = []; + foreach (var item in stop.Arrivals) + { + var departureTime = todayLocal.AddSeconds(item.ScheduledDepartureSeconds); + var minutesToArrive = (int)(departureTime - nowLocal).TotalMinutes; + //var isRunning = departureTime < nowLocal; + + Arrival arrival = new() + { + Route = new RouteInfo + { + ShortName = item.Trip.RouteShortName, + Colour = item.Trip.Route.Color, + TextColour = item.Trip.Route.TextColor + }, + Headsign = new HeadsignInfo + { + Destination = item.Headsign + }, + Estimate = new ArrivalDetails + { + Minutes = minutesToArrive, + Precission = departureTime < nowLocal ? ArrivalPrecission.Past : ArrivalPrecission.Scheduled + } + }; + + arrivals.Add(arrival); + } + + return Ok(new StopArrivalsResponse + { + StopCode = stop.Code, + StopName = stop.Name, + Arrivals = arrivals + }); } + + [LoggerMessage(LogLevel.Error, "Error fetching stop data, received {statusCode} {responseBody}")] + partial void LogErrorFetchingStopData(HttpStatusCode statusCode, string responseBody); } diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs index dfecdd6..53c1165 100644 --- a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs +++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs @@ -3,26 +3,34 @@ using System.Text.Json.Serialization; namespace Costasdev.Busurbano.Backend.GraphClient.App; -public class ArrivalsAtStopContent : IGraphRequest<string> +public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args> { - public static string Query(string id) + public record Args(string Id, int DepartureCount); + + public static string Query(Args args) { return string.Create(CultureInfo.InvariantCulture, $@" query Query {{ - stop(id:""{id}"") {{ + stop(id:""{args.Id}"") {{ code name - arrivals: stoptimesWithoutPatterns(numberOfDepartures:10) {{ + arrivals: stoptimesWithoutPatterns(numberOfDepartures:{args.DepartureCount}) {{ + headsign + scheduledDeparture + pickupType + trip {{ gtfsId + serviceId routeShortName route {{ color textColor }} + departureStoptime {{ + scheduledDeparture + }} }} - headsign - scheduledDeparture }} }} }} @@ -32,51 +40,97 @@ public class ArrivalsAtStopContent : IGraphRequest<string> public class ArrivalsAtStopResponse : AbstractGraphResponse { - [JsonPropertyName("stop")] - public StopItem Stop { get; set; } + [JsonPropertyName("stop")] public StopItem Stop { get; set; } public class StopItem { - [JsonPropertyName("code")] - public required string Code { get; set; } + [JsonPropertyName("code")] public required string Code { get; set; } - [JsonPropertyName("name")] - public required string Name { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } - [JsonPropertyName("arrivals")] - public List<Arrival> Arrivals { get; set; } = []; + [JsonPropertyName("arrivals")] public List<Arrival> Arrivals { get; set; } = []; } public class Arrival { - [JsonPropertyName("headsign")] - public required string Headsign { get; set; } + [JsonPropertyName("headsign")] public required string Headsign { get; set; } [JsonPropertyName("scheduledDeparture")] public int ScheduledDepartureSeconds { get; set; } - [JsonPropertyName("trip")] - public required TripDetails Trip { get; set; } + [JsonPropertyName("pickupType")] public required string PickupTypeOriginal { get; set; } + + public PickupType PickupTypeParsed => PickupTypeParsed.Parse(PickupTypeOriginal); + + [JsonPropertyName("trip")] public required TripDetails Trip { get; set; } } public class TripDetails { - [JsonPropertyName("gtfsId")] - public required string GtfsId { get; set; } + [JsonPropertyName("gtfsId")] public required string GtfsId { get; set; } + + [JsonPropertyName("serviceId")] public required string ServiceId { get; set; } + + [JsonPropertyName("routeShortName")] public required string RouteShortName { get; set; } + + [JsonPropertyName("departureStoptime")] + public required DepartureStoptime DepartureStoptime { get; set; } - [JsonPropertyName("routeShortName")] - public required string RouteShortName { get; set; } + [JsonPropertyName("route")] public required RouteDetails Route { get; set; } + } - [JsonPropertyName("route")] - public required RouteDetails Route { get; set; } + public class DepartureStoptime + { + [JsonPropertyName("scheduledDeparture")] + public int ScheduledDeparture { get; set; } } public class RouteDetails { - [JsonPropertyName("color")] - public required string Color { get; set; } + [JsonPropertyName("color")] public required string Color { get; set; } + + [JsonPropertyName("textColor")] public required string TextColor { get; set; } + } + + public class PickupType + { + private readonly string _value; + + private PickupType(string value) + { + _value = value; + } + + public PickupType Parse(string value) + { + return value switch + { + "SCHEDULED" => Scheduled, + "NONE" => None, + "CALL_AGENCY" => CallAgency, + "COORDINATE_WITH_DRIVER" => CoordinateWithDriver, + _ => throw new ArgumentException("Unsupported pickup type ", value) + }; + } + + public static readonly PickupType Scheduled = new PickupType("SCHEDULED"); + public static readonly PickupType None = new PickupType("NONE"); + public static readonly PickupType CallAgency = new PickupType("CALL_AGENCY"); + public static readonly PickupType CoordinateWithDriver = new PickupType("COORDINATE_WITH_DRIVER"); + + public override bool Equals(object? other) + { + if (other is not PickupType otherPt) + { + return false; + } + + return otherPt._value == _value; + } - [JsonPropertyName("textColor")] - public required string TextColor { get; set; } + public override int GetHashCode() + { + return _value.GetHashCode(); + } } } diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs index 74c6337..70372e8 100644 --- a/src/Costasdev.Busurbano.Backend/Program.cs +++ b/src/Costasdev.Busurbano.Backend/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Services; using Costasdev.Busurbano.Backend.Services.Providers; @@ -6,7 +7,13 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("App")); -builder.Services.AddControllers(); +builder.Services + .AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + builder.Services.AddHttpClient(); builder.Services.AddMemoryCache(); builder.Services.AddSingleton<ShapeTraversalService>(); diff --git a/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs new file mode 100644 index 0000000..c813ccf --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs @@ -0,0 +1,82 @@ +using System.Text.Json.Serialization; + +namespace Costasdev.Busurbano.Backend.Types.Arrivals; + +public class Arrival +{ + [JsonPropertyName("route")] + public required RouteInfo Route { get; set; } + + [JsonPropertyName("headsign")] + public required HeadsignInfo Headsign { get; set; } + + [JsonPropertyName("estimate")] + public required ArrivalDetails Estimate { get; set; } + + [JsonPropertyName("delay")] + public DelayBadge? Delay { get; set; } + + [JsonPropertyName("shift")] + public ShiftBadge? Shift { get; set; } +} + +public class RouteInfo +{ + [JsonPropertyName("shortName")] + public required string ShortName { get; set; } + + [JsonPropertyName("colour")] + public required string Colour { get; set; } + + [JsonPropertyName("textColour")] + public required string TextColour { get; set; } +} + +public class HeadsignInfo +{ + [JsonPropertyName("badge")] + public string? Badge { get; set; } + + [JsonPropertyName("destination")] + public required string Destination { get; set; } + + [JsonPropertyName("marquee")] + public string? Marquee { get; set; } +} + +public class ArrivalDetails +{ + [JsonPropertyName("minutes")] + public required int Minutes { get; set; } + + [JsonPropertyName("precission")] + public ArrivalPrecission Precission { get; set; } = ArrivalPrecission.Scheduled; +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ArrivalPrecission +{ + [JsonStringEnumMemberName("confident")] + Confident = 0, + [JsonStringEnumMemberName("unsure")] + Unsure = 1, + [JsonStringEnumMemberName("scheduled")] + Scheduled = 2, + [JsonStringEnumMemberName("past")] + Past = 3 +} + +public class DelayBadge +{ + [JsonPropertyName("minutes")] + public int Minutes { get; set; } +} + +public class ShiftBadge +{ + [JsonPropertyName("shiftName")] + public string ShiftName { get; set; } + + [JsonPropertyName("shiftTrip")] + public string ShiftTrip { get; set; } +} diff --git a/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs b/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs new file mode 100644 index 0000000..8c5438c --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Costasdev.Busurbano.Backend.Types.Arrivals; + +public class StopArrivalsResponse +{ + [JsonPropertyName("stopCode")] + public required string StopCode { get; set; } + + [JsonPropertyName("stopName")] + public required string StopName { get; set; } + + [JsonPropertyName("arrivals")] + public List<Arrival> Arrivals { get; set; } = []; +} diff --git a/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs b/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs index 1c47a4a..b67663d 100644 --- a/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs +++ b/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs @@ -98,7 +98,7 @@ public class OtpLeg public OtpGeometry? LegGeometry { get; set; } [JsonPropertyName("steps")] - public List<OtpWalkStep> Steps { get; set; } = new(); + public List<OtpWalkStep> Steps { get; set; } = []; [JsonPropertyName("headsign")] public string? Headsign { get; set; } @@ -113,7 +113,7 @@ public class OtpLeg public string? RouteTextColor { get; set; } [JsonPropertyName("intermediateStops")] - public List<OtpPlace> IntermediateStops { get; set; } = new(); + public List<OtpPlace> IntermediateStops { get; set; } = []; } public class OtpPlace diff --git a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerModels.cs b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs index c31d12a..c31d12a 100644 --- a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerModels.cs +++ b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs |
