mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix(tours): time a run's stages at its pace and leave a ride's stages untimed
A multi-day run's stages took a share of the foot router's time while the run itself is timed at the runner's pace, so the stages did not add up to the run. A ride has no time at all, so its stages have none either.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { runMinutesAtPace } from '@trek/shared';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { routeWalkingTour, enrichTourElevations, createTour, detailTour, updateTour, paddlePlan } = vi.hoisted(() => ({
|
||||
@@ -23,7 +24,7 @@ vi.mock('../useTourPermissions', () => ({
|
||||
import { validDraft } from './tourPlannerDraft';
|
||||
import { useTourPlanner } from './useTourPlanner';
|
||||
|
||||
// FE-TOUR-STAGES-001 to FE-TOUR-STAGES-009 (MDT-01)
|
||||
// FE-TOUR-STAGES-001 to FE-TOUR-STAGES-010 (MDT-01)
|
||||
|
||||
const POINTS: Array<[number, number]> = [
|
||||
[48, 11],
|
||||
@@ -271,4 +272,19 @@ describe('useTourPlanner, multi-day tours (MDT-01)', () => {
|
||||
expect(result.current.stages.lost).toBe(0);
|
||||
expect(result.current.waypoints.find((point) => point.id === stop)?.stageEnd).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-TOUR-STAGES-010: a run times every stage at the runner pace, as the whole run is timed, never by the router (RUN-01)', async () => {
|
||||
const { result } = await plannedHike(71);
|
||||
act(() => result.current.setTourType('run'));
|
||||
await settle();
|
||||
act(() => result.current.stages.toggleStageEnd(result.current.waypoints[1].id));
|
||||
// Without a pace the run has no time, and neither have its stages.
|
||||
expect(result.current.stages.stages.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
act(() => result.current.runPace.setPace(360));
|
||||
const stages = result.current.stages.stages;
|
||||
expect(stages.every((stage) => (stage.distanceKm ?? 0) > 0)).toBe(true);
|
||||
expect(stages.map((stage) => stage.minutes)).toEqual(
|
||||
stages.map((stage) => runMinutesAtPace(stage.distanceKm ?? 0, 360))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -552,7 +552,7 @@ export function useTourPlanner({ tripId, active = true, onSaved, canEdit: editPe
|
||||
: null, [effectiveSpeedKmh, isPaddle, paddleSpeedInvalid, portageSpeedKmh, routedLegLengths])
|
||||
const stages = useTourStages({
|
||||
waypoints, line: routeCurrent ? (enrichedGeometry ?? route) : null, tourType, routerSeconds: durationSeconds,
|
||||
paddle: stagePaddle, commit: commitWaypoints, mayEdit: mayEditPoints,
|
||||
paddle: stagePaddle, runPaceSecondsPerKm, commit: commitWaypoints, mayEdit: mayEditPoints,
|
||||
draftKey: `${editingPlaceId ?? 'new'}:${newDraftGeneration}`,
|
||||
lost: stageLoss,
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ export function savedTourStagesOf(
|
||||
tourType: openedTourFields(tour).tourType,
|
||||
routerSeconds: tour.duration == null ? null : tour.duration * 60,
|
||||
paddle: line ? paddleInputsOf(tour, points, line) : null,
|
||||
runPaceSecondsPerKm: tour.run_pace_seconds_per_km ?? null,
|
||||
});
|
||||
return stages.map((stage) => ({ ...stage, key: points[stage.from]?.key ?? null }));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FE-TOUR-STAGE-MODEL-001 to FE-TOUR-STAGE-MODEL-012: the stages of a multi-day tour, cut from its one route (MDT-01, MDT-02).
|
||||
import { MAX_TOUR_STAGES } from '@trek/shared';
|
||||
// FE-TOUR-STAGE-MODEL-001 to FE-TOUR-STAGE-MODEL-013: the stages of a multi-day tour, cut from its one route (MDT-01, MDT-02).
|
||||
import { MAX_TOUR_STAGES, runMinutesAtPace } from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hikingTimeMinutes } from '../hikingTime';
|
||||
import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
@@ -32,7 +32,15 @@ const withEnds = (...ids: string[]) => FIVE.map((p) => (ids.includes(p.id) ? { .
|
||||
const LINE = FIVE.map((p, index): [number, number, number] => [p.lat, p.lng, [500, 600, 550, 650, 600][index]]);
|
||||
|
||||
function input(points: TourPlannerWaypoint[], extra: Partial<TourStageInputs> = {}): TourStageInputs {
|
||||
return { points, line: LINE, tourType: 'hike', routerSeconds: 4 * 3600, paddle: null, ...extra };
|
||||
return {
|
||||
points,
|
||||
line: LINE,
|
||||
tourType: 'hike',
|
||||
routerSeconds: 4 * 3600,
|
||||
paddle: null,
|
||||
runPaceSecondsPerKm: null,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
describe('tourStages', () => {
|
||||
@@ -122,6 +130,21 @@ describe('tourStages', () => {
|
||||
expect(unknown.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
});
|
||||
|
||||
it('FE-TOUR-STAGE-MODEL-013: a run stage is its distance at the pace, a ride on horseback has no time (RUN-01, HOR-01)', () => {
|
||||
const run = tourStagesOf(input(withEnds('b'), { tourType: 'run', runPaceSecondsPerKm: 360 }));
|
||||
expect(run.map((stage) => stage.minutes)).toEqual(
|
||||
run.map((stage) => runMinutesAtPace(stage.distanceKm ?? 0, 360))
|
||||
);
|
||||
// The router's walking time never stands in, neither without a pace nor with one out of bounds.
|
||||
const noPace = tourStagesOf(input(withEnds('b'), { tourType: 'run' }));
|
||||
expect(noPace.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
const outOfBounds = tourStagesOf(input(withEnds('b'), { tourType: 'run', runPaceSecondsPerKm: 5 }));
|
||||
expect(outOfBounds.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
const ride = tourStagesOf(input(withEnds('b'), { tourType: 'horse' }));
|
||||
expect(ride.map((stage) => stage.minutes)).toEqual([null, null]);
|
||||
expect(ride.every((stage) => stage.distanceKm !== null && !stage.overDay)).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-TOUR-STAGE-MODEL-008: a paddle stage has no heights and is timed at the paddler speed, its lock passed once', () => {
|
||||
const lock = { wait_minutes: 10, transit_minutes: 5 };
|
||||
const points = withEnds('c').map((p) => (p.id === 'c' ? { ...p, lock } : p));
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { MAX_PLANNED_TOUR_DURATION_MINUTES, MAX_TOUR_STAGES, type PlannableTourType } from '@trek/shared';
|
||||
import {
|
||||
MAX_PLANNED_TOUR_DURATION_MINUTES,
|
||||
MAX_TOUR_STAGES,
|
||||
runMinutesAtPace,
|
||||
type PlannableTourType,
|
||||
} from '@trek/shared';
|
||||
import { analyzeRouteGeometry } from '../../../utils/routeGeometry';
|
||||
import { hikingTimeMinutes } from '../hikingTime';
|
||||
import { paddleEstimateOf } from '../planner/paddlePlanOf';
|
||||
import type { TourPlannerWaypoint } from '../planner/tourPlannerWaypoints';
|
||||
import { runPaceInvalid } from '../run/runPace';
|
||||
|
||||
/**
|
||||
* MDT-01: the stages of a multi-day tour, read off its points in route order.
|
||||
@@ -130,6 +136,8 @@ export interface TourStageInputs {
|
||||
routerSeconds: number | null;
|
||||
/** A paddle tour's speeds; null for any other type. */
|
||||
paddle: PaddleStageInputs | null;
|
||||
/** A run's pace in seconds per kilometre (RUN-01); null without one, and ignored for any other type. */
|
||||
runPaceSecondsPerKm: number | null;
|
||||
}
|
||||
|
||||
const METRES_PER_DEGREE = 111_320;
|
||||
@@ -203,13 +211,21 @@ function paddleStageMinutes(
|
||||
}
|
||||
|
||||
/**
|
||||
* A stage's moving time by the tour's own rule: a paddle stage at the
|
||||
* paddler's speed, a hike by the signposts' rule over its distance, climb and
|
||||
* descent, and otherwise the router's time for the whole tour shared out by
|
||||
* distance, which is how the router times a ride or a walk on the flat.
|
||||
* A stage's moving time by the tour's own rule, the same rule the whole tour
|
||||
* is timed by: a paddle stage at the paddler's speed, a run at the runner's
|
||||
* pace over its distance, a hike by the signposts' rule over its distance,
|
||||
* climb and descent, and otherwise the router's time for the whole tour
|
||||
* shared out by distance, which is how the router times a ride or a walk on
|
||||
* the flat. A ride on horseback (HOR-01) has no time, as the whole ride has
|
||||
* none, and a run without a pace has none either.
|
||||
*/
|
||||
function stageMinutes(input: TourStageInputs, span: StageSpan, stretch: Stretch, totalKm: number): number | null {
|
||||
if (input.paddle) return paddleStageMinutes(input.points, span, input.paddle);
|
||||
if (input.tourType === 'horse') return null;
|
||||
if (input.tourType === 'run') {
|
||||
const pace = input.runPaceSecondsPerKm;
|
||||
return pace === null || runPaceInvalid(pace) ? null : runMinutesAtPace(stretch.distanceKm, pace);
|
||||
}
|
||||
if (input.tourType === 'hike' && stretch.gain !== null && stretch.loss !== null) {
|
||||
return hikingTimeMinutes(stretch.distanceKm, stretch.gain, stretch.loss);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ interface TourStagesOptions {
|
||||
tourType: PlannableTourType;
|
||||
routerSeconds: number | null;
|
||||
paddle: PaddleStageInputs | null;
|
||||
/** A run's pace, seconds per kilometre; its stages are timed at it as the whole run is. */
|
||||
runPaceSecondsPerKm: number | null;
|
||||
/** The planner's own change of the points: one step of the history, the draft unsaved again. */
|
||||
commit: (change: (current: TourPlannerWaypoint[]) => TourPlannerWaypoint[]) => void;
|
||||
/** Whether the points may change now: the viewer may edit and no save is on its way. */
|
||||
@@ -41,6 +43,7 @@ export function useTourStages({
|
||||
tourType,
|
||||
routerSeconds,
|
||||
paddle,
|
||||
runPaceSecondsPerKm,
|
||||
commit,
|
||||
mayEdit,
|
||||
draftKey,
|
||||
@@ -50,8 +53,9 @@ export function useTourStages({
|
||||
const endCount = stageEndCount(waypoints);
|
||||
const multiDay = endCount > 0 || chosenFor === draftKey;
|
||||
const stages = useMemo(
|
||||
() => (multiDay ? tourStagesOf({ points: waypoints, line, tourType, routerSeconds, paddle }) : []),
|
||||
[line, multiDay, paddle, routerSeconds, tourType, waypoints]
|
||||
() =>
|
||||
multiDay ? tourStagesOf({ points: waypoints, line, tourType, routerSeconds, paddle, runPaceSecondsPerKm }) : [],
|
||||
[line, multiDay, paddle, routerSeconds, runPaceSecondsPerKm, tourType, waypoints]
|
||||
);
|
||||
|
||||
const setMultiDay = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user