aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-06 18:01:14 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-06 18:01:14 +0100
commit66758ffaa4238191010ccc3dde7e5cdc6445f315 (patch)
tree639e37ea14fdb8990451b904eb7c0b2ad2542e9d /src
parent785bc4569fc87aa289766847c862dd8148c5de0b (diff)
Seemingly fix algorithm (thanks copilot)
Diffstat (limited to 'src')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs117
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs32
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs9
-rw-r--r--src/gtfs_vigo_stops/stop_report.py6
4 files changed, 117 insertions, 47 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
index e82baed..01558b6 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
@@ -5,7 +5,6 @@ using Costasdev.Busurbano.Backend.Configuration;
using Costasdev.Busurbano.Backend.Types;
using Costasdev.VigoTransitApi;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using SysFile = System.IO.File;
@@ -15,9 +14,9 @@ namespace Costasdev.Busurbano.Backend.Controllers;
[Route("api/vigo")]
public class VigoController : ControllerBase
{
- private readonly ILogger<VigoController> _logger;
+ private readonly ILogger<VigoController> _logger;
private readonly VigoTransitApiClient _api;
- private readonly AppConfiguration _configuration;
+ private readonly AppConfiguration _configuration;
public VigoController(HttpClient http, IOptions<AppConfiguration> options, ILogger<VigoController> logger)
{
@@ -47,7 +46,7 @@ public class VigoController : ControllerBase
public async Task<IActionResult> GetStopTimetable(
[FromQuery] int stopId,
[FromQuery] string date
- )
+ )
{
// Validate date format
if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out _))
@@ -66,7 +65,7 @@ public class VigoController : ControllerBase
_logger.LogError(ex, "Stop data not found for stop {StopId} on date {Date}", stopId, date);
return StatusCode(404, $"Stop data not found for stop {stopId} on date {date}");
}
- catch(Exception ex)
+ catch (Exception ex)
{
_logger.LogError(ex, "Error loading stop data");
return StatusCode(500, "Error loading timetable");
@@ -94,82 +93,111 @@ public class VigoController : ControllerBase
{
StringBuilder outputBuffer = new();
- var now = DateTime.Now.AddSeconds(60 - DateTime.Now.Second);
var realtimeTask = _api.GetStopEstimates(stopId);
- var timetableTask = LoadTimetable(stopId.ToString(), now.ToString("yyyy-MM-dd"));
+ var timetableTask = LoadTimetable(stopId.ToString(), DateTime.Today.ToString("yyyy-MM-dd"));
await Task.WhenAll(realtimeTask, timetableTask);
var realTimeEstimates = realtimeTask.Result.Estimates;
var timetable = timetableTask.Result;
+ var now = DateTime.Now.AddSeconds(60 - DateTime.Now.Second);
+ var endOfScope = now.AddMinutes(
+ realTimeEstimates.OrderByDescending(e => e.Minutes).First().Minutes + 10
+ );
+
+ List<ConsolidatedCirculation> consolidatedCirculations = [];
+
foreach (var estimate in realTimeEstimates)
{
- outputBuffer.AppendLine($"Parsing estimate with line={estimate.Line}, route={estimate.Route} and minutes={estimate.Minutes} - Arrives at {now.AddMinutes(estimate.Minutes):HH:mm}");
- var fullArrivalTime = now.AddMinutes(estimate.Minutes);
+ var estimatedArrivalTime = now.AddMinutes(estimate.Minutes);
var possibleCirculations = timetable
- .Where(c => c.Line.Trim() == estimate.Line.Trim() && c.Route.Trim() == estimate.Route.Trim())
+ .Where(c =>
+ c.Line.Trim() == estimate.Line.Trim() &&
+ c.Route.Trim() == estimate.Route.Trim()
+ )
.OrderBy(c => c.CallingDateTime())
.ToArray();
- outputBuffer.AppendLine($"Found {possibleCirculations.Length} potential circulations");
-
ScheduledStop? closestCirculation = null;
- int closestCirculationTime = int.MaxValue;
- foreach (var circulation in possibleCirculations)
- {
- var diffBetweenScheduleAndTrip = (int)Math.Round((fullArrivalTime - circulation.CallingDateTime()).TotalMinutes);
- var diffBetweenNowAndSchedule = (int)(fullArrivalTime - now).TotalMinutes;
+ // Matching strategy:
+ // 1) Prefer a started trip whose scheduled calling time is close to the estimated arrival.
+ // 2) If no good started match, pick the next not-started trip (soonest in the future).
+ // 3) Fallbacks: if no future trips, use the best started one even if far.
+ const int startedMatchToleranceMinutes = 15; // how close a started trip must be to consider it a match
- var tolerance = Math.Max(2, diffBetweenNowAndSchedule * 0.15); // Positive amount of minutes
- if (diffBetweenScheduleAndTrip <= -tolerance)
+ var startedCandidates = possibleCirculations
+ .Where(c => c.StartingDateTime() <= now)
+ .Select(c => new
{
- break;
- }
+ Circulation = c,
+ AbsDiff = Math.Abs((estimatedArrivalTime - c.CallingDateTime()).TotalMinutes)
+ })
+ .OrderBy(x => x.AbsDiff)
+ .ToList();
- if (diffBetweenScheduleAndTrip < closestCirculationTime)
- {
- closestCirculation = circulation;
- closestCirculationTime = diffBetweenScheduleAndTrip;
- }
+ var bestStarted = startedCandidates.FirstOrDefault();
+
+ var futureCandidates = possibleCirculations
+ .Where(c => c.StartingDateTime() > now)
+ .ToList();
+ if (bestStarted != null && bestStarted.AbsDiff <= startedMatchToleranceMinutes)
+ {
+ closestCirculation = bestStarted.Circulation;
+ }
+ else if (futureCandidates.Count > 0)
+ {
+ // pick the soonest upcoming trip for this line/route
+ closestCirculation = futureCandidates.First();
+ }
+ else if (bestStarted != null)
+ {
+ // nothing upcoming today; fallback to the closest started one (even if far)
+ closestCirculation = bestStarted.Circulation;
}
if (closestCirculation == null)
{
+ _logger.LogError("No stop arrival merged for line {Line} towards {Route} in {Minutes} minutes", estimate.Line, estimate.Route, estimate.Minutes);
outputBuffer.AppendLine("**No circulation matched. List of all of them:**");
foreach (var circulation in possibleCirculations)
{
// Circulation A 03LP000_008003_16 stopping at 05/11/2025 22:06:00 (diff: -03:29:59.2644092)
- outputBuffer.AppendLine($"Circulation {circulation.TripId} stopping at {circulation.CallingDateTime()} (diff: {fullArrivalTime - circulation.CallingDateTime():HH:mm})");
+ outputBuffer.AppendLine(
+ $"Circulation {circulation.TripId} stopping at {circulation.CallingDateTime()} (diff: {estimatedArrivalTime - circulation.CallingDateTime():HH:mm})");
}
+
outputBuffer.AppendLine();
continue;
}
- if (closestCirculationTime > 0)
- {
- outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {closestCirculationTime} minutes LATE");
- }
- else if (closestCirculationTime == 0)
+ consolidatedCirculations.Add(new ConsolidatedCirculation
{
- outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving ON TIME");
- }
- else
- {
- outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY");
- }
-
- outputBuffer.AppendLine(
- $" -- Circulation expected at {closestCirculation.CallingDateTime():HH:mm)}");
-
- outputBuffer.AppendLine();
+ Line = estimate.Line,
+ Route = estimate.Route,
+ Schedule = new ScheduleData
+ {
+ Running = closestCirculation.StartingDateTime() <= now,
+ Minutes = (int)(closestCirculation.CallingDateTime() - now).TotalMinutes,
+ TripId = closestCirculation.TripId,
+ ServiceId = closestCirculation.ServiceId,
+ },
+ RealTime = new RealTimeData
+ {
+ Minutes = estimate.Minutes,
+ Distance = estimate.Meters,
+ Confidence = closestCirculation.StartingDateTime() <= now
+ ? RealTimeConfidence.High
+ : RealTimeConfidence.Low
+ }
+ });
}
- return Ok(outputBuffer.ToString());
+ return Ok(consolidatedCirculations);
}
private async Task<List<ScheduledStop>> LoadTimetable(string stopId, string dateString)
@@ -179,6 +207,7 @@ public class VigoController : ControllerBase
{
throw new FileNotFoundException();
}
+
var contents = await SysFile.ReadAllTextAsync(file);
return JsonSerializer.Deserialize<List<ScheduledStop>>(contents)!;
}
diff --git a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs
new file mode 100644
index 0000000..d65da61
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs
@@ -0,0 +1,32 @@
+namespace Costasdev.Busurbano.Backend.Types;
+
+public class ConsolidatedCirculation
+{
+ public required string Line { get; set; }
+ public required string Route { get; set; }
+
+ public ScheduleData? Schedule { get; set; }
+ public RealTimeData? RealTime { get; set; }
+}
+
+public class RealTimeData
+{
+ public required int Minutes { get; set; }
+ public required int Distance { get; set; }
+ public required RealTimeConfidence Confidence { get; set; }
+}
+
+public class ScheduleData
+{
+ public bool Running { get; set; }
+ public required int Minutes { get; set; }
+ public required string ServiceId { get; set; }
+ public required string TripId { get; set; }
+}
+
+public enum RealTimeConfidence
+{
+ NotApplicable = 0,
+ Low = 1,
+ High = 2
+}
diff --git a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs
index f8e5634..25fc34f 100644
--- a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs
+++ b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs
@@ -14,9 +14,17 @@ public class ScheduledStop
public required double ShapeDistTraveled { get; set; }
[JsonPropertyName("next_streets")] public required string[] NextStreets { get; set; }
+
[JsonPropertyName("starting_code")] public required string StartingCode { get; set; }
[JsonPropertyName("starting_name")] public required string StartingName { get; set; }
[JsonPropertyName("starting_time")] public required string StartingTime { get; set; }
+ public DateTime StartingDateTime()
+ {
+ var dt = DateTime.Today + TimeOnly.Parse(StartingTime).ToTimeSpan();
+ return dt.AddSeconds(60 - dt.Second);
+ }
+
+ [JsonPropertyName("calling_ssm")] public required int CallingSsm { get; set; }
[JsonPropertyName("calling_time")] public required string CallingTime { get; set; }
public DateTime CallingDateTime()
{
@@ -24,7 +32,6 @@ public class ScheduledStop
return dt.AddSeconds(60 - dt.Second);
}
- [JsonPropertyName("calling_ssm")] public required int CallingSsm { get; set; }
[JsonPropertyName("terminus_code")] public required string TerminusCode { get; set; }
[JsonPropertyName("terminus_name")] public required string TerminusName { get; set; }
[JsonPropertyName("terminus_time")] public required string TerminusTime { get; set; }
diff --git a/src/gtfs_vigo_stops/stop_report.py b/src/gtfs_vigo_stops/stop_report.py
index fa541ef..880eaf7 100644
--- a/src/gtfs_vigo_stops/stop_report.py
+++ b/src/gtfs_vigo_stops/stop_report.py
@@ -194,9 +194,11 @@ def get_stop_arrivals(
else:
next_streets = []
+ trip_id_fmt = "_".join(trip_id.split("_")[1:3])
+
stop_arrivals[stop_code].append({
- "trip_id": trip_id,
- "service_id": service_id,
+ "trip_id": trip_id_fmt,
+ "service_id": service_id.split("_")[1],
"line": route_short_name,
"route": trip_headsign,
"stop_sequence": stop_time.stop_sequence,