@mark1russell7/highways 0.8.0 → 0.9.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.
Files changed (40) hide show
  1. package/README.md +74 -8
  2. package/build/index.d.ts +705 -183
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +3458 -1979
  5. package/build/index.js.map +1 -1
  6. package/package.json +3 -2
  7. package/src/bench/compare.ts +174 -5
  8. package/src/bench/exact/index.ts +4 -0
  9. package/src/bench/properties.ts +8 -2
  10. package/src/bench/room.ts +1 -1
  11. package/src/bench/run.ts +38 -3
  12. package/src/bench/screen/generators.ts +11 -41
  13. package/src/bench/screen/screen.ts +16 -10
  14. package/src/bench/screen/set.ts +156 -49
  15. package/src/bench/suite.ts +110 -12
  16. package/src/cases.ts +64 -18
  17. package/src/families/cases.ts +108 -0
  18. package/src/families/fixtures.ts +101 -0
  19. package/src/families/index.ts +36 -0
  20. package/src/families/model.ts +55 -0
  21. package/src/fixtures.ts +8 -2
  22. package/src/highways/soft-keys/index.ts +8 -4
  23. package/src/highways/stretches.ts +28 -7
  24. package/src/index.ts +8 -0
  25. package/src/lane-order.ts +42 -2
  26. package/src/lane-orders/stretch/index.ts +486 -0
  27. package/src/lane-orders/stretch/router.ts +22 -0
  28. package/src/levels/depths/index.ts +57 -0
  29. package/src/plugs/facing/index.ts +22 -0
  30. package/src/portfolio/router.ts +44 -0
  31. package/src/routers/corners.ts +27 -3
  32. package/src/routers/sections.ts +22 -40
  33. package/src/routers/shared.ts +51 -19
  34. package/src/routers/trunks.ts +21 -3
  35. package/src/segments.ts +8 -26
  36. package/src/strategies/builtins.ts +54 -2
  37. package/src/strategies/model.ts +55 -10
  38. package/src/strategies/options.ts +36 -4
  39. package/src/strategies/schemas.ts +68 -5
  40. package/src/whole/pipeline/index.ts +66 -0
