From 911f3e25834753b2501b7a5be808387995cd4172 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 17 Sep 2026 13:26:10 +0200 Subject: [PATCH] fix(roadtrip): the phone chain drops the placeholder leg into an automatic night MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dayWindow.stationary()` puts a leg between the last stop of a day and the automatic night that closes it: same coordinate at both ends, nothing measured, no texts. It is bookkeeping, not a drive. Drawn as a pill under the last stop it said "No route" — and before the template was guarded, a bare " in " — under a place the traveller had simply arrived at. Matched on the shape rather than on the next stop being a night, because what makes it not a drive is that it goes nowhere: a day ended at a boundary dragged down the road HAS a leg with a distance, and that one keeps its pill. A leg nothing has routed yet keeps its row too, so it can still say it is pending. --- .../Roadtrip/roadtripRowModel.test.ts | 31 +++++++++++++++++++ .../components/Roadtrip/roadtripRowModel.ts | 22 ++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/client/src/components/Roadtrip/roadtripRowModel.test.ts b/client/src/components/Roadtrip/roadtripRowModel.test.ts index 3bee909ff..c254ccfcd 100644 --- a/client/src/components/Roadtrip/roadtripRowModel.test.ts +++ b/client/src/components/Roadtrip/roadtripRowModel.test.ts @@ -98,6 +98,37 @@ describe('roadtripRows numbering', () => { expect(stopRows(rows).map((r) => r.stop.name)).not.toContain('Nacht') }) + it('FE-RTROW-040: the placeholder leg into an automatic night is not a row at all', () => { + // `dayWindow.stationary()` puts one of these between the last stop of a day and the + // night that closes it: same coordinate at both ends, nothing measured. It is + // bookkeeping, not a drive, and as a pill it read "No route" under a stop the + // traveller had simply arrived at (and, before that was guarded, a bare " in "). + const stops = [stop('Hamburg'), night()] + const at: [number, number] = [53.54, 10.01] + const stationary: RouteSegment = { + mid: at, from: at, to: at, distance: 0, duration: 0, + walkingText: '', drivingText: '', distanceText: '', + } + const rows = roadtripRows(day(stops, { legs: [stationary] })) + + expect(rows.map((r) => r.kind)).toEqual(['stop', 'auto']) + }) + + it('FE-RTROW-041: a night the traveller drove to keeps its leg', () => { + // A boundary dragged down the road ends the day somewhere else, and that IS a drive. + const stops = [stop('Hamburg'), night()] + const rows = roadtripRows(day(stops, { legs: [seg(0)] })) + + expect(rows.map((r) => r.kind)).toEqual(['stop', 'leg', 'auto']) + }) + + it('FE-RTROW-042: a leg nothing has routed yet still gets its row, so it can say it is pending', () => { + const stops = [stop('Hamburg'), stop('Bremen')] + const rows = roadtripRows(day(stops, { legs: [undefined] })) + + expect(rows.map((r) => r.kind)).toEqual(['stop', 'leg', 'stop']) + }) + it('FE-RTROW-003: the automatic night carries no dwell and takes its time from the schedule', () => { // Die Nacht traegt dwellMinutes 480, aber keine Zeile, die eine Aufenthaltsdauer zeigen koennte. const rows = roadtripRows( diff --git a/client/src/components/Roadtrip/roadtripRowModel.ts b/client/src/components/Roadtrip/roadtripRowModel.ts index 6c6404351..4a51c8cc3 100644 --- a/client/src/components/Roadtrip/roadtripRowModel.ts +++ b/client/src/components/Roadtrip/roadtripRowModel.ts @@ -109,6 +109,26 @@ function stopRow( * card it is drawn on. The rows follow the card, because that is what the traveller * reads, and anything writing back has to use `ownerDayId` instead. */ +/** + * Whether a leg is the model's placeholder for "the day ended where you were standing". + * + * `dayWindow.stationary()` puts one of these between the last stop of a day and the + * automatic night that closes it: same coordinate at both ends, nothing measured, no + * texts. It is bookkeeping, not a drive, and drawn as a pill it said either " in " (the + * separator of `roadtrip.leg.driveText` with both slots empty) or, once that was guarded, + * "No route" under a stop the traveller had simply arrived at. + * + * Matched on the shape rather than on the next stop being an automatic night, because + * what makes it not a drive is that it goes nowhere. A night the traveller moved to a + * point down the road HAS a leg, with a distance, and that one keeps its pill. + */ +function isStationary(seg: RouteSegment | undefined): boolean { + return !!seg + && seg.distance === 0 + && seg.from[0] === seg.to[0] + && seg.from[1] === seg.to[1] +} + export function roadtripRows(day: RoadtripDay): RoadtripRow[] { const rows: RoadtripRow[] = [] const spills = day.spills ?? [] @@ -140,7 +160,7 @@ export function roadtripRows(day: RoadtripDay): RoadtripRow[] { // The leg AFTER this stop, plus the dry point that falls on it. Both belong // between two stops, so they are emitted here rather than in their own pass. const seg = day.legs[i] - if (i < day.stops.length - 1) { + if (i < day.stops.length - 1 && !isStationary(seg)) { rows.push({ kind: 'leg', index: i, seg, mode: day.stops[i + 1]?.incomingLegMode ?? stop.legMode ?? null }) const dry = (day.dryPoints ?? []).find(p => p.legIndex === i) if (dry) {