@mark1russell7/highways 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +66 -2
  2. package/build/index.d.ts +1009 -12
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +4408 -195
  5. package/build/index.js.map +1 -1
  6. package/package.json +4 -1
  7. package/src/bench/compare.ts +41 -6
  8. package/src/bench/measures/bundles.ts +101 -2
  9. package/src/bench/measures/index.ts +1 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/forms.ts +372 -0
  13. package/src/bench/screen/generators.ts +54 -0
  14. package/src/bench/screen/hash.ts +129 -0
  15. package/src/bench/screen/index.ts +6 -0
  16. package/src/bench/screen/measures.ts +126 -0
  17. package/src/bench/screen/screen.ts +454 -0
  18. package/src/bench/screen/set.ts +136 -0
  19. package/src/bench/suite.ts +71 -13
  20. package/src/highways/corridors/index.ts +305 -0
  21. package/src/highways/follow.ts +155 -0
  22. package/src/highways/index.ts +7 -0
  23. package/src/highways/keys/index.ts +15 -0
  24. package/src/highways/model.ts +111 -0
  25. package/src/highways/result.ts +34 -0
  26. package/src/highways/router.ts +40 -0
  27. package/src/highways/shared-ways/index.ts +15 -0
  28. package/src/index.ts +4 -0
  29. package/src/order/index.ts +65 -0
  30. package/src/plugs/justify.ts +48 -0
  31. package/src/plugs/side.ts +138 -0
  32. package/src/repair/index.ts +377 -0
  33. package/src/repair/walk.ts +220 -0
  34. package/src/room/lanes.ts +109 -10
  35. package/src/routers/corners.ts +73 -6
  36. package/src/routers/fan.ts +145 -0
  37. package/src/routers/index.ts +2 -2
  38. package/src/routers/shared.ts +107 -8
  39. package/src/routers/snell.ts +140 -0
  40. package/src/routers/trunks.ts +43 -2
  41. package/src/strategies/builtins.ts +82 -6
  42. package/src/strategies/model.ts +36 -6
  43. package/src/strategies/schemas.ts +66 -2
