From 04a8eb43eead686c0e32255965f6e573c5ffcbfa Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Fri, 21 Nov 2025 21:22:33 +0100 Subject: feat: Enhance shape retrieval with bus and stop point indexing; update related components --- .../Controllers/VigoController.cs | 76 +++++++++++++++++++--- .../Services/ShapeTraversalService.cs | 30 +++++++-- .../Types/ConsolidatedCirculation.cs | 1 + src/frontend/app/components/StopMapModal.tsx | 50 ++++++++++---- src/frontend/app/components/StopMapSheet.tsx | 12 +++- 5 files changed, 138 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs index 151baa3..1f81bf1 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs @@ -49,18 +49,60 @@ public class VigoController : ControllerBase [HttpGet("GetShape")] public async Task GetShape( [FromQuery] string shapeId, - [FromQuery] int startPointIndex = 0 + [FromQuery] int? startPointIndex = null, + [FromQuery] double? busLat = null, + [FromQuery] double? busLon = null, + [FromQuery] int? busShapeIndex = null, + [FromQuery] double? stopLat = null, + [FromQuery] double? stopLon = null, + [FromQuery] int? stopShapeIndex = null ) { - // Include a significant number of previous points to ensure continuity and context - // Backtrack 15 points to cover any potential gaps or dense point sequences - var adjustedStartIndex = Math.Max(0, startPointIndex - 15); - var path = await _shapeService.GetShapePathAsync(shapeId, adjustedStartIndex); + var path = await _shapeService.GetShapePathAsync(shapeId, 0); if (path == null) { return NotFound(); } + // Determine bus point + object? busPoint = null; + if (busShapeIndex.HasValue && busShapeIndex.Value >= 0 && busShapeIndex.Value < path.Count) + { + var p = path[busShapeIndex.Value]; + busPoint = new { lat = p.Latitude, lon = p.Longitude, index = busShapeIndex.Value }; + } + else if (busLat.HasValue && busLon.HasValue) + { + var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, busLat.Value, busLon.Value); + if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count) + { + var p = path[idx.Value]; + busPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value }; + } + } + else if (startPointIndex.HasValue && startPointIndex.Value >= 0 && startPointIndex.Value < path.Count) + { + var p = path[startPointIndex.Value]; + busPoint = new { lat = p.Latitude, lon = p.Longitude, index = startPointIndex.Value }; + } + + // Determine stop point + object? stopPoint = null; + if (stopShapeIndex.HasValue && stopShapeIndex.Value >= 0 && stopShapeIndex.Value < path.Count) + { + var p = path[stopShapeIndex.Value]; + stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = stopShapeIndex.Value }; + } + else if (stopLat.HasValue && stopLon.HasValue) + { + var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, stopLat.Value, stopLon.Value); + if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count) + { + var p = path[idx.Value]; + stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value }; + } + } + // Convert to GeoJSON LineString var coordinates = path.Select(p => new[] { p.Longitude, p.Latitude }).ToList(); @@ -72,7 +114,11 @@ public class VigoController : ControllerBase type = "LineString", coordinates = coordinates }, - properties = new { } + properties = new + { + busPoint, + stopPoint + } }; return Ok(geoJson); @@ -214,7 +260,7 @@ public class VigoController : ControllerBase // 2) From the valid trips, pick the one with smallest Abs(TimeDiff). // This handles "as late as it gets" (large negative TimeDiff) by preferring smaller delays if available, // but accepting large delays if that's the only option (and better than an invalid early trip). - const int maxEarlyArrivalMinutes = 3; + const int maxEarlyArrivalMinutes = 5; var bestMatch = possibleCirculations .Select(c => new @@ -259,6 +305,7 @@ public class VigoController : ControllerBase var isRunning = closestCirculation.StartingDateTime()!.Value <= now; Position? currentPosition = null; + int? stopShapeIndex = null; // Calculate bus position only for realtime trips that have already departed if (isRunning && !string.IsNullOrEmpty(closestCirculation.ShapeId)) @@ -266,7 +313,9 @@ public class VigoController : ControllerBase var shape = await _shapeService.LoadShapeAsync(closestCirculation.ShapeId); if (shape != null && stopLocation != null) { - currentPosition = _shapeService.GetBusPosition(shape, stopLocation, estimate.Meters); + var result = _shapeService.GetBusPosition(shape, stopLocation, estimate.Meters); + currentPosition = result.BusPosition; + stopShapeIndex = result.StopIndex; } } @@ -288,7 +337,8 @@ public class VigoController : ControllerBase Minutes = estimate.Minutes, Distance = estimate.Meters }, - CurrentPosition = currentPosition + CurrentPosition = currentPosition, + StopShapeIndex = stopShapeIndex }); usedTripIds.Add(closestCirculation.TripId); @@ -310,6 +360,12 @@ public class VigoController : ControllerBase continue; // already represented via a matched realtime } + var minutes = (int)(sched.CallingDateTime()!.Value - now).TotalMinutes; + if (minutes == 0) + { + continue; + } + consolidatedCirculations.Add(new ConsolidatedCirculation { Line = sched.Line, @@ -317,7 +373,7 @@ public class VigoController : ControllerBase Schedule = new ScheduleData { Running = sched.StartingDateTime()!.Value <= now, - Minutes = (int)(sched.CallingDateTime()!.Value - now).TotalMinutes, + Minutes = minutes, TripId = sched.TripId, ServiceId = sched.ServiceId, ShapeId = sched.ShapeId, diff --git a/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs index 37b76ee..7263ad0 100644 --- a/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs @@ -68,27 +68,45 @@ public class ShapeTraversalService 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++) { - result.Add(TransformToLatLng(shape.Points[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, or null if not calculable - public Position? GetBusPosition(Shape shape, Epsg25829 stopLocation, int distanceMeters) + /// 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; + return (null, -1); } // Find the closest point on the shape to the stop @@ -99,7 +117,7 @@ public class ShapeTraversalService if (busPoint == null) { - return null; + return (null, closestPointIndex); } var forwardPoint = shape.Points[forwardIndex]; @@ -114,7 +132,7 @@ public class ShapeTraversalService var pos = TransformToLatLng(busPoint); pos.OrientationDegrees = (int)Math.Round(bearing); pos.ShapeIndex = forwardIndex; - return pos; + return (pos, closestPointIndex); } /// diff --git a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs index 2ac3206..ff6dbde 100644 --- a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs +++ b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs @@ -8,6 +8,7 @@ public class ConsolidatedCirculation public ScheduleData? Schedule { get; set; } public RealTimeData? RealTime { get; set; } public Position? CurrentPosition { get; set; } + public int? StopShapeIndex { get; set; } public string[] NextStreets { get; set; } = []; } diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx index 67435b4..91cd513 100644 --- a/src/frontend/app/components/StopMapModal.tsx +++ b/src/frontend/app/components/StopMapModal.tsx @@ -25,6 +25,7 @@ export interface ConsolidatedCirculationForMap { line: string; route: string; currentPosition?: Position; + stopShapeIndex?: number; schedule?: { shapeId?: string; }; @@ -90,15 +91,32 @@ export const StopMapModal: React.FC = ({ if (!mapRef.current) return; const points: { lat: number; lon: number }[] = []; - if (stop.latitude && stop.longitude) { - points.push({ lat: stop.latitude, lon: stop.longitude }); - } + if ( + shapeData?.properties?.busPoint && + shapeData?.properties?.stopPoint && + shapeData?.geometry?.coordinates + ) { + const busIdx = shapeData.properties.busPoint.index; + const stopIdx = shapeData.properties.stopPoint.index; + const coords = shapeData.geometry.coordinates; - if (selectedBus?.currentPosition) { - points.push({ - lat: selectedBus.currentPosition.latitude, - lon: selectedBus.currentPosition.longitude, - }); + const start = Math.min(busIdx, stopIdx); + const end = Math.max(busIdx, stopIdx); + + for (let i = start; i <= end; i++) { + points.push({ lat: coords[i][1], lon: coords[i][0] }); + } + } else { + if (stop.latitude && stop.longitude) { + points.push({ lat: stop.latitude, lon: stop.longitude }); + } + + if (selectedBus?.currentPosition) { + points.push({ + lat: selectedBus.currentPosition.latitude, + lon: selectedBus.currentPosition.longitude, + }); + } } if (points.length === 0) return; @@ -132,7 +150,7 @@ export const StopMapModal: React.FC = ({ } as any); } } catch {} - }, [stop, selectedBus]); + }, [stop, selectedBus, shapeData]); // Load style without traffic layers for the stop map useEffect(() => { @@ -218,10 +236,18 @@ export const StopMapModal: React.FC = ({ const shapeId = selectedBus.schedule.shapeId; const shapeIndex = selectedBus.currentPosition.shapeIndex; + const stopShapeIndex = selectedBus.stopShapeIndex; + const stopLat = stop.latitude; + const stopLon = stop.longitude; + + let url = `${regionConfig.shapeEndpoint}?shapeId=${shapeId}&busShapeIndex=${shapeIndex}`; + if (stopShapeIndex !== undefined) { + url += `&stopShapeIndex=${stopShapeIndex}`; + } else { + url += `&stopLat=${stopLat}&stopLon=${stopLon}`; + } - fetch( - `${regionConfig.shapeEndpoint}?shapeId=${shapeId}&startPointIndex=${shapeIndex}` - ) + fetch(url) .then((res) => { if (res.ok) return res.json(); return null; diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx index 7dab82b..d70fcb6 100644 --- a/src/frontend/app/components/StopMapSheet.tsx +++ b/src/frontend/app/components/StopMapSheet.tsx @@ -19,6 +19,7 @@ export interface ConsolidatedCirculationForMap { line: string; route: string; currentPosition?: Position; + stopShapeIndex?: number; schedule?: { shapeId?: string; }; @@ -61,9 +62,14 @@ export const StopMap: React.FC = ({ ) { const key = `${c.schedule.shapeId}_${c.currentPosition.shapeIndex}`; if (!shapes[key]) { - fetch( - `${regionConfig.shapeEndpoint}?shapeId=${c.schedule.shapeId}&startPointIndex=${c.currentPosition.shapeIndex}` - ) + let url = `${regionConfig.shapeEndpoint}?shapeId=${c.schedule.shapeId}&busShapeIndex=${c.currentPosition.shapeIndex}`; + if (c.stopShapeIndex !== undefined) { + url += `&stopShapeIndex=${c.stopShapeIndex}`; + } else { + url += `&stopLat=${stop.latitude}&stopLon=${stop.longitude}`; + } + + fetch(url) .then((res) => { if (res.ok) return res.json(); return null; -- cgit v1.3