fix(roadtrip): a drive bent through a via prints its whole length, and a refusal is said once

A via cuts a drive into one routed leg per waypoint pair. Folding them back
onto the stop pair summed the metres and seconds but kept the first piece's
printed texts and end point, and the phone reads the texts first: a drive
into the next day of 3.9 mi in 14 min showed 1.9 mi in 6 min. Every choice
under "Other ways" writes vias, so every choice ran into it. The pieces are
now merged by one shared helper that writes the texts from the sums in the
reader's unit, for a day's own run, for the drive between two days and in
the server's plan; the drive between two days is asked for again when the
unit changes, as a day's run already is.

On the desk a refused way was said in the bar's status line and again in a
toast laid over that line. The desk keeps the status line; the phone, whose
bar has no room for the sentence, keeps the toast.
This commit is contained in:
Maurice
2026-09-23 12:03:00 +02:00
parent 451e6d240b
commit 34c1dc79b5
6 changed files with 161 additions and 39 deletions
+74
View File
@@ -0,0 +1,74 @@
/**
* ROADTRIP-ROUTERUN-001..003: a drive the router answered in pieces reads as one drive.
*
* A via cuts a stop-to-stop drive into one leg per waypoint pair. The numbers were summed
* back together, but the printed texts stayed those of the first piece, and the phone reads
* a segment's text before its numbers: a 3.9 mi drive into the next day said "1.9 mi in
* 6 min" (#2461).
*/
import type { RoadtripStop, RouteSegment } from './planning-types';
import { formatDurationShort } from './roadtripModel';
import { foldRouteRun, mergeRouteSegments, roadtripLegKey } from './routeRun';
import { formatDistance } from './units';
import { describe, expect, it } from 'vitest';
const piece = (from: [number, number], to: [number, number], distance: number, duration: number): RouteSegment => ({
mid: [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2],
from,
to,
distance,
duration,
distanceText: formatDistance(distance / 1000, 'imperial'),
durationText: formatDurationShort(duration),
drivingText: formatDurationShort(duration),
walkingText: '',
});
const A: [number, number] = [52.37, 4.89];
const VIA: [number, number] = [52.36, 4.9];
const B: [number, number] = [52.35, 4.87];
describe('mergeRouteSegments', () => {
it('ROADTRIP-ROUTERUN-001: the pieces of one drive add up, and their texts are written from the sums', () => {
const first = piece(A, VIA, 2996, 417);
const second = piece(VIA, B, 3280, 423);
const merged = mergeRouteSegments([first, second], 'imperial');
expect(merged.distance).toBe(6276);
expect(merged.duration).toBe(840);
expect(merged.distanceText).toBe(formatDistance(6.276, 'imperial'));
expect(merged.distanceText).not.toBe(first.distanceText);
expect(merged.durationText).toBe('14 min');
expect(merged.drivingText).toBe('14 min');
// The drive ends where its last piece does, not at the via.
expect(merged.from).toEqual(A);
expect(merged.to).toEqual(B);
});
it('ROADTRIP-ROUTERUN-002: a drive the router answered in one piece is handed on as it came', () => {
const only = piece(A, B, 3000, 600);
expect(mergeRouteSegments([only], 'metric')).toBe(only);
});
});
describe('foldRouteRun', () => {
it('ROADTRIP-ROUTERUN-003: a stop pair bent through a via gets the whole drive and its texts, in the reader unit', () => {
const stops = [
{ lat: A[0], lng: A[1] },
{ lat: B[0], lng: B[1] },
] as unknown as RoadtripStop[];
const legs = [piece(A, VIA, 2996, 417), piece(VIA, B, 3280, 423)];
const folded = foldRouteRun(stops, [0, 2], { coordinates: [A, VIA, B], legs }, 'driving', 'metric');
const seg = folded[roadtripLegKey(stops[0]!, stops[1]!)]!.seg;
expect(seg.distance).toBe(6276);
expect(seg.duration).toBe(840);
expect(seg.distanceText).toBe(formatDistance(6.276, 'metric'));
expect(seg.durationText).toBe('14 min');
expect(seg.mode).toBe('driving');
});
});
+37 -10
View File
@@ -1,17 +1,51 @@
import { lineMetres, sliceAtMeters, projectOntoRoute } from './corridor';
import type { RoadtripStop, RouteSegment, RouteVia, RoutedLeg } from './planning-types';
import { legIndexForAlong } from './roadtripModel';
import type { DistanceUnit, RoadtripStop, RouteSegment, RouteVia, RoutedLeg } from './planning-types';
import { formatDurationShort, legIndexForAlong } from './roadtripModel';
import { formatDistance } from './units';
export const roadtripStopKey = (stop: RoadtripStop): string =>
`${stop.lat.toFixed(5)},${stop.lng.toFixed(5)},${stop.legMode ?? ''},${stop.incomingLegMode ?? ''}`;
export const roadtripLegKey = (from: RoadtripStop, to: RoadtripStop): string =>
`${roadtripStopKey(from)}>${roadtripStopKey(to)}`;
/** The walking pace a segment's walking time is printed at, in metres per second. */
const WALKING_METRES_PER_SECOND = 5000 / 3600;
/**
* The pieces a via cuts one stop-to-stop drive into, read as that one drive again.
*
* The router answers one leg per pair of waypoints, so a drive bent through a via comes
* back in pieces. The numbers add up. The printed texts do not: carried over from the first
* piece, they had the phone print "1.9 mi in 6 min" under a 3.9 mi drive, because it reads
* a segment's text before its numbers. So the texts are written again from the sums, in the
* reader's unit, and the drive ends where its last piece does rather than at the first via.
*/
export function mergeRouteSegments(parts: RouteSegment[], unit: DistanceUnit): RouteSegment {
const first = parts[0]!;
if (parts.length === 1) return first;
const last = parts[parts.length - 1]!;
const distance = parts.reduce((sum, leg) => sum + (leg.distance ?? 0), 0);
const duration = parts.reduce((sum, leg) => sum + (leg.duration ?? 0), 0);
const durationText = formatDurationShort(duration);
return {
...first,
to: last.to,
mid: [(first.from[0] + last.to[0]) / 2, (first.from[1] + last.to[1]) / 2],
distance,
duration,
distanceText: formatDistance(distance / 1000, unit),
durationText,
drivingText: durationText,
walkingText: formatDurationShort(distance / WALKING_METRES_PER_SECOND),
};
}
export function foldRouteRun(
stops: RoadtripStop[],
stopAt: number[],
route: { coordinates: [number, number][]; legs: RouteSegment[]; vias?: RouteVia[] },
mode: string,
unit: DistanceUnit,
): Record<string, RoutedLeg> {
const spine = route.coordinates.map(([lat, lng]) => ({ lat, lng }));
const cuts: { key: string; from: number; to: number; seg: RouteSegment }[] = [];
@@ -19,14 +53,7 @@ export function foldRouteRun(
for (let index = 0; index < stops.length - 1; index++) {
const parts = route.legs.slice(stopAt[index], stopAt[index + 1]);
if (!parts.length) continue;
const merged =
parts.length === 1
? parts[0]!
: {
...parts[0]!,
distance: parts.reduce((sum, leg) => sum + leg.distance, 0),
duration: parts.reduce((sum, leg) => sum + leg.duration, 0),
};
const merged = mergeRouteSegments(parts, unit);
const from = travelled;
travelled += merged.distance;
cuts.push({ key: roadtripLegKey(stops[index]!, stops[index + 1]!), from, to: travelled, seg: { ...merged, mode } });