@mark1russell7/highways 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/visibility.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { dist, inflate, segmentHitsRect, type Point } from "./geom.ts";
1
+ import { dist, inflate, segmentHitsRect, type Box, type Point } from "./geom.ts";
2
2
  import { Grid } from "./grid.ts";
3
3
  import type { Node } from "./model.ts";
4
4
 
@@ -14,14 +14,21 @@ export class Obstacles {
14
14
 
15
15
  /** This method tells if the segment `ab` stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
16
16
  clear(a: Point, b: Point, pad: number, skip?: ReadonlySet<Node>): boolean {
17
- let ok = true;
18
- this.#grid.querySegment(a, b, pad, (n) => {
19
- if (ok && !skip?.has(n)) {
20
- const x = n.box;
21
- if (segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad)) ok = false;
22
- }
17
+ return this.hit(a, b, pad, skip) === undefined;
18
+ }
19
+
20
+ /**
21
+ * This method gives a node whose box, made larger by `pad`, the segment `ab` goes into, or `undefined` when the segment is clear.
22
+ * The nodes in `skip` do not count. The test stops at the first such node.
23
+ */
24
+ hit(a: Point, b: Point, pad: number, skip?: ReadonlySet<Node>): Node | undefined {
25
+ let found: Node | undefined;
26
+ this.#grid.someSegment(a, b, pad, (n) => {
27
+ if (skip?.has(n) || !hits(a, b, n, pad)) return false;
28
+ found = n;
29
+ return true;
23
30
  });
24
- return ok;
31
+ return found;
25
32
  }
26
33
 
27
34
  /** This method tells if the polyline stays out of each box, made larger by `pad`. The nodes in `skip` do not count. */
@@ -30,6 +37,11 @@ export class Obstacles {
30
37
  return true;
31
38
  }
32
39
 
40
+ /** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
41
+ query(box: Box, visit: (n: Node) => void): void {
42
+ this.#grid.query(box, visit);
43
+ }
44
+
33
45
  /** This method tells if the point is inside a box, made larger by `pad`. The node `skip` does not count. */
34
46
  inside(p: Point, pad: number, skip?: Node): boolean {
35
47
  let hit = false;
@@ -43,6 +55,26 @@ export class Obstacles {
43
55
  }
44
56
  }
45
57
 
58
+ /** This function tells if the segment `ab` goes into the box of the node, made larger by `pad`. */
59
+ function hits(a: Point, b: Point, n: Node, pad: number): boolean {
60
+ const x = n.box;
61
+ return segmentHitsRect(a, b, x.x - pad, x.y - pad, x.x + x.w + pad, x.y + x.h + pad);
62
+ }
63
+
64
+ /** A free point at an end of a search of `CornerGraph.pathAmong`, and the cost to reach it. */
65
+ export interface Terminal {
66
+ readonly point: Point;
67
+ readonly cost: number;
68
+ }
69
+
70
+ /** A path of `CornerGraph.pathAmong`: its points, the index of its first point in `from`, the index of its last point in `to`, and its cost. */
71
+ export interface Among {
72
+ readonly points: Point[];
73
+ readonly from: number;
74
+ readonly to: number;
75
+ readonly cost: number;
76
+ }
77
+
46
78
  /** The options of the corner graph. */
