@mark1russell7/highways 0.3.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.
@@ -6,9 +6,12 @@ import { nodeIndex, type Graph, type Node, type Result } from "../model.ts";
6
6
  import { nearestSide, SIDE_NORMAL } from "../plugs.ts";
7
7
  import { makeRoom, openGaps, roomLinks } from "../room/index.ts";
8
8
  import { sectionsRouter } from "../routers/sections.ts";
9
+ import { byDataGroup } from "../groups/keys/index.ts";
10
+ import { highwayRouterStrategy } from "../highways/router.ts";
9
11
  import { checkSections, type Section } from "../sections.ts";
12
+ import { laneRoutes } from "../segments.ts";
10
13
  import { STRATEGIES } from "../strategies/builtins.ts";
11
- import { asRouter, listStrategies, STEPS, type Registry, type Step, type Strategy } from "../strategies/model.ts";
14
+ import { asRouter, listStrategies, STEPS, type LaneBoard, type Registry, type Step, type Strategy } from "../strategies/model.ts";
12
15
  import { measureLevelFaults, sumFaults } from "./links.ts";
13
16
  import { checkLevels } from "./properties.ts";
14
17
  import { ambiguity, inkSaved } from "./measures/bundles.ts";
@@ -172,6 +175,12 @@ export const FRONT_OBJECTIVES: Readonly<Record<Step, readonly Objective[]>> = {
172
175
  { key: "C3.median", better: "min" },
173
176
  { key: "C6.length", better: "min" },
174
177
  ],
