aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-22 14:13:45 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-22 14:13:53 +0100
commit91f7d7dd5a4ca8453cfdbc9a3beeb216b6638ef7 (patch)
treef9d036a5b692aa10bb61b7a8ccc30c58f29a79f2
parent4e583cc587f9b8cc159cedeb63af2f38d5451507 (diff)
Implement fetching scheduled arrivals for stop
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs52
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TileController.cs (renamed from src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs)72
-rw-r--r--src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj2
-rw-r--r--src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs82
-rw-r--r--src/frontend/app/routes/map.tsx17
5 files changed, 182 insertions, 43 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
new file mode 100644
index 0000000..eb81784
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
@@ -0,0 +1,52 @@
+using Costasdev.Busurbano.Backend.GraphClient;
+using Costasdev.Busurbano.Backend.GraphClient.App;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Costasdev.Busurbano.Backend.Controllers;
+
+[ApiController]
+[Route("api")]
+public class ArrivalsController : ControllerBase
+{
+ private readonly ILogger<ArrivalsController> _logger;
+ private readonly IMemoryCache _cache;
+ private readonly HttpClient _httpClient;
+
+ public ArrivalsController(
+ ILogger<ArrivalsController> logger,
+ IMemoryCache cache,
+ HttpClient httpClient
+ )
+ {
+ _logger = logger;
+ _cache = cache;
+ _httpClient = httpClient;
+ }
+
+ [HttpGet("arrivals")]
+ public async Task<IActionResult> GetArrivals(string id)
+ {
+ var requestContent = ArrivalsAtStopContent.Query(id);
+ var request = new HttpRequestMessage(HttpMethod.Post, "http://100.67.54.115:3957/otp/gtfs/v1");
+ request.Content = JsonContent.Create(new GraphClientRequest
+ {
+ Query = requestContent
+ });
+
+ var response = await _httpClient.SendAsync(request);
+ var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<ArrivalsAtStopResponse>>();
+
+ if (responseBody is not { IsSuccess: true })
+ {
+ _logger.LogError(
+ "Error fetching stop data, received {StatusCode} {ResponseBody}",
+ response.StatusCode,
+ await response.Content.ReadAsStringAsync()
+ );
+ return StatusCode(500, "Error fetching stop data");
+ }
+
+ return Ok(responseBody.Data?.Stop);
+ }
+}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
index 56f836e..fad18e7 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/StopsTile.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
@@ -1,15 +1,18 @@
using Costasdev.Busurbano.Backend.GraphClient;
using Costasdev.Busurbano.Backend.GraphClient.App;
+
+using NetTopologySuite.Features;
using NetTopologySuite.IO.VectorTiles;
using NetTopologySuite.IO.VectorTiles.Mapbox;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
-using NetTopologySuite.Features;
using System.Text.Json;
using Costasdev.Busurbano.Backend.Helpers;
+namespace Costasdev.Busurbano.Backend.Controllers;
+
[ApiController]
[Route("api/tiles")]
public class TileController : ControllerBase
@@ -29,36 +32,25 @@ public class TileController : ControllerBase
_httpClient = httpClient;
}
- /*
- vitrasa:20223: # Castrelos (Pavillón) - Final U1
- hide: true
-vitrasa:20146: # García Barbón 7 - final líneas A y 18A
- hide: true
-vitrasa:20220: # (Samil) COIA-SAMIL - Final L15A
- hide: true
-vitrasa:20001: # (Samil) Samil por Beiramar - Final L15B
- hide: true
-vitrasa:20002: # (Samil) Samil por Torrecedeira - Final L15C
- hide: true
-vitrasa:20144: # (Samil) Samil por Coia - Final C3D+C3i
- hide: true
-vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i
- hide: true
- */
private static readonly string[] HiddenStops =
[
- "vitrasa:20223",
- "vitrasa:20146",
- "vitrasa:20220",
- "vitrasa:20001",
- "vitrasa:20002",
- "vitrasa:20144",
- "vitrasa:20145"
+ "vitrasa:20223", // Castrelos (Pavillón - U1)
+ "vitrasa:20146", // García Barbón, 7 (A, 18A)
+ "vitrasa:20220", // COIA-SAMIL (15)
+ "vitrasa:20001", // Samil por Beiramar (15B)
+ "vitrasa:20002", // Samil por Torrecedeira (15C)
+ "vitrasa:20144", // Samil por Coia (C3d, C3i)
+ "vitrasa:20145" // Samil por Bouzs (C3d, C3i)
];
- [HttpGet("stops/{z}/{x}/{y}")]
- public async Task<IActionResult> GetTrafficTile(int z, int x, int y)
+ [HttpGet("stops/{z:int}/{x:int}/{y:int}")]
+ public async Task<IActionResult> Stops(int z, int x, int y)
{
+ if (z < 9 || z > 16)
+ {
+ return BadRequest("Zoom level out of range (9-16)");
+ }
+
var cacheHit = _cache.TryGetValue($"stops-tile-{z}-{x}-{y}", out byte[]? cachedTile);
if (cacheHit && cachedTile != null)
{
@@ -67,15 +59,15 @@ vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i
}
// Calculate bounding box in EPSG:4326
- double n = Math.Pow(2, z);
- double lonMin = x / n * 360.0 - 180.0;
- double lonMax = (x + 1) / n * 360.0 - 180.0;
+ var n = Math.Pow(2, z);
+ var lonMin = x / n * 360.0 - 180.0;
+ var lonMax = (x + 1) / n * 360.0 - 180.0;
- double latMaxRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n)));
- double latMax = latMaxRad * 180.0 / Math.PI;
+ var latMaxRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n)));
+ var latMax = latMaxRad * 180.0 / Math.PI;
- double latMinRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n)));
- double latMin = latMinRad * 180.0 / Math.PI;
+ var latMinRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n)));
+ var latMin = latMinRad * 180.0 / Math.PI;
var requestContent = StopTileRequestContent.Query(new StopTileRequestContent.Bbox(lonMin, latMin, lonMax, latMax));
var request = new HttpRequestMessage(HttpMethod.Post, "http://100.67.54.115:3957/otp/gtfs/v1");
@@ -87,11 +79,13 @@ vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>();
- if (responseBody == null || !responseBody.IsSuccess)
+ if (responseBody is not { IsSuccess: true })
{
- _logger.LogError("Error fetching stop data: {StatusCode}", response.StatusCode);
- _logger.LogError("Sent request: {RequestContent}", await request.Content.ReadAsStringAsync());
- _logger.LogError("Response body: {ResponseBody}", await response.Content.ReadAsStringAsync());
+ _logger.LogError(
+ "Error fetching stop data, received {StatusCode} {ResponseBody}",
+ response.StatusCode,
+ await response.Content.ReadAsStringAsync()
+ );
return StatusCode(500, "Error fetching stop data");
}
@@ -135,7 +129,9 @@ vitrasa:20145: # (Samil) Samil por Bouzas - Final C3D+C3i
// The name of the stop
{ "name", stop.Name },
// Routes
- { "routes", JsonSerializer.Serialize(stop.Routes?.OrderBy(
+ { "routes", JsonSerializer.Serialize(stop.Routes?
+ .DistinctBy(r => r.ShortName)
+ .OrderBy(
r => r.ShortName,
Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
).Select(r => {
diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
index 4b556da..3bff631 100644
--- a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
+++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Costasdev.Busurbano.Backend</RootNamespace>
diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs
new file mode 100644
index 0000000..dfecdd6
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs
@@ -0,0 +1,82 @@
+using System.Globalization;
+using System.Text.Json.Serialization;
+
+namespace Costasdev.Busurbano.Backend.GraphClient.App;
+
+public class ArrivalsAtStopContent : IGraphRequest<string>
+{
+ public static string Query(string id)
+ {
+ return string.Create(CultureInfo.InvariantCulture, $@"
+ query Query {{
+ stop(id:""{id}"") {{
+ code
+ name
+ arrivals: stoptimesWithoutPatterns(numberOfDepartures:10) {{
+ trip {{
+ gtfsId
+ routeShortName
+ route {{
+ color
+ textColor
+ }}
+ }}
+ headsign
+ scheduledDeparture
+ }}
+ }}
+ }}
+ ");
+ }
+}
+
+public class ArrivalsAtStopResponse : AbstractGraphResponse
+{
+ [JsonPropertyName("stop")]
+ public StopItem Stop { get; set; }
+
+ public class StopItem
+ {
+ [JsonPropertyName("code")]
+ public required string Code { get; set; }
+
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("arrivals")]
+ public List<Arrival> Arrivals { get; set; } = [];
+ }
+
+ public class Arrival
+ {
+ [JsonPropertyName("headsign")]
+ public required string Headsign { get; set; }
+
+ [JsonPropertyName("scheduledDeparture")]
+ public int ScheduledDepartureSeconds { get; set; }
+
+ [JsonPropertyName("trip")]
+ public required TripDetails Trip { get; set; }
+ }
+
+ public class TripDetails
+ {
+ [JsonPropertyName("gtfsId")]
+ public required string GtfsId { get; set; }
+
+ [JsonPropertyName("routeShortName")]
+ public required string RouteShortName { get; set; }
+
+ [JsonPropertyName("route")]
+ public required RouteDetails Route { get; set; }
+ }
+
+ public class RouteDetails
+ {
+ [JsonPropertyName("color")]
+ public required string Color { get; set; }
+
+ [JsonPropertyName("textColor")]
+ public required string TextColor { get; set; }
+ }
+}
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index db9de59..279f096 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -122,7 +122,12 @@ export default function StopMap() {
Array.isArray(center) ? center[1] : center.lng;
const handlePointClick = (feature: any) => {
- const props: any = feature.properties;
+ const props: {
+ id: string;
+ code: string;
+ name: string;
+ routes: string;
+ } = feature.properties;
// TODO: Move ID to constant, improve type checking
if (!props || feature.layer.id !== "stops") {
console.warn("Invalid feature properties:", props);
@@ -130,14 +135,18 @@ export default function StopMap() {
}
const stopId = props.id;
-
- console.debug("Stop clicked:", stopId, props);
+ const routes: {
+ shortName: string;
+ colour: string;
+ textColour: string;
+ }[] = JSON.parse(props.routes || "[]");
setSelectedStop({
stopId: props.id,
stopCode: props.code,
name: props.name || "Unknown Stop",
- lines: JSON.parse(props.routes || "[]").map((route) => {
+ lines: routes.map((route) => {
+ console.log(route);
return {
line: route.shortName,
colour: route.colour,