@mark1russell7/highways 0.4.1 → 0.4.2
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 +27 -1
- package/build/index.d.ts +642 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5189 -2609
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +1 -0
- package/src/bench/screen/measures.ts +5 -0
- package/src/bench/screen/screen.ts +14 -0
- package/src/highways/base.ts +126 -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/index.ts +15 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -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/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -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/strategies/builtins.ts +149 -7
- package/src/strategies/schemas.ts +120 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { basePlan, nearestPlace, planRoutes, routeIndex, tidyPlan, tripsOf, type HighwayBase } from "../base.ts";
|
|
3
|
+
import { memberAt, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the highway strategy `weiszfeld` (HWY-043 of the note on highways). */
|
|
6
|
+
export interface WeiszfeldOptions {
|
|
7
|
+
/** The plan whose highways get new meeting points. The default is `shared-ways`. */
|
|
8
|
+
readonly from: HighwayBase;
|
|
9
|
+
/** The weight of the trunk is `k^alpha` for `k` members. With 0, the trunk costs its ink one time. With 1, it costs each lane. The default is 0.5. */
|
|
10
|
+
readonly alpha: number;
|
|
11
|
+
/** The number of steps of Weiszfeld. The default is 20. */
|
|
12
|
+
readonly steps: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The default options of the strategy `weiszfeld`. */
|
|
16
|
+
export const WEISZFELD_DEFAULTS: Readonly<WeiszfeldOptions> = Object.freeze({ from: "shared-ways", alpha: 0.5, steps: 20 });
|
|
17
|
+
|
|
18
|
+
const TINY = 1e-9;
|
|
19
|
+
|
|
20
|
+
/** This function gives one step of Weiszfeld for the point `m`. The point `m` is near the points `ends`, and tied to `other` with the weight `w`. */
|
|
21
|
+
function weiszfeldStep(m: Point, ends: readonly Point[], other: Point, w: number): Point {
|
|
22
|
+
let x = 0;
|
|
23
|
+
let y = 0;
|
|
24
|
+
let sum = 0;
|
|
25
|
+
for (const p of ends) {
|
|
26
|
+
const d = Math.max(TINY, dist(p, m));
|
|
27
|
+
x += p.x / d;
|
|
28
|
+
y += p.y / d;
|
|
29
|
+
sum += 1 / d;
|
|
30
|
+
}
|
|
31
|
+
const d = Math.max(TINY, dist(other, m));
|
|
32
|
+
x += (w * other.x) / d;
|
|
33
|
+
y += (w * other.y) / d;
|
|
34
|
+
sum += w / d;
|
|
35
|
+
return sum > 0 ? { x: x / sum, y: y / sum } : m;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** This function gives the mean of a list of points that is not empty. */
|
|
39
|
+
const mean = (ps: readonly Point[]): Point => ({ x: ps.reduce((s, p) => s + p.x, 0) / ps.length, y: ps.reduce((s, p) => s + p.y, 0) / ps.length });
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function gives the meeting points `M1` and `M2` of a bundle with the least weighted ink (HWY-043):
|
|
43
|
+
* `Σ |a_i - M1| + w |M1 - M2| + Σ |M2 - b_i|`. The lists `a` and `b` are the ends of the edges on each side, and `w` is the weight of the trunk.
|
|
44
|
+
* The function starts at the means of `a` and `b`. Each step moves `M1` and then `M2` by one step of Weiszfeld.
|
|
45
|
+
*/
|
|
46
|
+
export function meetingPoints(a: readonly Point[], b: readonly Point[], w: number, steps: number): [Point, Point] {
|
|
47
|
+
if (a.length === 0 || b.length === 0) throw new Error("A side of the bundle has no end.");
|
|
48
|
+
let m1 = mean(a);
|
|
49
|
+
let m2 = mean(b);
|
|
50
|
+
for (let s = 0; s < steps; s++) {
|
|
51
|
+
m1 = weiszfeldStep(m1, a, m2, w);
|
|
52
|
+
m2 = weiszfeldStep(m2, b, m1, w);
|
|
53
|
+
}
|
|
54
|
+
return [m1, m2];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function gives the highways of the plan `from`, with the meeting points of Weiszfeld (HWY-043).
|
|
59
|
+
* The ends of a member are the points where its edge comes to the highway and goes away from it after `follow` of the plan.
|
|
60
|
+
* They are the plugs, or the split point of the highway before and the merge point of the highway after.
|
|
61
|
+
* The weight of the trunk is `k^alpha` for `k` members. The trunk stays on the spine: the function projects `M1` and `M2` on the spine.
|
|
62
|
+
* Each member then joins the spine at `M1` and leaves it at `M2`, but not outside its own stretch. So a member only gets shorter on the spine.
|
|
63
|
+
*/
|
|
64
|
+
export function weiszfeldPlan(input: HighwayInput, o: Readonly<WeiszfeldOptions>): HighwayPlan {
|
|
65
|
+
const plan = basePlan(input, o.from);
|
|
66
|
+
if (plan.highways.length === 0) return plan;
|
|
67
|
+
const routes = routeIndex(planRoutes(input.graph, plan).routes);
|
|
68
|
+
const trips = tripsOf(plan);
|
|
69
|
+
const highways = plan.highways.map((h, hi) => {
|
|
70
|
+
const a: Point[] = [];
|
|
71
|
+
const b: Point[] = [];
|
|
72
|
+
for (const m of h.members) {
|
|
73
|
+
const trip = trips.get(m.edge) ?? [];
|
|
74
|
+
const route = routes.get(m.edge);
|
|
75
|
+
const k = trip.findIndex((s) => s.h === hi);
|
|
76
|
+
const before = k > 0 ? trip[k - 1]!.m.split : route?.[0];
|
|
77
|
+
const after = k >= 0 && k < trip.length - 1 ? trip[k + 1]!.m.merge : route?.[route.length - 1];
|
|
78
|
+
if (!before || !after) continue;
|
|
79
|
+
if (m.on <= m.off) {
|
|
80
|
+
a.push(before);
|
|
81
|
+
b.push(after);
|
|
82
|
+
} else {
|
|
83
|
+
a.push(after);
|
|
84
|
+
b.push(before);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (a.length < 2) return h;
|
|
88
|
+
const [m1, m2] = meetingPoints(a, b, h.members.length ** Math.max(0, o.alpha), Math.max(0, Math.round(o.steps)));
|
|
89
|
+
const pa = nearestPlace(h.spine, m1);
|
|
90
|
+
const pb = nearestPlace(h.spine, m2);
|
|
91
|
+
if (pb - pa <= 1e-6) return h;
|
|
92
|
+
const members: HighwayMember[] = h.members.map((m) => {
|
|
93
|
+
if (m.on <= m.off) {
|
|
94
|
+
const on = Math.max(m.on, pa);
|
|
95
|
+
const off = Math.min(m.off, pb);
|
|
96
|
+
return off - on > 1e-6 ? memberAt(h.spine, m.edge, on, off, m.seq) : m;
|
|
97
|
+
}
|
|
98
|
+
const on = Math.min(m.on, pb);
|
|
99
|
+
const off = Math.max(m.off, pa);
|
|
100
|
+
return on - off > 1e-6 ? memberAt(h.spine, m.edge, on, off, m.seq) : m;
|
|
101
|
+
});
|
|
102
|
+
return { ...h, members };
|
|
103
|
+
});
|
|
104
|
+
return tidyPlan({ highways });
|
|
105
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { center, dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the highway strategy `wspd` (HWY-018 of the note on highways). */
|
|
6
|
+
export interface WspdOptions {
|
|
7
|
+
/** The separation `s` of the pairs: the gap between two sets is at least `s` times the radius of the larger set. The default is 2. */
|
|
8
|
+
readonly separation: number;
|
|
9
|
+
/** A pair with this number of edges or more gets a highway. The default is 3. */
|
|
10
|
+
readonly minSize: number;
|
|
11
|
+
/** The spine of each pair. The default is `trunk`. */
|
|
12
|
+
readonly spine: GroupSpine;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The default options of the strategy `wspd`. */
|
|
16
|
+
export const WSPD_DEFAULTS: Readonly<WspdOptions> = Object.freeze({ separation: 2, minSize: 3, spine: "trunk" });
|
|
17
|
+
|
|
18
|
+
/** A cell of the split tree: the points at the places `lo` to `hi - 1` of the order, with their bounding circle. */
|
|
19
|
+
export interface SplitCell {
|
|
20
|
+
readonly id: number;
|
|
21
|
+
readonly lo: number;
|
|
22
|
+
readonly hi: number;
|
|
23
|
+
readonly c: Point;
|
|
24
|
+
readonly r: number;
|
|
25
|
+
readonly kids?: readonly [SplitCell, SplitCell];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The split tree of a set of points and its well-separated pairs (Callahan and Kosaraju 1995).
|
|
30
|
+
* Each cell splits at the middle of the longer side of its bounding box. A cell of equal points is a leaf.
|
|
31
|
+
*/
|
|
32
|
+
export class SplitTree {
|
|
33
|
+
readonly root: SplitCell;
|
|
34
|
+
/** The place of each point in the order of the tree. */
|
|
35
|
+
readonly #at: Int32Array;
|
|
36
|
+
#count = 0;
|
|
37
|
+
|
|
38
|
+
/** This constructor builds the tree of the points. */
|
|
39
|
+
constructor(points: readonly Point[]) {
|
|
40
|
+
const order = points.map((_, i) => i);
|
|
41
|
+
this.#at = new Int32Array(points.length);
|
|
42
|
+
this.root = this.#build(points, order, 0, points.length);
|
|
43
|
+
order.forEach((p, i) => (this.#at[p] = i));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#build(points: readonly Point[], order: number[], lo: number, hi: number): SplitCell {
|
|
47
|
+
let x0 = Infinity;
|
|
48
|
+
let y0 = Infinity;
|
|
49
|
+
let x1 = -Infinity;
|
|
50
|
+
let y1 = -Infinity;
|
|
51
|
+
for (let i = lo; i < hi; i++) {
|
|
52
|
+
const p = points[order[i]!]!;
|
|
53
|
+
x0 = Math.min(x0, p.x);
|
|
54
|
+
y0 = Math.min(y0, p.y);
|
|
55
|
+
x1 = Math.max(x1, p.x);
|
|
56
|
+
y1 = Math.max(y1, p.y);
|
|
57
|
+
}
|
|
58
|
+
const id = this.#count++;
|
|
59
|
+
const c = center({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
|
|
60
|
+
const r = Math.hypot(x1 - x0, y1 - y0) / 2;
|
|
61
|
+
if (hi - lo <= 1 || r <= 1e-9) return { id, lo, hi, c, r };
|
|
62
|
+
const byX = x1 - x0 >= y1 - y0;
|
|
63
|
+
const cut = byX ? (x0 + x1) / 2 : (y0 + y1) / 2;
|
|
64
|
+
const part = order.slice(lo, hi);
|
|
65
|
+
const left = part.filter((i) => (byX ? points[i]!.x : points[i]!.y) <= cut);
|
|
66
|
+
const right = part.filter((i) => (byX ? points[i]!.x : points[i]!.y) > cut);
|
|
67
|
+
[...left, ...right].forEach((p, k) => (order[lo + k] = p));
|
|
68
|
+
const mid = lo + left.length;
|
|
69
|
+
return { id, lo, hi, c, r, kids: [this.#build(points, order, lo, mid), this.#build(points, order, mid, hi)] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** This method tells if the cell holds the point `i`. */
|
|
73
|
+
holds(cell: SplitCell, i: number): boolean {
|
|
74
|
+
const at = this.#at[i]!;
|
|
75
|
+
return at >= cell.lo && at < cell.hi;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* This method gives the well-separated pair that holds the pair of points `u` and `v`, with the cell of `u` first.
|
|
80
|
+
* Each pair of two different points is in one pair of the decomposition. Two equal points give `undefined`.
|
|
81
|
+
*/
|
|
82
|
+
pairOf(u: number, v: number, s: number): [SplitCell, SplitCell] | undefined {
|
|
83
|
+
let cell = this.root;
|
|
84
|
+
while (cell.kids) {
|
|
85
|
+
const next = cell.kids.find((k) => this.holds(k, u) && this.holds(k, v));
|
|
86
|
+
if (!next) break;
|
|
87
|
+
cell = next;
|
|
88
|
+
}
|
|
89
|
+
if (!cell.kids) return undefined;
|
|
90
|
+
let a = cell.kids.find((k) => this.holds(k, u))!;
|
|
91
|
+
let b = cell.kids.find((k) => this.holds(k, v))!;
|
|
92
|
+
for (;;) {
|
|
93
|
+
const r = Math.max(a.r, b.r);
|
|
94
|
+
if (dist(a.c, b.c) - 2 * r >= s * r) return [a, b];
|
|
95
|
+
if (a.r >= b.r && a.kids) a = a.kids.find((k) => this.holds(k, u))!;
|
|
96
|
+
else if (b.kids) b = b.kids.find((k) => this.holds(k, v))!;
|
|
97
|
+
else if (a.kids) a = a.kids.find((k) => this.holds(k, u))!;
|
|
98
|
+
else return [a, b];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This function gives the groups of the edges by the well-separated pair of their two nodes (HWY-018).
|
|
105
|
+
* The points are the centers of the boxes. The edges of one pair, in either direction, are one group.
|
|
106
|
+
*/
|
|
107
|
+
export function wspdGroups(input: HighwayInput, o: Readonly<WspdOptions>): Map<string, EdgeGroup> {
|
|
108
|
+
const nodes = input.graph.nodes;
|
|
109
|
+
const index = new Map(nodes.map((n, i) => [n.id, i]));
|
|
110
|
+
const tree = new SplitTree(nodes.map((n) => center(n.box)));
|
|
111
|
+
const groups = new Map<string, EdgeGroup>();
|
|
112
|
+
for (const e of input.graph.edges) {
|
|
113
|
+
const u = index.get(e.source.node);
|
|
114
|
+
const v = index.get(e.target.node);
|
|
115
|
+
if (u === undefined || v === undefined || u === v) continue;
|
|
116
|
+
const pair = tree.pairOf(u, v, o.separation);
|
|
117
|
+
if (!pair) continue;
|
|
118
|
+
const [a, b] = pair;
|
|
119
|
+
groups.set(e.id, a.id < b.id ? { key: `w${a.id}:${b.id}`, flip: false } : { key: `w${b.id}:${a.id}`, flip: true });
|
|
120
|
+
}
|
|
121
|
+
return groups;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* This function gives the highways of clustered edge routing with the WSPD (HWY-018, after Bouts and Speckmann 2015).
|
|
126
|
+
* The well-separated pairs split all the pairs of nodes. The edges of one pair with `minSize` edges or more get one highway from one set to the other.
|
|
127
|
+
*/
|
|
128
|
+
export function wspdPlan(input: HighwayInput, o: Readonly<WspdOptions>): HighwayPlan {
|
|
129
|
+
return groupedPlan(input, wspdGroups(input, o), { spine: o.spine, minSize: o.minSize });
|
|
130
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { boardUnit, segmentKey } from "../../bench/measures/routes.ts";
|
|
2
|
+
import { dist, type Point } from "../../geom.ts";
|
|
3
|
+
import { Grid, segmentBounds } from "../../grid.ts";
|
|
4
|
+
import { WayFinder } from "../../ways.ts";
|
|
5
|
+
import { basePlan, orderAlongRoutes, planRoutes, routeIndex, tidyPlan, type HighwayBase } from "../base.ts";
|
|
6
|
+
import { memberAt, type Highway, type HighwayInput, type HighwayPlan } from "../model.ts";
|
|
7
|
+
|
|
8
|
+
/** The options of the highway strategy `zipper` (NAT-022 with WILD-037 of the notes). */
|
|
9
|
+
export interface ZipperOptions {
|
|
10
|
+
/** The plan whose routes the zipper joins. The default is `shared-ways`. */
|
|
11
|
+
readonly from: HighwayBase;
|
|
12
|
+
/** Two legs zip when they are this distance apart or nearer, in units of the board (the median short side of the boxes). The default is 1. */
|
|
13
|
+
readonly near: number;
|
|
14
|
+
/** Two legs zip when the angle between them is this value or less, in degrees. The default is 15. */
|
|
15
|
+
readonly maxAngle: number;
|
|
16
|
+
/** The adhesion over the tension: two legs zip when `adhesion * overlap` is more than the length that the zip adds. The default is 0.3. */
|
|
17
|
+
readonly adhesion: number;
|
|
18
|
+
/** Two legs zip when they overlap along this length or more, in units of the board. The default is 1. */
|
|
19
|
+
readonly minOverlap: number;
|
|
20
|
+
/** The number of passes. Each pass routes the plan and zips the legs again. The default is 1. */
|
|
21
|
+
readonly passes: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The default options of the strategy `zipper`. */
|
|
25
|
+
export const ZIPPER_DEFAULTS: Readonly<ZipperOptions> = Object.freeze({ from: "shared-ways", near: 1, maxAngle: 15, adhesion: 0.3, minOverlap: 1, passes: 1 });
|
|
26
|
+
|
|
27
|
+
/** One leg of a route that no other route shares. */
|
|
28
|
+
interface Leg {
|
|
29
|
+
readonly edge: string;
|
|
30
|
+
readonly a: Point;
|
|
31
|
+
readonly b: Point;
|
|
32
|
+
readonly len: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A pair of legs that can zip, with the line of the zip. */
|
|
36
|
+
export interface Zip {
|
|
37
|
+
readonly gain: number;
|
|
38
|
+
/** The first point of the zipped line. */
|
|
39
|
+
readonly from: Point;
|
|
40
|
+
/** The last point of the zipped line. */
|
|
41
|
+
readonly to: Point;
|
|
42
|
+
/** This value is `true` when the leg goes from `from` to `to`, for each of the two legs. */
|
|
43
|
+
readonly forward: readonly [boolean, boolean];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The limits of `zipLegs`, in the units of the board. */
|
|
47
|
+
export interface ZipLimits {
|
|
48
|
+
readonly near: number;
|
|
49
|
+
readonly maxAngle: number;
|
|
50
|
+
readonly adhesion: number;
|
|
51
|
+
readonly minOverlap: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This function tells if two legs zip (NAT-022), and gives the zipped line.
|
|
56
|
+
* The zipped line is the mean of the two legs, with the weight of their lengths, along the stretch where they overlap.
|
|
57
|
+
* The legs zip when the angle between them is `maxAngle` or less, and they are `near` or nearer along the overlap.
|
|
58
|
+
* The overlap must be `minOverlap` or more.
|
|
59
|
+
* The zip adds two jogs to each leg, so it adds the length `2 (d_a + d_b)`, with the distances of the legs to the zipped line.
|
|
60
|
+
* The legs zip only when `adhesion * overlap` is more than that length.
|
|
61
|
+
*/
|
|
62
|
+
export function zipLegs(a0: Point, a1: Point, b0: Point, b1: Point, o: ZipLimits): Zip | undefined {
|
|
63
|
+
const la = dist(a0, a1);
|
|
64
|
+
const lb = dist(b0, b1);
|
|
65
|
+
if (la <= 1e-9 || lb <= 1e-9) return undefined;
|
|
66
|
+
const ua = { x: (a1.x - a0.x) / la, y: (a1.y - a0.y) / la };
|
|
67
|
+
const ub = { x: (b1.x - b0.x) / lb, y: (b1.y - b0.y) / lb };
|
|
68
|
+
const dot = ua.x * ub.x + ua.y * ub.y;
|
|
69
|
+
if (Math.abs(dot) < Math.cos((o.maxAngle * Math.PI) / 180) - 1e-12) return undefined;
|
|
70
|
+
const sign = dot >= 0 ? 1 : -1;
|
|
71
|
+
const dx = ua.x * la + sign * ub.x * lb;
|
|
72
|
+
const dy = ua.y * la + sign * ub.y * lb;
|
|
73
|
+
const dl = Math.hypot(dx, dy);
|
|
74
|
+
if (dl <= 1e-9) return undefined;
|
|
75
|
+
const u = { x: dx / dl, y: dy / dl };
|
|
76
|
+
const o0 = { x: ((a0.x + a1.x) * la + (b0.x + b1.x) * lb) / (2 * (la + lb)), y: ((a0.y + a1.y) * la + (b0.y + b1.y) * lb) / (2 * (la + lb)) };
|
|
77
|
+
const along = (p: Point): number => (p.x - o0.x) * u.x + (p.y - o0.y) * u.y;
|
|
78
|
+
const across = (p: Point): number => (p.x - o0.x) * u.y - (p.y - o0.y) * u.x;
|
|
79
|
+
const ta = [along(a0), along(a1)] as const;
|
|
80
|
+
const tb = [along(b0), along(b1)] as const;
|
|
81
|
+
const lo = Math.max(Math.min(...ta), Math.min(...tb));
|
|
82
|
+
const hi = Math.min(Math.max(...ta), Math.max(...tb));
|
|
83
|
+
const overlap = hi - lo;
|
|
84
|
+
if (overlap < o.minOverlap || overlap <= 1e-9) return undefined;
|
|
85
|
+
// The offset of a leg from the zipped line at the place t along it.
|
|
86
|
+
const offset = (p0: Point, p1: Point, t0: number, t1: number, t: number): number => {
|
|
87
|
+
const s = Math.abs(t1 - t0) <= 1e-12 ? 0 : (t - t0) / (t1 - t0);
|
|
88
|
+
return across({ x: p0.x + (p1.x - p0.x) * s, y: p0.y + (p1.y - p0.y) * s });
|
|
89
|
+
};
|
|
90
|
+
let jog = 0;
|
|
91
|
+
for (const t of [lo, hi]) {
|
|
92
|
+
const da = offset(a0, a1, ta[0], ta[1], t);
|
|
93
|
+
const db = offset(b0, b1, tb[0], tb[1], t);
|
|
94
|
+
if (Math.abs(da - db) > o.near) return undefined;
|
|
95
|
+
jog += Math.abs(da) + Math.abs(db);
|
|
96
|
+
}
|
|
97
|
+
const gain = o.adhesion * overlap - jog;
|
|
98
|
+
if (!(gain > 0)) return undefined;
|
|
99
|
+
const from = { x: o0.x + u.x * lo, y: o0.y + u.y * lo };
|
|
100
|
+
const to = { x: o0.x + u.x * hi, y: o0.y + u.y * hi };
|
|
101
|
+
return { gain, from, to, forward: [ua.x * u.x + ua.y * u.y > 0, ub.x * u.x + ub.y * u.y > 0] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* This function gives the highways of the plan `from`, with the legs that run near and nearly parallel zipped together (NAT-022, WILD-037).
|
|
106
|
+
* A pass routes the plan with `follow` and takes each leg that no other route shares. A grid of the legs finds the near pairs.
|
|
107
|
+
* The pairs that zip (`zipLegs`) go from the largest gain to the smallest, and each leg zips one time in a pass at most.
|
|
108
|
+
* A zipped line that goes into a box, made larger by 1, does not zip.
|
|
109
|
+
* Each zip is a new highway of two members. The `seq` of each member follows the place of its merge point along the route of the edge.
|
|
110
|
+
*/
|
|
111
|
+
export function zipperPlan(input: HighwayInput, o: Readonly<ZipperOptions>): HighwayPlan {
|
|
112
|
+
const graph = input.graph;
|
|
113
|
+
const unit = boardUnit(graph);
|
|
114
|
+
const limits: ZipLimits = { near: o.near * unit, maxAngle: o.maxAngle, adhesion: o.adhesion, minOverlap: Math.max(1e-6, o.minOverlap * unit) };
|
|
115
|
+
const obstacles = new WayFinder(graph.nodes).obstacles;
|
|
116
|
+
let plan = basePlan(input, o.from);
|
|
117
|
+
for (let pass = 0; pass < Math.max(1, Math.round(o.passes)); pass++) {
|
|
118
|
+
const routes = planRoutes(graph, plan).routes;
|
|
119
|
+
const users = new Map<string, number>();
|
|
120
|
+
for (const r of routes) {
|
|
121
|
+
const own = new Set<string>();
|
|
122
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
123
|
+
const k = segmentKey(r.points[i - 1]!, r.points[i]!);
|
|
124
|
+
if (k !== undefined) own.add(k);
|
|
125
|
+
}
|
|
126
|
+
for (const k of own) users.set(k, (users.get(k) ?? 0) + 1);
|
|
127
|
+
}
|
|
128
|
+
const legs: Leg[] = [];
|
|
129
|
+
for (const r of routes)
|
|
130
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
131
|
+
const a = r.points[i - 1]!;
|
|
132
|
+
const b = r.points[i]!;
|
|
133
|
+
const k = segmentKey(a, b);
|
|
134
|
+
const len = dist(a, b);
|
|
135
|
+
if (k === undefined || (users.get(k) ?? 0) > 1 || len < limits.minOverlap) continue;
|
|
136
|
+
legs.push({ edge: r.edge, a, b, len });
|
|
137
|
+
}
|
|
138
|
+
const grid = new Grid<number>(Math.max(32, 4 * limits.near));
|
|
139
|
+
legs.forEach((l, i) => {
|
|
140
|
+
const bx = segmentBounds(l.a, l.b);
|
|
141
|
+
grid.add(i, { x: bx.x - limits.near, y: bx.y - limits.near, w: bx.w + 2 * limits.near, h: bx.h + 2 * limits.near });
|
|
142
|
+
});
|
|
143
|
+
const found: { i: number; j: number; zip: Zip }[] = [];
|
|
144
|
+
legs.forEach((l, i) => {
|
|
145
|
+
grid.query(segmentBounds(l.a, l.b), (j) => {
|
|
146
|
+
if (j <= i || legs[j]!.edge === l.edge) return;
|
|
147
|
+
const m = legs[j]!;
|
|
148
|
+
const zip = zipLegs(l.a, l.b, m.a, m.b, limits);
|
|
149
|
+
if (zip) found.push({ i, j, zip });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
found.sort((p, q) => q.zip.gain - p.zip.gain || p.i - q.i || p.j - q.j);
|
|
153
|
+
const used = new Set<number>();
|
|
154
|
+
const added: Highway[] = [];
|
|
155
|
+
for (const { i, j, zip } of found) {
|
|
156
|
+
if (used.has(i) || used.has(j)) continue;
|
|
157
|
+
if (!obstacles.clear(zip.from, zip.to, 1)) continue;
|
|
158
|
+
used.add(i);
|
|
159
|
+
used.add(j);
|
|
160
|
+
const spine = [zip.from, zip.to];
|
|
161
|
+
const members = [legs[i]!, legs[j]!].map((l, k) => (zip.forward[k] ? memberAt(spine, l.edge, 0, 1) : memberAt(spine, l.edge, 1, 0)));
|
|
162
|
+
added.push({ id: "", key: `zip${pass}`, spine, lanes: members.map((m) => m.edge), members });
|
|
163
|
+
}
|
|
164
|
+
if (added.length === 0) break;
|
|
165
|
+
plan = tidyPlan(orderAlongRoutes({ highways: [...plan.highways, ...added] }, routeIndex(routes)));
|
|
166
|
+
}
|
|
167
|
+
return tidyPlan(plan);
|
|
168
|
+
}
|
|
@@ -12,9 +12,26 @@ import { KD_DEFAULTS, kdGroups, type KdOptions } from "../groups/kd/index.ts";
|
|
|
12
12
|
import { byDataGroup, keyGroups, type KeyOptions } from "../groups/keys/index.ts";
|
|
13
13
|
import { quadtreeGroups } from "../groups/quadtree/index.ts";
|
|
14
14
|
import { CORRIDOR_DEFAULTS, corridorPlan, type CorridorOptions } from "../highways/corridors/index.ts";
|
|
15
|
+
import { ambiguityGate, type AmbiguityGuardOptions } from "../highways/gates/ambiguity/index.ts";
|
|
16
|
+
import { breakEvenGate, type BreakEvenOptions } from "../highways/gates/breakeven/index.ts";
|
|
17
|
+
import { hysteresisGate, type HysteresisOptions } from "../highways/gates/hysteresis/index.ts";
|
|
18
|
+
import { stableIdGate, type StableIdOptions } from "../highways/gates/ids/index.ts";
|
|
19
|
+
import { gateStrategy } from "../highways/gates/model.ts";
|
|
20
|
+
import { stretchGate, type StretchOptions } from "../highways/gates/stretch/index.ts";
|
|
21
|
+
import { FLOW_FIELD_DEFAULTS, flowFieldPlan, type FlowFieldOptions } from "../highways/flow-field/index.ts";
|
|
15
22
|
import { keysPlan, type KeysOptions } from "../highways/keys/index.ts";
|
|
23
|
+
import { LEVEL_HIGHWAY_DEFAULTS, levelHighwayPlan, type LevelHighwayOptions } from "../highways/level-links/index.ts";
|
|
24
|
+
import { MEET_DEFAULTS, meetPlan, type MeetOptions } from "../highways/meet/index.ts";
|
|
16
25
|
import type { HighwayInput } from "../highways/model.ts";
|
|
26
|
+
import { OD_CELL_DEFAULTS, odCellPlan, type OdCellOptions } from "../highways/od-cells/index.ts";
|
|
27
|
+
import { QUICKBUNDLES_DEFAULTS, quickBundlesPlan, type QuickBundlesOptions } from "../highways/quickbundles/index.ts";
|
|
28
|
+
import { ROAD_CLASS_DEFAULTS, roadClassPlan, type RoadClassOptions } from "../highways/road-classes/index.ts";
|
|
29
|
+
import { SELECT_DEFAULTS, selectPlan, type SelectOptions } from "../highways/select/index.ts";
|
|
17
30
|
import { sharedWaysPlan, type SharedWaysOptions } from "../highways/shared-ways/index.ts";
|
|
31
|
+
import { STUB_DEFAULTS, stubPlan, type StubOptions } from "../highways/stubs/index.ts";
|
|
32
|
+
import { WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "../highways/weiszfeld/index.ts";
|
|
33
|
+
import { WSPD_DEFAULTS, wspdPlan, type WspdOptions } from "../highways/wspd/index.ts";
|
|
34
|
+
import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zipper/index.ts";
|
|
18
35
|
import type { Graph, Result, Router } from "../model.ts";
|
|
19
36
|
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
20
37
|
import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
@@ -27,7 +44,7 @@ import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunk
|
|
|
27
44
|
import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
|
|
28
45
|
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
|
|
29
46
|
import type { OptionSchema } from "./options.ts";
|
|
30
|
-
import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CUT_SCHEMA, HIGHWAY_SCHEMA, KD_SCHEMA, KEY_SCHEMA, LEVEL_ROOM_SCHEMA, RADIUS_SCHEMA, ROOM_SCHEMA, SECTION_SCHEMA, SECTIONS_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, TENSION_SCHEMA, TRUNKS_SCHEMA } from "./schemas.ts";
|
|
47
|
+
import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CUT_SCHEMA, FLOW_FIELD_SCHEMA, HIGHWAY_SCHEMA, KD_SCHEMA, KEY_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, MEET_SCHEMA, OD_CELL_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA } from "./schemas.ts";
|
|
31
48
|
|
|
32
49
|
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
33
50
|
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
@@ -340,14 +357,43 @@ export const groupStrategies: {
|
|
|
340
357
|
}),
|
|
341
358
|
});
|
|
342
359
|
|
|
360
|
+
/** The highway strategy `corridors` (HWY-031): the base of the built-in gates. */
|
|
361
|
+
const corridors: Strategy<"highway", CorridorOptions> = defineStrategy({
|
|
362
|
+
step: "highway",
|
|
363
|
+
name: "corridors",
|
|
364
|
+
describe: "Corridor signatures. Edges whose first ways share long links make a cluster, found with MinHash. The longest common run of each cluster is its highway.",
|
|
365
|
+
defaults: { ...CORRIDOR_DEFAULTS } as CorridorOptions,
|
|
366
|
+
schema: CORRIDOR_SCHEMA,
|
|
367
|
+
maxNodes: 10_000,
|
|
368
|
+
run: (input: HighwayInput, o: Readonly<CorridorOptions>) => corridorPlan(input, o),
|
|
369
|
+
});
|
|
370
|
+
|
|
343
371
|
/**
|
|
344
372
|
* The built-in highway strategies: the decisions of the routers `trunks` and `shared` as plans, and corridor signatures (HWY-031).
|
|
373
|
+
* The strategies `corridors+<gate>` put a gate of BUILD-W2A after `corridors`: `breakeven` (HWY-047), `stretch` (HWY-048), `ambiguity` (HWY-049),
|
|
374
|
+
* `hysteresis` (HWY-046) and `ids` (HWY-054). `gateStrategy` puts a gate after any other plan.
|
|
345
375
|
* `follow` routes the edges along a plan, and `highwayRouter` gives a router that does both.
|
|
346
376
|
*/
|
|
347
377
|
export const highwayStrategies: {
|
|
348
378
|
readonly keys: Strategy<"highway", KeysOptions>;
|
|
349
379
|
readonly "shared-ways": Strategy<"highway", SharedWaysOptions>;
|
|
350
380
|
readonly corridors: Strategy<"highway", CorridorOptions>;
|
|
381
|
+
readonly meet: Strategy<"highway", MeetOptions>;
|
|
382
|
+
readonly weiszfeld: Strategy<"highway", WeiszfeldOptions>;
|
|
383
|
+
readonly stubs: Strategy<"highway", StubOptions>;
|
|
384
|
+
readonly zipper: Strategy<"highway", ZipperOptions>;
|
|
385
|
+
readonly "level-links": Strategy<"highway", LevelHighwayOptions>;
|
|
386
|
+
readonly quickbundles: Strategy<"highway", QuickBundlesOptions>;
|
|
387
|
+
readonly "road-classes": Strategy<"highway", RoadClassOptions>;
|
|
388
|
+
readonly "flow-field": Strategy<"highway", FlowFieldOptions>;
|
|
389
|
+
readonly wspd: Strategy<"highway", WspdOptions>;
|
|
390
|
+
readonly "od-cells": Strategy<"highway", OdCellOptions>;
|
|
391
|
+
readonly select: Strategy<"highway", SelectOptions>;
|
|
392
|
+
readonly "corridors+breakeven": Strategy<"highway", CorridorOptions & BreakEvenOptions>;
|
|
393
|
+
readonly "corridors+stretch": Strategy<"highway", CorridorOptions & StretchOptions>;
|
|
394
|
+
readonly "corridors+ambiguity": Strategy<"highway", CorridorOptions & AmbiguityGuardOptions>;
|
|
395
|
+
readonly "corridors+hysteresis": Strategy<"highway", CorridorOptions & HysteresisOptions>;
|
|
396
|
+
readonly "corridors+ids": Strategy<"highway", CorridorOptions & StableIdOptions>;
|
|
351
397
|
} = Object.freeze({
|
|
352
398
|
keys: defineStrategy({
|
|
353
399
|
step: "highway",
|
|
@@ -367,15 +413,111 @@ export const highwayStrategies: {
|
|
|
367
413
|
maxNodes: 10_000,
|
|
368
414
|
run: (input: HighwayInput, o: Readonly<SharedWaysOptions>) => sharedWaysPlan(input, o),
|
|
369
415
|
}),
|
|
370
|
-
corridors
|
|
416
|
+
corridors,
|
|
417
|
+
meet: defineStrategy({
|
|
418
|
+
step: "highway",
|
|
419
|
+
name: "meet",
|
|
420
|
+
describe: "Meet points (HWY-050). Each member of the plan `from` joins its spine where its first route meets the spine, and leaves where they part.",
|
|
421
|
+
defaults: { ...MEET_DEFAULTS } as MeetOptions,
|
|
422
|
+
schema: MEET_SCHEMA,
|
|
423
|
+
maxNodes: 10_000,
|
|
424
|
+
run: (input: HighwayInput, o: Readonly<MeetOptions>) => meetPlan(input, o),
|
|
425
|
+
}),
|
|
426
|
+
weiszfeld: defineStrategy({
|
|
427
|
+
step: "highway",
|
|
428
|
+
name: "weiszfeld",
|
|
429
|
+
describe: "Weiszfeld meeting points (HWY-043). Each highway of the plan `from` gets the merge and split points with the least ink, with a weighted trunk.",
|
|
430
|
+
defaults: { ...WEISZFELD_DEFAULTS } as WeiszfeldOptions,
|
|
431
|
+
schema: WEISZFELD_SCHEMA,
|
|
432
|
+
maxNodes: 10_000,
|
|
433
|
+
run: (input: HighwayInput, o: Readonly<WeiszfeldOptions>) => weiszfeldPlan(input, o),
|
|
434
|
+
}),
|
|
435
|
+
stubs: defineStrategy({
|
|
436
|
+
step: "highway",
|
|
437
|
+
name: "stubs",
|
|
438
|
+
describe: "Stubs (HWY-015, WILD-005). The ends on one side of a box that go in near directions share one plug and one short stub.",
|
|
439
|
+
defaults: { ...STUB_DEFAULTS } as StubOptions,
|
|
440
|
+
schema: STUB_SCHEMA,
|
|
441
|
+
maxNodes: 10_000,
|
|
442
|
+
run: (input: HighwayInput, o: Readonly<StubOptions>) => stubPlan(input, o),
|
|
443
|
+
}),
|
|
444
|
+
zipper: defineStrategy({
|
|
445
|
+
step: "highway",
|
|
446
|
+
name: "zipper",
|
|
447
|
+
describe: "The axon zipper (NAT-022, WILD-037). Two near, nearly parallel legs zip into one line when the shared length is more than the jogs.",
|
|
448
|
+
defaults: { ...ZIPPER_DEFAULTS } as ZipperOptions,
|
|
449
|
+
schema: ZIPPER_SCHEMA,
|
|
450
|
+
maxNodes: 10_000,
|
|
451
|
+
run: (input: HighwayInput, o: Readonly<ZipperOptions>) => zipperPlan(input, o),
|
|
452
|
+
}),
|
|
453
|
+
"level-links": defineStrategy({
|
|
454
|
+
step: "highway",
|
|
455
|
+
name: "level-links",
|
|
456
|
+
describe: "Level links as highways (HWY-056, WILD-034). The edges between two items of a zoom level ride the link of the level.",
|
|
457
|
+
defaults: { ...LEVEL_HIGHWAY_DEFAULTS } as LevelHighwayOptions,
|
|
458
|
+
schema: LEVEL_HIGHWAY_SCHEMA,
|
|
459
|
+
maxNodes: 10_000,
|
|
460
|
+
run: (input: HighwayInput, o: Readonly<LevelHighwayOptions>) => levelHighwayPlan(input, o),
|
|
461
|
+
}),
|
|
462
|
+
quickbundles: defineStrategy({
|
|
463
|
+
step: "highway",
|
|
464
|
+
name: "quickbundles",
|
|
465
|
+
describe: "QuickBundles (HWY-026). One pass puts each edge in the nearest cluster of resampled lines. Each cluster gets highways.",
|
|
466
|
+
defaults: { ...QUICKBUNDLES_DEFAULTS } as QuickBundlesOptions,
|
|
467
|
+
schema: QUICKBUNDLES_SCHEMA,
|
|
468
|
+
maxNodes: 10_000,
|
|
469
|
+
run: (input: HighwayInput, o: Readonly<QuickBundlesOptions>) => quickBundlesPlan(input, o),
|
|
470
|
+
}),
|
|
471
|
+
"road-classes": defineStrategy({
|
|
472
|
+
step: "highway",
|
|
473
|
+
name: "road-classes",
|
|
474
|
+
describe: "Road classes (HWY-034, EDA-050). The links that most ways use become main roads, and a second pass routes the edges onto them.",
|
|
475
|
+
defaults: { ...ROAD_CLASS_DEFAULTS } as RoadClassOptions,
|
|
476
|
+
schema: ROAD_CLASS_SCHEMA,
|
|
477
|
+
maxNodes: 10_000,
|
|
478
|
+
run: (input: HighwayInput, o: Readonly<RoadClassOptions>) => roadClassPlan(input, o),
|
|
479
|
+
}),
|
|
480
|
+
"flow-field": defineStrategy({
|
|
481
|
+
step: "highway",
|
|
482
|
+
name: "flow-field",
|
|
483
|
+
describe: "A flow field for each target (EDA-046, HWY-037). The edges into one side of a box merge like streams on shortest ways to one root.",
|
|
484
|
+
defaults: { ...FLOW_FIELD_DEFAULTS } as FlowFieldOptions,
|
|
485
|
+
schema: FLOW_FIELD_SCHEMA,
|
|
486
|
+
maxNodes: 10_000,
|
|
487
|
+
run: (input: HighwayInput, o: Readonly<FlowFieldOptions>) => flowFieldPlan(input, o),
|
|
488
|
+
}),
|
|
489
|
+
wspd: defineStrategy({
|
|
490
|
+
step: "highway",
|
|
491
|
+
name: "wspd",
|
|
492
|
+
describe: "Well-separated pairs (HWY-018). The edges between the two sets of one pair share one highway.",
|
|
493
|
+
defaults: { ...WSPD_DEFAULTS } as WspdOptions,
|
|
494
|
+
schema: WSPD_SCHEMA,
|
|
495
|
+
maxNodes: 10_000,
|
|
496
|
+
run: (input: HighwayInput, o: Readonly<WspdOptions>) => wspdPlan(input, o),
|
|
497
|
+
}),
|
|
498
|
+
"od-cells": defineStrategy({
|
|
499
|
+
step: "highway",
|
|
500
|
+
name: "od-cells",
|
|
501
|
+
describe: "Origin and destination cells (EDA-033, WILD-032). The edges between two cells of a zoom level share one trunk.",
|
|
502
|
+
defaults: { ...OD_CELL_DEFAULTS } as OdCellOptions,
|
|
503
|
+
schema: OD_CELL_SCHEMA,
|
|
504
|
+
maxNodes: 10_000,
|
|
505
|
+
run: (input: HighwayInput, o: Readonly<OdCellOptions>) => odCellPlan(input, o),
|
|
506
|
+
}),
|
|
507
|
+
select: defineStrategy({
|
|
371
508
|
step: "highway",
|
|
372
|
-
name: "
|
|
373
|
-
describe: "
|
|
374
|
-
defaults: { ...
|
|
375
|
-
schema:
|
|
509
|
+
name: "select",
|
|
510
|
+
describe: "Candidate generation and selection (HWY-045). Some strategies propose highways, and a greedy pass with swaps keeps the best set.",
|
|
511
|
+
defaults: { ...SELECT_DEFAULTS } as SelectOptions,
|
|
512
|
+
schema: SELECT_SCHEMA,
|
|
376
513
|
maxNodes: 10_000,
|
|
377
|
-
run: (input: HighwayInput, o: Readonly<
|
|
514
|
+
run: (input: HighwayInput, o: Readonly<SelectOptions>) => selectPlan(input, o),
|
|
378
515
|
}),
|
|
516
|
+
"corridors+breakeven": gateStrategy(corridors, breakEvenGate),
|
|
517
|
+
"corridors+stretch": gateStrategy(corridors, stretchGate),
|
|
518
|
+
"corridors+ambiguity": gateStrategy(corridors, ambiguityGate),
|
|
519
|
+
"corridors+hysteresis": gateStrategy(corridors, hysteresisGate),
|
|
520
|
+
"corridors+ids": gateStrategy(corridors, stableIdGate),
|
|
379
521
|
});
|
|
380
522
|
|
|
381
523
|
/** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
|