Next alpha stage commit
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user