@mark1russell7/highways 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +43 -1
  2. package/build/index.d.ts +1377 -5
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +8202 -1515
  5. package/build/index.js.map +1 -1
  6. package/package.json +3 -1
  7. package/src/bench/exact/index.ts +197 -0
  8. package/src/bench/measures/highways.ts +93 -0
  9. package/src/bench/measures/index.ts +2 -0
  10. package/src/bench/measures/lanes.ts +304 -0
  11. package/src/bench/measures/levels.ts +86 -2
  12. package/src/bench/screen/forms.ts +11 -5
  13. package/src/bench/screen/measures.ts +23 -0
  14. package/src/bench/screen/screen.ts +42 -0
  15. package/src/channels.ts +56 -0
  16. package/src/highways/base.ts +126 -0
  17. package/src/highways/diffusion/index.ts +103 -0
  18. package/src/highways/edge-paths/index.ts +155 -0
  19. package/src/highways/fixtures.test-util.ts +71 -0
  20. package/src/highways/flow-clusters/index.ts +143 -0
  21. package/src/highways/flow-field/index.ts +49 -0
  22. package/src/highways/gates/ambiguity/index.ts +110 -0
  23. package/src/highways/gates/breakeven/index.ts +62 -0
  24. package/src/highways/gates/hysteresis/index.ts +48 -0
  25. package/src/highways/gates/ids/index.ts +59 -0
  26. package/src/highways/gates/index.ts +6 -0
  27. package/src/highways/gates/model.ts +129 -0
  28. package/src/highways/gates/stretch/index.ts +39 -0
  29. package/src/highways/grouped.ts +123 -0
  30. package/src/highways/growth/index.ts +148 -0
  31. package/src/highways/hdbscan/index.ts +250 -0
  32. package/src/highways/hough/index.ts +182 -0
  33. package/src/highways/hub-arcs/index.ts +181 -0
  34. package/src/highways/index.ts +30 -0
  35. package/src/highways/lanes/index.ts +189 -0
  36. package/src/highways/level-links/index.ts +114 -0
  37. package/src/highways/meet/index.ts +84 -0
  38. package/src/highways/mingle/index.ts +171 -0
  39. package/src/highways/model.ts +5 -0
  40. package/src/highways/nested/index.ts +143 -0
  41. package/src/highways/od-cells/index.ts +50 -0
  42. package/src/highways/quickbundles/index.ts +117 -0
  43. package/src/highways/ridges/index.ts +328 -0
  44. package/src/highways/road-classes/index.ts +74 -0
  45. package/src/highways/router.ts +1 -1
  46. package/src/highways/select/index.ts +162 -0
  47. package/src/highways/snap.ts +110 -0
  48. package/src/highways/soft-keys/index.ts +114 -0
  49. package/src/highways/spiral/index.ts +205 -0
  50. package/src/highways/stretches.ts +141 -0
  51. package/src/highways/stubs/index.ts +117 -0
  52. package/src/highways/traclus/index.ts +296 -0
  53. package/src/highways/weiszfeld/index.ts +105 -0
  54. package/src/highways/wspd/index.ts +130 -0
  55. package/src/highways/zipper/index.ts +168 -0
  56. package/src/index.ts +1 -0
  57. package/src/routers/shared.ts +56 -3
  58. package/src/strategies/builtins.ts +303 -7
  59. package/src/strategies/schemas.ts +300 -0
  60. package/src/visibility.ts +5 -0
@@ -1,5 +1,5 @@
1
- import { center, dist } from "../../geom.ts";
2
- import type { Graph, Level } from "../../model.ts";
1
+ import { center, dist, pointBoxDistance, polylineLength, type Box, type Point } from "../../geom.ts";
2
+ import type { Graph, Level, Route } from "../../model.ts";
3
3
  import { kendallTau } from "./stats.ts";
4
4
 
5
5
  /** This function gives the levels in the order of their depth, from the far view to the near view. */
@@ -101,3 +101,87 @@ export function orderStability(graph: Graph, levels: readonly Level[]): number {
101
101
  }
102
102
  return n ? sum / n : 1;
103
103
  }
