mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
feat(tours): plan ski and winter tours, snowshoeing and winter hiking among them
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.
This commit is contained in:
@@ -28,7 +28,6 @@ export class CollectionTours {
|
||||
match_confidence?: number | null;
|
||||
max_hiking_difficulty?: number | null;
|
||||
bike_profile?: string | null;
|
||||
winter_activity?: string | null;
|
||||
run_profile?: string | null;
|
||||
run_pace_seconds_per_km?: number | null;
|
||||
ski_discipline?: string | null;
|
||||
@@ -76,7 +75,6 @@ export const CollectionToursSchema = defineEntity({
|
||||
match_confidence: p.double().nullable(),
|
||||
max_hiking_difficulty: p.integer().nullable(),
|
||||
bike_profile: p.text().nullable(),
|
||||
winter_activity: p.text().nullable(),
|
||||
run_profile: p.text().nullable(),
|
||||
run_pace_seconds_per_km: p.integer().nullable(),
|
||||
ski_discipline: p.text().nullable(),
|
||||
|
||||
@@ -44,7 +44,6 @@ export class Tours {
|
||||
taken_over_from_trip_title?: string | null;
|
||||
taken_over_from_tour_name?: string | null;
|
||||
bike_profile?: TToursBikeProfile | null;
|
||||
winter_activity?: TToursWinterActivity | null;
|
||||
run_profile?: TToursRunProfile | null;
|
||||
run_pace_seconds_per_km?: number | null;
|
||||
ski_discipline?: TToursSkiDiscipline | null;
|
||||
@@ -102,13 +101,6 @@ export const ToursBikeProfile = {
|
||||
|
||||
export type TToursBikeProfile = (typeof ToursBikeProfile)[keyof typeof ToursBikeProfile];
|
||||
|
||||
export const ToursWinterActivity = {
|
||||
WINTER_HIKE: 'winter_hike',
|
||||
SNOWSHOE: 'snowshoe',
|
||||
} as const;
|
||||
|
||||
export type TToursWinterActivity = (typeof ToursWinterActivity)[keyof typeof ToursWinterActivity];
|
||||
|
||||
export const ToursRunProfile = {
|
||||
RUNNING: 'running',
|
||||
TRAIL: 'trail',
|
||||
@@ -121,6 +113,8 @@ export const ToursSkiDiscipline = {
|
||||
SNOWBOARD: 'snowboard',
|
||||
CROSS_COUNTRY: 'cross_country',
|
||||
TOURING: 'touring',
|
||||
SNOWSHOE: 'snowshoe',
|
||||
WINTER_HIKE: 'winter_hike',
|
||||
} as const;
|
||||
|
||||
export type TToursSkiDiscipline = (typeof ToursSkiDiscipline)[keyof typeof ToursSkiDiscipline];
|
||||
@@ -181,7 +175,6 @@ export const ToursSchema = defineEntity({
|
||||
taken_over_from_trip_title: p.text().nullable(),
|
||||
taken_over_from_tour_name: p.text().nullable(),
|
||||
bike_profile: p.enum(() => ToursBikeProfile).nullable(),
|
||||
winter_activity: p.enum(() => ToursWinterActivity).nullable(),
|
||||
run_profile: p.enum(() => ToursRunProfile).nullable(),
|
||||
run_pace_seconds_per_km: p.integer().nullable(),
|
||||
ski_discipline: p.enum(() => ToursSkiDiscipline).nullable(),
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface CollectionToursTable {
|
||||
match_confidence: InsertOptional<number | null>;
|
||||
max_hiking_difficulty: InsertOptional<number | null>;
|
||||
bike_profile: InsertOptional<string | null>;
|
||||
winter_activity: InsertOptional<string | null>;
|
||||
run_profile: InsertOptional<string | null>;
|
||||
run_pace_seconds_per_km: InsertOptional<number | null>;
|
||||
ski_discipline: InsertOptional<string | null>;
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface ToursTable {
|
||||
taken_over_from_trip_title: InsertOptional<string | null>;
|
||||
taken_over_from_tour_name: InsertOptional<string | null>;
|
||||
bike_profile: InsertOptional<string | null>;
|
||||
winter_activity: InsertOptional<string | null>;
|
||||
run_profile: InsertOptional<string | null>;
|
||||
run_pace_seconds_per_km: InsertOptional<number | null>;
|
||||
ski_discipline: InsertOptional<string | null>;
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* HIK-01: a hike can carry a winter label, winter hiking or snowshoeing. It
|
||||
* is a column of the tour, NULL on every tour saved before and on every other
|
||||
* type, so no existing tour changes, and nothing routes or times by it.
|
||||
*/
|
||||
export class Migration20200101045500_a_hike_can_be_marked_as_a_winter_activity extends Migration {
|
||||
override name = 'Migration20200101045500_a_hike_can_be_marked_as_a_winter_activity';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(
|
||||
"ALTER TABLE tours ADD COLUMN winter_activity TEXT CHECK (winter_activity IS NULL OR winter_activity IN ('winter_hike', 'snowshoe'))",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* SKI-02: a tour can be a ski track, imported from a GPX file and kept as it
|
||||
* was recorded. The `ski` key has been in `tour_types`, disabled and without
|
||||
* a routing profile, since every contract type was seeded; it is switched on
|
||||
* here and keeps no routing profile, since TREK routes no ski track. The
|
||||
* discipline and, for cross-country, the technique are columns of the tour,
|
||||
* NULL on every other type, so no existing tour changes its meaning.
|
||||
* SKI-02: a tour can be a ski and winter tour, planned in the planner or
|
||||
* imported from a GPX file. The `ski` key has been in `tour_types`, disabled
|
||||
* and without a routing profile, since every contract type was seeded; it is
|
||||
* switched on here and keeps no routing profile: the planner draws its legs
|
||||
* itself. The discipline (alpine, snowboard, cross-country, ski touring,
|
||||
* snowshoe or winter hike) and, for cross-country, the technique are columns
|
||||
* of the tour, NULL on every other type, so no existing tour changes its
|
||||
* meaning.
|
||||
*/
|
||||
export class Migration20200101045520_a_tour_can_be_a_ski_track extends Migration {
|
||||
override name = 'Migration20200101045520_a_tour_can_be_a_ski_track';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(
|
||||
"ALTER TABLE tours ADD COLUMN ski_discipline TEXT CHECK (ski_discipline IS NULL OR ski_discipline IN ('alpine', 'snowboard', 'cross_country', 'touring'))",
|
||||
"ALTER TABLE tours ADD COLUMN ski_discipline TEXT CHECK (ski_discipline IS NULL OR ski_discipline IN ('alpine', 'snowboard', 'cross_country', 'touring', 'snowshoe', 'winter_hike'))",
|
||||
);
|
||||
this.addSql(
|
||||
"ALTER TABLE tours ADD COLUMN ski_technique TEXT CHECK (ski_technique IS NULL OR ski_technique IN ('classic', 'skating', 'unknown'))",
|
||||
|
||||
@@ -36,7 +36,6 @@ export class Migration20200101046100_a_collection_keeps_whole_tours extends Migr
|
||||
match_confidence REAL,
|
||||
max_hiking_difficulty INTEGER,
|
||||
bike_profile TEXT,
|
||||
winter_activity TEXT,
|
||||
run_profile TEXT,
|
||||
run_pace_seconds_per_km INTEGER,
|
||||
ski_discipline TEXT,
|
||||
|
||||
@@ -32,7 +32,6 @@ export interface CollectionTourRow {
|
||||
match_confidence: number | null;
|
||||
max_hiking_difficulty: number | null;
|
||||
bike_profile: string | null;
|
||||
winter_activity: string | null;
|
||||
run_profile: string | null;
|
||||
run_pace_seconds_per_km: number | null;
|
||||
ski_discipline: string | null;
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
RunProfile,
|
||||
SkiDiscipline,
|
||||
SkiTechnique,
|
||||
WinterActivity,
|
||||
} from '@trek/shared';
|
||||
|
||||
/**
|
||||
@@ -40,12 +39,10 @@ export interface TourListRow {
|
||||
origin: string;
|
||||
/** BIK-01: a bike tour's profile; NULL on every other type. */
|
||||
bike_profile: string | null;
|
||||
/** HIK-01: a hike's winter label; NULL for none and on every other type. */
|
||||
winter_activity: string | null;
|
||||
/** RUN-01: a run's profile and the pace it is planned at, seconds per km; NULL on every other type. */
|
||||
run_profile: string | null;
|
||||
run_pace_seconds_per_km: number | null;
|
||||
/** SKI-02: a ski track's discipline and, for cross-country, its technique; NULL on every other type. */
|
||||
/** SKI-02: a ski tour's discipline and, for cross-country, its technique; NULL on every other type. */
|
||||
ski_discipline: string | null;
|
||||
ski_technique: string | null;
|
||||
boat_type: string | null;
|
||||
@@ -120,7 +117,6 @@ export interface TourRow {
|
||||
taken_over_from_trip_title: string | null;
|
||||
taken_over_from_tour_name: string | null;
|
||||
bike_profile: string | null;
|
||||
winter_activity: string | null;
|
||||
run_profile: string | null;
|
||||
run_pace_seconds_per_km: number | null;
|
||||
ski_discipline: string | null;
|
||||
@@ -144,12 +140,10 @@ export interface TourInsert {
|
||||
max_hiking_difficulty: number | null;
|
||||
/** BIK-01: a bike tour's profile; left out, NULL. */
|
||||
bike_profile?: BikeProfile | null;
|
||||
/** HIK-01: a hike's winter label; left out, NULL on a new tour and kept as it is by an edit. */
|
||||
winter_activity?: WinterActivity | null;
|
||||
/** RUN-01: a run's profile and its pace, seconds per km; left out, NULL. */
|
||||
run_profile?: RunProfile | null;
|
||||
run_pace_seconds_per_km?: number | null;
|
||||
/** SKI-02: a ski track's discipline and technique; left out, NULL. */
|
||||
/** SKI-02: a ski tour's discipline and technique; left out, NULL. */
|
||||
ski_discipline?: SkiDiscipline | null;
|
||||
ski_technique?: SkiTechnique | null;
|
||||
/** A paddle tour's boat, water and chosen speed; left out, a hike's NULLs. */
|
||||
@@ -307,7 +301,6 @@ export class ToursRepository extends TrekRepository<Tours> {
|
||||
't.derived_from_place_id as derived_from_place_id',
|
||||
't.origin as origin',
|
||||
't.bike_profile as bike_profile',
|
||||
't.winter_activity as winter_activity',
|
||||
't.run_profile as run_profile',
|
||||
't.run_pace_seconds_per_km as run_pace_seconds_per_km',
|
||||
't.ski_discipline as ski_discipline',
|
||||
@@ -774,7 +767,6 @@ export class ToursRepository extends TrekRepository<Tours> {
|
||||
match_confidence: input.match_confidence,
|
||||
max_hiking_difficulty: input.max_hiking_difficulty,
|
||||
bike_profile: input.bike_profile ?? null,
|
||||
winter_activity: input.winter_activity ?? null,
|
||||
run_profile: input.run_profile ?? null,
|
||||
run_pace_seconds_per_km: input.run_pace_seconds_per_km ?? null,
|
||||
ski_discipline: input.ski_discipline ?? null,
|
||||
@@ -876,7 +868,6 @@ export class ToursRepository extends TrekRepository<Tours> {
|
||||
portage_speed_kmh: input.portage_speed_kmh,
|
||||
paddle_lake_style: input.paddle_lake_style,
|
||||
...(input.hiking_preferences !== undefined ? { hiking_preferences: input.hiking_preferences } : {}),
|
||||
...(input.winter_activity !== undefined ? { winter_activity: input.winter_activity } : {}),
|
||||
})
|
||||
.where('place_id', '=', place_id)
|
||||
.where('place_id', 'in', (eb) => eb.selectFrom('places').select('id').where('trip_id', '=', trip_id))
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type SkiTechnique,
|
||||
type TourOrigin,
|
||||
type TourTypeKey,
|
||||
type WinterActivity,
|
||||
} from '@trek/shared';
|
||||
|
||||
/** MDT-01: a kept multi-day tour's stages, one more than the stops that end one, as the trip's list counts them. */
|
||||
@@ -26,8 +25,7 @@ function keptStageCount(row: Pick<CollectionTourRow, 'waypoints'>): number | nul
|
||||
/**
|
||||
* LIB-03: a kept tour as the collection lists it, its measures as they were
|
||||
* saved, null staying null, with what the trip's list shows of its type (the
|
||||
* winter label, the ski discipline, the run's pace, the bike profile, the
|
||||
* stages), so a kept tour looks as it did in the trip it came from.
|
||||
* ski discipline, the run's pace, the bike profile, the stages), so a kept tour looks as it did in the trip it came from.
|
||||
*/
|
||||
export function keptTourItem(row: CollectionTourRow): CollectionTourItem {
|
||||
return {
|
||||
@@ -45,7 +43,6 @@ export function keptTourItem(row: CollectionTourRow): CollectionTourItem {
|
||||
max_hiking_difficulty: row.max_hiking_difficulty,
|
||||
wanderer_ref: row.wanderer_ref,
|
||||
bike_profile: row.bike_profile as BikeProfile | null,
|
||||
winter_activity: row.winter_activity as WinterActivity | null,
|
||||
run_profile: row.run_profile as RunProfile | null,
|
||||
run_pace_seconds_per_km: row.run_pace_seconds_per_km,
|
||||
ski_discipline: row.ski_discipline as SkiDiscipline | null,
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
SkiDiscipline,
|
||||
SkiTechnique,
|
||||
TourOrigin,
|
||||
WinterActivity,
|
||||
} from '@trek/shared';
|
||||
|
||||
/** The facet columns a copy carries over as its source has them, measures and settings alike. */
|
||||
@@ -27,7 +26,6 @@ interface TourCopiedColumns {
|
||||
match_confidence: number | null;
|
||||
max_hiking_difficulty: number | null;
|
||||
bike_profile: string | null;
|
||||
winter_activity: string | null;
|
||||
run_profile: string | null;
|
||||
run_pace_seconds_per_km: number | null;
|
||||
ski_discipline: string | null;
|
||||
@@ -67,7 +65,6 @@ export function copiedColumns(row: TourCopiedColumns): TourCopiedColumns {
|
||||
match_confidence: row.match_confidence,
|
||||
max_hiking_difficulty: row.max_hiking_difficulty,
|
||||
bike_profile: row.bike_profile,
|
||||
winter_activity: row.winter_activity,
|
||||
run_profile: row.run_profile,
|
||||
run_pace_seconds_per_km: row.run_pace_seconds_per_km,
|
||||
ski_discipline: row.ski_discipline,
|
||||
@@ -168,7 +165,6 @@ export function copiedFacet(
|
||||
...facet,
|
||||
place_id: placeId,
|
||||
bike_profile: facet.bike_profile as BikeProfile | null,
|
||||
winter_activity: facet.winter_activity as WinterActivity | null,
|
||||
run_profile: facet.run_profile as RunProfile | null,
|
||||
ski_discipline: facet.ski_discipline as SkiDiscipline | null,
|
||||
ski_technique: facet.ski_technique as SkiTechnique | null,
|
||||
|
||||
@@ -22,12 +22,18 @@ const RUN_ACTIVITY: Record<RunProfile, string> = {
|
||||
trail: 'trail_running',
|
||||
};
|
||||
|
||||
/** SKI-02: a ski track says how it was skied, in the same words; without a known discipline it is skiing. */
|
||||
/**
|
||||
* SKI-02: a ski tour says how it is done, in the same words: snowshoes make it
|
||||
* snowshoeing, and a winter hike is walked as any hike is. Without a known
|
||||
* discipline it is skiing.
|
||||
*/
|
||||
const SKI_ACTIVITY: Record<SkiDiscipline, string> = {
|
||||
alpine: 'alpine_skiing',
|
||||
snowboard: 'snowboarding',
|
||||
cross_country: 'cross_country_skiing',
|
||||
touring: 'backcountry_skiing',
|
||||
snowshoe: 'snowshoeing',
|
||||
winter_hike: 'hiking',
|
||||
};
|
||||
|
||||
/** The activity a ride names in GPX (HOR-01), in the words Garmin reads. */
|
||||
@@ -35,9 +41,9 @@ const HORSE_ACTIVITY = 'horseback_riding';
|
||||
|
||||
/**
|
||||
* The activity a tour's track names in GPX: a paddle tour its boat, a bike
|
||||
* tour and a run their profile, a ski track its discipline (SKI-02), a ride
|
||||
* horseback riding (HOR-01), a hike on snowshoes snowshoeing (HIK-01) and any
|
||||
* other hike hiking; whatever is missing falls back to the plainest.
|
||||
* tour and a run their profile, a ski tour its discipline (SKI-02), a ride
|
||||
* horseback riding (HOR-01) and a hike hiking; whatever is missing falls back
|
||||
* to the plainest.
|
||||
*/
|
||||
export function gpxActivityOf(tour: {
|
||||
tour_type: string;
|
||||
@@ -45,7 +51,6 @@ export function gpxActivityOf(tour: {
|
||||
bike_profile?: string | null;
|
||||
run_profile?: string | null;
|
||||
ski_discipline?: string | null;
|
||||
winter_activity?: string | null;
|
||||
}): string {
|
||||
const { tour_type: type, boat_type: boat, bike_profile: bike } = tour;
|
||||
if (type === 'kayak') return PADDLE_ACTIVITY[(boat ?? 'other') as PaddleBoatType] ?? PADDLE_ACTIVITY.other;
|
||||
@@ -53,7 +58,5 @@ export function gpxActivityOf(tour: {
|
||||
if (type === 'run') return RUN_ACTIVITY[(tour.run_profile ?? 'running') as RunProfile] ?? RUN_ACTIVITY.running;
|
||||
if (type === 'ski') return SKI_ACTIVITY[tour.ski_discipline as SkiDiscipline] ?? 'skiing';
|
||||
if (type === 'horse') return HORSE_ACTIVITY;
|
||||
if (type !== 'hike') return type;
|
||||
// A winter hike is walked as any hike is; only snowshoes make it an activity of its own.
|
||||
return tour.winter_activity === 'snowshoe' ? 'snowshoeing' : 'hiking';
|
||||
return type === 'hike' ? 'hiking' : type;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
type TourListItem,
|
||||
type TourOrigin,
|
||||
type TourProvenanceSource,
|
||||
type WinterActivity,
|
||||
} from '@trek/shared';
|
||||
|
||||
/** MDT-02: the stops a multi-day tour's stages run between; null where its points do not tell them. */
|
||||
@@ -60,7 +59,6 @@ export function tourItemOf(
|
||||
derived_from_place_id: r.derived_from_place_id,
|
||||
origin: r.origin as TourOrigin,
|
||||
bike_profile: r.bike_profile as BikeProfile | null,
|
||||
winter_activity: r.winter_activity as WinterActivity | null,
|
||||
run_profile: r.run_profile as RunProfile | null,
|
||||
run_pace_seconds_per_km: r.run_pace_seconds_per_km,
|
||||
ski_discipline: r.ski_discipline as SkiDiscipline | null,
|
||||
|
||||
@@ -70,12 +70,17 @@ interface PlanLeg {
|
||||
/**
|
||||
* routed: planned by the router; track: part of a recorded line (an import
|
||||
* without control points of its own), which no router planned; ferrata, free
|
||||
* and portage: a fixed line taken as it stands.
|
||||
* and portage: a fixed line taken as it stands. A leg of a planned ski tour
|
||||
* (SKI-02) is free: the planner draws it straight between its points.
|
||||
*/
|
||||
kind: 'routed' | 'track' | TourFixedLegKind;
|
||||
/** The via ferrata's name where OpenStreetMap gives one. */
|
||||
name: string | null;
|
||||
/** Derived: the length of a fixed leg's own line; null for a routed or track leg, whose length is not stored apart. */
|
||||
/**
|
||||
* Derived: the length of a fixed leg's own line, or of the straight line a
|
||||
* ski tour's leg is drawn as; null for a routed or track leg, whose length is
|
||||
* not stored apart.
|
||||
*/
|
||||
length_km: number | null;
|
||||
}
|
||||
|
||||
@@ -88,6 +93,8 @@ function roundKm(km: number): number {
|
||||
interface PlanLegSource {
|
||||
/** The line is a recorded track (the tour stores no control points of its own), so no router planned it. */
|
||||
recorded: boolean;
|
||||
/** SKI-02: the planner drew every leg straight between its points (a planned ski tour), so no router planned one. */
|
||||
drawn?: boolean;
|
||||
}
|
||||
|
||||
/** A fixed leg's line as points of a line: latitude and longitude, no height. */
|
||||
@@ -95,15 +102,25 @@ function legLine(geometry: TourFixedLeg['geometry']): GeometryPoint[] {
|
||||
return geometry.map(([lat, lng]): GeometryPoint => [lat, lng]);
|
||||
}
|
||||
|
||||
/** A leg drawn straight from one control point to the next, as the planner draws a ski tour's. */
|
||||
function straightLine(from: TourWaypoint, to: TourWaypoint): GeometryPoint[] {
|
||||
return [
|
||||
[from.lat, from.lng],
|
||||
[to.lat, to.lng],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The legs between consecutive control points, each saying whether the router
|
||||
* planned it, it follows a recorded track, or it is a fixed line.
|
||||
* planned it, it follows a recorded track, or it is a fixed line. A planned
|
||||
* ski tour's leg is a straight line between its points, as the planner drew it.
|
||||
*/
|
||||
export function planLegs(points: readonly TourWaypoint[], { recorded }: PlanLegSource): PlanLeg[] {
|
||||
const unfixed = recorded ? 'track' : 'routed';
|
||||
export function planLegs(points: readonly TourWaypoint[], { recorded, drawn = false }: PlanLegSource): PlanLeg[] {
|
||||
const unfixed = recorded ? 'track' : drawn ? 'free' : 'routed';
|
||||
return points.slice(1).map((to, index) => {
|
||||
const from = points[index] as TourWaypoint;
|
||||
const leg = from.fixed_leg ?? null;
|
||||
const line = leg ? legLine(leg.geometry) : unfixed === 'free' ? straightLine(from, to) : null;
|
||||
return {
|
||||
from_sequence: from.sequence,
|
||||
to_sequence: to.sequence,
|
||||
@@ -111,7 +128,7 @@ export function planLegs(points: readonly TourWaypoint[], { recorded }: PlanLegS
|
||||
to_key: to.key ?? null,
|
||||
kind: leg?.kind ?? unfixed,
|
||||
name: leg?.kind === 'ferrata' ? leg.name : null,
|
||||
length_km: leg ? roundKm(computeTourMetrics(legLine(leg.geometry)).distanceKm) : null,
|
||||
length_km: line ? roundKm(computeTourMetrics(line).distanceKm) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
tourStageIssues,
|
||||
tourWebsiteSchema,
|
||||
type TourWaypoint,
|
||||
type WinterActivity,
|
||||
} from '@trek/shared';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@@ -30,12 +29,16 @@ function hikingFields(input: TourCreateCommand): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* HIK-01: a save's winter label as stored. Only a hike keeps one; another type
|
||||
* takes it off. A hike's save that leaves the field out (a client from before
|
||||
* the label) keeps the one the tour has, rather than taking it off unasked.
|
||||
* SKI-02: a save's ski discipline and technique as stored. Only a ski tour
|
||||
* keeps them, the technique only for cross-country; another type takes them off.
|
||||
*/
|
||||
function winterField(input: TourCreateCommand): WinterActivity | null | undefined {
|
||||
return input.tour_type === 'hike' ? input.winter_activity : null;
|
||||
function skiFields(input: TourCreateCommand): Pick<TourUpdate, 'ski_discipline' | 'ski_technique'> {
|
||||
if (input.tour_type !== 'ski') return { ski_discipline: null, ski_technique: null };
|
||||
const discipline = input.ski_discipline ?? null;
|
||||
return {
|
||||
ski_discipline: discipline,
|
||||
ski_technique: discipline === 'cross_country' ? (input.ski_technique ?? null) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,11 +90,8 @@ export function tourRouteFields(input: TourCreateCommand): TourUpdate {
|
||||
match_confidence: 1,
|
||||
max_hiking_difficulty: input.max_hiking_difficulty,
|
||||
bike_profile: input.tour_type === 'bike' ? (input.bike_profile ?? null) : null,
|
||||
winter_activity: winterField(input),
|
||||
...runFields(input),
|
||||
// SKI-02: TREK plans no ski track, so a planned save is never one and takes its classification off.
|
||||
ski_discipline: null,
|
||||
ski_technique: null,
|
||||
...skiFields(input),
|
||||
boat_type: input.boat_type ?? null,
|
||||
water_context: input.water_context ?? null,
|
||||
paddle_speed_kmh: input.paddle_speed_kmh ?? null,
|
||||
|
||||
@@ -61,7 +61,8 @@ function planOf({ detail, days, line, names }: PlanRead, run: WalkProgress | nul
|
||||
waypoints: planPoints(detail.tour.tour_type, named, run),
|
||||
// ACT-01: the recorded tracks assigned to the tour, with their values and without their lines.
|
||||
activities: (detail.activities ?? []).map(withoutLine),
|
||||
legs: planLegs(detail.waypoints, { recorded }),
|
||||
// SKI-02: a planned ski tour's legs are drawn straight between its points; no router planned them.
|
||||
legs: planLegs(detail.waypoints, { recorded, drawn: detail.tour.tour_type === 'ski' }),
|
||||
line: saved,
|
||||
...(isWalk ? { run: run ? planRun(run) : null } : {}),
|
||||
derived: derivedFields(detail, { controlsFromTrack, line: saved, run }),
|
||||
@@ -101,7 +102,7 @@ export class ToursMcp {
|
||||
@Tool({
|
||||
name: 'list_tours',
|
||||
description:
|
||||
'List the saved tours of a trip (hikes, city walks, runs, bike tours, paddle tours, rides on horseback and ski tracks planned or imported in the Tours addon): name, type (hike, walk for a city walk, run, bike, kayak for a paddle tour, horse for a ride, or ski for a ski track; a ride has no riding time worked out, so its duration is null), where it came from (origin: trek, gpx for an imported GPX, KML or KMZ file, or wanderer), distance in km, ascent and descent in metres, the stored routing limit of a hike (max_hiking_difficulty, SAC T1 to T6) and its route choices (hiking_preferences: avoid_climbs for gentler ways, avoid_steps, avoid_ferries; weightings, never a grade of the path), the profile a bike tour is routed for (bike_profile: touring for paved cycle paths and quiet roads, road for asphalt, gravel for gravel and farm tracks, mtb for rough tracks; a routing preference, never a statement that a way is rideable or safe), the winter label of a hike (winter_activity: winter_hike or snowshoe, null for none; a label the travellers chose, while the route and its time are planned as for any hike, never a statement that a way can be walked in winter or anything about snow or avalanches), the profile and pace of a run (run_profile: running for runnable, mostly paved ways in towns and parks, trail for forest tracks and natural paths up to mountain paths (T2); a routing preference, never a statement that a way can be run; run_pace_seconds_per_km, the pace the runners planned with, null while they entered none; its duration is the distance at that pace and nothing else, no climb, descent or difficulty is in it, and it is a plan, never a recorded time), the classification of a ski track (ski_discipline: alpine, snowboard, cross_country or touring for ski touring; ski_technique for cross-country only: classic, skating or unknown when the travellers did not know; a ski track is imported from a GPX file and kept as recorded, never routed, its distance and heights come from the file, it has no ski time worked out, and nothing in it says anything about snow, avalanches or whether a run or trail is open), the boat, water and chosen speed of a paddle tour (boat_type, water_context, paddle_speed_kmh in km/h, null while the suggested speed of the boat applies), the check bench of a paddle tour (paddle_check: how far the OpenStreetMap obstacle data covers its line and how many obstacles are open, to check again or decided by the travellers; a planning status, never a safety or legal clearance), for a city walk the number of its stations (station_count: its points without the shaping points and the way back of a loop), their stays as far as entered (station_stay_minutes) and how many have none (station_stays_unknown), whether a tour still waits for its way to be planned (route_pending: saved with its points only, so its distance and walking time stay null until someone opens it in the planner and saves it), for a multi-day tour the number of stages its route is split into (stage_count: one route planned whole, each stage ending at a stop marked as its end; null for a tour of one day) and the stops they run between (stage_stops, in route order with key and coordinates: stage n runs from stop n-1 to stop n, and is known by the key of the stop it starts at), the revision (raised by every saved change of the tour), the recorded tracks assigned to a tour (activity_tracks) and, for a recorded track, the tour it documents (activity_of), when an imported track was recorded (track_started_at, track_ended_at; null when the file carried no times), and the days of the trip each tour is on (tour_stage_key names the stage a day holds by its starting stop, null where the day holds the whole tour; a stage may also be on no day yet).',
|
||||
'List the saved tours of a trip (hikes, city walks, runs, bike tours, paddle tours, rides on horseback and ski and winter tours planned or imported in the Tours addon): name, type (hike, walk for a city walk, run, bike, kayak for a paddle tour, horse for a ride, or ski for a ski and winter tour; a ride and a ski tour have no time worked out, so their duration is null), where it came from (origin: trek, gpx for an imported GPX, KML or KMZ file, or wanderer), distance in km, ascent and descent in metres, the stored routing limit of a hike (max_hiking_difficulty, SAC T1 to T6) and its route choices (hiking_preferences: avoid_climbs for gentler ways, avoid_steps, avoid_ferries; weightings, never a grade of the path), the profile a bike tour is routed for (bike_profile: touring for paved cycle paths and quiet roads, road for asphalt, gravel for gravel and farm tracks, mtb for rough tracks; a routing preference, never a statement that a way is rideable or safe), the profile and pace of a run (run_profile: running for runnable, mostly paved ways in towns and parks, trail for forest tracks and natural paths up to mountain paths (T2); a routing preference, never a statement that a way can be run; run_pace_seconds_per_km, the pace the runners planned with, null while they entered none; its duration is the distance at that pace and nothing else, no climb, descent or difficulty is in it, and it is a plan, never a recorded time), the discipline of a ski tour (ski_discipline: alpine, snowboard, cross_country, touring for ski touring, snowshoe or winter_hike; snowshoeing and winter hiking are ski tours, a hike is summer only; ski_technique for cross-country only: classic, skating or unknown when the travellers did not know; a planned ski tour has every leg drawn straight between its points, never routed along pistes, lifts or trails, an imported one is kept as recorded; its distance and heights come from that line or the file, it has no ski time worked out, and nothing in it says anything about snow, avalanches or whether a run or trail is open), the boat, water and chosen speed of a paddle tour (boat_type, water_context, paddle_speed_kmh in km/h, null while the suggested speed of the boat applies), the check bench of a paddle tour (paddle_check: how far the OpenStreetMap obstacle data covers its line and how many obstacles are open, to check again or decided by the travellers; a planning status, never a safety or legal clearance), for a city walk the number of its stations (station_count: its points without the shaping points and the way back of a loop), their stays as far as entered (station_stay_minutes) and how many have none (station_stays_unknown), whether a tour still waits for its way to be planned (route_pending: saved with its points only, so its distance and walking time stay null until someone opens it in the planner and saves it), for a multi-day tour the number of stages its route is split into (stage_count: one route planned whole, each stage ending at a stop marked as its end; null for a tour of one day) and the stops they run between (stage_stops, in route order with key and coordinates: stage n runs from stop n-1 to stop n, and is known by the key of the stop it starts at), the revision (raised by every saved change of the tour), the recorded tracks assigned to a tour (activity_tracks) and, for a recorded track, the tour it documents (activity_of), when an imported track was recorded (track_started_at, track_ended_at; null when the file carried no times), and the days of the trip each tour is on (tour_stage_key names the stage a day holds by its starting stop, null where the day holds the whole tour; a stage may also be on no day yet).',
|
||||
inputSchema: { tripId: idSchema },
|
||||
annotations: TOOL_ANNOTATIONS_READONLY,
|
||||
when: addonGate(ADDON_IDS.TOURS),
|
||||
@@ -116,7 +117,7 @@ export class ToursMcp {
|
||||
@Tool({
|
||||
name: 'get_tour_plan',
|
||||
description:
|
||||
'Read one saved tour of a trip as the travellers saved it: its facts (tour: place_id, name, tour_type, distance, heights and walking time as stored, null when not known; the routing options as list_tours describes them; revision, raised by every saved change; the days it is on), its ordered control points (start, via and end, each with its sequence and its key, the same across saves; an import without its own shows the two ends of its track), where it came from (provenance) and its legs. Drafts in a browser are never visible: only saved tours exist here, and reading changes nothing, not the tour, its revision nor any visit. A control point with linked_place_id stands for that place of the trip (linked_place_name names it); it keeps its own coordinates. A control point with stage_end true ends a stage of a multi-day tour, and the next stage begins there; the stages run in the order of the route, the first from the start, the last to the end, and a tour without such a point is a tour of one day. legs lists every step from one control point to the next (from_sequence, to_sequence, from_key, to_key) with its kind: routed (planned by the router), track (part of a recorded line, an import without control points of its own, which no router planned), ferrata (follows a via ferrata, name gives it), free (drawn by hand and checked by nothing), portage (a paddle tour carried over land) or horse_route (a ride only: a section of a signposted horse route from OpenStreetMap in the direction the rider chose; the fixed_leg of its control point says signposted, with or against the direction the route is signposted in, null where it is not signposted one way, and access, the metres of each horse=* value its ways carried when it was taken, unknown where untagged; being on a horse route is no permission to ride it); length_km is the length of the line a fixed leg keeps and null for a routed or track leg, whose length is not stored apart. line gives the saved line in brief: point_count, has_heights and a preview thinned to 200 points; null while the tour waits for its way to be planned. A paddle tour also carries its obstacle checklist (obstacle_check): the obstacles of the OpenStreetMap data along its line, the decisions the travellers made about them and their own portages and locks; a planning aid, never a safety or legal clearance. A city walk (tour_type walk) has stations for control points, shaping points and the way back of a loop aside, each with every field spelled out: key (the visits of a day are kept by it), name, stay_minutes (the stay in minutes; 0 for a point only passed through, null when not known), importance (optional or must; null for a normal station), protected (true for a station any change you propose has to keep where it is: neither removed, replaced nor moved; the travellers can still edit it themselves in the app), opening_hours (as OpenStreetMap writes them; a hint to check, never a promise; null when none is known) and visit_status. Visits belong to one day of the walk: name that day with dayId (one of its days) to read the status of each station that day (planned, visited or skipped) and run (the day, its assignment and what is still open); without dayId visit_status and run are null. A station visited or skipped that day is done: do not ask about it again or move it to another day. dayId is for a city walk only. The recorded tracks assigned to the tour (activities, ACT-01) each name their execution (assignment_id and day_id, null for none), the recording (track: its tour in the trip, origin, file name, when it came in) and what the recording measures (metrics: distance_km, elevation_gain_m and elevation_loss_m, null without heights; started_at, ended_at and elapsed_minutes from the first to the last time on its points, pauses included, null when the file carried no times, never estimated); these are the values of the recording, the planned values of the tour stay apart and unchanged. derived names the fields worked out on reading rather than stored with the tour: never present them as saved facts.',
|
||||
'Read one saved tour of a trip as the travellers saved it: its facts (tour: place_id, name, tour_type, distance, heights and walking time as stored, null when not known; the routing options as list_tours describes them; revision, raised by every saved change; the days it is on), its ordered control points (start, via and end, each with its sequence and its key, the same across saves; an import without its own shows the two ends of its track), where it came from (provenance) and its legs. Drafts in a browser are never visible: only saved tours exist here, and reading changes nothing, not the tour, its revision nor any visit. A control point with linked_place_id stands for that place of the trip (linked_place_name names it); it keeps its own coordinates. A control point with stage_end true ends a stage of a multi-day tour, and the next stage begins there; the stages run in the order of the route, the first from the start, the last to the end, and a tour without such a point is a tour of one day. legs lists every step from one control point to the next (from_sequence, to_sequence, from_key, to_key) with its kind: routed (planned by the router), track (part of a recorded line, an import without control points of its own, which no router planned), ferrata (follows a via ferrata, name gives it), free (drawn by hand and checked by nothing; every leg of a planned ski tour is free, a straight line between its points), portage (a paddle tour carried over land) or horse_route (a ride only: a section of a signposted horse route from OpenStreetMap in the direction the rider chose; the fixed_leg of its control point says signposted, with or against the direction the route is signposted in, null where it is not signposted one way, and access, the metres of each horse=* value its ways carried when it was taken, unknown where untagged; being on a horse route is no permission to ride it); length_km is the length of the line a fixed leg keeps (for a ski tour's leg the straight line) and null for a routed or track leg, whose length is not stored apart. line gives the saved line in brief: point_count, has_heights and a preview thinned to 200 points; null while the tour waits for its way to be planned. A paddle tour also carries its obstacle checklist (obstacle_check): the obstacles of the OpenStreetMap data along its line, the decisions the travellers made about them and their own portages and locks; a planning aid, never a safety or legal clearance. A city walk (tour_type walk) has stations for control points, shaping points and the way back of a loop aside, each with every field spelled out: key (the visits of a day are kept by it), name, stay_minutes (the stay in minutes; 0 for a point only passed through, null when not known), importance (optional or must; null for a normal station), protected (true for a station any change you propose has to keep where it is: neither removed, replaced nor moved; the travellers can still edit it themselves in the app), opening_hours (as OpenStreetMap writes them; a hint to check, never a promise; null when none is known) and visit_status. Visits belong to one day of the walk: name that day with dayId (one of its days) to read the status of each station that day (planned, visited or skipped) and run (the day, its assignment and what is still open); without dayId visit_status and run are null. A station visited or skipped that day is done: do not ask about it again or move it to another day. dayId is for a city walk only. The recorded tracks assigned to the tour (activities, ACT-01) each name their execution (assignment_id and day_id, null for none), the recording (track: its tour in the trip, origin, file name, when it came in) and what the recording measures (metrics: distance_km, elevation_gain_m and elevation_loss_m, null without heights; started_at, ended_at and elapsed_minutes from the first to the last time on its points, pauses included, null when the file carried no times, never estimated); these are the values of the recording, the planned values of the tour stay apart and unchanged. derived names the fields worked out on reading rather than stored with the tour: never present them as saved facts.',
|
||||
inputSchema: {
|
||||
tripId: idSchema,
|
||||
placeId: idSchema.describe('The place id of the tour, as list_tours gives it'),
|
||||
|
||||
@@ -841,7 +841,6 @@ export class ToursService {
|
||||
route_missing: 0,
|
||||
revision: 1,
|
||||
hiking_preferences: null,
|
||||
winter_activity: null,
|
||||
track_started_at: recorded?.started ?? null,
|
||||
track_ended_at: recorded?.ended ?? null,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user