feat(tours): a hike can be marked as a winter hike or a snowshoe hike

HIK-01. The details of the hiking planner gain an optional Winter activity
switch, off by default, so the normal hiking flow asks for nothing more.
Switched on, the hike is a winter hike or a snowshoe hike; the choice stays
dimmed and out of reach while the switch is off. The label is saved with the
tour, comes back when it is opened, survives a draft reload, a copy and a
take-over, and switching it off takes only the label away: the route, its
grade and the days stay as they were.

The label is a label only. Routing, the walking time and the 3D terrain view
work as for any hike, and the info dot and the head pill's tooltip say that
nothing was checked for snow, avalanches or whether a path can be walked in
winter. The saved label shows as a frosted snowflake on the corner of the
tour's tile in the tour list and the day plan, and as a pill beside the type
in the tour's detail.

The server keeps it in tours.winter_activity (CHECK list, NULL for every tour
saved before and for every other type), refuses it on a bike tour, a city walk
and a paddle tour, and keeps the stored label when an older client saves a
hike without the field. A snowshoe hike exports to GPX as snowshoeing.
list_tours names the field, with the same words on what it does not say.
This commit is contained in:
Maurice
2026-10-20 12:20:33 +02:00
parent 71fe69c1f1
commit cd407cf80c
61 changed files with 1266 additions and 24 deletions
@@ -16,14 +16,21 @@ const BIKE_ACTIVITY: Record<BikeProfile, string> = {
mtb: 'mountain_biking',
};
/** The activity a tour's track names in GPX: a paddle tour its boat, a bike tour its profile, a hike hiking; whatever is missing falls back to the plainest. */
/**
* The activity a tour's track names in GPX: a paddle tour its boat, a bike
* tour its profile, a hike on snowshoes snowshoeing (HIK-01) and any other
* hike hiking; whatever is missing falls back to the plainest.
*/
export function gpxActivityOf(tour: {
tour_type: string;
boat_type?: string | null;
bike_profile?: 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;
if (type === 'bike') return BIKE_ACTIVITY[(bike ?? 'touring') as BikeProfile] ?? BIKE_ACTIVITY.touring;
return type === 'hike' ? 'hiking' : type;
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';
}
+1 -1
View File
@@ -85,7 +85,7 @@ export class ToursMcp {
@Tool({
name: 'list_tours',
description:
'List the saved tours of a trip (hikes, city walks, bike tours and paddle tours planned or imported in the Tours addon): name, type (hike, walk for a city walk, bike, or kayak for a paddle tour), where it came from (origin: trek, gpx 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 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), the revision (raised by every saved change of the tour), and the days of the trip each tour is on.',
'List the saved tours of a trip (hikes, city walks, bike tours and paddle tours planned or imported in the Tours addon): name, type (hike, walk for a city walk, bike, or kayak for a paddle tour), where it came from (origin: trek, gpx 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 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), the revision (raised by every saved change of the tour), and the days of the trip each tour is on.',
inputSchema: { tripId: idSchema },
annotations: TOOL_ANNOTATIONS_READONLY,
when: addonGate(ADDON_IDS.TOURS),
+14
View File
@@ -58,6 +58,7 @@ import {
type TourProvenanceSource,
type TourWaypoint,
type WandererDifficulty,
type WinterActivity,
walkStationFacts,
walkStationsOf,
wandererDifficultyToSac,
@@ -103,6 +104,15 @@ function hikingFields(input: TourCreateCommand): string | null {
return preferences ? encodeJson(TOUR_HIKING_PREFERENCES, preferences) : 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.
*/
function winterField(input: TourCreateCommand): WinterActivity | null | undefined {
return input.tour_type === 'hike' ? input.winter_activity : null;
}
/**
* CW-03: every point with its key, kept from the write where it came with one,
* so the visits of a city walk's stations outlast a reorder or a new point. A
@@ -155,6 +165,7 @@ export class ToursService {
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,
boat_type: r.boat_type as PaddleBoatType | null,
water_context: r.water_context as PaddleWaterContext | null,
paddle_speed_kmh: r.paddle_speed_kmh,
@@ -196,6 +207,7 @@ export class ToursService {
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),
boat_type: input.boat_type ?? null,
water_context: input.water_context ?? null,
paddle_speed_kmh: input.paddle_speed_kmh ?? null,
@@ -601,6 +613,7 @@ export class ToursService {
match_confidence: row.match_confidence,
max_hiking_difficulty: row.max_hiking_difficulty,
bike_profile: row.bike_profile as BikeProfile | null,
winter_activity: row.winter_activity as WinterActivity | null,
boat_type: row.boat_type as PaddleBoatType | null,
water_context: row.water_context as PaddleWaterContext | null,
paddle_speed_kmh: row.paddle_speed_kmh,
@@ -843,6 +856,7 @@ export class ToursService {
route_missing: 0,
revision: 1,
hiking_preferences: null,
winter_activity: null,
}),
);
}