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;
///
/// Service for loading shapes and calculating remaining path from a given stop point
///
public class ShapeTraversalService
{
private readonly AppConfiguration _configuration;
private readonly ILogger _logger;
private readonly ICoordinateTransformation _transformation;
public ShapeTraversalService(IOptions options, ILogger 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);
}
///
/// Loads a shape from disk
///
public async Task 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;
}
}
public async Task?> GetShapePathAsync(string shapeId, int startIndex)
{
var shape = await LoadShapeAsync(shapeId);
if (shape == null) return null;
var result = new List();
// Ensure startIndex is within bounds
if (startIndex < 0) startIndex = 0;
// If startIndex is beyond the end, return empty list
if (startIndex >= shape.Points.Count) return result;
for (int i = startIndex; i < shape.Points.Count; i++)
{
var pos = TransformToLatLng(shape.Points[i]);
pos.ShapeIndex = i;
result.Add(pos);
}
return result;
}
public async Task FindClosestPointIndexAsync(string shapeId, double lat, double lon)
{
var shape = await LoadShapeAsync(shapeId);
if (shape == null) return null;
// Transform input WGS84 to EPSG:25829
// Input is [Longitude, Latitude]
var inverseTransform = _transformation.MathTransform.Inverse();
var transformed = inverseTransform.Transform(new[] { lon, lat });
var location = new Epsg25829 { X = transformed[0], Y = transformed[1] };
return FindClosestPointIndex(shape.Points, location);
}
///
/// Calculates the bus position by reverse-traversing the shape from the stop location
///
/// The shape points (in EPSG:25829 meters)
/// The stop location (in EPSG:25829 meters)
/// Distance in meters from the stop to traverse backwards
/// The lat/lng position of the bus and the stop index on the shape
public (Position? BusPosition, int StopIndex) GetBusPosition(Shape shape, Epsg25829 stopLocation, int distanceMeters)
{
if (shape.Points.Count == 0 || distanceMeters <= 0)
{
return (null, -1);
}
// 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, forwardIndex) = TraverseBackwards(shape.Points.ToArray(), closestPointIndex, distanceMeters);
if (busPoint == null)
{
return (null, closestPointIndex);
}
var forwardPoint = shape.Points[forwardIndex];
// 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);
pos.ShapeIndex = forwardIndex;
return (pos, closestPointIndex);
}
///
/// Traverses backwards along the shape from a starting point by the specified distance
///
private (Epsg25829 point, int forwardIndex) 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], 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, currentIndex);
}
remainingDistance -= segmentDistance;
currentIndex--;
}
// We've reached the beginning of the shape
var fwd = Math.Min(1, shapePoints.Length - 1);
return (shapePoints[0], fwd);
}
///
/// Interpolates a point between two points at a given ratio
///
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
};
}
///
/// Finds the index of the closest point in the shape to the given location
///
private int FindClosestPointIndex(IEnumerable 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;
}
///
/// Calculates Euclidean distance between two points in meters
///
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);
}
///
/// Transforms a point from EPSG:25829 (meters) to EPSG:4326 (lat/lng)
///
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)
};
}
}