@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.
- package/README.md +67 -1
- package/build/index.d.ts +954 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5914 -1419
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/measures/bundles.ts +101 -2
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +2 -0
- package/src/bench/measures/order.ts +97 -0
- package/src/bench/room.ts +126 -9
- package/src/bench/screen/measures.ts +17 -0
- package/src/bench/screen/screen.ts +87 -9
- package/src/highways/base.ts +126 -0
- package/src/highways/flow-field/index.ts +49 -0
- package/src/highways/gates/ambiguity/index.ts +110 -0
- package/src/highways/gates/breakeven/index.ts +62 -0
- package/src/highways/gates/hysteresis/index.ts +48 -0
- package/src/highways/gates/ids/index.ts +59 -0
- package/src/highways/gates/index.ts +6 -0
- package/src/highways/gates/model.ts +129 -0
- package/src/highways/gates/stretch/index.ts +39 -0
- package/src/highways/grouped.ts +123 -0
- package/src/highways/index.ts +15 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -0
- package/src/highways/road-classes/index.ts +74 -0
- package/src/highways/router.ts +1 -1
- package/src/highways/select/index.ts +162 -0
- package/src/highways/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -0
- package/src/highways/weiszfeld/index.ts +105 -0
- package/src/highways/wspd/index.ts +130 -0
- package/src/highways/zipper/index.ts +168 -0
- package/src/index.ts +2 -0
- package/src/order/index.ts +65 -0
- package/src/plugs/justify.ts +48 -0
- package/src/plugs/side.ts +138 -0
- package/src/repair/index.ts +377 -0
- package/src/repair/walk.ts +220 -0
- package/src/room/lanes.ts +109 -10
- package/src/routers/corners.ts +73 -6
- package/src/routers/fan.ts +145 -0
- package/src/routers/index.ts +2 -2
- package/src/routers/shared.ts +107 -8
- package/src/routers/snell.ts +140 -0
- package/src/routers/trunks.ts +43 -2
- package/src/strategies/builtins.ts +149 -7
- package/src/strategies/schemas.ts +173 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { TOLERANCE } from "../bench/properties.ts";
|
|
2
|
+
import { dist, inflate, segmentHitsBox, type Box, type Point } from "../geom.ts";
|
|
3
|
+
import type { Grid } from "../grid.ts";
|
|
4
|
+
import type { Node } from "../model.ts";
|
|
5
|
+
import { nearestSide } from "../plugs.ts";
|
|
6
|
+
|
|
7
|
+
/** The sides of a box, as the indexes of two corners: north, east, south and west. */
|
|
8
|
+
const SIDES: readonly (readonly [number, number])[] = [
|
|
9
|
+
[0, 1],
|
|
10
|
+
[1, 2],
|
|
11
|
+
[3, 2],
|
|
12
|
+
[0, 3],
|
|
13
|
+
];
|
|
14
|
+
const SIDE_INDEX = { north: 0, east: 1, south: 2, west: 3 } as const;
|
|
15
|
+
|
|
16
|
+
/** This function gives the four corners of a box: top left, top right, bottom right and bottom left. */
|
|
17
|
+
function corners(b: Box): Point[] {
|
|
18
|
+
return [
|
|
19
|
+
{ x: b.x, y: b.y },
|
|
20
|
+
{ x: b.x + b.w, y: b.y },
|
|
21
|
+
{ x: b.x + b.w, y: b.y + b.h },
|
|
22
|
+
{ x: b.x, y: b.y + b.h },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EPS = 1e-6;
|
|
27
|
+
const keyOf = (p: Point): string => `${Math.round(p.x * 1000)},${Math.round(p.y * 1000)}`;
|
|
28
|
+
|
|
29
|
+
/** This function tells if the point `c` is on the axis-parallel segment `pq`. */
|
|
30
|
+
function onSide(c: Point, p: Point, q: Point): boolean {
|
|
31
|
+
if (Math.abs(p.y - q.y) <= EPS) return Math.abs(c.y - p.y) <= EPS && c.x >= Math.min(p.x, q.x) - EPS && c.x <= Math.max(p.x, q.x) + EPS;
|
|
32
|
+
return Math.abs(c.x - p.x) <= EPS && c.y >= Math.min(p.y, q.y) - EPS && c.y <= Math.max(p.y, q.y) + EPS;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** This function drops each point that is on the straight line between the point before it and the point after it. */
|
|
36
|
+
function straighten(p: readonly Point[]): Point[] {
|
|
37
|
+
const out: Point[] = [];
|
|
38
|
+
for (const v of p) {
|
|
39
|
+
if (out.length > 0 && dist(out[out.length - 1]!, v) <= EPS) continue;
|
|
40
|
+
while (out.length >= 2) {
|
|
41
|
+
const a = out[out.length - 2]!;
|
|
42
|
+
const b = out[out.length - 1]!;
|
|
43
|
+
const cross = (b.x - a.x) * (v.y - b.y) - (b.y - a.y) * (v.x - b.x);
|
|
44
|
+
const dot = (b.x - a.x) * (v.x - b.x) + (b.y - a.y) * (v.y - b.y);
|
|
45
|
+
if (Math.abs(cross) > EPS || dot < 0) break;
|
|
46
|
+
out.pop();
|
|
47
|
+
}
|
|
48
|
+
out.push(v);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The boundary walk of the repair step (RULE-046): a graph of the sides of the boxes.
|
|
55
|
+
* The vertices are the corners of the boxes. The edges are the parts of each side between two corners on it.
|
|
56
|
+
* A walk on this graph goes along the boundaries of the boxes, and not through a box. Thus it joins two boxes also when the boxes touch.
|
|
57
|
+
* The graph drops each part of a side that goes through another box, so overlapping boxes do not open a way.
|
|
58
|
+
*/
|
|
59
|
+
export class BoundaryWalk {
|
|
60
|
+
readonly #points: Point[] = [];
|
|
61
|
+
readonly #index = new Map<string, number>();
|
|
62
|
+
readonly #next: { readonly to: number; readonly d: number }[][] = [];
|
|
63
|
+
/** The vertices on each side of each node, in the order along the side. */
|
|
64
|
+
readonly #sides = new Map<Node, number[][]>();
|
|
65
|
+
|
|
66
|
+
/** This constructor makes the graph of the sides of the nodes. `grid` holds the nodes by their boxes. */
|
|
67
|
+
constructor(nodes: readonly Node[], grid: Grid<Node>) {
|
|
68
|
+
for (const n of nodes) for (const c of corners(n.box)) this.#vertex(c);
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
const cs = corners(n.box);
|
|
71
|
+
const lists: number[][] = [];
|
|
72
|
+
for (const [i, j] of SIDES) {
|
|
73
|
+
const p = cs[i]!;
|
|
74
|
+
const q = cs[j]!;
|
|
75
|
+
const on = new Set<number>([this.#vertex(p), this.#vertex(q)]);
|
|
76
|
+
const area = { x: Math.min(p.x, q.x) - 1, y: Math.min(p.y, q.y) - 1, w: Math.abs(q.x - p.x) + 2, h: Math.abs(q.y - p.y) + 2 };
|
|
77
|
+
grid.query(area, (m) => {
|
|
78
|
+
for (const c of corners(m.box)) if (onSide(c, p, q)) on.add(this.#vertex(c));
|
|
79
|
+
});
|
|
80
|
+
const list = [...on].sort((u, v) => dist(p, this.#points[u]!) - dist(p, this.#points[v]!) || u - v);
|
|
81
|
+
lists.push(list);
|
|
82
|
+
for (let k = 1; k < list.length; k++) {
|
|
83
|
+
const u = list[k - 1]!;
|
|
84
|
+
const v = list[k]!;
|
|
85
|
+
const a = this.#points[u]!;
|
|
86
|
+
const b = this.#points[v]!;
|
|
87
|
+
if (dist(a, b) <= EPS || this.#blocked(grid, a, b)) continue;
|
|
88
|
+
this.#link(u, v);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.#sides.set(n, lists);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** This method gives the index of the vertex at the point, and adds the vertex if it is new. */
|
|
96
|
+
#vertex(p: Point): number {
|
|
97
|
+
const k = keyOf(p);
|
|
98
|
+
let i = this.#index.get(k);
|
|
99
|
+
if (i === undefined) {
|
|
100
|
+
i = this.#points.length;
|
|
101
|
+
this.#points.push(p);
|
|
102
|
+
this.#next.push([]);
|
|
103
|
+
this.#index.set(k, i);
|
|
104
|
+
}
|
|
105
|
+
return i;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#link(u: number, v: number): void {
|
|
109
|
+
const d = dist(this.#points[u]!, this.#points[v]!);
|
|
110
|
+
if (this.#next[u]!.some((e) => e.to === v)) return;
|
|
111
|
+
this.#next[u]!.push({ to: v, d });
|
|
112
|
+
this.#next[v]!.push({ to: u, d });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** This method tells if the segment goes through a box, as the rule `no-pass-through` tests it. */
|
|
116
|
+
#blocked(grid: Grid<Node>, a: Point, b: Point): boolean {
|
|
117
|
+
return grid.someSegment(a, b, 0, (n) => segmentHitsBox(a, b, inflate(n.box, -TOLERANCE)));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** This method finds the side of `node` that is nearest to `p`. It gives the two vertices of that side that are next to `p`. */
|
|
121
|
+
#around(node: Node, p: Point): number[] {
|
|
122
|
+
const list = this.#sides.get(node)?.[SIDE_INDEX[nearestSide(node.box, p)]];
|
|
123
|
+
if (!list || list.length === 0) return [];
|
|
124
|
+
const from = this.#points[list[0]!]!;
|
|
125
|
+
const at = dist(from, p);
|
|
126
|
+
let k = 0;
|
|
127
|
+
while (k < list.length && dist(from, this.#points[list[k]!]!) < at) k++;
|
|
128
|
+
return [list[k - 1], list[k]].filter((v): v is number => v !== undefined);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* This method gives a way along the boundaries of the boxes. The way goes from the point `a` on the box of `s` to the point `b` on the box of `t`.
|
|
133
|
+
* A point that is not on its box goes first to the nearest point of the boundary.
|
|
134
|
+
* The method gives `undefined` when no way joins the two boxes.
|
|
135
|
+
*/
|
|
136
|
+
path(a: Point, s: Node, b: Point, t: Node): Point[] | undefined {
|
|
137
|
+
const a1 = nearestOnBoundary(s.box, a);
|
|
138
|
+
const b1 = nearestOnBoundary(t.box, b);
|
|
139
|
+
const n = this.#points.length;
|
|
140
|
+
const src = n;
|
|
141
|
+
const dst = n + 1;
|
|
142
|
+
const extra = new Map<number, { readonly to: number; readonly d: number }[]>();
|
|
143
|
+
const add = (u: number, v: number, d: number): void => {
|
|
144
|
+
if (!extra.has(u)) extra.set(u, []);
|
|
145
|
+
extra.get(u)!.push({ to: v, d });
|
|
146
|
+
};
|
|
147
|
+
for (const v of this.#around(s, a1)) add(src, v, dist(a1, this.#points[v]!));
|
|
148
|
+
for (const v of this.#around(t, b1)) add(v, dst, dist(b1, this.#points[v]!));
|
|
149
|
+
if (!extra.has(src)) return undefined;
|
|
150
|
+
const pointOf = (i: number): Point => (i === src ? a1 : i === dst ? b1 : this.#points[i]!);
|
|
151
|
+
const best = new Float64Array(n + 2).fill(Infinity);
|
|
152
|
+
const prev = new Int32Array(n + 2).fill(-1);
|
|
153
|
+
best[src] = 0;
|
|
154
|
+
const heap: [number, number][] = [[0, src]];
|
|
155
|
+
const less = (x: [number, number], y: [number, number]): boolean => x[0] < y[0] || (x[0] === y[0] && x[1] < y[1]);
|
|
156
|
+
const pop = (): [number, number] => {
|
|
157
|
+
const top = heap[0]!;
|
|
158
|
+
const last = heap.pop()!;
|
|
159
|
+
if (heap.length > 0) {
|
|
160
|
+
heap[0] = last;
|
|
161
|
+
let i = 0;
|
|
162
|
+
for (;;) {
|
|
163
|
+
const l = 2 * i + 1;
|
|
164
|
+
const r = l + 1;
|
|
165
|
+
let m = i;
|
|
166
|
+
if (l < heap.length && less(heap[l]!, heap[m]!)) m = l;
|
|
167
|
+
if (r < heap.length && less(heap[r]!, heap[m]!)) m = r;
|
|
168
|
+
if (m === i) break;
|
|
169
|
+
[heap[i], heap[m]] = [heap[m]!, heap[i]!];
|
|
170
|
+
i = m;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return top;
|
|
174
|
+
};
|
|
175
|
+
const push = (x: [number, number]): void => {
|
|
176
|
+
heap.push(x);
|
|
177
|
+
let i = heap.length - 1;
|
|
178
|
+
while (i > 0) {
|
|
179
|
+
const p = (i - 1) >> 1;
|
|
180
|
+
if (!less(heap[i]!, heap[p]!)) break;
|
|
181
|
+
[heap[i], heap[p]] = [heap[p]!, heap[i]!];
|
|
182
|
+
i = p;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
while (heap.length > 0) {
|
|
186
|
+
const [d, u] = pop();
|
|
187
|
+
if (d > best[u]!) continue;
|
|
188
|
+
if (u === dst) break;
|
|
189
|
+
const edges = [...(u < n ? this.#next[u]! : []), ...(extra.get(u) ?? [])];
|
|
190
|
+
for (const e of edges) {
|
|
191
|
+
const nd = d + e.d;
|
|
192
|
+
if (nd < best[e.to]!) {
|
|
193
|
+
best[e.to] = nd;
|
|
194
|
+
prev[e.to] = u;
|
|
195
|
+
push([nd, e.to]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (prev[dst] === -1) return undefined;
|
|
200
|
+
const way: Point[] = [];
|
|
201
|
+
for (let i = dst; i !== -1; i = prev[i]!) way.push(pointOf(i));
|
|
202
|
+
way.reverse();
|
|
203
|
+
return straighten([a, ...way, b]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** This function gives the point of the boundary of the box that is nearest to `p`. A point on the boundary gives itself. */
|
|
208
|
+
export function nearestOnBoundary(box: Box, p: Point): Point {
|
|
209
|
+
const x0 = box.x;
|
|
210
|
+
const y0 = box.y;
|
|
211
|
+
const x1 = box.x + box.w;
|
|
212
|
+
const y1 = box.y + box.h;
|
|
213
|
+
const inside = p.x > x0 && p.x < x1 && p.y > y0 && p.y < y1;
|
|
214
|
+
if (!inside) return { x: Math.min(x1, Math.max(x0, p.x)), y: Math.min(y1, Math.max(y0, p.y)) };
|
|
215
|
+
const side = nearestSide(box, p);
|
|
216
|
+
if (side === "north") return { x: p.x, y: y0 };
|
|
217
|
+
if (side === "south") return { x: p.x, y: y1 };
|
|
218
|
+
if (side === "west") return { x: x0, y: p.y };
|
|
219
|
+
return { x: x1, y: p.y };
|
|
220
|
+
}
|