@@ -3,6 +3,7 @@ import { nodeIndex, type Edge, type Graph, type Node, type Result, type Route, t
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
4
  import { justifyPlugs } from "../plugs/justify.ts";
5
5
  import { sidePlugs } from "../plugs/side.ts";
6
+ import type { PlugPair, Strategy } from "../strategies/model.ts";
6
7
  import { WayFinder, type WayOptions } from "../ways.ts";
7
8
 
8
9
  /** The ways to place a free plug of the corner router. */
@@ -24,6 +25,13 @@ export interface CornersOptions extends WayOptions {
24
25
  * Above 0, the free plugs of each side keep their order and move the least distance (`justifyPlugs`, WILD-002).
25
26
  */
26
27
  readonly plugGap?: number;
28
+ /**
29
+ * The plug rule: a strategy of the step `plugs` that places the plugs of each edge. It takes the place of the option `plugs`.
30
+ * Without it, the router places the plugs with the option `plugs`. An edge without plugs from the strategy gets the plugs of `facing`.
31
+ */
32
+ readonly plug?: Strategy<"plugs">;
33
+ /** The options of the strategy `plug`. The strategy gets its `defaults` for each option that this object does not give. */
34
+ readonly plugOptions?: object;
27
35
  }
28
36
 
29
37
  /**
@@ -38,7 +46,7 @@ export function cornersRouter(options: CornersOptions): Router {
38
46
  const byId = nodeIndex(graph);
39
47
  const ways = new WayFinder(graph.nodes, options);
40
48
  const routes: Route[] = [];
41
- if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
49
+ if (!options.plug && (options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
42
50
  for (const e of graph.edges) {
43
51
  const s = byId.get(e.source.node);
44
52
  const t = byId.get(e.target.node);
@@ -51,12 +59,17 @@ export function cornersRouter(options: CornersOptions): Router {
51
59
  }
52
60
  // First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
53
61
  const ends: [Edge, Node, Node, Plug, Plug][] = [];
62
+ const placed = options.plug ? plugsByEdge(options.plug.run({ graph, ways }, options.plugOptions)) : undefined;
54
63
  for (const e of graph.edges) {
55
64
  const s = byId.get(e.source.node);
56
65
  const t = byId.get(e.target.node);
57
66
  if (!s || !t) continue;
58
- const [ps, pt] =
59
- options.plugs === "side" ? sidePlugs(ways, s, e.source, t, e.target) : [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
67
+ const own = placed?.get(e.id)?.shift();
68
+ const [ps, pt]: [Plug, Plug] = own
69
+ ? [own.source, own.target]
70
+ : options.plugs === "side" && !placed
71
+ ? sidePlugs(ways, s, e.source, t, e.target)
72
+ : [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
60
73
  ends.push([e, s, t, ps, pt]);
61
74
  }
62
75
  if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
@@ -67,6 +80,17 @@ export function cornersRouter(options: CornersOptions): Router {
67
80
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
68
81
  }
69
82
 
83
+ /** This function gives the plugs of a plug strategy by the edge id. Two edges with one id take the entries of that id in order. */
84
+ function plugsByEdge(list: readonly PlugPair[]): Map<string, PlugPair[]> {
85
+ const out = new Map<string, PlugPair[]>();
86
+ for (const p of list) {
87
+ const own = out.get(p.edge);
88
+ if (own) own.push(p);
89
+ else out.set(p.edge, [p]);
90
+ }
91
+ return out;
92
+ }
93
+
70
94
  /**
71
95
  * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
72
96
  * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
@@ -1,9 +1,10 @@
1
1
  import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
2
2
  import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
3
- import { LEVEL_LINK_DEFAULTS, levelLinks, type ItemPair, type LevelLinkOptions } from "../levels.ts";
3
+ import { LEVEL_LINK_DEFAULTS, type LevelLinkOptions } from "../levels.ts";
4
+ import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
4
5
  import { nodeIndex, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
5
6
  import { placePlug } from "../plugs.ts";
6
- import { allSections, buildSections, checkSections, levelCut, SECTION_DEFAULTS, treeDepth, type Section, type SectionOptions } from "../sections.ts";
7
+ import { allSections, buildSections, checkSections, SECTION_DEFAULTS, type Section, type SectionOptions } from "../sections.ts";
7
8
  import { orderLanes, type RunOrder } from "../segments.ts";
8
9
  import type { Strategy } from "../strategies/model.ts";
9
10
  import { WayFinder } from "../ways.ts";
@@ -45,6 +46,14 @@ export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
45
46
  * The default is each depth, from 0 to the depth of the deepest section.
46
47
  */
47
48
  readonly levels?: readonly number[];
49
+ /**
50
+ * The level rule: a strategy of the step `levels` that makes the zoom levels from the tree and the routes.
51
+ * Without it, the router uses the strategy `depths` (`depthLevels`). The strategy gets the options `levels`, `margin`, `k`, `tightMargin`, `plugInset` and `throughCost` of the router,
52
+ * and then `levelOptions`.
53
+ */
54
+ readonly level?: Strategy<"levels">;
55
+ /** The options of the strategy `level`. They go over the level options of the router. */
56
+ readonly levelOptions?: object;
48
57
  /** The lane order of the runs. With `run`, the default, each run gets its order alone. With `bundle`, two edges keep one order on each run of their common stretch. */
49
58
  readonly laneOrder?: RunOrder;
50
59
  /** The benchmark skips a fixture with more nodes. */
@@ -210,7 +219,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
210
219
  if (list.length < 2 || p.length < 2) continue;
211
220
  runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!), options.laneOrder) });
212
221
  }
213
- return { routes, runs, levels: makeLevels(graph, root, routeOf, options) };
222
+ return { routes, runs, levels: makeLevels(graph, root, routes, options) };
214
223
  },
215
224
  };
216
225
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
@@ -224,43 +233,16 @@ function groupNodes(graph: Graph, options: SectionsOptions): Section {
224
233
  return root;
225
234
  }
226
235
 
