Files
TREK/client/src/components/PDF/tripPdfRoute.ts
T
Maurice 3238fbc517 fix(pdf): route every leg of the trip map and mark what is left unrouted
The PDF export routed the whole trip under an 8 s deadline while the router
is asked one leg at a time, a second apart. A cold month-long trip got about
seven legs routed per export; the rest printed as straight lines at "0 m"
under a total that looked final, and every further export routed a few more
days from the device cache.

The export now waits for every leg, bounded by the request pacing, the
rate-limit retries and a per-leg timeout. The export dialog and the phone
sheet count the legs while they route and offer to print without the rest.
Any leg left without a road is marked in the document: the day reads "not
routed" or "at least ...", the total says it is a floor, and a note names
how many legs are missing. The planner overview shares the same wording.

The PDF also routes what the planner map routes now: the road trip's vias,
the multi-day tour stages and the reader's distance unit.

Closes #2876
2026-10-29 14:48:13 +01:00

73 lines
3.5 KiB
TypeScript

import { useSettingsStore } from '../../store/settingsStore';
import type { DistanceUnit } from '../../types';
import type { ViasByDay } from '../../utils/dayRouteVias';
import { formatDistance } from '../../utils/units';
import type { TourStageStops } from '../Map/dayTourProjection';
import {
routeTrip,
type TripRouteInputs,
type TripRouteProgress,
type TripRouteSummary,
} from '../Map/tripRouteGeometry';
import { renderTripMapImage } from './tripMapImage';
import { buildTripMapSvg } from './tripMapSvg';
/** What the planner knows about the trip's route beyond the plan, and how it follows the routing. */
export interface TripPdfRouteOptions {
/** The roads picked in the road trip (#2580), so the print bends where the planner map does. */
viasByDay?: ViasByDay;
/** Where the stages of the trip's multi-day tours run. */
tourStageStops?: TourStageStops;
/** Stops routing early: the legs asked by then are printed, the rest stay straight and are marked. */
signal?: AbortSignal;
onProgress?: TripRouteProgress;
}
interface TripPdfRoute {
route: TripRouteSummary | null;
mapSvg: string | null;
}
const MAP_FRAME = { width: 720, height: 420 };
/**
* The trip's route as one map (#1736), from the builder the planner overview uses and
* fed what the overview is fed, so the document and the screen show the same roads.
*
* Every leg is waited for: a deadline here printed a month-long trip with most of its
* days as straight lines and a total that looked final (#2876). Fail-safe all the same:
* a routing failure means no map rather than no PDF.
*/
export async function drawTripRoute(
input: Omit<TripRouteInputs, 'viasByDay' | 'tourStageStops'>,
{ tripId, unit, options = {} }: { tripId: number; unit: DistanceUnit; options?: TripPdfRouteOptions }
): Promise<TripPdfRoute> {
const route = await routeTrip(
{ ...input, viasByDay: options.viasByDay, tourStageStops: options.tourStageStops },
{ profile: 'driving', tripId, unit, signal: options.signal, onProgress: options.onProgress }
).catch((err: unknown) => {
// Logged rather than swallowed: a map that is silently absent looks exactly like a
// trip that has no route, and the two need different answers from whoever is
// looking. The export itself carries on — the itinerary matters more than the map.
console.warn('[tripPdfMap] routing the trip failed; the export continues without a map', err);
return null;
});
if (!route) return { route: null, mapSvg: null };
// A real basemap first — at city scale the bundled outlines are a country-sized
// blank, and only streets carry context that small. The outline map is what is left
// when there is no WebGL, no network, or a style that will not load.
const frame = { ...MAP_FRAME, formatDistance: (km: number) => formatDistance(km, unit) };
const style = useSettingsStore.getState().settings.maplibre_style;
const mapSvg = (await renderTripMapImage(route.days, { ...frame, style })) ?? buildTripMapSvg(route.days, frame);
// The other way to end up mapless, and the one that is not a failure: nothing in the
// trip routed. Only worth saying when there were stops to route — a trip nobody has
// planned yet is meant to print without a map.
if (!mapSvg && Object.values(input.assignments).some((list) => list?.length)) {
console.warn(
`[tripPdfMap] no map drawn: ${route.days.length} of ${input.days.length} day(s) produced a route.` +
' A day needs two located stops, or one located stop with an accommodation either side of it.'
);
}
return { route, mapSvg };
}