@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
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.4.1",
12
+ "version": "0.5.0",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
@@ -30,7 +30,9 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@mark1russell7/cue": "github:mark1russell7/cue",
33
+ "@mark1russell7/layout-contract": "github:mark1russell7/layout-contract#69933a61b0270c5915b82ab563d24908ade708af&path:/packages/layout-contract",
33
34
  "@types/node": "^26.6.4",
35
+ "highs": "^1.15.4",
34
36
  "typescript": "^7.0.2",
35
37
  "vitest": "^5.0.3"
36
38
  },
@@ -0,0 +1,197 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import { edgePlugs, sharedStretches, type KeyedWay } from "../../highways/stretches.ts";
3
+ import type { HighwayPlan } from "../../highways/model.ts";
4
+ import type { Graph } from "../../model.ts";
5
+ import { byCollection } from "../../routers/trunks.ts";
6
+ import { WayFinder, type WayOptions } from "../../ways.ts";
7
+
8
+ /** The edges that can share a link: all the edges, or only the edges of one collection, as with the rule `byCollection`. */
9
+ export type ExactRule = "all" | "collection";
10
+
11
+ /** The options of the exact model of fixed-charge network design (HWY-040). */
12
+ export interface ExactOptions extends WayOptions {
13
+ /** The way of each edge is this value times its shortest way or shorter. */
14
+ readonly stretch: number;
15
+ /** The edges that can share a link. */
16
+ readonly rule: ExactRule;
17
+ /** The cost of each unit of length of each way, as a share of the cost of the ink. A small value removes needless loops. The default is 0.001. */
18
+ readonly epsilon?: number;
19
+ }
20
+
21
+ /** A model of fixed-charge network design as the text of a mixed integer program (CPLEX LP format), and the function that reads its solution. */
22
+ export interface ExactModel {
23
+ /** The text of the program in the CPLEX LP format. */
24
+ readonly lp: string;
25
+ /** The number of binary variables. */
26
+ readonly columns: number;
27
+ /** The number of constraints. */
28
+ readonly rows: number;
29
+ /** The number of edges in the model. An edge without a way in the stretch limit is not in the model. */
30
+ readonly edges: number;
31
+ /** This function gives the plan of a solution. `value` gives the value of a variable by its name. */
32
+ decode(value: (name: string) => number): HighwayPlan;
33
+ }
34
+
35
+ interface Arc {
36
+ readonly from: number;
37
+ readonly to: number;
38
+ readonly link: number;
39
+ readonly len: number;
40
+ }
41
+
42
+ /** This function gives the shortest distance from `start` to each node, with Dijkstra on an adjacency list. */
43
+ function distances(adj: readonly (readonly { to: number; len: number }[])[], start: number): Float64Array {
44
+ const d = new Float64Array(adj.length).fill(Infinity);
45
+ d[start] = 0;
46
+ const done = new Uint8Array(adj.length);
47
+ // The boards of the model are small, so a scan for the smallest open node is fast enough.
48
+ for (;;) {
49
+ let u = -1;
50
+ for (let i = 0; i < adj.length; i++) if (!done[i] && d[i]! < Infinity && (u < 0 || d[i]! < d[u]!)) u = i;
51
+ if (u < 0) return d;
52
+ done[u] = 1;
53
+ for (const { to, len } of adj[u]!) if (d[u]! + len < d[to]!) d[to] = d[u]! + len;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * This function gives the model of fixed-charge network design (Magnanti and Wong 1984) for the edges of a graph (HWY-040).
59
+ * The network is the corner graph, plus the point out of each plug. Each link costs its length once for each key that uses it.
60
+ * Each edge goes one way between the points out of its two plugs. The way is `stretch` times its shortest way or shorter.
61
+ * The optimum has the least ink for the stretch limit. The ways of the solution give highways with `sharedStretches`.
62
+ */
63
+ export function exactModel(graph: Graph, o: Readonly<ExactOptions>): ExactModel {
64
+ const finder = new WayFinder(graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
65
+ const corners = finder.corners;
66
+ const k = o.k ?? 8;
67
+ const pad = finder.margin / 2;
68
+ const points: Point[] = [...corners.points];
69
+ const ids = new Map<string, number>();
70
+ const nodeOf = (p: Point): number => {
71
+ const key = `${p.x},${p.y}`;
72
+ let id = ids.get(key);
73
+ if (id === undefined) {
74
+ id = points.length;
75
+ points.push(p);
76
+ ids.set(key, id);
77
+ }
78
+ return id;
79
+ };
80
+ corners.points.forEach((p, i) => ids.set(`${p.x},${p.y}`, i));
81
+ const plugs = edgePlugs(graph);
82
+ const ends = plugs.map(({ edge, ps, pt }) => ({ edge: edge.id, key: o.rule === "all" ? "*" : (byCollection(edge) ?? `\u0000${edge.id}`), a: nodeOf(finder.out(ps)), b: nodeOf(finder.out(pt)) }));
83
+
84
+ // The links: each corner to its k nearest visible corners, as in the corner graph, and each end point to its 2k nearest visible corners.
85
+ const linkIds = new Map<string, number>();
86
+ const links: { u: number; v: number; len: number }[] = [];
87
+ const addLink = (u: number, v: number): void => {
88
+ if (u === v) return;
89
+ const key = u < v ? `${u},${v}` : `${v},${u}`;
90
+ if (linkIds.has(key)) return;
91
+ linkIds.set(key, links.length);
92
+ links.push({ u: Math.min(u, v), v: Math.max(u, v), len: dist(points[u]!, points[v]!) });
93
+ };
94
+ for (let i = 0; i < corners.points.length; i++) for (const j of corners.nearestVisible(corners.points[i]!, k, i)) addLink(i, j);
95
+ for (let t = corners.points.length; t < points.length; t++) for (const j of corners.nearestVisible(points[t]!, 2 * k)) addLink(t, j);
96
+ for (const e of ends) if (finder.obstacles.clear(points[e.a]!, points[e.b]!, pad)) addLink(e.a, e.b);
97
+ const adj: { to: number; len: number }[][] = points.map(() => []);
98
+ for (const l of links) {
99
+ adj[l.u]!.push({ to: l.v, len: l.len });
100
+ adj[l.v]!.push({ to: l.u, len: l.len });
101
+ }
102
+
103
+ // The arcs of each edge: the arcs on some way within the stretch limit.
104
+ const eps = o.epsilon ?? 0.001;
105
+ const objective: string[] = [];
106
+ const rows: string[] = [];
107
+ const binaries: string[] = [];
108
+ const keys = [...new Set(ends.map((e) => e.key))];
109
+ const used = new Map<string, Set<number>>();
110
+ const arcsOf: { e: number; arcs: Arc[] }[] = [];
111
+ ends.forEach((e, ei) => {
112
+ const da = distances(adj, e.a);
113
+ const db = distances(adj, e.b);
114
+ const best = da[e.b]!;
115
+ if (!Number.isFinite(best) || e.a === e.b) return;
116
+ const limit = o.stretch * best + 1e-9;
117
+ const arcs: Arc[] = [];
118
+ links.forEach((l, li) => {
119
+ if (da[l.u]! + l.len + db[l.v]! <= limit) arcs.push({ from: l.u, to: l.v, link: li, len: l.len });
120
+ if (da[l.v]! + l.len + db[l.u]! <= limit) arcs.push({ from: l.v, to: l.u, link: li, len: l.len });
121
+ });
122
+ arcsOf.push({ e: ei, arcs });
123
+ let set = used.get(e.key);
124
+ if (!set) used.set(e.key, (set = new Set()));
125
+ for (const a of arcs) set.add(a.link);
126
+ // Flow: one unit leaves `a` and arrives at `b`.
127
+ const flow = new Map<number, string[]>();
128
+ const term = (n: number, s: string): void => {
129
+ const list = flow.get(n);
130
+ if (list) list.push(s);
131
+ else flow.set(n, [s]);
132
+ };
133
+ const stretch: string[] = [];
134
+ for (const a of arcs) {
135
+ const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
136
+ binaries.push(x);
137
+ objective.push(`${(eps * a.len).toFixed(6)} ${x}`);
138
+ term(a.from, `+ ${x}`);
139
+ term(a.to, `- ${x}`);
140
+ stretch.push(`${a.len.toFixed(6)} ${x}`);
141
+ }
142
+ for (const [n, list] of flow) rows.push(`f${ei}_${n}: ${list.join(" ")} = ${n === e.a ? 1 : n === e.b ? -1 : 0}`);
143
+ rows.push(`s${ei}: ${stretch.join(" + ")} <= ${limit.toFixed(6)}`);
144
+ // An arc needs its link open for the key of the edge.
145
+ const byLink = new Map<number, string[]>();
146
+ for (const a of arcs) {
147
+ const list = byLink.get(a.link);
148
+ const x = `x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`;
149
+ if (list) list.push(x);
150
+ else byLink.set(a.link, [x]);
151
+ }
152
+ const ki = keys.indexOf(e.key);
153
+ for (const [li, xs] of byLink) rows.push(`o${ei}_${li}: ${xs.join(" + ")} - y${ki}_${li} <= 0`);
154
+ });
155
+ keys.forEach((key, ki) => {
156
+ for (const li of used.get(key) ?? []) {
157
+ const y = `y${ki}_${li}`;
158
+ binaries.push(y);
159
+ objective.push(`${links[li]!.len.toFixed(6)} ${y}`);
160
+ }
161
+ });
162
+ const wrap = (terms: readonly string[]): string => {
163
+ const out: string[] = [];
164
+ for (let i = 0; i < terms.length; i += 8) out.push(terms.slice(i, i + 8).join(" + "));
165
+ return out.join("\n + ");
166
+ };
167
+ const lp = [
168
+ "Minimize",
169
+ ` obj: ${objective.length > 0 ? wrap(objective) : "0 dummy"}`,
170
+ "Subject To",
171
+ ...rows.map((r) => ` ${r.replace(/((?:\S+\s+){24})/g, "$1\n ")}`),
172
+ "Binary",
173
+ ...Array.from({ length: Math.ceil(binaries.length / 8) }, (_, i) => ` ${binaries.slice(i * 8, i * 8 + 8).join(" ")}`),
174
+ "End",
175
+ "",
176
+ ].join("\n");
177
+
178
+ const decode = (value: (name: string) => number): HighwayPlan => {
179
+ const ways: KeyedWay[] = [];
180
+ for (const { e: ei, arcs } of arcsOf) {
181
+ const e = ends[ei]!;
182
+ const next = new Map<number, number>();
183
+ for (const a of arcs) if (value(`x${ei}_${a.link}_${a.from < a.to ? 0 : 1}`) > 0.5) next.set(a.from, a.to);
184
+ const seq: Point[] = [points[e.a]!];
185
+ let at = e.a;
186
+ for (let guard = 0; at !== e.b && guard <= points.length; guard++) {
187
+ const to = next.get(at);
188
+ if (to === undefined) break;
189
+ seq.push(points[to]!);
190
+ at = to;
191
+ }
192
+ if (at === e.b) ways.push({ edge: e.edge, key: e.key, points: seq });
193
+ }
194
+ return sharedStretches(ways, { minSize: 2 });
195
+ };
196
+ return { lp, columns: binaries.length, rows: rows.length, edges: arcsOf.length, decode };
197
+ }
@@ -0,0 +1,93 @@
1
+ import { follow } from "../../highways/follow.ts";
2
+ import { commonEdges, matchHighways } from "../../highways/gates/model.ts";
3
+ import type { HighwayPlan } from "../../highways/model.ts";
4
+ import { wayOptionsOf } from "../../highways/router.ts";
5
+ import type { Graph } from "../../model.ts";
6
+ import type { Strategy } from "../../strategies/model.ts";
7
+ import { changeGraph, routeStability } from "./stability.ts";
8
+
9
+ /**
10
+ * This function gives the churn of the highways over one edit (Z10 for highways, from HWY-046). Two edges are a pair when they share a highway.
11
+ * The churn is the share of the pairs that come or go: `|P ∆ P'| / |P ∪ P'|`. Only the edges of `edges` count, for example the edges of both graphs.
12
+ * Two plans without pairs give 0. The scale is 0 to 1, and lower is better.
13
+ */
14
+ export function highwayChurn(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): number {
15
+ const pairs = (plan: HighwayPlan): Set<string> => {
16
+ const out = new Set<string>();
17
+ for (const h of plan.highways) {
18
+ const list = [...new Set(h.members.map((m) => m.edge).filter((e) => edges.has(e)))].sort();
19
+ for (let i = 0; i < list.length; i++) for (let j = i + 1; j < list.length; j++) out.add(`${list[i]}\u0000${list[j]}`);
20
+ }
21
+ return out;
22
+ };
23
+ const a = pairs(before);
24
+ const b = pairs(after);
25
+ let common = 0;
26
+ for (const p of a) if (b.has(p)) common++;
27
+ const all = a.size + b.size - common;
28
+ return all === 0 ? 0 : (all - common) / all;
29
+ }
30
+
31
+ /** The match of the highways over one edit. */
32
+ export interface HighwayIdKeep {
33
+ /** The number of new highways with a match in the old plan: a Jaccard index of their edges of 0.5 or more. */
34
+ readonly matched: number;
35
+ /** The number of matched highways that keep the id of their match. */
36
+ readonly kept: number;
37
+ /** `kept / matched`, or `NaN` without a match. The scale is 0 to 1, and higher is better. */
38
+ readonly share: number;
39
+ }
40
+
41
+ /**
42
+ * This function tells how many highways keep their id over one edit (HWY-054). It matches the new highways with the old ones by `matchHighways`, with the index 0.5.
43
+ * A matched highway keeps its id when its id is the id of its match. Only the edges of `edges` count.
44
+ */
45
+ export function highwayIdKeep(before: HighwayPlan, after: HighwayPlan, edges: ReadonlySet<string>): HighwayIdKeep {
46
+ const match = matchHighways(before, after, 0.5, edges);
47
+ let kept = 0;
48
+ for (const [i, i0] of match) if (after.highways[i]!.id === before.highways[i0]!.id) kept++;
49
+ return { matched: match.size, kept, share: match.size === 0 ? Number.NaN : kept / match.size };
50
+ }
51
+
52
+ /** The stability of a highway strategy over a seeded sequence of edits. Each value is the mean over the edits. */
53
+ export interface HighwayEdits {
54
+ /** The mean Fréchet distance of the routes over one edit, in units of the board (Z10). */
55
+ readonly mean: number;
56
+ /** The share of the routes that move over one edit (Z10). */
57
+ readonly moved: number;
58
+ /** The churn of the highways over one edit (`highwayChurn`). */
59
+ readonly churn: number;
60
+ /** The share of the matched highways that keep their id over one edit (`highwayIdKeep`), or `NaN` without a match. */
61
+ readonly ids: number;
62
+ }
63
+
64
+ /**
65
+ * This function measures a highway strategy over a sequence of `edits` seeded edits. Each edit is the small change of `changeGraph` with the seed `seed + i`.
66
+ * The strategy gets the graph and its plan before the edit as `previous`, so a strategy with memory can keep its highways.
67
+ * `follow` routes each plan with the way options of the strategy. The same graph, strategy, options and seed give the same values.
68
+ */
69
+ export function highwayEdits(graph: Graph, strategy: Strategy<"highway", object>, options: object | undefined, edits: number, seed = 1): HighwayEdits {
70
+ const way = wayOptionsOf({ ...strategy.defaults, ...options });
71
+ let g = graph;
72
+ let plan = strategy.run({ graph: g }, options);
73
+ let routes = follow(g, plan, way);
74
+ const mean: number[] = [];
75
+ const moved: number[] = [];
76
+ const churn: number[] = [];
77
+ const ids: number[] = [];
78
+ for (let i = 0; i < edits; i++) {
79
+ const next = changeGraph(g, seed + i).graph;
80
+ const p = strategy.run({ graph: next, previous: { graph: g, plan } }, options);
81
+ const r = follow(next, p, way);
82
+ const s = routeStability(g, routes, r, { seed });
83
+ const edges = commonEdges(g, next);
84
+ mean.push(s.mean);
85
+ moved.push(s.moved);
86
+ churn.push(highwayChurn(plan, p, edges));
87
+ const keep = highwayIdKeep(plan, p, edges);
88
+ if (keep.matched > 0) ids.push(keep.share);
89
+ [g, plan, routes] = [next, p, r];
90
+ }
91
+ const avg = (v: readonly number[]): number => (v.length === 0 ? Number.NaN : v.reduce((a, b) => a + b, 0) / v.length);
92
+ return { mean: avg(mean), moved: avg(moved), churn: avg(churn), ids: avg(ids) };
93
+ }
@@ -8,3 +8,5 @@ export * from "./stability.ts";
8
8
  export * from "./room.ts";
9
9
  export * from "./pareto.ts";
10
10
  export * from "./order.ts";
11
+ export * from "./highways.ts";
12
+ export * from "./lanes.ts";
@@ -0,0 +1,304 @@
1
+ import { dist, frechet, pointSegmentDistance, segmentsCross, type Point } from "../../geom.ts";
2
+ import { Grid } from "../../grid.ts";
3
+ import type { Graph, Result, Route } from "../../model.ts";
4
+ import { laneRoutes, runKey, segmentsOf, type SegmentGraph } from "../../segments.ts";
5
+ import { measureRouteFaults } from "../links.ts";
6
+ import { segmentKey } from "./routes.ts";
7
+ import { quantile } from "./stats.ts";
8
+
9
+ /**
10
+ * The lane width of the lane measures, in units of the board. The room bench opens the lanes at the same width (`spacing` of `measureBoard`).
11
+ * R12 also uses it as the smallest correct distance between two parallel legs.
12
+ */
13
+ export const MEASURE_LANE_WIDTH = 4;
14
+
15
+ /** The routes of a result with each shared segment open: one lane for each edge. */
16
+ export interface OpenLanes {
17
+ /** The segments of the result. The shared segments are the runs and the trunks of the bundles. */
18
+ readonly graph: SegmentGraph;
19
+ /** The lane route of each edge, from `laneRoutes`. Without a shared segment, the lane routes are the routes. */
20
+ readonly lanes: readonly Route[];
21
+ /** The distance between two lanes. */
22
+ readonly width: number;
23
+ }
24
+
25
+ /** This function opens each shared segment of a result: each edge of a run or of a trunk gets its own lane, `width` apart. */
26
+ export function openLanes(result: Result, width: number = MEASURE_LANE_WIDTH): OpenLanes {
27
+ const graph = segmentsOf(result);
28
+ const open = new Set(graph.segments.filter((s) => s.count > 1).map((s) => runKey(s.edges)));
29
+ return { graph, lanes: laneRoutes(graph, open, width), width };
30
+ }
31
+
32
+ /** One drawn segment and the sorted ids of the edges that draw it. */
33
+ interface Leg {
34
+ readonly a: Point;
35
+ readonly b: Point;
36
+ readonly edges: string;
37
+ }
38
+
39
+ /** This function gives each drawn segment of the routes one time, with the edges that draw it. A segment shorter than the grain of `segmentKey` is not a leg. */
40
+ function legsOf(routes: readonly Route[]): Leg[] {
41
+ const byKey = new Map<string, { a: Point; b: Point; edges: Set<string> }>();
42
+ for (const r of routes) {
43
+ for (let i = 1; i < r.points.length; i++) {
44
+ const a = r.points[i - 1]!;
45
+ const b = r.points[i]!;
46
+ const k = segmentKey(a, b);
47
+ if (k === undefined) continue;
48
+ const leg = byKey.get(k);
49
+ if (leg) leg.edges.add(r.edge);
50
+ else byKey.set(k, { a, b, edges: new Set([r.edge]) });
51
+ }
52
+ }
53
+ return [...byKey.values()].map((l) => ({ a: l.a, b: l.b, edges: [...l.edges].sort().join("\u0000") }));
54
+ }
55
+
56
+ /** This function gives the distance between the segments `ab` and `cd`. It is 0 when they cross or touch. */
57
+ function segmentDistance(a: Point, b: Point, c: Point, d: Point): number {
58
+ if (segmentsCross(a, b, c, d)) return 0;
59
+ return Math.min(pointSegmentDistance(a, c, d), pointSegmentDistance(b, c, d), pointSegmentDistance(c, a, b), pointSegmentDistance(d, a, b));
60
+ }
61
+
62
+ /** This function gives the length of the union of intervals. */
63
+ function unionLength(intervals: [number, number][]): number {
64
+ intervals.sort((p, q) => p[0] - q[0]);
65
+ let sum = 0;
66
+ let lo = Number.NEGATIVE_INFINITY;
67
+ let hi = Number.NEGATIVE_INFINITY;
68
+ for (const [a, b] of intervals) {
69
+ if (a > hi) {
70
+ if (hi > lo) sum += hi - lo;
71
+ lo = a;
72
+ hi = b;
73
+ } else hi = Math.max(hi, b);
74
+ }
75
+ if (hi > lo) sum += hi - lo;
76
+ return sum;
77
+ }
78
+
79
+ /**
80
+ * This function gives the lane separation of the routes (R12 of MEAS0): the share of the ink that runs too near a parallel leg of other edges.
81
+ *
82
+ * A leg is a drawn segment of `S`, and a leg that several routes share counts one time, as in B1. Two legs are near when four things are true.
83
+ * The sets of their edges are different. They are within `maxAngle` degrees of parallel. Their projections overlap. Their distance is less than `near`.
84
+ *
85
+ * `ns = Σ_s length(the part of s that the projections of its near legs cover) / ink(S)`. Each part counts one time, so the scale is 0 to 1.
86
+ * Lower is better. Routes without ink give 0. A grid of buckets finds the near legs, so the cost is `n log n` for an even density.
87
+ */
88
+ export function routeSeparation(routes: readonly Route[], near: number = MEASURE_LANE_WIDTH, maxAngle = 5): number {
89
+ const legs = legsOf(routes);
90
+ let ink = 0;
91
+ for (const l of legs) ink += dist(l.a, l.b);
92
+ if (ink <= 0) return 0;
93
+ const grid = new Grid<number>(Math.max(16, 4 * near));
94
+ legs.forEach((l, i) => grid.addSegment(i, l.a, l.b));
95
+ const sin = Math.sin((maxAngle * Math.PI) / 180);
96
+ let close = 0;
97
+ legs.forEach((s, i) => {
98
+ const length = dist(s.a, s.b);
99
+ const ux = (s.b.x - s.a.x) / length;
100
+ const uy = (s.b.y - s.a.y) / length;
101
+ const cover: [number, number][] = [];
102
+ grid.querySegment(s.a, s.b, near, (j) => {
103
+ const t = legs[j]!;
104
+ if (j === i || t.edges === s.edges) return;
105
+ const tx = t.b.x - t.a.x;
106
+ const ty = t.b.y - t.a.y;
107
+ const tl = Math.hypot(tx, ty);
108
+ if (Math.abs(ux * ty - uy * tx) > sin * tl) return;
109
+ const p = (t.a.x - s.a.x) * ux + (t.a.y - s.a.y) * uy;
110
+ const q = (t.b.x - s.a.x) * ux + (t.b.y - s.a.y) * uy;
111
+ const lo = Math.max(0, Math.min(p, q));
112
+ const hi = Math.min(length, Math.max(p, q));
113
+ if (hi - lo <= 1e-9 || segmentDistance(s.a, s.b, t.a, t.b) >= near) return;
114
+ cover.push([lo, hi]);
115
+ });
116
+ close += unionLength(cover);
117
+ });
118
+ return Math.min(1, close / ink);
119
+ }
120
+
121
+ /**
122
+ * This function gives the orthogonality of the routes (R14 of MEAS0): `orth = Σ L(legs within maxAngle degrees of an axis) / Σ_e L(r_e)`.
123
+ * Each route counts its own legs, also on a shared segment. The scale is 0 to 1, and higher is better for an orthogonal style.
124
+ * Routes without length give 1. The cost is linear.
125
+ */
126
+ export function orthogonality(routes: readonly Route[], maxAngle = 1): number {
127
+ const tan = Math.tan((maxAngle * Math.PI) / 180);
128
+ let total = 0;
129
+ let straight = 0;
130
+ for (const r of routes) {
131
+ for (let i = 1; i < r.points.length; i++) {
132
+ const dx = Math.abs(r.points[i]!.x - r.points[i - 1]!.x);
133
+ const dy = Math.abs(r.points[i]!.y - r.points[i - 1]!.y);
134
+ const l = Math.hypot(dx, dy);
135
+ total += l;
136
+ if (Math.min(dx, dy) <= tan * Math.max(dx, dy)) straight += l;
137
+ }
138
+ }
139
+ return total > 0 ? straight / total : 1;
140
+ }
141
+
142
+ /**
143
+ * This function counts the lane crossings of the segments of a result (B5 of MEAS0): the pairs of edges whose lane order flips.
144
+ * Along the route of each edge `e`, it takes the shared segments in order. On each one, another edge `f` is on the left or on the right of `e`, in the way of `e`.
145
+ *
146
+ * A pair `(e, f)` counts one time when the side of `f` changes from one shared segment of both edges to the next one.
147
+ * Lower is better. A result without shared segments gives 0. The cost is the sum of the squares of the edge counts of the shared segments.
148
+ */
149
+ export function laneCrossings(graph: SegmentGraph): number {
150
+ const shared = new Map(graph.segments.filter((s) => s.count > 1).map((s) => [s.id, s] as const));
151
+ const flipped = new Set<string>();
152
+ for (const path of graph.paths) {
153
+ const side = new Map<string, number>();
154
+ for (const ref of path.segments) {
155
+ const s = shared.get(ref.segment);
156
+ if (!s) continue;
157
+ s.edges.forEach((f, lane) => {
158
+ if (f === path.edge) return;
159
+ const now = Math.sign(lane - ref.lane) * (ref.reverse ? -1 : 1);
160
+ const before = side.get(f);
161
+ if (before !== undefined && before !== now) flipped.add(path.edge < f ? `${path.edge}\u0000${f}` : `${f}\u0000${path.edge}`);
162
+ side.set(f, now);
163
+ });
164
+ }
165
+ }
166
+ return flipped.size;
167
+ }
168
+
169
+ /** This function adds the key of each cell of side `u` that the segment `ab` touches, with an exact walk over the cells (Amanatides and Woo, 1987). */
170
+ function touchCells(a: Point, b: Point, u: number, cells: Set<string>): void {
171
+ let i = Math.floor(a.x / u);
172
+ let j = Math.floor(a.y / u);
173
+ const iEnd = Math.floor(b.x / u);
174
+ const jEnd = Math.floor(b.y / u);
175
+ const dx = b.x - a.x;
176
+ const dy = b.y - a.y;
177
+ const sx = Math.sign(dx);
178
+ const sy = Math.sign(dy);
179
+ const deltaX = dx !== 0 ? u / Math.abs(dx) : Number.POSITIVE_INFINITY;
180
+ const deltaY = dy !== 0 ? u / Math.abs(dy) : Number.POSITIVE_INFINITY;
181
+ let maxX = dx !== 0 ? ((sx > 0 ? (i + 1) * u : i * u) - a.x) / dx : Number.POSITIVE_INFINITY;
182
+ let maxY = dy !== 0 ? ((sy > 0 ? (j + 1) * u : j * u) - a.y) / dy : Number.POSITIVE_INFINITY;
183
+ cells.add(`${i},${j}`);
184
+ while (i !== iEnd || j !== jEnd) {
185
+ // At a corner of four cells, the walk steps on both axes, so it does not touch the two side cells.
186
+ const moveX = j === jEnd || (i !== iEnd && maxX <= maxY);
187
+ const moveY = i === iEnd || (j !== jEnd && maxY <= maxX);
188
+ if (moveX) {
189
+ i += sx;
190
+ maxX += deltaX;
191
+ }
192
+ if (moveY) {
193
+ j += sy;
194
+ maxY += deltaY;
195
+ }
196
+ cells.add(`${i},${j}`);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * This function gives the occupied area of the routes (B7 of MEAS0): `moa = cells that S touches / cells that the straight lines touch`.
202
+ * The cells are squares of side `unit` from the origin, and `unit` is `u` of the board (`boardUnit`). The straight line of an edge joins the two ends of its route.
203
+ * The scale is a ratio, and lower is better. Routes without a straight line give 1. The cost is linear in the cells.
204
+ */
205
+ export function occupiedArea(routes: readonly Route[], unit: number): number {
206
+ const u = unit > 0 ? unit : 1;
207
+ const drawn = new Set<string>();
208
+ const straight = new Set<string>();
209
+ for (const r of routes) {
210
+ const p = r.points;
211
+ if (p.length < 2) continue;
212
+ for (let i = 1; i < p.length; i++) touchCells(p[i - 1]!, p[i]!, u, drawn);
213
+ touchCells(p[0]!, p[p.length - 1]!, u, straight);
214
+ }
215
+ return straight.size > 0 ? drawn.size / straight.size : 1;
216
+ }
217
+
218
+ /** The run profile of a result (B8 of MEAS0). */
219
+ export interface RunProfile {
220
+ /** The number of shared segments: the runs and the trunks of the bundles with two edges or more. */
221
+ readonly runs: number;
222
+ /** The median of the edges of each shared segment. It is 0 without a shared segment. */
223
+ readonly median: number;
224
+ /** The most edges of one shared segment. It is 0 without a shared segment. */
225
+ readonly max: number;
226
+ }
227
+
228
+ /** This function gives the run profile of a result (B8 of MEAS0): the shared segments, and the median and the most edges of each. The cost is linear. */
229
+ export function runProfile(graph: SegmentGraph): RunProfile {
230
+ const counts = graph.segments.filter((s) => s.count > 1).map((s) => s.count);
231
+ if (counts.length === 0) return { runs: 0, median: 0, max: 0 };
232
+ return { runs: counts.length, median: quantile(counts, 0.5), max: Math.max(...counts) };
233
+ }
234
+
235
+ /** The lane fidelity of a result (B9 of MEAS0). */
236
+ export interface LaneFidelity {
237
+ /** The median of `lf(e)` over the edges with a shared segment. */
238
+ readonly median: number;
239
+ /** The largest `lf(e)`. */
240
+ readonly max: number;
241
+ /** The number of edges with a shared segment. */
242
+ readonly edges: number;
243
+ }
244
+
245
+ /**
246
+ * This function gives the lane fidelity (B9 of MEAS0): `lf(e) = frechet(lane(e), r_e) / (width · k / 2)` for each edge `e` with a shared segment.
247
+ * `lane(e)` is the lane route of `openLanes`, `r_e` is the route, and `k` is the edge count of the widest shared segment of `e`.
248
+ * `frechet` is the discrete Fréchet distance of the points. A value of 1 or less is correct, and lower is better.
249
+ * It gives `undefined` when no edge has a shared segment. The cost is quadratic in the points of each route.
250
+ */
251
+ export function laneFidelity(routes: readonly Route[], open: OpenLanes): LaneFidelity | undefined {
252
+ const count = new Map(open.graph.segments.map((s) => [s.id, s.count] as const));
253
+ const lanes = new Map(open.lanes.map((r) => [r.edge, r.points] as const));
254
+ const own = new Map(routes.map((r) => [r.edge, r.points] as const));
255
+ const values: number[] = [];
256
+ for (const path of open.graph.paths) {
257
+ let k = 0;
258
+ for (const ref of path.segments) k = Math.max(k, count.get(ref.segment) ?? 0);
259
+ const lane = lanes.get(path.edge);
260
+ const route = own.get(path.edge);
261
+ if (k < 2 || !lane || !route || open.width <= 0) continue;
262
+ values.push(frechet(lane, route) / ((open.width * k) / 2));
263
+ }
264
+ if (values.length === 0) return undefined;
265
+ return { median: quantile(values, 0.5), max: Math.max(...values), edges: values.length };
266
+ }
267
+
268
+ /**
269
+ * This function counts the lane routes that go through a box that is not one of their ends (M4 of MEAS0).
270
+ * It is the count `laneThrough` of the room bench (`measureBoard`), for the lanes of any result. Lower is better. The cost is `n log n`.
271
+ */
272
+ export function lanesThrough(graph: Graph, open: OpenLanes): number {
273
+ return measureRouteFaults(graph, { routes: open.lanes }).through;
274
+ }
275
+
276
+ /**
277
+ * This function gives the area of a result (M1 of MEAS0) for a router: the growth of the board that the open lanes need.
278
+ * The value is the area of the bounding box of the boxes and the lane routes. The function divides it by the area of the bounding box of the boxes.
279
+ *
280
+ * It is the room form `area(open) / area(packed)` with the boxes as they are. The scale is a ratio of 1 or more, and lower is better.
281
+ * A board without area gives 1. The cost is linear.
282
+ */
283
+ export function laneArea(graph: Graph, open: OpenLanes): number {
284
+ let x0 = Number.POSITIVE_INFINITY;
285
+ let y0 = Number.POSITIVE_INFINITY;
286
+ let x1 = Number.NEGATIVE_INFINITY;
287
+ let y1 = Number.NEGATIVE_INFINITY;
288
+ for (const n of graph.nodes) {
289
+ x0 = Math.min(x0, n.box.x);
290
+ y0 = Math.min(y0, n.box.y);
291
+ x1 = Math.max(x1, n.box.x + n.box.w);
292
+ y1 = Math.max(y1, n.box.y + n.box.h);
293
+ }
294
+ const packed = (x1 - x0) * (y1 - y0);
295
+ if (!(packed > 0)) return 1;
296
+ for (const r of open.lanes)
297
+ for (const p of r.points) {
298
+ x0 = Math.min(x0, p.x);
299
+ y0 = Math.min(y0, p.y);
300
+ x1 = Math.max(x1, p.x);
301
+ y1 = Math.max(y1, p.y);
302
+ }
303
+ return ((x1 - x0) * (y1 - y0)) / packed;
304
+ }