fix(tours): get_tour_plan calls the leg of a recorded track a track, not routed

An import without control points of its own answers the two ends of its
recorded track as its points, and the one leg between them was labelled
routed, as if the router had planned it. The assistant read a false fact.
Such a leg now says track, and the tool description and the wiki explain
the kind.
This commit is contained in:
Maurice
2026-10-20 12:13:06 +02:00
parent 19946039ad
commit b826ecd425
5 changed files with 49 additions and 15 deletions
+20 -6
View File
@@ -61,11 +61,15 @@ interface PlanLeg {
/** The keys of its two points, the same across saves; null for a point that has none. */
from_key: string | null;
to_key: string | null;
/** routed: planned by the router; ferrata, free and portage: a fixed line taken as it stands. */
kind: 'routed' | TourFixedLegKind;
/**
* 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.
*/
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 leg, whose length is not stored apart. */
/** Derived: the length of a fixed leg's own line; null for a routed or track leg, whose length is not stored apart. */
length_km: number | null;
}
@@ -74,8 +78,18 @@ function roundKm(km: number): number {
return Math.round(km * 100) / 100;
}
/** The legs between consecutive control points, each saying whether the router planned it or it is a fixed line. */
export function planLegs(points: readonly TourWaypoint[]): PlanLeg[] {
/** How the line between the control points came to be, as far as the tour itself says. */
interface PlanLegSource {
/** The line is a recorded track (the tour stores no control points of its own), so no router planned it. */
recorded: boolean;
}
/**
* The legs between consecutive control points, each saying whether the router
* planned it, it follows a recorded track, or it is a fixed line.
*/
export function planLegs(points: readonly TourWaypoint[], { recorded }: PlanLegSource): PlanLeg[] {
const unfixed = recorded ? 'track' : 'routed';
return points.slice(1).map((to, index) => {
const from = points[index] as TourWaypoint;
const leg = from.fixed_leg ?? null;
@@ -84,7 +98,7 @@ export function planLegs(points: readonly TourWaypoint[]): PlanLeg[] {
to_sequence: to.sequence,
from_key: from.key ?? null,
to_key: to.key ?? null,
kind: leg?.kind ?? 'routed',
kind: leg?.kind ?? unfixed,
name: leg?.kind === 'ferrata' ? leg.name : null,
length_km: leg ? roundKm(computeTourMetrics(leg.geometry).distanceKm) : null,
};
+4 -3
View File
@@ -50,13 +50,14 @@ function planOf({ detail, days, line, names }: PlanRead, run: WalkProgress | nul
);
const saved = planLine(line);
// A tour that stores no control points answers the two ends of its line instead.
const controlsFromTrack = detail.tour.has_waypoints !== true && detail.waypoints.length > 0;
const recorded = detail.tour.has_waypoints !== true;
const controlsFromTrack = recorded && detail.waypoints.length > 0;
const isWalk = detail.tour.tour_type === 'walk';
return {
...detail,
tour: withDays(detail.tour, days),
waypoints: planPoints(detail.tour.tour_type, named, run),
legs: planLegs(detail.waypoints),
legs: planLegs(detail.waypoints, { recorded }),
line: saved,
...(isWalk ? { run: run ? planRun(run) : null } : {}),
derived: derivedFields(detail, { controlsFromTrack, line: saved, run }),
@@ -111,7 +112,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. 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), 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 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. 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. 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. 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'),