@mark1russell7/highways 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Plug justification (WILD-002, with NAT-031): the plugs of one side keep their order and a minimum gap.
3
+ * They move the least distance from their ideal places.
4
+ */
5
+
6
+ /**
7
+ * This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
8
+ * The points keep the order of their ideal places. Two neighbours are at least `gap` apart, or the side divided evenly when it is too short.
9
+ * The sum of the squares of the moves is the least. The answer is exact: a fit by pool adjacent violators.
10
+ *
11
+ * The function `tie` orders two points with the same ideal place. The result is in the order of `ideal`.
12
+ */
13
+ export function justifyPlugs(ideal: readonly number[], lo: number, hi: number, gap: number, tie?: (i: number) => number): number[] {
14
+ const k = ideal.length;
15
+ if (k === 0) return [];
16
+ const order = ideal.map((_, i) => i).sort((a, b) => ideal[a]! - ideal[b]! || (tie ? tie(a) - tie(b) : 0) || a - b);
17
+ const s = k > 1 ? Math.max(0, Math.min(gap, (hi - lo) / (k - 1))) : 0;
18
+ // With y_i = x_i - i·s, the gap becomes the rule y_i <= y_(i+1): an isotonic fit of a_i - i·s.
19
+ const y = isotonic(order.map((j, i) => ideal[j]! - i * s));
20
+ const top = Math.max(lo, hi - (k - 1) * s);
21
+ const out = new Array<number>(k);
22
+ order.forEach((j, i) => {
23
+ out[j] = Math.max(lo, Math.min(top, y[i]!)) + i * s;
24
+ });
25
+ return out;
26
+ }
27
+
28
+ /** This function gives the non-decreasing sequence nearest to `v` in the sum of squares (pool adjacent violators). */
29
+ export function isotonic(v: readonly number[]): number[] {
30
+ const mean: number[] = [];
31
+ const size: number[] = [];
32
+ for (const x of v) {
33
+ mean.push(x);
34
+ size.push(1);
35
+ while (mean.length > 1 && mean[mean.length - 2]! > mean[mean.length - 1]!) {
36
+ const m = mean.pop()!;
37
+ const n = size.pop()!;
38
+ const i = mean.length - 1;
39
+ mean[i] = (mean[i]! * size[i]! + m * n) / (size[i]! + n);
40
+ size[i]! += n;
41
+ }
42
+ }
43
+ const out: number[] = [];
44
+ mean.forEach((m, i) => {
45
+ for (let j = 0; j < size[i]!; j++) out.push(m);
46
+ });
47
+ return out;
48
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Free plugs on a side (WILD-001): the search of a route starts from the whole free side, not from one point of it.
3
+ * Thus the first leg goes straight out to the first corner of the way, and the plug is the foot of that leg.
4
+ */
5
+ import { center, dist, type Box, type Point } from "../geom.ts";
6
+ import { fixedPortPoint, type End, type Node, type Side } from "../model.ts";
7
+ import { placePlug, pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "../plugs.ts";
8
+ import type { Terminal } from "../visibility.ts";
9
+ import type { WayFinder } from "../ways.ts";
10
+
11
+ /** The options of `sidePlugs`. */
12
+ export interface SidePlugOptions {
13
+ /** The cost of a slant first leg: the length of the leg grows by `bend` × its angle to the normal, in radians. The default is 1. */
14
+ readonly bend?: number;
15
+ /** The distance from a plug to a corner of its box. The default is 6, as in `pointOnSide`. */
16
+ readonly inset?: number;
17
+ /** The largest number of places that the search tries on each side. The default is 16. */
18
+ readonly most?: number;
19
+ }
20
+
21
+ /** A free side of one end: its node, its side and its range on the axis along the side. It also has the plug that faces the other end. */
22
+ interface FreeSide {
23
+ readonly node: Node;
24
+ readonly side: Side;
25
+ readonly normal: Point;
26
+ /** The axis along the side. */
27
+ readonly along: "x" | "y";
28
+ /** The place of the side on the other axis. */
29
+ readonly at: number;
30
+ readonly lo: number;
31
+ readonly hi: number;
32
+ readonly facing: Plug;
33
+ }
34
+
35
+ /** A place that the search tries: the plug, and the free point where the search starts from it, with its cost. */
36
+ interface Start extends Terminal {
37
+ readonly plug: Plug;
38
+ }
39
+
40
+ /** This function gives the free side of an end, or `undefined` when the plug of the end is fixed. */
41
+ function freeSide(node: Node, end: End, toward: Point, inset: number): FreeSide | undefined {
42
+ if (fixedPortPoint(node, end)) return undefined;
43
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
44
+ const side = port?.side ?? sideToward(node.box, toward);
45
+ const b: Box = node.box;
46
+ const along = side === "east" || side === "west" ? "y" : "x";
47
+ const start = along === "y" ? b.y : b.x;
48
+ const len = along === "y" ? b.h : b.w;
49
+ const m = Math.min(inset, len / 4);
50
+ const at = side === "east" ? b.x + b.w : side === "west" ? b.x : side === "south" ? b.y + b.h : b.y;
51
+ const point = pointOnSide(b, side, toward, inset);
52
+ return { node, side, normal: SIDE_NORMAL[side], along, at, lo: start + m, hi: start + len - m, facing: { point, normal: SIDE_NORMAL[side] } };
53
+ }
54
+
55
+ /** This function gives the point of the side at `v` on the axis along the side. */
56
+ function footAt(f: FreeSide, v: number): Point {
57
+ return f.along === "y" ? { x: f.at, y: v } : { x: v, y: f.at };
58
+ }
59
+
60
+ /**
61
+ * This function gives the places that the search tries on one free side. A place is the out point of a plug on the side.
62
+ * A place is also a corner in front of the side. The plug of a corner is the foot of the leg to it.
63
+ */
64
+ function startsOf(ways: WayFinder, f: FreeSide, others: readonly number[], bend: number, most: number): Start[] {
65
+ const margin = ways.margin;
66
+ const pad = margin / 2;
67
+ const own = new Set<Node>([f.node]);
68
+ const starts: Start[] = [];
69
+ const out = (p: Point): Point => ({ x: p.x + f.normal.x * margin, y: p.y + f.normal.y * margin });
70
+ const plugAt = (v: number): Plug => ({ point: footAt(f, v), normal: f.normal });
71
+ // The plug that faces the other end is always a place of the search, so the search is never worse than the default.
72
+ starts.push({ plug: f.facing, point: out(f.facing.point), cost: margin });
73
+ const mid = (f.lo + f.hi) / 2;
74
+ for (const v of [f.lo, mid, f.hi, ...others]) {
75
+ if (v < f.lo - 1e-9 || v > f.hi + 1e-9) continue;
76
+ const p = out(footAt(f, v));
77
+ if (!ways.obstacles.inside(p, 0, f.node)) starts.push({ plug: plugAt(v), point: p, cost: margin });
78
+ }
79
+ // The corners in front of the side that its out points see.
80
+ const seen = new Set<number>();
81
+ for (const v of [f.lo, mid, f.hi, f.facing.point[f.along]]) for (const c of ways.corners.nearestVisible(out(footAt(f, v)), 16)) seen.add(c);
82
+ for (const c of seen) {
83
+ const q = ways.corners.points[c]!;
84
+ const across = f.along === "y" ? "x" : "y";
85
+ const depth = (q[across] - f.at) * f.normal[across];
86
+ if (depth < margin - 1e-9) continue;
87
+ const v = Math.max(f.lo, Math.min(f.hi, q[f.along]));
88
+ const foot = footAt(f, v);
89
+ const angle = Math.atan2(Math.abs(q[f.along] - v), depth);
90
+ if (angle > Math.PI / 4 + 1e-9) continue;
91
+ if (!ways.obstacles.clear(foot, q, pad, own)) continue;
92
+ starts.push({ plug: plugAt(v), point: q, cost: dist(foot, q) * (1 + bend * angle) });
93
+ }
94
+ // One place for each foot: the cheapest. The places nearest to the facing plug come first.
95
+ const best = new Map<number, Start>();
96
+ for (const s of starts) {
97
+ const key = Math.round(s.plug.point[f.along] * 2);
98
+ const old = best.get(key);
99
+ if (!old || s.cost < old.cost) best.set(key, s);
100
+ }
101
+ const facing = f.facing.point[f.along];
102
+ const list = [...best.values()].sort((a, b) => Math.abs(a.plug.point[f.along] - facing) - Math.abs(b.plug.point[f.along] - facing));
103
+ return list.slice(0, Math.max(1, most));
104
+ }
105
+
106
+ /**
107
+ * This function places the two plugs of an edge with a free side. A fixed plug stays at its point.
108
+ * The search (A*) starts from each place of the free side of the source. It ends at each place of the free side of the target.
109
+ * The plugs are the places of the shortest way. Without a way, the plugs face the other end, as `placePlug` places them.
110
+ */
111
+ export function sidePlugs(ways: WayFinder, s: Node, es: End, t: Node, et: End, options: SidePlugOptions = {}): [Plug, Plug] {
112
+ const inset = options.inset ?? 6;
113
+ const ps = placePlug(s, es, center(t.box));
114
+ const pt = placePlug(t, et, center(s.box));
115
+ if (s === t) return [ps, pt];
116
+ const fs = freeSide(s, es, center(t.box), inset);
117
+ const ft = freeSide(t, et, center(s.box), inset);
118
+ if (!fs && !ft) return [ps, pt];
119
+ const bend = options.bend ?? 1;
120
+ const most = options.most ?? 16;
121
+ const fixed = (p: Plug): Start[] => [{ plug: p, point: ways.out(p), cost: ways.margin }];
122
+ // Two sides that face each other on one axis share the places of a straight line.
123
+ const facingPair = fs && ft && fs.along === ft.along && fs.normal.x === -ft.normal.x && fs.normal.y === -ft.normal.y;
124
+ const shared = (a: FreeSide | undefined, b: FreeSide | undefined, p: Plug): number[] => {
125
+ if (!a) return [];
126
+ if (facingPair && b) {
127
+ const lo = Math.max(a.lo, b.lo);
128
+ const hi = Math.min(a.hi, b.hi);
129
+ return lo <= hi ? [lo, (lo + hi) / 2, hi] : [];
130
+ }
131
+ return [p.point[a.along]];
132
+ };
133
+ const from = fs ? startsOf(ways, fs, shared(fs, ft, pt), bend, most) : fixed(ps);
134
+ const to = ft ? startsOf(ways, ft, shared(ft, fs, ps), bend, most) : fixed(pt);
135
+ const way = ways.corners.pathAmong(from, to);
136
+ if (!way) return [ps, pt];
137
+ return [from[way.from]!.plug, to[way.to]!.plug];
138
+ }
@@ -0,0 +1,377 @@
1
+ import { TOLERANCE } from "../bench/properties.ts";
2
+ import { center, dist, inflate, pointBoxDistance, segmentHitsBox, type Box, type Point } from "../geom.ts";
3
+ import { Grid } from "../grid.ts";
4
+ import { fixedPortPoint, nodeIndex, type Bundle, type Edge, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
5
+ import { nearestSide, placePlug, SIDE_NORMAL } from "../plugs.ts";
6
+ import { defineStrategy, type Strategy } from "../strategies/model.ts";
7
+ import { WayFinder } from "../ways.ts";
8
+ import { BoundaryWalk, nearestOnBoundary } from "./walk.ts";
9
+
10
+ export { BoundaryWalk, nearestOnBoundary } from "./walk.ts";
11
+
12
+ /** The options of the repair step (RULE-046). */
13
+ export interface RepairOptions {
14
+ /** The distance from a box to the corners of a detour, and twice the clearance of a pushed leg. The default is 5. */
15
+ readonly margin?: number;
16
+ /** The number of visible neighbours of each corner of the detour graph. The default is 8. */
17
+ readonly k?: number;
18
+ /** With `false`, the step does not push a leg out of a box. The default is `true`. */
19
+ readonly push?: boolean;
20
+ /** With `false`, the step takes no detour through the corner graph. The default is `true`. */
21
+ readonly detour?: boolean;
22
+ /** With `false`, the step does not route a whole faulty route again through the corner graph. The default is `true`. */
23
+ readonly reroute?: boolean;
24
+ /** With `false`, the step does not walk along the boundaries of the boxes. The default is `true`. */
25
+ readonly walk?: boolean;
26
+ }
27
+
28
+ /** The counts of one repair: what the step found and what it did. */
29
+ export interface RepairCounts {
30
+ /** The number of edges whose two nodes are in the graph. */
31
+ readonly routes: number;
32
+ /** The routes with a break of a hard rule before the repair, and the missing routes. */
33
+ readonly faulty: number;
34
+ /** The ends that the step moved back to their plugs or to the boxes of their nodes. */
35
+ readonly snapped: number;
36
+ /** The legs that the step pushed out of a box. */
37
+ readonly pushed: number;
38
+ /** The legs that the step replaced with a detour through the corner graph. */
39
+ readonly detoured: number;
40
+ /** The faulty routes that the step routed again from end to end through the corner graph. */
41
+ readonly rerouted: number;
42
+ /** The faulty routes that the step replaced with a walk along the boundaries of the boxes. */
43
+ readonly walked: number;
44
+ /** The missing routes that the step added. */
45
+ readonly added: number;
46
+ /** The faulty routes that the step could not repair. They keep their base routes. */
47
+ readonly kept: number;
48
+ /** The edges that the step took out of a run or a bundle. The repaired route of such an edge does not hold the run or the trunk. */
49
+ readonly unbundled: number;
50
+ /** The ends of level links that the step moved to the boundaries of their items. */
51
+ readonly linkEnds: number;
52
+ }
53
+
54
+ /** The result of `repairResult`: the repaired result and the counts. */
55
+ export interface RepairReport {
56
+ readonly result: Result;
57
+ readonly counts: RepairCounts;
58
+ }
59
+
60
+ /** The repair step makes at most this number of fixes for one route. A route that needs more keeps its base route. */
61
+ const MAX_FIXES = 64;
62
+
63
+ /** This function tells if the point is on the boundary of the box, within `TOLERANCE`, as the rule `link-ends` tests it. */
64
+ function onBoundary(p: Point, b: Box): boolean {
65
+ const inner = inflate(b, -TOLERANCE);
66
+ const deep = p.x > inner.x && p.x < inner.x + inner.w && p.y > inner.y && p.y < inner.y + inner.h;
67
+ return !deep && pointBoxDistance(p, b) <= TOLERANCE;
68
+ }
69
+
70
+ /** This function tells if the polyline `p` holds the points of `part` in a row, within `TOLERANCE`. */
71
+ function holds(p: readonly Point[], part: readonly Point[]): boolean {
72
+ if (part.length === 0) return true;
73
+ for (let i = 0; i + part.length <= p.length; i++) if (part.every((q, j) => dist(q, p[i + j]!) <= TOLERANCE)) return true;
74
+ return false;
75
+ }
76
+
77
+ /** This function drops each point that is at the same place as the point before it. */
78
+ function dedupe(p: readonly Point[]): Point[] {
79
+ const out: Point[] = [];
80
+ for (const v of p) if (out.length === 0 || dist(out[out.length - 1]!, v) > 1e-9) out.push(v);
81
+ return out;
82
+ }
83
+
84
+ /**
85
+ * This function repairs the breaks of the hard rules in the routes of any router (RULE-046). The input result does not change.
86
+ *
87
+ * - It adds a route through the corner graph for each edge without a route (`complete`).
88
+ * - It moves each end back to its fixed plug, or to the nearest point of the box of its node (`ends-at-plugs` and `fixed-plugs`).
89
+ * - It pushes each inner leg out of a box that it goes through, to the near side of the box (`no-pass-through`).
90
+ * - It replaces each other leg through a box with a detour through the corner graph.
91
+ * - When a leg does not repair, it routes the whole route again through the corner graph.
92
+ * Else it walks along the boundaries of the boxes (`BoundaryWalk`).
93
+ * - A route that still breaks a rule keeps its base route.
94
+ * - It takes an edge out of a run or a bundle when its new route does not hold the run or the trunk (`run-shared` and `bundle-trunk`).
95
+ * - It moves the ends of each level link to the boundaries of its items (`link-ends`).
96
+ *
97
+ * A route without a break does not change. The same input gives the same output.
98
+ */
99
+ export function repairResult(graph: Graph, result: Result, options: RepairOptions = {}): RepairReport {
100
+ const margin = options.margin ?? 5;
101
+ const doPush = options.push ?? true;
102
+ const doDetour = options.detour ?? true;
103
+ const doReroute = options.reroute ?? true;
104
+ const doWalk = options.walk ?? true;
105
+ const byId = nodeIndex(graph);
106
+ const grid = new Grid<Node>(180);
107
+ for (const n of graph.nodes) grid.add(n, n.box);
108
+ let ways: WayFinder | undefined;
109
+ let walker: BoundaryWalk | undefined;
110
+ const walkerOf = (): BoundaryWalk => (walker ??= new BoundaryWalk(graph.nodes, grid));
111
+ const waysOf = (): WayFinder => (ways ??= new WayFinder(graph.nodes, { margin, ...(options.k === undefined ? {} : { k: options.k }) }));
112
+ const counts = { routes: 0, faulty: 0, snapped: 0, pushed: 0, detoured: 0, rerouted: 0, walked: 0, added: 0, kept: 0, unbundled: 0, linkEnds: 0 };
113
+
114
+ // The test of the rule `no-pass-through`: a node whose box the segment goes through. The end nodes `s` and `t` do not count.
115
+ const hitOf = (p: Point, q: Point, s: Node, t: Node): Node | undefined => {
116
+ let hit: Node | undefined;
117
+ grid.querySegment(p, q, 0, (n) => {
118
+ if (!hit && n !== s && n !== t && segmentHitsBox(p, q, inflate(n.box, -TOLERANCE))) hit = n;
119
+ });
120
+ return hit;
121
+ };
122
+ const clear = (p: readonly Point[], s: Node, t: Node): boolean => {
123
+ for (let i = 1; i < p.length; i++) if (hitOf(p[i - 1]!, p[i]!, s, t)) return false;
124
+ return true;
125
+ };
126
+
127
+ // The point at `margin` out of the box that `p` is on or in, along the normal of its nearest side. A point on no box stays.
128
+ const leave = (p: Point): Point => {
129
+ let on: Node | undefined;
130
+ grid.query({ x: p.x - TOLERANCE, y: p.y - TOLERANCE, w: 2 * TOLERANCE, h: 2 * TOLERANCE }, (n) => {
131
+ if (!on && pointBoxDistance(p, n.box) <= TOLERANCE) on = n;
132
+ });
133
+ if (!on) return p;
134
+ const at = nearestOnBoundary(on.box, p);
135
+ const nrm = SIDE_NORMAL[nearestSide(on.box, at)];
136
+ return { x: at.x + nrm.x * margin, y: at.y + nrm.y * margin };
137
+ };
138
+
139
+ // The polyline with the leg `i` moved to one side of the box, with the clearance `margin / 2`, or `undefined`.
140
+ const push = (p: readonly Point[], i: number, box: Box, s: Node, t: Node): Point[] | undefined => {
141
+ const a = p[i - 1]!;
142
+ const b = p[i]!;
143
+ const len = dist(a, b);
144
+ if (len <= 0) return undefined;
145
+ const nx = -(b.y - a.y) / len;
146
+ const ny = (b.x - a.x) / len;
147
+ const big = inflate(box, margin / 2);
148
+ const proj = [
149
+ { x: big.x, y: big.y },
150
+ { x: big.x + big.w, y: big.y },
151
+ { x: big.x, y: big.y + big.h },
152
+ { x: big.x + big.w, y: big.y + big.h },
153
+ ].map((c) => (c.x - a.x) * nx + (c.y - a.y) * ny);
154
+ const shifts = [Math.max(...proj), Math.min(...proj)].sort((u, v) => Math.abs(u) - Math.abs(v));
155
+ for (const d of shifts) {
156
+ const moved = [...p];
157
+ moved[i - 1] = { x: a.x + nx * d, y: a.y + ny * d };
158
+ moved[i] = { x: b.x + nx * d, y: b.y + ny * d };
159
+ if (clear([p[i - 2]!, moved[i - 1]!, moved[i]!, p[i + 1]!], s, t)) return moved;
160
+ }
161
+ return undefined;
162
+ };
163
+
164
+ // The polyline with the leg `i` replaced by a detour through the corner graph, or `undefined`.
165
+ const detour = (p: readonly Point[], i: number, s: Node, t: Node): Point[] | undefined => {
166
+ const a = p[i - 1]!;
167
+ const b = p[i]!;
168
+ const finder = waysOf();
169
+ const way = finder.corners.path(leave(a), leave(b));
170
+ if (!way) return undefined;
171
+ const mid = dedupe([a, ...finder.corners.pull(way), b]);
172
+ if (!clear(mid, s, t)) return undefined;
173
+ return [...p.slice(0, i - 1), ...mid, ...p.slice(i + 1)];
174
+ };
175
+
176
+ const changed = new Set<string>();
177
+ const repairOne = (r: Route, s: Node, t: Node, e: Edge): Route => {
178
+ let p = dedupe(r.points);
179
+ if (p.length < 2) p = [...r.points];
180
+ let snapped = 0;
181
+ const first = p[0]!;
182
+ const last = p[p.length - 1]!;
183
+ const wantA = fixedPortPoint(s, e.source) ?? (pointBoxDistance(first, s.box) > TOLERANCE ? nearestOnBoundary(s.box, first) : undefined);
184
+ const wantB = fixedPortPoint(t, e.target) ?? (pointBoxDistance(last, t.box) > TOLERANCE ? nearestOnBoundary(t.box, last) : undefined);
185
+ if (wantA && dist(wantA, first) > TOLERANCE) {
186
+ p[0] = wantA;
187
+ snapped++;
188
+ }
189
+ if (wantB && dist(wantB, last) > TOLERANCE) {
190
+ p[p.length - 1] = wantB;
191
+ snapped++;
192
+ }
193
+ let fault = snapped > 0;
194
+ let pushed = 0;
195
+ let detoured = 0;
196
+ let fixes = 0;
197
+ let failed = false;
198
+ for (let i = 1; i < p.length; ) {
199
+ const hit = hitOf(p[i - 1]!, p[i]!, s, t);
200
+ if (!hit) {
201
+ i++;
202
+ continue;
203
+ }
204
+ fault = true;
205
+ if (++fixes > MAX_FIXES) {
206
+ failed = true;
207
+ break;
208
+ }
209
+ const moved = doPush && i >= 2 && i + 1 < p.length ? push(p, i, hit.box, s, t) : undefined;
210
+ if (moved) {
211
+ p = moved;
212
+ pushed++;
213
+ i++;
214
+ continue;
215
+ }
216
+ const around = doDetour ? detour(p, i, s, t) : undefined;
217
+ if (!around) {
218
+ failed = true;
219
+ break;
220
+ }
221
+ i += around.length - p.length + 1;
222
+ p = around;
223
+ detoured++;
224
+ }
225
+ if (!fault) return r;
226
+ counts.faulty++;
227
+ let whole: "rerouted" | "walked" | undefined;
228
+ if (failed) {
229
+ // The legs do not repair one by one. The step routes the whole route again from its two ends.
230
+ const a = p[0]!;
231
+ const b = p[p.length - 1]!;
232
+ const finder = doReroute ? waysOf() : undefined;
233
+ const way = finder?.corners.path(leave(a), leave(b));
234
+ const again = way ? dedupe([a, ...finder!.corners.pull(way), b]) : undefined;
235
+ if (again && clear(again, s, t)) {
236
+ p = again;
237
+ whole = "rerouted";
238
+ } else {
239
+ const walk = doWalk ? walkerOf().path(a, s, b, t) : undefined;
240
+ if (walk && clear(walk, s, t)) {
241
+ p = walk;
242
+ whole = "walked";
243
+ }
244
+ }
245
+ if (!whole) {
246
+ counts.kept++;
247
+ return r;
248
+ }
249
+ counts[whole]++;
250
+ }
251
+ counts.snapped += snapped;
252
+ if (!whole) {
253
+ counts.pushed += pushed;
254
+ counts.detoured += detoured;
255
+ }
256
+ changed.add(r.edge);
257
+ return { edge: r.edge, points: p };
258
+ };
259
+
260
+ const routeOf = new Map<string, Route>();
261
+ for (const r of result.routes) if (!routeOf.has(r.edge)) routeOf.set(r.edge, r);
262
+ const repaired = new Map<string, Route>();
263
+ for (const e of graph.edges) {
264
+ const s = byId.get(e.source.node);
265
+ const t = byId.get(e.target.node);
266
+ if (!s || !t || repaired.has(e.id)) continue;
267
+ counts.routes++;
268
+ const r = routeOf.get(e.id);
269
+ if (r && r.points.length >= 2) {
270
+ repaired.set(e.id, repairOne(r, s, t, e));
271
+ continue;
272
+ }
273
+ counts.faulty++;
274
+ const ps = placePlug(s, e.source, center(t.box));
275
+ const pt = placePlug(t, e.target, center(s.box));
276
+ let way: Point[] | undefined = waysOf().between(ps, pt, new Set<Node>([s, t]));
277
+ if (!clear(way, s, t)) way = doWalk ? walkerOf().path(ps.point, s, pt.point, t) : undefined;
278
+ if (!way || !clear(way, s, t)) {
279
+ counts.kept++;
280
+ continue;
281
+ }
282
+ counts.added++;
283
+ changed.add(e.id);
284
+ repaired.set(e.id, { edge: e.id, points: way });
285
+ }
286
+ // The routes keep the order of the result. A new route goes after them, in the order of the edges.
287
+ const out: Route[] = [];
288
+ const placed = new Set<string>();
289
+ for (const r of result.routes) {
290
+ const first = !placed.has(r.edge);
291
+ placed.add(r.edge);
292
+ out.push(first ? (repaired.get(r.edge) ?? r) : r);
293
+ }
294
+ for (const e of graph.edges) if (!placed.has(e.id) && repaired.has(e.id)) out.push(repaired.get(e.id)!);
295
+
296
+ const final = new Map<string, Route>();
297
+ for (const r of out) if (!final.has(r.edge)) final.set(r.edge, r);
298
+ let runs: SharedRun[] | undefined;
299
+ if (result.runs) {
300
+ runs = [];
301
+ for (const run of result.runs) {
302
+ const edges = run.edges.filter((id) => {
303
+ if (!changed.has(id)) return true;
304
+ const r = final.get(id);
305
+ const ok = r !== undefined && (holds(r.points, run.points) || holds(r.points, [...run.points].reverse()));
306
+ if (!ok) counts.unbundled++;
307
+ return ok;
308
+ });
309
+ if (edges.length === run.edges.length) runs.push(run);
310
+ else if (edges.length >= 2) runs.push({ ...run, edges });
311
+ else counts.unbundled += edges.length;
312
+ }
313
+ }
314
+ let bundles: Bundle[] | undefined;
315
+ if (result.bundles) {
316
+ bundles = [];
317
+ for (const b of result.bundles) {
318
+ const edges = b.edges.filter((id) => {
319
+ if (!changed.has(id)) return true;
320
+ const r = final.get(id);
321
+ const ok = r !== undefined && r.points.length >= b.trunk.length && b.trunk.every((q, i) => dist(q, r.points[i]!) <= TOLERANCE);
322
+ if (!ok) counts.unbundled++;
323
+ return ok;
324
+ });
325
+ if (edges.length === b.edges.length) bundles.push(b);
326
+ else if (edges.length > 0) bundles.push({ ...b, edges });
327
+ }
328
+ }
329
+ let levels: Level[] | undefined;
330
+ if (result.levels) {
331
+ levels = result.levels.map((level) => {
332
+ const boxOf = new Map(level.items.map((it) => [it.id, it.box]));
333
+ let touched = false;
334
+ const links = level.links.map((l) => {
335
+ const sb = boxOf.get(l.source);
336
+ const tb = boxOf.get(l.target);
337
+ const p = l.points;
338
+ if (!sb || !tb || p.length < 2) return l;
339
+ const a = onBoundary(p[0]!, sb) ? undefined : nearestOnBoundary(sb, p[0]!);
340
+ const b = onBoundary(p[p.length - 1]!, tb) ? undefined : nearestOnBoundary(tb, p[p.length - 1]!);
341
+ if (!a && !b) return l;
342
+ touched = true;
343
+ counts.linkEnds += (a ? 1 : 0) + (b ? 1 : 0);
344
+ const points = [...p];
345
+ if (a) points[0] = a;
346
+ if (b) points[points.length - 1] = b;
347
+ return { ...l, points };
348
+ });
349
+ return touched ? { ...level, links } : level;
350
+ });
351
+ }
352
+ return {
353
+ result: { ...result, routes: out, ...(runs ? { runs } : {}), ...(bundles ? { bundles } : {}), ...(levels ? { levels } : {}) },
354
+ counts,
355
+ };
356
+ }
357
+
358
+ /** This function gives a router that repairs the routes of `router` with `repairResult`. It keeps the name and the `maxNodes` of the router. */
359
+ export function withRepair(router: Router, options: RepairOptions = {}): Router {
360
+ return { ...router, route: (graph: Graph): Result => repairResult(graph, router.route(graph), options).result };
361
+ }
362
+
363
+ /**
364
+ * This function gives a router strategy that runs `strategy` and then the repair step (RULE-046) with `repair`.
365
+ * The new strategy has the name `<name>+repair`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
366
+ */
367
+ export function repairStrategy<O extends object>(strategy: Strategy<"router", O>, repair: RepairOptions = {}): Strategy<"router", O> {
368
+ return defineStrategy<"router", O>({
369
+ step: "router",
370
+ name: `${strategy.name}+repair`,
371
+ describe: `${strategy.describe} Then one repair step fixes the breaks of the hard rules.`,
372
+ defaults: { ...strategy.defaults } as O,
373
+ ...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
374
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
375
+ run: (graph: Graph, options: Readonly<O>): Result => repairResult(graph, strategy.run(graph, options), repair).result,
376
+ });
377
+ }