mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
ae699187f7
A saved ski tour now opens with the line, heights, time, build and planned legs it was saved with, read from the place this device keeps, and nothing is asked of the piste router until a point, an option or the discipline changes. Opened offline, while TREK Places is down or with piste routing switched off, it no longer turns every leg straight, and a rename saves the planned tour as it was. A kept line whose length is not the tour's is another save's, and the tour is planned afresh. - the save names the legs the router planned (ski_planned_legs, checked against the tour's own legs); get_tour_plan reports each leg routed or drawn from them instead of from the build alone, a tour that names none counting as planned only with the router's time - copies keep ski_options in every path: in the trip, taken over, kept in a collection and made from one (collection_tours gets the column in the same unshipped migration) - the route summary shows the data build, the km of runs, the height skied down on them and the lift rides, and the credit the server sends in place of a fixed one; a tour opened as saved says so - answers refused while piste routing is off, or planned on an older kept copy, are not remembered, so they are asked again later - terrain heights between known ones are filled in one walk along the line - i18n: five summary keys in all 27 locales
182 lines
8.2 KiB
TypeScript
182 lines
8.2 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* SKI-02: a ski and winter tour, planned in the planner or imported from a
|
|
* GPX file. A planned one has its legs planned by the piste router where the
|
|
* piste data reaches (runs, lifts, cross-country trails, ski touring and
|
|
* winter paths from OpenStreetMap), and drawn straight where it does not; an
|
|
* imported one is kept as it was recorded. A ski time is worked out only for
|
|
* legs the router planned, and nothing is said about snow, avalanches or
|
|
* whether a run or a trail is open. What the traveller names on purpose is
|
|
* the discipline: alpine, snowboard, cross-country, ski touring, snowshoeing
|
|
* or winter hiking (both of which live here and not on a hike), and for
|
|
* cross-country the technique.
|
|
*/
|
|
export const SKI_DISCIPLINES = ['alpine', 'snowboard', 'cross_country', 'touring', 'snowshoe', 'winter_hike'] as const;
|
|
export const skiDisciplineSchema = z.enum(SKI_DISCIPLINES);
|
|
export type SkiDiscipline = z.infer<typeof skiDisciplineSchema>;
|
|
|
|
/**
|
|
* How a cross-country track is skied. `unknown` is an answer the skier gives
|
|
* on purpose (a borrowed track, a mixed day), not a gap TREK fills in; every
|
|
* other discipline has no technique at all.
|
|
*/
|
|
export const SKI_TECHNIQUES = ['classic', 'skating', 'unknown'] as const;
|
|
export const skiTechniqueSchema = z.enum(SKI_TECHNIQUES);
|
|
export type SkiTechnique = z.infer<typeof skiTechniqueSchema>;
|
|
|
|
type SkiIssue = { path: string[]; message: string };
|
|
|
|
/**
|
|
* What a ski tour names, and what nothing else may: a ski tour its
|
|
* discipline, a cross-country tour its technique as well; a technique on any
|
|
* other discipline, and either field on another type, is refused. Planned and
|
|
* imported ski tours are held to the same rule.
|
|
*/
|
|
export function skiIssues(
|
|
isSki: boolean,
|
|
value: { ski_discipline?: SkiDiscipline | null; ski_technique?: SkiTechnique | null },
|
|
): SkiIssue[] {
|
|
if (!isSki) {
|
|
return (['ski_discipline', 'ski_technique'] as const)
|
|
.filter((field) => value[field] != null)
|
|
.map((field) => ({ path: [field], message: `${field} belongs to a ski tour` }));
|
|
}
|
|
if (value.ski_discipline == null) return [{ path: ['ski_discipline'], message: 'a ski tour needs a discipline' }];
|
|
const crossCountry = value.ski_discipline === 'cross_country';
|
|
if (crossCountry && value.ski_technique == null)
|
|
return [{ path: ['ski_technique'], message: 'a cross-country tour needs a technique' }];
|
|
if (!crossCountry && value.ski_technique != null)
|
|
return [{ path: ['ski_technique'], message: 'only a cross-country tour has a technique' }];
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* A ski tour classified again after its import: the discipline and the
|
|
* technique, nothing of the track. Refused with a 409 once the tour moved on
|
|
* past the revision the choice was made at.
|
|
*/
|
|
export const tourSkiClassificationRequestSchema = z
|
|
.object({
|
|
ski_discipline: skiDisciplineSchema,
|
|
ski_technique: skiTechniqueSchema.nullable().optional(),
|
|
/** P2C-03: the revision the classification was made at. */
|
|
expected_revision: z.number().int().min(1).nullable().optional(),
|
|
})
|
|
.superRefine((value, ctx) =>
|
|
skiIssues(true, value).forEach((issue) => ctx.addIssue({ code: z.ZodIssueCode.custom, ...issue })),
|
|
);
|
|
export type TourSkiClassificationRequest = z.infer<typeof tourSkiClassificationRequestSchema>;
|
|
|
|
/** The run difficulties a skier may set as a limit, easiest first (freeride and extreme are a switch of their own). */
|
|
export const SKI_DIFFICULTIES = ['novice', 'easy', 'intermediate', 'advanced', 'expert'] as const;
|
|
export const skiDifficultySchema = z.enum(SKI_DIFFICULTIES);
|
|
export type SkiDifficulty = z.infer<typeof skiDifficultySchema>;
|
|
|
|
/** The kinds of lift a skier may leave out: surface lifts, open chairs, gondolas, cable cars and rail. */
|
|
export const SKI_LIFT_CLASSES = ['drag', 'open_chair', 'gondola', 'cable_car', 'rail'] as const;
|
|
export const skiLiftClassSchema = z.enum(SKI_LIFT_CLASSES);
|
|
export type SkiLiftClass = z.infer<typeof skiLiftClassSchema>;
|
|
|
|
/** What the router weighs most: the quickest way, the most skiing, or the easiest runs. */
|
|
export const SKI_ROUTE_PRIORITIES = ['fastest', 'most_skiing', 'easiest'] as const;
|
|
export type SkiRoutePriority = (typeof SKI_ROUTE_PRIORITIES)[number];
|
|
|
|
/** How fast the skier goes down a run, against an average skier. */
|
|
export const SKI_PACES = ['relaxed', 'normal', 'sporty'] as const;
|
|
export type SkiPace = (typeof SKI_PACES)[number];
|
|
|
|
/**
|
|
* What the piste router is asked for besides the discipline. Every field has
|
|
* its default, so an empty object is a complete answer. `use_lifts` left out
|
|
* means the discipline's own default: lifts for alpine and snowboard, none for
|
|
* the others.
|
|
*/
|
|
export const skiRouteOptionsSchema = z
|
|
.object({
|
|
max_difficulty: skiDifficultySchema.default('intermediate'),
|
|
allow_freeride: z.boolean().default(false),
|
|
avoid_lifts: z.array(skiLiftClassSchema).max(SKI_LIFT_CLASSES.length).default([]),
|
|
allow_lift_down: z.boolean().default(true),
|
|
priority: z.enum(SKI_ROUTE_PRIORITIES).default('fastest'),
|
|
pace: z.enum(SKI_PACES).default('normal'),
|
|
groomed_only: z.boolean().default(false),
|
|
lit_only: z.boolean().default(false),
|
|
touring_uphill_on_pistes: z.boolean().default(false),
|
|
use_lifts: z.boolean().optional(),
|
|
avoid_inferred_links: z.boolean().default(false),
|
|
})
|
|
.strict();
|
|
export type SkiRouteOptions = z.infer<typeof skiRouteOptionsSchema>;
|
|
export type SkiRouteOptionsInput = z.input<typeof skiRouteOptionsSchema>;
|
|
|
|
export const DEFAULT_SKI_ROUTE_OPTIONS: SkiRouteOptions = skiRouteOptionsSchema.parse({});
|
|
|
|
/** Disciplines that ride lifts unless the skier says otherwise. */
|
|
const LIFTS_BY_DEFAULT: ReadonlySet<SkiDiscipline> = new Set(['alpine', 'snowboard']);
|
|
|
|
/** Whether a discipline rides lifts with these options: the skier's word, else the discipline's default. */
|
|
export function skiUsesLifts(discipline: SkiDiscipline, options: Pick<SkiRouteOptions, 'use_lifts'>): boolean {
|
|
return options.use_lifts ?? LIFTS_BY_DEFAULT.has(discipline);
|
|
}
|
|
|
|
/** The options a downhill skier reads, on skis or a board. */
|
|
const DOWNHILL_OPTIONS: ReadonlyArray<keyof SkiRouteOptions> = [
|
|
'max_difficulty',
|
|
'allow_freeride',
|
|
'avoid_lifts',
|
|
'allow_lift_down',
|
|
'priority',
|
|
'pace',
|
|
'use_lifts',
|
|
'avoid_inferred_links',
|
|
];
|
|
|
|
/** The options each discipline reads; the rest are put back to their defaults. */
|
|
const OPTIONS_BY_DISCIPLINE: Record<SkiDiscipline, ReadonlyArray<keyof SkiRouteOptions>> = {
|
|
alpine: DOWNHILL_OPTIONS,
|
|
snowboard: DOWNHILL_OPTIONS,
|
|
cross_country: ['groomed_only', 'lit_only', 'use_lifts', 'avoid_inferred_links'],
|
|
touring: [
|
|
'max_difficulty',
|
|
'allow_freeride',
|
|
'avoid_lifts',
|
|
'allow_lift_down',
|
|
'touring_uphill_on_pistes',
|
|
'use_lifts',
|
|
'avoid_inferred_links',
|
|
],
|
|
snowshoe: ['allow_lift_down', 'use_lifts', 'avoid_inferred_links'],
|
|
winter_hike: ['allow_lift_down', 'use_lifts', 'avoid_inferred_links'],
|
|
};
|
|
|
|
/** The options a discipline reads, the ones the planner offers for it. */
|
|
export function skiOptionsReadBy(discipline: SkiDiscipline): ReadonlyArray<keyof SkiRouteOptions> {
|
|
return OPTIONS_BY_DISCIPLINE[discipline];
|
|
}
|
|
|
|
/**
|
|
* The options as a discipline reads them: whatever belongs to another
|
|
* discipline goes back to its default rather than being refused, so switching
|
|
* the discipline keeps a request that is valid as it is.
|
|
*/
|
|
export function skiOptionsForDiscipline(discipline: SkiDiscipline, options: SkiRouteOptionsInput): SkiRouteOptions {
|
|
const parsed = skiRouteOptionsSchema.parse(options);
|
|
const kept = new Set(OPTIONS_BY_DISCIPLINE[discipline]);
|
|
const result: Record<string, unknown> = { ...DEFAULT_SKI_ROUTE_OPTIONS };
|
|
for (const key of Object.keys(parsed) as Array<keyof SkiRouteOptions>) {
|
|
if (kept.has(key)) result[key] = parsed[key];
|
|
}
|
|
if (!kept.has('use_lifts') || parsed.use_lifts === undefined) delete result.use_lifts;
|
|
return result as SkiRouteOptions;
|
|
}
|
|
|
|
/**
|
|
* The legs of a ski tour the piste router planned, each named by the index of
|
|
* the control point it starts at; the others were drawn straight or by hand.
|
|
*/
|
|
export const skiPlannedLegsSchema = z.array(z.number().int().nonnegative()).max(5000);
|
|
|
|
/** The name of a piste data build a ski tour's legs were planned on, as the Places host names it. */
|
|
export const skiRouteBuildSchema = z.string().trim().min(1).max(40).regex(/^[0-9A-Za-z._-]+$/);
|