@mark1russell7/highways 0.4.0 → 0.4.2

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 (52) hide show
  1. package/README.md +67 -1
  2. package/build/index.d.ts +954 -2
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +5914 -1419
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/measures/bundles.ts +101 -2
  8. package/src/bench/measures/highways.ts +93 -0
  9. package/src/bench/measures/index.ts +2 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/measures.ts +17 -0
  13. package/src/bench/screen/screen.ts +87 -9
  14. package/src/highways/base.ts +126 -0
  15. package/src/highways/flow-field/index.ts +49 -0
  16. package/src/highways/gates/ambiguity/index.ts +110 -0
  17. package/src/highways/gates/breakeven/index.ts +62 -0
  18. package/src/highways/gates/hysteresis/index.ts +48 -0
  19. package/src/highways/gates/ids/index.ts +59 -0
  20. package/src/highways/gates/index.ts +6 -0
  21. package/src/highways/gates/model.ts +129 -0
  22. package/src/highways/gates/stretch/index.ts +39 -0
  23. package/src/highways/grouped.ts +123 -0
  24. package/src/highways/index.ts +15 -0
  25. package/src/highways/level-links/index.ts +114 -0
  26. package/src/highways/meet/index.ts +84 -0
  27. package/src/highways/model.ts +5 -0
  28. package/src/highways/od-cells/index.ts +50 -0
  29. package/src/highways/quickbundles/index.ts +117 -0
  30. package/src/highways/road-classes/index.ts +74 -0
  31. package/src/highways/router.ts +1 -1
  32. package/src/highways/select/index.ts +162 -0
  33. package/src/highways/stretches.ts +141 -0
  34. package/src/highways/stubs/index.ts +117 -0
  35. package/src/highways/weiszfeld/index.ts +105 -0
  36. package/src/highways/wspd/index.ts +130 -0
  37. package/src/highways/zipper/index.ts +168 -0
  38. package/src/index.ts +2 -0
  39. package/src/order/index.ts +65 -0
  40. package/src/plugs/justify.ts +48 -0
  41. package/src/plugs/side.ts +138 -0
  42. package/src/repair/index.ts +377 -0
  43. package/src/repair/walk.ts +220 -0
  44. package/src/room/lanes.ts +109 -10
  45. package/src/routers/corners.ts +73 -6
  46. package/src/routers/fan.ts +145 -0
  47. package/src/routers/index.ts +2 -2
  48. package/src/routers/shared.ts +107 -8
  49. package/src/routers/snell.ts +140 -0
  50. package/src/routers/trunks.ts +43 -2
  51. package/src/strategies/builtins.ts +149 -7
  52. package/src/strategies/schemas.ts +173 -2
@@ -1,8 +1,13 @@
1
1
  import { highwayRouterStrategy } from "../../highways/router.ts";
2
- import type { Graph, Result } from "../../model.ts";
2
+ import type { Graph, Result, Route } from "../../model.ts";
3
+ import { stableOrderStrategy } from "../../order/index.ts";
4
+ import { repairStrategy } from "../../repair/index.ts";
3
5
  import { STRATEGIES } from "../../strategies/builtins.ts";
