mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
164a3633ac
Ski becomes "Ski & winter", a type the planner plans with six disciplines (alpine, snowboard, cross-country, ski touring, snowshoeing, winter hiking), nothing preselected; saving needs a discipline, and a technique for cross-country. Every leg is drawn straight between the points through planner/skiRouting.ts, the one place the piste router will plug in, with its heights from the elevation source and no ski time worked out. The hike's winter label goes entirely: its migration, column, field, marks, GPX rule, MCP field and strings. A hike is summer only; a ski tour with the snowshoe discipline exports as snowshoeing. The unreleased ski migration takes the six disciplines and the collection migration loses the winter column. The pistes are offered for a ski tour, shown until switched off, and with no tour open, never for a hike. Export, copy, collections, stages (untimed), actual tracks, the atlas, list and day marks (one glyph per discipline), 3D, planning over a ski import and list_tours/get_tour_plan know the planned ski tour.
158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
import type { TourMaxHikingDifficulty } from '@trek/shared';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { freeLegBetween } from '../planner/tourRouteActions';
|
|
import { isTourRoute } from '../planner/tourRouteOutcome';
|
|
import { routeBikeTour, routeRun, routeWalkingTour } from '../planner/tourRouting';
|
|
import { paddleFieldsOf } from '../planner/usePaddleFields';
|
|
import type { TourPlannerController } from '../planner/useTourPlanner';
|
|
import { skiChoiceMissing, skiChoiceOf } from '../ski/skiChoice';
|
|
import { compareRoutes, deriveControlPoints, type RouteComparison, type TrackPoint } from './gpxConversion';
|
|
|
|
/** The grade a planned version routes with when the import carries none: the paths a T3 walk may take. */
|
|
const DEFAULT_DIFFICULTY: TourMaxHikingDifficulty = 3;
|
|
|
|
type GpxConversionStatus = 'idle' | 'analyzing' | 'ready' | 'failed';
|
|
|
|
interface ConversionState {
|
|
/** The import the analysis belongs to; another import shown means it is idle again. */
|
|
placeId: number | null;
|
|
status: GpxConversionStatus;
|
|
points: TrackPoint[];
|
|
line: [number, number][] | null;
|
|
comparison: RouteComparison | null;
|
|
/** Why it failed: the track is too short to plan from, or the router found no way. */
|
|
error: 'track' | 'routing' | null;
|
|
}
|
|
|
|
const IDLE: ConversionState = { placeId: null, status: 'idle', points: [], line: null, comparison: null, error: null };
|
|
|
|
/**
|
|
* Planning an editable tour from an import shown in the tour planner (PR1-05A/B).
|
|
* The analysis starts only when asked: control points from the track, a route
|
|
* through them by the planner's own router, and how far that route strays from
|
|
* the track. Nothing is stored. Adopting hands the points to the planner as a
|
|
* new draft that remembers the import; only saving that draft creates a tour,
|
|
* and the import stays as it is.
|
|
*/
|
|
export function useGpxConversion(planner: TourPlannerController) {
|
|
const [state, setState] = useState<ConversionState>(IDLE);
|
|
const abort = useRef<AbortController | null>(null);
|
|
const tour = planner.mode.type === 'view-gpx' ? (planner.readOnlyGpxTour?.tour ?? null) : null;
|
|
const track = planner.readOnlyGpxAnalysis?.routeCoordinates ?? null;
|
|
const current = tour !== null && state.placeId === tour.place_id ? state : IDLE;
|
|
const difficulty = (tour?.max_hiking_difficulty ?? DEFAULT_DIFFICULTY) as TourMaxHikingDifficulty;
|
|
// A paddle import is planned as a paddle tour is: every leg drawn by hand, no router asked.
|
|
const paddle = tour?.tour_type === 'kayak';
|
|
// BIK-01: a bike import is planned for its profile.
|
|
const bikeProfile = tour?.tour_type === 'bike' ? (tour.bike_profile ?? 'touring') : null;
|
|
// RUN-01: a run import is planned as a run for its profile, at the pace it holds; a fresh
|
|
// import holds none, so its pace is the runner's to enter.
|
|
const runProfile = tour?.tour_type === 'run' ? (tour.run_profile ?? 'running') : null;
|
|
// SKI-02: a ski import is planned as a ski tour of its discipline, its legs drawn as the planner draws them.
|
|
const ski = useMemo(() => (tour?.tour_type === 'ski' ? skiChoiceOf(tour) : null), [tour]);
|
|
const drawn = paddle || ski !== null;
|
|
|
|
useEffect(() => () => abort.current?.abort(), []);
|
|
|
|
const analyze = useCallback(async () => {
|
|
if (!tour || !track) return;
|
|
abort.current?.abort();
|
|
const controller = new AbortController();
|
|
abort.current = controller;
|
|
const trackPoints = track.map(([lat, lng]) => ({ lat, lng }));
|
|
const points = deriveControlPoints(trackPoints);
|
|
const base = { ...IDLE, placeId: tour.place_id, points };
|
|
if (points.length < 2) {
|
|
setState({ ...base, status: 'failed', error: 'track' });
|
|
return;
|
|
}
|
|
setState({ ...base, status: 'analyzing' });
|
|
const legs = drawn
|
|
? points.map((point, index) => ({
|
|
...point,
|
|
fixedLeg: index < points.length - 1 ? freeLegBetween(point, points[index + 1]) : null,
|
|
}))
|
|
: points;
|
|
const routed = bikeProfile
|
|
? await routeBikeTour(legs, bikeProfile, controller.signal)
|
|
: runProfile
|
|
? await routeRun(legs, runProfile, controller.signal)
|
|
: await routeWalkingTour(legs, controller.signal, difficulty);
|
|
if (controller.signal.aborted) return;
|
|
if (!isTourRoute(routed)) {
|
|
setState({ ...base, status: 'failed', error: 'routing' });
|
|
return;
|
|
}
|
|
const suggestion = routed.coordinates.map(([lat, lng]) => ({ lat, lng }));
|
|
setState({
|
|
...base,
|
|
status: 'ready',
|
|
line: routed.coordinates,
|
|
comparison: compareRoutes(trackPoints, suggestion),
|
|
});
|
|
}, [bikeProfile, difficulty, drawn, runProfile, tour, track]);
|
|
|
|
const cancel = useCallback(() => {
|
|
abort.current?.abort();
|
|
setState(IDLE);
|
|
}, []);
|
|
|
|
const { startDerivedTour } = planner;
|
|
/**
|
|
* The suggestion as a draft in the planner: the import planned over in place
|
|
* (`replace`), or a new tour beside it. True once the planner took it.
|
|
*/
|
|
const adopt = useCallback(
|
|
(how: 'replace' | 'new') => {
|
|
if (!tour || current.status !== 'ready') return false;
|
|
const started = startDerivedTour({
|
|
name: tour.name,
|
|
description: tour.description ?? null,
|
|
website: tour.website ?? null,
|
|
difficulty,
|
|
points: current.points,
|
|
sourcePlaceId: tour.place_id,
|
|
inPlace: how === 'replace',
|
|
revision: tour.revision,
|
|
...(paddle ? { tourType: 'kayak' as const, paddle: paddleFieldsOf(tour) } : {}),
|
|
...(bikeProfile ? { tourType: 'bike' as const, bikeProfile } : {}),
|
|
...(runProfile
|
|
? { tourType: 'run' as const, runProfile, runPaceSecondsPerKm: tour.run_pace_seconds_per_km ?? null }
|
|
: {}),
|
|
...(ski ? { tourType: 'ski' as const, ski } : {}),
|
|
});
|
|
if (started) setState(IDLE);
|
|
return started;
|
|
},
|
|
[bikeProfile, current.points, current.status, difficulty, paddle, runProfile, ski, startDerivedTour, tour]
|
|
);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
...current,
|
|
/**
|
|
* The import shown can be planned from: it has a track and the user may add tours. A ski
|
|
* track (SKI-02) once it names its discipline, planned as a ski tour with its legs drawn.
|
|
* A ride (HOR-01) never: no router plans one, and walking it again on foot would be no ride.
|
|
*/
|
|
available:
|
|
tour !== null &&
|
|
(ski === null || skiChoiceMissing(ski) === null) &&
|
|
tour.tour_type !== 'horse' &&
|
|
(track?.length ?? 0) >= 2 &&
|
|
planner.canEdit,
|
|
difficulty,
|
|
/** The import is a paddle or a ski tour: its version is drawn with straight legs, not routed. */
|
|
paddle: drawn,
|
|
/** SKI-02: the import is a ski track, planned as a ski tour of its discipline. */
|
|
ski: ski !== null,
|
|
analyze,
|
|
cancel,
|
|
adopt,
|
|
}),
|
|
[adopt, analyze, cancel, current, difficulty, drawn, planner.canEdit, ski, tour, track]
|
|
);
|
|
}
|
|
|
|
export type GpxConversion = ReturnType<typeof useGpxConversion>;
|