aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-22 22:06:06 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-22 22:06:06 +0100
commitbed48c3d7e49b1736d50ce42d92bb6c18cf02504 (patch)
tree475571ad6fa8c7aa1f8e81520689bf1eb425164c /src
parent68f49dec91d68579803d6d579b1f1ecb4fc1dd1f (diff)
Refactor arrivals handling and improve type definitions; reorganise components
Diffstat (limited to 'src')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs29
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TileController.cs98
-rw-r--r--src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs20
-rw-r--r--src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs20
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs9
-rw-r--r--src/Costasdev.Busurbano.Backend/appsettings.Development.json8
-rw-r--r--src/frontend/app/api/schema.ts8
-rw-r--r--src/frontend/app/components/arrivals/ArrivalCard.css (renamed from src/frontend/app/components/Stops/ArrivalCard.css)0
-rw-r--r--src/frontend/app/components/arrivals/ArrivalCard.tsx (renamed from src/frontend/app/components/Stops/ArrivalCard.tsx)10
-rw-r--r--src/frontend/app/components/arrivals/ArrivalList.tsx (renamed from src/frontend/app/components/Stops/ArrivalList.tsx)0
-rw-r--r--src/frontend/app/components/map/StopSummarySheet.tsx2
11 files changed, 138 insertions, 66 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
index 5dee48d..7158137 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
@@ -1,7 +1,6 @@
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;
@@ -37,7 +36,14 @@ public partial class ArrivalsController : ControllerBase
var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz);
var todayLocal = nowLocal.Date;
- var requestContent = ArrivalsAtStopContent.Query(new(id, reduced ? 4 : 10));
+ var requestContent = ArrivalsAtStopContent.Query(
+ new ArrivalsAtStopContent.Args(
+ id,
+ reduced ? 4 : 10,
+ ShouldFetchPastArrivals(id)
+ )
+ );
+
var request = new HttpRequestMessage(HttpMethod.Post, "http://100.67.54.115:3957/otp/gtfs/v1");
request.Content = JsonContent.Create(new GraphClientRequest
{
@@ -61,13 +67,20 @@ public partial class ArrivalsController : ControllerBase
var minutesToArrive = (int)(departureTime - nowLocal).TotalMinutes;
//var isRunning = departureTime < nowLocal;
+ // TODO: Handle this properly, since many times it's "tomorrow" but not handled properly
+ if (minutesToArrive < ArrivalsAtStopContent.PastArrivalMinutesIncluded)
+ {
+ continue;
+ }
+
Arrival arrival = new()
{
+ TripId = item.Trip.GtfsId,
Route = new RouteInfo
{
ShortName = item.Trip.RouteShortName,
- Colour = item.Trip.Route.Color,
- TextColour = item.Trip.Route.TextColor
+ Colour = item.Trip.Route.Color ?? "FFFFFF",
+ TextColour = item.Trip.Route.TextColor ?? "000000"
},
Headsign = new HeadsignInfo
{
@@ -76,7 +89,7 @@ public partial class ArrivalsController : ControllerBase
Estimate = new ArrivalDetails
{
Minutes = minutesToArrive,
- Precission = departureTime < nowLocal ? ArrivalPrecission.Past : ArrivalPrecission.Scheduled
+ Precision = departureTime < nowLocal ? ArrivalPrecision.Past : ArrivalPrecision.Scheduled
}
};
@@ -91,6 +104,12 @@ public partial class ArrivalsController : ControllerBase
});
}
+ private static bool ShouldFetchPastArrivals(string id)
+ {
+ string feedId = id.Split(':', 2)[0];
+ return feedId == "xunta";
+ }
+
[LoggerMessage(LogLevel.Error, "Error fetching stop data, received {statusCode} {responseBody}")]
partial void LogErrorFetchingStopData(HttpStatusCode statusCode, string responseBody);
}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
index fad18e7..6354d67 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
@@ -46,9 +46,9 @@ public class TileController : ControllerBase
[HttpGet("stops/{z:int}/{x:int}/{y:int}")]
public async Task<IActionResult> Stops(int z, int x, int y)
{
- if (z < 9 || z > 16)
+ if (z is < 9 or > 20)
{
- return BadRequest("Zoom level out of range (9-16)");
+ return BadRequest("Zoom level out of range (9-20)");
}
var cacheHit = _cache.TryGetValue($"stops-tile-{z}-{x}-{y}", out byte[]? cachedTile);
@@ -96,10 +96,11 @@ public class TileController : ControllerBase
responseBody.Data?.StopsByBbox?.ForEach(stop =>
{
var idParts = stop.GtfsId.Split(':', 2);
+ string feedId = idParts[0];
string codeWithinFeed = stop.Code ?? string.Empty;
// TODO: Refactor this, maybe do it client-side or smth
- if (idParts[0] == "vitrasa")
+ if (feedId == "vitrasa")
{
var digits = new string(codeWithinFeed.Where(char.IsDigit).ToArray());
if (int.TryParse(digits, out int code))
@@ -108,12 +109,13 @@ public class TileController : ControllerBase
}
}
- if (HiddenStops.Contains($"{idParts[0]}:{codeWithinFeed}"))
+ if (HiddenStops.Contains($"{feedId}:{codeWithinFeed}"))
{
return;
}
- var fallbackColours = GetFallbackColourForFeed(idParts[0]);
+ var (Color, TextColor) = GetFallbackColourForFeed(idParts[0]);
+ var distinctRoutes = GetDistinctRoutes(feedId, stop.Routes ?? []);
Feature feature = new()
{
@@ -129,36 +131,34 @@ public class TileController : ControllerBase
// The name of the stop
{ "name", stop.Name },
// Routes
- { "routes", JsonSerializer.Serialize(stop.Routes?
- .DistinctBy(r => r.ShortName)
- .OrderBy(
- r => r.ShortName,
- Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
- ).Select(r => {
- var colour = r.Color ?? fallbackColours.Color;
- string textColour;
+ { "routes", JsonSerializer
+ .Serialize(
+ distinctRoutes.Select(r => {
+ var colour = r.Color ?? Color;
+ string textColour;
- if (r.Color is null) // None is present, use fallback
- {
- textColour = fallbackColours.TextColor;
- }
- else if (r.TextColor is null || r.TextColor.EndsWith("000000"))
- {
- // Text colour not provided, or default-black; check the better contrasting
- textColour = ContrastHelper.GetBestTextColour(colour);
- }
- else
- {
- // Use provided text colour
- textColour = r.TextColor;
- }
+ if (r.Color is null) // None is present, use fallback
+ {
+ textColour = TextColor;
+ }
+ else if (r.TextColor is null || r.TextColor.EndsWith("000000"))
+ {
+ // Text colour not provided, or default-black; check the better contrasting
+ textColour = ContrastHelper.GetBestTextColour(colour);
+ }
+ else
+ {
+ // Use provided text colour
+ textColour = r.TextColor;
+ }
- return new {
- shortName = r.ShortName,
- colour,
- textColour
- };
- })) }
+ return new {
+ shortName = r.ShortName,
+ colour,
+ textColour
+ };
+ }))
+ }
}
};
@@ -176,6 +176,37 @@ public class TileController : ControllerBase
return File(ms.ToArray(), "application/x-protobuf");
}
+ private static List<StopTileResponse.Route> GetDistinctRoutes(string feedId, List<StopTileResponse.Route> routes)
+ {
+ List<StopTileResponse.Route> distinctRoutes = [];
+ HashSet<string> seen = new();
+
+ foreach (var route in routes)
+ {
+ var seenId = route.ShortName;
+ if (feedId == "xunta")
+ {
+ // For Xunta routes we take only the contract number (XG123, for example)
+ seenId = seenId.Substring(0, 5);
+
+ route.ShortName = seenId;
+ }
+
+ if (seen.Contains(seenId))
+ {
+ continue;
+ }
+
+ seen.Add(seenId);
+ distinctRoutes.Add(route);
+ }
+
+ return [.. distinctRoutes.OrderBy(
+ r => r.ShortName,
+ Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
+ )];
+ }
+
private static (string Color, string TextColor) GetFallbackColourForFeed(string feed)
{
return feed switch
@@ -190,5 +221,4 @@ public class TileController : ControllerBase
};
}
-
}
diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs
index 53c1165..2c34784 100644
--- a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs
+++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs
@@ -5,16 +5,26 @@ namespace Costasdev.Busurbano.Backend.GraphClient.App;
public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args>
{
- public record Args(string Id, int DepartureCount);
+ public const int PastArrivalMinutesIncluded = -15;
+
+ public record Args(string Id, int DepartureCount, bool PastArrivals);
public static string Query(Args args)
{
+ var startTime = DateTimeOffset.Now;
+ if (args.PastArrivals)
+ {
+ startTime = DateTimeOffset.Now.AddMinutes(PastArrivalMinutesIncluded);
+ }
+
+ var startTimeUnix = startTime.ToUnixTimeSeconds();
+
return string.Create(CultureInfo.InvariantCulture, $@"
query Query {{
stop(id:""{args.Id}"") {{
code
name
- arrivals: stoptimesWithoutPatterns(numberOfDepartures:{args.DepartureCount}) {{
+ arrivals: stoptimesWithoutPatterns(numberOfDepartures:{args.DepartureCount}, startTime: {startTimeUnix}) {{
headsign
scheduledDeparture
pickupType
@@ -40,7 +50,7 @@ public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args>
public class ArrivalsAtStopResponse : AbstractGraphResponse
{
- [JsonPropertyName("stop")] public StopItem Stop { get; set; }
+ [JsonPropertyName("stop")] public required StopItem Stop { get; set; }
public class StopItem
{
@@ -87,9 +97,9 @@ public class ArrivalsAtStopResponse : AbstractGraphResponse
public class RouteDetails
{
- [JsonPropertyName("color")] public required string Color { get; set; }
+ [JsonPropertyName("color")] public string? Color { get; set; }
- [JsonPropertyName("textColor")] public required string TextColor { get; set; }
+ [JsonPropertyName("textColor")] public string? TextColor { get; set; }
}
public class PickupType
diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs
index 8a271f2..802de9a 100644
--- a/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs
+++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/StopTile.cs
@@ -42,35 +42,35 @@ public class StopTileResponse : AbstractGraphResponse
public record Stop
{
[JsonPropertyName("gtfsId")]
- public required string GtfsId { get; init; }
+ public required string GtfsId { get; set; }
[JsonPropertyName("code")]
- public string? Code { get; init; }
+ public string? Code { get; set; }
[JsonPropertyName("name")]
- public required string Name { get; init; }
+ public required string Name { get; set; }
[JsonPropertyName("lat")]
- public required double Lat { get; init; }
+ public required double Lat { get; set; }
[JsonPropertyName("lon")]
- public required double Lon { get; init; }
+ public required double Lon { get; set; }
[JsonPropertyName("routes")]
- public List<Route>? Routes { get; init; }
+ public List<Route>? Routes { get; set; }
}
public record Route
{
[JsonPropertyName("gtfsId")]
- public required string GtfsId { get; init; }
+ public required string GtfsId { get; set; }
[JsonPropertyName("shortName")]
- public required string ShortName { get; init; }
+ public required string ShortName { get; set; }
[JsonPropertyName("color")]
- public string? Color { get; init; }
+ public string? Color { get; set; }
[JsonPropertyName("textColor")]
- public string? TextColor { get; init; }
+ public string? TextColor { get; set; }
}
}
diff --git a/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs
index c813ccf..516a1c5 100644
--- a/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs
+++ b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs
@@ -4,6 +4,9 @@ namespace Costasdev.Busurbano.Backend.Types.Arrivals;
public class Arrival
{
+ [JsonPropertyName("tripId")]
+ public required string TripId { get; set; }
+
[JsonPropertyName("route")]
public required RouteInfo Route { get; set; }
@@ -49,12 +52,12 @@ public class ArrivalDetails
[JsonPropertyName("minutes")]
public required int Minutes { get; set; }
- [JsonPropertyName("precission")]
- public ArrivalPrecission Precission { get; set; } = ArrivalPrecission.Scheduled;
+ [JsonPropertyName("precision")]
+ public ArrivalPrecision Precision { get; set; } = ArrivalPrecision.Scheduled;
}
[JsonConverter(typeof(JsonStringEnumConverter))]
-public enum ArrivalPrecission
+public enum ArrivalPrecision
{
[JsonStringEnumMemberName("confident")]
Confident = 0,
diff --git a/src/Costasdev.Busurbano.Backend/appsettings.Development.json b/src/Costasdev.Busurbano.Backend/appsettings.Development.json
new file mode 100644
index 0000000..2b38630
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Costasdev.Busurbano.Backend": "Debug"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts
index 60e2d97..bb1e96c 100644
--- a/src/frontend/app/api/schema.ts
+++ b/src/frontend/app/api/schema.ts
@@ -12,7 +12,7 @@ export const HeadsignInfoSchema = z.object({
marquee: z.string().optional().nullable(),
});
-export const ArrivalPrecissionSchema = z.enum([
+export const ArrivalPrecisionSchema = z.enum([
"confident",
"unsure",
"scheduled",
@@ -20,8 +20,8 @@ export const ArrivalPrecissionSchema = z.enum([
]);
export const ArrivalDetailsSchema = z.object({
- minutes: z.number(),
- precission: ArrivalPrecissionSchema,
+ minutes: z.number().int(),
+ precision: ArrivalPrecisionSchema,
});
export const DelayBadgeSchema = z.object({
@@ -49,7 +49,7 @@ export const StopArrivalsResponseSchema = z.object({
export type RouteInfo = z.infer<typeof RouteInfoSchema>;
export type HeadsignInfo = z.infer<typeof HeadsignInfoSchema>;
-export type ArrivalPrecission = z.infer<typeof ArrivalPrecissionSchema>;
+export type ArrivalPrecision = z.infer<typeof ArrivalPrecisionSchema>;
export type ArrivalDetails = z.infer<typeof ArrivalDetailsSchema>;
export type DelayBadge = z.infer<typeof DelayBadgeSchema>;
export type ShiftBadge = z.infer<typeof ShiftBadgeSchema>;
diff --git a/src/frontend/app/components/Stops/ArrivalCard.css b/src/frontend/app/components/arrivals/ArrivalCard.css
index 5835352..5835352 100644
--- a/src/frontend/app/components/Stops/ArrivalCard.css
+++ b/src/frontend/app/components/arrivals/ArrivalCard.css
diff --git a/src/frontend/app/components/Stops/ArrivalCard.tsx b/src/frontend/app/components/arrivals/ArrivalCard.tsx
index 96d0af0..de4fcc7 100644
--- a/src/frontend/app/components/Stops/ArrivalCard.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalCard.tsx
@@ -16,11 +16,11 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
const { t } = useTranslation();
const { route, headsign, estimate } = arrival;
- const etaValue = Math.max(0, Math.round(estimate.minutes)).toString();
+ const etaValue = estimate.minutes.toString();
const etaUnit = t("estimates.minutes", "min");
const timeClass = useMemo(() => {
- switch (estimate.precission) {
+ switch (estimate.precision) {
case "confident":
return "time-running";
case "unsure":
@@ -30,7 +30,7 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
default:
return "time-scheduled";
}
- }, [estimate.precission]);
+ }, [estimate.precision]);
return (
<div
@@ -50,7 +50,9 @@ export const ArrivalCard: React.FC<ArrivalCardProps> = ({
/>
</div>
<div className="flex-1 min-w-0 flex flex-col gap-1">
- <strong className="text-base text-(--text-color) overflow-hidden text-ellipsis line-clamp-2 leading-tight">
+ <strong
+ className={`text-base overflow-hidden text-ellipsis line-clamp-2 leading-tight ${estimate.precision == "past" ? "line-through" : ""}`}
+ >
{headsign.destination}
</strong>
</div>
diff --git a/src/frontend/app/components/Stops/ArrivalList.tsx b/src/frontend/app/components/arrivals/ArrivalList.tsx
index a1210d5..a1210d5 100644
--- a/src/frontend/app/components/Stops/ArrivalList.tsx
+++ b/src/frontend/app/components/arrivals/ArrivalList.tsx
diff --git a/src/frontend/app/components/map/StopSummarySheet.tsx b/src/frontend/app/components/map/StopSummarySheet.tsx
index 16a9cbe..e318bee 100644
--- a/src/frontend/app/components/map/StopSummarySheet.tsx
+++ b/src/frontend/app/components/map/StopSummarySheet.tsx
@@ -3,7 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { Sheet } from "react-modal-sheet";
import { Link } from "react-router";
-import { ArrivalList } from "~/components/Stops/ArrivalList";
+import { ArrivalList } from "~/components/arrivals/ArrivalList";
import { useStopArrivals } from "../../hooks/useArrivals";
import { ErrorDisplay } from "../ErrorDisplay";
import LineIcon from "../LineIcon";