aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-06 00:29:25 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-06 00:29:25 +0100
commit236d760d20a5ade402f8e4e4da92332a09f0bcde (patch)
tree75598c091dc46616bab593a42617fd243cf5fade /src
parent234d069f74499872a1d6612dc1c3dff418c52f20 (diff)
Begin (poorly) implementing merging scheduled and real-time data
Diffstat (limited to 'src')
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs38
-rw-r--r--src/Costasdev.Busurbano.Backend/VigoController.cs181
-rw-r--r--src/frontend/app/components/TimetableTable.css6
-rw-r--r--src/frontend/package-lock.json3
4 files changed, 175 insertions, 53 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs
new file mode 100644
index 0000000..d6012e5
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs
@@ -0,0 +1,38 @@
+using System.Text.Json.Serialization;
+
+namespace Costasdev.Busurbano.Backend.Types;
+
+public class ScheduledStop
+{
+ [JsonPropertyName("line")] public required Line Line { get; set; }
+ [JsonPropertyName("trip")] public required Trip Trip { get; set; }
+ [JsonPropertyName("route_id")] public required string RouteId { get; set; }
+ [JsonPropertyName("departure_time")] public required string DepartureTime { get; set; }
+
+ public DateTime DepartureDateTime()
+ {
+ var dt = DateTime.Today + TimeOnly.Parse(DepartureTime).ToTimeSpan();
+ return dt.AddSeconds(60 - dt.Second);
+ }
+
+ [JsonPropertyName("stop_sequence")] public required int StopSequence { get; set; }
+
+ [JsonPropertyName("shape_dist_traveled")]
+ public required float ShapeDistTraveled { get; set; }
+
+ [JsonPropertyName("next_streets")] public required string[] NextStreets { get; set; }
+}
+
+public class Line
+{
+ [JsonPropertyName("name")] public required string Name { get; set; }
+ [JsonPropertyName("colour")] public required string Colour { get; set; }
+}
+
+public class Trip
+{
+ [JsonPropertyName("id")] public required string Id { get; set; }
+ [JsonPropertyName("service_id")] public required string ServiceId { get; set; }
+ [JsonPropertyName("headsign")] public required string Headsign { get; set; }
+ [JsonPropertyName("direction_id")] public required int DirectionId { get; set; }
+}
diff --git a/src/Costasdev.Busurbano.Backend/VigoController.cs b/src/Costasdev.Busurbano.Backend/VigoController.cs
index 147c22a..6d321b8 100644
--- a/src/Costasdev.Busurbano.Backend/VigoController.cs
+++ b/src/Costasdev.Busurbano.Backend/VigoController.cs
@@ -1,7 +1,11 @@
+using System.Globalization;
+using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Costasdev.VigoTransitApi;
using System.Text.Json;
+using Costasdev.Busurbano.Backend.Types;
+using Costasdev.VigoTransitApi.Types;
namespace Costasdev.Busurbano.Backend;
@@ -21,23 +25,13 @@ public class VigoController : ControllerBase
}
[HttpGet("GetStopEstimates")]
- public async Task<IActionResult> Run()
+ public async Task<IActionResult> Run(
+ [FromQuery] int stopId
+ )
{
- var argumentAvailable = Request.Query.TryGetValue("id", out var requestedStopIdString);
- if (!argumentAvailable)
- {
- return BadRequest("Please provide a stop id as a query parameter with the name 'id'.");
- }
-
- var argumentNumber = int.TryParse(requestedStopIdString, out var requestedStopId);
- if (!argumentNumber)
- {
- return BadRequest("The provided stop id is not a valid number.");
- }
-
try
{
- var response = await _api.GetStopEstimates(requestedStopId);
+ var response = await _api.GetStopEstimates(stopId);
// Return only the estimates array, not the stop metadata
return new OkObjectResult(response.Estimates);
}
@@ -48,56 +42,30 @@ public class VigoController : ControllerBase
}
[HttpGet("GetStopTimetable")]
- public async Task<IActionResult> GetStopTimetable()
+ public async Task<IActionResult> GetStopTimetable(
+ [FromQuery] int stopId,
+ [FromQuery] string date
+ )
{
- // Get date parameter (default to today if not provided)
- var dateString = Request.Query.TryGetValue("date", out var requestedDate)
- ? requestedDate.ToString()
- : DateTime.Today.ToString("yyyy-MM-dd");
-
// Validate date format
- if (!DateTime.TryParseExact(dateString, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out _))
+ if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out _))
{
return BadRequest("Invalid date format. Please use yyyy-MM-dd format.");
}
- // Get stopId parameter
- if (!Request.Query.TryGetValue("stopId", out var requestedStopIdString))
- {
- return BadRequest("Please provide a stop id as a query parameter with the name 'stopId'.");
- }
-
- if (!int.TryParse(requestedStopIdString, out var requestedStopId))
- {
- return BadRequest("The provided stop id is not a valid number.");
- }
-
// Create cache key
- var cacheKey = $"timetable_{dateString}_{requestedStopId}";
+ var cacheKey = $"timetable_{date}_{stopId}";
// Try to get from cache first
if (_cache.TryGetValue(cacheKey, out var cachedData))
{
+ Response.Headers.Append("App-CacheUsage", "HIT");
return new OkObjectResult(cachedData);
}
try
{
- // Fetch data from external API
- var url = $"https://costas.dev/static-storage/vitrasa_svc/stops/{dateString}/{requestedStopId}.json";
- var response = await _httpClient.GetAsync(url);
-
- if (!response.IsSuccessStatusCode)
- {
- if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
- {
- return NotFound($"Timetable data not found for stop {requestedStopId} on {dateString}");
- }
- return StatusCode((int)response.StatusCode, "Error fetching timetable data");
- }
-
- var jsonContent = await response.Content.ReadAsStringAsync();
- var timetableData = JsonSerializer.Deserialize<JsonElement>(jsonContent);
+ var timetableData = await LoadTimetable(stopId.ToString(), date);
// Cache the data for 12 hours
var cacheOptions = new MemoryCacheEntryOptions
@@ -109,11 +77,12 @@ public class VigoController : ControllerBase
_cache.Set(cacheKey, timetableData, cacheOptions);
+ Response.Headers.Append("App-CacheUsage", "MISS");
return new OkObjectResult(timetableData);
}
catch (HttpRequestException ex)
{
- return StatusCode(500, $"Error fetching timetable data: {ex.Message}");
+ return StatusCode((int?)ex.StatusCode ?? 500, $"Error fetching timetable data: {ex.Message}");
}
catch (JsonException ex)
{
@@ -124,5 +93,117 @@ public class VigoController : ControllerBase
return StatusCode(500, $"Unexpected error: {ex.Message}");
}
}
-}
+ /*private StopEstimate[] LoadDebugEstimates()
+ {
+ var file = @"C:\Users\ariel\Desktop\GetStopEstimates.json";
+ var contents = System.IO.File.ReadAllText(file);
+ return JsonSerializer.Deserialize<StopEstimate[]>(contents, JsonSerializerOptions.Web)!;
+ }
+
+ private ScheduledStop[] LoadDebugTimetable()
+ {
+ var file = @"C:\Users\ariel\Desktop\GetStopTimetable.json";
+ var contents = System.IO.File.ReadAllText(file);
+ return JsonSerializer.Deserialize<ScheduledStop[]>(contents)!;
+ }*/
+
+ [HttpGet("GetStopArrivalsMerged")]
+ public async Task<IActionResult> GetStopArrivalsMerged(
+ [FromQuery] int stopId
+ )
+ {
+ 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"));
+
+ Task.WaitAll(realtimeTask, timetableTask);
+
+ var realTimeEstimates = realtimeTask.Result.Estimates;
+ var timetable = timetableTask.Result;
+
+ /*var now = DateTime.Today.AddHours(17).AddMinutes(59);
+ var realTimeEstimates = LoadDebugEstimates();
+ var timetable = LoadDebugTimetable();*/
+
+ 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)}");
+ var fullArrivalTime = now.AddMinutes(estimate.Minutes);
+
+ var possibleCirculations = timetable
+ .Where(c => c.Line.Name.Trim() == estimate.Line.Trim() && c.Trip.Headsign.Trim() == estimate.Route.Trim())
+ .OrderBy(c => c.DepartureDateTime())
+ .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.DepartureDateTime()).TotalMinutes);
+ var diffBetweenNowAndSchedule = (int)(fullArrivalTime - now).TotalMinutes;
+
+ var tolerance = Math.Max(2, diffBetweenNowAndSchedule * 0.15); // Positive amount of minutes
+ if (diffBetweenScheduleAndTrip <= -tolerance)
+ {
+ break;
+ }
+
+ if (diffBetweenScheduleAndTrip < closestCirculationTime)
+ {
+ closestCirculation = circulation;
+ closestCirculationTime = diffBetweenScheduleAndTrip;
+ }
+
+ }
+
+ if (closestCirculation == null)
+ {
+ 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.Trip.Id} stopping at {circulation.DepartureDateTime()} (diff: {fullArrivalTime - circulation.DepartureDateTime():HH:mm})");
+ }
+ outputBuffer.AppendLine();
+
+ continue;
+ }
+
+ if (closestCirculationTime > 0)
+ {
+ outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {closestCirculationTime} minutes LATE");
+ } else if (closestCirculationTime == 0)
+ {
+ outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving ON TIME");
+ }
+ else
+ {
+ outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY");
+ }
+
+ outputBuffer.AppendLine(
+ $" -- Circulation expected at {closestCirculation.DepartureDateTime():HH:mm)}");
+
+ outputBuffer.AppendLine();
+ }
+
+ return Ok(outputBuffer.ToString());
+ }
+
+ private async Task<List<ScheduledStop>> LoadTimetable(string stopId, string dateString)
+ {
+ var url = $"https://www.costas.dev/static-storage/vitrasa_svc/stops/{dateString}/{stopId}.json";
+ var response = await _httpClient.GetAsync(url);
+
+ response.EnsureSuccessStatusCode();
+
+ var jsonContent = await response.Content.ReadAsStringAsync();
+ return JsonSerializer.Deserialize<List<ScheduledStop>>(jsonContent) ?? [];
+ }
+}
diff --git a/src/frontend/app/components/TimetableTable.css b/src/frontend/app/components/TimetableTable.css
index 52bd9ae..8980fb4 100644
--- a/src/frontend/app/components/TimetableTable.css
+++ b/src/frontend/app/components/TimetableTable.css
@@ -18,15 +18,15 @@
}
.timetable-card {
- background: var(--surface-future, #fff);
+ background-color: var(--surface-future, #fff);
border: 1px solid var(--card-border, #e0e0e0);
border-radius: 10px;
padding: 1.25rem;
- transition: background 0.2s ease, border 0.2s ease;
+ transition: background-color 0.2s ease, border 0.2s ease;
}
.timetable-card.timetable-past {
- background: var(--surface-past, #f3f3f3);
+ background-color: var(--surface-past, #f3f3f3);
color: var(--text-secondary, #aaa);
border: 1px solid #e0e0e0;
}
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 7de81f1..e8bdee3 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -9,6 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@fontsource-variable/roboto": "^5.2.8",
+ "@react-router/node": "^7.9.4",
+ "@react-router/serve": "^7.9.4",
"framer-motion": "^12.23.24",
"fuse.js": "^7.1.0",
"i18next-browser-languagedetector": "^8.2.0",
@@ -3384,6 +3386,7 @@
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",