@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.
- package/README.md +59 -2
- package/build/index.d.ts +2333 -193
- package/build/index.d.ts.map +1 -1
- package/build/index.js +13042 -2334
- package/build/index.js.map +1 -1
- package/package.json +12 -1
- package/src/bench/exact/index.ts +258 -0
- package/src/bench/exact/optima-exact-v1-bounds.json +14409 -0
- package/src/bench/exact/optima-exact-v1.json +4809 -0
- package/src/bench/exact/solve.ts +129 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/activation.ts +3 -0
- package/src/bench/screen/bounds.ts +140 -0
- package/src/bench/screen/compare.ts +26 -0
- package/src/bench/screen/exact-set.ts +100 -0
- package/src/bench/screen/exercise.ts +290 -0
- package/src/bench/screen/forms.ts +25 -5
- package/src/bench/screen/gap.ts +316 -0
- package/src/bench/screen/hash.ts +7 -76
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +89 -1
- package/src/bench/screen/portfolio-pick.ts +20 -0
- package/src/bench/screen/screen.ts +78 -2
- package/src/bench/screen/set.ts +3 -2
- package/src/channels.ts +56 -0
- package/src/highways/areas/index.ts +160 -0
- package/src/highways/confluent/index.ts +154 -0
- package/src/highways/confluent/k23.test-util.ts +8 -0
- package/src/highways/consensus/index.ts +129 -0
- package/src/highways/crossbars/index.ts +184 -0
- package/src/highways/cubu/index.ts +26 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/divided/index.ts +111 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fdeb/index.ts +193 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/frechet/index.ts +232 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hierarchy/index.ts +140 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +79 -0
- package/src/highways/kdeeb/index.ts +355 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/link-communities/index.ts +189 -0
- package/src/highways/mesh/index.ts +209 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/mlseb/index.ts +204 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/p-hub/index.ts +248 -0
- package/src/highways/power/index.ts +117 -0
- package/src/highways/resistance/index.ts +159 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/sbeb/index.ts +303 -0
- package/src/highways/scale-space/index.ts +135 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spectral/index.ts +391 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/territories/index.ts +150 -0
- package/src/highways/trace.ts +153 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/index.ts +2 -0
- package/src/portfolio/bands.ts +33 -0
- package/src/portfolio/fit.ts +29 -0
- package/src/portfolio/index.ts +6 -0
- package/src/portfolio/key.ts +98 -0
- package/src/portfolio/members.ts +171 -0
- package/src/portfolio/router.ts +133 -0
- package/src/portfolio/sets.ts +28 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +376 -1
- package/src/strategies/schemas.ts +460 -1
- package/src/visibility.ts +5 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -19,4 +19,83 @@ export { FLOW_FIELD_DEFAULTS, flowFieldPlan, type FieldEnd, type FlowFieldOption
|
|
|
19
19
|
export { SplitTree, WSPD_DEFAULTS, wspdGroups, wspdPlan, type SplitCell, type WspdOptions } from "./wspd/index.ts";
|
|
20
20
|
export { OD_CELL_DEFAULTS, odCellPlan, odGroups, type OdCellOptions } from "./od-cells/index.ts";
|
|
21
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 { 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";
|
|
22
101
|
export * from "./gates/index.ts";
|