47
79
  export interface CornerGraphOptions {
48
80
  /** The distance from a box to its corners. */
@@ -78,9 +110,18 @@ export class CornerGraph {
78
110
  #open = new Int32Array(0);
79
111
  #closed = new Int32Array(0);
80
112
  #stamp = 0;
81
- // The stamp of the last call of nearestVisible that tested each corner.
82
- #tested = new Int32Array(0);
83
- #testStamp = 0;
113
+ // The corners in each bucket of the point grid, by the key of the bucket. A corner is in one bucket: the bucket of its point.
114
+ readonly #buckets = new Map<number, number[]>();
115
+ // The distance from the point of the last call of nearestVisible to each corner that the call found.
116
+ #dist = new Float64Array(0);
117
+ // The corners of one ring of nearestVisible, nearest first.
118
+ readonly #near = new MinHeap();
119
+ // The corners that each end of a path sees, by the coordinates of the end.
120
+ readonly #endCache = new Map<string, number[]>();
121
+ // The corners that each end of a path of `pathAmong` sees. Such a path has several ends, so each end takes fewer corners.
122
+ readonly #fewCache = new Map<string, number[]>();
123
+ // The connected part of the graph of each corner. The first call of `path` makes it.
124
+ #part: Int32Array | undefined;
84
125
 
85
126
  /** This constructor makes the graph of the nodes. */
86
127
  constructor(nodes: readonly Node[], obstacles: Obstacles, options: CornerGraphOptions) {
@@ -136,7 +177,12 @@ export class CornerGraph {
136
177
  }
137
178
 
138
179
  #addPoint(c: Point): void {
139
- this.#pointGrid.add(this.points.length, { x: c.x, y: c.y, w: 0, h: 0 });
180
+ const j = this.points.length;
181
+ this.#pointGrid.add(j, { x: c.x, y: c.y, w: 0, h: 0 });
182
+ const key = bucketKey(Math.floor(c.x / this.#cell), Math.floor(c.y / this.#cell));
183
+ const list = this.#buckets.get(key);
184
+ if (list === undefined) this.#buckets.set(key, [j]);
185
+ else list.push(j);
140
186
  this.points.push(c);
141
187
  }
142
188
 
@@ -167,23 +213,81 @@ export class CornerGraph {
167
213
 
168
214
  /** This method gives at most `k` corners that `p` can see, nearest first. The corner `self` does not count. */
169
215
  nearestVisible(p: Point, k: number, self = -1): number[] {
216
+ return this.#nearestVisible(p, k, self, this.#pad);
217
+ }
218
+
219
+ /**
220
+ * This method tests the corners in rings of radius `r` = 1, 2, 4 and 8 cells. In each ring, it tests the corners
221
+ * within `r` that an earlier ring did not test, nearest first, and equal distances by index.
222
+ * The ring of `r` takes the corners of the buckets that the square of side `2r` around `p` touches.
223
+ * A corner of these buckets beyond `r` waits for the next ring. So the method visits each bucket one time.
224
+ */
225
+ #nearestVisible(p: Point, k: number, self: number, pad: number): number[] {
170
226
  const out: number[] = [];
171
- if (this.#tested.length < this.points.length) this.#tested = new Int32Array(this.points.length);
172
- const tested = this.#tested;
173
- const stamp = ++this.#testStamp;
174
- for (let r = this.#cell; r <= this.#cell * 8; r *= 2) {
175
- const cands: [number, number][] = [];
176
- this.#pointGrid.query({ x: p.x - r, y: p.y - r, w: 2 * r, h: 2 * r }, (j) => {
177
- if (j !== self && tested[j] !== stamp) cands.push([dist(p, this.points[j]!), j]);
178
- });
179
- cands.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
180
- for (const [d, j] of cands) {
181
- // A corner beyond r can be nearer than a corner of the next ring, so it waits for that ring.
182
- if (d > r) break;
183
- tested[j] = stamp;
184
- if (this.#obstacles.clear(p, this.points[j]!, this.#pad)) out.push(j);
185
- if (out.length >= k) return out;
227
+ if (this.#dist.length < this.points.length) this.#dist = new Float64Array(this.points.length);
228
+ const dd = this.#dist;
229
+ const points = this.points;
230
+ const cell = this.#cell;
231
+ const blockers: Node[] = [];
232
+ const now = this.#near;
233
+ // The corners that the rings found beyond their radius. The next ring takes them.
234
+ let wait: number[] = [];
235
+ // The bucket range of the ring before. The first ring has none.
236
+ let pi0 = 1;
237
+ let pi1 = 0;
238
+ let pj0 = 1;
239
+ let pj1 = 0;
240
+ for (let r = cell; r <= cell * 8; r *= 2) {
241
+ // These bounds are the bounds of a query of the box at (p.x - r, p.y - r) with the side 2r.
242
+ const x0 = p.x - r;
243
+ const y0 = p.y - r;
244
+ const i0 = Math.floor(x0 / cell);
245
+ const j0 = Math.floor(y0 / cell);
246
+ const i1 = Math.floor((x0 + 2 * r) / cell);
247
+ const j1 = Math.floor((y0 + 2 * r) / cell);
248
+ const later: number[] = [];
249
+ now.clear();
250
+ for (const j of wait) {
251
+ if (dd[j]! > r) later.push(j);
252
+ else now.add(dd[j]!, j);
253
+ }
254
+ for (let bi = i0; bi <= i1; bi++) {
255
+ const before = bi >= pi0 && bi <= pi1;
256
+ for (let bj = j0; bj <= j1; bj++) {
257
+ if (before && bj >= pj0 && bj <= pj1) continue;
258
+ const list = this.#buckets.get(bucketKey(bi, bj));
259
+ if (list === undefined) continue;
260
+ for (const j of list) {
261
+ if (j === self) continue;
262
+ const d = dist(p, points[j]!);
263
+ dd[j] = d;
264
+ if (d > r) later.push(j);
265
+ else now.add(d, j);
266
+ }
267
+ }
268
+ }
269
+ // The heap gives the corners nearest first, and equal distances by index. A call often stops after a few corners.
270
+ now.build();
271
+ while (now.size > 0) {
272
+ const j = now.pop();
273
+ // A node that stopped an earlier segment often stops this segment too, so the test takes these nodes first.
274
+ // The order of the nodes does not change the result.
275
+ const q = points[j]!;
276
+ let blocked = false;
277
+ for (let b = 0; b < blockers.length && !blocked; b++) blocked = hits(p, q, blockers[b]!, pad);
278
+ if (blocked) continue;
279
+ const hit = this.#obstacles.hit(p, q, pad);
280
+ if (hit !== undefined) blockers.push(hit);
281
+ else {
282
+ out.push(j);
283
+ if (out.length >= k) return out;
284
+ }
186
285
  }
286
+ wait = later;
287
+ pi0 = i0;
288
+ pi1 = i1;
289
+ pj0 = j0;
290
+ pj1 = j1;
187
291
  }
188
292
  return out;
189
293
  }
@@ -196,6 +300,35 @@ export class CornerGraph {
196
300
  */
197
301
  path(from: Point, to: Point, cost?: (i: number, j: number, d: number) => number, floor: number = 1): Point[] | undefined {
198
302
  if (this.#obstacles.clear(from, to, this.#pad)) return [from, to];
303
+ return this.#search([{ point: from, cost: 0 }], [{ point: to, cost: 0 }], cost, floor, this.#k * 2)?.points;
304
+ }
305
+
306
+ /**
307
+ * This method gives the shortest path from a point of `from` to a point of `to`, or `undefined` when the graph has none.
308
+ * The cost of a path includes the costs of its two points. One search finds the path, so this method is faster than a call of `path` for each pair.
309
+ */
310
+ pathAmong(from: readonly Terminal[], to: readonly Terminal[]): Among | undefined {
311
+ const found = this.#search(from, to, undefined, 1, this.#k);
312
+ // A straight line between two points that costs less than the found path is better. The test takes the lines in order of cost.
313
+ const lines: Among[] = [];
314
+ from.forEach((a, i) =>
315
+ to.forEach((b, k) => {
316
+ const c = a.cost + dist(a.point, b.point) + b.cost;
317
+ if (!found || c < found.cost) lines.push({ points: [a.point, b.point], from: i, to: k, cost: c });
318
+ }),
319
+ );
320
+ lines.sort((u, v) => u.cost - v.cost);
321
+ return lines.find((l) => this.#obstacles.clear(l.points[0]!, l.points[1]!, this.#pad)) ?? found;
322
+ }
323
+
324
+ /** This method is the search (A*) of `path` and `pathAmong`. */
325
+ #search(
326
+ from: readonly Terminal[],
327
+ to: readonly Terminal[],
328
+ cost: ((i: number, j: number, d: number) => number) | undefined,
329
+ floor: number,
330
+ count: number,
331
+ ): Among | undefined {
199
332
  const n = this.points.length;
200
333
  if (this.#g.length < n) {
201
334
  this.#g = new Float64Array(n);
@@ -204,28 +337,54 @@ export class CornerGraph {
204
337
  this.#closed = new Int32Array(n);
205
338
  }
206
339
  const stamp = ++this.#stamp;
340
+ const points = this.points;
341
+ // The cost from each corner near a point of `to` to that point, and the index of that point.
207
342
  const goal = new Map<number, number>();
208
- for (const j of this.nearestVisible(to, this.#k * 2)) goal.set(j, dist(this.points[j]!, to));
343
+ const goalOf = new Map<number, number>();
344
+ to.forEach((b, k) => {
345
+ for (const j of this.#ends(b.point, count)) {
346
+ const tail = dist(points[j]!, b.point) + b.cost;
347
+ const now = goal.get(j);
348
+ if (now === undefined || tail < now) {
349
+ goal.set(j, tail);
350
+ goalOf.set(j, k);
351
+ }
352
+ }
353
+ });
209
354
  if (goal.size === 0) return undefined;
355
+ const starts = from.map((a) => this.#ends(a.point, count));
356
+ // Without a start and a goal in one connected part, A* finds no path. This test stops it before it visits the whole part.
357
+ if (!starts.some((list) => this.#joined(list, goal))) return undefined;
210
358
  const heap = new MinHeap();
211
- const h = (i: number): number => floor * dist(this.points[i]!, to);
359
+ const G = this.#g;
360
+ const parentOf = this.#parent;
361
+ const open = this.#open;
362
+ const closed = this.#closed;
363
+ const estimate = (p: Point): number => {
364
+ let h = Infinity;
365
+ for (const b of to) h = Math.min(h, dist(p, b.point));
366
+ return floor * h;
367
+ };
212
368
  const relax = (j: number, g: number, parent: number): void => {
213
- if (this.#closed[j] === stamp) return;
214
- if (this.#open[j] === stamp && this.#g[j]! <= g) return;
215
- this.#open[j] = stamp;
216
- this.#g[j] = g;
217
- this.#parent[j] = parent;
218
- heap.push(g + h(j), j);
369
+ if (closed[j] === stamp) return;
370
+ if (open[j] === stamp && G[j]! <= g) return;
371
+ open[j] = stamp;
372
+ G[j] = g;
373
+ parentOf[j] = parent;
374
+ heap.push(g + estimate(points[j]!), j);
219
375
  };
220
- for (const j of this.nearestVisible(from, this.#k * 2)) relax(j, dist(from, this.points[j]!), -1);
376
+ // The parent of a first corner is -1 - i, where i is the index of its point in `from`.
377
+ from.forEach((a, i) => {
378
+ for (const j of starts[i]!) relax(j, a.cost + dist(a.point, points[j]!), -1 - i);
379
+ });
221
380
  let best = Infinity;
222
381
  let bestEnd = -1;
223
382
  while (heap.size > 0) {
224
383
  if (heap.topKey >= best) break;
225
384
  const i = heap.pop();
226
- if (this.#closed[i] === stamp) continue;
227
- this.#closed[i] = stamp;
228
- const gi = this.#g[i]!;
385
+ if (closed[i] === stamp) continue;
386
+ closed[i] = stamp;
387
+ const gi = G[i]!;
229
388
  const tail = goal.get(i);
230
389
  if (tail !== undefined && gi + tail < best) {
231
390
  best = gi + tail;
@@ -235,15 +394,69 @@ export class CornerGraph {
235
394
  const len = this.#len[i]!;
236
395
  for (let m = 0; m < adj.length; m++) {
237
396
  const j = adj[m]!;
397
+ if (closed[j] === stamp) continue;
238
398
  const d = len[m]!;
239
399
  relax(j, gi + (cost ? cost(i, j, d) : d), i);
240
400
  }
241
401
  }
242
402
  if (bestEnd < 0) return undefined;
243
- const out: Point[] = [to];
244
- for (let i = bestEnd; i >= 0; i = this.#parent[i]!) out.push(this.points[i]!);
245
- out.push(from);
246
- return out.reverse();
403
+ const k = goalOf.get(bestEnd)!;
404
+ const out: Point[] = [to[k]!.point];
405
+ for (let i = bestEnd; ; i = parentOf[i]!) {
406
+ out.push(points[i]!);
407
+ if (parentOf[i]! < 0) {
408
+ const f = -1 - parentOf[i]!;
409
+ out.push(from[f]!.point);
410
+ return { points: out.reverse(), from: f, to: k, cost: best };
411
+ }
412
+ }
413
+ }
414
+
415
+ /** This method tells if a corner of `starts` and a corner of `goal` are in one connected part of the graph. */
416
+ #joined(starts: readonly number[], goal: ReadonlyMap<number, number>): boolean {
417
+ const part = (this.#part ??= this.#parts());
418
+ const goals = new Set<number>();
419
+ for (const j of goal.keys()) goals.add(part[j]!);
420
+ for (const j of starts) if (goals.has(part[j]!)) return true;
421
+ return false;
422
+ }
423
+
424
+ /** This method gives the number of the connected part of each corner. */
425
+ #parts(): Int32Array {
426
+ const part = new Int32Array(this.points.length).fill(-1);
427
+ const stack: number[] = [];
428
+ let count = 0;
429
+ for (let i = 0; i < part.length; i++) {
430
+ if (part[i] !== -1) continue;
431
+ part[i] = count;
432
+ stack.push(i);
433
+ while (stack.length > 0) {
434
+ for (const j of this.#adj[stack.pop()!]!) {
435
+ if (part[j] !== -1) continue;
436
+ part[j] = count;
437
+ stack.push(j);
438
+ }
439
+ }
440
+ count++;
441
+ }
442
+ return part;
443
+ }
444
+
445
+ /**
446
+ * This method gives at most `count` corners that an end of a path can see. An end near a box sees no corner with the clearance.
447
+ * Then the end takes the corners that it sees without the clearance, so the path does not cut through the boxes.
448
+ * The graph does not change, so the method keeps the corners of each end.
449
+ */
450
+ #ends(p: Point, count: number): number[] {
451
+ const cache = count === this.#k * 2 ? this.#endCache : this.#fewCache;
452
+ const key = `${p.x},${p.y}`;
453
+ let out = cache.get(key);
454
+ if (!out) {
455
+ out = this.nearestVisible(p, count);
456
+ if (out.length === 0) out = this.#nearestVisible(p, count, -1, 0);
457
+ cache.set(key, out);
458
+ }
459
+ return out;
247
460
  }
248
461
 
249
462
  /** This method removes the vertices that a later vertex makes needless: from each vertex, it goes to the farthest vertex in view. */
@@ -261,65 +474,91 @@ export class CornerGraph {
261
474
  }
262
475
  }
263
476
 
264
- /** A binary min-heap of (key, value) pairs. Equal keys come out by the smaller value, so the order is deterministic. */
265
- class MinHeap {
266
- readonly #k: number[] = [];
267
- readonly #v: number[] = [];
268
-
269
- get size(): number {
270
- return this.#k.length;
271
- }
272
-
273
- #less(a: number, b: number): boolean {
274
- return this.#k[a]! < this.#k[b]! || (this.#k[a] === this.#k[b] && this.#v[a]! < this.#v[b]!);
275
- }
477
+ /** This function packs the two indices of a bucket into one number. Each index stays within ±2^20. */
478
+ const bucketKey = (i: number, j: number): number => (i + 1_048_576) * 2_097_152 + (j + 1_048_576);
276
479
 
277
- #swap(a: number, b: number): void {
278
- const k = this.#k[a]!;
279
- this.#k[a] = this.#k[b]!;
280
- this.#k[b] = k;
281
- const v = this.#v[a]!;
282
- this.#v[a] = this.#v[b]!;
283
- this.#v[b] = v;
284
- }
480
+ /**
481
+ * A binary min-heap of (key, value) pairs in typed arrays. The pairs come out by key, and equal keys by value, so the order is deterministic.
482
+ * The method `push` adds a pair to the heap. The method `add` only appends a pair, and the method `build` then makes the heap of all the pairs.
483
+ */
484
+ class MinHeap {
485
+ #d = new Float64Array(64);
486
+ #j = new Int32Array(64);
487
+ size = 0;
285
488
 
286
489
  /** The smallest key. */
287
490
  get topKey(): number {
288
- return this.#k[0]!;
491
+ return this.#d[0]!;
289
492
  }
290
493
 
291
494
  push(key: number, value: number): void {
292
- this.#k.push(key);
293
- this.#v.push(value);
294
- let i = this.#k.length - 1;
495
+ this.add(key, value);
496
+ const d = this.#d;
497
+ const jj = this.#j;
498
+ let i = this.size - 1;
295
499
  while (i > 0) {
296
500
  const p = (i - 1) >> 1;
297
- if (!this.#less(i, p)) break;
298
- this.#swap(i, p);
501
+ if (!(key < d[p]! || (key === d[p] && value < jj[p]!))) break;
502
+ d[i] = d[p]!;
503
+ jj[i] = jj[p]!;
299
504
  i = p;
300
505
  }
506
+ d[i] = key;
507
+ jj[i] = value;
508
+ }
509
+
510
+ clear(): void {
511
+ this.size = 0;
512
+ }
513
+
514
+ add(d: number, j: number): void {
515
+ if (this.size === this.#d.length) {
516
+ const dd = new Float64Array(2 * this.size);
517
+ dd.set(this.#d);
518
+ this.#d = dd;
519
+ const jj = new Int32Array(2 * this.size);
520
+ jj.set(this.#j);
521
+ this.#j = jj;
522
+ }
523
+ this.#d[this.size] = d;
524
+ this.#j[this.size] = j;
525
+ this.size++;
526
+ }
527
+
528
+ build(): void {
529
+ for (let i = (this.size >> 1) - 1; i >= 0; i--) this.#down(i);
301
530
  }
302
531
 
303
532
  /** This method removes the pair with the smallest key and gives its value. */
304
533
  pop(): number {
305
- const top = this.#v[0]!;
306
- const k = this.#k.pop()!;
307
- const v = this.#v.pop()!;
308
- if (this.#k.length > 0) {
309
- this.#k[0] = k;
310
- this.#v[0] = v;
311
- let i = 0;
312
- for (;;) {
313
- const l = 2 * i + 1;
314
- const r = l + 1;
315
- let m = i;
316
- if (l < this.#k.length && this.#less(l, m)) m = l;
317
- if (r < this.#k.length && this.#less(r, m)) m = r;
318
- if (m === i) break;
319
- this.#swap(i, m);
320
- i = m;
321
- }
534
+ const top = this.#j[0]!;
535
+ this.size--;
536
+ if (this.size > 0) {
537
+ this.#d[0] = this.#d[this.size]!;
538
+ this.#j[0] = this.#j[this.size]!;
539
+ this.#down(0);
322
540
  }
323
541
  return top;
324
542
  }
543
+
544
+ #down(i: number): void {
545
+ const d = this.#d;
546
+ const jj = this.#j;
547
+ const n = this.size;
548
+ const di = d[i]!;
549
+ const ji = jj[i]!;
550
+ for (;;) {
551
+ const l = 2 * i + 1;
552
+ if (l >= n) break;
553
+ const r = l + 1;
554
+ let m = l;
555
+ if (r < n && (d[r]! < d[l]! || (d[r] === d[l] && jj[r]! < jj[l]!))) m = r;
556
+ if (!(d[m]! < di || (d[m] === di && jj[m]! < ji))) break;
557
+ d[i] = d[m]!;
558
+ jj[i] = jj[m]!;
559
+ i = m;
560
+ }
561
+ d[i] = di;
562
+ jj[i] = ji;
563
+ }
325
564
  }