@mark1russell7/highways 0.2.0 → 0.4.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 +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { center, dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { nodeIndex, type Graph, type Route } from "../../model.ts";
|
|
3
|
+
import { placePlug } from "../../plugs.ts";
|
|
4
|
+
import { orderLanes } from "../../segments.ts";
|
|
5
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
6
|
+
import { memberAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The options of the highway strategy `corridors` (HWY-031 of the note on highways).
|
|
10
|
+
* The options `margin` and `k` set the corner graph of the first ways, when the input has no first routes.
|
|
11
|
+
*/
|
|
12
|
+
export interface CorridorOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
13
|
+
/** Two edges join when their common links are this share of the shorter way, or longer. The default is 0.3. */
|
|
14
|
+
readonly rho: number;
|
|
15
|
+
/** A highway has this number of edges or more. The default is 2. */
|
|
16
|
+
readonly minSize: number;
|
|
17
|
+
/** The number of hash functions of the MinHash signature of a way. The default is 64. */
|
|
18
|
+
readonly hashes: number;
|
|
19
|
+
/** The number of bands of the signature. Two ways with one equal band are a candidate pair. The default is 32. */
|
|
20
|
+
readonly bands: number;
|
|
21
|
+
/** Two link ends nearer than this grid size are one point. With 0, the default, only equal points are one point. */
|
|
22
|
+
readonly near: number;
|
|
23
|
+
/** The gap between two lanes. Two edges join only when their common links are longer than the break-even overlap for this gap. The default is 4. */
|
|
24
|
+
readonly lane: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The default options of the strategy `corridors`. */
|
|
28
|
+
export const CORRIDOR_DEFAULTS: Readonly<Required<Omit<CorridorOptions, "margin" | "k">>> = Object.freeze({ rho: 0.3, minSize: 2, hashes: 64, bands: 32, near: 0, lane: 4 });
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The break-even overlap of two parallel edges for each unit of the lane gap, with the cost exponent 0.5 of the note.
|
|
32
|
+
* A shared stretch shorter than this times the gap adds ink.
|
|
33
|
+
*/
|
|
34
|
+
export const BREAK_EVEN: number = Math.sqrt((2 + Math.SQRT2) / (2 - Math.SQRT2));
|
|
35
|
+
|
|
36
|
+
/** One link of a way: its key, its length and its ends in the order of the way. */
|
|
37
|
+
interface Link {
|
|
38
|
+
readonly key: string;
|
|
39
|
+
readonly len: number;
|
|
40
|
+
/** The snapped first end of the link, in the order of the way. */
|
|
41
|
+
readonly a: string;
|
|
42
|
+
/** The index of the first end in the points of the way. */
|
|
43
|
+
readonly at: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The first way of an edge, with its links. */
|
|
47
|
+
interface Way {
|
|
48
|
+
readonly edge: string;
|
|
49
|
+
readonly points: readonly Point[];
|
|
50
|
+
readonly links: readonly Link[];
|
|
51
|
+
/** The length of each link, by its key. */
|
|
52
|
+
readonly set: ReadonlyMap<string, number>;
|
|
53
|
+
readonly length: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** This function gives the FNV-1a hash of a string, as an unsigned 32-bit number. */
|
|
57
|
+
function fnv1a(text: string): number {
|
|
58
|
+
let h = 0x811c9dc5;
|
|
59
|
+
for (let i = 0; i < text.length; i++) {
|
|
60
|
+
h ^= text.charCodeAt(i);
|
|
61
|
+
h = Math.imul(h, 0x01000193);
|
|
62
|
+
}
|
|
63
|
+
return h >>> 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** This function mixes the bits of a 32-bit number (the finalizer of MurmurHash3). */
|
|
67
|
+
function mix(x: number): number {
|
|
68
|
+
let h = x >>> 0;
|
|
69
|
+
h ^= h >>> 16;
|
|
70
|
+
h = Math.imul(h, 0x85ebca6b);
|
|
71
|
+
h ^= h >>> 13;
|
|
72
|
+
h = Math.imul(h, 0xc2b2ae35);
|
|
73
|
+
h ^= h >>> 16;
|
|
74
|
+
return h >>> 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** This function gives the MinHash signature of a set of keys with `count` hash functions. */
|
|
78
|
+
export function minHash(keys: Iterable<string>, count: number): number[] {
|
|
79
|
+
const sig = new Array<number>(count).fill(0xffffffff);
|
|
80
|
+
const seeds = Array.from({ length: count }, (_, i) => mix(Math.imul(i + 1, 0x9e3779b9)));
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const base = fnv1a(key);
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
const v = mix(base ^ seeds[i]!);
|
|
85
|
+
if (v < sig[i]!) sig[i] = v;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return sig;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* This function gives the first way of each edge: the way through the corner graph, with the plugs at its ends.
|
|
93
|
+
* The router `corners` finds the same way before it drops the needless corners.
|
|
94
|
+
*/
|
|
95
|
+
function cornerWays(graph: Graph, options: Pick<WayOptions, "margin" | "k">): Route[] {
|
|
96
|
+
const byId = nodeIndex(graph);
|
|
97
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
98
|
+
const out: Route[] = [];
|
|
99
|
+
for (const e of graph.edges) {
|
|
100
|
+
const s = byId.get(e.source.node);
|
|
101
|
+
const t = byId.get(e.target.node);
|
|
102
|
+
if (!s || !t) continue;
|
|
103
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
104
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
105
|
+
const raw = ways.corners.path(ways.out(ps), ways.out(pt));
|
|
106
|
+
out.push({ edge: e.id, points: raw ? [ps.point, ...raw, pt.point] : [ps.point, pt.point] });
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** This function gives the way of a route, with a key for each link. Two links with the same ends, in either order, have one key. */
|
|
112
|
+
function wayOf(route: Route, near: number): Way {
|
|
113
|
+
const snap = (p: Point): string => (near > 0 ? `${Math.round(p.x / near)},${Math.round(p.y / near)}` : `${p.x},${p.y}`);
|
|
114
|
+
const links: Link[] = [];
|
|
115
|
+
const set = new Map<string, number>();
|
|
116
|
+
let length = 0;
|
|
117
|
+
for (let i = 1; i < route.points.length; i++) {
|
|
118
|
+
const p = route.points[i - 1]!;
|
|
119
|
+
const q = route.points[i]!;
|
|
120
|
+
const len = dist(p, q);
|
|
121
|
+
if (len <= 1e-9) continue;
|
|
122
|
+
const a = snap(p);
|
|
123
|
+
const b = snap(q);
|
|
124
|
+
if (a === b) continue;
|
|
125
|
+
const key = a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
126
|
+
links.push({ key, len, a, at: i - 1 });
|
|
127
|
+
if (!set.has(key)) set.set(key, len);
|
|
128
|
+
length += len;
|
|
129
|
+
}
|
|
130
|
+
return { edge: route.edge, points: route.points, links, set, length };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** This function gives the length of the common links of two ways. */
|
|
134
|
+
function overlap(p: Way, q: Way): number {
|
|
135
|
+
const [small, large] = p.set.size <= q.set.size ? [p, q] : [q, p];
|
|
136
|
+
let sum = 0;
|
|
137
|
+
for (const [key, len] of small.set) if (large.set.has(key)) sum += len;
|
|
138
|
+
return sum;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** This function gives the pairs of ways with one equal band of their signatures, as `i * n + j` with `i < j`, in order. */
|
|
142
|
+
function candidates(ways: readonly Way[], hashes: number, bands: number): number[] {
|
|
143
|
+
const rows = Math.max(1, Math.floor(hashes / Math.max(1, bands)));
|
|
144
|
+
const count = Math.max(1, Math.floor(hashes / rows));
|
|
145
|
+
const n = ways.length;
|
|
146
|
+
const pairs = new Set<number>();
|
|
147
|
+
const buckets = new Map<string, number[]>();
|
|
148
|
+
ways.forEach((w, i) => {
|
|
149
|
+
if (w.set.size === 0) return;
|
|
150
|
+
const sig = minHash(w.set.keys(), rows * count);
|
|
151
|
+
for (let b = 0; b < count; b++) {
|
|
152
|
+
const key = `${b}:${sig.slice(b * rows, (b + 1) * rows).join(",")}`;
|
|
153
|
+
const list = buckets.get(key);
|
|
154
|
+
if (list) {
|
|
155
|
+
for (const j of list) pairs.add(j * n + i);
|
|
156
|
+
list.push(i);
|
|
157
|
+
} else buckets.set(key, [i]);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return [...pairs].sort((a, b) => a - b);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** This function gives the root of an element of a union-find forest, and shortens the path. */
|
|
164
|
+
function root(parent: number[], i: number): number {
|
|
165
|
+
while (parent[i] !== i) {
|
|
166
|
+
parent[i] = parent[parent[i]!]!;
|
|
167
|
+
i = parent[i]!;
|
|
168
|
+
}
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* This function gives the clusters of the ways. Two ways join when their common links are long.
|
|
174
|
+
* A cluster is a connected part of the joins.
|
|
175
|
+
* MinHash with bands finds the candidate pairs. The function then measures the overlap of each candidate pair.
|
|
176
|
+
* Each cluster keeps the order of the ways, and the clusters come in the order of their first way.
|
|
177
|
+
*/
|
|
178
|
+
function corridorClusters(ways: readonly Way[], o: Readonly<CorridorOptions>): number[][] {
|
|
179
|
+
const n = ways.length;
|
|
180
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
181
|
+
const floor = BREAK_EVEN * o.lane;
|
|
182
|
+
for (const pair of candidates(ways, o.hashes, o.bands)) {
|
|
183
|
+
const i = Math.floor(pair / n);
|
|
184
|
+
const j = pair % n;
|
|
185
|
+
const p = ways[i]!;
|
|
186
|
+
const q = ways[j]!;
|
|
187
|
+
if (overlap(p, q) >= Math.max(floor, o.rho * Math.min(p.length, q.length))) {
|
|
188
|
+
const a = root(parent, i);
|
|
189
|
+
const b = root(parent, j);
|
|
190
|
+
if (a !== b) parent[Math.max(a, b)] = Math.min(a, b);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const groups = new Map<number, number[]>();
|
|
194
|
+
for (let i = 0; i < n; i++) {
|
|
195
|
+
const r = root(parent, i);
|
|
196
|
+
const list = groups.get(r);
|
|
197
|
+
if (list) list.push(i);
|
|
198
|
+
else groups.set(r, [i]);
|
|
199
|
+
}
|
|
200
|
+
return [...groups.values()].filter((g) => g.length >= 2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* This function gives the highway of a pool of ways, or `undefined`. The core is the way with the most shared length.
|
|
205
|
+
* The spine is the stretch of the core with the largest saving. Each link of the spine has two ways of the pool or more.
|
|
206
|
+
* A member is a way of the pool whose links on the spine are long enough, all in one direction.
|
|
207
|
+
*/
|
|
208
|
+
function highwayOf(pool: readonly Way[], o: Readonly<CorridorOptions>): { core: Way; spine: Point[]; members: { way: Way; on: number; off: number }[] } | undefined {
|
|
209
|
+
const count = new Map<string, number>();
|
|
210
|
+
for (const w of pool) for (const key of w.set.keys()) count.set(key, (count.get(key) ?? 0) + 1);
|
|
211
|
+
const gain = (key: string, len: number): number => len * ((count.get(key) ?? 1) - 1);
|
|
212
|
+
let core: Way | undefined;
|
|
213
|
+
let best = 0;
|
|
214
|
+
for (const w of pool) {
|
|
215
|
+
let g = 0;
|
|
216
|
+
for (const [key, len] of w.set) g += gain(key, len);
|
|
217
|
+
if (g > best) [core, best] = [w, g];
|
|
218
|
+
}
|
|
219
|
+
if (!core) return undefined;
|
|
220
|
+
// The best stretch of shared links of the core, by the saving of each link (Kadane on a list with only good values in each stretch).
|
|
221
|
+
let from = -1;
|
|
222
|
+
let to = -1;
|
|
223
|
+
let top = 0;
|
|
224
|
+
let start = 0;
|
|
225
|
+
let sum = 0;
|
|
226
|
+
core.links.forEach((l, i) => {
|
|
227
|
+
if ((count.get(l.key) ?? 0) < 2) {
|
|
228
|
+
sum = 0;
|
|
229
|
+
start = i + 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
sum += gain(l.key, l.len);
|
|
233
|
+
if (sum > top) [top, from, to] = [sum, start, i];
|
|
234
|
+
});
|
|
235
|
+
if (from < 0) return undefined;
|
|
236
|
+
const chosen = core.links.slice(from, to + 1);
|
|
237
|
+
// The spine: the first end of each chosen link, then the last end of the last link.
|
|
238
|
+
const spine: Point[] = [core.points[chosen[0]!.at]!, ...chosen.map((l) => core.points[l.at + 1]!)];
|
|
239
|
+
const index = new Map<string, number>();
|
|
240
|
+
chosen.forEach((l, j) => {
|
|
241
|
+
if (!index.has(l.key)) index.set(l.key, j);
|
|
242
|
+
});
|
|
243
|
+
const spineLength = chosen.reduce((s, l) => s + l.len, 0);
|
|
244
|
+
const floor = BREAK_EVEN * o.lane;
|
|
245
|
+
const members: { way: Way; on: number; off: number }[] = [];
|
|
246
|
+
for (const w of pool) {
|
|
247
|
+
let first = -1;
|
|
248
|
+
let last = -1;
|
|
249
|
+
let forward: boolean | undefined;
|
|
250
|
+
let ok = true;
|
|
251
|
+
let shared = 0;
|
|
252
|
+
for (const l of w.links) {
|
|
253
|
+
const j = index.get(l.key);
|
|
254
|
+
if (j === undefined) continue;
|
|
255
|
+
const dir = l.a === chosen[j]!.a;
|
|
256
|
+
if (forward === undefined) forward = dir;
|
|
257
|
+
else if (forward !== dir) ok = false;
|
|
258
|
+
if (first < 0) first = j;
|
|
259
|
+
else if (forward ? j < last : j > last) ok = false;
|
|
260
|
+
last = j;
|
|
261
|
+
shared += l.len;
|
|
262
|
+
}
|
|
263
|
+
if (!ok || forward === undefined || shared < Math.max(floor, o.rho * Math.min(w.length, spineLength))) continue;
|
|
264
|
+
members.push(forward ? { way: w, on: first, off: last + 1 } : { way: w, on: first + 1, off: last });
|
|
265
|
+
}
|
|
266
|
+
return { core, spine, members };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* This function gives the highways of corridor signatures (HWY-031). Each edge gets a first way: the route of `input.routes`, or its way through the corner graph.
|
|
271
|
+
* The signature of a way is the set of its links. Two edges join when their common links are at least `rho` times the shorter way, and longer than the break-even overlap.
|
|
272
|
+
* MinHash with `hashes` hash functions in `bands` bands finds the candidate pairs fast. Union-find gives the clusters.
|
|
273
|
+
*
|
|
274
|
+
* In each cluster, the spine is the longest common run of links of the way with the most shared length. Each edge on the spine joins it at its first common link and leaves it at its last one.
|
|
275
|
+
* The edges of the cluster that are not on the spine make the next highway, while `minSize` of them remain.
|
|
276
|
+
*/
|
|
277
|
+
export function corridorPlan(input: HighwayInput, o: Readonly<CorridorOptions>): HighwayPlan {
|
|
278
|
+
const routes = input.routes?.routes ?? cornerWays(input.graph, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
279
|
+
const known = new Set(input.graph.edges.map((e) => e.id));
|
|
280
|
+
const ways = routes.filter((r) => known.has(r.edge)).map((r) => wayOf(r, o.near));
|
|
281
|
+
const highways: Highway[] = [];
|
|
282
|
+
corridorClusters(ways, o).forEach((cluster, c) => {
|
|
283
|
+
let pool = cluster.map((i) => ways[i]!);
|
|
284
|
+
while (pool.length >= Math.max(2, o.minSize)) {
|
|
285
|
+
const found = highwayOf(pool, o);
|
|
286
|
+
if (!found) break;
|
|
287
|
+
const { core, spine, members } = found;
|
|
288
|
+
if (members.length < Math.max(2, o.minSize)) {
|
|
289
|
+
// The core does not make a highway. The other ways can still make one.
|
|
290
|
+
pool = pool.filter((w) => w !== core);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const lanes = orderLanes(spine, members.map((m) => ({ edge: m.way.edge, points: m.way.points })), "bundle");
|
|
294
|
+
const byEdge = new Map(members.map((m) => [m.way.edge, m]));
|
|
295
|
+
const list: HighwayMember[] = lanes.map((edge) => {
|
|
296
|
+
const m = byEdge.get(edge)!;
|
|
297
|
+
return memberAt(spine, edge, m.on, m.off);
|
|
298
|
+
});
|
|
299
|
+
highways.push({ id: `h${highways.length}`, key: `c${c}`, spine, lanes, members: list });
|
|
300
|
+
const used = new Set(members.map((m) => m.way));
|
|
301
|
+
pool = pool.filter((w) => !used.has(w));
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return { highways };
|
|
305
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { center, dist, pointBoxDistance, type Point } from "../geom.ts";
|
|
2
|
+
import { fixedPortPoint, nodeIndex, type End, type Graph, type Node, type Result, type Route, type SharedRun } from "../model.ts";
|
|
3
|
+
import { placePlug } from "../plugs.ts";
|
|
4
|
+
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
5
|
+
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
6
|
+
import { pointAt, type HighwayMember, type HighwayPlan } from "./model.ts";
|
|
7
|
+
|
|
8
|
+
/** The options of `follow`. */
|
|
9
|
+
export interface FollowOptions extends WayOptions {
|
|
10
|
+
/**
|
|
11
|
+
* These routes are the routes of the base router. Each edge outside the plan keeps its route from them.
|
|
12
|
+
* Without them, or for an edge without a route in them, `follow` routes the edge as the router `corners` does.
|
|
13
|
+
*/
|
|
14
|
+
readonly routes?: Result;
|
|
15
|
+
/** The lane order of the shared runs. The default is `bundle`. */
|
|
16
|
+
readonly laneOrder?: RunOrder;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SAME = 1e-6;
|
|
20
|
+
|
|
21
|
+
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > SAME);
|
|
22
|
+
|
|
23
|
+
/** This function tells if a polyline holds the points of `part` in a row, in this order. */
|
|
24
|
+
function holdsInOrder(p: readonly Point[], part: readonly Point[]): boolean {
|
|
25
|
+
for (let i = 0; i + part.length <= p.length; i++) {
|
|
26
|
+
let ok = true;
|
|
27
|
+
for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j]!, part[j]!) <= SAME;
|
|
28
|
+
if (ok) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** This function tells if a polyline holds the points of `part` in a row, in either direction. */
|
|
34
|
+
const holds = (p: readonly Point[], part: readonly Point[]): boolean => holdsInOrder(p, part) || holdsInOrder(p, [...part].reverse());
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function tells if a point of a spine can be the plug of an end. The point is on the box of the node.
|
|
38
|
+
* When the end has a fixed plug, the point is at that plug.
|
|
39
|
+
*/
|
|
40
|
+
function plugAt(node: Node, end: End, p: Point): boolean {
|
|
41
|
+
if (pointBoxDistance(p, node.box) > SAME) return false;
|
|
42
|
+
const fixed = fixedPortPoint(node, end);
|
|
43
|
+
return fixed === undefined || dist(fixed, p) <= SAME;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The stops of one spine: each point of the spine and each merge point and split point, by their place, in order. */
|
|
47
|
+
type Stops = readonly (readonly [at: number, point: Point])[];
|
|
48
|
+
|
|
49
|
+
/** This function gives the points of the stops from the place `on` to the place `off`. When `on` is larger than `off`, the points go backward. */
|
|
50
|
+
function slice(stops: Stops, on: number, off: number): Point[] {
|
|
51
|
+
const lo = Math.min(on, off);
|
|
52
|
+
const hi = Math.max(on, off);
|
|
53
|
+
const out = stops.filter(([at]) => at >= lo && at <= hi).map(([, p]) => p);
|
|
54
|
+
return on <= off ? out : out.reverse();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function routes the edges along a highway plan.
|
|
59
|
+
*
|
|
60
|
+
* An edge of a highway goes from its source plug to its merge point. Then it goes along the spine to its split point, and to its target plug.
|
|
61
|
+
* The ramps go around the boxes through the corner graph (`WayFinder.fromPlug` and `WayFinder.toPlug`).
|
|
62
|
+
* A spine point on the box of the node is the plug itself. When the end has a fixed plug, the point is at that plug.
|
|
63
|
+
*
|
|
64
|
+
* An edge on several highways takes them in the order of `seq`.
|
|
65
|
+
* A way through the corner graph goes from each split point to the next merge point.
|
|
66
|
+
* An edge outside the plan keeps its route from `options.routes`, or goes as in the router `corners`.
|
|
67
|
+
*
|
|
68
|
+
* The result has a shared run for each stretch of a spine where the same two edges or more go. Each run holds the points of its routes in a row.
|
|
69
|
+
* The same graph, plan and options give the same result.
|
|
70
|
+
*/
|
|
71
|
+
export function follow(graph: Graph, plan: HighwayPlan, options: FollowOptions = {}): Result {
|
|
72
|
+
const byId = nodeIndex(graph);
|
|
73
|
+
const ways = new WayFinder(graph.nodes, options);
|
|
74
|
+
const base = new Map((options.routes?.routes ?? []).map((r) => [r.edge, r.points]));
|
|
75
|
+
const lines: Stops[] = plan.highways.map((h) => {
|
|
76
|
+
const stops = new Map<number, Point>(h.spine.map((p, i) => [i, p]));
|
|
77
|
+
for (const m of h.members) for (const at of [m.on, m.off]) if (!stops.has(at)) stops.set(at, pointAt(h.spine, at));
|
|
78
|
+
return [...stops].sort((a, b) => a[0] - b[0]);
|
|
79
|
+
});
|
|
80
|
+
const trips = new Map<string, { h: number; m: HighwayMember }[]>();
|
|
81
|
+
plan.highways.forEach((h, hi) => {
|
|
82
|
+
for (const m of h.members) {
|
|
83
|
+
const list = trips.get(m.edge);
|
|
84
|
+
if (list) list.push({ h: hi, m });
|
|
85
|
+
else trips.set(m.edge, [{ h: hi, m }]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
for (const list of trips.values()) list.sort((a, b) => a.m.seq - b.m.seq || a.h - b.h);
|
|
89
|
+
|
|
90
|
+
const routes: Route[] = [];
|
|
91
|
+
for (const e of graph.edges) {
|
|
92
|
+
const s = byId.get(e.source.node);
|
|
93
|
+
const t = byId.get(e.target.node);
|
|
94
|
+
if (!s || !t) continue;
|
|
95
|
+
const trip = trips.get(e.id);
|
|
96
|
+
if (!trip) {
|
|
97
|
+
const known = base.get(e.id);
|
|
98
|
+
if (known) {
|
|
99
|
+
routes.push({ edge: e.id, points: known });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const ps = placePlug(s, e.source, center(t.box));
|
|
103
|
+
const pt = placePlug(t, e.target, center(s.box));
|
|
104
|
+
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const out: Point[] = [];
|
|
108
|
+
let at: Point | undefined;
|
|
109
|
+
for (const { h, m } of trip) {
|
|
110
|
+
const piece = slice(lines[h]!, m.on, m.off);
|
|
111
|
+
const first = piece[0]!;
|
|
112
|
+
if (at === undefined) {
|
|
113
|
+
if (!plugAt(s, e.source, first)) {
|
|
114
|
+
const ps = placePlug(s, e.source, first);
|
|
115
|
+
out.push(...(ways.fromPlug(ps, first) ?? [ps.point, first]));
|
|
116
|
+
}
|
|
117
|
+
} else if (dist(at, first) > SAME) {
|
|
118
|
+
const way = ways.corners.path(at, first);
|
|
119
|
+
out.push(...(way ? ways.corners.pull(way) : [at, first]));
|
|
120
|
+
}
|
|
121
|
+
out.push(...piece);
|
|
122
|
+
at = piece[piece.length - 1]!;
|
|
123
|
+
}
|
|
124
|
+
if (at !== undefined && !plugAt(t, e.target, at)) {
|
|
125
|
+
const pt = placePlug(t, e.target, at);
|
|
126
|
+
out.push(...(ways.toPlug(at, pt) ?? [at, pt.point]));
|
|
127
|
+
}
|
|
128
|
+
routes.push({ edge: e.id, points: dedupe(out) });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The runs: each stretch between two stops of a spine, with the same two edges or more on it, joined with the next stretch of the same edges.
|
|
132
|
+
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
133
|
+
const runs: SharedRun[] = [];
|
|
134
|
+
plan.highways.forEach((h, hi) => {
|
|
135
|
+
const stops = lines[hi]!;
|
|
136
|
+
const usersOf = (k: number): string[] =>
|
|
137
|
+
h.members
|
|
138
|
+
.filter((m) => routeOf.has(m.edge) && Math.min(m.on, m.off) <= stops[k]![0] && Math.max(m.on, m.off) >= stops[k + 1]![0])
|
|
139
|
+
.map((m) => m.edge)
|
|
140
|
+
.sort();
|
|
141
|
+
let k = 0;
|
|
142
|
+
while (k < stops.length - 1) {
|
|
143
|
+
const users = usersOf(k);
|
|
144
|
+
let end = k + 1;
|
|
145
|
+
while (end < stops.length - 1 && usersOf(end).join("\u0000") === users.join("\u0000")) end++;
|
|
146
|
+
const points = dedupe(stops.slice(k, end + 1).map(([, p]) => p));
|
|
147
|
+
k = end;
|
|
148
|
+
if (users.length < 2 || points.length < 2) continue;
|
|
149
|
+
const mine = users.map((id) => routeOf.get(id)!).filter((r) => holds(r.points, points));
|
|
150
|
+
if (mine.length < 2) continue;
|
|
151
|
+
runs.push({ id: `r${runs.length}`, key: h.key, points, edges: orderLanes(points, mine, options.laneOrder ?? "bundle") });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return runs.length > 0 ? { routes, runs } : { routes };
|
|
155
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { checkPlan, lengthBetween, memberAt, pointAt, type Highway, type HighwayInput, type HighwayMember, type HighwayPlan } from "./model.ts";
|
|
2
|
+
export { follow, type FollowOptions } from "./follow.ts";
|
|
3
|
+
export { planOfResult } from "./result.ts";
|
|
4
|
+
export { highwayRouter, highwayRouterStrategy } from "./router.ts";
|
|
5
|
+
export { keysPlan, type KeysOptions } from "./keys/index.ts";
|
|
6
|
+
export { sharedWaysPlan, type SharedWaysOptions } from "./shared-ways/index.ts";
|
|
7
|
+
export { BREAK_EVEN, CORRIDOR_DEFAULTS, corridorPlan, minHash, type CorridorOptions } from "./corridors/index.ts";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { trunksRouter, type TrunksOptions } from "../../routers/trunks.ts";
|
|
2
|
+
import { planOfResult } from "../result.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the highway strategy `keys`: the options of the router `trunks`, without the name and the limit of the benchmark. */
|
|
6
|
+
export type KeysOptions = Omit<TrunksOptions, "name" | "maxNodes">;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This function gives the highways that the router `trunks` decides. A rule gives each edge a key.
|
|
10
|
+
* The edges from one plug with one key share a trunk to a split point. The edges that go on together share a run past it.
|
|
11
|
+
* Each trunk and each run becomes one highway. The function does not change the router: it reads its bundles and its runs.
|
|
12
|
+
*/
|
|
13
|
+
export function keysPlan(input: HighwayInput, options: KeysOptions): HighwayPlan {
|
|
14
|
+
return planOfResult(trunksRouter({ ...options, name: "keys" }).route(input.graph));
|
|
15
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { dist, type Point } from "../geom.ts";
|
|
2
|
+
import type { Graph, Result } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** The input of a highway strategy: the boxes and the edges, and optionally the first routes of a router. */
|
|
5
|
+
export interface HighwayInput {
|
|
6
|
+
readonly graph: Graph;
|
|
7
|
+
/**
|
|
8
|
+
* These routes are the first routes, from a router. A strategy that reads first routes makes them itself when this value is absent.
|
|
9
|
+
* The strategy `corridors` makes the ways of the corner graph, as the router `corners` finds them.
|
|
10
|
+
*/
|
|
11
|
+
readonly routes?: Result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One edge of a highway. The edge joins the spine at the merge point and leaves it at the split point.
|
|
16
|
+
* A place on the spine is the index of a point of the spine plus the share of the next leg. Point `i` of the spine is at `i`.
|
|
17
|
+
* When `on` is larger than `off`, the edge goes along the spine from its last point to its first point.
|
|
18
|
+
*/
|
|
19
|
+
export interface HighwayMember {
|
|
20
|
+
readonly edge: string;
|
|
21
|
+
/** The place of the merge point on the spine. */
|
|
22
|
+
readonly on: number;
|
|
23
|
+
/** The place of the split point on the spine. */
|
|
24
|
+
readonly off: number;
|
|
25
|
+
/** The point where the edge joins the spine. */
|
|
26
|
+
readonly merge: Point;
|
|
27
|
+
/** The point where the edge leaves the spine. */
|
|
28
|
+
readonly split: Point;
|
|
29
|
+
/**
|
|
30
|
+
* The place of this highway in the trip of the edge, from 0. An edge can go along several highways, one after the other.
|
|
31
|
+
* `follow` takes the highways of an edge in the order of this value.
|
|
32
|
+
*/
|
|
33
|
+
readonly seq: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A highway: a shared way that a bundle of edges follows, from their merge points to their split points. */
|
|
37
|
+
export interface Highway {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** The key of the highway, for example the key of the grouping rule or the index of a cluster. */
|
|
40
|
+
readonly key: string;
|
|
41
|
+
/** The spine: the polyline that the edges follow, in free space. */
|
|
42
|
+
readonly spine: readonly Point[];
|
|
43
|
+
/** The edges in lane order. Lane 0 is on the left of the spine, from its first point to its last point. The y axis points down. */
|
|
44
|
+
readonly lanes: readonly string[];
|
|
45
|
+
/** The edges with their merge points and their split points, in the order of `lanes`. */
|
|
46
|
+
readonly members: readonly HighwayMember[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The output of a highway strategy: the highways, as data. `follow` routes the edges along them. */
|
|
50
|
+
export interface HighwayPlan {
|
|
51
|
+
readonly highways: readonly Highway[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** This function gives the point at a place on a polyline. A place outside the polyline gives its nearest end. */
|
|
55
|
+
export function pointAt(line: readonly Point[], at: number): Point {
|
|
56
|
+
if (line.length === 0) throw new Error("The polyline has no point.");
|
|
57
|
+
if (at <= 0) return line[0]!;
|
|
58
|
+
if (at >= line.length - 1) return line[line.length - 1]!;
|
|
59
|
+
const i = Math.floor(at);
|
|
60
|
+
const u = at - i;
|
|
61
|
+
const a = line[i]!;
|
|
62
|
+
if (u === 0) return a;
|
|
63
|
+
const b = line[i + 1]!;
|
|
64
|
+
return { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** This function gives the length of a polyline from the place `from` to the place `to`, with `from` not larger than `to`. */
|
|
68
|
+
export function lengthBetween(line: readonly Point[], from: number, to: number): number {
|
|
69
|
+
if (to <= from) return 0;
|
|
70
|
+
let sum = 0;
|
|
71
|
+
let prev = pointAt(line, from);
|
|
72
|
+
for (let i = Math.floor(from) + 1; i < to; i++) {
|
|
73
|
+
const p = line[i]!;
|
|
74
|
+
sum += dist(prev, p);
|
|
75
|
+
prev = p;
|
|
76
|
+
}
|
|
77
|
+
return sum + dist(prev, pointAt(line, to));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** This function gives a member of a highway with its merge point and its split point at the places `on` and `off` of the spine. */
|
|
81
|
+
export function memberAt(spine: readonly Point[], edge: string, on: number, off: number, seq = 0): HighwayMember {
|
|
82
|
+
return { edge, on, off, merge: pointAt(spine, on), split: pointAt(spine, off), seq };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* This function lists the problems of a plan against its graph. A good plan gives an empty list.
|
|
87
|
+
* Each highway has a spine of two points or more, and two members or more. Each member is an edge of the graph, with `on` and `off` on the spine and apart.
|
|
88
|
+
* An edge is a member of a highway one time at most.
|
|
89
|
+
*/
|
|
90
|
+
export function checkPlan(graph: Graph, plan: HighwayPlan): string[] {
|
|
91
|
+
const edges = new Set(graph.edges.map((e) => e.id));
|
|
92
|
+
const out: string[] = [];
|
|
93
|
+
const ids = new Set<string>();
|
|
94
|
+
for (const h of plan.highways) {
|
|
95
|
+
if (ids.has(h.id)) out.push(`Two highways have the id "${h.id}".`);
|
|
96
|
+
ids.add(h.id);
|
|
97
|
+
const last = h.spine.length - 1;
|
|
98
|
+
if (h.spine.length < 2) out.push(`The highway "${h.id}" has a spine with fewer than two points.`);
|
|
99
|
+
if (h.members.length < 2) out.push(`The highway "${h.id}" has fewer than two members.`);
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
for (const m of h.members) {
|
|
102
|
+
if (!edges.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}", which is not an edge of the graph.`);
|
|
103
|
+
if (seen.has(m.edge)) out.push(`The highway "${h.id}" has the member "${m.edge}" two times.`);
|
|
104
|
+
seen.add(m.edge);
|
|
105
|
+
if (!(m.on >= 0 && m.on <= last && m.off >= 0 && m.off <= last)) out.push(`The member "${m.edge}" of the highway "${h.id}" has a place off the spine.`);
|
|
106
|
+
if (m.on === m.off) out.push(`The member "${m.edge}" of the highway "${h.id}" joins and leaves the spine at one place.`);
|
|
107
|
+
}
|
|
108
|
+
if (h.lanes.length !== h.members.length || h.lanes.some((e) => !seen.has(e))) out.push(`The lanes of the highway "${h.id}" are not its members.`);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Result } from "../model.ts";
|
|
2
|
+
import { segmentsOf } from "../segments.ts";
|
|
3
|
+
import { memberAt, type Highway, type HighwayMember, type HighwayPlan } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This function gives the highways of a result of a router: one highway for each shared run and for each trunk of a bundle.
|
|
7
|
+
* The spine is the points of the run or of the trunk. Each edge of the run is a member, from one end of the spine to the other end, in the way of its route.
|
|
8
|
+
* The member `seq` is the place of the shared stretch in the route of the edge. The lanes keep the lane order of the router.
|
|
9
|
+
*
|
|
10
|
+
* Thus `follow` routes each edge along the shared stretches of the router. A route that does not hold its run is not a member.
|
|
11
|
+
*/
|
|
12
|
+
export function planOfResult(result: Result): HighwayPlan {
|
|
13
|
+
const graph = segmentsOf(result);
|
|
14
|
+
const keys = new Map<string, string>([...(result.runs ?? []).map((r) => [r.id, r.key] as const), ...(result.bundles ?? []).map((b) => [b.id, b.key] as const)]);
|
|
15
|
+
const shared = graph.segments.filter((s) => s.count > 1);
|
|
16
|
+
const ids = new Set(shared.map((s) => s.id));
|
|
17
|
+
const place = new Map<string, { seq: number; reverse: boolean }>();
|
|
18
|
+
for (const path of graph.paths) {
|
|
19
|
+
let seq = 0;
|
|
20
|
+
for (const ref of path.segments) if (ids.has(ref.segment)) place.set(`${ref.segment}\u0000${path.edge}`, { seq: seq++, reverse: ref.reverse });
|
|
21
|
+
}
|
|
22
|
+
const highways: Highway[] = [];
|
|
23
|
+
for (const s of shared) {
|
|
24
|
+
const last = s.points.length - 1;
|
|
25
|
+
const members: HighwayMember[] = [];
|
|
26
|
+
for (const edge of s.edges) {
|
|
27
|
+
const at = place.get(`${s.id}\u0000${edge}`);
|
|
28
|
+
if (at) members.push(at.reverse ? memberAt(s.points, edge, last, 0, at.seq) : memberAt(s.points, edge, 0, last, at.seq));
|
|
29
|
+
}
|
|
30
|
+
if (members.length < 2) continue;
|
|
31
|
+
highways.push({ id: `h${highways.length}`, key: keys.get(s.id) ?? s.id, spine: s.points, lanes: members.map((m) => m.edge), members });
|
|
32
|
+
}
|
|
33
|
+
return { highways };
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Graph, Result, Router } from "../model.ts";
|
|
2
|
+
import { defineStrategy, type Strategy } from "../strategies/model.ts";
|
|
3
|
+
import type { WayOptions } from "../ways.ts";
|
|
4
|
+
import { follow, type FollowOptions } from "./follow.ts";
|
|
5
|
+
|
|
6
|
+
/** This function gives the options of the way finder in the options of a strategy: `margin` and `k`, when they are present. */
|
|
7
|
+
function wayOptionsOf(options: object): WayOptions {
|
|
8
|
+
const o = options as { margin?: unknown; k?: unknown };
|
|
9
|
+
return { ...(typeof o.margin === "number" ? { margin: o.margin } : {}), ...(typeof o.k === "number" ? { k: o.k } : {}) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* This function gives a router that runs a highway strategy and then `follow`. The strategy gets the graph without first routes.
|
|
14
|
+
* The way finder of `follow` takes `margin` and `k` from the options of the strategy, and then from `followOptions`.
|
|
15
|
+
* The router keeps the name and the `maxNodes` of the strategy.
|
|
16
|
+
*/
|
|
17
|
+
export function highwayRouter<O extends object>(strategy: Strategy<"highway", O>, options?: Partial<O>, followOptions: FollowOptions = {}): Router {
|
|
18
|
+
return {
|
|
19
|
+
name: strategy.name,
|
|
20
|
+
...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
|
|
21
|
+
route: (graph: Graph): Result => follow(graph, strategy.run({ graph }, options), { ...followOptions, ...wayOptionsOf({ ...strategy.defaults, ...options }) }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This function gives a router strategy that runs a highway strategy and then `follow`.
|
|
27
|
+
* The router strategy has the name, the options and the schema of the highway strategy.
|
|
28
|
+
* The benchmark and the screen race such a router with the other routers.
|
|
29
|
+
*/
|
|
30
|
+
export function highwayRouterStrategy<O extends object>(strategy: Strategy<"highway", O>): Strategy<"router", O> {
|
|
31
|
+
return defineStrategy<"router", O>({
|
|
32
|
+
step: "router",
|
|
33
|
+
name: strategy.name,
|
|
34
|
+
describe: strategy.describe,
|
|
35
|
+
defaults: strategy.defaults as O,
|
|
36
|
+
...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
|
|
37
|
+
...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
|
|
38
|
+
run: (graph: Graph, options: Readonly<O>): Result => follow(graph, strategy.run({ graph }, options), wayOptionsOf(options)),
|
|
39
|
+
});
|
|
40
|
+
}
|