aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend
diff options
context:
space:
mode:
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs38
-rw-r--r--src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj1
-rw-r--r--src/Costasdev.Busurbano.Backend/Program.cs2
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs196
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs8
5 files changed, 242 insertions, 3 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
index 84b0461..05a3025 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
@@ -2,6 +2,7 @@ using System.Globalization;
using System.Text;
using System.Text.Json;
using Costasdev.Busurbano.Backend.Configuration;
+using Costasdev.Busurbano.Backend.Services;
using Costasdev.Busurbano.Backend.Types;
using Costasdev.VigoTransitApi;
using Microsoft.AspNetCore.Mvc;
@@ -18,12 +19,14 @@ public class VigoController : ControllerBase
private readonly ILogger<VigoController> _logger;
private readonly VigoTransitApiClient _api;
private readonly AppConfiguration _configuration;
+ private readonly ShapeTraversalService _shapeService;
- public VigoController(HttpClient http, IOptions<AppConfiguration> options, ILogger<VigoController> logger)
+ public VigoController(HttpClient http, IOptions<AppConfiguration> options, ILogger<VigoController> logger, ShapeTraversalService shapeService)
{
_logger = logger;
_api = new VigoTransitApiClient(http);
_configuration = options.Value;
+ _shapeService = shapeService;
}
[HttpGet("GetStopEstimates")]
@@ -93,6 +96,8 @@ public class VigoController : ControllerBase
.Where(c => c.StartingDateTime() != null && c.CallingDateTime() != null)
.ToList();
+ var stopLocation = timetableTask.Result.Location;
+
var now = nowLocal.AddSeconds(60 - nowLocal.Second);
// Define the scope end as the time of the last realtime arrival (no extra buffer)
var lastEstimateArrivalMinutes = realTimeEstimates.Max(e => e.Minutes);
@@ -206,13 +211,26 @@ public class VigoController : ControllerBase
continue;
}
+ var isRunning = closestCirculation.StartingDateTime()!.Value <= now;
+ Position? currentPosition = null;
+
+ // Calculate bus position only for realtime trips that have already departed
+ if (isRunning && !string.IsNullOrEmpty(closestCirculation.ShapeId))
+ {
+ var shape = await _shapeService.LoadShapeAsync(closestCirculation.ShapeId);
+ if (shape != null && stopLocation != null)
+ {
+ currentPosition = _shapeService.GetBusPosition(shape, stopLocation, estimate.Meters);
+ }
+ }
+
consolidatedCirculations.Add(new ConsolidatedCirculation
{
Line = estimate.Line,
Route = estimate.Route,
Schedule = new ScheduleData
{
- Running = closestCirculation.StartingDateTime()!.Value <= now,
+ Running = isRunning,
Minutes = (int)(closestCirculation.CallingDateTime()!.Value - now).TotalMinutes,
TripId = closestCirculation.TripId,
ServiceId = closestCirculation.ServiceId,
@@ -221,7 +239,8 @@ public class VigoController : ControllerBase
{
Minutes = estimate.Minutes,
Distance = estimate.Meters
- }
+ },
+ CurrentPosition = currentPosition
});
usedTripIds.Add(closestCirculation.TripId);
@@ -280,6 +299,19 @@ public class VigoController : ControllerBase
return stopArrivals;
}
+ private async Task<Shape> LoadShapeProto(string shapeId)
+ {
+ var file = Path.Combine(_configuration.ScheduleBasePath, shapeId + ".pb");
+ if (!SysFile.Exists(file))
+ {
+ throw new FileNotFoundException();
+ }
+
+ var contents = await SysFile.ReadAllBytesAsync(file);
+ var shape = Shape.Parser.ParseFrom(contents);
+ return shape;
+ }
+
private async Task<List<ScheduledStop>> LoadTimetable(string stopId, string dateString)
{
var file = Path.Combine(_configuration.ScheduleBasePath, dateString, stopId + ".json");
diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
index d25a59e..0ec51df 100644
--- a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
+++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj
@@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Costasdev.VigoTransitApi" Version="0.1.0" />
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
+ <PackageReference Include="ProjNet" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs
index 7de4039..46f2595 100644
--- a/src/Costasdev.Busurbano.Backend/Program.cs
+++ b/src/Costasdev.Busurbano.Backend/Program.cs
@@ -1,4 +1,5 @@
using Costasdev.Busurbano.Backend.Configuration;
+using Costasdev.Busurbano.Backend.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -7,6 +8,7 @@ builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("A
builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
+builder.Services.AddSingleton<ShapeTraversalService>();
var app = builder.Build();
diff --git a/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs
new file mode 100644
index 0000000..bded78b
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs
@@ -0,0 +1,196 @@
+using Costasdev.Busurbano.Backend.Configuration;
+using Costasdev.Busurbano.Backend.Types;
+using Microsoft.Extensions.Options;
+using ProjNet.CoordinateSystems;
+using ProjNet.CoordinateSystems.Transformations;
+using SysFile = System.IO.File;
+
+namespace Costasdev.Busurbano.Backend.Services;
+
+/// <summary>
+/// Service for loading shapes and calculating remaining path from a given stop point
+/// </summary>
+public class ShapeTraversalService
+{
+ private readonly AppConfiguration _configuration;
+ private readonly ILogger<ShapeTraversalService> _logger;
+ private readonly ICoordinateTransformation _transformation;
+
+ public ShapeTraversalService(IOptions<AppConfiguration> options, ILogger<ShapeTraversalService> logger)
+ {
+ _configuration = options.Value;
+ _logger = logger;
+
+ // Set up coordinate transformation from EPSG:25829 (meters) to EPSG:4326 (lat/lng)
+ var ctFactory = new CoordinateTransformationFactory();
+ var csFactory = new CoordinateSystemFactory();
+
+ // EPSG:25829 - ETRS89 / UTM zone 29N
+ var source = csFactory.CreateFromWkt(
+ "PROJCS[\"ETRS89 / UTM zone 29N\",GEOGCS[\"ETRS89\",DATUM[\"European_Terrestrial_Reference_System_1989\",SPHEROID[\"GRS 1980\",6378137,298.257222101,AUTHORITY[\"EPSG\",\"7019\"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",\"6258\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4258\"]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",-9],PARAMETER[\"scale_factor\",0.9996],PARAMETER[\"false_easting\",500000],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],AUTHORITY[\"EPSG\",\"25829\"]]");
+
+ // EPSG:4326 - WGS84
+ var target = GeographicCoordinateSystem.WGS84;
+
+ _transformation = ctFactory.CreateFromCoordinateSystems(source, target);
+ }
+
+ /// <summary>
+ /// Loads a shape from disk
+ /// </summary>
+ public async Task<Shape?> LoadShapeAsync(string shapeId)
+ {
+ var file = Path.Combine(_configuration.ScheduleBasePath, "shapes", shapeId + ".pb");
+ if (!SysFile.Exists(file))
+ {
+ _logger.LogWarning("Shape file not found: {ShapeId}", shapeId);
+ return null;
+ }
+
+ try
+ {
+ var contents = await SysFile.ReadAllBytesAsync(file);
+ var shape = Shape.Parser.ParseFrom(contents);
+ return shape;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error loading shape {ShapeId}", shapeId);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Calculates the bus position by reverse-traversing the shape from the stop location
+ /// </summary>
+ /// <param name="shape">The shape points (in EPSG:25829 meters)</param>
+ /// <param name="stopLocation">The stop location (in EPSG:25829 meters)</param>
+ /// <param name="distanceMeters">Distance in meters from the stop to traverse backwards</param>
+ /// <returns>The lat/lng position of the bus, or null if not calculable</returns>
+ public Position? GetBusPosition(Shape shape, Epsg25829 stopLocation, int distanceMeters)
+ {
+ if (shape.Points.Count == 0 || distanceMeters <= 0)
+ {
+ return null;
+ }
+
+ // Find the closest point on the shape to the stop
+ int closestPointIndex = FindClosestPointIndex(shape.Points, stopLocation);
+
+ // Traverse backwards from the closest point to find the position at the given distance
+ var (busPoint, forwardPoint) = TraverseBackwards(shape.Points.ToArray(), closestPointIndex, distanceMeters);
+
+ if (busPoint == null)
+ {
+ return null;
+ }
+
+ // Compute orientation in EPSG:25829 (meters): 0°=North, 90°=East (azimuth)
+ var dx = forwardPoint.X - busPoint.X; // Easting difference
+ var dy = forwardPoint.Y - busPoint.Y; // Northing difference
+ var bearing = Math.Atan2(dx, dy) * 180.0 / Math.PI; // swap for 0° north
+ if (bearing < 0) bearing += 360.0;
+
+ // Transform from EPSG:25829 (meters) to EPSG:4326 (lat/lng)
+ var pos = TransformToLatLng(busPoint);
+ pos.OrientationDegrees = (int)Math.Round(bearing);
+ return pos;
+ }
+
+ /// <summary>
+ /// Traverses backwards along the shape from a starting point by the specified distance
+ /// </summary>
+ private (Epsg25829 point, Epsg25829 forward) TraverseBackwards(Epsg25829[] shapePoints, int startIndex, double distanceMeters)
+ {
+ if (startIndex <= 0)
+ {
+ // Already at the beginning, return the first point
+ var forwardIdx = Math.Min(1, shapePoints.Length - 1);
+ return (shapePoints[0], shapePoints[forwardIdx]);
+ }
+
+ double remainingDistance = distanceMeters;
+ int currentIndex = startIndex;
+
+ while (currentIndex > 0 && remainingDistance > 0)
+ {
+ var segmentDistance = CalculateDistance(shapePoints[currentIndex], shapePoints[currentIndex - 1]);
+
+ if (segmentDistance >= remainingDistance)
+ {
+ // The bus position is somewhere along this segment
+ // Interpolate between the two points
+ var ratio = remainingDistance / segmentDistance;
+ var interpolated = InterpolatePoint(shapePoints[currentIndex], shapePoints[currentIndex - 1], ratio);
+ // Forward direction is towards the stop (increasing index direction)
+ return (interpolated, shapePoints[currentIndex]);
+ }
+
+ remainingDistance -= segmentDistance;
+ currentIndex--;
+ }
+
+ // We've reached the beginning of the shape
+ var fwd = shapePoints[Math.Min(1, shapePoints.Length - 1)];
+ return (shapePoints[0], fwd);
+ }
+
+ /// <summary>
+ /// Interpolates a point between two points at a given ratio
+ /// </summary>
+ private Epsg25829 InterpolatePoint(Epsg25829 from, Epsg25829 to, double ratio)
+ {
+ return new Epsg25829
+ {
+ X = from.X + (to.X - from.X) * ratio,
+ Y = from.Y + (to.Y - from.Y) * ratio
+ };
+ }
+
+ /// <summary>
+ /// Finds the index of the closest point in the shape to the given location
+ /// </summary>
+ private int FindClosestPointIndex(IEnumerable<Epsg25829> shapePoints, Epsg25829 location)
+ {
+ var pointsArray = shapePoints.ToArray();
+ var minDistance = double.MaxValue;
+ var closestIndex = 0;
+
+ for (int i = 0; i < pointsArray.Length; i++)
+ {
+ var distance = CalculateDistance(pointsArray[i], location);
+ if (distance < minDistance)
+ {
+ minDistance = distance;
+ closestIndex = i;
+ }
+ }
+
+ return closestIndex;
+ }
+
+ /// <summary>
+ /// Calculates Euclidean distance between two points in meters
+ /// </summary>
+ private double CalculateDistance(Epsg25829 p1, Epsg25829 p2)
+ {
+ var dx = p1.X - p2.X;
+ var dy = p1.Y - p2.Y;
+ return Math.Sqrt(dx * dx + dy * dy);
+ }
+
+ /// <summary>
+ /// Transforms a point from EPSG:25829 (meters) to EPSG:4326 (lat/lng)
+ /// </summary>
+ private Position TransformToLatLng(Epsg25829 point)
+ {
+ var transformed = _transformation.MathTransform.Transform(new[] { point.X, point.Y });
+ return new Position
+ {
+ // Round to 6 decimals (~0.1m precision)
+ Longitude = Math.Round(transformed[0], 6),
+ Latitude = Math.Round(transformed[1], 6)
+ };
+ }
+
+}
diff --git a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs
index 7cc79c0..3806241 100644
--- a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs
+++ b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs
@@ -7,6 +7,7 @@ public class ConsolidatedCirculation
public ScheduleData? Schedule { get; set; }
public RealTimeData? RealTime { get; set; }
+ public Position? CurrentPosition { get; set; }
}
public class RealTimeData
@@ -22,3 +23,10 @@ public class ScheduleData
public required string ServiceId { get; set; }
public required string TripId { get; set; }
}
+
+public class Position
+{
+ public required double Latitude { get; set; }
+ public required double Longitude { get; set; }
+ public int OrientationDegrees { get; set; }
+}