fix(roadtrip): other ways come from the router that draws the leg, and a choice is saved once it holds

The picker of other ways asked OSRM with nothing avoided while the rail drives a trip
that avoids a road class through Valhalla, so the list had no current road, called a road
the rail was not on the fastest, and choosing it changed nothing. A choice was stored as
one via where the offer strayed furthest, without asking whether the router then drove
it: a point on a ferry was pulled to the pier and the drive went overland, and a way
weighed away from motorways kept the motorway after its one point. An OSRM answer standing
in for a Valhalla that did not respond was cached as the avoided road, unmarked.

The rail now hands out a router per leg with the leg's own mode, avoided classes and
engine, and its days carry the line of every leg. Offers come from that engine, and the
rail's own leg always heads the list as Current. A choice is pinned and proven against
that router with at most three pins before anything is written; a way it will not follow
is not saved and says why, with a hint when it crosses by ferry, and a leg that changed
meanwhile or a device offline writes nothing. The OSRM stand-in is filed under the plain
key only and flags the day as not avoided.
This commit is contained in:
Maurice
2026-09-23 09:04:58 +02:00
parent 6124e0b056
commit 53a422d22d
53 changed files with 2121 additions and 378 deletions
@@ -225,6 +225,22 @@ describe('browser-independent roadtrip calculation', () => {
expect(day.schedule.entries.map((e: { departure: string }) => e.departure)).toEqual(['07:30', '09:30', '12:00']);
expect(day.schedule.warnings).toEqual([{ index: 1, code: 'missedLeave', minutes: 30 }]);
});
it('keeps the per-leg lines the browser planner reads out of the calculate_roadtrip answer', async () => {
// The shared assembler now carries each leg's line beside its figures, for the picker of
// other ways in the browser. A tool answer carries geometry only when asked, and then
// once per day: the same road a second time, split by leg, is weight and nothing else.
const s = setup();
const settings = { roadtrip_day_start: '', roadtrip_day_end: '' };
const plan = await s.plans.calculate(10, 5, settings);
expect(plan.calculated.days[0].legLines).toHaveLength(plan.calculated.days[0].legs.length);
const mcp = new RoadtripPlanningMcp(s.plans, {} as never, {} as never);
const answer = await mcp.calculate({ tripId: 10, includeGeometry: true, settings }, ctx);
const [day] = JSON.parse(answer.content[0].text as string).days;
expect(day).toHaveProperty('geometry');
expect(day).not.toHaveProperty('legLines');
expect(day).not.toHaveProperty('arrivingLine');
});
it('keeps explicit end-day visits and manual boundaries in the shared planning path', async () => {
const s = setup();
s.visits[0].end_day = 1;