feat: Add combined track functionality with repository and service layers
Some checks failed
Backend CI / lint-and-test (push) Failing after 2m27s
Frontend CI / lint-and-build (push) Successful in 57s

- Introduced CombinedTrackModel, CombinedTrackCreate, and CombinedTrackRepository for managing combined tracks.
- Implemented logic to create combined tracks based on existing tracks between two stations.
- Added methods to check for existing combined tracks and retrieve constituent track IDs.
- Enhanced TrackModel and TrackRepository to support OSM ID and track updates.
- Created migration scripts for adding combined tracks table and OSM ID to tracks.
- Updated services and API endpoints to handle combined track operations.
- Added tests for combined track creation, repository methods, and API interactions.
This commit is contained in:
2025-11-10 14:12:28 +01:00
parent f73ab7ad14
commit 68048ff574
21 changed files with 1107 additions and 103 deletions

View File

@@ -22,6 +22,7 @@ from backend.app.repositories import StationRepository, TrackRepository
@dataclass(slots=True)
class ParsedTrack:
coordinates: list[tuple[float, float]]
osm_id: str | None = None
name: str | None = None
length_meters: float | None = None
max_speed_kph: float | None = None
@@ -97,7 +98,8 @@ def _parse_track_entries(entries: Iterable[Mapping[str, Any]]) -> list[ParsedTra
processed_coordinates: list[tuple[float, float]] = []
for pair in coordinates:
if not isinstance(pair, Sequence) or len(pair) != 2:
raise ValueError(f"Invalid coordinate pair {pair!r} in track entry")
raise ValueError(
f"Invalid coordinate pair {pair!r} in track entry")
lat, lon = pair
processed_coordinates.append((float(lat), float(lon)))
@@ -106,10 +108,12 @@ def _parse_track_entries(entries: Iterable[Mapping[str, Any]]) -> list[ParsedTra
max_speed = _safe_float(entry.get("maxSpeedKph"))
status = entry.get("status", "operational")
is_bidirectional = entry.get("isBidirectional", True)
osm_id = entry.get("osmId")
parsed.append(
ParsedTrack(
coordinates=processed_coordinates,
osm_id=str(osm_id) if osm_id else None,
name=str(name) if name else None,
length_meters=length,
max_speed_kph=max_speed,
@@ -133,6 +137,12 @@ def load_tracks(tracks: Iterable[ParsedTrack], commit: bool = True) -> int:
}
for track_data in tracks:
# Skip if track with this OSM ID already exists
if track_data.osm_id and track_repo.exists_by_osm_id(track_data.osm_id):
print(
f"Skipping track {track_data.osm_id} - already exists by OSM ID")
continue
start_station = _nearest_station(
track_data.coordinates[0],
station_index,
@@ -145,13 +155,19 @@ def load_tracks(tracks: Iterable[ParsedTrack], commit: bool = True) -> int:
)
if not start_station or not end_station:
print(
f"Skipping track {track_data.osm_id} - no start/end stations found")
continue
if start_station.id == end_station.id:
print(
f"Skipping track {track_data.osm_id} - start and end stations are the same")
continue
pair = (start_station.id, end_station.id)
if pair in existing_pairs:
print(
f"Skipping track {track_data.osm_id} - station pair {pair} already exists")
continue
length = track_data.length_meters or _polyline_length(
@@ -163,6 +179,7 @@ def load_tracks(tracks: Iterable[ParsedTrack], commit: bool = True) -> int:
else None
)
create_schema = TrackCreate(
osm_id=track_data.osm_id,
name=track_data.name,
start_station_id=start_station.id,
end_station_id=end_station.id,
@@ -193,7 +210,8 @@ def _nearest_station(
best_station: StationRef | None = None
best_distance = math.inf
for station in stations:
distance = _haversine(coordinate, (station.latitude, station.longitude))
distance = _haversine(
coordinate, (station.latitude, station.longitude))
if distance < best_distance:
best_station = station
best_distance = distance