@mark1russell7/highways 0.4.2 → 0.5.1

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.
Files changed (78) hide show
  1. package/README.md +59 -2
  2. package/build/index.d.ts +2333 -193
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +13042 -2334
  5. package/build/index.js.map +1 -1
  6. package/package.json +12 -1
  7. package/src/bench/exact/index.ts +258 -0
  8. package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
  9. package/src/bench/exact/optima-exact-v1.json +4809 -0
  10. package/src/bench/exact/solve.ts +129 -0
  11. package/src/bench/measures/index.ts +1 -0
  12. package/src/bench/measures/lanes.ts +304 -0
  13. package/src/bench/measures/levels.ts +86 -2
  14. package/src/bench/screen/activation.ts +3 -0
  15. package/src/bench/screen/bounds.ts +140 -0
  16. package/src/bench/screen/compare.ts +26 -0
  17. package/src/bench/screen/exact-set.ts +100 -0
  18. package/src/bench/screen/exercise.ts +290 -0
  19. package/src/bench/screen/forms.ts +25 -5
  20. package/src/bench/screen/gap.ts +316 -0
  21. package/src/bench/screen/hash.ts +7 -76
  22. package/src/bench/screen/index.ts +6 -0
  23. package/src/bench/screen/measures.ts +89 -1
  24. package/src/bench/screen/portfolio-pick.ts +20 -0
  25. package/src/bench/screen/screen.ts +78 -2
  26. package/src/bench/screen/set.ts +3 -2
  27. package/src/channels.ts +56 -0
  28. package/src/highways/areas/index.ts +160 -0
  29. package/src/highways/confluent/index.ts +154 -0
  30. package/src/highways/confluent/k23.test-util.ts +8 -0
  31. package/src/highways/consensus/index.ts +129 -0
  32. package/src/highways/crossbars/index.ts +184 -0
  33. package/src/highways/cubu/index.ts +26 -0
  34. package/src/highways/diffusion/index.ts +103 -0
  35. package/src/highways/divided/index.ts +111 -0
  36. package/src/highways/edge-paths/index.ts +155 -0
  37. package/src/highways/fdeb/index.ts +193 -0
  38. package/src/highways/fixtures.test-util.ts +71 -0
  39. package/src/highways/flow-clusters/index.ts +143 -0
  40. package/src/highways/frechet/index.ts +232 -0
  41. package/src/highways/growth/index.ts +148 -0
  42. package/src/highways/hdbscan/index.ts +250 -0
  43. package/src/highways/hierarchy/index.ts +140 -0
  44. package/src/highways/hough/index.ts +182 -0
  45. package/src/highways/hub-arcs/index.ts +181 -0
  46. package/src/highways/index.ts +79 -0
  47. package/src/highways/kdeeb/index.ts +355 -0
  48. package/src/highways/lanes/index.ts +189 -0
  49. package/src/highways/link-communities/index.ts +189 -0
  50. package/src/highways/mesh/index.ts +209 -0
  51. package/src/highways/mingle/index.ts +171 -0
  52. package/src/highways/mlseb/index.ts +204 -0
  53. package/src/highways/nested/index.ts +143 -0
  54. package/src/highways/p-hub/index.ts +248 -0
  55. package/src/highways/power/index.ts +117 -0
  56. package/src/highways/resistance/index.ts +159 -0
  57. package/src/highways/ridges/index.ts +328 -0
  58. package/src/highways/sbeb/index.ts +303 -0
  59. package/src/highways/scale-space/index.ts +135 -0
  60. package/src/highways/snap.ts +110 -0
  61. package/src/highways/soft-keys/index.ts +114 -0
  62. package/src/highways/spectral/index.ts +391 -0
  63. package/src/highways/spiral/index.ts +205 -0
  64. package/src/highways/territories/index.ts +150 -0
  65. package/src/highways/trace.ts +153 -0
  66. package/src/highways/traclus/index.ts +296 -0
  67. package/src/index.ts +2 -0
  68. package/src/portfolio/bands.ts +33 -0
  69. package/src/portfolio/fit.ts +29 -0
  70. package/src/portfolio/index.ts +6 -0
  71. package/src/portfolio/key.ts +98 -0
  72. package/src/portfolio/members.ts +171 -0
  73. package/src/portfolio/router.ts +133 -0
  74. package/src/portfolio/sets.ts +28 -0
  75. package/src/routers/shared.ts +56 -3
  76. package/src/strategies/builtins.ts +376 -1
  77. package/src/strategies/schemas.ts +460 -1
  78. package/src/visibility.ts +5 -0
