Next alpha stage commit

This commit is contained in:
2026-07-06 20:54:46 +02:00
parent e23387738b
commit 1d7ec956d2
42 changed files with 11603 additions and 2837 deletions

View File

@@ -6,8 +6,9 @@ from shapely.geometry import LineString, Point, shape
from sqlalchemy import select
from app.db import reset_db, session_scope
from app.journey import find_journeys, search_scheduled_stops
from app.journey import find_journeys, nearest_scheduled_stops, search_scheduled_stops
from app.models import (
CanonicalStop,
CanonicalStopLink,
Dataset,
GtfsCalendar,
@@ -33,6 +34,7 @@ from app.pipeline.route_layer import (
rebuild_route_layer,
)
from app.pipeline.utils import geometry_json_and_bbox, norm_ref
from app.routing import direct_route_between_points
def test_directional_candidate_selection_prefers_matching_osm_geometry_orientation():
@@ -245,6 +247,318 @@ def test_opposite_gtfs_shapes_share_osm_visual_route_and_reverse_journey_segment
assert tuple(coords[-1]) == (0.0, 0.0)
def test_exact_stop_token_does_not_expand_to_merged_canonical_stop():
reset_db()
with session_scope() as session:
source = Source(name="Merged Station GTFS", kind="gtfs", url="./merged.zip")
session.add(source)
session.flush()
dataset = Dataset(
source_id=source.id,
kind="gtfs",
local_path="./merged.zip",
sha256="merged",
is_active=True,
status="imported",
)
session.add(dataset)
session.flush()
session.add_all(
[
GtfsStop(dataset_id=dataset.id, stop_id="station_bus", name="Example Hbf", lat=52.0, lon=13.0000),
GtfsStop(dataset_id=dataset.id, stop_id="station_train", name="Example Hbf", lat=52.0001, lon=13.0001),
GtfsStop(dataset_id=dataset.id, stop_id="target", name="Target", lat=52.01, lon=13.01),
GtfsRoute(
dataset_id=dataset.id,
route_id="bus",
short_name="B",
route_type=3,
mode="bus",
route_key=norm_ref("B"),
),
GtfsRoute(
dataset_id=dataset.id,
route_id="train",
short_name="T",
route_type=2,
mode="train",
route_key=norm_ref("T"),
),
GtfsTrip(dataset_id=dataset.id, route_id="bus", trip_id="bus_trip", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="train", trip_id="train_trip", service_id="daily"),
_stop_time(dataset.id, "train_trip", "station_train", 1, "08:00:00", 28800),
_stop_time(dataset.id, "train_trip", "target", 2, "08:05:00", 29100),
_stop_time(dataset.id, "bus_trip", "station_bus", 1, "08:20:00", 30000),
_stop_time(dataset.id, "bus_trip", "target", 2, "08:35:00", 30900),
]
)
session.flush()
rebuild_route_layer(session)
grouped = search_scheduled_stops(session, "Example Hbf")
assert grouped
assert grouped[0]["grouped"] is True
exact_hits = nearest_scheduled_stops(session, lat=52.0, lon=13.0, radius_m=30, grouped=False)
assert exact_hits[0]["id"] == f"stop:{dataset.id}:station_bus"
assert exact_hits[0]["grouped"] is False
journey = find_journeys(
session,
from_stop_id=f"stop:{dataset.id}:station_bus",
to_stop_id="target",
departure="07:55",
max_transfers=0,
limit=1,
)
assert journey["journeys"]
legs = journey["journeys"][0]["legs"]
assert legs[0]["from"]["stop_id"] == "station_bus"
assert legs[0]["mode"] in {"walk", "bus"}
if legs[0]["mode"] == "walk":
assert legs[0]["to"]["stop_id"] == "station_train"
assert legs[1]["trip_id"] == "train_trip"
else:
assert legs[0]["trip_id"] == "bus_trip"
def test_exact_stop_can_walk_to_adjacent_concrete_stop_for_transit():
reset_db()
with session_scope() as session:
source = Source(name="Adjacent Stops GTFS", kind="gtfs", url="./adjacent.zip")
session.add(source)
session.flush()
dataset = Dataset(
source_id=source.id,
kind="gtfs",
local_path="./adjacent.zip",
sha256="adjacent",
is_active=True,
status="imported",
)
session.add(dataset)
session.flush()
session.add_all(
[
GtfsStop(dataset_id=dataset.id, stop_id="station_bus", name="Example Hbf", lat=52.0, lon=13.0000),
GtfsStop(dataset_id=dataset.id, stop_id="station_train", name="Example Hbf", lat=52.0001, lon=13.0001),
GtfsStop(dataset_id=dataset.id, stop_id="target", name="Target", lat=52.01, lon=13.01),
GtfsStop(dataset_id=dataset.id, stop_id="elsewhere", name="Elsewhere", lat=52.02, lon=13.02),
GtfsRoute(
dataset_id=dataset.id,
route_id="bus",
short_name="B",
route_type=3,
mode="bus",
route_key=norm_ref("B"),
),
GtfsRoute(
dataset_id=dataset.id,
route_id="train",
short_name="T",
route_type=2,
mode="train",
route_key=norm_ref("T"),
),
GtfsTrip(dataset_id=dataset.id, route_id="bus", trip_id="bus_unrelated", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="train", trip_id="train_trip", service_id="daily"),
_stop_time(dataset.id, "bus_unrelated", "station_bus", 1, "09:00:00", 32400),
_stop_time(dataset.id, "bus_unrelated", "elsewhere", 2, "09:20:00", 33600),
_stop_time(dataset.id, "train_trip", "station_train", 1, "08:05:00", 29100),
_stop_time(dataset.id, "train_trip", "target", 2, "08:15:00", 29700),
]
)
session.flush()
rebuild_route_layer(session)
journey = find_journeys(
session,
from_stop_id=f"stop:{dataset.id}:station_bus",
to_stop_id="target",
departure="08:00",
max_transfers=0,
limit=2,
)
assert journey["journeys"]
legs = journey["journeys"][0]["legs"]
assert [leg["mode"] for leg in legs] == ["walk", "train"]
assert legs[0]["from"]["stop_id"] == "station_bus"
assert legs[0]["to"]["stop_id"] == "station_train"
assert legs[0]["arrival_time"] == legs[1]["departure_time"]
assert legs[0]["departure_time"] != "08:00:00"
assert legs[1]["trip_id"] == "train_trip"
walk_points = [
feature
for feature in journey["journeys"][0]["features"]["features"]
if feature["geometry"]["type"] == "Point" and feature["properties"].get("mode") == "walk"
]
assert [point["properties"]["stop_id"] for point in walk_points] == ["station_bus", "station_train"]
def test_walk_only_journey_reuses_routed_geometry(monkeypatch):
reset_db()
with session_scope() as session:
source = Source(name="Walk Geometry GTFS", kind="gtfs", url="./walk.zip")
session.add(source)
session.flush()
dataset = Dataset(
source_id=source.id,
kind="gtfs",
local_path="./walk.zip",
sha256="walk",
is_active=True,
status="imported",
)
session.add(dataset)
session.flush()
origin = GtfsStop(dataset_id=dataset.id, stop_id="origin", name="Origin", lat=52.0, lon=13.0)
target = GtfsStop(dataset_id=dataset.id, stop_id="target", name="Target", lat=52.001, lon=13.002)
session.add_all(
[
origin,
target,
GtfsRoute(dataset_id=dataset.id, route_id="marker", short_name="M", route_type=3, mode="bus"),
GtfsTrip(dataset_id=dataset.id, route_id="marker", trip_id="origin_marker", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="marker", trip_id="target_marker", service_id="daily"),
]
)
session.flush()
session.add_all(
[
_stop_time(dataset.id, "origin_marker", "origin", 1, "07:00:00", 25200),
_stop_time(dataset.id, "target_marker", "target", 1, "07:00:00", 25200),
]
)
origin_place = CanonicalStop(
stop_key=f"gtfs:{dataset.id}:origin",
name="Origin",
normalized_name="origin",
lat=52.0,
lon=13.0,
mode="bus",
)
target_place = CanonicalStop(
stop_key=f"gtfs:{dataset.id}:target",
name="Target",
normalized_name="target",
lat=52.001,
lon=13.002,
mode="bus",
)
session.add_all([origin_place, target_place])
session.flush()
session.add_all(
[
CanonicalStopLink(
canonical_stop_id=origin_place.id,
layer="timetable",
object_type="gtfs_stop",
dataset_id=dataset.id,
object_id=origin.id,
external_id="origin",
role="primary",
confidence=1.0,
),
CanonicalStopLink(
canonical_stop_id=target_place.id,
layer="timetable",
object_type="gtfs_stop",
dataset_id=dataset.id,
object_id=target.id,
external_id="target",
role="primary",
confidence=1.0,
),
]
)
session.flush()
routed_coordinates = [[13.0, 52.0], [13.001, 52.0007], [13.002, 52.001]]
def fake_route_between_points(*args, **kwargs):
assert kwargs["mode"] == "walk"
return {
"distance_m": 240.0,
"duration_seconds": 180.2,
"features": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": routed_coordinates},
"properties": {"mode": "walk"},
}
],
},
}
monkeypatch.setattr("app.journey.route_between_points", fake_route_between_points)
result = find_journeys(
session,
from_stop_id="origin",
to_stop_id="target",
departure="08:00",
max_transfers=0,
limit=3,
)
walk_journey = next(
journey
for journey in result["journeys"]
if [leg["mode"] for leg in journey["legs"]] == ["walk"]
)
walk_feature = next(
feature
for feature in walk_journey["features"]["features"]
if feature["geometry"]["type"] == "LineString"
)
assert walk_feature["geometry"]["coordinates"] == routed_coordinates
assert walk_feature["properties"]["geometry_source"] == "routing_layer:walk"
assert walk_journey["legs"][0]["distance_m"] == 240.0
assert walk_journey["legs"][0]["duration_seconds"] == 181
def test_direct_point_routing_supports_bike_mode_and_aliases():
reset_db()
with session_scope() as session:
walk = direct_route_between_points(
session,
from_lon=13.0,
from_lat=52.0,
to_lon=13.002,
to_lat=52.001,
mode="walk",
)
bike = direct_route_between_points(
session,
from_lon=13.0,
from_lat=52.0,
to_lon=13.002,
to_lat=52.001,
mode="bike",
)
bicycle = direct_route_between_points(
session,
from_lon=13.0,
from_lat=52.0,
to_lon=13.002,
to_lat=52.001,
mode="bicycle",
)
assert bike["mode"] == "bike"
assert bicycle["mode"] == "bike"
assert bike["duration_seconds"] < walk["duration_seconds"]
bike_feature = bike["features"]["features"][0]
assert bike_feature["properties"]["mode"] == "bike"
def test_journey_geometry_rejects_remote_route_pattern_and_uses_trip_shape():
reset_db()
with session_scope() as session:
@@ -959,6 +1273,105 @@ def test_journey_service_date_filters_duplicate_clock_time_trips():
assert [journey["legs"][0]["trip_id"] for journey in monday["journeys"]] == ["weekday_trip"]
def test_direct_nearby_destination_walk_dominates_same_endpoint_transfer():
reset_db()
with session_scope() as session:
source = Source(name="Nearby Destination GTFS", kind="gtfs", url="./nearby.zip")
session.add(source)
session.flush()
dataset = Dataset(
source_id=source.id,
kind="gtfs",
local_path="./nearby.zip",
sha256="nearby",
is_active=True,
status="imported",
)
session.add(dataset)
session.flush()
session.add_all(
[
GtfsStop(dataset_id=dataset.id, stop_id="origin", name="Eppelheim Rathaus", lat=49.4010, lon=8.6330),
GtfsStop(dataset_id=dataset.id, stop_id="market", name="Pfaffengrund Marktstrasse", lat=49.4050, lon=8.6410),
GtfsStop(dataset_id=dataset.id, stop_id="henkel", name="Pfaffengrund Henkel-Teroson", lat=49.4090, lon=8.6500),
GtfsStop(dataset_id=dataset.id, stop_id="hbf", name="Heidelberg Hbf", lat=49.4030, lon=8.6550),
GtfsStop(dataset_id=dataset.id, stop_id="target", name="Heidelberg Ochsenkopf", lat=49.4108, lon=8.6520),
GtfsStop(dataset_id=dataset.id, stop_id="marker", name="Target Marker", lat=49.4120, lon=8.6540),
GtfsRoute(
dataset_id=dataset.id,
route_id="rnv22",
short_name="RNV 22",
route_type=0,
mode="tram",
route_key=norm_ref("RNV 22"),
),
GtfsRoute(
dataset_id=dataset.id,
route_id="marker",
short_name="M",
route_type=3,
mode="bus",
route_key=norm_ref("M"),
),
GtfsRoute(
dataset_id=dataset.id,
route_id="rnv5",
short_name="RNV 5",
route_type=0,
mode="tram",
route_key=norm_ref("RNV 5"),
),
GtfsTrip(dataset_id=dataset.id, route_id="rnv22", trip_id="through_tram", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="rnv22", trip_id="later_tram", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="marker", trip_id="target_marker", service_id="daily"),
GtfsTrip(dataset_id=dataset.id, route_id="rnv5", trip_id="exact_destination_tram", service_id="daily"),
_stop_time(dataset.id, "through_tram", "origin", 1, "08:04:00", 29040),
_stop_time(dataset.id, "through_tram", "market", 2, "08:08:00", 29280),
_stop_time(dataset.id, "through_tram", "henkel", 3, "08:09:00", 29340),
_stop_time(dataset.id, "through_tram", "hbf", 4, "08:17:00", 29820),
_stop_time(dataset.id, "later_tram", "market", 1, "08:18:00", 29880),
_stop_time(dataset.id, "later_tram", "henkel", 2, "08:19:00", 29940),
_stop_time(dataset.id, "target_marker", "marker", 1, "07:00:00", 25200),
_stop_time(dataset.id, "target_marker", "target", 2, "07:01:00", 25260),
_stop_time(dataset.id, "exact_destination_tram", "hbf", 1, "08:28:00", 30480),
_stop_time(dataset.id, "exact_destination_tram", "target", 2, "08:31:00", 30660),
]
)
session.flush()
rebuild_route_layer(session)
result = find_journeys(
session,
from_stop_id="origin",
to_stop_id="target",
departure="08:00",
max_transfers=1,
limit=5,
)
assert result["journeys"]
direct_with_final_walk = [
journey
for journey in result["journeys"]
if [leg.get("trip_id") for leg in journey["legs"] if leg.get("mode") != "walk"] == ["through_tram"]
and journey["legs"][-1]["mode"] == "walk"
and str(journey["legs"][-1]["from"]["stop_id"]).startswith("canonical:")
and journey["legs"][-1]["from"]["name"] == "Pfaffengrund Henkel-Teroson"
and journey["transfers"] == 0
]
assert direct_with_final_walk
assert all(
[leg.get("trip_id") for leg in journey["legs"] if leg.get("mode") != "walk"]
!= ["through_tram", "later_tram"]
for journey in result["journeys"]
)
assert any(
[leg.get("trip_id") for leg in journey["legs"] if leg.get("mode") != "walk"]
== ["through_tram", "exact_destination_tram"]
for journey in result["journeys"]
)
def _route_candidate(
feature_id: int,
osm_id: str,