@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,296 @@
|
|
|
1
|
+
import { boardUnit } from "../../bench/measures/routes.ts";
|
|
2
|
+
import { dist, pointSegmentDistance, polylineLength, type Point } from "../../geom.ts";
|
|
3
|
+
import { orderLanes } from "../../segments.ts";
|
|
4
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
5
|
+
import { firstRoutes, nearestPlace, orderAlongRoutes, tidyPlan } from "../base.ts";
|
|
6
|
+
import { lengthBetween, memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
|
|
8
|
+
/** The options of the highway strategy `traclus` (HWY-025 of the note on highways). */
|
|
9
|
+
export interface TraclusOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
10
|
+
/** Two segments are neighbours when their distance is this value or less, in units of the board. The default is 2. */
|
|
11
|
+
readonly eps: number;
|
|
12
|
+
/** A cluster has segments of this number of edges or more, and a core segment has this number of neighbours or more. The default is 3. */
|
|
13
|
+
readonly minLns: number;
|
|
14
|
+
/** The weight of the perpendicular distance. The default is 1. */
|
|
15
|
+
readonly wPerp: number;
|
|
16
|
+
/** The weight of the parallel distance. The default is 1. */
|
|
17
|
+
readonly wPar: number;
|
|
18
|
+
/** The weight of the angle distance. The default is 1. */
|
|
19
|
+
readonly wTheta: number;
|
|
20
|
+
/** A route keeps a corner when the cost of the line without it is more than this value times the cost with it. The default is 1. */
|
|
21
|
+
readonly mdl: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The default options of the strategy `traclus`. */
|
|
25
|
+
export const TRACLUS_DEFAULTS: Readonly<Required<Omit<TraclusOptions, "margin" | "k">>> = Object.freeze({ eps: 2, minLns: 3, wPerp: 1, wPar: 1, wTheta: 1, mdl: 1 });
|
|
26
|
+
|
|
27
|
+
/** One segment of a partitioned route: the edge, the index of the segment in the route, and its two ends. */
|
|
28
|
+
export interface RouteSegment {
|
|
29
|
+
readonly edge: string;
|
|
30
|
+
readonly index: number;
|
|
31
|
+
readonly a: Point;
|
|
32
|
+
readonly b: Point;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** This function gives the distance of the point `p` to the line through `a` and `b`. It also gives the place of its projection as a share of `ab`. */
|
|
36
|
+
function project(p: Point, a: Point, b: Point): { perp: number; u: number } {
|
|
37
|
+
const dx = b.x - a.x;
|
|
38
|
+
const dy = b.y - a.y;
|
|
39
|
+
const len2 = dx * dx + dy * dy;
|
|
40
|
+
if (len2 < 1e-12) return { perp: dist(p, a), u: 0 };
|
|
41
|
+
const u = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
42
|
+
return { perp: Math.abs((p.x - a.x) * dy - (p.y - a.y) * dx) / Math.sqrt(len2), u };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* This function gives the TRACLUS distance of two segments (Lee, Han and Whang 2007): `w_perp d_perp + w_par d_par + w_theta d_theta`.
|
|
47
|
+
* `d_perp` is `(l1² + l2²) / (l1 + l2)` with the distances of the ends of the shorter segment to the line of the longer segment.
|
|
48
|
+
* `d_par` is the gap along the longer segment between the two segments, 0 when they overlap. `d_theta` is `|shorter| sin(theta)`.
|
|
49
|
+
* The segments have no direction, so the angle is 90 degrees or less.
|
|
50
|
+
*/
|
|
51
|
+
export function segmentDistance(p: { a: Point; b: Point }, q: { a: Point; b: Point }, w: { perp: number; par: number; theta: number }): number {
|
|
52
|
+
const [long, short] = dist(p.a, p.b) >= dist(q.a, q.b) ? [p, q] : [q, p];
|
|
53
|
+
const len = dist(long.a, long.b);
|
|
54
|
+
const s = project(short.a, long.a, long.b);
|
|
55
|
+
const e = project(short.b, long.a, long.b);
|
|
56
|
+
const perp = s.perp + e.perp > 1e-12 ? (s.perp * s.perp + e.perp * e.perp) / (s.perp + e.perp) : 0;
|
|
57
|
+
const lo = Math.min(s.u, e.u);
|
|
58
|
+
const hi = Math.max(s.u, e.u);
|
|
59
|
+
const par = (hi < 0 ? -hi : lo > 1 ? lo - 1 : 0) * len;
|
|
60
|
+
const shortLen = dist(short.a, short.b);
|
|
61
|
+
let theta = 0;
|
|
62
|
+
if (len > 1e-12 && shortLen > 1e-12) {
|
|
63
|
+
const cos = Math.abs(((long.b.x - long.a.x) * (short.b.x - short.a.x) + (long.b.y - long.a.y) * (short.b.y - short.a.y)) / (len * shortLen));
|
|
64
|
+
theta = shortLen * Math.sqrt(Math.max(0, 1 - cos * cos));
|
|
65
|
+
}
|
|
66
|
+
return w.perp * perp + w.par * par + w.theta * theta;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function gives the characteristic points of a route by the rule of the minimum description length (MDL) of TRACLUS.
|
|
71
|
+
* A partition from point `i` to point `j` costs `L(H) + L(D|H)`. `L(H)` is the log of its length, and `L(D|H)` the logs of the perpendicular and angle distances of the route parts.
|
|
72
|
+
* The route keeps the point before `j` when that cost is more than `ratio` times the cost of the route parts. The lengths are in units of the board.
|
|
73
|
+
*/
|
|
74
|
+
export function characteristicPoints(route: readonly Point[], unit: number, ratio: number): Point[] {
|
|
75
|
+
const n = route.length;
|
|
76
|
+
if (n <= 2) return [...route];
|
|
77
|
+
const lg = (x: number): number => Math.log2(1 + x / unit);
|
|
78
|
+
const costs = (i: number, j: number): { par: number; nopar: number } => {
|
|
79
|
+
const a = route[i]!;
|
|
80
|
+
const b = route[j]!;
|
|
81
|
+
let perp = 0;
|
|
82
|
+
let theta = 0;
|
|
83
|
+
let nopar = 0;
|
|
84
|
+
for (let k = i; k < j; k++) {
|
|
85
|
+
const part = { a: route[k]!, b: route[k + 1]! };
|
|
86
|
+
perp += segmentDistance({ a, b }, part, { perp: 1, par: 0, theta: 0 });
|
|
87
|
+
theta += segmentDistance({ a, b }, part, { perp: 0, par: 0, theta: 1 });
|
|
88
|
+
nopar += lg(dist(part.a, part.b));
|
|
89
|
+
}
|
|
90
|
+
return { par: lg(dist(a, b)) + lg(perp) + lg(theta), nopar };
|
|
91
|
+
};
|
|
92
|
+
const out: Point[] = [route[0]!];
|
|
93
|
+
let start = 0;
|
|
94
|
+
let length = 1;
|
|
95
|
+
while (start + length < n) {
|
|
96
|
+
const curr = start + length;
|
|
97
|
+
const c = costs(start, curr);
|
|
98
|
+
if (c.par > ratio * c.nopar && curr - 1 > start) {
|
|
99
|
+
out.push(route[curr - 1]!);
|
|
100
|
+
start = curr - 1;
|
|
101
|
+
length = 1;
|
|
102
|
+
} else length++;
|
|
103
|
+
}
|
|
104
|
+
out.push(route[n - 1]!);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* This function gives the clusters of segments of DBSCAN. A segment with `minLns` neighbours or more within `eps` (itself included) is a core segment.
|
|
110
|
+
* A cluster grows from each core segment through its neighbours. A cluster with segments of fewer than `minLns` edges is dropped.
|
|
111
|
+
*/
|
|
112
|
+
export function segmentClusters(segments: readonly RouteSegment[], eps: number, minLns: number, w: { perp: number; par: number; theta: number }): RouteSegment[][] {
|
|
113
|
+
const n = segments.length;
|
|
114
|
+
// A grid of cells of side `eps` prunes the pairs. Two segments within `eps` have boxes within `eps` of each other in each axis.
|
|
115
|
+
const near: number[][] = segments.map(() => []);
|
|
116
|
+
const box = segments.map((s) => ({ x0: Math.min(s.a.x, s.b.x), y0: Math.min(s.a.y, s.b.y), x1: Math.max(s.a.x, s.b.x), y1: Math.max(s.a.y, s.b.y) }));
|
|
117
|
+
for (let i = 0; i < n; i++) {
|
|
118
|
+
near[i]!.push(i);
|
|
119
|
+
for (let j = i + 1; j < n; j++) {
|
|
120
|
+
const a = box[i]!;
|
|
121
|
+
const b = box[j]!;
|
|
122
|
+
if (a.x0 > b.x1 + eps || b.x0 > a.x1 + eps || a.y0 > b.y1 + eps || b.y0 > a.y1 + eps) continue;
|
|
123
|
+
if (segmentDistance(segments[i]!, segments[j]!, w) <= eps) {
|
|
124
|
+
near[i]!.push(j);
|
|
125
|
+
near[j]!.push(i);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const label = new Int32Array(n).fill(-1);
|
|
130
|
+
const out: RouteSegment[][] = [];
|
|
131
|
+
for (let i = 0; i < n; i++) {
|
|
132
|
+
if (label[i] !== -1 || near[i]!.length < minLns) continue;
|
|
133
|
+
const id = out.length;
|
|
134
|
+
const members: number[] = [];
|
|
135
|
+
const queue = [i];
|
|
136
|
+
label[i] = id;
|
|
137
|
+
while (queue.length > 0) {
|
|
138
|
+
const k = queue.shift()!;
|
|
139
|
+
members.push(k);
|
|
140
|
+
if (near[k]!.length < minLns) continue;
|
|
141
|
+
for (const j of near[k]!) {
|
|
142
|
+
if (label[j] !== -1) continue;
|
|
143
|
+
label[j] = id;
|
|
144
|
+
queue.push(j);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
out.push(members.sort((a, b) => a - b).map((k) => segments[k]!));
|
|
148
|
+
}
|
|
149
|
+
return out.filter((c) => new Set(c.map((s) => s.edge)).size >= minLns);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* This function gives the representative line of a cluster of segments. The line goes along the mean direction of the segments.
|
|
154
|
+
* A sweep along that direction stops at each end of a segment. Where `minLns` segments or more cover the stop, the point is the mean of their points there.
|
|
155
|
+
* Two points of the line are at least `gap` apart.
|
|
156
|
+
*/
|
|
157
|
+
export function representative(cluster: readonly RouteSegment[], minLns: number, gap: number): Point[] {
|
|
158
|
+
const first = cluster[0]!;
|
|
159
|
+
const fx = first.b.x - first.a.x;
|
|
160
|
+
const fy = first.b.y - first.a.y;
|
|
161
|
+
let vx = 0;
|
|
162
|
+
let vy = 0;
|
|
163
|
+
for (const s of cluster) {
|
|
164
|
+
let dx = s.b.x - s.a.x;
|
|
165
|
+
let dy = s.b.y - s.a.y;
|
|
166
|
+
const len = Math.hypot(dx, dy);
|
|
167
|
+
if (len < 1e-9) continue;
|
|
168
|
+
if (dx * fx + dy * fy < 0) [dx, dy] = [-dx, -dy];
|
|
169
|
+
vx += dx / len;
|
|
170
|
+
vy += dy / len;
|
|
171
|
+
}
|
|
172
|
+
const vl = Math.hypot(vx, vy);
|
|
173
|
+
if (vl < 1e-9) return [];
|
|
174
|
+
const ux = vx / vl;
|
|
175
|
+
const uy = vy / vl;
|
|
176
|
+
const along = (p: Point): number => p.x * ux + p.y * uy;
|
|
177
|
+
const across = (p: Point): number => -p.x * uy + p.y * ux;
|
|
178
|
+
const spans = cluster.map((s) => {
|
|
179
|
+
const [p, q] = along(s.a) <= along(s.b) ? [s.a, s.b] : [s.b, s.a];
|
|
180
|
+
return { x0: along(p), y0: across(p), x1: along(q), y1: across(q) };
|
|
181
|
+
});
|
|
182
|
+
const stops = [...new Set(spans.flatMap((s) => [s.x0, s.x1]))].sort((a, b) => a - b);
|
|
183
|
+
const out: Point[] = [];
|
|
184
|
+
let lastX = -Infinity;
|
|
185
|
+
for (const x of stops) {
|
|
186
|
+
const cover = spans.filter((s) => s.x0 <= x + 1e-9 && s.x1 >= x - 1e-9);
|
|
187
|
+
if (cover.length < minLns) continue;
|
|
188
|
+
if (x - lastX < gap) continue;
|
|
189
|
+
let y = 0;
|
|
190
|
+
for (const s of cover) y += s.x1 - s.x0 > 1e-9 ? s.y0 + ((s.y1 - s.y0) * (x - s.x0)) / (s.x1 - s.x0) : (s.y0 + s.y1) / 2;
|
|
191
|
+
y /= cover.length;
|
|
192
|
+
out.push({ x: x * ux - y * uy, y: x * uy + y * ux });
|
|
193
|
+
lastX = x;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** This function gives the distance of the point `p` to the polyline `line`. */
|
|
199
|
+
function lineDistance(line: readonly Point[], p: Point): number {
|
|
200
|
+
let best = Infinity;
|
|
201
|
+
for (let i = 1; i < line.length; i++) best = Math.min(best, pointSegmentDistance(p, line[i - 1]!, line[i]!));
|
|
202
|
+
return best;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The largest ratio of the length along the spine of a member to the length of its own route between the same ends. */
|
|
206
|
+
const DETOUR = 1.25;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* This function gives the highways of TRACLUS (HWY-025). Each first route breaks into a few long segments by the rule of MDL.
|
|
210
|
+
* DBSCAN puts the segments that are near in distance and angle into clusters. Each cluster with segments of `minLns` edges or more is one highway.
|
|
211
|
+
* The spine is the representative line of the cluster, or a way through the corner graph between its ends when the line hits a box.
|
|
212
|
+
* Each edge of the cluster joins the spine nearest to the first end of its segments. It leaves the spine nearest to the last end.
|
|
213
|
+
* An edge rides only when both ends of its part are within `eps` of the spine.
|
|
214
|
+
* Its length on the spine is also at most 1.25 times the length of its own route there.
|
|
215
|
+
* Each edge rides one highway at most: the highway where it rides the longest length.
|
|
216
|
+
*/
|
|
217
|
+
export function traclusPlan(input: HighwayInput, o: Readonly<TraclusOptions>): HighwayPlan {
|
|
218
|
+
const graph = input.graph;
|
|
219
|
+
const unit = boardUnit(graph);
|
|
220
|
+
const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
221
|
+
const pad = finder.margin / 2;
|
|
222
|
+
const routes = firstRoutes(input);
|
|
223
|
+
const segments: RouteSegment[] = [];
|
|
224
|
+
const corners = new Map<string, Point[]>();
|
|
225
|
+
for (const e of graph.edges) {
|
|
226
|
+
const route = routes.get(e.id);
|
|
227
|
+
if (!route || route.length < 2) continue;
|
|
228
|
+
const cps = characteristicPoints(route, unit, o.mdl);
|
|
229
|
+
corners.set(e.id, cps);
|
|
230
|
+
for (let i = 1; i < cps.length; i++) if (dist(cps[i - 1]!, cps[i]!) > 1e-9) segments.push({ edge: e.id, index: i - 1, a: cps[i - 1]!, b: cps[i]! });
|
|
231
|
+
}
|
|
232
|
+
const minLns = Math.max(2, Math.round(o.minLns));
|
|
233
|
+
const w = { perp: o.wPerp, par: o.wPar, theta: o.wTheta };
|
|
234
|
+
const eps = o.eps * unit;
|
|
235
|
+
const clusters = segmentClusters(segments, eps, minLns, w);
|
|
236
|
+
type Placed = { edge: string; on: number; off: number; ride: number };
|
|
237
|
+
const found: { spine: Point[]; placed: Placed[] }[] = [];
|
|
238
|
+
for (const cluster of clusters) {
|
|
239
|
+
const line = representative(cluster, minLns, finder.margin);
|
|
240
|
+
if (line.length < 2 || polylineLength(line) <= finder.margin) continue;
|
|
241
|
+
let spine: Point[] | undefined = line;
|
|
242
|
+
if (!finder.obstacles.clearPath(line, pad) || line.some((p) => finder.obstacles.inside(p, pad))) {
|
|
243
|
+
const a = line[0]!;
|
|
244
|
+
const b = line[line.length - 1]!;
|
|
245
|
+
const way = finder.obstacles.inside(a, pad) || finder.obstacles.inside(b, pad) ? undefined : finder.corners.path(a, b);
|
|
246
|
+
spine = way ? finder.corners.pull(way) : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (!spine || spine.length < 2 || polylineLength(spine) <= finder.margin) continue;
|
|
249
|
+
// The part of each edge in the cluster: its first and its last segment in the order of its route.
|
|
250
|
+
const parts = new Map<string, { first: RouteSegment; last: RouteSegment }>();
|
|
251
|
+
for (const s of cluster) {
|
|
252
|
+
const p = parts.get(s.edge);
|
|
253
|
+
if (!p) parts.set(s.edge, { first: s, last: s });
|
|
254
|
+
else {
|
|
255
|
+
if (s.index < p.first.index) p.first = s;
|
|
256
|
+
if (s.index > p.last.index) p.last = s;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const placed: Placed[] = [];
|
|
260
|
+
for (const [edge, p] of parts) {
|
|
261
|
+
if (lineDistance(spine, p.first.a) > eps || lineDistance(spine, p.last.b) > eps) continue;
|
|
262
|
+
const on = nearestPlace(spine, p.first.a);
|
|
263
|
+
const off = nearestPlace(spine, p.last.b);
|
|
264
|
+
const ride = lengthBetween(spine, Math.min(on, off), Math.max(on, off));
|
|
265
|
+
const own = polylineLength(corners.get(edge)!.slice(p.first.index, p.last.index + 2));
|
|
266
|
+
if (ride > finder.margin && ride <= DETOUR * own + finder.margin) placed.push({ edge, on, off, ride });
|
|
267
|
+
}
|
|
268
|
+
if (placed.length >= 2) found.push({ spine, placed });
|
|
269
|
+
}
|
|
270
|
+
// Each edge keeps the highway where it rides the longest length. The first highway wins a tie.
|
|
271
|
+
const best = new Map<string, { at: number; ride: number }>();
|
|
272
|
+
found.forEach((f, at) => {
|
|
273
|
+
for (const x of f.placed) if ((best.get(x.edge)?.ride ?? -1) < x.ride) best.set(x.edge, { at, ride: x.ride });
|
|
274
|
+
});
|
|
275
|
+
const highways: Highway[] = [];
|
|
276
|
+
found.forEach((f, at) => {
|
|
277
|
+
const placed = f.placed.filter((x) => best.get(x.edge)!.at === at);
|
|
278
|
+
if (placed.length < 2) return;
|
|
279
|
+
const route = f.spine;
|
|
280
|
+
// The lane order reads the plugs of each edge on each side of the spine.
|
|
281
|
+
const trip = (x: Placed): Point[] => {
|
|
282
|
+
const own = routes.get(x.edge)!;
|
|
283
|
+
const [from, to] = [own[0]!, own[own.length - 1]!];
|
|
284
|
+
return x.on <= x.off ? [from, ...route, to] : [to, ...route, from];
|
|
285
|
+
};
|
|
286
|
+
const lanes = orderLanes(
|
|
287
|
+
route,
|
|
288
|
+
placed.map((x) => ({ edge: x.edge, points: trip(x) })),
|
|
289
|
+
"bundle",
|
|
290
|
+
);
|
|
291
|
+
const byEdge = new Map(placed.map((x) => [x.edge, x]));
|
|
292
|
+
const members: HighwayMember[] = lanes.map((edge) => memberAt(route, edge, byEdge.get(edge)!.on, byEdge.get(edge)!.off));
|
|
293
|
+
highways.push({ id: `h${highways.length}`, key: `t${highways.length}`, spine: route, lanes, members });
|
|
294
|
+
});
|
|
295
|
+
return tidyPlan(orderAlongRoutes({ highways }, routes));
|
|
296
|
+
}
|
|
@@ -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
|
+
}
|