4
- import { createRegistry, getStrategy, listStrategies, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
6
+ import { asRouter, createRegistry, getStrategy, listStrategies, register, STEPS, type AnyStrategy, type Registry, type Strategy } from "../../strategies/model.ts";
5
7
  import { compareStep, type CompareRow, type CompareStep, type StepComparison } from "../compare.ts";
8
+ import { bundledShare, bundleStretch, mergeAngle } from "../measures/bundles.ts";
9
+ import { highwayEdits } from "../measures/highways.ts";
10
+ import { orderFragility } from "../measures/order.ts";
6
11
  import { paretoFront, type Objective } from "../measures/pareto.ts";
7
12
  import { routeStability } from "../measures/stability.ts";
8
13
  import { quantile } from "../measures/stats.ts";
@@ -26,6 +31,15 @@ export const SCREEN_STEPS: readonly ScreenStep[] = ["group", "router", "highway"
26
31
  /** The routers that the screen of the step `highway` also runs by default. They are the baselines of the note on highways. */
27
32
  export const HIGHWAY_BASE_ROUTERS: readonly string[] = ["corners", "shared", "trunks"];
28
33
 
34
+ /**
35
+ * A wrapper of each router of a screen. `stable` routes the edges in the stable order of WILD-053 (`stableOrderStrategy`).
36
+ * `repair` runs the repair step of RULE-046 after the router (`repairStrategy`).
37
+ */
38
+ export type ScreenWrap = "stable" | "repair";
39
+
40
+ /** The wrappers of `ScreenWrap`. */
41
+ export const SCREEN_WRAPS: readonly ScreenWrap[] = ["stable", "repair"];
42
+
29
43
  /** The hard rules of the routes of a router, or of a plan after `follow`. */
30
44
  const ROUTE_RULES: readonly string[] = ["complete", "ends-at-plugs", "fixed-plugs", "no-pass-through", "counts-sum", "link-ends", "bundle-trunk", "run-shared", "same-twice"];
31
45
 
@@ -59,8 +73,24 @@ export interface ScreenOptions {
59
73
  * The default is `HIGHWAY_BASE_ROUTERS`. The other steps do not read it.
60
74
  */
61
75
  readonly baseRouters?: readonly string[];
62
- /** The seed of the small change of the stability measures. The default is 1. */
76
+ /** The seed of the small change of the stability measures, and of the first random order of `shuffles`. The default is 1. */
63
77
  readonly seed?: number;
78
+ /**
79
+ * The wrappers of each router of the steps `router` and `highway`. Each wrapper `w` adds the router `<name>+<w>` as a new strategy, beside the router `<name>`.
80
+ * The default is none. The steps `group` and `curve` do not read it.
81
+ */
82
+ readonly wrap?: readonly ScreenWrap[];
83
+ /**
84
+ * The number of random orders of the edges for the order fragility (W3, from WILD-056). Each router routes each input this number of times more.
85
+ * The default is 0: no measure of the order fragility. The steps `group` and `curve` do not read it.
86
+ */
87
+ readonly shuffles?: number;
88
+ /**
89
+ * 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.
90
+ * The records then get `Z10.edits.*`, the churn of the highways and the share of the highways that keep their id.
91
+ * The default is 0: no edits. The other steps and the base routers do not read it.
92
+ */
93
+ readonly edits?: number;
64
94
  /** The commit of the code, for the records. */
65
95
  readonly commit?: string;
66
96
  /** The predictions of the run. Each record names the first prediction with its strategy, its set and its input. */
@@ -175,8 +205,11 @@ function screenRegistry(step: ScreenStep, strategies: readonly (Strategy<ScreenS
175
205
  /** This function tells if a step of the screen gives routes. The step `router` gives routes, and the step `highway` gives routes after `follow`. */
176
206
  const givesRoutes = (step: ScreenStep): step is "router" | "highway" => step === "router" || step === "highway";
177
207
 
178
- /** This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router. */
179
- function rowValues(step: ScreenStep, graph: Graph, row: CompareRow): Record<string, number> {
208
+ /**
209
+ * This function gives the values of a row of `compareStep`, with the measures of `measureQuality` for a router.
210
+ * A router also gets the bundled share (B2), the merge angle (B6) and the bundle stretch (B3) against the routes `base` of `corners`.
211
+ */
212
+ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow, base: () => readonly Route[]): Record<string, number> {
180
213
  const values: Record<string, number> = { ...row.values };
181
214
  delete values["ms"];
182
215
  if (givesRoutes(step) && row.result) {
@@ -186,6 +219,11 @@ function rowValues(step: ScreenStep, graph: Graph, row: CompareRow): Record<stri
186
219
  values["R8.tight"] = q.tight;
187
220
  values["R10.plugAngle"] = q.plugAngle;
188
221
  values["R15.inflections"] = q.inflections;
222
+ values["B2.share"] = bundledShare(row.result);
223
+ const st = bundleStretch(row.result.routes, base());
224
+ values["B3.median"] = st.median;
225
+ values["B3.q95"] = st.q95;
226
+ values["B6.max"] = mergeAngle(row.result.routes);
189
227
  }
190
228
  return values;
191
229
  }
@@ -208,6 +246,20 @@ function rowRules(step: ScreenStep, graph: Graph, row: CompareRow, again: Compar
208
246
  return { hard: { ...countRules(rules, []), "same-twice": deterministic ? 0 : 1 }, deterministic };
209
247
  }
210
248
 
249
+ /** This function adds the router `<name>+<w>` for each router of the registry and each wrapper `w`. It gives the new registry and the new names. */
250
+ function wrapRouters(registry: Registry, wraps: readonly ScreenWrap[]): { registry: Registry; added: string[] } {
251
+ const routers = listStrategies(registry, "router");
252
+ const added: AnyStrategy[] = [];
253
+ for (const w of wraps) for (const s of routers) added.push((w === "stable" ? stableOrderStrategy(s) : repairStrategy(s)) as AnyStrategy);
254
+ return { registry: added.length === 0 ? registry : register(registry, ...added), added: added.map((s) => s.name) };
255
+ }
256
+
257
+ /** This function gives the name of a router without the suffix of a wrapper, for example `shared` for `shared+repair`. */
258
+ export function unwrappedName(name: string): string {
259
+ for (const w of SCREEN_WRAPS) if (name.endsWith(`+${w}`)) return name.slice(0, -(w.length + 1));
260
+ return name;
261
+ }
262
+
211
263
  /** This function keeps the finite values and gives them the names of a record. */
212
264
  function named(values: Readonly<Record<string, number>>): Record<string, number> {
213
265
  const out: Record<string, number> = {};
@@ -232,15 +284,26 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
232
284
  const candidates = screened.candidates;
233
285
  // The step `highway` runs as the step `router`. Each highway strategy with `follow` is a router, after the base routers.
234
286
  const base = step === "highway" ? (options.baseRouters ?? HIGHWAY_BASE_ROUTERS).map((name) => getStrategy(STRATEGIES, "router", name)) : [];
235
- const registry =
287
+ const unwrapped =
236
288
  step === "highway"
237
289
  ? createRegistry([...base, ...listStrategies(screened.registry, "highway").map((s) => highwayRouterStrategy(s))] as readonly AnyStrategy[])
238
290
  : screened.registry;
291
+ const wrapped = givesRoutes(step) ? wrapRouters(unwrapped, options.wrap ?? []) : { registry: unwrapped, added: [] };
292
+ const registry = wrapped.registry;
293
+ for (const name of wrapped.added) candidates.add(name);
239
294
  const compareAs: CompareStep = step === "highway" ? "router" : step;
240
295
  const baseNames = new Set(base.map((s) => s.name));
241
- const stepOf = (name: string): ScreenStep => (baseNames.has(name) ? "router" : step);
242
- const own = options.options ?? {};
296
+ const stepOf = (name: string): ScreenStep => (baseNames.has(unwrappedName(name)) ? "router" : step);
297
+ // A wrapped router gets the options of its router, unless the call gives its own.
298
+ const own: Record<string, object> = { ...options.options };
299
+ for (const name of wrapped.added) {
300
+ const from = options.options?.[unwrappedName(name)];
301
+ if (own[name] === undefined && from !== undefined) own[name] = from;
302
+ }
303
+ const shuffles = givesRoutes(step) ? Math.max(0, Math.floor(options.shuffles ?? 0)) : 0;
243
304
  const seed = options.seed ?? 1;
305
+ const edits = step === "highway" ? Math.max(0, Math.floor(options.edits ?? 0)) : 0;
306
+ const plans = new Map(listStrategies(screened.registry, "highway").map((s) => [s.name, s]));
244
307
  const log = options.log ?? ((): void => {});
245
308
  const families = options.families;
246
309
  const inputs = (options.inputs ?? screenInputs(set)).filter((i) => families?.includes(i.family) ?? true);
@@ -259,6 +322,10 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
259
322
  comparisons.push(cmp);
260
323
  log(`${input.id}: ${graph.nodes.length} nodes, ${graph.edges.length} edges, ${cmp.rows.map((r) => `${r.strategy} ${r.ms.toFixed(0)}`).join(", ")}`);
261
324
  if (bases.has(input.id)) kept.set(input.id, { graph, results: new Map(cmp.rows.flatMap((r) => (r.result ? [[r.strategy, r.result] as const] : []))) });
325
+ // The routes of `corners` on the input: the base of the bundle stretch (B3). The row of `corners` gives them, or one more run.
326
+ let cornersRoutes: readonly Route[] | undefined;
327
+ const cornersBase = (): readonly Route[] =>
328
+ (cornersRoutes ??= (cmp.rows.find((r) => r.strategy === "corners" && r.result)?.result ?? asRouter(getStrategy(STRATEGIES, "router", "corners")).route(graph)).routes);
262
329
  cmp.rows.forEach((row, i) => {
263
330
  const strategy = byName.get(row.strategy)!;
264
331
  const head = {
@@ -282,7 +349,18 @@ export function screen(step: ScreenStep, strategies: readonly (Strategy<ScreenSt
282
349
  return;
283
350
  }
284
351
  const { hard, deterministic } = rowRules(step, graph, row, again?.rows[i]);
285
- const record = { ...head, measures: named(rowValues(step, graph, row)), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail };
352
+ const values = rowValues(step, graph, row, cornersBase);
353
+ if (shuffles > 0 && row.result) {
354
+ const router = asRouter(strategy as Strategy<"router">, own[row.strategy]);
355
+ const f = orderFragility(graph, (g) => router.route(g), { orders: shuffles, seed, base: row.result });
356
+ 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 });
357
+ }
358
+ const plan = edits > 0 && row.result ? plans.get(row.strategy) : undefined;
359
+ if (plan) {
360
+ const z = highwayEdits(graph, plan, own[row.strategy], edits, seed);
361
+ Object.assign(values, { "Z10.edits.mean": z.mean, "Z10.edits.moved": z.moved, "Z10.churn": z.churn, "Z10.ids": z.ids });
362
+ }
363
+ const record = { ...head, measures: named(values), hard, tolerance: { through: TOLERANCE }, deterministic, ms: row.ms, ...tail };
286
364
  records.push(record);
287
365
  if (givesRoutes(step) && input.base !== undefined && row.result) variants.push({ record, base: input.base, result: row.result });
288
366
  });
@@ -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,49 @@
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 end of the edges that gets a field: the target of a fan-in, or the source of a fan-out. */
7
+ export type FieldEnd = "target" | "source";
8
+
9
+ /** The options of the highway strategy `flow-field` (EDA-046 with HWY-037 of the notes). */
10
+ export interface FlowFieldOptions extends Pick<WayOptions, "margin" | "k"> {
11
+ /** An end with this number of edges or more on one side of its box gets a field. The default is 2. */
12
+ readonly minFanIn: number;
13
+ /** The end that gets a field. The default is `target`. */
14
+ readonly end: FieldEnd;
15
+ }
16
+
17
+ /** The default options of the strategy `flow-field`. */
18
+ export const FLOW_FIELD_DEFAULTS: Readonly<Required<Omit<FlowFieldOptions, "margin" | "k">>> = Object.freeze({ minFanIn: 2, end: "target" });
19
+
20
+ /**
21
+ * This function gives the highways of a flow field for each target (EDA-046, HWY-037).
22
+ * The edges into one side of one box make a fan. The root of the fan is the point out of the median plug of the fan.
23
+ * Each edge of the fan goes the shortest way from the point out of its other plug to the root, through the corner graph.
24
+ * The shortest ways to one root make a tree, so they merge like streams and do not cross. Each stretch that two ways share is one highway.
25
+ * With the end `source`, the same holds for the edges out of one side of one box.
26
+ */
27
+ export function flowFieldPlan(input: HighwayInput, o: Readonly<FlowFieldOptions>): HighwayPlan {
28
+ const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
29
+ const fans = new Map<string, { edge: string; root: Point; far: Point; along: number }[]>();
30
+ for (const { edge, ps, pt } of edgePlugs(input.graph)) {
31
+ const [near, far, node] = o.end === "target" ? [pt, ps, edge.target.node] : [ps, pt, edge.source.node];
32
+ const key = `${node}\u0000${near.normal.x},${near.normal.y}`;
33
+ const item = { edge: edge.id, root: finder.out(near), far: finder.out(far), along: near.normal.x === 0 ? near.point.x : near.point.y };
34
+ const list = fans.get(key);
35
+ if (list) list.push(item);
36
+ else fans.set(key, [item]);
37
+ }
38
+ const ways: KeyedWay[] = [];
39
+ for (const [key, fan] of fans) {
40
+ if (fan.length < Math.max(2, o.minFanIn)) continue;
41
+ const sorted = [...fan].sort((a, b) => a.along - b.along || (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0));
42
+ const root = sorted[Math.floor((sorted.length - 1) / 2)]!.root;
43
+ for (const f of fan) {
44
+ const way = finder.corners.path(f.far, root);
45
+ if (way) ways.push({ edge: f.edge, key, points: o.end === "target" ? way : [...way].reverse() });
46
+ }
47
+ }
48
+ return sharedStretches(ways, { minSize: 2, pull: (p) => finder.corners.pull(p) });
49
+ }
@@ -0,0 +1,110 @@
1
+ import type { Graph } from "../../../model.ts";
2
+ import type { HighwayInput, HighwayPlan } from "../../model.ts";
3
+ import { keepMembers, type Gate } from "../model.ts";
4
+
5
+ /** The options of the gate `ambiguity` (HWY-049). */
6
+ export interface AmbiguityGuardOptions {
7
+ /** Two groups of edges merge only when the merge adds this share of false pairs, or less. The default is 0.5. */
8
+ readonly maxAmbiguity: number;
9
+ /** With `node`, the default, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides. */
10
+ readonly link: "node" | "any";
11
+ }
12
+
13
+ /** The default options of the gate `ambiguity`. */
14
+ export const AMBIGUITY_GUARD_DEFAULTS: Readonly<AmbiguityGuardOptions> = Object.freeze({ maxAmbiguity: 0.5, link: "node" });
15
+
16
+ /** The adjacency of the nodes of a graph: the neighbours of each node, by the edges. */
17
+ function adjacency(graph: Graph): Map<string, Set<string>> {
18
+ const out = new Map<string, Set<string>>();
19
+ const add = (a: string, b: string): void => {
20
+ const set = out.get(a);
21
+ if (set) set.add(b);
22
+ else out.set(a, new Set([b]));
23
+ };
24
+ for (const e of graph.edges) {
25
+ add(e.source.node, e.target.node);
26
+ add(e.target.node, e.source.node);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ /**
32
+ * This function gives the ambiguity of a set of nodes: the share of the pairs of distinct nodes without an edge between them.
33
+ * The nodes are the ends of the edges of one highway. A set with fewer than two nodes gives 0.
34
+ */
35
+ export function nodeAmbiguity(nodes: ReadonlySet<string>, adj: ReadonlyMap<string, ReadonlySet<string>>): number {
36
+ const n = nodes.size;
37
+ if (n < 2) return 0;
38
+ let linked = 0;
39
+ for (const v of nodes) for (const w of adj.get(v) ?? []) if (w !== v && nodes.has(w)) linked++;
40
+ return 1 - linked / 2 / ((n * (n - 1)) / 2);
41
+ }
42
+
43
+ /** A group of the members of one highway: the edges, their end nodes and the ambiguity of the nodes. */
44
+ interface Group {
45
+ readonly edges: string[];
46
+ readonly nodes: Set<string>;
47
+ readonly amb: number;
48
+ /** The first lane of the group, for a fixed order. */
49
+ readonly first: number;
50
+ }
51
+
52
+ /**
53
+ * This function applies the ambiguity guard of HWY-049 to a plan. A highway joins edges that belong together.
54
+ *
55
+ * Two nodes are a false pair when their edges share a highway and no edge joins them. The ambiguity of a group of edges is the share of false pairs among the pairs of its end nodes.
56
+ * In each highway, each edge starts as a group. Two groups merge only when `amb(A ∪ B) - max(amb A, amb B)` is at most `maxAmbiguity`.
57
+ * With the link `node`, they also must share an end node. The merge with the smallest rise comes first.
58
+ *
59
+ * The largest group stays on the highway. The other edges leave it and go alone.
60
+ */
61
+ export function ambiguityPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<AmbiguityGuardOptions>): HighwayPlan {
62
+ const adj = adjacency(input.graph);
63
+ const ends = new Map(input.graph.edges.map((e) => [e.id, [e.source.node, e.target.node] as const]));
64
+ const stay = new Map<string, Set<string>>();
65
+ for (const h of plan.highways) {
66
+ let groups: Group[] = h.lanes.flatMap((edge, i): Group[] => {
67
+ const end = ends.get(edge);
68
+ if (!end) return [];
69
+ const nodes = new Set(end);
70
+ return [{ edges: [edge], nodes, amb: nodeAmbiguity(nodes, adj), first: i }];
71
+ });
72
+ for (;;) {
73
+ let best: { a: number; b: number; rise: number; group: Group } | undefined;
74
+ for (let a = 0; a < groups.length; a++)
75
+ for (let b = a + 1; b < groups.length; b++) {
76
+ const ga = groups[a]!;
77
+ const gb = groups[b]!;
78
+ if (options.link === "node" && ![...gb.nodes].some((v) => ga.nodes.has(v))) continue;
79
+ const nodes = new Set([...ga.nodes, ...gb.nodes]);
80
+ const amb = nodeAmbiguity(nodes, adj);
81
+ const rise = amb - Math.max(ga.amb, gb.amb);
82
+ if (rise > options.maxAmbiguity + 1e-12) continue;
83
+ if (best === undefined || rise < best.rise - 1e-12)
84
+ best = { a, b, rise, group: { edges: [...ga.edges, ...gb.edges], nodes, amb, first: Math.min(ga.first, gb.first) } };
85
+ }
86
+ if (!best) break;
87
+ const merged = best.group;
88
+ const { a, b } = best;
89
+ groups = groups.filter((_, i) => i !== a && i !== b);
90
+ groups.push(merged);
91
+ groups.sort((p, q) => p.first - q.first);
92
+ }
93
+ let top: Group | undefined;
94
+ for (const g of groups) if (top === undefined || g.edges.length > top.edges.length) top = g;
95
+ stay.set(h.id, new Set(top?.edges ?? []));
96
+ }
97
+ return keepMembers(plan, (h, m) => stay.get(h.id)?.has(m.edge) ?? false);
98
+ }
99
+
100
+ /** The gate `ambiguity` (HWY-049): the ambiguity guard. A highway does not join edges whose end nodes have no edges between them. */
101
+ export const ambiguityGate: Gate<AmbiguityGuardOptions> = {
102
+ name: "ambiguity",
103
+ describe: "The gate `ambiguity` (HWY-049) keeps on each highway the largest group of edges that a reader can tell apart. A merge of two groups must add few false pairs of nodes.",
104
+ defaults: AMBIGUITY_GUARD_DEFAULTS,
105
+ schema: {
106
+ maxAmbiguity: { type: "number", min: 0, max: 1, step: 0.05, marks: [0.3, 0.5], default: 0.5, describe: "Two groups merge only when the merge adds this share of false pairs, or less." },
107
+ link: { type: "enum", values: ["node", "any"], default: "node", describe: "With `node`, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides." },
108
+ },
109
+ apply: (input, plan, options) => ambiguityPlan(input, plan, options),
110
+ };
@@ -0,0 +1,62 @@
1
+ import type { WayOptions } from "../../../ways.ts";
2
+ import { lengthBetween, type HighwayInput, type HighwayPlan } from "../../model.ts";
3
+ import { edgeLengths, keepMembers, type Gate } from "../model.ts";
4
+
5
+ /** The options of the gate `breakeven` (HWY-047). */
6
+ export interface BreakEvenOptions {
7
+ /**
8
+ * The exponent of the cost law: a stretch with `n` edges costs `n^shareAlpha` times its length. Each edge pays the share `n^(shareAlpha - 1)`.
9
+ * With 0, a shared stretch is one line, as the ink of B1 counts it. The default is 0.5, as the break-even floor of `corridors`.
10
+ */
11
+ readonly shareAlpha: number;
12
+ }
13
+
14
+ /** The default options of the gate `breakeven`. */
15
+ export const BREAK_EVEN_DEFAULTS: Readonly<BreakEvenOptions> = Object.freeze({ shareAlpha: 0.5 });
16
+
17
+ /**
18
+ * This function applies the break-even law of HWY-047 to a plan. An edge stays on its highways only when sharing saves ink.
19
+ *
20
+ * The edge pays `n^(shareAlpha - 1)` of each shared stretch, with `n` the members of the highway. Thus it saves `(1 - n^(shareAlpha - 1)) L` on a stretch of the length `L`.
21
+ * It adds the length of its route along the plan less the length of its route alone. An edge stays when the saving is as large as the added length.
22
+ * For two parallel edges at the gap `d`, this rule gives the break-even overlap `d √((2 + t) / (2 - t))` with `t = 2^shareAlpha`.
23
+ *
24
+ * An edge that does not save ink leaves each of its highways. Then the other members of those highways save less, so the test repeats until no edge leaves.
25
+ */
26
+ export function breakEvenPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<BreakEvenOptions>, way: WayOptions = {}): HighwayPlan {
27
+ const lengths = edgeLengths(input.graph, plan, way);
28
+ let now = plan;
29
+ for (;;) {
30
+ const saving = new Map<string, number>();
31
+ for (const h of now.highways) {
32
+ const share = 1 - Math.pow(h.members.length, options.shareAlpha - 1);
33
+ for (const m of h.members) saving.set(m.edge, (saving.get(m.edge) ?? 0) + share * lengthBetween(h.spine, Math.min(m.on, m.off), Math.max(m.on, m.off)));
34
+ }
35
+ const out = new Set<string>();
36
+ for (const [edge, saved] of saving) {
37
+ const l = lengths.get(edge);
38
+ if (l && saved < l.route - l.alone) out.add(edge);
39
+ }
40
+ if (out.size === 0) return now;
41
+ now = keepMembers(now, (_, m) => !out.has(m.edge));
42
+ }
43
+ }
44
+
45
+ /** The gate `breakeven` (HWY-047): an edge stays on a highway only when sharing saves ink. */
46
+ export const breakEvenGate: Gate<BreakEvenOptions> = {
47
+ name: "breakeven",
48
+ describe: "The gate `breakeven` (HWY-047) keeps an edge on a highway only when its share of the highway saves more ink than its detour adds.",
49
+ defaults: BREAK_EVEN_DEFAULTS,
50
+ schema: {
51
+ shareAlpha: {
52
+ type: "number",
53
+ min: 0,
54
+ max: 0.9,
55
+ step: 0.1,
56
+ marks: [0, 0.5],
57
+ default: 0.5,
58
+ describe: "The exponent of the cost law. An edge pays the share `n^(shareAlpha - 1)` of a stretch with `n` edges.",
59
+ },
60
+ },
61
+ apply: breakEvenPlan,
62
+ };
@@ -0,0 +1,48 @@
1
+ import type { HighwayInput, HighwayPlan } from "../../model.ts";
2
+ import { commonEdges, matchHighways, type Gate } from "../model.ts";
3
+
4
+ /** The options of the gate `hysteresis` (HWY-046). */
5
+ export interface HysteresisOptions {
6
+ /** A new highway needs this number of edges or more. The default is 3. */
7
+ readonly kOn: number;
8
+ /** A highway of the earlier plan stays while it has this number of edges or more. The default is 2. */
9
+ readonly kOff: number;
10
+ }
11
+
12
+ /** The default options of the gate `hysteresis`. */
13
+ export const HYSTERESIS_DEFAULTS: Readonly<HysteresisOptions> = Object.freeze({ kOn: 3, kOff: 2 });
14
+
15
+ /** Two highways of two plans are the same highway when the Jaccard index of their edges is this value or more. */
16
+ export const SAME_HIGHWAY = 0.5;
17
+
18
+ /**
19
+ * This function applies the count threshold with hysteresis of HWY-046 to a plan.
20
+ * A highway with `kOn` edges or more stays. A highway with fewer than `kOff` edges goes away.
21
+ * A highway between the two limits stays only when it was in the earlier plan, `input.previous`.
22
+ * The highway of the earlier plan with the most common edges is its match, with a Jaccard index of `SAME_HIGHWAY` or more.
23
+ *
24
+ * Thus an edit that adds or removes one edge near the limit does not make a highway come and go. Without an earlier plan, the limit is `kOn`.
25
+ */
26
+ export function hysteresisPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<HysteresisOptions>): HighwayPlan {
27
+ const matched = input.previous ? matchHighways(input.previous.plan, plan, SAME_HIGHWAY, commonEdges(input.previous.graph, input.graph)) : new Map<number, number>();
28
+ const kOff = Math.max(2, Math.min(options.kOff, options.kOn));
29
+ const highways = plan.highways.filter((h, i) => {
30
+ const n = h.members.length;
31
+ if (n >= options.kOn) return true;
32
+ if (n < kOff) return false;
33
+ return matched.has(i);
34
+ });
35
+ return highways.length === plan.highways.length ? plan : { highways };
36
+ }
37
+
38
+ /** The gate `hysteresis` (HWY-046): a highway forms at `kOn` edges and goes away below `kOff` edges. */
39
+ export const hysteresisGate: Gate<HysteresisOptions> = {
40
+ name: "hysteresis",
41
+ describe: "The gate `hysteresis` (HWY-046) keeps a new highway with `kOn` edges or more. A highway of the earlier plan stays while it has `kOff` edges or more.",
42
+ defaults: HYSTERESIS_DEFAULTS,
43
+ schema: {
44
+ kOn: { type: "integer", min: 2, max: 16, step: 1, default: 3, describe: "A new highway needs this number of edges or more." },
45
+ kOff: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A highway of the earlier plan stays while it has this number of edges or more." },
46
+ },
47
+ apply: (input, plan, options) => hysteresisPlan(input, plan, options),
48
+ };
@@ -0,0 +1,59 @@
1
+ import type { Highway, HighwayInput, HighwayPlan } from "../../model.ts";
2
+ import { commonEdges, matchHighways, type Gate } from "../model.ts";
3
+
4
+ /** The options of the gate `ids` (HWY-054). */
5
+ export interface StableIdOptions {
6
+ /** A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more. The default is 0.5. */
7
+ readonly minJaccard: number;
8
+ }
9
+
10
+ /** The default options of the gate `ids`. */
11
+ export const STABLE_ID_DEFAULTS: Readonly<StableIdOptions> = Object.freeze({ minJaccard: 0.5 });
12
+
13
+ /** This function gives the number at the end of an id, or -1 for an id without a number at its end. */
14
+ function numberOf(id: string): number {
15
+ let start = id.length;
16
+ while (start > 0 && id.charCodeAt(start - 1) >= 48 && id.charCodeAt(start - 1) <= 57) start--;
17
+ return start === id.length ? -1 : Number(id.slice(start));
18
+ }
19
+
20
+ /**
21
+ * This function gives stable ids to the highways of a plan (HWY-054). The earlier plan is `input.previous`.
22
+ * Each new highway takes the id of the old highway with the most common edges.
23
+ * The pairs with the largest Jaccard index of their edges come first.
24
+ *
25
+ * A pair needs an index of `minJaccard` or more, and only the edges of both graphs count.
26
+ * A new highway without a match gets a new id `h<n>` that no old highway and no new highway has. Thus the old highway of a new id ends.
27
+ * The gate changes only the ids, so the routes do not change. Without an earlier plan, the plan does not change.
28
+ */
29
+ export function stableIdPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<StableIdOptions>): HighwayPlan {
30
+ if (!input.previous) return plan;
31
+ const before = input.previous.plan;
32
+ const matched = matchHighways(before, plan, options.minJaccard, commonEdges(input.previous.graph, input.graph));
33
+ const taken = new Set<string>([...before.highways.map((h) => h.id)]);
34
+ for (const [, i0] of matched) taken.add(before.highways[i0]!.id);
35
+ let next = 1 + Math.max(-1, ...before.highways.map((h) => numberOf(h.id)), ...plan.highways.map((h) => numberOf(h.id)));
36
+ const fresh = (): string => {
37
+ while (taken.has(`h${next}`)) next++;
38
+ const id = `h${next++}`;
39
+ taken.add(id);
40
+ return id;
41
+ };
42
+ const highways = plan.highways.map((h, i): Highway => {
43
+ const i0 = matched.get(i);
44
+ const id = i0 === undefined ? fresh() : before.highways[i0]!.id;
45
+ return id === h.id ? h : { ...h, id };
46
+ });
47
+ return { highways };
48
+ }
49
+
50
+ /** The gate `ids` (HWY-054): stable highway ids across edits. A highway keeps the id of its match in the earlier plan. */
51
+ export const stableIdGate: Gate<StableIdOptions> = {
52
+ name: "ids",
53
+ describe: "The gate `ids` (HWY-054) gives each highway the id of the highway of the earlier plan with the most common edges. The routes do not change.",
54
+ defaults: STABLE_ID_DEFAULTS,
55
+ schema: {
56
+ minJaccard: { type: "number", min: 0.3, max: 0.8, step: 0.05, default: 0.5, describe: "A new highway takes the id of an old highway when the Jaccard index of their edges is this value or more." },
57
+ },
58
+ apply: (input, plan, options) => stableIdPlan(input, plan, options),
59
+ };
@@ -0,0 +1,6 @@
1
+ export { commonEdges, edgeLengths, gateStrategy, jaccard, keepMembers, matchHighways, type EdgeLengths, type Gate } from "./model.ts";
2
+ export { BREAK_EVEN_DEFAULTS, breakEvenGate, breakEvenPlan, type BreakEvenOptions } from "./breakeven/index.ts";
3
+ export { STRETCH_DEFAULTS, stretchGate, stretchPlan, type StretchOptions } from "./stretch/index.ts";
4
+ export { AMBIGUITY_GUARD_DEFAULTS, ambiguityGate, ambiguityPlan, nodeAmbiguity, type AmbiguityGuardOptions } from "./ambiguity/index.ts";
5
+ export { HYSTERESIS_DEFAULTS, hysteresisGate, hysteresisPlan, SAME_HIGHWAY, type HysteresisOptions } from "./hysteresis/index.ts";
6
+ export { STABLE_ID_DEFAULTS, stableIdGate, stableIdPlan, type StableIdOptions } from "./ids/index.ts";