From 45b9f0723834aa754068c2cd906fc9cdf32bfd03 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 18 Oct 2026 22:35:33 +0000 Subject: [PATCH] fix(tours): a lake leg along the shore stays along it when it starts at the bank WATER-005 found the shore style crossing straight over like the direct one. Pulling a cell path straight allowed a shortcut through water as costly as its dearer end, and since the clearance ramp a cell at the bank costs up to five times, so a leg from a river mouth lifted that ceiling above the open water and was pulled across the lake. A shortcut is now held to the cost of the open water alone (the style's band and the boat's reach); how near the bank it may come was already held by the path it replaces. The search costs are unchanged. Measured on the test lake and a larger one with a bay and a spit: direct, canoe and SUP legs are exactly as before; along the shore the leg no longer crosses (median 98 to 161 m off the bank, was up to 500), and no more samples on land than before. --- .../nest/paddle-routing/engine/lake-field.ts | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/server/src/nest/paddle-routing/engine/lake-field.ts b/server/src/nest/paddle-routing/engine/lake-field.ts index c2c0395d5..9a01d4a14 100644 --- a/server/src/nest/paddle-routing/engine/lake-field.ts +++ b/server/src/nest/paddle-routing/engine/lake-field.ts @@ -63,17 +63,33 @@ interface Grid { weight: (cell: number) => number; } +/** With `shore`, what open water costs on top: nothing up to SHORE_NEAR_M, rising to SHORE_WEIGHT at SHORE_FAR_M. */ +function bandOf(shore: number, profile: LakeProfile): number { + if (profile.style !== 'shore') return 0; + return SHORE_WEIGHT * Math.min(1, Math.max(0, (shore - SHORE_NEAR_M) / (SHORE_FAR_M - SHORE_NEAR_M))); +} + function weightOf(shore: number, profile: LakeProfile): number { let weight = 1; const clearance = CLEARANCE_M[profile.style]; if (shore < clearance) weight += CLEARANCE_WEIGHT * (1 - shore / clearance) ** 2; - if (profile.style === 'shore') { - weight += SHORE_WEIGHT * Math.min(1, Math.max(0, (shore - SHORE_NEAR_M) / (SHORE_FAR_M - SHORE_NEAR_M))); - } + weight += bandOf(shore, profile); if (shore > profile.maxCrossingM / 2) weight *= OVER_LIMIT_WEIGHT; return weight; } +/** + * {@link weightOf} without the clearance: what the water costs for how far + * out it lies. A shortcut is held to this, and to the path's own distance + * from the shore (see `pull`), so a leg that starts or ends at the bank, as + * every one from a river does, cannot lift the ceiling with the bank's cost + * and pull a route along the shore straight across the open water. + */ +function openWeightOf(shore: number, profile: LakeProfile): number { + const weight = 1 + bandOf(shore, profile); + return shore > profile.maxCrossingM / 2 ? weight * OVER_LIMIT_WEIGHT : weight; +} + function step(grid: Grid, at: number, each: (next: number, length: number) => void): void { const { width, height, cell } = grid; const col = at % width; @@ -244,11 +260,6 @@ export class LakeField { return d; } - /** What crossing a cell costs per metre, for this profile. */ - weight(cell: number, profile: LakeProfile): number { - return weightOf(this.shoreDistance()[cell] as number, profile); - } - private fine(profile: LakeProfile): Grid { const shore = this.shoreDistance(); return { @@ -322,8 +333,8 @@ export class LakeField { /** * The line from one cell to another as a paddler would take it: the * cheapest cell path, then pulled straight wherever a straight line stays on - * water no more costly than the cells it joins and no nearer the shore than - * the path it replaces. + * open water no more costly than the cells it joins and no nearer the shore + * than the path it replaces. */ path(from: number, to: number, profile: LakeProfile): Array<[number, number]> | null { const cells = astar(this.fine(profile), from, to); @@ -356,25 +367,26 @@ export class LakeField { } /** - * Whether a straight line between two cells runs on water no more costly - * than its ends and nowhere nearer the shore than `floor` (less half a cell, - * for the line's rounding onto cells). + * Whether a straight line between two cells runs on open water no more + * costly than its ends ({@link openWeightOf}) and nowhere nearer the shore + * than `floor` (less half a cell, for the line's rounding onto cells). */ private clear(a: number, b: number, profile: LakeProfile, floor: number): boolean { const shore = this.shoreDistance(); + const open = (cell: number) => openWeightOf(shore[cell] as number, profile); const least = floor - this.lake.cell / 2; const { width } = this.lake; const ac = a % width; const ar = (a - ac) / width; const bc = b % width; const br = (b - bc) / width; - const ceiling = Math.max(this.weight(a, profile), this.weight(b, profile)) * 1.05; + const ceiling = Math.max(open(a), open(b)) * 1.05; const steps = Math.max(Math.abs(bc - ac), Math.abs(br - ar)) * 2; for (let s = 1; s < steps; s++) { const c = Math.round(ac + ((bc - ac) * s) / steps); const r = Math.round(ar + ((br - ar) * s) / steps); const cell = r * width + c; - if (!this.isWater(cell) || this.weight(cell, profile) > ceiling || (shore[cell] as number) < least) return false; + if (!this.isWater(cell) || open(cell) > ceiling || (shore[cell] as number) < least) return false; } return true; }