104
+
105
+ /** This function gives the points of a polyline at most `step` apart: each vertex, and evenly spaced points on each longer segment. */
106
+ function samplePolyline(p: readonly Point[], step: number): Point[] {
107
+ const out: Point[] = [];
108
+ for (let i = 0; i < p.length; i++) {
109
+ const b = p[i]!;
110
+ if (i > 0) {
111
+ const a = p[i - 1]!;
112
+ const n = Math.ceil(dist(a, b) / step);
113
+ for (let k = 1; k < n; k++) out.push({ x: a.x + ((b.x - a.x) * k) / n, y: a.y + ((b.y - a.y) * k) / n });
114
+ }
115
+ out.push(b);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** This function gives the Hausdorff distance between two sets of points. Both sets have one point or more. */
121
+ function hausdorff(a: readonly Point[], b: readonly Point[]): number {
122
+ const directed = (p: readonly Point[], q: readonly Point[]): number => {
123
+ let worst = 0;
124
+ for (const x of p) {
125
+ let best = Number.POSITIVE_INFINITY;
126
+ for (const y of q) best = Math.min(best, dist(x, y));
127
+ worst = Math.max(worst, best);
128
+ }
129
+ return worst;
130
+ };
131
+ return Math.max(directed(a, b), directed(b, a));
132
+ }
133
+
134
+ /** The number of points of the sample of one polyline for the link agreement, at most. */
135
+ const AGREEMENT_SAMPLES = 64;
136
+
137
+ /**
138
+ * This function gives the link agreement of the levels with groups (Z9 of MEAS0).
139
+ * `la = mean over links of hausdorff(link, routes of its edges outside the two items) / u`, with `u` the argument `unit`.
140
+ * The edges of a link are the edges between a node of one item and a node of the other item, in either direction.
141
+ *
142
+ * The function samples each polyline at points `u / 4` apart, with at most 64 points for each polyline. It keeps the points outside the boxes of the two items.
143
+ * A link without such points on both sides does not count. It gives `undefined` when no link counts.
144
+ * The value has no unit, and lower is better. The cost is quadratic in the points of each link.
145
+ */
146
+ export function linkAgreement(graph: Graph, levels: readonly Level[], routes: readonly Route[], unit: number): number | undefined {
147
+ const u = unit > 0 ? unit : 1;
148
+ const endsOf = new Map(graph.edges.map((e) => [e.id, [e.source.node, e.target.node] as const]));
149
+ const sample = (p: readonly Point[], boxes: readonly Box[]): Point[] => {
150
+ const step = Math.max(u / 4, polylineLength(p) / AGREEMENT_SAMPLES);
151
+ return samplePolyline(p, step).filter((q) => boxes.every((b) => pointBoxDistance(q, b) > 0));
152
+ };
153
+ let sum = 0;
154
+ let n = 0;
155
+ for (const level of levels) {
156
+ if (!level.items.some((i) => i.nodes.length > 1)) continue;
157
+ const itemOf = itemIndex(level);
158
+ const indexOf = new Map(level.items.map((it, i) => [it.id, i] as const));
159
+ // The edges between each pair of items, by the key of the pair.
160
+ const between = new Map<string, Route[]>();
161
+ for (const r of routes) {
162
+ const ends = endsOf.get(r.edge);
163
+ if (!ends) continue;
164
+ const i = itemOf.get(ends[0]);
165
+ const j = itemOf.get(ends[1]);
166
+ if (i === undefined || j === undefined || i === j) continue;
167
+ const key = i < j ? `${i},${j}` : `${j},${i}`;
168
+ const list = between.get(key);
169
+ if (list) list.push(r);
170
+ else between.set(key, [r]);
171
+ }
172
+ for (const link of level.links) {
173
+ const i = indexOf.get(link.source);
174
+ const j = indexOf.get(link.target);
175
+ if (i === undefined || j === undefined || i === j) continue;
176
+ const mine = between.get(i < j ? `${i},${j}` : `${j},${i}`);
177
+ if (!mine) continue;
178
+ const boxes = [level.items[i]!.box, level.items[j]!.box];
179
+ const a = sample(link.points, boxes);
180
+ const b = mine.flatMap((r) => sample(r.points, boxes));
181
+ if (a.length === 0 || b.length === 0) continue;
182
+ sum += hausdorff(a, b) / u;
183
+ n++;
184
+ }
185
+ }
186
+ return n > 0 ? sum / n : undefined;
187
+ }
@@ -47,8 +47,8 @@ export interface LayoutBox {
47
47
  readonly parent?: string;
48
48
  /** The group of the box. */
49
49
  readonly group?: string;
50
- /** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. The router ignores it. */
51
- readonly locked?: true;
50
+ /** This value is `true` when the box is an obstacle: the packer keeps its `x` and `y`. `false` is the same as no value. The router ignores it. */
51
+ readonly locked?: boolean;
52
52
  /** The reading order that the caller wants. The router ignores it. */
53
53
  readonly rank?: number;
54
54
  readonly ports?: readonly LayoutPort[];
@@ -87,7 +87,7 @@ export interface LayoutGenerator {
87
87
  * The packer reads the first six dimensions. The router reads the last four.
88
88
  */
89
89
  export interface LayoutCell {
90
- readonly containerSize?: "fixed" | "freeWidth" | "freeAspect";
90
+ readonly containerSize?: "fixed" | "freeHeight" | "freeWidth" | "freeAspect" | "freeArea";
91
91
  readonly containerShape?: "rect" | "convex" | "polygon" | "circle" | "ellipse" | "holes";
92
92
  readonly obstacles?: "none" | "center" | "scattered" | "wall" | "corridors" | "touching" | "edge";
93
93
  readonly sizes?: "identical" | "uniform" | "heavyTail" | "bimodal" | "extremeAspect" | "nearIdentical";
@@ -291,19 +291,25 @@ export function regionBounds(region: Region): { x: number; y: number; w: number;
291
291
  return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
292
292
  }
293
293
 
294
+ /**
295
+ * The fields of an input that `inputGraph` reads. A contract input, for example the result of `applyPack`, has these fields.
296
+ * Without `edges`, the input has no edges.
297
+ */
298
+ export type RouteInput = Pick<LayoutInput, "id" | "boxes" | "obstacles"> & { readonly edges?: LayoutInput["edges"] };
299
+
294
300
  /**
295
301
  * This function reads the graph of the router from an input file. The router reads the positions, the ports, the edges and the data.
296
302
  * Each obstacle becomes a node without edges, with the bounding box of its region, after the boxes.
297
303
  * The router ignores the other fields of the packer: `container`, `gap`, `locked`, `rank`, the `shape` of a box and the `move` of an obstacle.
298
304
  * A box without `x` and `y` is an error.
299
305
  */
300
- export function inputGraph(input: LayoutInput): Graph {
306
+ export function inputGraph(input: RouteInput): Graph {
301
307
  const nodes = input.boxes.map((b): Node => {
302
308
  if (b.x === undefined || b.y === undefined) throw new Error(`The box "${b.id}" of the input "${input.id}" has no position. The router needs a placed input.`);
303
309
  return { id: b.id, box: { x: b.x, y: b.y, w: b.w, h: b.h }, ...(b.ports ? { ports: b.ports.map(portToModel) } : {}), ...(b.data ? { data: b.data } : {}) };
304
310
  });
305
311
  for (const o of input.obstacles ?? []) nodes.push({ id: o.id, box: regionBounds(o.region) });
306
- const edges = input.edges.map(
312
+ const edges = (input.edges ?? []).map(
307
313
  (e): Edge => ({
308
314
  id: e.id,
309
315
  source: { node: e.source, ...(e.sourcePort !== undefined ? { port: e.sourcePort } : {}) },
@@ -45,6 +45,9 @@ const ROWS: readonly Row[] = [
45
45
  ["R11.q05", "R11", "route.plug-spacing.q05", "0 to 1", "higher", true],
46
46
  ["R13.outside", "R13", "route.outside", "count", "lower", false],
47
47
  ["R15.inflections", "R15", "route.inflections", "count for each route", "lower", false],
48
+ // MEAS3A, for the steps `router` and `highway`. R12 is the share of the ink near a parallel leg of other edges. A narrow channel between boxes moves it.
49
+ ["R12.share", "R12", "route.lane.separation", "0 to 1", "lower", true],
50
+ ["R14.share", "R14", "route.orthogonality", "0 to 1", "higher", false],
48
51
  ["ink", "R1", "route.ink", "units", "lower", true],
49
52
  // The bundles.
50
53
  ["B1.inkSaved", "B1", "route.bundle.ink-saved", "0 to 1", "higher", false],
@@ -54,6 +57,18 @@ const ROWS: readonly Row[] = [
54
57
  ["B3.median", "B3", "route.bundle.stretch.median", "ratio", "lower", false],
55
58
  ["B3.q95", "B3", "route.bundle.stretch.q95", "ratio", "lower", false],
56
59
  ["B6.max", "B6", "route.bundle.merge-angle.max", "degrees", "lower", false],
60
+ // MEAS3A, for the steps `router` and `highway`. B5, B8 and B9 read the runs and the trunks of the result. B8 is good or bad only in context, as B2.
61
+ ["B5.crossings", "B5", "route.bundle.lane-crossings", "count", "lower", false],
62
+ ["B7.ratio", "B7", "route.bundle.occupied-area", "ratio", "lower", false],
63
+ ["B8.runs", "B8", "route.bundle.runs", "count", "lower", false],
64
+ ["B8.median", "B8", "route.bundle.run-edges.median", "count", "higher", false],
65
+ ["B8.max", "B8", "route.bundle.run-edges.max", "count", "higher", false],
66
+ ["B9.median", "B9", "route.bundle.lane-fidelity.median", "ratio", "lower", false],
67
+ ["B9.max", "B9", "route.bundle.lane-fidelity.max", "ratio", "lower", false],
68
+ // M1 and M4 of the room, for the steps `router` and `highway`: the lanes of each shared segment open at `MEASURE_LANE_WIDTH`.
69
+ // The step `room` of `runSuite` gives the same keys for the boards that a room strategy opens.
70
+ ["M1.area", "M1", "route.lane.area", "ratio", "lower", false],
71
+ ["M4.laneThrough", "M4", "route.lane.through", "count", "lower", true],
57
72
  // The order fragility (W3 of MEAS3, from WILD-056): the spread of a measure over random orders of the edges. The placement can move it.
58
73
  ["W3.crossings", "W3", "route.order.crossings", "IQR over the median", "lower", true],
59
74
  ["W3.detour", "W3", "route.order.detour", "IQR over the median", "lower", true],
@@ -68,8 +83,16 @@ const ROWS: readonly Row[] = [
68
83
  ["Z10.base.mean", "Z10", "route.stability.base.mean", "units of the board", "lower", false],
69
84
  ["Z10.base.max", "Z10", "route.stability.base.max", "units of the board", "lower", false],
70
85
  ["Z10.base.moved", "Z10", "route.stability.base.moved", "0 to 1", "lower", false],
86
+ // The stability of a highway strategy over a sequence of seeded edits, with its plan before each edit (the screen option `edits`).
87
+ ["Z10.edits.mean", "Z10", "route.stability.edits.mean", "units of the board", "lower", false],
88
+ ["Z10.edits.moved", "Z10", "route.stability.edits.moved", "0 to 1", "lower", false],
89
+ ["Z10.churn", "Z10", "route.stability.highway-churn", "0 to 1", "lower", false],
90
+ ["Z10.ids", "Z10", "route.stability.highway-ids", "0 to 1", "higher", false],
71
91
  // The zoom levels and the groups. The default grouping rule reads the positions, so most of these measures depend on the placement.
72
92
  ["level-nesting", "Z6", "route.level.nesting", "count", "lower", false],
93
+ // MEAS3A, for the steps `router`, `highway` and `group`. Z2 is the count of the hard rule `link-ends`. Z9 needs a result with zoom levels.
94
+ ["Z2.linkEnds", "Z2", "route.level.link-ends", "count", "lower", false],
95
+ ["Z9.mean", "Z9", "route.level.link-agreement", "no unit", "lower", true],
73
96
  ["Z3.through", "Z3", "route.level.through", "count", "lower", true],
74
97
  ["Z3.length", "Z3", "route.level.link-length", "units", "lower", true],
75
98
  ["Z4.farItems", "Z4", "route.level.far-items", "count", "lower", true],
@@ -6,8 +6,12 @@ import { STRATEGIES } from "../../strategies/builtins.ts";
6
6
  import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
7
7
  import { compareStep, type CompareRow, type CompareStep, type StepComparison } from "../compare.ts";
8
8
  import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
9
+ import { highwayEdits } from "../measures/highways.ts";
10
+ import { laneArea, laneCrossings, laneFidelity, lanesThrough, occupiedArea, openLanes, orthogonality, routeSeparation, runProfile } from "../measures/lanes.ts";
11
+ import { linkAgreement } from "../measures/levels.ts";
9
12
  import { orderFragility } from "../measures/order.ts";
10
13
  import { paretoFront, type Objective } from "../measures/pareto.ts";
14
+ import { boardUnit } from "../measures/routes.ts";
11
15
  import { routeStability } from "../measures/stability.ts";
12
16
  import { quantile } from "../measures/stats.ts";
13
17
  import { checkLevels, checkProperties, TOLERANCE } from "../properties.ts";
@@ -84,6 +88,12 @@ export interface ScreenOptions {
84
88
  * The default is 0: no measure of the order fragility. The steps `group` and `curve` do not read it.
85
89
  */
86
90
  readonly shuffles?: number;
91
+ /**
92
+ * The number of seeded edits of the step `highway` (BUILD-W2A). Each highway strategy runs again after each edit of `highwayEdits`, with its plan before the edit.
93
+ * The records then get `Z10.edits.*`, the churn of the highways and the share of the highways that keep their id.
94
+ * The default is 0: no edits. The other steps and the base routers do not read it.
95
+ */
96
+ readonly edits?: number;
87
97
  /** The commit of the code, for the records. */
88
98
  readonly commit?: string;
89
99
  /** The predictions of the run. Each record names the first prediction with its strategy, its set and its input. */
@@ -201,6 +211,8 @@ const givesRoutes = (step: ScreenStep): step is "router" | "highway" => step ===
201
211
  /**
202
212
  * This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
203
213
  * A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
214
+ * It also gets the measures of MEAS3A: R12, R14, B5, B7, B8, B9, M1 and M4. The lane measures open each shared segment at `MEASURE_LANE_WIDTH`.
215
+ * A result with zoom levels also gets the link agreement (Z9), for a router and for a grouping rule.
204
216
  */
205
217
  function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () => readonly Route[]): Record<string, number> {
206
218
  const values: Record<string, number> = { ...row.values };
@@ -217,6 +229,27 @@ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () =>
217
229
  values["B3.median"] = st.median;
218
230
  values["B3.q95"] = st.q95;
219
231
  values["B6.max"] = mergeAngle(row.result.routes);
232
+ const routes = row.result.routes;
233
+ const open = openLanes(row.result);
234
+ values["R12.share"] = routeSeparation(routes);
235
+ values["R14.share"] = orthogonality(routes);
236
+ values["B5.crossings"] = laneCrossings(open.graph);
237
+ values["B7.ratio"] = occupiedArea(routes, boardUnit(graph));
238
+ const runs = runProfile(open.graph);
239
+ values["B8.runs"] = runs.runs;
240
+ values["B8.median"] = runs.median;
241
+ values["B8.max"] = runs.max;
242
+ const lf = laneFidelity(routes, open);
243
+ if (lf) {
244
+ values["B9.median"] = lf.median;
245
+ values["B9.max"] = lf.max;
246
+ }
247
+ values["M1.area"] = laneArea(graph, open);
248
+ values["M4.laneThrough"] = lanesThrough(graph, open);
249
+ }
250
+ if ((givesRoutes(step) || step === "group") && row.result?.levels) {
251
+ const la = linkAgreement(graph, row.result.levels, row.result.routes, boardUnit(graph));
252
+ if (la !== undefined) values["Z9.mean"] = la;
220
253
  }
221
254
  return values;
222
255
  }
@@ -295,6 +328,8 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
295
328
  }
296
329
  const shuffles = givesRoutes(step) ? Math.max(0, Math.floor(options.shuffles ?? 0)) : 0;
297
330
  const seed = options.seed ?? 1;
331
+ const edits = step === "highway" ? Math.max(0, Math.floor(options.edits ?? 0)) : 0;
332
+ const plans = new Map(listStrategies(screened.registry, "highway").map((s) => [s.name, s]));
298
333
  const log = options.log ?? ((): void => {});
299
334
  const families = options.families;
300
335
  const inputs = (options.inputs ?? screenInputs(set)).filter((i) => families?.includes(i.family) ?? true);
@@ -341,11 +376,18 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
341
376
  }
342
377
  const { hard, deterministic } = rowRules(step, graph, row, again?.rows[i]);
343
378
  const values = rowValues(step, graph, row, cornersBase);
379
+ // Z2 is the count of the hard rule `link-ends`, as a measure. The steps `router`, `highway` and `group` check the rule.
380
+ if (hard["link-ends"] !== undefined) values["Z2.linkEnds"] = hard["link-ends"];
344
381
  if (shuffles > 0 && row.result) {
345
382
  const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
346
383
  const f = orderFragility(graph, (g) => router.route(g), { orders: shuffles, seed, base: row.result });
347
384
  Object.assign(values, { "W3.crossings": f.crossings, "W3.detour": f.detour, "W3.inkSaved": f.inkSaved, "W3.ink": f.ink, "W3.changed": f.changed, "W3.poa": f.poa });
348
385
  }
386
+ const plan = edits > 0 && row.result ? plans.get(row.strategy) : undefined;
387
+ if (plan) {
388
+ const z = highwayEdits(graph, plan, own[row.strategy], edits, seed);
389
+ Object.assign(values, { "Z10.edits.mean": z.mean, "Z10.edits.moved": z.moved, "Z10.churn": z.churn, "Z10.ids": z.ids });
390
+ }
349
391
  const record = { ...head, measures: named(values), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail };
350
392
  records.push(record);
351
393
  if (givesRoutes(step) && input.base !== undefined && row.result) variants.push({ record, base: input.base, result: row.result });
@@ -0,0 +1,56 @@
1
+ import { dist, type Box, type Point } from "./geom.ts";
2
+ import type { Obstacles } from "./visibility.ts";
3
+
4
+ /** The largest distance that `channelWidth` looks on each side of a link. A wider channel counts as this width on that side. */
5
+ export const CHANNEL_REACH = 64;
6
+
7
+ /**
8
+ * This function gives the distance from `p` along the unit direction `d` to the box, or `Infinity` when the ray does not meet the box.
9
+ * A point inside the box gives 0. The function uses the slab test.
10
+ */
11
+ export function rayToBox(p: Point, d: Point, box: Box): number {
12
+ let lo = 0;
13
+ let hi = Infinity;
14
+ for (const [o, v, min, max] of [
15
+ [p.x, d.x, box.x, box.x + box.w],
16
+ [p.y, d.y, box.y, box.y + box.h],
17
+ ] as const) {
18
+ if (Math.abs(v) < 1e-12) {
19
+ if (o < min || o > max) return Infinity;
20
+ continue;
21
+ }
22
+ const t1 = (min - o) / v;
23
+ const t2 = (max - o) / v;
24
+ lo = Math.max(lo, Math.min(t1, t2));
25
+ hi = Math.min(hi, Math.max(t1, t2));
26
+ if (lo > hi) return Infinity;
27
+ }
28
+ return lo;
29
+ }
30
+
31
+ /** This function gives the free distance from `p` along the unit direction `d` to the nearest box, up to `reach`. */
32
+ function freeRay(obstacles: Obstacles, p: Point, d: Point, reach: number): number {
33
+ const q = { x: p.x + d.x * reach, y: p.y + d.y * reach };
34
+ let best = reach;
35
+ obstacles.query({ x: Math.min(p.x, q.x), y: Math.min(p.y, q.y), w: Math.abs(q.x - p.x), h: Math.abs(q.y - p.y) }, (n) => {
36
+ best = Math.min(best, rayToBox(p, d, n.box));
37
+ });
38
+ return best;
39
+ }
40
+
41
+ /**
42
+ * This function gives the width of the channel of the link from `a` to `b`. The width is the free space between the nearest boxes on the two sides.
43
+ * It measures the width at five points of the link, and gives the smallest. Each side counts up to `reach`.
44
+ * A link of length 0 gives the free space across the x axis at its point.
45
+ */
46
+ export function channelWidth(obstacles: Obstacles, a: Point, b: Point, reach: number = CHANNEL_REACH): number {
47
+ const len = dist(a, b);
48
+ const n = len > 1e-9 ? { x: -(b.y - a.y) / len, y: (b.x - a.x) / len } : { x: 0, y: 1 };
49
+ const back = { x: -n.x, y: -n.y };
50
+ let width = Infinity;
51
+ for (const u of [0.1, 0.3, 0.5, 0.7, 0.9]) {
52
+ const p = { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u };
53
+ width = Math.min(width, freeRay(obstacles, p, n, reach) + freeRay(obstacles, p, back, reach));
54
+ }
55
+ return width;
56
+ }
@@ -0,0 +1,126 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+ import type { Graph, Result, Route } from "../model.ts";
3
+ import { byCollection } from "../routers/trunks.ts";
4
+ import { CORRIDOR_DEFAULTS, corridorPlan } from "./corridors/index.ts";
5
+ import { follow } from "./follow.ts";
6
+ import type { Highway, HighwayInput, HighwayMember, HighwayPlan } from "./model.ts";
7
+ import { sharedWaysPlan } from "./shared-ways/index.ts";
8
+
9
+ /**
10
+ * The plan that a post step changes. `none` is the empty plan: `follow` then routes each edge as the router `corners` does.
11
+ * `shared-ways` and `corridors` are the plans of those highway strategies, with their defaults.
12
+ */
13
+ export type HighwayBase = "none" | "shared-ways" | "corridors";
14
+
15
+ /** The values of `HighwayBase`. */
16
+ export const HIGHWAY_BASES: readonly HighwayBase[] = Object.freeze(["none", "shared-ways", "corridors"]);
17
+
18
+ /** This function gives the plan of a base. */
19
+ export function basePlan(input: HighwayInput, base: HighwayBase): HighwayPlan {
20
+ if (base === "shared-ways") return sharedWaysPlan(input, { rule: byCollection });
21
+ if (base === "corridors") return corridorPlan(input, CORRIDOR_DEFAULTS);
22
+ return { highways: [] };
23
+ }
24
+
25
+ /** This function gives the routes of a plan after `follow`, with the default way finder. The plan `none` gives the first routes of the edges. */
26
+ export function planRoutes(graph: Graph, plan: HighwayPlan): Result {
27
+ return follow(graph, plan);
28
+ }
29
+
30
+ /** This function gives the first route of each edge: the route of `input.routes`, or the route of the empty plan after `follow`. */
31
+ export function firstRoutes(input: HighwayInput): Map<string, readonly Point[]> {
32
+ const given = routeIndex(input.routes?.routes ?? []);
33
+ const known = new Set(input.graph.edges.map((e) => e.id));
34
+ if (input.graph.edges.every((e) => given.has(e.id))) return given;
35
+ const made = routeIndex(planRoutes(input.graph, { highways: [] }).routes);
36
+ for (const [edge, points] of given) if (known.has(edge)) made.set(edge, points);
37
+ return made;
38
+ }
39
+
40
+ /** One stop of the trip of an edge: the index of the highway in the plan, and the member. */
41
+ export interface TripStop {
42
+ readonly h: number;
43
+ readonly m: HighwayMember;
44
+ }
45
+
46
+ /** This function gives the trip of each edge of a plan. The trip is its highways in the order of `seq`, then in the order of the plan. */
47
+ export function tripsOf(plan: HighwayPlan): Map<string, TripStop[]> {
48
+ const trips = new Map<string, TripStop[]>();
49
+ plan.highways.forEach((h, hi) => {
50
+ for (const m of h.members) {
51
+ const list = trips.get(m.edge);
52
+ if (list) list.push({ h: hi, m });
53
+ else trips.set(m.edge, [{ h: hi, m }]);
54
+ }
55
+ });
56
+ for (const list of trips.values()) list.sort((a, b) => a.m.seq - b.m.seq || a.h - b.h);
57
+ return trips;
58
+ }
59
+
60
+ /**
61
+ * This function gives a plan with the same trips, and with `seq` from 0 to the number of highways of the edge less 1.
62
+ * A post step can give `seq` values with gaps or below 0, and this function makes them whole again.
63
+ * It also drops each highway with fewer than two members, and gives the highways new ids in order.
64
+ */
65
+ export function tidyPlan(plan: HighwayPlan): HighwayPlan {
66
+ const kept = plan.highways.filter((h) => h.members.length >= 2);
67
+ const seq = new Map<string, number>();
68
+ for (const list of tripsOf({ highways: kept }).values()) list.forEach((stop, i) => seq.set(`${stop.h}\u0000${stop.m.edge}`, i));
69
+ const highways: Highway[] = kept.map((h, hi) => ({
70
+ ...h,
71
+ id: `h${hi}`,
72
+ lanes: h.lanes.filter((e) => h.members.some((m) => m.edge === e)),
73
+ members: h.members.map((m) => ({ ...m, seq: seq.get(`${hi}\u0000${m.edge}`) ?? 0 })),
74
+ }));
75
+ return { highways };
76
+ }
77
+
78
+ /** This function gives the place along a polyline (as a length from its first point) of the point of the polyline nearest to `p`. */
79
+ export function arcPlace(line: readonly Point[], p: Point): number {
80
+ let best = Infinity;
81
+ let at = 0;
82
+ let walked = 0;
83
+ for (let i = 1; i < line.length; i++) {
84
+ const a = line[i - 1]!;
85
+ const b = line[i]!;
86
+ const len = dist(a, b);
87
+ const u = len > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / (len * len))) : 0;
88
+ const d = dist(p, { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
89
+ if (d < best) [best, at] = [d, walked + u * len];
90
+ walked += len;
91
+ }
92
+ return at;
93
+ }
94
+
95
+ /** This function gives the place on a spine (the index of a point plus the share of the next leg) of the point of the spine nearest to `p`. */
96
+ export function nearestPlace(spine: readonly Point[], p: Point): number {
97
+ let best = Infinity;
98
+ let at = 0;
99
+ for (let i = 1; i < spine.length; i++) {
100
+ const a = spine[i - 1]!;
101
+ const b = spine[i]!;
102
+ const len2 = (b.x - a.x) ** 2 + (b.y - a.y) ** 2;
103
+ const u = len2 > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / len2)) : 0;
104
+ const d = dist(p, { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u });
105
+ if (d < best) [best, at] = [d, i - 1 + u];
106
+ }
107
+ return at;
108
+ }
109
+
110
+ /**
111
+ * This function gives a plan where the `seq` of each member follows the place of its merge point along the route of its edge.
112
+ * A post step that adds highways to the trips of edges uses it. Equal places keep the order of `seq`.
113
+ */
114
+ export function orderAlongRoutes(plan: HighwayPlan, routes: ReadonlyMap<string, readonly Point[]>): HighwayPlan {
115
+ const rank = new Map<string, number>();
116
+ for (const [edge, list] of tripsOf(plan)) {
117
+ const route = routes.get(edge);
118
+ const placed = list.map((stop, i) => ({ stop, i, at: route ? arcPlace(route, stop.m.merge) : i }));
119
+ placed.sort((a, b) => a.at - b.at || a.i - b.i);
120
+ placed.forEach((p, k) => rank.set(`${p.stop.h}\u0000${edge}`, k));
121
+ }
122
+ return { highways: plan.highways.map((h, hi) => ({ ...h, members: h.members.map((m) => ({ ...m, seq: rank.get(`${hi}\u0000${m.edge}`) ?? m.seq })) })) };
123
+ }
124
+
125
+ /** This function gives the route of each edge by its id. */
126
+ export const routeIndex = (routes: readonly Route[]): Map<string, readonly Point[]> => new Map(routes.map((r) => [r.edge, r.points]));
@@ -0,0 +1,103 @@
1
+ import type { Point } from "../../geom.ts";
2
+ import { WayFinder, type WayOptions } from "../../ways.ts";
3
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
4
+ import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
5
+
6
+ /** The options of the highway strategy `diffusion` (HWY-020 of the note on highways). */
7
+ export interface DiffusionOptions extends Pick<WayOptions, "margin" | "k"> {
8
+ /** The share of the density that each step of the diffusion takes from the links next to a link. The default is 0.25. */
9
+ readonly a: number;
10
+ /** The number of steps of the diffusion. The default is 3. */
11
+ readonly steps: number;
12
+ /** The weight of the density in the cost of a link. The default is 1. */
13
+ readonly lambda: number;
14
+ /** The power of the discount of a dense link. The default is 1. */
15
+ readonly gamma: number;
16
+ /** A highway has this number of edges or more. The default is 2. */
17
+ readonly minSize: number;
18
+ }
19
+
20
+ /** The default options of the strategy `diffusion`. */
21
+ export const DIFFUSION_DEFAULTS: Readonly<Required<Omit<DiffusionOptions, "margin" | "k">>> = Object.freeze({ a: 0.25, steps: 3, lambda: 1, gamma: 1, minSize: 2 });
22
+
23
+ /**
24
+ * This function gives the density of the edges on the links of the corner graph after the diffusion, as a share of the largest density.
25
+ * The density of a link starts as the number of first ways on it. Each step mixes it with the mean density of the links that share a corner with it:
26
+ * `rho_{k+1}(l) = (1 - a) rho_k(l) + a mean(rho_k(l'))`. The key of a link is `i * M + j` for the corners `i < j` of a graph of `M` corners.
27
+ */
28
+ export function linkDensity(neighbours: (i: number) => readonly number[], corners: number, use: ReadonlyMap<number, number>, a: number, steps: number): Map<number, number> {
29
+ const M = corners;
30
+ const links: number[] = [];
31
+ for (let i = 0; i < M; i++) for (const j of neighbours(i)) if (j > i) links.push(i * M + j);
32
+ let rho = new Map<number, number>(links.map((l) => [l, use.get(l) ?? 0]));
33
+ const share = Math.max(0, Math.min(1, a));
34
+ for (let s = 0; s < Math.round(steps); s++) {
35
+ // The sum of the densities and the number of the links at each corner.
36
+ const sum = new Float64Array(M);
37
+ const deg = new Int32Array(M);
38
+ for (const [l, v] of rho) {
39
+ const i = Math.floor(l / M);
40
+ const j = l % M;
41
+ sum[i] = sum[i]! + v;
42
+ sum[j] = sum[j]! + v;
43
+ deg[i] = deg[i]! + 1;
44
+ deg[j] = deg[j]! + 1;
45
+ }
46
+ const next = new Map<number, number>();
47
+ for (const [l, v] of rho) {
48
+ const i = Math.floor(l / M);
49
+ const j = l % M;
50
+ const n = deg[i]! + deg[j]! - 2;
51
+ const mean = n > 0 ? (sum[i]! + sum[j]! - 2 * v) / n : v;
52
+ next.set(l, (1 - share) * v + share * mean);
53
+ }
54
+ rho = next;
55
+ }
56
+ let max = 0;
57
+ for (const v of rho.values()) max = Math.max(max, v);
58
+ if (max > 0) for (const [l, v] of rho) rho.set(l, v / max);
59
+ return rho;
60
+ }
61
+
62
+ /**
63
+ * This function gives the highways of the route-aware bundling on the corner graph (HWY-020).
64
+ * The first ways of the edges give the density of each link. A diffusion along the graph smooths it.
65
+ * Then each edge takes a shortest way where a dense link costs less: `cost(l) = len(l) / (1 + lambda rho(l))^gamma`, with `rho` from 0 to 1.
66
+ * Each stretch that `minSize` ways or more share after the second pass is one highway. The highways stay on the links of the corner graph.
67
+ */
68
+ export function diffusionPlan(input: HighwayInput, o: Readonly<DiffusionOptions>): HighwayPlan {
69
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
70
+ const corners = finder.corners;
71
+ const id = new Map<Point, number>();
72
+ corners.points.forEach((p, i) => id.set(p, i));
73
+ const M = corners.points.length;
74
+ const link = (i: number, j: number): number => (i < j ? i * M + j : j * M + i);
75
+ const plugs = edgePlugs(input.graph);
76
+ const ends = plugs.map(({ ps, pt }) => [finder.out(ps), finder.out(pt)] as const);
77
+ // The first pass: the number of first ways on each link.
78
+ const use = new Map<number, number>();
79
+ for (const [a, b] of ends) {
80
+ const way = corners.path(a, b);
81
+ if (!way) continue;
82
+ const own = new Set<number>();
83
+ for (let i = 1; i < way.length; i++) {
84
+ const p = id.get(way[i - 1]!);
85
+ const q = id.get(way[i]!);
86
+ if (p !== undefined && q !== undefined) own.add(link(p, q));
87
+ }
88
+ for (const l of own) use.set(l, (use.get(l) ?? 0) + 1);
89
+ }
90
+ const rho = linkDensity((i) => corners.neighbours(i), M, use, o.a, o.steps);
91
+ const lambda = Math.max(0, o.lambda);
92
+ const gamma = Math.max(0, o.gamma);
93
+ const floor = 1 / Math.pow(1 + lambda, gamma);
94
+ const cost = (i: number, j: number, d: number): number => d / Math.pow(1 + lambda * (rho.get(link(i, j)) ?? 0), gamma);
95
+ // The second pass: the dense links cost less, so the ways come together on them.
96
+ const ways: KeyedWay[] = [];
97
+ plugs.forEach(({ edge }, n) => {
98
+ const [a, b] = ends[n]!;
99
+ const way = corners.path(a, b, cost, floor);
100
+ if (way) ways.push({ edge: edge.id, key: "diffusion", points: way });
101
+ });
102
+ return sharedStretches(ways, { minSize: o.minSize, pull: (p) => corners.pull(p) });
103
+ }