178
+ highway: [
179
+ { key: "R1.perEdge", better: "min" },
180
+ { key: "R5.median", better: "min" },
181
+ { key: "B1.inkSaved", better: "max" },
182
+ { key: "R4.turn", better: "min" },
183
+ ],
175
184
  room: [
176
185
  { key: "M1.area", better: "min" },
177
186
  { key: "M4.laneThrough", better: "min" },
@@ -241,6 +250,8 @@ export function routeMeasures(graph: Graph, result: Result, row: BenchRow): Reco
241
250
  /** The input of one curve: a route with the normals of its plugs. */
242
251
  interface CurveRoute {
243
252
  readonly points: readonly { x: number; y: number }[];
253
+ /** The index of the route in the order of `laneRoutes`, when the curves draw the lanes of a board. */
254
+ readonly lane?: number;
244
255
  readonly startNormal: { x: number; y: number };
245
256
  readonly endNormal: { x: number; y: number };
246
257
  }
@@ -257,6 +268,11 @@ export interface CurveRunOptions {
257
268
  readonly normals: ReadonlySet<string>;
258
269
  /** The options of each curve strategy, by its name. A strategy without an entry gets its defaults. */
259
270
  readonly curveOptions?: Readonly<Record<string, object>>;
271
+ /**
272
+ * With this board, each curve draws the lanes of `laneRoutes` in place of the routes of the result, and each input has its `lane`.
273
+ * So the curve `highway` bends the lanes of a bundle as one. Without it, each curve draws the routes of the result.
274
+ */
275
+ readonly lanes?: LaneBoard;
260
276
  }
261
277
 
262
278
  /** One curve strategy on the routes of one result: the row of the suite and the drawn curves. */
@@ -279,19 +295,24 @@ export function curveRuns(graph: Graph, result: Result, curves: readonly Strateg
279
295
  const u = boardUnit(graph);
280
296
  const routes: CurveRoute[] = [];
281
297
  const ids: string[] = [];
282
- for (const r of result.routes) {
298
+ const board = options.lanes;
299
+ const source = board ? laneRoutes(board.graph, board.open, board.width, board.maxWidth) : result.routes;
300
+ source.forEach((r, i) => {
283
301
  const e = edges.get(r.edge);
284
302
  const s = e && byId.get(e.source.node);
285
303
  const t = e && byId.get(e.target.node);
286
- if (!s || !t || r.points.length < 2) continue;
304
+ if (!s || !t || r.points.length < 2) return;
287
305
  ids.push(r.edge);
288
- routes.push({ points: r.points, startNormal: SIDE_NORMAL[nearestSide(s.box, r.points[0]!)], endNormal: SIDE_NORMAL[nearestSide(t.box, r.points[r.points.length - 1]!)] });
289
- }
306
+ const normals = { startNormal: SIDE_NORMAL[nearestSide(s.box, r.points[0]!)], endNormal: SIDE_NORMAL[nearestSide(t.box, r.points[r.points.length - 1]!)] };
307
+ routes.push({ points: r.points, ...normals, ...(board ? { lane: i } : {}) });
308
+ });
290
309
  const ink = routes.reduce((sum, r) => sum + polylineLength(r.points), 0);
291
310
  return curves.map((curve): CurveRun => {
292
311
  const own = options.curveOptions?.[curve.name];
293
312
  const t0 = now();
294
- const drawn = routes.map((r) => curve.run({ points: r.points, startNormal: r.startNormal, endNormal: r.endNormal, space }, own));
313
+ const drawn = routes.map((r) =>
314
+ curve.run({ points: r.points, startNormal: r.startNormal, endNormal: r.endNormal, space, ...(board && r.lane !== undefined ? { lane: { board, index: r.lane } } : {}) }, own),
315
+ );
295
316
  const samples = drawn.map((d) => sampleCurve(d, 0.25));
296
317
  const ms = now() - t0;
297
318
  const t1 = now();
@@ -392,8 +413,8 @@ export function groupRun(
392
413
  }
393
414
 
394
415
  /**
395
- * This function gives the options of a grouping rule on a fixture. The rule `keys` gets a key from the data of the edges:
396
- * the collection of the first edge into the node. A node without such an edge is a group alone.
416
+ * This function gives the options of a grouping rule on a fixture. The rule `keys` gets the string `data.group` of the node.
417
+ * A node without it gets a key from the data of the edges: the collection of the first edge into the node. A node without such an edge is a group alone.
397
418
  */
398
419
  export function fixtureGroupOptions(strategy: string, graph: Graph): object {
399
420
  if (strategy !== "keys") return {};
@@ -402,7 +423,7 @@ export function fixtureGroupOptions(strategy: string, graph: Graph): object {
402
423
  const c = e.data?.["collection"];
403
424
  if (typeof c === "string" && !key.has(e.target.node)) key.set(e.target.node, c);
404
425
  }
405
- return { key: (n: Node) => key.get(n.id) };
426
+ return { key: (n: Node, i: number) => byDataGroup(n, i) ?? key.get(n.id) };
406
427
  }
407
428
 
408
429
  /**
@@ -424,10 +445,12 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
424
445
  const smooth = new Set(options.smoothCurves ?? ["rounded", "arc", "spline", "smooth", "clothoid", "highway"]);
425
446
  const normals = new Set(options.normalCurves ?? ["spline"]);
426
447
  const log = options.log ?? ((): void => {});
427
- const fixtures = steps.has("router") || steps.has("group") ? (options.fixtures ?? FIXTURES).filter((f) => f.nodes <= max) : [];
448
+ const fixtures = steps.has("router") || steps.has("group") || steps.has("highway") ? (options.fixtures ?? FIXTURES).filter((f) => f.nodes <= max) : [];
428
449
  const routers = steps.has("router") ? listStrategies(registry, "router").filter((s) => options.routers?.includes(s.name) ?? true) : [];
429
450
  const curves = steps.has("curve") ? listStrategies(registry, "curve") : [];
430
451
  const groups = steps.has("group") ? listStrategies(registry, "group") : [];
452
+ // The step `highway`: each highway strategy, then `follow`, as one router.
453
+ const highways = steps.has("highway") ? listStrategies(registry, "highway").map((s) => highwayRouterStrategy(s)) : [];
431
454
  const rooms = listStrategies(registry, "room");
432
455
  const rows: SuiteRow[] = [];
433
456
  const bench: BenchRow[] = [];
@@ -475,6 +498,39 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
475
498
  if (strategy.name === curveRouter && curves.length > 0 && graph.nodes.length <= curveMax)
476
499
  rows.push(...curveRuns(graph, result, curves, { fixture: spec.name, seed: k, smooth, normals }).map((c) => c.row));
477
500
  }
501
+ // The step `highway`: the plan of each strategy, routed with `follow`. The measures are the measures of the step `router`.
502
+ for (const strategy of highways) {
503
+ if (graph.nodes.length > (strategy.maxNodes ?? Infinity)) continue;
504
+ const router = asRouter(strategy);
505
+ const t0 = now();
506
+ const { row, result } = benchRun(router, spec.name, graph, 2, cellLimit);
507
+ const ms = now() - t0;
508
+ const t1 = now();
509
+ const values: Record<string, number> = routeMeasures(graph, result, row);
510
+ const measureMs = now() - t1;
511
+ let extraMs = 0;
512
+ if (graph.nodes.length <= stabilityMax) {
513
+ const t2 = now();
514
+ const s = routeStability(graph, result, router.route(change.graph), { seed });
515
+ extraMs = now() - t2;
516
+ values["Z10.mean"] = s.mean;
517
+ values["Z10.max"] = s.max;
518
+ values["Z10.moved"] = s.moved;
519
+ }
520
+ rows.push({
521
+ step: "highway",
522
+ fixture: spec.name,
523
+ seed: k,
524
+ strategy: strategy.name,
525
+ nodes: row.nodes,
526
+ edges: row.edges,
527
+ violations: row.violations + (row.deterministic ? 0 : 1),
528
+ ms,
529
+ measureMs,
530
+ extraMs,
531
+ values,
532
+ });
533
+ }
478
534
  // The step `group`: the router `sections` with each grouping rule, and the grouping of the changed graph (G9).
479
535
  for (const strategy of groups) {
480
536
  const opts = fixtureGroupOptions(strategy.name, graph);
@@ -550,7 +606,7 @@ export function runSuite(options: SuiteOptions = {}): { report: SuiteReport; ben
550
606
 
551
607
  /**
552
608
  * This function puts the rows of the suite in the order of one run. It sorts them by fixture family, then by seed.
553
- * In one seed, the routers come first, and the curves come after the router of the curves. The grouping rules and the room come last.
609
+ * In one seed, the routers come first, and the curves come after the router of the curves. Then the highway strategies come, and the grouping rules and the room come last.
554
610
  * A name that is not in its list goes after the names of the list.
555
611
  * The workflow `Bench` uses it to join the reports of its parts.
556
612
  */
@@ -572,8 +628,9 @@ export function orderSuiteRows(
572
628
  const rank = (r: SuiteRow): [number, number] => {
573
629
  if (r.step === "router") return [0, 2 * place(routers, r.strategy) * (curves.length + 1)];
574
630
  if (r.step === "curve") return [0, 2 * after * (curves.length + 1) + 1 + place(curves, r.strategy)];
575
- if (r.step === "group") return [1, place(names("group"), r.strategy)];
576
- return [2, r.strategy === "gutter" ? -1 : place(names("room"), r.strategy)];
631
+ if (r.step === "highway") return [1, place(names("highway"), r.strategy)];
632
+ if (r.step === "group") return [2, place(names("group"), r.strategy)];
633
+ return [3, r.strategy === "gutter" ? -1 : place(names("room"), r.strategy)];
577
634
  };
578
635
  return rows
579
636
  .map((row, i) => ({ row, i, f: place(families, row.fixture), rank: rank(row) }))
@@ -701,6 +758,7 @@ const fmt = (v: number | undefined, d = 3): string => (v === undefined || Number
701
758
  /** The columns of the tables of `formatSuite`: the keys of the measures of each step. */
702
759
  export const SUITE_COLUMNS: Readonly<Record<Step, readonly string[]>> = {
703
760
  router: ["R1.perEdge", "R2.angle", "R2.min", "B1.inkSaved", "R5.median", "R5.q95", "R4.turn", "R11.q05", "B4.ambiguity", "Z10.moved", "Z8.order", "Z7.position", "G8.treeCost"],
761
+ highway: ["R1.perEdge", "R2.angle", "R2.min", "B1.inkSaved", "R5.median", "R5.q95", "R4.turn", "R11.q05", "B4.ambiguity", "Z10.moved"],
704
762
  curve: ["C1.share", "C1.max", "C2.median", "C3.median", "C3.q95", "C6.length"],
705
763
  room: ["M1.area", "M4.laneThrough", "crossings", "through", "room-order"],
706
764
  group: ["Z4.farItems", "G1.modularity", "G2.cut", "G6.balance", "G8.treeCost", "G9.ari", "Z7.position", "Z8.order", "Z3.through", "treeMs"],
@@ -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
+ }