@mark1russell7/highways 0.5.0 → 0.6.0

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 (56) hide show
  1. package/README.md +47 -2
  2. package/build/index.d.ts +1489 -63
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +10139 -3356
  5. package/build/index.js.map +1 -1
  6. package/package.json +11 -2
  7. package/src/bench/exact/index.ts +218 -11
  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/screen/activation.ts +3 -0
  12. package/src/bench/screen/bounds.ts +140 -0
  13. package/src/bench/screen/compare.ts +26 -0
  14. package/src/bench/screen/exact-set.ts +100 -0
  15. package/src/bench/screen/exercise.ts +290 -0
  16. package/src/bench/screen/forms.ts +14 -0
  17. package/src/bench/screen/gap.ts +316 -0
  18. package/src/bench/screen/hash.ts +7 -76
  19. package/src/bench/screen/index.ts +6 -0
  20. package/src/bench/screen/measures.ts +71 -1
  21. package/src/bench/screen/portfolio-pick.ts +20 -0
  22. package/src/bench/screen/screen.ts +50 -2
  23. package/src/bench/screen/set.ts +3 -2
  24. package/src/highways/areas/index.ts +160 -0
  25. package/src/highways/confluent/index.ts +154 -0
  26. package/src/highways/confluent/k23.test-util.ts +8 -0
  27. package/src/highways/consensus/index.ts +129 -0
  28. package/src/highways/crossbars/index.ts +184 -0
  29. package/src/highways/cubu/index.ts +26 -0
  30. package/src/highways/divided/index.ts +111 -0
  31. package/src/highways/fdeb/index.ts +193 -0
  32. package/src/highways/frechet/index.ts +232 -0
  33. package/src/highways/hierarchy/index.ts +140 -0
  34. package/src/highways/index.ts +64 -0
  35. package/src/highways/kdeeb/index.ts +355 -0
  36. package/src/highways/link-communities/index.ts +189 -0
  37. package/src/highways/mesh/index.ts +209 -0
  38. package/src/highways/mlseb/index.ts +204 -0
  39. package/src/highways/p-hub/index.ts +248 -0
  40. package/src/highways/power/index.ts +117 -0
  41. package/src/highways/resistance/index.ts +159 -0
  42. package/src/highways/sbeb/index.ts +303 -0
  43. package/src/highways/scale-space/index.ts +135 -0
  44. package/src/highways/spectral/index.ts +391 -0
  45. package/src/highways/territories/index.ts +150 -0
  46. package/src/highways/trace.ts +153 -0
  47. package/src/index.ts +3 -0
  48. package/src/portfolio/bands.ts +38 -0
  49. package/src/portfolio/fit.ts +41 -0
  50. package/src/portfolio/index.ts +6 -0
  51. package/src/portfolio/key.ts +98 -0
  52. package/src/portfolio/members.ts +171 -0
  53. package/src/portfolio/router.ts +133 -0
  54. package/src/portfolio/sets.ts +40 -0
  55. package/src/strategies/builtins.ts +222 -1
  56. package/src/strategies/schemas.ts +280 -1