@@ -0,0 +1,136 @@
1
+ import { hubChainBoard, manyGroupsBoard, packedGridBoard } from "../../cases.ts";
2
+ import { FIXTURES, makeFixture, type FixtureSpec } from "../../fixtures.ts";
3
+ import type { Graph } from "../../model.ts";
4
+ import { familySeed } from "../suite.ts";
5
+ import { graphInput, type LayoutCell, type LayoutGenerator, type LayoutInput } from "./forms.ts";
6
+
7
+ /** A family of boards of a screen set. The member `k` of the family has the seed `k`. */
8
+ export interface ScreenFamily {
9
+ readonly name: string;
10
+ /** The known hard case that the family shows, in a few words. */
11
+ readonly shows: string;
12
+ /** The number of members. */
13
+ readonly seeds: number;
14
+ /** This function makes the member `k`. The same `k` always gives the same graph. */
15
+ readonly make: (k: number) => Graph;
16
+ /** This function gives the generator of the member `k`: its name, version, parameters and seed. */
17
+ readonly generator: (k: number) => LayoutGenerator;
18
+ /** The cell of each member in the taxonomy of the corpus. */
19
+ readonly cell: LayoutCell;
20
+ }
21
+
22
+ /** A screen set: a fixed list of small boards with an id. A new board or a changed board gets a new id. */
23
+ export interface ScreenSet {
24
+ /** The id of the set, for example `screen-v1`. The records name it in `set` and `corpus`. */
25
+ readonly id: string;
26
+ readonly families: readonly ScreenFamily[];
27
+ }
28
+
29
+ const fixture = (name: string): FixtureSpec => FIXTURES.find((f) => f.name === name)!;
30
+ const DENSE: FixtureSpec = { name: "dense-groups", kind: "clustered", nodes: 150, edgesPerNode: 2, seed: 3 };
31
+ const PACKED = { cols: 12, rows: 8, long: 48 } as const;
32
+
33
+ /** This function gives a family of `makeFixture`. The member `k` has the seed `familySeed(spec, k)`, as in `runSuite`. */
34
+ function fixtureFamily(spec: FixtureSpec, shows: string, seeds: number, cell: LayoutCell): ScreenFamily {
35
+ const { name, seed: _seed, ...params } = spec;
36
+ return {
37
+ name,
38
+ shows,
39
+ seeds,
40
+ cell,
41
+ make: (k) => makeFixture({ ...spec, seed: familySeed(spec, k) }),
42
+ generator: (k) => ({ name: "fixture", version: 1, params, seed: familySeed(spec, k) }),
43
+ };
44
+ }
45
+
46
+ /** The cell dimensions that each family of `screen-v1` shares. */
47
+ const BASE = { containerSize: "freeAspect", containerShape: "rect", obstacles: "none", mode: "offline", groups: "none" } as const;
48
+
49
+ /**
50
+ * The screen set `screen-v1`: 14 boards of made-up data from 10 to 150 nodes. Each family shows a known hard case.
51
+ * The seed 0 of each family is the fixture or the case board of the benchmark. The seed `k` of a fixture family is `familySeed(spec, k)`, as in `runSuite`.
52
+ * The generators wrap the fixtures and the case boards without a change to their output.
53
+ * This set does not change. The tests compare it with the files in `corpus/screen-v1`.
54
+ */
55
+ export const SCREEN_V1: ScreenSet = {
56
+ id: "screen-v1",
57
+ families: [
58
+ fixtureFamily(fixture("tiny-uniform"), "A tiny board: the cost of a strategy at its start.", 3, { ...BASE, count: "10", edgeDensity: "medium", edgeShape: "random", ports: "free" }),
59
+ fixtureFamily(fixture("small-clustered"), "Clusters, and fixed plugs on 20% of the nodes.", 3, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "fixed" }),
60
+ fixtureFamily(DENSE, "Dense groups whose boxes overlap at the far view.", 3, { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" }),
61
+ {
62
+ name: "hub-chain",
63
+ shows: "A chain of hubs: routes climb away from their targets.",
64
+ seeds: 1,
65
+ cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "hubChain", ports: "free" },
66
+ make: () => hubChainBoard(),
67
+ generator: () => ({ name: "case.hub-chain", version: 1, params: {}, seed: 3 }),
68
+ },
69
+ {
70
+ name: "many-groups",
71
+ shows: "Many groups: level links go around the board.",
72
+ seeds: 1,
73
+ cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "clustered", ports: "free" },
74
+ make: () => manyGroupsBoard(),
75
+ generator: () => ({ name: "case.many-groups", version: 1, params: {}, seed: 7 }),
76
+ },
77
+ {
78
+ name: "packed-grid",
79
+ shows: "A packed grid: boxes touch, with no channel for the routes.",
80
+ seeds: 3,
81
+ cell: { ...BASE, count: "100", edgeDensity: "medium", edgeShape: "longRange", ports: "free" },
82
+ make: (k) => packedGridBoard({ ...PACKED, seed: 11 + 1009 * k }),
83
+ generator: (k) => ({ name: "case.packed-grid", version: 1, params: PACKED, seed: 11 + 1009 * k }),
84
+ },
85
+ ],
86
+ };
87
+
88
+ /** The screen sets by id. */
89
+ export const SCREEN_SETS: Readonly<Record<string, ScreenSet>> = { [SCREEN_V1.id]: SCREEN_V1 };
90
+
91
+ /** This function gives the id of the member `seed` of a family of a set: `<set>/<family>/s<seed>`. */
92
+ export function inputId(set: string, family: string, seed: number): string {
93
+ return `${set}/${family}/s${seed}`;
94
+ }
95
+
96
+ /** This function gives the input files of a screen set, in the order of its families and seeds. Each file has its hash. */
97
+ export function screenInputs(set: ScreenSet = SCREEN_V1): LayoutInput[] {
98
+ return set.families.flatMap((f) =>
99
+ Array.from({ length: f.seeds }, (_, k) => graphInput(f.make(k), { id: inputId(set.id, f.name, k), set: set.id, family: f.name, seed: k, generator: f.generator(k), cell: f.cell })),
100
+ );
101
+ }
102
+
103
+ /** A spec of a manifest: one generator with its version and parameters, and the seeds that the corpus uses. */
104
+ export interface CorpusSpec {
105
+ readonly generator: string;
106
+ readonly version: number;
107
+ readonly params: Readonly<Record<string, unknown>>;
108
+ readonly seeds: readonly number[];
109
+ }
110
+
111
+ /** The manifest `corpus.json` of a corpus, of the form `layout-corpus/1`. */
112
+ export interface LayoutCorpus {
113
+ readonly schema: "layout-corpus/1";
114
+ readonly corpus: string;
115
+ readonly specs: readonly CorpusSpec[];
116
+ readonly inputs: readonly { readonly id: string; readonly hash: string; readonly cell: LayoutCell }[];
117
+ }
118
+
119
+ /** This function gives the manifest of a list of inputs. The inputs of one generator, version and parameters share one spec. */
120
+ export function corpusManifest(corpus: string, inputs: readonly LayoutInput[]): LayoutCorpus {
121
+ const specs = new Map<string, { generator: string; version: number; params: Readonly<Record<string, unknown>>; seeds: number[] }>();
122
+ for (const input of inputs) {
123
+ const g = input.generator;
124
+ if (!g) continue;
125
+ const key = JSON.stringify([g.name, g.version, g.params]);
126
+ const spec = specs.get(key) ?? { generator: g.name, version: g.version, params: g.params, seeds: [] };
127
+ if (!spec.seeds.includes(g.seed)) spec.seeds.push(g.seed);
128
+ specs.set(key, spec);
129
+ }
130
+ return {
131
+ schema: "layout-corpus/1",
132
+ corpus,
133
+ specs: [...specs.values()],
134
+ inputs: inputs.map((i) => ({ id: i.id, hash: i.hash, cell: i.cell ?? {} })),
135
+ };
136
+ }
@@ -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
+ }