236
+ /** The keys of the router options that the level rule gets. */
237
+ const LEVEL_KEYS = ["levels", "margin", "k", "tightMargin", "plugInset", "throughCost"] as const;
238
+
227
239
  /**
228
- * This function gives one zoom level for each depth of the tree, and a last level with each node alone.
229
- * With the option `levels`, only those depths get a level with groups.
230
- * At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
231
- * In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
232
- * In the last level, the points of a link are the route of its first edge.
240
+ * This function gives the zoom levels of the router: the levels of the strategy `level`, or else the levels of `depthLevels`.
241
+ * The strategy gets the level options of the router that have a value, and then `levelOptions`.
233
242
  */
234
- function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options: SectionsOptions): Level[] {
235
- const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
236
- const levels: Level[] = [];
237
- const deepest = treeDepth(root);
238
- const shown = options.levels && new Set(options.levels);
239
- for (let d = 0; d <= deepest + 1; d++) {
240
- if (shown && d <= deepest && !shown.has(d)) continue;
241
- const { items, owner } = levelCut(graph.nodes, root, d);
242
- const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
243
- for (const e of graph.edges) {
244
- const iu = indexOfNode.get(e.source.node);
245
- const iv = indexOfNode.get(e.target.node);
246
- if (iu === undefined || iv === undefined) continue;
247
- const a = owner[iu]!;
248
- const b = owner[iv]!;
249
- if (a === b) continue;
250
- const ida = items[a]!.id;
251
- const idb = items[b]!.id;
252
- const key = `${ida}\u0000${idb}`;
253
- const link = links.get(key);
254
- if (link) {
255
- link.count++;
256
- continue;
257
- }
258
- links.set(key, { source: ida, target: idb, count: 1, edge: e.id });
259
- }
260
- const pairs: ItemPair[] = [...links.values()].map(({ source, target, count }) => ({ source, target, count }));
261
- if (d > deepest)
262
- levels.push({ depth: d, items, links: [...links.values()].map(({ edge, ...l }) => ({ ...l, points: routeOf.get(edge)?.points ?? [] })) });
263
- else levels.push({ depth: d, items, links: levelLinks(items, pairs, options) });
264
- }
265
- return levels;
243
+ function makeLevels(graph: Graph, root: Section, routes: readonly Route[], options: SectionsOptions): Level[] {
244
+ if (!options.level) return depthLevels({ graph, root, routes }, options);
245
+ const own: Record<string, unknown> = {};
246
+ for (const key of LEVEL_KEYS) if (options[key] !== undefined) own[key] = options[key];
247
+ return options.level.run({ graph, root, routes }, { ...(own as DepthLevelOptions), ...options.levelOptions });
266
248
  }
@@ -2,12 +2,14 @@ import { center, dist, type Point } from "../geom.ts";
2
2
  import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
3
  import { channelWidth } from "../channels.ts";
4
4
  import { orderRun } from "../lane-order.ts";
5
+ import { stretchLaneOrders, type LaneStretchOrder } from "../lane-orders/stretch/index.ts";
5
6
  import { byText } from "../compare.ts";
6
7
  import { placePlug, type Plug } from "../plugs.ts";
7
8
  import { buildSections, type Section } from "../sections.ts";
8
9
  import { orderLanes, type RunOrder } from "../segments.ts";
9
10
  import { WayFinder, type WayOptions } from "../ways.ts";
10
11
  import { snellMerges } from "./snell.ts";
12
+ import { groupRuleOf, type GroupRuleName } from "./trunks.ts";
11
13
 
12
14
  /** The cost of a link that edges of one key share: a fixed discount, or the flow cost `len * f^alpha` (EDA-029). */
13
15
  export type ShareCost = "fixed" | "flow";
@@ -22,12 +24,22 @@ export type ShareMerge = "corner" | "snell";
22
24
  export interface SharedOptions extends WayOptions {
23
25
  /** The name in the benchmark. */
24
26
  readonly name: string;
25
- /** The edges with one key share their ways. Without a rule, all edges share. An edge without a key shares with no edge. */
26
- readonly rule?: GroupRule;
27
+ /**
28
+ * The edges with one key share their ways. The option takes a rule or the name of a built-in rule (`GROUP_RULE_NAMES`).
29
+ * Without a rule, or with `all`, all edges share. An edge without a key shares with no edge.
30
+ */
31
+ readonly rule?: GroupRule | GroupRuleName;
27
32
  /** The cost of a link that an edge with the same key uses already, as a share of its length. The default is 0.5. */
28
33
  readonly discount?: number;
29
34
  /** The lane order of the runs. With `bundle`, the default, two edges keep one order on each run of their common stretch. With `run`, each run gets its order alone. */
30
35
  readonly laneOrder?: RunOrder;
36
+ /**
37
+ * The lane order of each stretch of runs (LANE-STRETCH). With `none`, the default, each run gets its order alone.
38
+ * With `consensus`, `periphery` or `sifting`, the runs that follow each other on the routes share one order. Only the lane order `bundle` reads it.
39
+ */
40
+ readonly stretchOrder?: LaneStretchOrder;
41
+ /** The number of passes of the stretch order `sifting`, from 1 to 4. The default is 2. */
42
+ readonly siftPasses?: number;
31
43
  /**
32
44
  * The cost of a shared link. With `fixed`, the default, a link that the key uses already costs `discount` times its length.
33
45
  * With `flow`, one more edge on a link with `f` edges costs `len * ((f + 1)^alpha - f^alpha)` (EDA-029).
@@ -113,9 +125,11 @@ export function sharedRouter(options: SharedOptions): Router {
113
125
  const reuseExponent = Math.max(0, options.reuseExponent ?? 1);
114
126
  const capacity = Math.max(0, options.capacity ?? 0);
115
127
  const laneWidth = Math.max(0, options.laneWidth ?? 4);
128
+ // The name `all` is the same as no rule.
129
+ const rule = options.rule === undefined || options.rule === "all" ? undefined : groupRuleOf(options.rule);
116
130
  const keyOf = (e: Edge): string => {
117
- if (!options.rule) return "*";
118
- return options.rule(e) ?? `\u0000${e.id}`;
131
+ if (!rule) return "*";
132
+ return rule(e) ?? `\u0000${e.id}`;
119
133
  };
120
134
  const router: Router = {
121
135
  name: options.name,
@@ -238,20 +252,38 @@ export function sharedRouter(options: SharedOptions): Router {
238
252
  }
239
253
 
240
254
  // The users of each link, with the links to the ends. Two edges from one plug share the link from it.
241
- const users = new Map<string, Map<string, string[]>>();
255
+ const N = corners.points.length + ends.size + 1;
256
+ const pairOf = (a: number, b: number): number => (a < b ? a * N + b : b * N + a);
257
+ const users = new Map<string, Map<number, string[]>>();
242
258
  for (const w of order) {
243
259
  if (!w.found) continue;
244
260
  let mine = users.get(w.key);
245
261
  if (!mine) users.set(w.key, (mine = new Map()));
246
262
  for (let i = 1; i < w.ids.length; i++) {
247
- const a = w.ids[i - 1]!;
248
- const b = w.ids[i]!;
249
- const k = a < b ? `${a}:${b}` : `${b}:${a}`;
263
+ const k = pairOf(w.ids[i - 1]!, w.ids[i]!);
250
264
  const list = mine.get(k);
251
265
  if (list) list.push(w.edge.id);
252
266
  else mine.set(k, [w.edge.id]);
253
267
  }
254
268
  }
269
+ // The signature of each link (ROUTE-SCALE): 0 for a link with fewer than 2 users, else the number of the sorted list of its users.
270
+ // Each link gets its signature one time. Two links get the same number only when they have the same sorted list, so the runs do not change.
271
+ const texts = new Map<string, number>();
272
+ const sigs = new Map<string, Map<number, number>>();
273
+ for (const [key, mine] of users) {
274
+ const own = new Map<number, number>();
275
+ for (const [k, list] of mine) {
276
+ if (list.length < 2) {
277
+ own.set(k, 0);
278
+ continue;
279
+ }
280
+ const text = [...list].sort(byText).join("\u0001");
281
+ let n = texts.get(text);
282
+ if (n === undefined) texts.set(text, (n = texts.size + 1));
283
+ own.set(k, n);
284
+ }
285
+ sigs.set(key, own);
286
+ }
255
287
 
256
288
  // Each way breaks into stretches where the set of edges on the links stays the same.
257
289
  // A shared stretch loses its needless corners one time, so it has the same points in each of its routes.
@@ -259,13 +291,8 @@ export function sharedRouter(options: SharedOptions): Router {
259
291
  const points = new Map<string, Point[]>();
260
292
  for (const w of all) {
261
293
  if (!w.found) continue;
262
- const used = users.get(w.key)!;
263
- const sig = (i: number): string => {
264
- const a = w.ids[i]!;
265
- const b = w.ids[i + 1]!;
266
- const list = used.get(a < b ? `${a}:${b}` : `${b}:${a}`)!;
267
- return list.length < 2 ? "" : [...list].sort(byText).join("\u0001");
268
- };
294
+ const own = sigs.get(w.key)!;
295
+ const sig = (i: number): number => own.get(pairOf(w.ids[i]!, w.ids[i + 1]!))!;
269
296
  const out: Point[] = [w.ps.point];
270
297
  const append = (piece: readonly Point[]): void => {
271
298
  for (const p of piece) if (dist(p, out[out.length - 1]!) > 1e-9) out.push(p);
@@ -275,7 +302,7 @@ export function sharedRouter(options: SharedOptions): Router {
275
302
  const s = sig(i);
276
303
  if (i + 1 < w.seq.length - 1 && sig(i + 1) === s) continue;
277
304
  const raw = w.seq.slice(start, i + 2);
278
- if (s === "") append(corners.pull(raw));
305
+ if (s === 0) append(corners.pull(raw));
279
306
  else {
280
307
  const a = w.ids[start]!;
281
308
  const b = w.ids[i + 1]!;
@@ -298,10 +325,15 @@ export function sharedRouter(options: SharedOptions): Router {
298
325
  for (const w of all) routes.push({ edge: w.edge.id, points: points.get(w.edge.id) ?? [w.ps.point, w.pt.point] });
299
326
  const shared: SharedRun[] = [];
300
327
  const routeOf = new Map(routes.map((r) => [r.edge, r]));
301
- for (const run of runs.values()) {
302
- const edges = options.laneOrder === "run" ? laneOrderOfRun(run) : orderLanes(run.points, run.ways.map((w) => routeOf.get(w.edge.id)!), "bundle");
328
+ const list = [...runs.values()];
329
+ const routesOf = (run: Run): Route[] => run.ways.map((w) => routeOf.get(w.edge.id)!);
330
+ // With a stretch order, the runs that follow each other on the routes share one lane order (LANE-STRETCH).
331
+ const method = options.laneOrder === "run" ? "none" : (options.stretchOrder ?? "none");
332
+ const stretch = method === "none" ? undefined : stretchLaneOrders(list.map((run) => ({ points: run.points, routes: routesOf(run) })), { method, passes: options.siftPasses ?? 2 });
333
+ list.forEach((run, i) => {
334
+ const edges = options.laneOrder === "run" ? laneOrderOfRun(run) : (stretch?.[i] ?? orderLanes(run.points, routesOf(run), "bundle"));
303
335
  shared.push({ id: `r${shared.length}`, key: run.key, points: run.points, edges });
304
- }
336
+ });
305
337
  return { routes, runs: shared };
306
338
  },
307
339
  };
@@ -9,8 +9,8 @@ import { balancedFan, fanPath, gilbertPoint } from "./fan.ts";
9
9
  export interface TrunksOptions extends WayOptions {
10
10
  /** The name in the benchmark. */
11
11
  readonly name: string;
12
- /** The grouping rule. The edges from one plug with one key share a trunk. */
13
- readonly rule: GroupRule;
12
+ /** The grouping rule, or the name of a built-in rule (`GROUP_RULE_NAMES`). The edges from one plug with one key share a trunk. */
13
+ readonly rule: GroupRule | GroupRuleName;
14
14
  /** The split point is at this share of the way to the center of the targets. The default is 0.5. */
15
15
  readonly split?: number;
16
16
  /** A group with fewer edges stays unbundled. The default is 2. */
@@ -55,6 +55,23 @@ export const byCollection: GroupRule = (e) => {
55
55
  return typeof c === "string" ? c : undefined;
56
56
  };
57
57
 
58
+ /** The names of the built-in grouping rules of the edges. With `collection`, the rule is `byCollection`. With `all`, each edge has one key. */
59
+ export type GroupRuleName = "collection" | "all";
60
+
61
+ /** The names of the built-in grouping rules, in the order of a control. */
62
+ export const GROUP_RULE_NAMES: readonly GroupRuleName[] = ["collection", "all"];
63
+
64
+ /** The rule `all`: each edge has the same key. */
65
+ const allEdges: GroupRule = () => "*";
66
+
67
+ /** This function gives the rule of a name of `GROUP_RULE_NAMES`. It gives a rule of the caller as it is. It throws a `RangeError` for an unknown name. */
68
+ export function groupRuleOf(rule: GroupRule | GroupRuleName): GroupRule {
69
+ if (typeof rule === "function") return rule;
70
+ if (rule === "collection") return byCollection;
71
+ if (rule === "all") return allEdges;
72
+ throw new RangeError(`There is no grouping rule "${String(rule)}". The names are ${GROUP_RULE_NAMES.join(" and ")}.`);
73
+ }
74
+
58
75
  /**
59
76
  * This function groups the edges by their source plug and their key. Each group keeps the order of the edges.
60
77
  * A group with fewer than `minSize` edges does not appear.
@@ -189,6 +206,7 @@ export function sharedWays(routes: readonly Route[], from: number): { points: Po
189
206
  * The edges outside the groups go as in the corner router.
190
207
  */
191
208
  export function trunksRouter(options: TrunksOptions): Router {
209
+ const rule = groupRuleOf(options.rule);
192
210
  const router: Router = {
193
211
  name: options.name,
194
212
  route(graph: Graph): Result {
@@ -200,7 +218,7 @@ export function trunksRouter(options: TrunksOptions): Router {
200
218
  const part = (options.leave ?? "part") === "part";
201
219
  // A branch leaves the trunk early only on a line that keeps out of the boxes as far as the corner graph does.
202
220
  const clear = (a: Point, b: Point): boolean => ways.obstacles.clear(a, b, ways.margin / 2);
203
- for (const [id, group] of groupEdges(graph.edges, options.rule, options.minSize ?? 2)) {
221
+ for (const [id, group] of groupEdges(graph.edges, rule, options.minSize ?? 2)) {
204
222
  const first = group[0]!;
205
223
  const s = byId.get(first.source.node);
206
224
  const targets = group.map((e) => byId.get(e.target.node));
package/src/segments.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { dist, type Point } from "./geom.ts";
2
- import { orderPlaced, type Placed } from "./lane-order.ts";
2
+ import { findRun, orderPlaced, placeRoutes } from "./lane-order.ts";
3
3
  import type { Result, Route } from "./model.ts";
4
4
 
5
5
  /** A piece of drawn way. A shared segment carries several edges, in lane order. A private segment carries one edge. */
@@ -34,16 +34,6 @@ export interface SegmentGraph {
34
34
 
35
35
  const SAME = 1e-6;
36
36
 
37
- /** This function gives the index where `p` holds the points of `part` in a row, from `from` on, or -1. */
38
- function indexOf(p: readonly Point[], part: readonly Point[], from: number): number {
39
- for (let i = from; i + part.length <= p.length; i++) {
40
- let ok = true;
41
- for (let j = 0; ok && j < part.length; j++) ok = dist(p[i + j]!, part[j]!) <= SAME;
42
- if (ok) return i;
43
- }
44
- return -1;
45
- }
46
-
47
37
  /**
48
38
  * This function breaks each route into segments. The runs and the trunks of the bundles become shared segments.
49
39
  * The other parts of each route become private segments. The segments of a path, in order, give the points of its route again.
@@ -68,10 +58,10 @@ export function segmentsOf(result: Result): SegmentGraph {
68
58
  // The place of each shared segment in the route, in the order of the route.
69
59
  const found: { at: number; end: number; ref: SegmentRef }[] = [];
70
60
  for (const s of onEdge.get(route.edge) ?? []) {
71
- let at = indexOf(p, s.points, 0);
61
+ let at = findRun(p, s.points, 0);
72
62
  let reverse = false;
73
63
  if (at < 0) {
74
- at = indexOf(p, [...s.points].reverse(), 0);
64
+ at = findRun(p, [...s.points].reverse(), 0);
75
65
  reverse = true;
76
66
  }
77
67
  if (at >= 0) found.push({ at, end: at + s.points.length - 1, ref: { segment: s.id, reverse, lane: s.lane } });
@@ -174,6 +164,8 @@ export function laneRoutes(source: Result | SegmentGraph, open: ReadonlySet<stri
174
164
  /** This function gives the route of each path, with the lanes of each segment at the spacing that `spacingOf` gives. */
175
165
  function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Route[] {
176
166
  const byId = new Map(graph.segments.map((s) => [s.id, s]));
167
+ // Each segment gets its spacing one time (ROUTE-SCALE). A key of `runKey` sorts the edges of the segment, and a busy segment is on many paths.
168
+ const spacings = new Map<Segment, number>();
177
169
  const routes: Route[] = [];
178
170
  for (const path of graph.paths) {
179
171
  const ends = pathPoints(graph, path);
@@ -181,7 +173,8 @@ function lanePaths(graph: SegmentGraph, spacingOf: (s: Segment) => number): Rout
181
173
  for (const ref of path.segments) {
182
174
  const s = byId.get(ref.segment);
183
175
  if (!s) continue;
184
- const spacing = spacingOf(s);
176
+ let spacing = spacings.get(s);
177
+ if (spacing === undefined) spacings.set(s, (spacing = spacingOf(s)));
185
178
  const moved = offsetPolyline(s.points, ((s.count - 1) / 2 - ref.lane) * spacing);
186
179
  for (const q of ref.reverse ? moved.reverse() : moved) if (out.length === 0 || dist(q, out[out.length - 1]!) > SAME) out.push(q);
187
180
  }
@@ -218,18 +211,7 @@ export type RunOrder = "bundle" | "run";
218
211
  * On a run with a cycle, the order depends on the order of `routes`. The item LANE-STRETCH is to remove the cycles.
219
212
  */
220
213
  export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
221
- const placed: Placed[] = [];
222
- const lost: string[] = [];
223
- for (const r of routes) {
224
- let at = indexOf(r.points, run, 0);
225
- if (at >= 0) placed.push({ edge: r.edge, p: r.points, s: at, e: at + run.length - 1 });
226
- else {
227
- const p = [...r.points].reverse();
228
- at = indexOf(p, run, 0);
229
- if (at >= 0) placed.push({ edge: r.edge, p, s: at, e: at + run.length - 1 });
230
- else lost.push(r.edge);
231
- }
232
- }
214
+ const { placed, lost } = placeRoutes(run, routes);
233
215
  const lanes = orderPlaced(placed, order);
234
216
  return [...lanes.map((x) => x.edge), ...lost.sort()];
235
217
  }
@@ -66,7 +66,9 @@ import { TRACLUS_DEFAULTS, traclusPlan, type TraclusOptions } from "../highways/
66
66
  import { WEISZFELD_DEFAULTS, weiszfeldPlan, type WeiszfeldOptions } from "../highways/weiszfeld/index.ts";
67
67
  import { WSPD_DEFAULTS, wspdPlan, type WspdOptions } from "../highways/wspd/index.ts";
68
68
  import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zipper/index.ts";
69
+ import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
69
70
  import type { Graph, Result, Router } from "../model.ts";
71
+ import { facingEnds } from "../plugs/facing/index.ts";
70
72
  import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
71
73
  import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
72
74
  import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
@@ -76,10 +78,12 @@ import { sharedRouter, type SharedOptions } from "../routers/shared.ts";
76
78
  import { straightRouter } from "../routers/straight.ts";
77
79
  import { byCollection, trunksRouter, type TrunksOptions } from "../routers/trunks.ts";
78
80
  import { buildSections, SECTION_DEFAULTS, type SectionOptions } from "../sections.ts";
81
+ import { runPipeline, type PipelineOptions } from "../whole/pipeline/index.ts";
79
82
  import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
80
83
  import type { OptionSchema } from "./options.ts";
81
84
  import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
82
- import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA } from "./schemas.ts";
85
+ import { DEPTHS_SCHEMA, PIPELINE_SCHEMA } from "./schemas.ts";
86
+ import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA } from "./schemas.ts";
83
87
 
84
88
  /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
85
89
  export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
@@ -150,7 +154,7 @@ export const routerStrategies: {
150
154
  sharedRouter,
151
155
  SHARED_SCHEMA,
152
156
  ),
153
- "shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter, SHARED_SCHEMA),
157
+ "shared-all": routerStrategy<RouterPreset<SharedOptions>>("shared-all", "The same as `shared`, but all edges share their ways.", 10_000, {}, sharedRouter, SHARED_ALL_SCHEMA),
154
158
  sections: routerStrategy<RouterPreset<SectionsOptions>>(
155
159
  "sections",
156
160
  "A tree of sections, with a hub in each section. A long edge goes through the hubs, and a short edge goes directly.",
@@ -895,6 +899,51 @@ export const highwayStrategies: {
895
899
  "corridors+ids": gateStrategy(corridors, stableIdGate),
896
900
  });
897
901
 
902
+ /** The built-in plug rules: the strategies of the step `plugs`. The router `corners` routes between their plugs with its option `plug`. */
903
+ export const plugStrategies: {
904
+ readonly facing: Strategy<"plugs", object>;
905
+ } = Object.freeze({
906
+ facing: defineStrategy({
907
+ step: "plugs",
908
+ name: "facing",
909
+ describe: "Each free plug faces the other end of its edge, on the side of the larger component of the direction. It is the default of the router `corners`.",
910
+ defaults: {},
911
+ schema: {},
912
+ run: (input) => facingEnds(input),
913
+ }),
914
+ });
915
+
916
+ /** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
917
+ export const levelStrategies: {
918
+ readonly depths: Strategy<"levels", DepthLevelOptions>;
919
+ } = Object.freeze({
920
+ depths: defineStrategy<"levels", DepthLevelOptions>({
921
+ step: "levels",
922
+ name: "depths",
923
+ describe: "One zoom level for each depth of the tree, and a last level with each node alone. A link of a level goes around the other items. It is the default of the router `sections`.",
924
+ defaults: {},
925
+ schema: DEPTHS_SCHEMA,
926
+ run: (input, o) => depthLevels(input, o),
927
+ }),
928
+ });
929
+
930
+ /**
931
+ * The built-in whole strategies: the strategies of the step `whole`. The strategy `pipeline` runs one strategy of each step, as the demo of the site does.
932
+ * Without options, it routes the graph with the router `corners`.
933
+ */
934
+ export const wholeStrategies: {
935
+ readonly pipeline: Strategy<"whole", PipelineOptions>;
936
+ } = Object.freeze({
937
+ pipeline: defineStrategy<"whole", PipelineOptions>({
938
+ step: "whole",
939
+ name: "pipeline",
940
+ describe: "One setting of the steps. The room opens the board, then the router or the highway plan routes it with the rules that the router reads.",
941
+ defaults: { router: routerStrategies.corners as Strategy<"router"> },
942
+ schema: PIPELINE_SCHEMA,
943
+ run: (graph, o) => runPipeline(graph, o),
944
+ }),
945
+ });
946
+
898
947
  /** The built-in strategies of each step, in order. A consumer adds its own: `createRegistry([...BUILT_IN_STRATEGIES, mine])`. */
899
948
  export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
900
949
  ...Object.values(groupStrategies),
@@ -902,6 +951,9 @@ export const BUILT_IN_STRATEGIES: readonly AnyStrategy[] = Object.freeze([
902
951
  ...Object.values(highwayStrategies),
903
952
  ...Object.values(curveStrategies),
904
953
  ...Object.values(roomStrategies),
954
+ ...Object.values(plugStrategies),
955
+ ...Object.values(levelStrategies),
956
+ ...Object.values(wholeStrategies),
905
957
  ]);
906
958
 
907
959
  /** The registry of the built-in strategies. The benchmark and the site take their lists from it. */