@@ -0,0 +1,193 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import { WayFinder, type WayOptions } from "../../ways.ts";
3
+ import { compatibility } from "../hdbscan/index.ts";
4
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
5
+ import { edgePlugs } from "../stretches.ts";
6
+ import { boxPush, outOfBoxes, tracePlan } from "../trace.ts";
7
+
8
+ /** The options of the highway strategy `fdeb` (HWY-002 with RULE-002 of the notes). */
9
+ export interface FdebOptions extends Pick<WayOptions, "margin" | "k"> {
10
+ /** Two edges attract each other when their compatibility is this value or more. The default is 0.6. */
11
+ readonly compatibility: number;
12
+ /** The spring constant that keeps each edge smooth. The default is 0.1. */
13
+ readonly spring: number;
14
+ /** The number of cycles. Each cycle doubles the points of each edge and halves the step. The default is 5. */
15
+ readonly cycles: number;
16
+ /** The number of steps of the first cycle. Each later cycle has two thirds of the steps of the cycle before it. The default is 50. */
17
+ readonly iterations: number;
18
+ /** The weight of the push of the boxes (RULE-002). The value 0 turns it off. The default is 1. */
19
+ readonly repulsion: number;
20
+ /**
21
+ * The stretch budget of each edge (RULE-002). An edge whose polyline is longer than this value times its straight line gets no pull.
22
+ * `Infinity` sets no budget. The default is 1.25.
23
+ */
24
+ readonly pullLimit: number;
25
+ /** The side of the grid that the points of the edges snap to after the cycles, in units of the board. The default is 8. */
26
+ readonly snap: number;
27
+ /** A highway has this number of edges or more. The default is 2. */
28
+ readonly minSize: number;
29
+ }
30
+
31
+ /** The default options of the strategy `fdeb`. */
32
+ export const FDEB_DEFAULTS: Readonly<Required<Omit<FdebOptions, "margin" | "k">>> = Object.freeze({
33
+ compatibility: 0.6,
34
+ spring: 0.1,
35
+ cycles: 5,
36
+ iterations: 50,
37
+ repulsion: 1,
38
+ pullLimit: 1.25,
39
+ snap: 8,
40
+ minSize: 2,
41
+ });
42
+
43
+ /** The largest number of partners of one edge. The partners with the largest compatibility stay. */
44
+ const PARTNERS = 32;
45
+
46
+ /** The first step of the points, as a share of the median length of the edges. */
47
+ const STEP = 0.04;
48
+
49
+ /** One straight edge: its two ends. */
50
+ interface Segment {
51
+ readonly a: Point;
52
+ readonly b: Point;
53
+ }
54
+
55
+ /** One partner of an edge: its index, its compatibility, and `flip` when it goes the other way. */
56
+ interface Partner {
57
+ readonly j: number;
58
+ readonly c: number;
59
+ readonly flip: boolean;
60
+ }
61
+
62
+ /**
63
+ * This function gives the partners of each edge: the edges with a compatibility of `threshold` or more, at most 32, the largest first.
64
+ * The search reads only the edges whose middles are near enough in x, so it does not test each pair of a large graph.
65
+ */
66
+ function partnersOf(segments: readonly Segment[], threshold: number): Partner[][] {
67
+ const n = segments.length;
68
+ const len = segments.map((s) => dist(s.a, s.b));
69
+ const mid = segments.map((s) => ({ x: (s.a.x + s.b.x) / 2, y: (s.a.y + s.b.y) / 2 }));
70
+ const longest = len.reduce((m, l) => Math.max(m, l), 0);
71
+ const t = Math.max(1e-3, Math.min(1, threshold));
72
+ const order = Array.from({ length: n }, (_, i) => i).sort((i, j) => mid[i]!.x - mid[j]!.x || i - j);
73
+ const found: Partner[][] = Array.from({ length: n }, () => []);
74
+ for (let a = 0; a < n; a++) {
75
+ const i = order[a]!;
76
+ // `Cp >= t` needs a distance of the middles of `avg (1 / t - 1)` or less.
77
+ const reach = ((len[i]! + longest) / 2) * (1 / t - 1);
78
+ for (let b = a + 1; b < n; b++) {
79
+ const j = order[b]!;
80
+ if (mid[j]!.x - mid[i]!.x > reach) break;
81
+ const c = compatibility(segments[i]!.a, segments[i]!.b, segments[j]!.a, segments[j]!.b);
82
+ if (c < t) continue;
83
+ const si = segments[i]!;
84
+ const sj = segments[j]!;
85
+ const flip = (si.b.x - si.a.x) * (sj.b.x - sj.a.x) + (si.b.y - si.a.y) * (sj.b.y - sj.a.y) < 0;
86
+ found[i]!.push({ j, c, flip });
87
+ found[j]!.push({ j: i, c, flip });
88
+ }
89
+ }
90
+ return found.map((list) => list.sort((p, q) => q.c - p.c || p.j - q.j).slice(0, PARTNERS));
91
+ }
92
+
93
+ /** This function gives the length of a polyline. */
94
+ const lengthOf = (p: readonly Point[]): number => {
95
+ let s = 0;
96
+ for (let i = 1; i < p.length; i++) s += dist(p[i - 1]!, p[i]!);
97
+ return s;
98
+ };
99
+
100
+ /** This function gives a polyline with a new point in the middle of each leg. */
101
+ function subdivide(p: readonly Point[]): Point[] {
102
+ const out: Point[] = [p[0]!];
103
+ for (let i = 1; i < p.length; i++) {
104
+ const a = p[i - 1]!;
105
+ const b = p[i]!;
106
+ out.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, b);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * This function gives the polylines of the force-directed edge bundling (HWY-002, Holten and van Wijk 2009), with the box field of RULE-002.
113
+ * Each edge starts as the straight line between its two points out of the plugs. Each cycle doubles its points.
114
+ * In each step, a point moves by `step` times the mean pull to the matching points of its partners. The springs to its two neighbours also move it.
115
+ * The boxes push the points, and after each cycle a point in a box moves out of it. An edge over its stretch budget gets no pull in that step.
116
+ */
117
+ export function fdebLines(input: HighwayInput, finder: WayFinder, o: Readonly<FdebOptions>): Map<string, Point[]> {
118
+ const plugs = edgePlugs(input.graph);
119
+ const segments = plugs.map(({ ps, pt }) => ({ a: finder.out(ps), b: finder.out(pt) }));
120
+ const n = segments.length;
121
+ const out = new Map<string, Point[]>();
122
+ if (n === 0) return out;
123
+ const partners = partnersOf(segments, o.compatibility);
124
+ const lengths = segments.map((s) => dist(s.a, s.b));
125
+ const sorted = [...lengths].sort((a, b) => a - b);
126
+ const median = sorted[Math.floor(sorted.length / 2)]!;
127
+ const pad = finder.margin / 2;
128
+ const reach = finder.margin * 2;
129
+ const budget = o.pullLimit > 0 ? o.pullLimit : Infinity;
130
+ let lines: Point[][] = segments.map((s) => [s.a, s.b]);
131
+ let step = STEP * median;
132
+ let iterations = Math.max(0, Math.round(o.iterations));
133
+ for (let cycle = 0; cycle < Math.max(0, Math.round(o.cycles)); cycle++) {
134
+ lines = lines.map(subdivide);
135
+ const P = lines[0]!.length;
136
+ for (let it = 0; it < iterations; it++) {
137
+ const next = lines.map((l) => [...l]);
138
+ for (let e = 0; e < n; e++) {
139
+ const line = lines[e]!;
140
+ const pull = lengths[e]! > 1e-9 && lengthOf(line) <= budget * lengths[e]!;
141
+ for (let i = 1; i < P - 1; i++) {
142
+ const p = line[i]!;
143
+ let fx = 0;
144
+ let fy = 0;
145
+ if (pull) {
146
+ let w = 0;
147
+ for (const q of partners[e]!) {
148
+ const m = lines[q.j]![q.flip ? P - 1 - i : i]!;
149
+ const d = dist(p, m);
150
+ if (d < 1e-9) continue;
151
+ fx += (q.c * (m.x - p.x)) / d;
152
+ fy += (q.c * (m.y - p.y)) / d;
153
+ w += q.c;
154
+ }
155
+ if (w > 0) {
156
+ fx /= w;
157
+ fy /= w;
158
+ }
159
+ }
160
+ if (o.repulsion > 0) {
161
+ const push = boxPush(finder.obstacles, p, reach);
162
+ fx += o.repulsion * push.x;
163
+ fy += o.repulsion * push.y;
164
+ }
165
+ const a = line[i - 1]!;
166
+ const b = line[i + 1]!;
167
+ const sx = o.spring * ((a.x + b.x) / 2 - p.x);
168
+ const sy = o.spring * ((a.y + b.y) / 2 - p.y);
169
+ next[e]![i] = { x: p.x + step * fx + sx, y: p.y + step * fy + sy };
170
+ }
171
+ }
172
+ lines = next;
173
+ }
174
+ // The hard projection of RULE-002: no point stays in a box.
175
+ lines = lines.map((l) => l.map((p, i) => (i === 0 || i === l.length - 1 ? p : outOfBoxes(finder.obstacles, p, pad))));
176
+ step /= 2;
177
+ iterations = Math.round((iterations * 2) / 3);
178
+ }
179
+ plugs.forEach(({ edge }, e) => out.set(edge.id, lines[e]!));
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * This function gives the highways of the force-directed edge bundling (HWY-002 with RULE-002).
185
+ * The points of the bundled polylines snap to a grid of side `snap`, so the points of one bundle come together.
186
+ * Each edge then goes through its points along the corner graph, and the stretches that `minSize` edges share become highways.
187
+ */
188
+ export function fdebPlan(input: HighwayInput, o: Readonly<FdebOptions>): HighwayPlan {
189
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
190
+ const lines = fdebLines(input, finder, o);
191
+ const waypoints = new Map([...lines].map(([edge, l]) => [edge, l.slice(1, -1)] as const));
192
+ return tracePlan(input, finder, waypoints, { snap: o.snap, minSize: o.minSize, key: "fdeb" });
193
+ }
@@ -0,0 +1,232 @@
1
+ import { boardUnit } from "../../bench/measures/routes.ts";
2
+ import { dist, polylineLength, type Point } from "../../geom.ts";
3
+ import { orderLanes } from "../../segments.ts";
4
+ import { WayFinder, type WayOptions } from "../../ways.ts";
5
+ import { arcPlace, firstRoutes, orderAlongRoutes, tidyPlan } from "../base.ts";
6
+ import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
7
+
8
+ /** The options of the highway strategy `frechet` (HWY-027 of the notes). */
9
+ export interface FrechetOptions extends Pick<WayOptions, "margin" | "k"> {
10
+ /** Two parts of routes are one bundle when their discrete Fréchet distance is this value or less, as a multiple of `boardUnit`. The default is 1. */
11
+ readonly eps: number;
12
+ /** A bundle has this number of edges or more, with the reference edge. The default is 2. */
13
+ readonly minSize: number;
14
+ /** The reference part of a bundle is this length or longer, as a multiple of `boardUnit`. The default is 2. */
15
+ readonly minLength: number;
16
+ /** The distance between two samples of a route, as a multiple of `boardUnit`. The default is 0.5. */
17
+ readonly step: number;
18
+ /** A search starts at each sample of this number along each route. The default is 2. */
19
+ readonly stride: number;
20
+ }
21
+
22
+ /** The default options of the strategy `frechet`. */
23
+ export const FRECHET_DEFAULTS: Readonly<Required<Omit<FrechetOptions, "margin" | "k">>> = Object.freeze({
24
+ eps: 1,
25
+ minSize: 2,
26
+ minLength: 2,
27
+ step: 0.5,
28
+ stride: 2,
29
+ });
30
+
31
+ /** This function gives the points of a polyline at equal distances of `step` or less, with its first and its last point. */
32
+ export function samplesOf(line: readonly Point[], step: number): Point[] {
33
+ if (line.length < 2) return [...line];
34
+ const out: Point[] = [line[0]!];
35
+ for (let i = 1; i < line.length; i++) {
36
+ const a = line[i - 1]!;
37
+ const b = line[i]!;
38
+ const n = Math.max(1, Math.ceil(dist(a, b) / Math.max(1e-9, step)));
39
+ for (let s = 1; s <= n; s++) out.push({ x: a.x + ((b.x - a.x) * s) / n, y: a.y + ((b.y - a.y) * s) / n });
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** The cells of one row of the free space that a monotone path reaches. Each cell is a sorted index of the other curve. */
45
+ type Row = readonly number[];
46
+
47
+ /**
48
+ * This function gives the next row of the free space of the discrete Fréchet distance. The curve `q` has a free start and a free end.
49
+ * A cell `(j, b)` is free when the distance of `p[j]` to `q[b]` is `eps` or less.
50
+ * A path reaches a free cell from `(j - 1, b)`, `(j - 1, b - 1)` or `(j, b - 1)`.
51
+ */
52
+ export function nextRow(prev: Row, pj: Point, q: readonly Point[], eps: number): number[] {
53
+ const out: number[] = [];
54
+ if (prev.length === 0) return out;
55
+ const has = new Set(prev);
56
+ const last = prev[prev.length - 1]!;
57
+ let left = false;
58
+ for (let b = prev[0]!; b < q.length; b++) {
59
+ const reach: boolean = dist(pj, q[b]!) <= eps && (has.has(b) || has.has(b - 1) || left);
60
+ if (reach) out.push(b);
61
+ left = reach;
62
+ if (!reach && b > last) break;
63
+ }
64
+ return out;
65
+ }
66
+
67
+ /**
68
+ * This function tells if a part of `q` has a discrete Fréchet distance of `eps` or less to the whole of `p`. The part of `q` is free to start and to end anywhere.
69
+ * It uses the rows of `nextRow`.
70
+ */
71
+ export function partWithin(p: readonly Point[], q: readonly Point[], eps: number): boolean {
72
+ if (p.length === 0) return true;
73
+ let row: number[] = [];
74
+ for (let b = 0; b < q.length; b++) if (dist(p[0]!, q[b]!) <= eps) row.push(b);
75
+ for (let j = 1; j < p.length && row.length > 0; j++) row = nextRow(row, p[j]!, q, eps);
76
+ return row.length > 0;
77
+ }
78
+
79
+ /** One bundle of the search: the reference edge, the part of its samples, and the other edges with their sense. */
80
+ export interface FrechetBundle {
81
+ readonly edge: string;
82
+ readonly from: number;
83
+ readonly to: number;
84
+ readonly length: number;
85
+ /** The other edges. `back` is true when the edge goes along the reference part in the reverse sense. */
86
+ readonly members: readonly { readonly edge: string; readonly back: boolean }[];
87
+ }
88
+
89
+ /**
90
+ * This function gives the bundles of the search (HWY-027, after Buchin, Buchin, Gudmundsson, Löffler and Luo 2011, in a simple exact form).
91
+ * For each reference route and each start sample (at each `stride`), it grows the reference part one sample at a time.
92
+ * It keeps the other routes, in both senses, with a part at a discrete Fréchet distance of `eps` or less.
93
+ * It stops when fewer than `minSize - 1` routes remain.
94
+ * Each start gives the part with the best score: the number of edges times the length.
95
+ * The search uses the discrete distance on samples only. It does not use the continuous distance or the full free space diagram.
96
+ */
97
+ export function frechetBundles(samples: ReadonlyMap<string, readonly Point[]>, eps: number, minSize: number, minLength: number, stride: number): FrechetBundle[] {
98
+ const edges = [...samples.keys()];
99
+ const curves = new Map<string, readonly Point[]>();
100
+ for (const e of edges) {
101
+ curves.set(`${e}\u0000f`, samples.get(e)!);
102
+ curves.set(`${e}\u0000b`, [...samples.get(e)!].reverse());
103
+ }
104
+ // A grid of the samples of each curve, with cells of the side `eps`, finds the first row of each start.
105
+ const cell = Math.max(1e-6, eps);
106
+ const grid = new Map<string, Set<string>>();
107
+ const cellKey = (p: Point): string => `${Math.floor(p.x / cell)},${Math.floor(p.y / cell)}`;
108
+ for (const [key, c] of curves)
109
+ for (const p of c) {
110
+ const k = cellKey(p);
111
+ let set = grid.get(k);
112
+ if (!set) grid.set(k, (set = new Set()));
113
+ set.add(key);
114
+ }
115
+ const near = (p: Point): string[] => {
116
+ const cx = Math.floor(p.x / cell);
117
+ const cy = Math.floor(p.y / cell);
118
+ const out = new Set<string>();
119
+ for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) for (const k of grid.get(`${cx + dx},${cy + dy}`) ?? []) out.add(k);
120
+ return [...out].sort();
121
+ };
122
+ const need = Math.max(1, minSize - 1);
123
+ const out: FrechetBundle[] = [];
124
+ for (const r of edges) {
125
+ const p = samples.get(r)!;
126
+ const lengths = [0];
127
+ for (let i = 1; i < p.length; i++) lengths.push(lengths[i - 1]! + dist(p[i - 1]!, p[i]!));
128
+ for (let i = 0; i < p.length - 1; i += Math.max(1, Math.round(stride))) {
129
+ let rows = new Map<string, number[]>();
130
+ for (const key of near(p[i]!)) {
131
+ if (key.startsWith(`${r}\u0000`)) continue;
132
+ const q = curves.get(key)!;
133
+ const row: number[] = [];
134
+ q.forEach((s, b) => {
135
+ if (dist(p[i]!, s) <= eps) row.push(b);
136
+ });
137
+ if (row.length > 0) rows.set(key, row);
138
+ }
139
+ let best: FrechetBundle | undefined;
140
+ let bestScore = 0;
141
+ for (let j = i + 1; j < p.length; j++) {
142
+ const next = new Map<string, number[]>();
143
+ for (const [key, row] of rows) {
144
+ const n = nextRow(row, p[j]!, curves.get(key)!, eps);
145
+ if (n.length > 0) next.set(key, n);
146
+ }
147
+ rows = next;
148
+ // One edge counts once: its forward sense wins over its reverse sense.
149
+ const alive = new Map<string, boolean>();
150
+ for (const key of rows.keys()) {
151
+ const [edge, sense] = key.split("\u0000") as [string, string];
152
+ if (!alive.has(edge) || sense === "f") alive.set(edge, sense === "b");
153
+ }
154
+ if (alive.size < need) break;
155
+ const length = lengths[j]! - lengths[i]!;
156
+ const score = (alive.size + 1) * length;
157
+ if (length >= minLength && score > bestScore) {
158
+ bestScore = score;
159
+ best = { edge: r, from: i, to: j, length, members: [...alive].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([edge, back]) => ({ edge, back })) };
160
+ }
161
+ }
162
+ if (best) out.push(best);
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * This function gives the highways of the subtrajectory clusters of the first routes (HWY-027). The samples of each route leave out its two end points.
170
+ * The bundles go in the order of their score, the best first. A bundle keeps the members whose part of the route is not on a bundle already.
171
+ * The spine of a highway is the reference part. Each member rides the whole spine, in its own sense.
172
+ */
173
+ export function frechetPlan(input: HighwayInput, o: Readonly<FrechetOptions>): HighwayPlan {
174
+ const graph = input.graph;
175
+ const unit = boardUnit(graph);
176
+ const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
177
+ const routes = firstRoutes(input);
178
+ const step = Math.max(1e-3, o.step * unit);
179
+ const samples = new Map<string, Point[]>();
180
+ for (const e of graph.edges) {
181
+ const route = routes.get(e.id);
182
+ if (!route || route.length < 4) continue;
183
+ const s = samplesOf(route.slice(1, -1), step);
184
+ if (s.length >= 2) samples.set(e.id, s);
185
+ }
186
+ const minSize = Math.max(2, Math.round(o.minSize));
187
+ const bundles = frechetBundles(samples, Math.max(1e-3, o.eps * unit), minSize, Math.max(finder.margin, o.minLength * unit), o.stride);
188
+ bundles.sort((a, b) => (b.members.length + 1) * b.length - (a.members.length + 1) * a.length || (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : a.from - b.from));
189
+ // The used parts of each route, as places along the route.
190
+ const used = new Map<string, [number, number][]>();
191
+ const free = (edge: string, lo: number, hi: number): boolean => (used.get(edge) ?? []).every(([a, b]) => hi <= a || lo >= b);
192
+ const take = (edge: string, lo: number, hi: number): void => {
193
+ const list = used.get(edge);
194
+ if (list) list.push([lo, hi]);
195
+ else used.set(edge, [[lo, hi]]);
196
+ };
197
+ const highways: Highway[] = [];
198
+ for (const bundle of bundles) {
199
+ const spine = samples.get(bundle.edge)!.slice(bundle.from, bundle.to + 1);
200
+ if (polylineLength(spine) <= finder.margin) continue;
201
+ const own = routes.get(bundle.edge)!;
202
+ const span = (route: readonly Point[]): [number, number] => {
203
+ const a = arcPlace(route, spine[0]!);
204
+ const b = arcPlace(route, spine[spine.length - 1]!);
205
+ return [Math.min(a, b), Math.max(a, b)];
206
+ };
207
+ const [rlo, rhi] = span(own);
208
+ if (!free(bundle.edge, rlo, rhi)) continue;
209
+ const riders = bundle.members.filter((m) => {
210
+ const [lo, hi] = span(routes.get(m.edge)!);
211
+ return free(m.edge, lo, hi);
212
+ });
213
+ if (riders.length + 1 < minSize) continue;
214
+ const all = [{ edge: bundle.edge, back: false }, ...riders];
215
+ for (const m of all) take(m.edge, ...span(routes.get(m.edge)!));
216
+ const last = spine.length - 1;
217
+ const trip = (m: { edge: string; back: boolean }): Point[] => {
218
+ const r = routes.get(m.edge)!;
219
+ const [from, to] = [r[0]!, r[r.length - 1]!];
220
+ return m.back ? [to, ...spine, from] : [from, ...spine, to];
221
+ };
222
+ const lanes = orderLanes(
223
+ spine,
224
+ all.map((m) => ({ edge: m.edge, points: trip(m) })),
225
+ "bundle",
226
+ );
227
+ const back = new Map(all.map((m) => [m.edge, m.back]));
228
+ const members: HighwayMember[] = lanes.map((edge) => (back.get(edge) ? memberAt(spine, edge, last, 0) : memberAt(spine, edge, 0, last)));
229
+ highways.push({ id: `h${highways.length}`, key: `f${highways.length}`, spine, lanes, members });
230
+ }
231
+ return tidyPlan(orderAlongRoutes({ highways }, routes));
232
+ }
@@ -0,0 +1,140 @@
1
+ import { center, type Point } from "../../geom.ts";
2
+ import { cutGroups, kdGroups, quadtreeGroups } from "../../groups/index.ts";
3
+ import type { Section } from "../../sections.ts";
4
+ import { WayFinder, type WayOptions } from "../../ways.ts";
5
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
6
+ import { edgePlugs } from "../stretches.ts";
7
+ import { tracePlan } from "../trace.ts";
8
+
9
+ /** The trees of sections that the strategy `hierarchy` can use. */
10
+ export type HierarchyGroups = "quadtree" | "kd" | "cut";
11
+
12
+ /** The values of `HierarchyGroups`. */
13
+ export const HIERARCHY_GROUPS: readonly HierarchyGroups[] = Object.freeze(["quadtree", "kd", "cut"]);
14
+
15
+ /** The options of the highway strategy `hierarchy` (HWY-001 of the notes). */
16
+ export interface HierarchyOptions extends Pick<WayOptions, "margin" | "k"> {
17
+ /** The tree of sections. The default is `quadtree`. */
18
+ readonly group: HierarchyGroups;
19
+ /** A section with more nodes splits. The default is 8. */
20
+ readonly cap: number;
21
+ /** The pull of the hubs, from 0 to 1 (Holten 2006). The value 0 gives the straight line, and 1 goes through each hub. The default is 0.85. */
22
+ readonly beta: number;
23
+ /** The largest number of hubs on each side of the lowest common section. `Infinity` uses all of them. The default is `Infinity`. */
24
+ readonly hubs: number;
25
+ /** The side of the grid that the hubs snap to, in units of the board. The default is 8. */
26
+ readonly snap: number;
27
+ /** A highway has this number of edges or more. The default is 2. */
28
+ readonly minSize: number;
29
+ }
30
+
31
+ /** The default options of the strategy `hierarchy`. */
32
+ export const HIERARCHY_DEFAULTS: Readonly<Required<Omit<HierarchyOptions, "margin" | "k">>> = Object.freeze({
33
+ group: "quadtree",
34
+ cap: 8,
35
+ beta: 0.85,
36
+ hubs: Infinity,
37
+ snap: 8,
38
+ minSize: 2,
39
+ });
40
+
41
+ /** This function gives the tree of sections of the option `group`. */
42
+ function treeOf(input: HighwayInput, group: HierarchyGroups, cap: number): Section {
43
+ const c = Math.max(1, Math.round(cap));
44
+ if (group === "kd") return kdGroups(input.graph, { cap: c });
45
+ if (group === "cut") return cutGroups(input.graph, { cap: c });
46
+ return quadtreeGroups(input.graph, { cap: c });
47
+ }
48
+
49
+ /**
50
+ * This function gives the hub of each section of the tree: the mean of the middles of the boxes of its nodes.
51
+ * A hub in a box moves to the nearest corner of the corner graph.
52
+ */
53
+ function hubsOf(input: HighwayInput, root: Section, finder: WayFinder): Map<string, Point> {
54
+ const middles = input.graph.nodes.map((n) => center(n.box));
55
+ const hubs = new Map<string, Point>();
56
+ const visit = (s: Section): void => {
57
+ if (s.nodes.length > 0) {
58
+ let x = 0;
59
+ let y = 0;
60
+ for (const i of s.nodes) {
61
+ x += middles[i]!.x;
62
+ y += middles[i]!.y;
63
+ }
64
+ let hub = { x: x / s.nodes.length, y: y / s.nodes.length };
65
+ if (finder.obstacles.inside(hub, finder.margin / 2)) {
66
+ const j = finder.corners.nearest(hub);
67
+ if (j >= 0) hub = finder.corners.points[j]!;
68
+ }
69
+ hubs.set(s.id, hub);
70
+ }
71
+ for (const c of s.children) visit(c);
72
+ };
73
+ visit(root);
74
+ return hubs;
75
+ }
76
+
77
+ /** This function gives the chain of sections from the root to the leaf of each node, by the index of the node. */
78
+ function chainsOf(root: Section, count: number): (readonly Section[])[] {
79
+ const chains: Section[][] = Array.from({ length: count }, () => []);
80
+ const visit = (s: Section, path: Section[]): void => {
81
+ const here = [...path, s];
82
+ if (s.children.length === 0) for (const i of s.nodes) chains[i] = here;
83
+ for (const c of s.children) visit(c, here);
84
+ };
85
+ visit(root, []);
86
+ return chains;
87
+ }
88
+
89
+ /**
90
+ * This function gives the control points of one edge. They are the sections from the leaf of the source up to the lowest common section, then down to the target leaf.
91
+ * The lowest common section is not in the chain (Holten 2006), so the edges do not all meet at the hub of the root.
92
+ * `hubs` limits the sections on each side. The sections nearest to the common section stay.
93
+ */
94
+ export function controlChain(from: readonly Section[], to: readonly Section[], hubs: number): Section[] {
95
+ let lca = 0;
96
+ while (lca < from.length && lca < to.length && from[lca]!.id === to[lca]!.id) lca++;
97
+ // The first sections that differ are at the depth `lca`.
98
+ const limit = Number.isFinite(hubs) ? Math.max(0, Math.round(hubs)) : Infinity;
99
+ const up = from.slice(lca, lca + limit).reverse();
100
+ const down = to.slice(lca, lca + limit);
101
+ return [...up, ...down];
102
+ }
103
+
104
+ /**
105
+ * This function gives the highways of the hierarchical edge bundling (HWY-001, Holten 2006).
106
+ * The tree of sections gives a hub to each section. Each edge goes near the hubs of the sections between its two leaves.
107
+ * The waypoint of a hub is `beta hub + (1 - beta) p`, with `p` the point of the straight line at the same share of the chain.
108
+ * The waypoints snap to a grid of side `snap`, and the ways go through the corner graph. The stretches that `minSize` ways share become highways.
109
+ */
110
+ export function hierarchyPlan(input: HighwayInput, o: Readonly<HierarchyOptions>): HighwayPlan {
111
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
112
+ const index = new Map(input.graph.nodes.map((n, i) => [n.id, i] as const));
113
+ const waypoints = new Map<string, Point[]>();
114
+ if (input.graph.nodes.length > 0) {
115
+ const root = treeOf(input, o.group, o.cap);
116
+ const hubs = hubsOf(input, root, finder);
117
+ const chains = chainsOf(root, input.graph.nodes.length);
118
+ const beta = Math.max(0, Math.min(1, o.beta));
119
+ for (const { edge, ps, pt } of edgePlugs(input.graph)) {
120
+ const s = index.get(edge.source.node);
121
+ const t = index.get(edge.target.node);
122
+ if (s === undefined || t === undefined) continue;
123
+ const chain = controlChain(chains[s] ?? [], chains[t] ?? [], o.hubs)
124
+ .map((c) => hubs.get(c.id))
125
+ .filter((p): p is Point => p !== undefined);
126
+ if (chain.length === 0) continue;
127
+ const a = finder.out(ps);
128
+ const b = finder.out(pt);
129
+ waypoints.set(
130
+ edge.id,
131
+ chain.map((h, i) => {
132
+ const u = (i + 1) / (chain.length + 1);
133
+ const line = { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u };
134
+ return { x: beta * h.x + (1 - beta) * line.x, y: beta * h.y + (1 - beta) * line.y };
135
+ }),
136
+ );
137
+ }
138
+ }
139
+ return tracePlan(input, finder, waypoints, { snap: o.snap, minSize: o.minSize, key: "hierarchy" });
140
+ }
@@ -34,4 +34,68 @@ export { HOUGH_DEFAULTS, houghLines, houghPeaks, houghPlan, type HoughOptions, t
34
34
  export { densityRidges, lineDensity, RIDGE_DEFAULTS, ridgeLines, ridgePlan, type DensityImage, type Ridge, type RidgeOptions, type RidgeSource } from "./ridges/index.ts";
35
35
  export { compatibility, EDGE_DISTANCES, HDBSCAN_DEFAULTS, hdbscanClusters, hdbscanLabels, hdbscanPlan, type EdgeDistance, type HdbscanOptions } from "./hdbscan/index.ts";
36
36
  export { ARC_SOURCES, candidateArcs, HUB_ARC_DEFAULTS, hubArcPlan, type ArcSource, type HubArcOptions } from "./hub-arcs/index.ts";
37
+ export { boxPush, innerPoints, outOfBoxes, tracePlan, waypointWay, type TraceOptions } from "./trace.ts";
38
+ export { DIVIDED_DEFAULTS, divideHighway, dividedPlan, offsetLine, TRAFFIC_RULES, type DividedOptions, type TrafficRule } from "./divided/index.ts";
39
+ export { controlChain, HIERARCHY_DEFAULTS, HIERARCHY_GROUPS, hierarchyPlan, type HierarchyGroups, type HierarchyOptions } from "./hierarchy/index.ts";
40
+ export { FDEB_DEFAULTS, fdebLines, fdebPlan, type FdebOptions } from "./fdeb/index.ts";
41
+ export { boxUnit, clusters1d, MESH_DEFAULTS, meshPlan, meshWaypoints, type MeshOptions } from "./mesh/index.ts";
42
+ export {
43
+ DENSITY_KERNELS,
44
+ densityPlan,
45
+ fft,
46
+ KDEEB_DEFAULTS,
47
+ kdeebLines,
48
+ kdeebPlan,
49
+ pointDensity,
50
+ type AdvectionSettings,
51
+ type DensityGrid,
52
+ type DensityKernel,
53
+ type DensitySource,
54
+ type KdeebOptions,
55
+ } from "./kdeeb/index.ts";
56
+ export { CUBU_DEFAULTS, cubuPlan, type CubuOptions } from "./cubu/index.ts";
57
+ export { MLS_FITS, MLSEB_DEFAULTS, mlsebLines, mlsebPlan, mlsPoint, type MlsebOptions, type MlsFit } from "./mlseb/index.ts";
58
+ export {
59
+ bigGroups,
60
+ firstLines,
61
+ meanLine,
62
+ nearestOnLine,
63
+ nearestSkeleton,
64
+ SBEB_DEFAULTS,
65
+ sbebLines,
66
+ sbebPlan,
67
+ shapeSkeleton,
68
+ SKELETON_GROUPS,
69
+ SKELETON_TARGETS,
70
+ skeletonGroups,
71
+ type SbebOptions,
72
+ type ShapeSkeleton,
73
+ type SkeletonGroups,
74
+ type SkeletonTarget,
75
+ } from "./sbeb/index.ts";
76
+ export { AREA_DEFAULTS, AREA_WIDTHS, areaPlan, skeletonPath, type AreaOptions, type AreaWidth } from "./areas/index.ts";
77
+ export { bicliqueGroups, CONFLUENT_DEFAULTS, confluentPlan, GROUP_SCOPES, greedyBicliques, nodeLinks, type Biclique, type ConfluentOptions, type GroupScope, type NodeLinks } from "./confluent/index.ts";
78
+ export { jaccardIndex, MODULE_RULES, modulesOf, POWER_DEFAULTS, powerPlan, type ModuleRule, type PowerOptions } from "./power/index.ts";
79
+ export { CROSSBAR_DEFAULTS, crossbarPlan, greedyCrossbars, routeCrossings, type Crossbar, type CrossbarOptions, type RouteCrossing } from "./crossbars/index.ts";
80
+ export { FRECHET_DEFAULTS, frechetBundles, frechetPlan, nextRow, partWithin, samplesOf, type FrechetBundle, type FrechetOptions } from "./frechet/index.ts";
81
+ export {
82
+ compatibilityMatrix,
83
+ edgeBlocks,
84
+ edgeEnds,
85
+ kMeans,
86
+ labelGroups,
87
+ SPECTRAL_DEFAULTS,
88
+ spectralClusters,
89
+ spectralLabels,
90
+ spectralPlan,
91
+ symmetricEigen,
92
+ type EdgeEnds,
93
+ type SpectralOptions,
94
+ } from "./spectral/index.ts";
95
+ export { edgeResistance, RESISTANCE_DEFAULTS, resistanceClusters, resistancePlan, type ResistanceOptions } from "./resistance/index.ts";
96
+ export { LINK_COMMUNITY_DEFAULTS, linkCommunities, linkCommunityGroups, linkCommunityPlan, type LinkCommunityOptions, type LinkEdge } from "./link-communities/index.ts";
97
+ export { cellSteps, nearestCentre, pointGroups, TERRITORY_DEFAULTS, TERRITORY_POINTS, territoryPlan, type CellStep, type TerritoryOptions, type TerritoryPoints } from "./territories/index.ts";
98
+ export { coAssociation, CONSENSUS_DEFAULTS, CONSENSUS_LINKAGES, consensusGroups, consensusLabels, consensusPlan, type ConsensusLinkage, type ConsensusOptions } from "./consensus/index.ts";
99
+ export { HUB_ALLOCATIONS, HUB_CANDIDATES, hubCandidates, P_HUB_DEFAULTS, pHubPlan, pMedian, type HubAllocation, type HubCandidates, type PHubOptions } from "./p-hub/index.ts";
100
+ export { SCALE_SPACE_DEFAULTS, scaleClusters, scaleSpaceGroups, scaleSpacePlan, type ScaleCluster, type ScaleSpaceOptions } from "./scale-space/index.ts";
37
101
  export * from "./gates/index.ts";