Files
TREK/shared/src/tours/run-pace.spec.ts
T
Maurice c83eb42730 feat(tours): plan a run at your own pace
RUN-01. Run joins the tour types in the planner, with Running as its first
profile, chosen on purpose like a bike tour's. A run is routed on foot for
runnable, mostly paved ways: tracks and ferries only where there is no other
way (use_tracks 0, use_ferry 0, measured against the public Valhalla), no
hiking limit and no via ferrata. The runner types a pace in min/km; the
running time is the distance at that pace and nothing else, shown under the
field as badges (8 km x 6:00 /km = 48 min). TREK suggests no pace: without
one the time stays unknown and Save waits, text that is no pace or a pace
outside 2:00 to 20:00 is marked. The info dot, the overview and the head say
climbs, descents and difficulty are not in the time, and the time reads as
planned everywhere, so it never passes for a recorded one; the heights are
shown as for any tour on land.

Type, profile, pace and route come back on opening, survive a draft reload,
an undo of the type, a copy and a take-over; switching to another type drops
profile and pace. Day assignment, the 3D terrain view (with topo and
satellite, no ferratas) and a routing failure keeping the last route work as
for every tour. A GPX file can be imported as a run with its profile and no
pace, and planned over as a run. Runs export to GPX as running and get their
own places along the way (water, toilets, parks, viewpoints).

The server keeps tours.run_profile (CHECK list, already holding trail for
RUN-02, since SQLite cannot widen a CHECK in place) and
tours.run_pace_seconds_per_km (120 to 1200), switches the run type on,
refuses a run without a profile and run fields on any other type, and
list_tours names both with what the planned time does not include.
2026-10-20 15:05:38 +02:00

27 lines
994 B
TypeScript

import {
MAX_RUN_PACE_SECONDS_PER_KM,
MIN_RUN_PACE_SECONDS_PER_KM,
runMinutesAtPace,
runPaceSecondsPerKmSchema,
} from './run-pace';
import { describe, expect, it } from 'vitest';
describe('run pace (RUN-01)', () => {
it('plans the distance at the pace: 8 km at 6:00 min/km is 48 minutes', () => {
expect(runMinutesAtPace(8, 360)).toBe(48);
expect(runMinutesAtPace(10, 330)).toBe(55);
// Rounded to the minute, never cut short: 5.03 km at 5:00 is 25 min 9 s.
expect(runMinutesAtPace(5.03, 300)).toBe(25);
expect(runMinutesAtPace(0, 360)).toBe(0);
});
it('takes whole seconds between 2:00 and 20:00 min/km only', () => {
expect([MIN_RUN_PACE_SECONDS_PER_KM, MAX_RUN_PACE_SECONDS_PER_KM]).toEqual([120, 1200]);
expect(runPaceSecondsPerKmSchema.safeParse(360).success).toBe(true);
for (const pace of [0, -1, 119, 1201, 300.5, Number.POSITIVE_INFINITY]) {
expect(runPaceSecondsPerKmSchema.safeParse(pace).success).toBe(false);
}
});
});