Files
TREK/shared/src/roadtrip/routeRun.ts
T
Maurice 83d088e842 fix(roadtrip): a merged drive keeps its first piece ends when a leg comes without them
A leg a routing provider answers without its from and to points threw in
the merge, and the day it belonged to never finished routing. Such a leg now
keeps the first piece's ends; the texts are still written from the sums.
2026-09-23 12:53:24 +02:00

88 lines
3.7 KiB
TypeScript

import { lineMetres, sliceAtMeters, projectOntoRoute } from './corridor';
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);
// A leg a provider handed over without its ends keeps the first piece's; the texts are
// what the reader was shown wrong, the ends only place the label.
const ends = first.from && last.to ? { from: first.from, to: last.to } : null;
return {
...first,
...(ends
? {
to: ends.to,
mid: [(ends.from[0] + ends.to[0]) / 2, (ends.from[1] + ends.to[1]) / 2] as [number, number],
}
: {}),
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 }[] = [];
let travelled = 0;
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 = mergeRouteSegments(parts, unit);
const from = travelled;
travelled += merged.distance;
cuts.push({ key: roadtripLegKey(stops[index]!, stops[index + 1]!), from, to: travelled, seg: { ...merged, mode } });
}
const scale = travelled > 0 ? lineMetres(spine) / travelled : 0;
const legs: Record<string, RoutedLeg> = {};
for (const cut of cuts)
legs[cut.key] = {
seg: cut.seg,
line: sliceAtMeters(spine, cut.from * scale, cut.to * scale).map((p) => [p.lat, p.lng]),
vias: [],
};
for (const via of route.vias ?? []) {
if (!via.label && via.dwellSeconds == null) continue;
const along = (projectOntoRoute(via, spine)?.alongKm ?? 0) * 1000;
const index = legIndexForAlong(
cuts.map((cut) => cut.to),
along,
);
const cut = cuts[index];
if (cut) legs[cut.key]!.vias.push(via);
}
return legs;
}