Add car journey route generator
This commit is contained in:
parent
1e9169b740
commit
b7d95f26ec
3 changed files with 1115 additions and 0 deletions
344
tests/test_car_journey_yaml.py
Normal file
344
tests/test_car_journey_yaml.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
"""Tests for car journey YAML generation."""
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import agenda.car_journey_yaml
|
||||
from agenda.types import Trip
|
||||
|
||||
|
||||
def write_home_route(data_dir: Path) -> None:
|
||||
"""Write a route that identifies PCH as the home coordinate."""
|
||||
route_dir = data_dir / "car_routes"
|
||||
route_dir.mkdir()
|
||||
(route_dir / "PCH_to_EMF.geojson").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-2.60283, 51.44083], [-2.37789, 52.038]],
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_car_journeys_for_trip_uses_accommodation_dates_and_locations(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Build outbound and return journeys for the trip accommodation."""
|
||||
write_home_route(tmp_path)
|
||||
trip = Trip(
|
||||
start=date(2025, 4, 22),
|
||||
accommodation=[
|
||||
{
|
||||
"location": "Callestick",
|
||||
"from": datetime(2025, 4, 22, 15, 0),
|
||||
"to": datetime(2025, 4, 25, 10, 0),
|
||||
"latitude": 50.307093,
|
||||
"longitude": -5.13466,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
journeys = agenda.car_journey_yaml.car_journeys_for_trip(
|
||||
trip, tmp_path, destination="accommodation"
|
||||
)
|
||||
|
||||
assert [journey.route for journey in journeys] == [
|
||||
"PCH_to_Callestick",
|
||||
"Callestick_to_PCH",
|
||||
]
|
||||
assert [journey.depart for journey in journeys] == [
|
||||
date(2025, 4, 22),
|
||||
date(2025, 4, 25),
|
||||
]
|
||||
assert journeys[0].start == (-2.60283, 51.44083)
|
||||
assert journeys[0].end == (-5.13466, 50.307093)
|
||||
assert journeys[1].start == (-5.13466, 50.307093)
|
||||
assert journeys[1].end == (-2.60283, 51.44083)
|
||||
|
||||
|
||||
def test_car_journeys_for_trip_can_route_to_airport(tmp_path: Path) -> None:
|
||||
"""Build outbound and return journeys for airport access."""
|
||||
write_home_route(tmp_path)
|
||||
trip = Trip(
|
||||
start=date(2025, 11, 17),
|
||||
travel=[
|
||||
{
|
||||
"type": "flight",
|
||||
"depart": datetime(2025, 11, 17, 17, 55),
|
||||
"arrive": datetime(2025, 11, 18, 14, 50),
|
||||
"from_airport": {
|
||||
"iata": "LHR",
|
||||
"latitude": 51.4775,
|
||||
"longitude": -0.461389,
|
||||
},
|
||||
"to_airport": {
|
||||
"iata": "HKG",
|
||||
"latitude": 22.308889,
|
||||
"longitude": 113.914444,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "flight",
|
||||
"depart": datetime(2025, 11, 30, 22, 45),
|
||||
"arrive": datetime(2025, 12, 1, 5, 35),
|
||||
"from_airport": {
|
||||
"iata": "HKG",
|
||||
"latitude": 22.308889,
|
||||
"longitude": 113.914444,
|
||||
},
|
||||
"to_airport": {
|
||||
"iata": "LHR",
|
||||
"latitude": 51.4775,
|
||||
"longitude": -0.461389,
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
journeys = agenda.car_journey_yaml.car_journeys_for_trip(
|
||||
trip, tmp_path, destination="airport"
|
||||
)
|
||||
|
||||
assert [journey.route for journey in journeys] == ["PCH_to_LHR", "LHR_to_PCH"]
|
||||
assert [journey.depart for journey in journeys] == [
|
||||
date(2025, 11, 17),
|
||||
date(2025, 12, 1),
|
||||
]
|
||||
assert journeys[0].start == (-2.60283, 51.44083)
|
||||
assert journeys[0].end == (-0.461389, 51.4775)
|
||||
assert journeys[1].start == (-0.461389, 51.4775)
|
||||
assert journeys[1].end == (-2.60283, 51.44083)
|
||||
|
||||
|
||||
def test_car_journeys_for_trip_can_route_around_ferry(tmp_path: Path) -> None:
|
||||
"""Build home, ferry terminal and accommodation driving legs."""
|
||||
write_home_route(tmp_path)
|
||||
trip = Trip(
|
||||
start=date(2025, 7, 4),
|
||||
travel=[
|
||||
{
|
||||
"type": "ferry",
|
||||
"depart": datetime(2025, 7, 4, 22, 0),
|
||||
"arrive": datetime(2025, 7, 5, 8, 0),
|
||||
"from": "Plymouth",
|
||||
"to": "Roscoff",
|
||||
"from_terminal": {
|
||||
"name": "Plymouth",
|
||||
"latitude": 50.3651254,
|
||||
"longitude": -4.1578164,
|
||||
},
|
||||
"to_terminal": {
|
||||
"name": "Roscoff",
|
||||
"latitude": 48.721672,
|
||||
"longitude": -3.966925,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "ferry",
|
||||
"depart": datetime(2025, 7, 21, 15, 0),
|
||||
"arrive": datetime(2025, 7, 21, 20, 10),
|
||||
"from": "Roscoff",
|
||||
"to": "Plymouth",
|
||||
"from_terminal": {
|
||||
"name": "Roscoff",
|
||||
"latitude": 48.721672,
|
||||
"longitude": -3.966925,
|
||||
},
|
||||
"to_terminal": {
|
||||
"name": "Plymouth",
|
||||
"latitude": 50.3651254,
|
||||
"longitude": -4.1578164,
|
||||
},
|
||||
},
|
||||
],
|
||||
accommodation=[
|
||||
{
|
||||
"location": "Brest",
|
||||
"from": datetime(2025, 7, 5, 16, 0),
|
||||
"to": datetime(2025, 7, 21, 10, 0),
|
||||
"latitude": 48.3645307,
|
||||
"longitude": -4.5534685,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
journeys = agenda.car_journey_yaml.car_journeys_for_trip(
|
||||
trip, tmp_path, destination="ferry"
|
||||
)
|
||||
|
||||
assert [journey.route for journey in journeys] == [
|
||||
"PCH_to_Plymouth",
|
||||
"Roscoff_to_Brest",
|
||||
"Brest_to_Roscoff",
|
||||
"Plymouth_to_PCH",
|
||||
]
|
||||
assert [journey.depart for journey in journeys] == [
|
||||
date(2025, 7, 4),
|
||||
date(2025, 7, 5),
|
||||
date(2025, 7, 21),
|
||||
date(2025, 7, 21),
|
||||
]
|
||||
assert journeys[0].start == (-2.60283, 51.44083)
|
||||
assert journeys[0].end == (-4.1578164, 50.3651254)
|
||||
assert journeys[1].start == (-3.966925, 48.721672)
|
||||
assert journeys[1].end == (-4.5534685, 48.3645307)
|
||||
assert journeys[2].start == (-4.5534685, 48.3645307)
|
||||
assert journeys[2].end == (-3.966925, 48.721672)
|
||||
assert journeys[3].start == (-4.1578164, 50.3651254)
|
||||
assert journeys[3].end == (-2.60283, 51.44083)
|
||||
|
||||
|
||||
def test_car_journeys_for_trip_can_route_fly_drive_chain(tmp_path: Path) -> None:
|
||||
"""Build home, destination rental and return airport driving legs."""
|
||||
write_home_route(tmp_path)
|
||||
trip = Trip(
|
||||
start=date(2025, 9, 13),
|
||||
travel=[
|
||||
{
|
||||
"type": "flight",
|
||||
"depart": datetime(2025, 9, 14, 6, 50),
|
||||
"arrive": datetime(2025, 9, 14, 8, 55),
|
||||
"from_airport": {
|
||||
"iata": "LTN",
|
||||
"latitude": 51.8747,
|
||||
"longitude": -0.3683,
|
||||
"country": "gb",
|
||||
},
|
||||
"to_airport": {
|
||||
"iata": "KEF",
|
||||
"latitude": 63.985,
|
||||
"longitude": -22.605556,
|
||||
"country": "is",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "flight",
|
||||
"depart": datetime(2025, 10, 1, 9, 0),
|
||||
"arrive": datetime(2025, 10, 1, 13, 0),
|
||||
"from_airport": {
|
||||
"iata": "KEF",
|
||||
"latitude": 63.985,
|
||||
"longitude": -22.605556,
|
||||
"country": "is",
|
||||
},
|
||||
"to_airport": {
|
||||
"iata": "LTN",
|
||||
"latitude": 51.8747,
|
||||
"longitude": -0.3683,
|
||||
"country": "gb",
|
||||
},
|
||||
},
|
||||
],
|
||||
accommodation=[
|
||||
{
|
||||
"location": "Luton Airport",
|
||||
"country": "gb",
|
||||
"from": datetime(2025, 9, 13, 15, 0),
|
||||
"to": datetime(2025, 9, 14, 12, 0),
|
||||
"latitude": 51.8745678,
|
||||
"longitude": -0.3844256,
|
||||
},
|
||||
{
|
||||
"location": "Akranes",
|
||||
"country": "is",
|
||||
"from": datetime(2025, 9, 14, 16, 0),
|
||||
"to": datetime(2025, 9, 16, 11, 0),
|
||||
"latitude": 64.4344723,
|
||||
"longitude": -21.5517961,
|
||||
},
|
||||
{
|
||||
"location": "Ísafjörður",
|
||||
"country": "is",
|
||||
"from": datetime(2025, 9, 16, 15, 0),
|
||||
"to": datetime(2025, 10, 1, 11, 0),
|
||||
"latitude": 66.072445,
|
||||
"longitude": -23.120111,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
journeys = agenda.car_journey_yaml.car_journeys_for_trip(
|
||||
trip, tmp_path, destination="fly-drive"
|
||||
)
|
||||
|
||||
assert [journey.route for journey in journeys] == [
|
||||
"PCH_to_Luton_Airport",
|
||||
"KEF_to_Akranes",
|
||||
"Akranes_to_Isafjordur",
|
||||
"Isafjordur_to_KEF",
|
||||
"LTN_to_PCH",
|
||||
]
|
||||
assert [journey.depart for journey in journeys] == [
|
||||
date(2025, 9, 13),
|
||||
date(2025, 9, 14),
|
||||
date(2025, 9, 16),
|
||||
date(2025, 10, 1),
|
||||
date(2025, 10, 1),
|
||||
]
|
||||
|
||||
|
||||
def test_write_route_files_skips_existing_routes(tmp_path: Path) -> None:
|
||||
"""Existing route files should not call the route API."""
|
||||
write_home_route(tmp_path)
|
||||
journey = agenda.car_journey_yaml.CarJourney(
|
||||
trip=date(2025, 4, 22),
|
||||
depart=date(2025, 4, 22),
|
||||
arrive=date(2025, 4, 22),
|
||||
route="PCH_to_EMF",
|
||||
start=(-2.60283, 51.44083),
|
||||
end=(-2.37789, 52.038),
|
||||
)
|
||||
|
||||
def fail_fetch(
|
||||
_start: agenda.car_journey_yaml.LonLat,
|
||||
_end: agenda.car_journey_yaml.LonLat,
|
||||
) -> agenda.car_journey_yaml.GeoJSON:
|
||||
raise AssertionError("route API should not be called")
|
||||
|
||||
written = agenda.car_journey_yaml.write_route_files(tmp_path, [journey], fail_fetch)
|
||||
|
||||
assert written == 0
|
||||
|
||||
|
||||
def test_import_car_journeys_inserts_chronologically_and_is_idempotent(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Generated journeys should be inserted without duplicates."""
|
||||
(tmp_path / "car_journeys.yaml").write_text("""---
|
||||
- trip: 2025-05-01
|
||||
depart: 2025-05-01
|
||||
arrive: 2025-05-01
|
||||
route: PCH_to_Later
|
||||
""")
|
||||
journeys = [
|
||||
agenda.car_journey_yaml.CarJourney(
|
||||
trip=date(2025, 4, 22),
|
||||
depart=date(2025, 4, 22),
|
||||
arrive=date(2025, 4, 22),
|
||||
route="PCH_to_Callestick",
|
||||
start=(-2.60283, 51.44083),
|
||||
end=(-5.13466, 50.307093),
|
||||
),
|
||||
agenda.car_journey_yaml.CarJourney(
|
||||
trip=date(2025, 4, 22),
|
||||
depart=date(2025, 4, 25),
|
||||
arrive=date(2025, 4, 25),
|
||||
route="Callestick_to_PCH",
|
||||
start=(-5.13466, 50.307093),
|
||||
end=(-2.60283, 51.44083),
|
||||
),
|
||||
]
|
||||
|
||||
added = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys)
|
||||
added_again = agenda.car_journey_yaml.import_car_journeys(tmp_path, journeys)
|
||||
|
||||
assert added == 2
|
||||
assert added_again == 0
|
||||
text = (tmp_path / "car_journeys.yaml").read_text()
|
||||
assert text.index("PCH_to_Callestick") < text.index("PCH_to_Later")
|
||||
Loading…
Add table
Add a link
Reference in a new issue