Add OSM Track Harvesting Policy and demo database initialization script
- Updated documentation to include OSM Track Harvesting Policy with details on railway types, service filters, usage filters, and geometry guardrails. - Introduced a new script `init_demo_db.py` to automate the database setup process, including environment checks, running migrations, and loading OSM fixtures for demo data.
This commit is contained in:
@@ -8,11 +8,15 @@ import math
|
||||
import sys
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
from typing import Any, Iterable, Mapping
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from backend.app.core.osm_config import (
|
||||
DEFAULT_REGIONS,
|
||||
TRACK_ALLOWED_RAILWAY_TYPES,
|
||||
TRACK_EXCLUDED_SERVICE_TAGS,
|
||||
TRACK_EXCLUDED_USAGE_TAGS,
|
||||
TRACK_MIN_LENGTH_METERS,
|
||||
TRACK_TAG_FILTERS,
|
||||
compile_overpass_filters,
|
||||
)
|
||||
@@ -104,13 +108,15 @@ def normalize_track_elements(elements: Iterable[dict[str, Any]]) -> list[dict[st
|
||||
continue
|
||||
|
||||
tags: dict[str, Any] = element.get("tags", {})
|
||||
length_meters = _polyline_length(coordinates)
|
||||
if not _should_include_track(tags, length_meters):
|
||||
continue
|
||||
|
||||
name = tags.get("name")
|
||||
maxspeed = _parse_maxspeed(tags.get("maxspeed"))
|
||||
status = _derive_status(tags.get("railway"))
|
||||
is_bidirectional = not _is_oneway(tags.get("oneway"))
|
||||
|
||||
length_meters = _polyline_length(coordinates)
|
||||
|
||||
tracks.append(
|
||||
{
|
||||
"osmId": str(element.get("id")),
|
||||
@@ -156,6 +162,25 @@ def _derive_status(value: Any) -> str:
|
||||
return "operational"
|
||||
|
||||
|
||||
def _should_include_track(tags: Mapping[str, Any], length_meters: float) -> bool:
|
||||
railway = str(tags.get("railway", "")).lower()
|
||||
if railway not in TRACK_ALLOWED_RAILWAY_TYPES:
|
||||
return False
|
||||
|
||||
if length_meters < TRACK_MIN_LENGTH_METERS:
|
||||
return False
|
||||
|
||||
service = str(tags.get("service", "")).lower()
|
||||
if service and service in TRACK_EXCLUDED_SERVICE_TAGS:
|
||||
return False
|
||||
|
||||
usage = str(tags.get("usage", "")).lower()
|
||||
if usage and usage in TRACK_EXCLUDED_USAGE_TAGS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _is_oneway(value: Any) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, Iterable, Mapping, Sequence
|
||||
from geoalchemy2.elements import WKBElement, WKTElement
|
||||
from geoalchemy2.shape import to_shape
|
||||
|
||||
from backend.app.core.osm_config import TRACK_STATION_SNAP_RADIUS_METERS
|
||||
from backend.app.db.session import SessionLocal
|
||||
from backend.app.models import TrackCreate
|
||||
from backend.app.repositories import StationRepository, TrackRepository
|
||||
@@ -133,26 +134,40 @@ def load_tracks(tracks: Iterable[ParsedTrack], commit: bool = True) -> int:
|
||||
|
||||
for track_data in tracks:
|
||||
start_station = _nearest_station(
|
||||
track_data.coordinates[0], station_index)
|
||||
track_data.coordinates[0],
|
||||
station_index,
|
||||
TRACK_STATION_SNAP_RADIUS_METERS,
|
||||
)
|
||||
end_station = _nearest_station(
|
||||
track_data.coordinates[-1], station_index)
|
||||
track_data.coordinates[-1],
|
||||
station_index,
|
||||
TRACK_STATION_SNAP_RADIUS_METERS,
|
||||
)
|
||||
|
||||
if not start_station or not end_station:
|
||||
continue
|
||||
|
||||
if start_station.id == end_station.id:
|
||||
continue
|
||||
|
||||
pair = (start_station.id, end_station.id)
|
||||
if pair in existing_pairs:
|
||||
continue
|
||||
|
||||
length = track_data.length_meters or _polyline_length(
|
||||
track_data.coordinates)
|
||||
max_speed = (
|
||||
int(round(track_data.max_speed_kph))
|
||||
if track_data.max_speed_kph is not None
|
||||
else None
|
||||
)
|
||||
create_schema = TrackCreate(
|
||||
name=track_data.name,
|
||||
start_station_id=start_station.id,
|
||||
end_station_id=end_station.id,
|
||||
coordinates=track_data.coordinates,
|
||||
length_meters=length,
|
||||
max_speed_kph=track_data.max_speed_kph,
|
||||
max_speed_kph=max_speed,
|
||||
status=track_data.status,
|
||||
is_bidirectional=track_data.is_bidirectional,
|
||||
)
|
||||
@@ -170,7 +185,9 @@ def load_tracks(tracks: Iterable[ParsedTrack], commit: bool = True) -> int:
|
||||
|
||||
|
||||
def _nearest_station(
|
||||
coordinate: tuple[float, float], stations: Sequence[StationRef]
|
||||
coordinate: tuple[float, float],
|
||||
stations: Sequence[StationRef],
|
||||
max_distance_meters: float,
|
||||
) -> StationRef | None:
|
||||
best_station: StationRef | None = None
|
||||
best_distance = math.inf
|
||||
@@ -180,7 +197,9 @@ def _nearest_station(
|
||||
if distance < best_distance:
|
||||
best_station = station
|
||||
best_distance = distance
|
||||
return best_station
|
||||
if best_distance <= max_distance_meters:
|
||||
return best_station
|
||||
return None
|
||||
|
||||
|
||||
def _build_station_index(stations: Iterable[Any]) -> list[StationRef]:
|
||||
@@ -192,11 +211,15 @@ def _build_station_index(stations: Iterable[Any]) -> list[StationRef]:
|
||||
point = _to_point(location)
|
||||
if point is None:
|
||||
continue
|
||||
latitude = getattr(point, "y", None)
|
||||
longitude = getattr(point, "x", None)
|
||||
if latitude is None or longitude is None:
|
||||
continue
|
||||
index.append(
|
||||
StationRef(
|
||||
id=str(station.id),
|
||||
latitude=float(point.y),
|
||||
longitude=float(point.x),
|
||||
latitude=float(latitude),
|
||||
longitude=float(longitude),
|
||||
)
|
||||
)
|
||||
return index
|
||||
|
||||
Reference in New Issue
Block a user