mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(tours): a lake shortcut costs no more than the stretch of path it replaces
A shortcut was held to the dearer of its two end cells, so an end out on open water lifted the ceiling just as a bank end did before: a leg from a point out on a lake was pulled straight across, through water the search had gone round, for a SUP beyond its reach and for the shore style beyond its band. The same happened midway once a kept vertex lay in water past the boat's reach. A shortcut is now priced along its line on open water and taken only when it costs no more than the stretch of cell path it replaces (with 5 % to spare); the shore distance check stays as it was. Measured on the WATER-005 lake and a larger one with a bay and a spit: legs from bank to bank are as before within a few metres, except a kayak no longer crosses 409 m of water past its reach that the search avoided; from a point 500 m out a SUP covers 321 m past its reach (300 m cannot be avoided) instead of 1301 m, a canoe 133 m instead of 1132 m, and the shore style follows the bank. WATER-012 holds the case from a point out on the lake.
This commit is contained in:
@@ -80,10 +80,11 @@ function weightOf(shore: number, profile: LakeProfile): number {
|
||||
|
||||
/**
|
||||
* {@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.
|
||||
* out it lies. A shortcut is priced with this against the stretch of path it
|
||||
* replaces, and held to that stretch's distance from the shore (see `pull`),
|
||||
* so neither a leg's end at the bank, as every one from a river has, nor one
|
||||
* out on the lake lets a route the search kept along the shore, or within a
|
||||
* boat's reach, be pulled straight across the open water.
|
||||
*/
|
||||
function openWeightOf(shore: number, profile: LakeProfile): number {
|
||||
const weight = 1 + bandOf(shore, profile);
|
||||
@@ -333,8 +334,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
|
||||
* open water no more costly than the cells it joins and no nearer the shore
|
||||
* than the path it replaces.
|
||||
* water, costs no more than the stretch it replaces and comes no nearer the
|
||||
* shore than that stretch.
|
||||
*/
|
||||
path(from: number, to: number, profile: LakeProfile): Array<[number, number]> | null {
|
||||
const cells = astar(this.fine(profile), from, to);
|
||||
@@ -343,20 +344,34 @@ export class LakeField {
|
||||
}
|
||||
|
||||
/**
|
||||
* Straightens a cell path. A shortcut may not run nearer the shore than the
|
||||
* stretch of path it replaces (up to the style's clearance): pulling a line
|
||||
* taut round a bend otherwise draws it onto the bank the path kept off.
|
||||
* Straightens a cell path. A shortcut may cost no more on open water than
|
||||
* the stretch of path it replaces, and may not run nearer the shore than
|
||||
* that stretch (up to the style's clearance): pulling a line taut otherwise
|
||||
* draws it onto the bank the path kept off, or straight over the water the
|
||||
* search went round.
|
||||
*/
|
||||
private pull(cells: number[], profile: LakeProfile): number[] {
|
||||
const shore = this.shoreDistance();
|
||||
const clearance = CLEARANCE_M[profile.style];
|
||||
const depth = (cell: number) => Math.min(clearance, shore[cell] as number);
|
||||
const open = (cell: number) => openWeightOf(shore[cell] as number, profile);
|
||||
const { width, cell: size } = this.lake;
|
||||
// What the path costs on open water from its first cell up to each cell.
|
||||
const along = [0];
|
||||
for (let i = 1; i < cells.length; i++) {
|
||||
const from = cells[i - 1] as number;
|
||||
const to = cells[i] as number;
|
||||
const step =
|
||||
Math.abs((to % width) - (from % width)) + Math.abs(Math.floor(to / width) - Math.floor(from / width));
|
||||
along.push((along[i - 1] as number) + ((open(from) + open(to)) / 2) * size * (step === 2 ? Math.SQRT2 : 1));
|
||||
}
|
||||
const kept = [cells[0] as number];
|
||||
let anchor = 0;
|
||||
let floor = depth(cells[0] as number);
|
||||
for (let i = 2; i < cells.length; i++) {
|
||||
floor = Math.min(floor, depth(cells[i - 1] as number), depth(cells[i] as number));
|
||||
if (!this.clear(cells[anchor] as number, cells[i] as number, profile, floor)) {
|
||||
const budget = ((along[i] as number) - (along[anchor] as number)) * 1.05;
|
||||
if (!this.clear(cells[anchor] as number, cells[i] as number, profile, floor, budget)) {
|
||||
kept.push(cells[i - 1] as number);
|
||||
anchor = i - 1;
|
||||
floor = Math.min(depth(cells[i - 1] as number), depth(cells[i] as number));
|
||||
@@ -367,11 +382,12 @@ export class LakeField {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Whether a straight line between two cells runs on water, costs no more
|
||||
* than `budget` on open water ({@link openWeightOf}) and comes 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 {
|
||||
private clear(a: number, b: number, profile: LakeProfile, floor: number, budget: number): boolean {
|
||||
const shore = this.shoreDistance();
|
||||
const open = (cell: number) => openWeightOf(shore[cell] as number, profile);
|
||||
const least = floor - this.lake.cell / 2;
|
||||
@@ -380,13 +396,16 @@ export class LakeField {
|
||||
const ar = (a - ac) / width;
|
||||
const bc = b % width;
|
||||
const br = (b - bc) / width;
|
||||
const ceiling = Math.max(open(a), open(b)) * 1.05;
|
||||
const steps = Math.max(Math.abs(bc - ac), Math.abs(br - ar)) * 2;
|
||||
// Each sample stands for its share of the line; the two ends for half a share each.
|
||||
const share = (Math.hypot(bc - ac, br - ar) * this.lake.cell) / steps;
|
||||
let cost = ((open(a) + open(b)) / 2) * share;
|
||||
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) || open(cell) > ceiling || (shore[cell] as number) < least) return false;
|
||||
cost += open(cell) * share;
|
||||
if (!this.isWater(cell) || cost > budget || (shore[cell] as number) < least) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,35 @@ function ok(leg: PaddleRouteLeg): Extract<PaddleRouteLeg, { status: 'ok' }> {
|
||||
|
||||
const kinds = (leg: PaddleRouteLeg) => [...new Set(ok(leg).segments.map((segment) => segment.kind))];
|
||||
|
||||
/** In from the north, a lake 1 km wide and 3 km long (the Woblitz), out to the south. */
|
||||
function woblitz(): WaterWorld {
|
||||
const world = new WaterWorld();
|
||||
world.line(
|
||||
[
|
||||
[0, 6000],
|
||||
[0, 4000],
|
||||
],
|
||||
{ waterway: 'river', name: 'Havel' },
|
||||
);
|
||||
world.ring(
|
||||
[
|
||||
[-500, 4000],
|
||||
[500, 4000],
|
||||
[500, 1000],
|
||||
[-500, 1000],
|
||||
],
|
||||
{ natural: 'water', name: 'Woblitz' },
|
||||
);
|
||||
world.line(
|
||||
[
|
||||
[0, 1000],
|
||||
[0, -1000],
|
||||
],
|
||||
{ waterway: 'river', name: 'Havel' },
|
||||
);
|
||||
return world;
|
||||
}
|
||||
|
||||
/** A river 6 km long, flowing south, its centre line at east 0, a node every 100 m (node `k` at north 6000 - 100k). */
|
||||
function river(world: WaterWorld, tags: Record<string, string> = {}) {
|
||||
return world.line(
|
||||
@@ -166,31 +195,7 @@ describe('water routing from an extract (PAD-11)', () => {
|
||||
});
|
||||
|
||||
it('WATER-005: crosses a lake straight over, along its shore when asked, and a SUP keeps to the shore anyway', async () => {
|
||||
const world = new WaterWorld();
|
||||
// In from the north, a lake 1 km wide and 3 km long, out to the south.
|
||||
world.line(
|
||||
[
|
||||
[0, 6000],
|
||||
[0, 4000],
|
||||
],
|
||||
{ waterway: 'river', name: 'Havel' },
|
||||
);
|
||||
world.ring(
|
||||
[
|
||||
[-500, 4000],
|
||||
[500, 4000],
|
||||
[500, 1000],
|
||||
[-500, 1000],
|
||||
],
|
||||
{ natural: 'water', name: 'Woblitz' },
|
||||
);
|
||||
world.line(
|
||||
[
|
||||
[0, 1000],
|
||||
[0, -1000],
|
||||
],
|
||||
{ waterway: 'river', name: 'Havel' },
|
||||
);
|
||||
const world = woblitz();
|
||||
const { router, stats } = await built(world);
|
||||
expect(stats.lakes).toBe(1);
|
||||
|
||||
@@ -207,6 +212,20 @@ describe('water routing from an extract (PAD-11)', () => {
|
||||
expect(router.route(world.at(200, 2500), to, KAYAK).status).toBe('ok');
|
||||
});
|
||||
|
||||
it('WATER-012: from a point out on a lake a SUP makes for the shore and the shore style follows it, neither drawn straight across', async () => {
|
||||
const world = woblitz();
|
||||
const { router } = await built(world);
|
||||
// 500 m off both side banks: past a SUP's reach of open water, and beyond the shore style's band.
|
||||
const from = world.at(0, 2500);
|
||||
const to = world.at(0, 0);
|
||||
const direct = ok(router.route(from, to, KAYAK));
|
||||
expect(direct.distance_m).toBeLessThan(2550);
|
||||
expect(ok(router.route(from, to, { ...KAYAK, lakeStyle: 'shore' })).distance_m).toBeGreaterThan(
|
||||
direct.distance_m + 200,
|
||||
);
|
||||
expect(ok(router.route(from, to, SUP)).distance_m).toBeGreaterThan(direct.distance_m + 200);
|
||||
});
|
||||
|
||||
it('WATER-006: goes round an island of a multipolygon lake, not across it', async () => {
|
||||
const world = new WaterWorld();
|
||||
const outer = world.ring([
|
||||
|
||||
Reference in New Issue
Block a user