From fbd2c1aa2dd25dd61483553d114c484060f71bd6 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sat, 27 Dec 2025 19:22:02 +0100 Subject: IDEK --- .../Controllers/RoutePlannerController.cs | 2 - .../Services/FareService.cs | 22 +- .../Services/OtpService.cs | 3 +- .../Types/Planner/PlannerResponse.cs | 3 + .../Queries/PlanConnectionContent.cs | 248 +++++++++++---------- src/frontend/app/api/schema.ts | 3 + src/frontend/app/routes/planner.tsx | 4 +- 7 files changed, 156 insertions(+), 129 deletions(-) diff --git a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs index 823cfa5..7d47383 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs @@ -78,8 +78,6 @@ public partial class RoutePlannerController : ControllerBase var response = await _httpClient.SendAsync(request); var responseBody = await response.Content.ReadFromJsonAsync>(); - Console.WriteLine(responseBody); - if (responseBody is not { IsSuccess: true } || responseBody.Data?.PlanConnection.Edges.Length == 0) { LogErrorFetchingRoutes(response.StatusCode, await response.Content.ReadAsStringAsync()); diff --git a/src/Costasdev.Busurbano.Backend/Services/FareService.cs b/src/Costasdev.Busurbano.Backend/Services/FareService.cs index 0e4fefc..9a11a56 100644 --- a/src/Costasdev.Busurbano.Backend/Services/FareService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/FareService.cs @@ -20,23 +20,39 @@ public class FareService var busLegs = legs.Where(l => l.Mode != null && l.Mode.ToUpper() != "WALK").ToList(); // Cash fare logic - // TODO: In the future, this should depend on the operator/feed - var cashFare = busLegs.Count * 1.63; // Defaulting to Vitrasa for now + double cashFare = 0; + foreach (var leg in busLegs) + { + // TODO: In the future, this should depend on the operator/feed + if (leg.FeedId == "vitrasa") + { + cashFare += 1.63; + } + else + { + cashFare += 1.63; // Default fallback + } + } // Card fare logic (45-min transfer window) int cardTicketsRequired = 0; DateTime? lastTicketPurchased = null; int tripsPaidWithTicket = 0; + string? lastFeedId = null; foreach (var leg in busLegs) { + // If no ticket purchased, ticket expired (no free transfers after 45 mins), or max trips with ticket reached + // Also check if we changed operator (assuming no free transfers between different operators for now) if (lastTicketPurchased == null || (leg.StartTime - lastTicketPurchased.Value).TotalMinutes > 45 || - tripsPaidWithTicket >= 3) + tripsPaidWithTicket >= 3 || + leg.FeedId != lastFeedId) { cardTicketsRequired++; lastTicketPurchased = leg.StartTime; tripsPaidWithTicket = 1; + lastFeedId = leg.FeedId; } else { diff --git a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs index 7eba590..8d47225 100644 --- a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Text; using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Helpers; using Costasdev.Busurbano.Backend.Types.Otp; @@ -430,6 +429,8 @@ public class OtpService return new Leg { Mode = leg.Mode, + FeedId = feedId, + RouteId = leg.Route?.GtfsId, RouteName = leg.Route?.LongName, RouteShortName = shortName, RouteLongName = leg.Route?.LongName, diff --git a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs index b2f4d6a..a4e54d7 100644 --- a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs +++ b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs @@ -23,6 +23,9 @@ public class Itinerary public class Leg { public string? Mode { get; set; } // WALK, BUS, etc. + public string? FeedId { get; set; } + public string? RouteId { get; set; } + public string? TripId { get; set; } public string? RouteName { get; set; } public string? RouteShortName { get; set; } public string? RouteLongName { get; set; } diff --git a/src/Costasdev.Busurbano.Sources.OpenTripPlannerGql/Queries/PlanConnectionContent.cs b/src/Costasdev.Busurbano.Sources.OpenTripPlannerGql/Queries/PlanConnectionContent.cs index a4bf8d1..f325336 100644 --- a/src/Costasdev.Busurbano.Sources.OpenTripPlannerGql/Queries/PlanConnectionContent.cs +++ b/src/Costasdev.Busurbano.Sources.OpenTripPlannerGql/Queries/PlanConnectionContent.cs @@ -7,30 +7,31 @@ namespace Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries; public class PlanConnectionContent : IGraphRequest { - public record Args( - double StartLatitude, - double StartLongitude, - double EndLatitude, - double EndLongitude, - DateTimeOffset ReferenceTime, - bool ReferenceIsArrival = false - ); - - public static string Query(Args args) - { - var madridTz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); - - // Treat incoming DateTime as Madrid local wall-clock time - var localMadridTime = - DateTime.SpecifyKind(args.ReferenceTime.UtcDateTime, DateTimeKind.Unspecified); - - var offset = madridTz.GetUtcOffset(localMadridTime); - var actualTimeOfQuery = new DateTimeOffset(localMadridTime, offset); - - var dateTimeParameter = args.ReferenceIsArrival ? $"latestArrival:\"{actualTimeOfQuery:O}\"" : $"earliestDeparture:\"{actualTimeOfQuery:O}\""; - - return string.Create(CultureInfo.InvariantCulture, - $$""" + public record Args( + double StartLatitude, + double StartLongitude, + double EndLatitude, + double EndLongitude, + DateTimeOffset ReferenceTime, + bool ReferenceIsArrival = false + ); + + public static string Query(Args args) + { + var madridTz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid"); + + // Treat incoming DateTime as Madrid local wall-clock time + var localMadridTime = + DateTime.SpecifyKind(args.ReferenceTime.UtcDateTime, DateTimeKind.Unspecified); + + var offset = madridTz.GetUtcOffset(localMadridTime); + + var dateTimeToUse = new DateTimeOffset(args.ReferenceTime.DateTime + offset, offset); + + var dateTimeParameter = args.ReferenceIsArrival ? $"latestArrival:\"{dateTimeToUse:O}\"" : $"earliestDeparture:\"{dateTimeToUse:O}\""; + + return string.Create(CultureInfo.InvariantCulture, + $$""" query Query { planConnection( first: 4 @@ -71,6 +72,7 @@ public class PlanConnectionContent : IGraphRequest } mode route { + gtfsId shortName longName agency { @@ -133,106 +135,108 @@ public class PlanConnectionContent : IGraphRequest } } """); - } + } } public class PlanConnectionResponse : AbstractGraphResponse { - public PlanConnectionItem PlanConnection { get; set; } - - public class PlanConnectionItem - { - [JsonPropertyName("edges")] - public Edge[] Edges { get; set; } - } - - public class Edge - { - [JsonPropertyName("node")] - public Node Node { get; set; } - } - - public class Node - { - [JsonPropertyName("duration")] public int DurationSeconds { get; set; } - [JsonPropertyName("start")] public string Start8601 { get; set; } - [JsonPropertyName("end")] public string End8601 { get; set; } - [JsonPropertyName("walkTime")] public int WalkSeconds { get; set; } - [JsonPropertyName("walkDistance")] public double WalkDistance { get; set; } - [JsonPropertyName("waitingTime")] public int WaitingSeconds { get; set; } - [JsonPropertyName("legs")] public Leg[] Legs { get; set; } - } - - public class Leg - { - [JsonPropertyName("start")] public ScheduledTimeContainer Start { get; set; } - [JsonPropertyName("end")] public ScheduledTimeContainer End { get; set; } - [JsonPropertyName("mode")] public string Mode { get; set; } // TODO: Make enum, maybe - [JsonPropertyName("route")] public TransitRoute? Route { get; set; } - [JsonPropertyName("from")] public LegPosition From { get; set; } - [JsonPropertyName("to")] public LegPosition To { get; set; } - [JsonPropertyName("stopCalls")] public StopCall[] StopCalls { get; set; } - [JsonPropertyName("legGeometry")] public LegGeometry LegGeometry { get; set; } - [JsonPropertyName("steps")] public Step[] Steps { get; set; } - [JsonPropertyName("headsign")] public string? Headsign { get; set; } - [JsonPropertyName("distance")] public double Distance { get; set; } - } - - public class TransitRoute - { - [JsonPropertyName("shortName")] public string ShortName { get; set; } - [JsonPropertyName("longName")] public string LongName { get; set; } - [JsonPropertyName("agency")] public AgencyNameContainer Agency { get; set; } - [JsonPropertyName("color")] public string Color { get; set; } - [JsonPropertyName("textColor")] public string TextColor { get; set; } - } - - public class LegPosition - { - [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("lat")] public double Latitude { get; set; } - [JsonPropertyName("lon")] public double Longitude { get; set; } - [JsonPropertyName("stop")] public StopLocation Stop { get; set; } - } - - public class ScheduledTimeContainer - { - [JsonPropertyName("scheduledTime")] - public string ScheduledTime8601 { get; set; } - } - - public class AgencyNameContainer - { - [JsonPropertyName("name")] public string Name { get; set; } - } - - public class StopCall - { - [JsonPropertyName("stopLocation")] - public StopLocation StopLocation { get; set; } - } - - public class StopLocation - { - [JsonPropertyName("gtfsId")] public string GtfsId { get; set; } - [JsonPropertyName("code")] public string Code { get; set; } - [JsonPropertyName("name")] public string Name { get; set; } - [JsonPropertyName("lat")] public double Latitude { get; set; } - [JsonPropertyName("lon")] public double Longitude { get; set; } - } - - public class Step - { - [JsonPropertyName("distance")] public double Distance { get; set; } - [JsonPropertyName("relativeDirection")] public string RelativeDirection { get; set; } - [JsonPropertyName("streetName")] public string StreetName { get; set; } // TODO: "sidewalk", "path" or actual street name - [JsonPropertyName("absoluteDirection")] public string AbsoluteDirection { get; set; } - [JsonPropertyName("lat")] public double Latitude { get; set; } - [JsonPropertyName("lon")] public double Longitude { get; set; } - } - - public class LegGeometry - { - [JsonPropertyName("points")] public string? Points { get; set; } - } + public PlanConnectionItem PlanConnection { get; set; } + + public class PlanConnectionItem + { + [JsonPropertyName("edges")] + public Edge[] Edges { get; set; } + } + + public class Edge + { + [JsonPropertyName("node")] + public Node Node { get; set; } + } + + public class Node + { + [JsonPropertyName("duration")] public int DurationSeconds { get; set; } + [JsonPropertyName("start")] public string Start8601 { get; set; } + [JsonPropertyName("end")] public string End8601 { get; set; } + [JsonPropertyName("walkTime")] public int WalkSeconds { get; set; } + [JsonPropertyName("walkDistance")] public double WalkDistance { get; set; } + [JsonPropertyName("waitingTime")] public int WaitingSeconds { get; set; } + [JsonPropertyName("legs")] public Leg[] Legs { get; set; } + } + + public class Leg + { + [JsonPropertyName("start")] public ScheduledTimeContainer Start { get; set; } + [JsonPropertyName("end")] public ScheduledTimeContainer End { get; set; } + [JsonPropertyName("mode")] public string Mode { get; set; } // TODO: Make enum, maybe + [JsonPropertyName("route")] public TransitRoute? Route { get; set; } + [JsonPropertyName("from")] public LegPosition From { get; set; } + [JsonPropertyName("to")] public LegPosition To { get; set; } + [JsonPropertyName("stopCalls")] public StopCall[] StopCalls { get; set; } + [JsonPropertyName("legGeometry")] public LegGeometry LegGeometry { get; set; } + [JsonPropertyName("steps")] public Step[] Steps { get; set; } + [JsonPropertyName("headsign")] public string? Headsign { get; set; } + [JsonPropertyName("distance")] public double Distance { get; set; } + } + + public class TransitRoute + { + [JsonPropertyName("gtfsId")] public string GtfsId { get; set; } + [JsonPropertyName("shortName")] public string ShortName { get; set; } + [JsonPropertyName("longName")] public string LongName { get; set; } + [JsonPropertyName("agency")] public AgencyNameContainer Agency { get; set; } + [JsonPropertyName("color")] public string Color { get; set; } + [JsonPropertyName("textColor")] public string TextColor { get; set; } + } + + public class LegPosition + { + [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("lat")] public double Latitude { get; set; } + [JsonPropertyName("lon")] public double Longitude { get; set; } + [JsonPropertyName("stop")] public StopLocation Stop { get; set; } + } + + public class ScheduledTimeContainer + { + [JsonPropertyName("scheduledTime")] + public string ScheduledTime8601 { get; set; } + } + + public class AgencyNameContainer + { + [JsonPropertyName("name")] public string Name { get; set; } + } + + public class StopCall + { + [JsonPropertyName("stopLocation")] + public StopLocation StopLocation { get; set; } + } + + public class StopLocation + { + [JsonPropertyName("gtfsId")] public string GtfsId { get; set; } + [JsonPropertyName("code")] public string Code { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("lat")] public double Latitude { get; set; } + [JsonPropertyName("lon")] public double Longitude { get; set; } + [JsonPropertyName("zoneId")] public string? ZoneId { get; set; } + } + + public class Step + { + [JsonPropertyName("distance")] public double Distance { get; set; } + [JsonPropertyName("relativeDirection")] public string RelativeDirection { get; set; } + [JsonPropertyName("streetName")] public string StreetName { get; set; } // TODO: "sidewalk", "path" or actual street name + [JsonPropertyName("absoluteDirection")] public string AbsoluteDirection { get; set; } + [JsonPropertyName("lat")] public double Latitude { get; set; } + [JsonPropertyName("lon")] public double Longitude { get; set; } + } + + public class LegGeometry + { + [JsonPropertyName("points")] public string? Points { get; set; } + } } diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts index 05f3a87..bb2fbcc 100644 --- a/src/frontend/app/api/schema.ts +++ b/src/frontend/app/api/schema.ts @@ -134,6 +134,9 @@ export const PlannerStepSchema = z.object({ export const PlannerLegSchema = z.object({ mode: z.string().optional().nullable(), + feedId: z.string().optional().nullable(), + routeId: z.string().optional().nullable(), + tripId: z.string().optional().nullable(), routeName: z.string().optional().nullable(), routeShortName: z.string().optional().nullable(), routeLongName: z.string().optional().nullable(), diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx index 44488c8..5968bc2 100644 --- a/src/frontend/app/routes/planner.tsx +++ b/src/frontend/app/routes/planner.tsx @@ -587,7 +587,9 @@ const ItineraryDetail = ({ minute: "2-digit", timeZone: "Europe/Madrid", })}{" "} - -{" "} + + + {( (new Date(leg.endTime).getTime() - new Date(leg.startTime).getTime()) / -- cgit v1.3