fix(roadtrip): file a via on the drive between connected days after the stop it leaves

With connected days the drive from one day's last stop to the next day's
first is drawn at the head of the arriving card, in the colour of the day
before. anchorFor only knew that stretch as a night spill, so a click on it
anchored to the arriving card's first stop: the via was stored on the leg
after that stop, and the next day drove out to the point, turned and came
back, while the stretch that was clicked kept its shape.

assembleRoadtrip now names the stop that joined drive leaves from, and
anchorFor treats it like the stop a night drive left from. Placing and
dragging both resolve to the previous day's last stop, which is the anchor
the seam request already reads its vias from.
This commit is contained in:
Maurice
2026-09-14 22:36:29 +02:00
parent 1b49995c15
commit 2ffe4d6054
5 changed files with 131 additions and 9 deletions
+62 -1
View File
@@ -1,5 +1,6 @@
/**
* ROADTRIP-ASSEMBLE-001..003 — what the assembler does with the model's warnings.
* ROADTRIP-ASSEMBLE-001..005: what the assembler does with the model's warnings, and
* what it tells a surface about the drive between connected days.
*
* The model itself is pinned by roadtripModel.spec.ts. This file pins the
* wrapper, which is where the arguments are chosen: `deriveDriveWarnings` takes
@@ -102,3 +103,63 @@ describe('assembleRoadtrip drive warnings', () => {
expect(rangeWarnings(assemble(stops, [700, 500]))).toEqual([]);
});
});
describe('assembleRoadtrip connected days', () => {
const lineFrom = (from: RoadtripStop, to: RoadtripStop): RoutedLeg => ({
...leg(100),
line: [
[from.lat, from.lng],
[to.lat, to.lng],
],
});
function assembleTwoDays(connectDays: boolean) {
const first = [stop({ ownerIndex: 0 }), stop({ ownerIndex: 1 }), stop({ ownerIndex: 2 })];
const second = [
stop({ ownerDayId: 2, ownerIndex: 0, assignmentId: 300, placeId: 400, lat: 60 }),
stop({ ownerDayId: 2, ownerIndex: 1, assignmentId: 301, placeId: 401, lat: 61 }),
];
const allLegs: Record<string, RoutedLeg> = {};
for (const run of [first, second, [first[2]!, second[0]!]]) {
run.slice(0, -1).forEach((from, i) => {
allLegs[legKey(from, run[i + 1]!)] = lineFrom(from, run[i + 1]!);
});
}
return assembleRoadtrip({
plan: [
{ dayId: 1, dayNumber: 1, date: '2026-06-01', title: null, stops: first },
{ dayId: 2, dayNumber: 2, date: '2026-06-02', title: null, stops: second },
],
quietDays: [],
window: null,
distanceUnit: 'metric',
allLegs,
snapByDay: {},
missedByDay: {},
loading: false,
limits: { rangeKm: null, legMinutes: null, dayMinutes: null },
vehicleKind: null,
connectDays,
boundaries: [],
labels: { start: 'start', end: 'end' },
});
}
it('ROADTRIP-ASSEMBLE-004: the drive drawn at the head of a connected day names the stop it left from', () => {
const routes = assembleTwoDays(true);
const [first, second] = routes.days;
// Yesterday's last stop, with the numbers it is stored under. A via placed on that
// stretch has to be filed after it, and the card's own stops cannot say which it is.
expect(second!.arrivingFrom).toEqual(expect.objectContaining({ ownerDayId: 1, ownerIndex: 2 }));
expect(second!.geometry[0]).toEqual([first!.stops[2]!.lat, first!.stops[2]!.lng]);
expect(first!.arrivingFrom).toBeUndefined();
});
it('ROADTRIP-ASSEMBLE-005: days that are not connected draw no such drive and name no such stop', () => {
const routes = assembleTwoDays(false);
expect(routes.days[1]!.arrivingFrom).toBeUndefined();
expect(routes.days[1]!.geometry[0]).toEqual([60, 10]);
});
});
+3
View File
@@ -113,6 +113,7 @@ export function assembleRoadtrip({
const inboundAt = new Map<number, { seg: RouteSegment | undefined; line: [number, number][]; drawnAs: number }>();
let arrivingLeg: RouteSegment | undefined;
let arrivingFrom: RoadtripStop | undefined;
if (connectDays && !automaticSchedule) {
for (const spill of chain.spills) {
inboundAt.set(spill.at, { seg: spill.leg, line: spill.line, drawnAs: spill.fromDayNumber });
@@ -122,6 +123,7 @@ export function assembleRoadtrip({
if (joined) {
inboundAt.set(0, { seg: joined.seg, line: joined.line, drawnAs: previousDayNumber ?? chain.dayNumber });
arrivingLeg = joined.seg;
arrivingFrom = previousStop;
}
}
previousStop = chain.stops[chain.stops.length - 1]! ?? previousStop;
@@ -246,6 +248,7 @@ export function assembleRoadtrip({
// Only where no stop actually crossed over: a crossing already draws its own band,
// with this same road under it, and a second one would be the drive twice.
arrivingLeg: chain.spills.length ? undefined : arrivingLeg,
arrivingFrom,
stops,
legs,
legVias,
+13
View File
@@ -111,6 +111,19 @@ export interface RoadtripDay {
*/
arrivingLeg?: RouteSegment;
/**
* The stop the drive at the head of this card's `geometry` sets off from, when the
* days are connected.
*
* Kept even where `arrivingLeg` is withheld for a crossing further down the card: the
* band goes, the road stays drawn. A point placed on that stretch belongs after THIS
* stop, on the day it is stored on, and nothing else on the card says which stop that
* is. Measured against the card's own first stop instead, a via dropped there was
* filed on the leg after it, and the drive ran on, turned back to the point and came
* the same way again.
*/
arrivingFrom?: RoadtripStop;
schedule: Schedule;
legVias: RouteVia[][];