@mark1russell7/highways 0.4.1 → 0.5.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.
- package/README.md +43 -1
- package/build/index.d.ts +1377 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8202 -1515
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/exact/index.ts +197 -0
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +2 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/forms.ts +11 -5
- package/src/bench/screen/measures.ts +23 -0
- package/src/bench/screen/screen.ts +42 -0
- package/src/channels.ts +56 -0
- package/src/highways/base.ts +126 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/flow-field/index.ts +49 -0
- package/src/highways/gates/ambiguity/index.ts +110 -0
- package/src/highways/gates/breakeven/index.ts +62 -0
- package/src/highways/gates/hysteresis/index.ts +48 -0
- package/src/highways/gates/ids/index.ts +59 -0
- package/src/highways/gates/index.ts +6 -0
- package/src/highways/gates/model.ts +129 -0
- package/src/highways/gates/stretch/index.ts +39 -0
- package/src/highways/grouped.ts +123 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +30 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/road-classes/index.ts +74 -0
- package/src/highways/router.ts +1 -1
- package/src/highways/select/index.ts +162 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/highways/weiszfeld/index.ts +105 -0
- package/src/highways/wspd/index.ts +130 -0
- package/src/highways/zipper/index.ts +168 -0
- package/src/index.ts +1 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +303 -7
- package/src/strategies/schemas.ts +300 -0
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { flowDistance } from "../flow-clusters/index.ts";
|
|
3
|
+
import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
|
|
4
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
5
|
+
import { edgePlugs } from "../stretches.ts";
|
|
6
|
+
|
|
7
|
+
/** The distance of two edges: 1 less their compatibility of FDEB, or the flow distance in 4D of HWY-023. */
|
|
8
|
+
export type EdgeDistance = "compatibility" | "flow";
|
|
9
|
+
|
|
10
|
+
/** The values of `EdgeDistance`. */
|
|
11
|
+
export const EDGE_DISTANCES: readonly EdgeDistance[] = Object.freeze(["compatibility", "flow"]);
|
|
12
|
+
|
|
13
|
+
/** The options of the highway strategy `hdbscan` (HWY-028 of the note on highways). */
|
|
14
|
+
export interface HdbscanOptions {
|
|
15
|
+
/** A cluster has this number of edges or more. The default is 3. */
|
|
16
|
+
readonly minClusterSize: number;
|
|
17
|
+
/** The core distance of an edge is the distance to its neighbour of this rank. The default is 3. */
|
|
18
|
+
readonly minSamples: number;
|
|
19
|
+
/** The distance of two edges. The default is `compatibility`. */
|
|
20
|
+
readonly distance: EdgeDistance;
|
|
21
|
+
/** The spine of each cluster. The default is `trunk`: one trunk from one area to the other area. */
|
|
22
|
+
readonly spine: GroupSpine;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The default options of the strategy `hdbscan`. */
|
|
26
|
+
export const HDBSCAN_DEFAULTS: Readonly<HdbscanOptions> = Object.freeze({ minClusterSize: 3, minSamples: 3, distance: "compatibility", spine: "trunk" });
|
|
27
|
+
|
|
28
|
+
/** The largest number of edges in one distance matrix. A larger graph splits into blocks of edges in the order of their sources. */
|
|
29
|
+
const BLOCK = 2500;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This function gives the compatibility of two edges of FDEB (Holten and van Wijk 2009): `Ca Cs Cp Cv`, from 0 to 1.
|
|
33
|
+
* `Ca` is the angle, `Cs` the scale, `Cp` the position and `Cv` the visibility. The direction of an edge does not change the value.
|
|
34
|
+
*/
|
|
35
|
+
export function compatibility(p0: Point, p1: Point, q0: Point, q1: Point): number {
|
|
36
|
+
const lp = dist(p0, p1);
|
|
37
|
+
const lq = dist(q0, q1);
|
|
38
|
+
if (lp <= 1e-9 || lq <= 1e-9) return 0;
|
|
39
|
+
const ca = Math.abs(((p1.x - p0.x) * (q1.x - q0.x) + (p1.y - p0.y) * (q1.y - q0.y)) / (lp * lq));
|
|
40
|
+
const avg = (lp + lq) / 2;
|
|
41
|
+
const cs = 2 / (avg / Math.min(lp, lq) + Math.max(lp, lq) / avg);
|
|
42
|
+
const pm = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
|
|
43
|
+
const qm = { x: (q0.x + q1.x) / 2, y: (q0.y + q1.y) / 2 };
|
|
44
|
+
const cp = avg / (avg + dist(pm, qm));
|
|
45
|
+
const cv = Math.min(visibility(p0, p1, q0, q1), visibility(q0, q1, p0, p1));
|
|
46
|
+
return ca * cs * cp * cv;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** This function gives the visibility of the edge `q` from the edge `p` of FDEB: `max(1 - 2 |pm - im| / |i0 i1|, 0)`. The points `i` are the projection of `q` on the line of `p`. */
|
|
50
|
+
function visibility(p0: Point, p1: Point, q0: Point, q1: Point): number {
|
|
51
|
+
const dx = p1.x - p0.x;
|
|
52
|
+
const dy = p1.y - p0.y;
|
|
53
|
+
const l2 = dx * dx + dy * dy;
|
|
54
|
+
const project = (q: Point): Point => {
|
|
55
|
+
const u = ((q.x - p0.x) * dx + (q.y - p0.y) * dy) / l2;
|
|
56
|
+
return { x: p0.x + u * dx, y: p0.y + u * dy };
|
|
57
|
+
};
|
|
58
|
+
const i0 = project(q0);
|
|
59
|
+
const i1 = project(q1);
|
|
60
|
+
const span = dist(i0, i1);
|
|
61
|
+
if (span <= 1e-9) return 0;
|
|
62
|
+
const im = { x: (i0.x + i1.x) / 2, y: (i0.y + i1.y) / 2 };
|
|
63
|
+
const pm = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 };
|
|
64
|
+
return Math.max(1 - (2 * dist(pm, im)) / span, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* This function gives the HDBSCAN clusters of the points of a distance matrix (Campello, Moulavi and Sander 2013). The value -1 is noise.
|
|
69
|
+
* The core distance of a point is the distance to its `minSamples`-th nearest point. The mutual reachability is `max(core(a), core(b), d(a, b))`.
|
|
70
|
+
* The minimum spanning tree of the mutual reachability gives the tree of the clusters. A split with two parts of `minClusterSize` or more gives two clusters.
|
|
71
|
+
* The selection keeps the clusters with the largest stability, `sum (lambda_leave - lambda_birth)`, with `lambda = 1 / distance`. The root is not a cluster.
|
|
72
|
+
*/
|
|
73
|
+
export function hdbscanLabels(n: number, d: Float64Array, minClusterSize: number, minSamples: number): Int32Array {
|
|
74
|
+
const labels = new Int32Array(n).fill(-1);
|
|
75
|
+
const mcs = Math.max(2, Math.round(minClusterSize));
|
|
76
|
+
if (n < mcs || n < 2) return labels;
|
|
77
|
+
const k = Math.max(1, Math.min(n - 1, Math.round(minSamples)));
|
|
78
|
+
// The core distance of each point.
|
|
79
|
+
const core = new Float64Array(n);
|
|
80
|
+
const row = new Float64Array(n - 1);
|
|
81
|
+
for (let i = 0; i < n; i++) {
|
|
82
|
+
let c = 0;
|
|
83
|
+
for (let j = 0; j < n; j++) if (j !== i) row[c++] = d[i * n + j]!;
|
|
84
|
+
row.sort();
|
|
85
|
+
core[i] = row[k - 1]!;
|
|
86
|
+
}
|
|
87
|
+
const reach = (i: number, j: number): number => Math.max(core[i]!, core[j]!, d[i * n + j]!);
|
|
88
|
+
// The minimum spanning tree (Prim), with ties to the lower index.
|
|
89
|
+
const inTree = new Uint8Array(n);
|
|
90
|
+
const best = new Float64Array(n).fill(Infinity);
|
|
91
|
+
const from = new Int32Array(n).fill(-1);
|
|
92
|
+
const links: { a: number; b: number; w: number }[] = [];
|
|
93
|
+
let cur = 0;
|
|
94
|
+
inTree[0] = 1;
|
|
95
|
+
for (let step = 1; step < n; step++) {
|
|
96
|
+
let next = -1;
|
|
97
|
+
for (let j = 0; j < n; j++) {
|
|
98
|
+
if (inTree[j]) continue;
|
|
99
|
+
const w = reach(cur, j);
|
|
100
|
+
if (w < best[j]!) [best[j], from[j]] = [w, cur];
|
|
101
|
+
if (next < 0 || best[j]! < best[next]!) next = j;
|
|
102
|
+
}
|
|
103
|
+
inTree[next] = 1;
|
|
104
|
+
links.push({ a: from[next]!, b: next, w: best[next]! });
|
|
105
|
+
cur = next;
|
|
106
|
+
}
|
|
107
|
+
links.sort((p, q) => p.w - q.w || p.a - q.a || p.b - q.b);
|
|
108
|
+
// The single linkage tree: the node `n + i` is the merge `i`.
|
|
109
|
+
const parent = new Int32Array(2 * n - 1).fill(-1);
|
|
110
|
+
const left = new Int32Array(n - 1);
|
|
111
|
+
const right = new Int32Array(n - 1);
|
|
112
|
+
const height = new Float64Array(n - 1);
|
|
113
|
+
const size = new Int32Array(2 * n - 1).fill(1);
|
|
114
|
+
const uf = new Int32Array(2 * n - 1);
|
|
115
|
+
for (let i = 0; i < uf.length; i++) uf[i] = i;
|
|
116
|
+
const find = (i: number): number => {
|
|
117
|
+
while (uf[i] !== i) i = uf[i] = uf[uf[i]!]!;
|
|
118
|
+
return i;
|
|
119
|
+
};
|
|
120
|
+
links.forEach((l, i) => {
|
|
121
|
+
const ra = find(l.a);
|
|
122
|
+
const rb = find(l.b);
|
|
123
|
+
const node = n + i;
|
|
124
|
+
left[i] = ra;
|
|
125
|
+
right[i] = rb;
|
|
126
|
+
height[i] = l.w;
|
|
127
|
+
size[node] = size[ra]! + size[rb]!;
|
|
128
|
+
parent[ra] = node;
|
|
129
|
+
parent[rb] = node;
|
|
130
|
+
uf[ra] = node;
|
|
131
|
+
uf[rb] = node;
|
|
132
|
+
});
|
|
133
|
+
const root = 2 * n - 2;
|
|
134
|
+
const lambdaOf = (h: number): number => 1 / Math.max(h, 1e-9);
|
|
135
|
+
const leaves = (node: number): number[] => {
|
|
136
|
+
const out: number[] = [];
|
|
137
|
+
const stack = [node];
|
|
138
|
+
while (stack.length > 0) {
|
|
139
|
+
const x = stack.pop()!;
|
|
140
|
+
if (x < n) out.push(x);
|
|
141
|
+
else stack.push(left[x - n]!, right[x - n]!);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
};
|
|
145
|
+
// The condensed tree. Each cluster has a birth lambda, a parent cluster, its stability and the points that leave it at their lambda.
|
|
146
|
+
const birth: number[] = [0];
|
|
147
|
+
const up: number[] = [-1];
|
|
148
|
+
const stability: number[] = [0];
|
|
149
|
+
const fall = new Int32Array(n).fill(0);
|
|
150
|
+
const queue: [node: number, cluster: number][] = [[root, 0]];
|
|
151
|
+
while (queue.length > 0) {
|
|
152
|
+
const [node, c] = queue.shift()!;
|
|
153
|
+
const i = node - n;
|
|
154
|
+
const lambda = lambdaOf(height[i]!);
|
|
155
|
+
const kids = [left[i]!, right[i]!];
|
|
156
|
+
const big = kids.filter((x) => size[x]! >= mcs);
|
|
157
|
+
if (big.length === 2) {
|
|
158
|
+
for (const x of kids) {
|
|
159
|
+
const id = birth.length;
|
|
160
|
+
birth.push(lambda);
|
|
161
|
+
up.push(c);
|
|
162
|
+
stability.push(0);
|
|
163
|
+
stability[c] = stability[c]! + size[x]! * (lambda - birth[c]!);
|
|
164
|
+
queue.push([x, id]);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const x of kids) {
|
|
169
|
+
if (big.includes(x)) {
|
|
170
|
+
if (x >= n) queue.push([x, c]);
|
|
171
|
+
else {
|
|
172
|
+
fall[x] = c;
|
|
173
|
+
stability[c] = stability[c]! + (lambda - birth[c]!);
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const p of leaves(x)) {
|
|
178
|
+
fall[p] = c;
|
|
179
|
+
stability[c] = stability[c]! + (lambda - birth[c]!);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// The selection by the excess of mass, from the last cluster to the first. A child cluster always has a larger id than its parent.
|
|
184
|
+
const count = birth.length;
|
|
185
|
+
const chosen = new Uint8Array(count);
|
|
186
|
+
const value = [...stability];
|
|
187
|
+
const children: number[][] = Array.from({ length: count }, () => []);
|
|
188
|
+
for (let c = 1; c < count; c++) children[up[c]!]!.push(c);
|
|
189
|
+
for (let c = count - 1; c >= 1; c--) {
|
|
190
|
+
const sub = children[c]!.reduce((s, x) => s + value[x]!, 0);
|
|
191
|
+
if (children[c]!.length > 0 && sub > value[c]!) value[c] = sub;
|
|
192
|
+
else {
|
|
193
|
+
chosen[c] = 1;
|
|
194
|
+
const stack = [...children[c]!];
|
|
195
|
+
while (stack.length > 0) {
|
|
196
|
+
const x = stack.pop()!;
|
|
197
|
+
chosen[x] = 0;
|
|
198
|
+
stack.push(...children[x]!);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (let p = 0; p < n; p++) {
|
|
203
|
+
let c = fall[p]!;
|
|
204
|
+
while (c > 0 && !chosen[c]) c = up[c]!;
|
|
205
|
+
labels[p] = c > 0 ? c : -1;
|
|
206
|
+
}
|
|
207
|
+
return labels;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* This function gives the HDBSCAN clusters of the edges (HWY-028). The edges that are noise get no group.
|
|
212
|
+
* Each edge of a cluster gets `flip` when it goes opposite to the first edge of the cluster.
|
|
213
|
+
*/
|
|
214
|
+
export function hdbscanClusters(input: HighwayInput, o: Readonly<HdbscanOptions>): Map<string, EdgeGroup> {
|
|
215
|
+
const ends = edgePlugs(input.graph).map(({ edge, ps, pt }) => ({ edge: edge.id, s: ps.point, t: pt.point }));
|
|
216
|
+
const groups = new Map<string, EdgeGroup>();
|
|
217
|
+
const order = ends.length > BLOCK ? [...ends].sort((a, b) => a.s.x - b.s.x || a.s.y - b.s.y || (a.edge < b.edge ? -1 : 1)) : ends;
|
|
218
|
+
const between = (a: (typeof ends)[number], b: (typeof ends)[number]): number =>
|
|
219
|
+
o.distance === "flow" ? flowDistance(a.s, a.t, b.s, b.t, false).d : 1 - compatibility(a.s, a.t, b.s, b.t);
|
|
220
|
+
for (let from = 0; from < order.length; from += BLOCK) {
|
|
221
|
+
const block = order.slice(from, from + BLOCK);
|
|
222
|
+
const n = block.length;
|
|
223
|
+
const d = new Float64Array(n * n);
|
|
224
|
+
for (let i = 0; i < n; i++)
|
|
225
|
+
for (let j = i + 1; j < n; j++) {
|
|
226
|
+
const v = between(block[i]!, block[j]!);
|
|
227
|
+
d[i * n + j] = v;
|
|
228
|
+
d[j * n + i] = v;
|
|
229
|
+
}
|
|
230
|
+
const labels = hdbscanLabels(n, d, o.minClusterSize, o.minSamples);
|
|
231
|
+
const lead = new Map<number, number>();
|
|
232
|
+
block.forEach((e, i) => {
|
|
233
|
+
const c = labels[i]!;
|
|
234
|
+
if (c < 0) return;
|
|
235
|
+
if (!lead.has(c)) lead.set(c, i);
|
|
236
|
+
const l = block[lead.get(c)!]!;
|
|
237
|
+
const flip = (e.t.x - e.s.x) * (l.t.x - l.s.x) + (e.t.y - e.s.y) * (l.t.y - l.s.y) < 0;
|
|
238
|
+
groups.set(e.edge, { key: `c${from}:${c}`, flip });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return groups;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* This function gives the highways of the HDBSCAN clusters (HWY-028). The clusters that live over a long range of densities are the highways.
|
|
246
|
+
* The number of highways is not fixed, and the noise edges stay alone.
|
|
247
|
+
*/
|
|
248
|
+
export function hdbscanPlan(input: HighwayInput, o: Readonly<HdbscanOptions>): HighwayPlan {
|
|
249
|
+
return groupedPlan(input, hdbscanClusters(input, o), { spine: o.spine, minSize: Math.max(2, Math.round(o.minClusterSize)) });
|
|
250
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import type { WayOptions } from "../../ways.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { linePoint, snapPlan, type SnapLine } from "../snap.ts";
|
|
5
|
+
import { edgePlugs } from "../stretches.ts";
|
|
6
|
+
|
|
7
|
+
/** The options of the highway strategy `hough` (HWY-024 of the note on highways). */
|
|
8
|
+
export interface HoughOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
9
|
+
/** The width of the votes in the angle, in degrees. An edge joins a peak within this angle. The default is 8. */
|
|
10
|
+
readonly dTheta: number;
|
|
11
|
+
/** The width of the votes in the offset, in units of the board. An edge joins a peak when its middle is this near to the line. The default is 12. */
|
|
12
|
+
readonly dRho: number;
|
|
13
|
+
/** A peak with fewer edges than this number is not a corridor. The default is 3. */
|
|
14
|
+
readonly minVotes: number;
|
|
15
|
+
/** With `true`, the peaks are only horizontal or vertical lines. The default is `false`. */
|
|
16
|
+
readonly orthogonal: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The default options of the strategy `hough`. */
|
|
20
|
+
export const HOUGH_DEFAULTS: Readonly<Required<Omit<HoughOptions, "margin" | "k">>> = Object.freeze({ dTheta: 8, dRho: 12, minVotes: 3, orthogonal: false });
|
|
21
|
+
|
|
22
|
+
/** The line of one edge: its direction angle from 0 to pi, its offset along the normal, its middle and its ends. */
|
|
23
|
+
interface EdgeLine {
|
|
24
|
+
readonly edge: string;
|
|
25
|
+
readonly theta: number;
|
|
26
|
+
readonly s: Point;
|
|
27
|
+
readonly t: Point;
|
|
28
|
+
readonly mid: Point;
|
|
29
|
+
readonly len: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A peak of the votes: its angle, its offset and the sum of its votes. */
|
|
33
|
+
export interface HoughPeak {
|
|
34
|
+
readonly theta: number;
|
|
35
|
+
readonly rho: number;
|
|
36
|
+
readonly votes: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEG = Math.PI / 180;
|
|
40
|
+
|
|
41
|
+
/** This function gives the offset of a point from the line through the origin with the direction angle `theta`. */
|
|
42
|
+
const offsetOf = (p: Point, theta: number): number => -p.x * Math.sin(theta) + p.y * Math.cos(theta);
|
|
43
|
+
|
|
44
|
+
/** This function gives the place of a point along the direction angle `theta`. */
|
|
45
|
+
const placeOf = (p: Point, theta: number): number => p.x * Math.cos(theta) + p.y * Math.sin(theta);
|
|
46
|
+
|
|
47
|
+
/** This function gives the difference of two direction angles, from 0 to pi / 2. */
|
|
48
|
+
function angleGap(a: number, b: number): number {
|
|
49
|
+
const d = Math.abs(a - b) % Math.PI;
|
|
50
|
+
return Math.min(d, Math.PI - d);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function gives the peaks of the votes of the lines (Duda and Hart 1972). Each line votes for its angle and its offset, with a weight of its length.
|
|
55
|
+
* A vote spreads as a Gaussian with the widths `dTheta / 2` and `dRho / 2`. The accumulator has bins of 1 degree and of `dRho / 4`.
|
|
56
|
+
* A peak is a bin with a larger sum than each other bin within `dTheta` and `dRho`. The peaks come in the order of their votes.
|
|
57
|
+
*/
|
|
58
|
+
export function houghPeaks(lines: readonly { theta: number; mid: Point; len: number }[], o: Readonly<Pick<HoughOptions, "dTheta" | "dRho" | "orthogonal">>): HoughPeak[] {
|
|
59
|
+
if (lines.length === 0) return [];
|
|
60
|
+
const thetas = o.orthogonal ? [0, 90] : Array.from({ length: 180 }, (_, i) => i);
|
|
61
|
+
const T = thetas.length;
|
|
62
|
+
const rhoStep = Math.max(1e-6, o.dRho / 4);
|
|
63
|
+
let maxRho = 0;
|
|
64
|
+
for (const l of lines) maxRho = Math.max(maxRho, Math.abs(l.mid.x) + Math.abs(l.mid.y));
|
|
65
|
+
const R = Math.ceil(maxRho / rhoStep) + 4;
|
|
66
|
+
const rows = 2 * R + 1;
|
|
67
|
+
const acc = new Float64Array(T * rows);
|
|
68
|
+
const wTheta = Math.max(1e-6, o.dTheta);
|
|
69
|
+
const sT = wTheta / 2;
|
|
70
|
+
const sR = o.dRho / 2;
|
|
71
|
+
const reachR = Math.ceil(o.dRho / rhoStep);
|
|
72
|
+
for (const l of lines) {
|
|
73
|
+
for (let ti = 0; ti < T; ti++) {
|
|
74
|
+
const gap = angleGap(thetas[ti]! * DEG, l.theta) / DEG;
|
|
75
|
+
if (gap > wTheta) continue;
|
|
76
|
+
const rho = offsetOf(l.mid, thetas[ti]! * DEG);
|
|
77
|
+
const center = Math.round(rho / rhoStep);
|
|
78
|
+
const wt = Math.exp(-(gap * gap) / (2 * sT * sT)) * l.len;
|
|
79
|
+
for (let r = center - reachR; r <= center + reachR; r++) {
|
|
80
|
+
if (r < -R || r > R) continue;
|
|
81
|
+
const dr = r * rhoStep - rho;
|
|
82
|
+
acc[ti * rows + r + R] = acc[ti * rows + r + R]! + wt * Math.exp(-(dr * dr) / (2 * sR * sR));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const reachT = o.orthogonal ? 0 : Math.ceil(wTheta);
|
|
87
|
+
const peaks: HoughPeak[] = [];
|
|
88
|
+
for (let ti = 0; ti < T; ti++)
|
|
89
|
+
for (let r = 0; r < rows; r++) {
|
|
90
|
+
const v = acc[ti * rows + r]!;
|
|
91
|
+
if (v <= 0) continue;
|
|
92
|
+
let top = true;
|
|
93
|
+
for (let dt = -reachT; dt <= reachT && top; dt++)
|
|
94
|
+
for (let dr = -reachR; dr <= reachR && top; dr++) {
|
|
95
|
+
if (dt === 0 && dr === 0) continue;
|
|
96
|
+
let tj = ti + dt;
|
|
97
|
+
let rj = r - R;
|
|
98
|
+
// The angle wraps at 180 degrees, and the offset changes its sign there.
|
|
99
|
+
if (tj < 0) [tj, rj] = [tj + T, -rj];
|
|
100
|
+
else if (tj >= T) [tj, rj] = [tj - T, -rj];
|
|
101
|
+
rj += dr;
|
|
102
|
+
if (rj < -R || rj > R) continue;
|
|
103
|
+
const w = acc[tj * rows + rj + R]!;
|
|
104
|
+
// A tie goes to the first bin in the order of the accumulator.
|
|
105
|
+
if (w > v || (w === v && tj * rows + rj + R < ti * rows + r)) top = false;
|
|
106
|
+
}
|
|
107
|
+
if (top) peaks.push({ theta: thetas[ti]! * DEG, rho: (r - R) * rhoStep, votes: v });
|
|
108
|
+
}
|
|
109
|
+
peaks.sort((a, b) => b.votes - a.votes || a.theta - b.theta || a.rho - b.rho);
|
|
110
|
+
return peaks;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* This function gives the corridors of the votes (HWY-024). Each edge joins the first peak with an angle within `dTheta` and its middle within `dRho` of the line.
|
|
115
|
+
* The corridor runs over the common span of the projections of its edges on the line. Each edge snaps to the ends of that span.
|
|
116
|
+
* A member that makes the span shorter than four margins leaves, while the peak keeps `minVotes` edges.
|
|
117
|
+
*/
|
|
118
|
+
export function houghLines(input: HighwayInput, o: Readonly<HoughOptions>): SnapLine[] {
|
|
119
|
+
const margin = o.margin ?? 5;
|
|
120
|
+
const lines: EdgeLine[] = [];
|
|
121
|
+
for (const { edge, ps, pt } of edgePlugs(input.graph)) {
|
|
122
|
+
const s = ps.point;
|
|
123
|
+
const t = pt.point;
|
|
124
|
+
const len = dist(s, t);
|
|
125
|
+
if (len <= 1e-9) continue;
|
|
126
|
+
const theta = (((Math.atan2(t.y - s.y, t.x - s.x) % Math.PI) + Math.PI) % Math.PI);
|
|
127
|
+
lines.push({ edge: edge.id, theta, s, t, mid: { x: (s.x + t.x) / 2, y: (s.y + t.y) / 2 }, len });
|
|
128
|
+
}
|
|
129
|
+
const peaks = houghPeaks(lines, o);
|
|
130
|
+
const taken = new Set<string>();
|
|
131
|
+
const out: SnapLine[] = [];
|
|
132
|
+
const need = Math.max(2, Math.round(o.minVotes));
|
|
133
|
+
for (const [pi, p] of peaks.entries()) {
|
|
134
|
+
let members = lines.filter((l) => !taken.has(l.edge) && angleGap(l.theta, p.theta) <= o.dTheta * DEG && Math.abs(offsetOf(l.mid, p.theta) - p.rho) <= o.dRho);
|
|
135
|
+
const span = (list: readonly EdgeLine[]): [number, number] => {
|
|
136
|
+
let lo = -Infinity;
|
|
137
|
+
let hi = Infinity;
|
|
138
|
+
for (const l of list) {
|
|
139
|
+
const a = placeOf(l.s, p.theta);
|
|
140
|
+
const b = placeOf(l.t, p.theta);
|
|
141
|
+
lo = Math.max(lo, Math.min(a, b));
|
|
142
|
+
hi = Math.min(hi, Math.max(a, b));
|
|
143
|
+
}
|
|
144
|
+
return [lo, hi];
|
|
145
|
+
};
|
|
146
|
+
let [lo, hi] = span(members);
|
|
147
|
+
while (members.length >= need && hi - lo < 4 * margin) {
|
|
148
|
+
// The member that limits the span the most leaves.
|
|
149
|
+
let best = -1;
|
|
150
|
+
let gain = -Infinity;
|
|
151
|
+
members.forEach((_, i) => {
|
|
152
|
+
const [l2, h2] = span(members.filter((__, j) => j !== i));
|
|
153
|
+
if (h2 - l2 > gain) [gain, best] = [h2 - l2, i];
|
|
154
|
+
});
|
|
155
|
+
members = members.filter((_, j) => j !== best);
|
|
156
|
+
[lo, hi] = span(members);
|
|
157
|
+
}
|
|
158
|
+
if (members.length < need || hi - lo < 4 * margin) continue;
|
|
159
|
+
for (const l of members) taken.add(l.edge);
|
|
160
|
+
const a = linePoint(p.theta, p.rho, lo);
|
|
161
|
+
const b = linePoint(p.theta, p.rho, hi);
|
|
162
|
+
out.push({
|
|
163
|
+
key: `p${pi}`,
|
|
164
|
+
a,
|
|
165
|
+
b,
|
|
166
|
+
members: members.map((l) => {
|
|
167
|
+
const forward = placeOf(l.s, p.theta) <= placeOf(l.t, p.theta);
|
|
168
|
+
return { edge: l.edge, from: forward ? a : b, to: forward ? b : a, at: 0 };
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* This function gives the highways of the Hough votes (HWY-024). The peaks of the votes of the straight lines of the edges are corridors.
|
|
177
|
+
* Each corridor is a spine through the corner graph, and its edges ride it over their common span.
|
|
178
|
+
*/
|
|
179
|
+
export function houghPlan(input: HighwayInput, o: Readonly<HoughOptions>): HighwayPlan {
|
|
180
|
+
const way = { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) };
|
|
181
|
+
return snapPlan(input, houghLines(input, o), { ...way, minSize: Math.max(2, Math.round(o.minVotes)) });
|
|
182
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { dist, polylineLength, type Point } from "../../geom.ts";
|
|
2
|
+
import { orderLanes } from "../../segments.ts";
|
|
3
|
+
import { WayFinder } from "../../ways.ts";
|
|
4
|
+
import { firstRoutes, nearestPlace, tidyPlan } from "../base.ts";
|
|
5
|
+
import { lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayPlan } from "../model.ts";
|
|
6
|
+
import { candidatePlan, type CandidateSource } from "../select/index.ts";
|
|
7
|
+
import { spinePart } from "../snap.ts";
|
|
8
|
+
import { edgePlugs, sharedStretches } from "../stretches.ts";
|
|
9
|
+
|
|
10
|
+
/** A generator of candidate arcs: `runs` (the shared stretches of the first ways of all edges) or a generator of the strategy `select`. */
|
|
11
|
+
export type ArcSource = "runs" | CandidateSource;
|
|
12
|
+
|
|
13
|
+
/** The values of `ArcSource`. */
|
|
14
|
+
export const ARC_SOURCES: readonly ArcSource[] = Object.freeze(["runs", "corridors", "quickbundles", "wspd", "road-classes", "flow-field", "od-cells", "shared-ways"]);
|
|
15
|
+
|
|
16
|
+
/** The options of the highway strategy `hub-arcs` (HWY-042 of the note on highways). */
|
|
17
|
+
export interface HubArcOptions {
|
|
18
|
+
/** The largest number of arcs (highways). The default is 12. */
|
|
19
|
+
readonly q: number;
|
|
20
|
+
/** The cost of a length on an arc against a length off the arcs, from 0 to 1. The default is 0.5. */
|
|
21
|
+
readonly alpha: number;
|
|
22
|
+
/** An edge rides an arc only when its legs plus its part of the arc are this many times its first route or less. The default is 1.25. */
|
|
23
|
+
readonly stretch: number;
|
|
24
|
+
/** The number of passes of the local search with one swap. The default is 1. */
|
|
25
|
+
readonly swaps: number;
|
|
26
|
+
/** An arc with fewer edges than this number is not a highway. The default is 2. */
|
|
27
|
+
readonly minSize: number;
|
|
28
|
+
/** The generators of the candidate arcs. The default is `runs`, `corridors` and `road-classes`. */
|
|
29
|
+
readonly generators: readonly ArcSource[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The default options of the strategy `hub-arcs`. */
|
|
33
|
+
export const HUB_ARC_DEFAULTS: Readonly<HubArcOptions> = Object.freeze({
|
|
34
|
+
q: 12,
|
|
35
|
+
alpha: 0.5,
|
|
36
|
+
stretch: 1.25,
|
|
37
|
+
swaps: 1,
|
|
38
|
+
minSize: 2,
|
|
39
|
+
generators: Object.freeze(["runs", "corridors", "road-classes"] as ArcSource[]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** The largest number of candidate arcs. The longest arcs stay. */
|
|
43
|
+
const MAX_ARCS = 400;
|
|
44
|
+
|
|
45
|
+
/** The place of one edge on one arc: where it joins and leaves, and the cost it saves against its first route. */
|
|
46
|
+
interface Ride {
|
|
47
|
+
readonly on: number;
|
|
48
|
+
readonly off: number;
|
|
49
|
+
readonly save: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** This function gives the candidate arcs: the spines of the plans of the generators, without the copies, with the longest first. */
|
|
53
|
+
export function candidateArcs(input: HighwayInput, generators: readonly ArcSource[]): Point[][] {
|
|
54
|
+
const spines: Point[][] = [];
|
|
55
|
+
for (const g of generators) {
|
|
56
|
+
const plan = g === "runs" ? runsPlan(input) : candidatePlan(input, g);
|
|
57
|
+
for (const h of plan.highways) spines.push([...h.spine]);
|
|
58
|
+
}
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const out: Point[][] = [];
|
|
61
|
+
for (const s of spines) {
|
|
62
|
+
if (s.length < 2 || polylineLength(s) <= 1e-6) continue;
|
|
63
|
+
const key = s.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(";");
|
|
64
|
+
const back = [...s].reverse().map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(";");
|
|
65
|
+
if (seen.has(key) || seen.has(back)) continue;
|
|
66
|
+
seen.add(key);
|
|
67
|
+
out.push(s);
|
|
68
|
+
}
|
|
69
|
+
out.sort((a, b) => polylineLength(b) - polylineLength(a));
|
|
70
|
+
return out.slice(0, MAX_ARCS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** This function gives the shared stretches of the first ways of all edges, with no keys. */
|
|
74
|
+
function runsPlan(input: HighwayInput): HighwayPlan {
|
|
75
|
+
const finder = new WayFinder(input.graph.nodes);
|
|
76
|
+
const ways = edgePlugs(input.graph).map(({ edge, ps, pt }) => {
|
|
77
|
+
const a = finder.out(ps);
|
|
78
|
+
const b = finder.out(pt);
|
|
79
|
+
return { edge: edge.id, key: "", points: finder.corners.path(a, b) ?? [a, b] };
|
|
80
|
+
});
|
|
81
|
+
return sharedStretches(ways, { minSize: 2, pull: (p) => finder.corners.pull(p) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* This function gives the highways of the hub arc location (Campbell, Ernst and Krishnamoorthy 2005). It chooses at most `q` arcs from the candidates.
|
|
86
|
+
* An edge on an arc costs its two legs plus `alpha` times its part of the arc. An edge off the arcs costs its first route.
|
|
87
|
+
* The greedy step adds the arc that saves the most cost. Then each swap pass changes one chosen arc for one other arc while the saving grows.
|
|
88
|
+
* Each edge rides the arc that saves the most for it.
|
|
89
|
+
*/
|
|
90
|
+
export function hubArcPlan(input: HighwayInput, o: Readonly<HubArcOptions>): HighwayPlan {
|
|
91
|
+
const margin = 5;
|
|
92
|
+
const routes = firstRoutes(input);
|
|
93
|
+
const plugs = edgePlugs(input.graph);
|
|
94
|
+
const arcs = candidateArcs(input, o.generators);
|
|
95
|
+
const q = Math.max(0, Math.round(o.q));
|
|
96
|
+
if (q === 0 || arcs.length === 0) return { highways: [] };
|
|
97
|
+
// The saving of each edge on each arc. A saving of 0 means that the edge does not ride the arc.
|
|
98
|
+
const rides: (Ride | undefined)[][] = arcs.map((spine) =>
|
|
99
|
+
plugs.map(({ edge, ps, pt }) => {
|
|
100
|
+
const first = routes.get(edge.id);
|
|
101
|
+
const base = first ? polylineLength(first) : dist(ps.point, pt.point);
|
|
102
|
+
const on = nearestPlace(spine, ps.point);
|
|
103
|
+
const off = nearestPlace(spine, pt.point);
|
|
104
|
+
const along = lengthBetween(spine, Math.min(on, off), Math.max(on, off));
|
|
105
|
+
if (along < margin) return undefined;
|
|
106
|
+
const legs = dist(ps.point, pointAt(spine, on)) + dist(pointAt(spine, off), pt.point);
|
|
107
|
+
if (legs + along > o.stretch * base) return undefined;
|
|
108
|
+
const save = base - (legs + o.alpha * along);
|
|
109
|
+
return save > 0 ? { on, off, save } : undefined;
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
const n = plugs.length;
|
|
113
|
+
const total = (chosen: readonly number[]): number => {
|
|
114
|
+
let sum = 0;
|
|
115
|
+
for (let e = 0; e < n; e++) {
|
|
116
|
+
let best = 0;
|
|
117
|
+
for (const a of chosen) best = Math.max(best, rides[a]![e]?.save ?? 0);
|
|
118
|
+
sum += best;
|
|
119
|
+
}
|
|
120
|
+
return sum;
|
|
121
|
+
};
|
|
122
|
+
const chosen: number[] = [];
|
|
123
|
+
while (chosen.length < q) {
|
|
124
|
+
const now = total(chosen);
|
|
125
|
+
let pick = -1;
|
|
126
|
+
let gain = 1e-9;
|
|
127
|
+
for (let a = 0; a < arcs.length; a++) {
|
|
128
|
+
if (chosen.includes(a)) continue;
|
|
129
|
+
const g = total([...chosen, a]) - now;
|
|
130
|
+
if (g > gain) [gain, pick] = [g, a];
|
|
131
|
+
}
|
|
132
|
+
if (pick < 0) break;
|
|
133
|
+
chosen.push(pick);
|
|
134
|
+
}
|
|
135
|
+
for (let pass = 0; pass < Math.round(o.swaps); pass++) {
|
|
136
|
+
let better = false;
|
|
137
|
+
for (let i = 0; i < chosen.length; i++) {
|
|
138
|
+
const now = total(chosen);
|
|
139
|
+
let pick = -1;
|
|
140
|
+
let gain = 1e-9;
|
|
141
|
+
for (let a = 0; a < arcs.length; a++) {
|
|
142
|
+
if (chosen.includes(a)) continue;
|
|
143
|
+
const next = chosen.map((x, j) => (j === i ? a : x));
|
|
144
|
+
const g = total(next) - now;
|
|
145
|
+
if (g > gain) [gain, pick] = [g, a];
|
|
146
|
+
}
|
|
147
|
+
if (pick >= 0) {
|
|
148
|
+
chosen[i] = pick;
|
|
149
|
+
better = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!better) break;
|
|
153
|
+
}
|
|
154
|
+
// Each edge rides its best arc.
|
|
155
|
+
const riders = new Map<number, { edge: string; ride: Ride; points: Point[] }[]>();
|
|
156
|
+
plugs.forEach(({ edge, ps, pt }, e) => {
|
|
157
|
+
let best = -1;
|
|
158
|
+
for (const a of chosen) if ((rides[a]![e]?.save ?? 0) > (best < 0 ? 0 : rides[best]![e]!.save)) best = a;
|
|
159
|
+
if (best < 0) return;
|
|
160
|
+
const ride = rides[best]![e]!;
|
|
161
|
+
const list = riders.get(best) ?? [];
|
|
162
|
+
list.push({ edge: edge.id, ride, points: [ps.point, ...spinePart(arcs[best]!, ride.on, ride.off), pt.point] });
|
|
163
|
+
riders.set(best, list);
|
|
164
|
+
});
|
|
165
|
+
const highways: Highway[] = [];
|
|
166
|
+
for (const a of [...chosen].sort((x, y) => x - y)) {
|
|
167
|
+
const list = riders.get(a) ?? [];
|
|
168
|
+
if (list.length < Math.max(2, o.minSize)) continue;
|
|
169
|
+
const spine = arcs[a]!;
|
|
170
|
+
const lanes = orderLanes(spine, list.map((r) => ({ edge: r.edge, points: r.points })), "bundle");
|
|
171
|
+
const byEdge = new Map(list.map((r) => [r.edge, r.ride]));
|
|
172
|
+
highways.push({
|
|
173
|
+
id: `h${highways.length}`,
|
|
174
|
+
key: `a${a}`,
|
|
175
|
+
spine,
|
|
176
|
+
lanes,
|
|
177
|
+
members: lanes.map((edge) => memberAt(spine, edge, byEdge.get(edge)!.on, byEdge.get(edge)!.off)),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return tidyPlan({ highways });
|
|
181
|
+
}
|
package/src/highways/index.ts
CHANGED
|
@@ -5,3 +5,33 @@ export { highwayRouter, highwayRouterStrategy } from "./router.ts";
|
|
|
5
5
|
export { keysPlan, type KeysOptions } from "./keys/index.ts";
|
|
6
6
|
export { sharedWaysPlan, type SharedWaysOptions } from "./shared-ways/index.ts";
|
|
7
7
|
export { BREAK_EVEN, CORRIDOR_DEFAULTS, corridorPlan, minHash, type CorridorOptions } from "./corridors/index.ts";
|
|
8
|
+
export { basePlan, HIGHWAY_BASES, type HighwayBase } from "./base.ts";
|
|
9
|
+
export { MEET_DEFAULTS, meetMember, meetPlan, type MeetOptions } from "./meet/index.ts";
|
|
10
|
+
export { meetingPoints, WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "./weiszfeld/index.ts";
|
|
11
|
+
export { STUB_DEFAULTS, stubPlan, type StubEnds, type StubOptions } from "./stubs/index.ts";
|
|
12
|
+
export { ZIPPER_DEFAULTS, zipLegs, zipperPlan, type Zip, type ZipLimits, type ZipperOptions } from "./zipper/index.ts";
|
|
13
|
+
export { LEVEL_HIGHWAY_DEFAULTS, levelHighwayPlan, type LevelHighwayOptions } from "./level-links/index.ts";
|
|
14
|
+
export { edgePlugs, sharedStretches, type EdgePlugs, type KeyedWay, type SharedStretchOptions } from "./stretches.ts";
|
|
15
|
+
export { GROUP_SPINES, groupedPlan, weberPoint, type EdgeGroup, type GroupedOptions, type GroupSpine } from "./grouped.ts";
|
|
16
|
+
export { mdf, QUICKBUNDLES_DEFAULTS, quickBundles, quickBundlesPlan, resample, type BundleLine, type BundleOrder, type QuickBundlesOptions } from "./quickbundles/index.ts";
|
|
17
|
+
export { ROAD_CLASS_DEFAULTS, roadClassPlan, type RoadClassOptions } from "./road-classes/index.ts";
|
|
18
|
+
export { FLOW_FIELD_DEFAULTS, flowFieldPlan, type FieldEnd, type FlowFieldOptions } from "./flow-field/index.ts";
|
|
19
|
+
export { SplitTree, WSPD_DEFAULTS, wspdGroups, wspdPlan, type SplitCell, type WspdOptions } from "./wspd/index.ts";
|
|
20
|
+
export { OD_CELL_DEFAULTS, odCellPlan, odGroups, type OdCellOptions } from "./od-cells/index.ts";
|
|
21
|
+
export { CANDIDATE_SOURCES, candidatePlan, candidateScore, SELECT_DEFAULTS, selectPlan, type CandidateSource, type SelectOptions } from "./select/index.ts";
|
|
22
|
+
export { MINGLE_DEFAULTS, mingleBundles, minglePlan, type MingleOptions } from "./mingle/index.ts";
|
|
23
|
+
export { SPIRAL_DEFAULTS, spiralJoin, spiralPlan, spiralTree, type SpiralNode, type SpiralOptions } from "./spiral/index.ts";
|
|
24
|
+
export { FLOW_CLUSTER_DEFAULTS, flowClusterPlan, flowClusters, flowDistance, chainClusters, LINKAGES, type FlowClusterOptions, type Linkage } from "./flow-clusters/index.ts";
|
|
25
|
+
export { characteristicPoints, representative, segmentClusters, segmentDistance, TRACLUS_DEFAULTS, traclusPlan, type RouteSegment, type TraclusOptions } from "./traclus/index.ts";
|
|
26
|
+
export { DIFFUSION_DEFAULTS, diffusionPlan, linkDensity, type DiffusionOptions } from "./diffusion/index.ts";
|
|
27
|
+
export { NESTED_DEFAULTS, nestedGroups, nestedPlan, nestingViolations, type NestedOptions } from "./nested/index.ts";
|
|
28
|
+
export { channelOverflows, LANES_DEFAULTS, lanesPlan, type LanesOptions } from "./lanes/index.ts";
|
|
29
|
+
export { GROWTH_DEFAULTS, growthPlan, type GrowthOptions, type GrowthOrder } from "./growth/index.ts";
|
|
30
|
+
export { EDGE_PATH_DEFAULTS, edgePathPlan, edgePaths, type EdgePathOptions, type PathStep } from "./edge-paths/index.ts";
|
|
31
|
+
export { SOFT_KEY_DEFAULTS, softDistance, softKeyClusters, softKeyPlan, type SoftKeyOptions } from "./soft-keys/index.ts";
|
|
32
|
+
export { linePoint, snapPlan, spinePart, type SnapLine, type SnapMember, type SnapOptions } from "./snap.ts";
|
|
33
|
+
export { HOUGH_DEFAULTS, houghLines, houghPeaks, houghPlan, type HoughOptions, type HoughPeak } from "./hough/index.ts";
|
|
34
|
+
export { densityRidges, lineDensity, RIDGE_DEFAULTS, ridgeLines, ridgePlan, type DensityImage, type Ridge, type RidgeOptions, type RidgeSource } from "./ridges/index.ts";
|
|
35
|
+
export { compatibility, EDGE_DISTANCES, HDBSCAN_DEFAULTS, hdbscanClusters, hdbscanLabels, hdbscanPlan, type EdgeDistance, type HdbscanOptions } from "./hdbscan/index.ts";
|
|
36
|
+
export { ARC_SOURCES, candidateArcs, HUB_ARC_DEFAULTS, hubArcPlan, type ArcSource, type HubArcOptions } from "./hub-arcs/index.ts";
|
|
37
|
+
export * from "./gates/index.ts";
|