diff --git a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts index 5f3eda6..bba3cfb 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing-controls.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing-controls.ts @@ -703,7 +703,10 @@ export class RoutingControls { let response: TrackPoint[]; try { - response = await route(targetTrackPoints.map((trkpt) => trkpt.getCoordinates())); + response = await route( + targetTrackPoints.map((trkpt) => trkpt.getCoordinates()), + targetTrackPoints.map((trkpt) => trkpt.ele) + ); } catch (e: any) { toast.error(i18n._(e.message, e.message)); return false; diff --git a/website/src/lib/components/toolbar/tools/routing/routing.ts b/website/src/lib/components/toolbar/tools/routing/routing.ts index 3c1bec3..9ab2e45 100644 --- a/website/src/lib/components/toolbar/tools/routing/routing.ts +++ b/website/src/lib/components/toolbar/tools/routing/routing.ts @@ -23,7 +23,10 @@ export const routingProfiles: { [key: string]: RoutingProfile } = { railway: { engine: 'brouter', profile: 'rail' }, }; -export function route(points: Coordinates[]): Promise { +export function route( + points: Coordinates[], + elevations?: (number | undefined)[] +): Promise { if (get(routing)) { const profile = routingProfiles[get(routingProfile)]; if (profile.engine === 'graphhopper') { @@ -32,7 +35,7 @@ export function route(points: Coordinates[]): Promise { return getBRouterRoute(points, profile.profile); } } else { - return getIntermediatePoints(points); + return getIntermediatePoints(points, elevations); } } @@ -267,22 +270,83 @@ function getTags(message: string): { [key: string]: string } { return tags; } -function getIntermediatePoints(points: Coordinates[]): Promise { +// Fills in anchors with unknown elevation (e.g. a point just dragged to a new location) by +// interpolating between the nearest anchors on either side that do have a known elevation, +// weighted by distance along the path. Falls back to extrapolating the nearest known value +// when there is no known anchor on one side. Leaves entries as undefined only when none of the +// anchors have a known elevation at all. +function fillMissingElevations( + points: Coordinates[], + elevations?: (number | undefined)[] +): (number | undefined)[] { + if (!elevations) { + return points.map(() => undefined); + } + + const known = elevations + .map((ele, index) => ({ ele, index })) + .filter((item): item is { ele: number; index: number } => item.ele !== undefined); + + if (known.length === 0) { + return elevations; + } + + const cumulativeDistance = [0]; + for (let i = 1; i < points.length; i++) { + cumulativeDistance.push(cumulativeDistance[i - 1] + distance(points[i - 1], points[i])); + } + + return elevations.map((ele, index) => { + if (ele !== undefined) { + return ele; + } + + const before = known.filter((item) => item.index < index).at(-1); + const after = known.find((item) => item.index > index); + + if (before && after) { + const span = cumulativeDistance[after.index] - cumulativeDistance[before.index]; + const ratio = + span === 0 + ? 0 + : (cumulativeDistance[index] - cumulativeDistance[before.index]) / span; + return before.ele + ratio * (after.ele - before.ele); + } + + return (before ?? after)!.ele; + }); +} + +function getIntermediatePoints( + points: Coordinates[], + elevations?: (number | undefined)[] +): Promise { + elevations = fillMissingElevations(points, elevations); + let route: TrackPoint[] = []; let step = 0.05; for (let i = 0; i < points.length - 1; i++) { // Add intermediate points between each pair of points let dist = distance(points[i], points[i + 1]) / 1000; + let eleStart = elevations?.[i]; + let eleEnd = elevations?.[i + 1]; for (let d = 0; d < dist; d += step) { let lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat); let lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon); + // Interpolate between the known elevations of the surrounding anchor points, + // rather than requesting elevation data for every intermediate point + let ele = + eleStart !== undefined && eleEnd !== undefined + ? eleStart + (d / dist) * (eleEnd - eleStart) + : undefined; route.push( new TrackPoint({ attributes: { lat: lat, lon: lon, }, + ele, }) ); } @@ -294,12 +358,19 @@ function getIntermediatePoints(points: Coordinates[]): Promise { lat: points[points.length - 1].lat, lon: points[points.length - 1].lon, }, + ele: elevations?.[points.length - 1], }) ); + if (route.every((point) => point.ele !== undefined)) { + return Promise.resolve(route); + } + return getElevation(route).then((elevations) => { route.forEach((point, i) => { - point.ele = elevations[i]; + if (point.ele === undefined) { + point.ele = elevations[i]; + } }); return route; });