@@ -0,0 +1,355 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import { WayFinder, type WayOptions } from "../../ways.ts";
3
+ import { firstRoutes } from "../base.ts";
4
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
5
+ import { resample } from "../quickbundles/index.ts";
6
+ import { edgePlugs } from "../stretches.ts";
7
+ import { outOfBoxes, tracePlan } from "../trace.ts";
8
+
9
+ /**
10
+ * The way to compute the density from the sample points. `direct` adds a Gaussian at each sample point (KDEEB).
11
+ * `separable` counts the points in the cells and blurs the counts along x, then along y (CUBu). `fft` blurs the counts with a product of Fourier transforms (FFTEB).
12
+ */
13
+ export type DensityKernel = "direct" | "separable" | "fft";
14
+
15
+ /** The values of `DensityKernel`. */
16
+ export const DENSITY_KERNELS: readonly DensityKernel[] = Object.freeze(["direct", "separable", "fft"]);
17
+
18
+ /** The lines that make the first density: the straight lines of the edges, or their first routes. */
19
+ export type DensitySource = "lines" | "routes";
20
+
21
+ /** The options of the highway strategy `kdeeb` (RULE-028 with HWY-006 of the notes). */
22
+ export interface KdeebOptions extends Pick<WayOptions, "margin" | "k"> {
23
+ /** The lines of the first density. The default is `lines`. */
24
+ readonly source: DensitySource;
25
+ /** The side of a cell of the density image, in units of the board. The default is 8. A large board gets larger cells, up to 512 cells on a side. */
26
+ readonly cell: number;
27
+ /** The width of the kernel at the first step, in units of the board. The default is 24. */
28
+ readonly bandwidth: number;
29
+ /** Each step multiplies the width of the kernel by this value. The default is 0.7. */
30
+ readonly decay: number;
31
+ /** The number of steps. The default is 10. */
32
+ readonly steps: number;
33
+ /** The share of the Laplacian smoothing of each polyline after each step, from 0 to 1. The default is 0.5. */
34
+ readonly smoothing: number;
35
+ /** With this option, the density is 0 in the boxes, so no ridge forms over a box (RULE-028). The default is `true`. */
36
+ readonly mask: boolean;
37
+ /** The side of the grid that the points of the edges snap to after the steps, in units of the board. The default is 8. */
38
+ readonly snap: number;
39
+ /** A highway has this number of edges or more. The default is 2. */
40
+ readonly minSize: number;
41
+ }
42
+
43
+ /** The default options of the strategy `kdeeb`. */
44
+ export const KDEEB_DEFAULTS: Readonly<Required<Omit<KdeebOptions, "margin" | "k">>> = Object.freeze({
45
+ source: "lines",
46
+ cell: 8,
47
+ bandwidth: 24,
48
+ decay: 0.7,
49
+ steps: 10,
50
+ smoothing: 0.5,
51
+ mask: true,
52
+ snap: 8,
53
+ minSize: 2,
54
+ });
55
+
56
+ /** The settings of the advection that the strategies `kdeeb` and `cubu` share. */
57
+ export interface AdvectionSettings {
58
+ /** The way to compute the density. */
59
+ readonly kernel: DensityKernel;
60
+ /** The spacing of the sample points after each step, in units of the board. The value 0 keeps the points where they are. */
61
+ readonly resample: number;
62
+ }
63
+
64
+ /** A grid of cells over the board: its size, the side of a cell, and the place of its first cell. */
65
+ export interface DensityGrid {
66
+ readonly w: number;
67
+ readonly h: number;
68
+ readonly cell: number;
69
+ readonly x0: number;
70
+ readonly y0: number;
71
+ }
72
+
73
+ /** This function gives a Gaussian kernel of the deviation `sigma` cells, from `-r` to `r` with `r = ceil(3 sigma)`, with a sum of 1. */
74
+ function kernel1d(sigma: number): Float64Array {
75
+ const s = Math.max(1e-6, sigma);
76
+ const r = Math.ceil(3 * s);
77
+ const k = new Float64Array(2 * r + 1);
78
+ let sum = 0;
79
+ for (let i = -r; i <= r; i++) sum += k[i + r] = Math.exp(-(i * i) / (2 * s * s));
80
+ for (let i = 0; i < k.length; i++) k[i] = k[i]! / sum;
81
+ return k;
82
+ }
83
+
84
+ /** This function counts the points in the cells of the grid. A point adds 1 to the cell that holds it. */
85
+ function splat(points: readonly Point[], g: DensityGrid): Float64Array {
86
+ const raw = new Float64Array(g.w * g.h);
87
+ for (const p of points) {
88
+ const x = Math.floor((p.x - g.x0) / g.cell);
89
+ const y = Math.floor((p.y - g.y0) / g.cell);
90
+ if (x >= 0 && y >= 0 && x < g.w && y < g.h) raw[y * g.w + x] = raw[y * g.w + x]! + 1;
91
+ }
92
+ return raw;
93
+ }
94
+
95
+ /** This function gives the density of the direct kernel: each point adds a Gaussian of the deviation `sigma` cells to the cells near it. */
96
+ function directDensity(points: readonly Point[], g: DensityGrid, sigma: number): Float64Array {
97
+ const rho = new Float64Array(g.w * g.h);
98
+ const s = Math.max(1e-6, sigma);
99
+ const r = Math.ceil(3 * s);
100
+ const norm = 1 / (2 * Math.PI * s * s);
101
+ for (const p of points) {
102
+ const cx = (p.x - g.x0) / g.cell - 0.5;
103
+ const cy = (p.y - g.y0) / g.cell - 0.5;
104
+ const ix = Math.round(cx);
105
+ const iy = Math.round(cy);
106
+ for (let y = Math.max(0, iy - r); y <= Math.min(g.h - 1, iy + r); y++)
107
+ for (let x = Math.max(0, ix - r); x <= Math.min(g.w - 1, ix + r); x++) {
108
+ const d2 = (x - cx) ** 2 + (y - cy) ** 2;
109
+ rho[y * g.w + x] = rho[y * g.w + x]! + norm * Math.exp(-d2 / (2 * s * s));
110
+ }
111
+ }
112
+ return rho;
113
+ }
114
+
115
+ /** This function gives the density of the separable kernel: the counts of the cells blurred along x, then along y. */
116
+ function separableDensity(points: readonly Point[], g: DensityGrid, sigma: number): Float64Array {
117
+ const raw = splat(points, g);
118
+ const k = kernel1d(sigma);
119
+ const r = (k.length - 1) / 2;
120
+ const tmp = new Float64Array(g.w * g.h);
121
+ for (let y = 0; y < g.h; y++)
122
+ for (let x = 0; x < g.w; x++) {
123
+ let v = 0;
124
+ for (let i = -r; i <= r; i++) {
125
+ const xi = x + i;
126
+ if (xi >= 0 && xi < g.w) v += raw[y * g.w + xi]! * k[i + r]!;
127
+ }
128
+ tmp[y * g.w + x] = v;
129
+ }
130
+ const rho = new Float64Array(g.w * g.h);
131
+ for (let y = 0; y < g.h; y++)
132
+ for (let x = 0; x < g.w; x++) {
133
+ let v = 0;
134
+ for (let i = -r; i <= r; i++) {
135
+ const yi = y + i;
136
+ if (yi >= 0 && yi < g.h) v += tmp[yi * g.w + x]! * k[i + r]!;
137
+ }
138
+ rho[y * g.w + x] = v;
139
+ }
140
+ return rho;
141
+ }
142
+
143
+ /** This function gives the fast Fourier transform of a row of `n` complex values in place, with `n` a power of 2. `inverse` gives the inverse without the division by `n`. */
144
+ export function fft(re: Float64Array, im: Float64Array, inverse = false): void {
145
+ const n = re.length;
146
+ for (let i = 1, j = 0; i < n; i++) {
147
+ let bit = n >> 1;
148
+ for (; j & bit; bit >>= 1) j ^= bit;
149
+ j ^= bit;
150
+ if (i < j) {
151
+ [re[i], re[j]] = [re[j]!, re[i]!];
152
+ [im[i], im[j]] = [im[j]!, im[i]!];
153
+ }
154
+ }
155
+ for (let len = 2; len <= n; len <<= 1) {
156
+ const ang = ((inverse ? 2 : -2) * Math.PI) / len;
157
+ const wr = Math.cos(ang);
158
+ const wi = Math.sin(ang);
159
+ for (let i = 0; i < n; i += len) {
160
+ let cr = 1;
161
+ let ci = 0;
162
+ for (let k = 0; k < len / 2; k++) {
163
+ const ar = re[i + k]!;
164
+ const ai = im[i + k]!;
165
+ const br = re[i + k + len / 2]! * cr - im[i + k + len / 2]! * ci;
166
+ const bi = re[i + k + len / 2]! * ci + im[i + k + len / 2]! * cr;
167
+ re[i + k] = ar + br;
168
+ im[i + k] = ai + bi;
169
+ re[i + k + len / 2] = ar - br;
170
+ im[i + k + len / 2] = ai - bi;
171
+ const t = cr * wr - ci * wi;
172
+ ci = cr * wi + ci * wr;
173
+ cr = t;
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ /** This function gives the 2D transform of a grid of `W` by `H` complex values in place. It transforms each row, then each column. */
180
+ function fft2(re: Float64Array, im: Float64Array, W: number, H: number, inverse: boolean): void {
181
+ const rr = new Float64Array(W);
182
+ const ri = new Float64Array(W);
183
+ for (let y = 0; y < H; y++) {
184
+ for (let x = 0; x < W; x++) {
185
+ rr[x] = re[y * W + x]!;
186
+ ri[x] = im[y * W + x]!;
187
+ }
188
+ fft(rr, ri, inverse);
189
+ for (let x = 0; x < W; x++) {
190
+ re[y * W + x] = rr[x]!;
191
+ im[y * W + x] = ri[x]!;
192
+ }
193
+ }
194
+ const cr = new Float64Array(H);
195
+ const ci = new Float64Array(H);
196
+ for (let x = 0; x < W; x++) {
197
+ for (let y = 0; y < H; y++) {
198
+ cr[y] = re[y * W + x]!;
199
+ ci[y] = im[y * W + x]!;
200
+ }
201
+ fft(cr, ci, inverse);
202
+ for (let y = 0; y < H; y++) {
203
+ re[y * W + x] = cr[y]!;
204
+ im[y * W + x] = ci[y]!;
205
+ }
206
+ }
207
+ }
208
+
209
+ /** This function gives the smallest power of 2 that is `n` or more. */
210
+ const pow2 = (n: number): number => {
211
+ let p = 1;
212
+ while (p < n) p <<= 1;
213
+ return p;
214
+ };
215
+
216
+ /**
217
+ * This function gives the density of the kernel `fft`: `rho = IFFT(FFT(counts) FFT(K))` (FFTEB). The grid gets zeros up to a power of 2.
218
+ * The zeros are as wide as the kernel, so the product does not wrap around.
219
+ */
220
+ function fftDensity(points: readonly Point[], g: DensityGrid, sigma: number): Float64Array {
221
+ const raw = splat(points, g);
222
+ const r = Math.ceil(3 * Math.max(1e-6, sigma));
223
+ const W = pow2(g.w + r);
224
+ const H = pow2(g.h + r);
225
+ const are = new Float64Array(W * H);
226
+ const aim = new Float64Array(W * H);
227
+ for (let y = 0; y < g.h; y++) for (let x = 0; x < g.w; x++) are[y * W + x] = raw[y * g.w + x]!;
228
+ const k = kernel1d(sigma);
229
+ const kre = new Float64Array(W * H);
230
+ const kim = new Float64Array(W * H);
231
+ for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) kre[((dy + H) % H) * W + ((dx + W) % W)] = k[dx + r]! * k[dy + r]!;
232
+ fft2(are, aim, W, H, false);
233
+ fft2(kre, kim, W, H, false);
234
+ for (let i = 0; i < W * H; i++) {
235
+ const a = are[i]!;
236
+ const b = aim[i]!;
237
+ are[i] = a * kre[i]! - b * kim[i]!;
238
+ aim[i] = a * kim[i]! + b * kre[i]!;
239
+ }
240
+ fft2(are, aim, W, H, true);
241
+ const rho = new Float64Array(g.w * g.h);
242
+ const scale = 1 / (W * H);
243
+ for (let y = 0; y < g.h; y++) for (let x = 0; x < g.w; x++) rho[y * g.w + x] = Math.max(0, are[y * W + x]! * scale);
244
+ return rho;
245
+ }
246
+
247
+ /** This function gives the density of the points on the grid with the kernel `kernel` of the width `bandwidth` in units of the board. */
248
+ export function pointDensity(points: readonly Point[], g: DensityGrid, bandwidth: number, kernel: DensityKernel): Float64Array {
249
+ const sigma = bandwidth / g.cell;
250
+ if (kernel === "direct") return directDensity(points, g, sigma);
251
+ if (kernel === "fft") return fftDensity(points, g, sigma);
252
+ return separableDensity(points, g, sigma);
253
+ }
254
+
255
+ /** This function gives the value of a grid at a point of the board, with a bilinear mix of the four nearest cells. */
256
+ function sample(rho: Float64Array, g: DensityGrid, p: Point): number {
257
+ const fx = (p.x - g.x0) / g.cell - 0.5;
258
+ const fy = (p.y - g.y0) / g.cell - 0.5;
259
+ const x = Math.floor(fx);
260
+ const y = Math.floor(fy);
261
+ const u = fx - x;
262
+ const v = fy - y;
263
+ const at = (i: number, j: number): number => (i < 0 || j < 0 || i >= g.w || j >= g.h ? 0 : rho[j * g.w + i]!);
264
+ return at(x, y) * (1 - u) * (1 - v) + at(x + 1, y) * u * (1 - v) + at(x, y + 1) * (1 - u) * v + at(x + 1, y + 1) * u * v;
265
+ }
266
+
267
+ /**
268
+ * This function gives the polylines of the density bundling (KDEEB, Hurter, Ersoy and Telea 2012). The ends of each polyline stay.
269
+ * In each step, the sample points of all edges make a density. Each inner point moves `h / 4` up the gradient of the density, with `h` the width of the kernel.
270
+ * The width then shrinks by `decay`, so the edges gather into ridges. A Laplacian smoothing keeps each polyline smooth.
271
+ * With `mask`, the density is 0 in the boxes (RULE-028), and a point in a box moves out of it.
272
+ */
273
+ export function kdeebLines(input: HighwayInput, finder: WayFinder, o: Readonly<KdeebOptions>, settings: Readonly<AdvectionSettings>): Map<string, Point[]> {
274
+ const plugs = edgePlugs(input.graph);
275
+ const out = new Map<string, Point[]>();
276
+ if (plugs.length === 0) return out;
277
+ const routes = o.source === "routes" ? firstRoutes(input) : undefined;
278
+ const spacing = Math.max(1e-3, o.cell);
279
+ let lines: Point[][] = plugs.map(({ edge, ps, pt }) => {
280
+ const a = finder.out(ps);
281
+ const b = finder.out(pt);
282
+ const route = routes?.get(edge.id);
283
+ const base = route && route.length > 2 ? [a, ...route.slice(1, -1), b] : [a, b];
284
+ let len = 0;
285
+ for (let i = 1; i < base.length; i++) len += dist(base[i - 1]!, base[i]!);
286
+ return resample(base, Math.max(2, Math.min(129, Math.ceil(len / spacing) + 1)));
287
+ });
288
+ let x0 = Infinity;
289
+ let y0 = Infinity;
290
+ let x1 = -Infinity;
291
+ let y1 = -Infinity;
292
+ for (const n of input.graph.nodes) {
293
+ x0 = Math.min(x0, n.box.x);
294
+ y0 = Math.min(y0, n.box.y);
295
+ x1 = Math.max(x1, n.box.x + n.box.w);
296
+ y1 = Math.max(y1, n.box.y + n.box.h);
297
+ }
298
+ const pad = 3 * Math.max(0, o.bandwidth);
299
+ const cell = Math.max(spacing, (x1 - x0 + 2 * pad) / 512, (y1 - y0 + 2 * pad) / 512);
300
+ const g: DensityGrid = { cell, x0: x0 - pad, y0: y0 - pad, w: Math.max(1, Math.ceil((x1 - x0 + 2 * pad) / cell)), h: Math.max(1, Math.ceil((y1 - y0 + 2 * pad) / cell)) };
301
+ const boxPad = finder.margin / 2;
302
+ // The mask: the cells whose middle is in a box get no density.
303
+ const masked = new Uint8Array(g.w * g.h);
304
+ if (o.mask)
305
+ for (let y = 0; y < g.h; y++)
306
+ for (let x = 0; x < g.w; x++) if (finder.obstacles.inside({ x: g.x0 + (x + 0.5) * cell, y: g.y0 + (y + 0.5) * cell }, boxPad)) masked[y * g.w + x] = 1;
307
+ const smooth = Math.max(0, Math.min(1, o.smoothing));
308
+ let h = Math.max(1e-3, o.bandwidth);
309
+ for (let step = 0; step < Math.max(0, Math.round(o.steps)); step++) {
310
+ const points = lines.flatMap((l) => l.slice(1, -1));
311
+ const rho = pointDensity(points, g, h, settings.kernel);
312
+ if (o.mask) for (let i = 0; i < rho.length; i++) if (masked[i]) rho[i] = 0;
313
+ const eta = h / 4;
314
+ const d = cell;
315
+ lines = lines.map((l) => {
316
+ const moved = l.map((p, i) => {
317
+ if (i === 0 || i === l.length - 1) return p;
318
+ const gx = sample(rho, g, { x: p.x + d, y: p.y }) - sample(rho, g, { x: p.x - d, y: p.y });
319
+ const gy = sample(rho, g, { x: p.x, y: p.y + d }) - sample(rho, g, { x: p.x, y: p.y - d });
320
+ const n = Math.hypot(gx, gy);
321
+ return n > 1e-12 ? { x: p.x + (eta * gx) / n, y: p.y + (eta * gy) / n } : p;
322
+ });
323
+ const smoothed = moved.map((p, i) => {
324
+ if (i === 0 || i === moved.length - 1 || smooth === 0) return p;
325
+ const a = moved[i - 1]!;
326
+ const b = moved[i + 1]!;
327
+ return { x: (1 - smooth) * p.x + (smooth * (a.x + b.x)) / 2, y: (1 - smooth) * p.y + (smooth * (a.y + b.y)) / 2 };
328
+ });
329
+ const free = o.mask ? smoothed.map((p, i) => (i === 0 || i === smoothed.length - 1 ? p : outOfBoxes(finder.obstacles, p, boxPad))) : smoothed;
330
+ if (settings.resample <= 0) return free;
331
+ let len = 0;
332
+ for (let i = 1; i < free.length; i++) len += dist(free[i - 1]!, free[i]!);
333
+ return resample(free, Math.max(2, Math.min(129, Math.ceil(len / settings.resample) + 1)));
334
+ });
335
+ h *= Math.max(0, o.decay);
336
+ }
337
+ plugs.forEach(({ edge }, e) => out.set(edge.id, lines[e]!));
338
+ return out;
339
+ }
340
+
341
+ /** This function gives the highways of the bundled polylines of `kdeebLines`. The points snap to a grid of side `snap`, and the ways go through the corner graph. */
342
+ export function densityPlan(input: HighwayInput, o: Readonly<KdeebOptions>, settings: Readonly<AdvectionSettings>, key: string): HighwayPlan {
343
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
344
+ const lines = kdeebLines(input, finder, o, settings);
345
+ const waypoints = new Map([...lines].map(([edge, l]) => [edge, l.slice(1, -1)] as const));
346
+ return tracePlan(input, finder, waypoints, { snap: o.snap, minSize: o.minSize, key });
347
+ }
348
+
349
+ /**
350
+ * This function gives the highways of the density bundling with a mask of the free space (RULE-028 with HWY-006).
351
+ * The kernel is direct: each sample point adds a Gaussian. The final ridges of the polylines become the highways.
352
+ */
353
+ export function kdeebPlan(input: HighwayInput, o: Readonly<KdeebOptions>): HighwayPlan {
354
+ return densityPlan(input, o, { kernel: "direct", resample: 0 }, "kdeeb");
355
+ }
@@ -0,0 +1,189 @@
1
+ import { channelWidth } from "../../channels.ts";
2
+ import { dist, polylineLength, type Point } from "../../geom.ts";
3
+ import type { Graph } from "../../model.ts";
4
+ import { WayFinder } from "../../ways.ts";
5
+ import { basePlan, nearestPlace, tidyPlan, type HighwayBase } from "../base.ts";
6
+ import { memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
7
+
8
+ /** The options of the highway strategy `lanes` (HWY-051 of the note on highways). */
9
+ export interface LanesOptions {
10
+ /** The distance from a box to the corners of the graph. The default is 5. */
11
+ readonly margin?: number;
12
+ /** The number of visible neighbours of each corner. The default is 8. */
13
+ readonly k?: number;
14
+ /** The plan that the strategy changes. The default is `shared-ways`. */
15
+ readonly from: HighwayBase;
16
+ /** The gap between two lanes. The default is 4. */
17
+ readonly laneGap: number;
18
+ /** The largest width of a highway. The default is 32. */
19
+ readonly maxWidth: number;
20
+ /** The number of times that a highway can split, one half after the other. The default is 3. */
21
+ readonly depth: number;
22
+ }
23
+
24
+ /** The default options of the strategy `lanes`. */
25
+ export const LANES_DEFAULTS: Readonly<LanesOptions> = Object.freeze({ from: "shared-ways", laneGap: 4, maxWidth: 32, depth: 3 });
26
+
27
+ /** The cost factor of a link that is too narrow for the lanes, for the way of the second half. */
28
+ const NARROW_COST = 8;
29
+
30
+ /** This function gives the number of lanes of a highway on each leg of its spine. A lane is a member whose part on the spine holds the leg. */
31
+ function lanesOnLegs(h: Highway): number[] {
32
+ const legs = new Array<number>(Math.max(0, h.spine.length - 1)).fill(0);
33
+ for (const m of h.members) {
34
+ const lo = Math.min(m.on, m.off);
35
+ const hi = Math.max(m.on, m.off);
36
+ for (let i = 0; i < legs.length; i++) if (lo < i + 1 && hi > i) legs[i]!++;
37
+ }
38
+ return legs;
39
+ }
40
+
41
+ /** This function gives the width of `n` lanes. */
42
+ const widthOf = (n: number, o: Readonly<LanesOptions>): number => Math.min(o.maxWidth, n * o.laneGap);
43
+
44
+ /**
45
+ * This function gives the number of legs of the spines of a plan where the lanes do not fit in the channel.
46
+ * A leg with two lanes or more needs `min(maxWidth, lanes × laneGap)`. The channel is the free space across the leg (`channelWidth`).
47
+ */
48
+ export function channelOverflows(graph: Graph, plan: HighwayPlan, o: Readonly<Pick<LanesOptions, "laneGap" | "maxWidth" | "margin" | "k">>): number {
49
+ const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
50
+ const full = { from: "none" as const, depth: 0, ...o };
51
+ let n = 0;
52
+ for (const h of plan.highways) {
53
+ const legs = lanesOnLegs(h);
54
+ legs.forEach((count, i) => {
55
+ if (count > 1 && channelWidth(finder.obstacles, h.spine[i]!, h.spine[i + 1]!) < widthOf(count, full)) n++;
56
+ });
57
+ }
58
+ return n;
59
+ }
60
+
61
+ /**
62
+ * This function gives the plan of `from`, with each highway that does not fit in its channels split in two (HWY-051).
63
+ * The lanes split into two halves by lane order. The first half keeps the spine. The second half gets a new spine through the corners, where the narrow links cost more.
64
+ * A highway with fewer than four lanes moves as one to the new spine.
65
+ * A half that still does not fit splits again, up to `depth` times. A new spine needs a shorter length of narrow legs than the old spine.
66
+ */
67
+ export function lanesPlan(input: HighwayInput, o: Readonly<LanesOptions>): HighwayPlan {
68
+ const plan = basePlan(input, o.from);
69
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
70
+ const corners = finder.corners;
71
+ const widths = new Map<string, number>();
72
+ const channel = (a: Point, b: Point): number => {
73
+ const key = a.x < b.x || (a.x === b.x && a.y <= b.y) ? `${a.x},${a.y},${b.x},${b.y}` : `${b.x},${b.y},${a.x},${a.y}`;
74
+ let w = widths.get(key);
75
+ if (w === undefined) widths.set(key, (w = channelWidth(finder.obstacles, a, b)));
76
+ return w;
77
+ };
78
+ const narrowLegs = (h: Highway): number[] => {
79
+ const out: number[] = [];
80
+ lanesOnLegs(h).forEach((count, i) => {
81
+ if (count > 1 && channel(h.spine[i]!, h.spine[i + 1]!) < widthOf(count, o)) out.push(i);
82
+ });
83
+ return out;
84
+ };
85
+ // The length of the legs of a spine that are too narrow for `need`.
86
+ const tight = (spine: readonly Point[], need: number): number => spine.reduce((n, p, i) => (i > 0 && channel(spine[i - 1]!, p) < need ? n + dist(spine[i - 1]!, p) : n), 0);
87
+
88
+ const pad = finder.margin / 2;
89
+ const reach = 2 * (o.k ?? 8);
90
+ /**
91
+ * This function gives the cheapest way from `from` to `to` through the corners (Dijkstra). A link narrower than `need` costs `NARROW_COST` times its length.
92
+ * Unlike `corners.path`, it also prices the first link and the last link, and it does not take the straight line without a price.
93
+ */
94
+ const detour = (from: Point, to: Point, need: number): Point[] | undefined => {
95
+ const pts: Point[] = [from, to];
96
+ const id = (j: number): number => j + 2;
97
+ const pointOf = (n: number): Point => (n < 2 ? pts[n]! : corners.points[n - 2]!);
98
+ const best = new Map<number, number>([[0, 0]]);
99
+ const parent = new Map<number, number>();
100
+ const done = new Set<number>();
101
+ const heap: [number, number][] = [[0, 0]];
102
+ const push = (c: number, n: number): void => {
103
+ heap.push([c, n]);
104
+ for (let i = heap.length - 1; i > 0; ) {
105
+ const up = (i - 1) >> 1;
106
+ if (heap[up]![0] <= heap[i]![0]) break;
107
+ [heap[up], heap[i]] = [heap[i]!, heap[up]!];
108
+ i = up;
109
+ }
110
+ };
111
+ const pop = (): [number, number] => {
112
+ const top = heap[0]!;
113
+ const last = heap.pop()!;
114
+ if (heap.length > 0) {
115
+ heap[0] = last;
116
+ for (let i = 0; ; ) {
117
+ const l = 2 * i + 1;
118
+ const r = l + 1;
119
+ let m = i;
120
+ if (l < heap.length && heap[l]![0] < heap[m]![0]) m = l;
121
+ if (r < heap.length && heap[r]![0] < heap[m]![0]) m = r;
122
+ if (m === i) break;
123
+ [heap[m], heap[i]] = [heap[i]!, heap[m]!];
124
+ i = m;
125
+ }
126
+ }
127
+ return top;
128
+ };
129
+ while (heap.length > 0 && done.size < 20_000) {
130
+ const [c, n] = pop();
131
+ if (done.has(n)) continue;
132
+ done.add(n);
133
+ if (n === 1) break;
134
+ const p = pointOf(n);
135
+ const next = corners.nearestVisible(p, reach, n >= 2 ? n - 2 : -1).map(id);
136
+ if (finder.obstacles.clear(p, to, pad)) next.push(1);
137
+ for (const m of next) {
138
+ if (done.has(m)) continue;
139
+ const q = pointOf(m);
140
+ const d = dist(p, q);
141
+ const g = c + (channel(p, q) < need ? d * NARROW_COST : d);
142
+ if (g < (best.get(m) ?? Infinity)) {
143
+ best.set(m, g);
144
+ parent.set(m, n);
145
+ push(g, m);
146
+ }
147
+ }
148
+ }
149
+ if (!done.has(1)) return undefined;
150
+ const out: Point[] = [];
151
+ for (let n: number | undefined = 1; n !== undefined; n = parent.get(n)) out.push(pointOf(n));
152
+ return out.reverse();
153
+ };
154
+
155
+ // This function moves the members to a new spine. A member whose two places meet on the new spine leaves the highway.
156
+ const move = (h: Highway, spine: readonly Point[], edges: readonly string[]): Highway => {
157
+ const byEdge = new Map(h.members.map((m) => [m.edge, m]));
158
+ const members: HighwayMember[] = [];
159
+ for (const edge of edges) {
160
+ const m = byEdge.get(edge)!;
161
+ const on = nearestPlace(spine, pointAt(h.spine, m.on));
162
+ const off = nearestPlace(spine, pointAt(h.spine, m.off));
163
+ if (Math.abs(on - off) < 1e-6) continue;
164
+ members.push({ ...memberAt(spine, edge, on, off), seq: m.seq });
165
+ }
166
+ return { ...h, spine, lanes: members.map((m) => m.edge), members };
167
+ };
168
+
169
+ const split = (h: Highway, depth: number): Highway[] => {
170
+ if (depth <= 0 || narrowLegs(h).length === 0) return [h];
171
+ // A highway with fewer than four lanes cannot give two halves of two lanes. All its lanes move to the new spine.
172
+ const half = h.lanes.length < 4 ? 0 : Math.ceil(h.lanes.length / 2);
173
+ const first = h.lanes.slice(0, half);
174
+ const second = h.lanes.slice(half);
175
+ const need = widthOf(second.length, o);
176
+ const from = h.spine[0]!;
177
+ const to = h.spine[h.spine.length - 1]!;
178
+ const raw = detour(from, to, need);
179
+ if (!raw) return [h];
180
+ const pulled = corners.pull(raw);
181
+ const spine = tight(pulled, need) <= tight(raw, need) ? pulled : raw;
182
+ if (spine.length < 2 || polylineLength(spine) <= finder.margin || tight(spine, need) >= tight(h.spine, need)) return [h];
183
+ const moved = move(h, spine, second);
184
+ if (first.length === 0) return split(moved, depth - 1);
185
+ return [...split(move(h, h.spine, first), depth - 1), ...split(moved, depth - 1)];
186
+ };
187
+
188
+ return tidyPlan({ highways: plan.highways.flatMap((h) => split(h, Math.max(0, Math.round(o.depth)))) });
189
+ }