@mark1russell7/highways 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +67 -1
  2. package/build/index.d.ts +954 -2
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +5914 -1419
  5. package/build/index.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/bench/measures/bundles.ts +101 -2
  8. package/src/bench/measures/highways.ts +93 -0
  9. package/src/bench/measures/index.ts +2 -0
  10. package/src/bench/measures/order.ts +97 -0
  11. package/src/bench/room.ts +126 -9
  12. package/src/bench/screen/measures.ts +17 -0
  13. package/src/bench/screen/screen.ts +87 -9
  14. package/src/highways/base.ts +126 -0
  15. package/src/highways/flow-field/index.ts +49 -0
  16. package/src/highways/gates/ambiguity/index.ts +110 -0
  17. package/src/highways/gates/breakeven/index.ts +62 -0
  18. package/src/highways/gates/hysteresis/index.ts +48 -0
  19. package/src/highways/gates/ids/index.ts +59 -0
  20. package/src/highways/gates/index.ts +6 -0
  21. package/src/highways/gates/model.ts +129 -0
  22. package/src/highways/gates/stretch/index.ts +39 -0
  23. package/src/highways/grouped.ts +123 -0
  24. package/src/highways/index.ts +15 -0
  25. package/src/highways/level-links/index.ts +114 -0
  26. package/src/highways/meet/index.ts +84 -0
  27. package/src/highways/model.ts +5 -0
  28. package/src/highways/od-cells/index.ts +50 -0
  29. package/src/highways/quickbundles/index.ts +117 -0
  30. package/src/highways/road-classes/index.ts +74 -0
  31. package/src/highways/router.ts +1 -1
  32. package/src/highways/select/index.ts +162 -0
  33. package/src/highways/stretches.ts +141 -0
  34. package/src/highways/stubs/index.ts +117 -0
  35. package/src/highways/weiszfeld/index.ts +105 -0
  36. package/src/highways/wspd/index.ts +130 -0
  37. package/src/highways/zipper/index.ts +168 -0
  38. package/src/index.ts +2 -0
  39. package/src/order/index.ts +65 -0
  40. package/src/plugs/justify.ts +48 -0
  41. package/src/plugs/side.ts +138 -0
  42. package/src/repair/index.ts +377 -0
  43. package/src/repair/walk.ts +220 -0
  44. package/src/room/lanes.ts +109 -10
  45. package/src/routers/corners.ts +73 -6
  46. package/src/routers/fan.ts +145 -0
  47. package/src/routers/index.ts +2 -2
  48. package/src/routers/shared.ts +107 -8
  49. package/src/routers/snell.ts +140 -0
  50. package/src/routers/trunks.ts +43 -2
  51. package/src/strategies/builtins.ts +149 -7
  52. package/src/strategies/schemas.ts +173 -2
@@ -0,0 +1,105 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import { basePlan, nearestPlace, planRoutes, routeIndex, tidyPlan, tripsOf, type HighwayBase } from "../base.ts";
3
+ import { memberAt, type HighwayInput, type HighwayMember, type HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `weiszfeld` (HWY-043 of the note on highways). */
6
+ export interface WeiszfeldOptions {
7
+ /** The plan whose highways get new meeting points. The default is `shared-ways`. */
8
+ readonly from: HighwayBase;
9
+ /** The weight of the trunk is `k^alpha` for `k` members. With 0, the trunk costs its ink one time. With 1, it costs each lane. The default is 0.5. */
10
+ readonly alpha: number;
11
+ /** The number of steps of Weiszfeld. The default is 20. */
12
+ readonly steps: number;
13
+ }
14
+
15
+ /** The default options of the strategy `weiszfeld`. */
16
+ export const WEISZFELD_DEFAULTS: Readonly<WeiszfeldOptions> = Object.freeze({ from: "shared-ways", alpha: 0.5, steps: 20 });
17
+
18
+ const TINY = 1e-9;
19
+
20
+ /** This function gives one step of Weiszfeld for the point `m`. The point `m` is near the points `ends`, and tied to `other` with the weight `w`. */
21
+ function weiszfeldStep(m: Point, ends: readonly Point[], other: Point, w: number): Point {
22
+ let x = 0;
23
+ let y = 0;
24
+ let sum = 0;
25
+ for (const p of ends) {
26
+ const d = Math.max(TINY, dist(p, m));
27
+ x += p.x / d;
28
+ y += p.y / d;
29
+ sum += 1 / d;
30
+ }
31
+ const d = Math.max(TINY, dist(other, m));
32
+ x += (w * other.x) / d;
33
+ y += (w * other.y) / d;
34
+ sum += w / d;
35
+ return sum > 0 ? { x: x / sum, y: y / sum } : m;
36
+ }
37
+
38
+ /** This function gives the mean of a list of points that is not empty. */
39
+ const mean = (ps: readonly Point[]): Point => ({ x: ps.reduce((s, p) => s + p.x, 0) / ps.length, y: ps.reduce((s, p) => s + p.y, 0) / ps.length });
40
+
41
+ /**
42
+ * This function gives the meeting points `M1` and `M2` of a bundle with the least weighted ink (HWY-043):
43
+ * `Σ |a_i - M1| + w |M1 - M2| + Σ |M2 - b_i|`. The lists `a` and `b` are the ends of the edges on each side, and `w` is the weight of the trunk.
44
+ * The function starts at the means of `a` and `b`. Each step moves `M1` and then `M2` by one step of Weiszfeld.
45
+ */
46
+ export function meetingPoints(a: readonly Point[], b: readonly Point[], w: number, steps: number): [Point, Point] {
47
+ if (a.length === 0 || b.length === 0) throw new Error("A side of the bundle has no end.");
48
+ let m1 = mean(a);
49
+ let m2 = mean(b);
50
+ for (let s = 0; s < steps; s++) {
51
+ m1 = weiszfeldStep(m1, a, m2, w);
52
+ m2 = weiszfeldStep(m2, b, m1, w);
53
+ }
54
+ return [m1, m2];
55
+ }
56
+
57
+ /**
58
+ * This function gives the highways of the plan `from`, with the meeting points of Weiszfeld (HWY-043).
59
+ * The ends of a member are the points where its edge comes to the highway and goes away from it after `follow` of the plan.
60
+ * They are the plugs, or the split point of the highway before and the merge point of the highway after.
61
+ * The weight of the trunk is `k^alpha` for `k` members. The trunk stays on the spine: the function projects `M1` and `M2` on the spine.
62
+ * Each member then joins the spine at `M1` and leaves it at `M2`, but not outside its own stretch. So a member only gets shorter on the spine.
63
+ */
64
+ export function weiszfeldPlan(input: HighwayInput, o: Readonly<WeiszfeldOptions>): HighwayPlan {
65
+ const plan = basePlan(input, o.from);
66
+ if (plan.highways.length === 0) return plan;
67
+ const routes = routeIndex(planRoutes(input.graph, plan).routes);
68
+ const trips = tripsOf(plan);
69
+ const highways = plan.highways.map((h, hi) => {
70
+ const a: Point[] = [];
71
+ const b: Point[] = [];
72
+ for (const m of h.members) {
73
+ const trip = trips.get(m.edge) ?? [];
74
+ const route = routes.get(m.edge);
75
+ const k = trip.findIndex((s) => s.h === hi);
76
+ const before = k > 0 ? trip[k - 1]!.m.split : route?.[0];
77
+ const after = k >= 0 && k < trip.length - 1 ? trip[k + 1]!.m.merge : route?.[route.length - 1];
78
+ if (!before || !after) continue;
79
+ if (m.on <= m.off) {
80
+ a.push(before);
81
+ b.push(after);
82
+ } else {
83
+ a.push(after);
84
+ b.push(before);
85
+ }
86
+ }
87
+ if (a.length < 2) return h;
88
+ const [m1, m2] = meetingPoints(a, b, h.members.length ** Math.max(0, o.alpha), Math.max(0, Math.round(o.steps)));
89
+ const pa = nearestPlace(h.spine, m1);
90
+ const pb = nearestPlace(h.spine, m2);
91
+ if (pb - pa <= 1e-6) return h;
92
+ const members: HighwayMember[] = h.members.map((m) => {
93
+ if (m.on <= m.off) {
94
+ const on = Math.max(m.on, pa);
95
+ const off = Math.min(m.off, pb);
96
+ return off - on > 1e-6 ? memberAt(h.spine, m.edge, on, off, m.seq) : m;
97
+ }
98
+ const on = Math.min(m.on, pb);
99
+ const off = Math.max(m.off, pa);
100
+ return on - off > 1e-6 ? memberAt(h.spine, m.edge, on, off, m.seq) : m;
101
+ });
102
+ return { ...h, members };
103
+ });
104
+ return tidyPlan({ highways });
105
+ }
@@ -0,0 +1,130 @@
1
+ import { center, dist, type Point } from "../../geom.ts";
2
+ import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
3
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
4
+
5
+ /** The options of the highway strategy `wspd` (HWY-018 of the note on highways). */
6
+ export interface WspdOptions {
7
+ /** The separation `s` of the pairs: the gap between two sets is at least `s` times the radius of the larger set. The default is 2. */
8
+ readonly separation: number;
9
+ /** A pair with this number of edges or more gets a highway. The default is 3. */
10
+ readonly minSize: number;
11
+ /** The spine of each pair. The default is `trunk`. */
12
+ readonly spine: GroupSpine;
13
+ }
14
+
15
+ /** The default options of the strategy `wspd`. */
16
+ export const WSPD_DEFAULTS: Readonly<WspdOptions> = Object.freeze({ separation: 2, minSize: 3, spine: "trunk" });
17
+
18
+ /** A cell of the split tree: the points at the places `lo` to `hi - 1` of the order, with their bounding circle. */
19
+ export interface SplitCell {
20
+ readonly id: number;
21
+ readonly lo: number;
22
+ readonly hi: number;
23
+ readonly c: Point;
24
+ readonly r: number;
25
+ readonly kids?: readonly [SplitCell, SplitCell];
26
+ }
27
+
28
+ /**
29
+ * The split tree of a set of points and its well-separated pairs (Callahan and Kosaraju 1995).
30
+ * Each cell splits at the middle of the longer side of its bounding box. A cell of equal points is a leaf.
31
+ */
32
+ export class SplitTree {
33
+ readonly root: SplitCell;
34
+ /** The place of each point in the order of the tree. */
35
+ readonly #at: Int32Array;
36
+ #count = 0;
37
+
38
+ /** This constructor builds the tree of the points. */
39
+ constructor(points: readonly Point[]) {
40
+ const order = points.map((_, i) => i);
41
+ this.#at = new Int32Array(points.length);
42
+ this.root = this.#build(points, order, 0, points.length);
43
+ order.forEach((p, i) => (this.#at[p] = i));
44
+ }
45
+
46
+ #build(points: readonly Point[], order: number[], lo: number, hi: number): SplitCell {
47
+ let x0 = Infinity;
48
+ let y0 = Infinity;
49
+ let x1 = -Infinity;
50
+ let y1 = -Infinity;
51
+ for (let i = lo; i < hi; i++) {
52
+ const p = points[order[i]!]!;
53
+ x0 = Math.min(x0, p.x);
54
+ y0 = Math.min(y0, p.y);
55
+ x1 = Math.max(x1, p.x);
56
+ y1 = Math.max(y1, p.y);
57
+ }
58
+ const id = this.#count++;
59
+ const c = center({ x: x0, y: y0, w: x1 - x0, h: y1 - y0 });
60
+ const r = Math.hypot(x1 - x0, y1 - y0) / 2;
61
+ if (hi - lo <= 1 || r <= 1e-9) return { id, lo, hi, c, r };
62
+ const byX = x1 - x0 >= y1 - y0;
63
+ const cut = byX ? (x0 + x1) / 2 : (y0 + y1) / 2;
64
+ const part = order.slice(lo, hi);
65
+ const left = part.filter((i) => (byX ? points[i]!.x : points[i]!.y) <= cut);
66
+ const right = part.filter((i) => (byX ? points[i]!.x : points[i]!.y) > cut);
67
+ [...left, ...right].forEach((p, k) => (order[lo + k] = p));
68
+ const mid = lo + left.length;
69
+ return { id, lo, hi, c, r, kids: [this.#build(points, order, lo, mid), this.#build(points, order, mid, hi)] };
70
+ }
71
+
72
+ /** This method tells if the cell holds the point `i`. */
73
+ holds(cell: SplitCell, i: number): boolean {
74
+ const at = this.#at[i]!;
75
+ return at >= cell.lo && at < cell.hi;
76
+ }
77
+
78
+ /**
79
+ * This method gives the well-separated pair that holds the pair of points `u` and `v`, with the cell of `u` first.
80
+ * Each pair of two different points is in one pair of the decomposition. Two equal points give `undefined`.
81
+ */
82
+ pairOf(u: number, v: number, s: number): [SplitCell, SplitCell] | undefined {
83
+ let cell = this.root;
84
+ while (cell.kids) {
85
+ const next = cell.kids.find((k) => this.holds(k, u) && this.holds(k, v));
86
+ if (!next) break;
87
+ cell = next;
88
+ }
89
+ if (!cell.kids) return undefined;
90
+ let a = cell.kids.find((k) => this.holds(k, u))!;
91
+ let b = cell.kids.find((k) => this.holds(k, v))!;
92
+ for (;;) {
93
+ const r = Math.max(a.r, b.r);
94
+ if (dist(a.c, b.c) - 2 * r >= s * r) return [a, b];
95
+ if (a.r >= b.r && a.kids) a = a.kids.find((k) => this.holds(k, u))!;
96
+ else if (b.kids) b = b.kids.find((k) => this.holds(k, v))!;
97
+ else if (a.kids) a = a.kids.find((k) => this.holds(k, u))!;
98
+ else return [a, b];
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * This function gives the groups of the edges by the well-separated pair of their two nodes (HWY-018).
105
+ * The points are the centers of the boxes. The edges of one pair, in either direction, are one group.
106
+ */
107
+ export function wspdGroups(input: HighwayInput, o: Readonly<WspdOptions>): Map<string, EdgeGroup> {
108
+ const nodes = input.graph.nodes;
109
+ const index = new Map(nodes.map((n, i) => [n.id, i]));
110
+ const tree = new SplitTree(nodes.map((n) => center(n.box)));
111
+ const groups = new Map<string, EdgeGroup>();
112
+ for (const e of input.graph.edges) {
113
+ const u = index.get(e.source.node);
114
+ const v = index.get(e.target.node);
115
+ if (u === undefined || v === undefined || u === v) continue;
116
+ const pair = tree.pairOf(u, v, o.separation);
117
+ if (!pair) continue;
118
+ const [a, b] = pair;
119
+ groups.set(e.id, a.id < b.id ? { key: `w${a.id}:${b.id}`, flip: false } : { key: `w${b.id}:${a.id}`, flip: true });
120
+ }
121
+ return groups;
122
+ }
123
+
124
+ /**
125
+ * This function gives the highways of clustered edge routing with the WSPD (HWY-018, after Bouts and Speckmann 2015).
126
+ * The well-separated pairs split all the pairs of nodes. The edges of one pair with `minSize` edges or more get one highway from one set to the other.
127
+ */
128
+ export function wspdPlan(input: HighwayInput, o: Readonly<WspdOptions>): HighwayPlan {
129
+ return groupedPlan(input, wspdGroups(input, o), { spine: o.spine, minSize: o.minSize });
130
+ }
@@ -0,0 +1,168 @@
1
+ import { boardUnit, segmentKey } from "../../bench/measures/routes.ts";
2
+ import { dist, type Point } from "../../geom.ts";
3
+ import { Grid, segmentBounds } from "../../grid.ts";
4
+ import { WayFinder } from "../../ways.ts";
5
+ import { basePlan, orderAlongRoutes, planRoutes, routeIndex, tidyPlan, type HighwayBase } from "../base.ts";
6
+ import { memberAt, type Highway, type HighwayInput, type HighwayPlan } from "../model.ts";
7
+
8
+ /** The options of the highway strategy `zipper` (NAT-022 with WILD-037 of the notes). */
9
+ export interface ZipperOptions {
10
+ /** The plan whose routes the zipper joins. The default is `shared-ways`. */
11
+ readonly from: HighwayBase;
12
+ /** Two legs zip when they are this distance apart or nearer, in units of the board (the median short side of the boxes). The default is 1. */
13
+ readonly near: number;
14
+ /** Two legs zip when the angle between them is this value or less, in degrees. The default is 15. */
15
+ readonly maxAngle: number;
16
+ /** The adhesion over the tension: two legs zip when `adhesion * overlap` is more than the length that the zip adds. The default is 0.3. */
17
+ readonly adhesion: number;
18
+ /** Two legs zip when they overlap along this length or more, in units of the board. The default is 1. */
19
+ readonly minOverlap: number;
20
+ /** The number of passes. Each pass routes the plan and zips the legs again. The default is 1. */
21
+ readonly passes: number;
22
+ }
23
+
24
+ /** The default options of the strategy `zipper`. */
25
+ export const ZIPPER_DEFAULTS: Readonly<ZipperOptions> = Object.freeze({ from: "shared-ways", near: 1, maxAngle: 15, adhesion: 0.3, minOverlap: 1, passes: 1 });
26
+
27
+ /** One leg of a route that no other route shares. */
28
+ interface Leg {
29
+ readonly edge: string;
30
+ readonly a: Point;
31
+ readonly b: Point;
32
+ readonly len: number;
33
+ }
34
+
35
+ /** A pair of legs that can zip, with the line of the zip. */
36
+ export interface Zip {
37
+ readonly gain: number;
38
+ /** The first point of the zipped line. */
39
+ readonly from: Point;
40
+ /** The last point of the zipped line. */
41
+ readonly to: Point;
42
+ /** This value is `true` when the leg goes from `from` to `to`, for each of the two legs. */
43
+ readonly forward: readonly [boolean, boolean];
44
+ }
45
+
46
+ /** The limits of `zipLegs`, in the units of the board. */
47
+ export interface ZipLimits {
48
+ readonly near: number;
49
+ readonly maxAngle: number;
50
+ readonly adhesion: number;
51
+ readonly minOverlap: number;
52
+ }
53
+
54
+ /**
55
+ * This function tells if two legs zip (NAT-022), and gives the zipped line.
56
+ * The zipped line is the mean of the two legs, with the weight of their lengths, along the stretch where they overlap.
57
+ * The legs zip when the angle between them is `maxAngle` or less, and they are `near` or nearer along the overlap.
58
+ * The overlap must be `minOverlap` or more.
59
+ * The zip adds two jogs to each leg, so it adds the length `2 (d_a + d_b)`, with the distances of the legs to the zipped line.
60
+ * The legs zip only when `adhesion * overlap` is more than that length.
61
+ */
62
+ export function zipLegs(a0: Point, a1: Point, b0: Point, b1: Point, o: ZipLimits): Zip | undefined {
63
+ const la = dist(a0, a1);
64
+ const lb = dist(b0, b1);
65
+ if (la <= 1e-9 || lb <= 1e-9) return undefined;
66
+ const ua = { x: (a1.x - a0.x) / la, y: (a1.y - a0.y) / la };
67
+ const ub = { x: (b1.x - b0.x) / lb, y: (b1.y - b0.y) / lb };
68
+ const dot = ua.x * ub.x + ua.y * ub.y;
69
+ if (Math.abs(dot) < Math.cos((o.maxAngle * Math.PI) / 180) - 1e-12) return undefined;
70
+ const sign = dot >= 0 ? 1 : -1;
71
+ const dx = ua.x * la + sign * ub.x * lb;
72
+ const dy = ua.y * la + sign * ub.y * lb;
73
+ const dl = Math.hypot(dx, dy);
74
+ if (dl <= 1e-9) return undefined;
75
+ const u = { x: dx / dl, y: dy / dl };
76
+ const o0 = { x: ((a0.x + a1.x) * la + (b0.x + b1.x) * lb) / (2 * (la + lb)), y: ((a0.y + a1.y) * la + (b0.y + b1.y) * lb) / (2 * (la + lb)) };
77
+ const along = (p: Point): number => (p.x - o0.x) * u.x + (p.y - o0.y) * u.y;
78
+ const across = (p: Point): number => (p.x - o0.x) * u.y - (p.y - o0.y) * u.x;
79
+ const ta = [along(a0), along(a1)] as const;
80
+ const tb = [along(b0), along(b1)] as const;
81
+ const lo = Math.max(Math.min(...ta), Math.min(...tb));
82
+ const hi = Math.min(Math.max(...ta), Math.max(...tb));
83
+ const overlap = hi - lo;
84
+ if (overlap < o.minOverlap || overlap <= 1e-9) return undefined;
85
+ // The offset of a leg from the zipped line at the place t along it.
86
+ const offset = (p0: Point, p1: Point, t0: number, t1: number, t: number): number => {
87
+ const s = Math.abs(t1 - t0) <= 1e-12 ? 0 : (t - t0) / (t1 - t0);
88
+ return across({ x: p0.x + (p1.x - p0.x) * s, y: p0.y + (p1.y - p0.y) * s });
89
+ };
90
+ let jog = 0;
91
+ for (const t of [lo, hi]) {
92
+ const da = offset(a0, a1, ta[0], ta[1], t);
93
+ const db = offset(b0, b1, tb[0], tb[1], t);
94
+ if (Math.abs(da - db) > o.near) return undefined;
95
+ jog += Math.abs(da) + Math.abs(db);
96
+ }
97
+ const gain = o.adhesion * overlap - jog;
98
+ if (!(gain > 0)) return undefined;
99
+ const from = { x: o0.x + u.x * lo, y: o0.y + u.y * lo };
100
+ const to = { x: o0.x + u.x * hi, y: o0.y + u.y * hi };
101
+ return { gain, from, to, forward: [ua.x * u.x + ua.y * u.y > 0, ub.x * u.x + ub.y * u.y > 0] };
102
+ }
103
+
104
+ /**
105
+ * This function gives the highways of the plan `from`, with the legs that run near and nearly parallel zipped together (NAT-022, WILD-037).
106
+ * A pass routes the plan with `follow` and takes each leg that no other route shares. A grid of the legs finds the near pairs.
107
+ * The pairs that zip (`zipLegs`) go from the largest gain to the smallest, and each leg zips one time in a pass at most.
108
+ * A zipped line that goes into a box, made larger by 1, does not zip.
109
+ * Each zip is a new highway of two members. The `seq` of each member follows the place of its merge point along the route of the edge.
110
+ */
111
+ export function zipperPlan(input: HighwayInput, o: Readonly<ZipperOptions>): HighwayPlan {
112
+ const graph = input.graph;
113
+ const unit = boardUnit(graph);
114
+ const limits: ZipLimits = { near: o.near * unit, maxAngle: o.maxAngle, adhesion: o.adhesion, minOverlap: Math.max(1e-6, o.minOverlap * unit) };
115
+ const obstacles = new WayFinder(graph.nodes).obstacles;
116
+ let plan = basePlan(input, o.from);
117
+ for (let pass = 0; pass < Math.max(1, Math.round(o.passes)); pass++) {
118
+ const routes = planRoutes(graph, plan).routes;
119
+ const users = new Map<string, number>();
120
+ for (const r of routes) {
121
+ const own = new Set<string>();
122
+ for (let i = 1; i < r.points.length; i++) {
123
+ const k = segmentKey(r.points[i - 1]!, r.points[i]!);
124
+ if (k !== undefined) own.add(k);
125
+ }
126
+ for (const k of own) users.set(k, (users.get(k) ?? 0) + 1);
127
+ }
128
+ const legs: Leg[] = [];
129
+ for (const r of routes)
130
+ for (let i = 1; i < r.points.length; i++) {
131
+ const a = r.points[i - 1]!;
132
+ const b = r.points[i]!;
133
+ const k = segmentKey(a, b);
134
+ const len = dist(a, b);
135
+ if (k === undefined || (users.get(k) ?? 0) > 1 || len < limits.minOverlap) continue;
136
+ legs.push({ edge: r.edge, a, b, len });
137
+ }
138
+ const grid = new Grid<number>(Math.max(32, 4 * limits.near));
139
+ legs.forEach((l, i) => {
140
+ const bx = segmentBounds(l.a, l.b);
141
+ grid.add(i, { x: bx.x - limits.near, y: bx.y - limits.near, w: bx.w + 2 * limits.near, h: bx.h + 2 * limits.near });
142
+ });
143
+ const found: { i: number; j: number; zip: Zip }[] = [];
144
+ legs.forEach((l, i) => {
145
+ grid.query(segmentBounds(l.a, l.b), (j) => {
146
+ if (j <= i || legs[j]!.edge === l.edge) return;
147
+ const m = legs[j]!;
148
+ const zip = zipLegs(l.a, l.b, m.a, m.b, limits);
149
+ if (zip) found.push({ i, j, zip });
150
+ });
151
+ });
152
+ found.sort((p, q) => q.zip.gain - p.zip.gain || p.i - q.i || p.j - q.j);
153
+ const used = new Set<number>();
154
+ const added: Highway[] = [];
155
+ for (const { i, j, zip } of found) {
156
+ if (used.has(i) || used.has(j)) continue;
157
+ if (!obstacles.clear(zip.from, zip.to, 1)) continue;
158
+ used.add(i);
159
+ used.add(j);
160
+ const spine = [zip.from, zip.to];
161
+ const members = [legs[i]!, legs[j]!].map((l, k) => (zip.forward[k] ? memberAt(spine, l.edge, 0, 1) : memberAt(spine, l.edge, 1, 0)));
162
+ added.push({ id: "", key: `zip${pass}`, spine, lanes: members.map((m) => m.edge), members });
163
+ }
164
+ if (added.length === 0) break;
165
+ plan = tidyPlan(orderAlongRoutes({ highways: [...plan.highways, ...added] }, routeIndex(routes)));
166
+ }
167
+ return tidyPlan(plan);
168
+ }
package/src/index.ts CHANGED
@@ -29,3 +29,5 @@ export * from "./strategies/index.ts";
29
29
  export * from "./bench/compare.ts";
30
30
  export * from "./bench/screen/index.ts";
31
31
  export * from "./highways/index.ts";
32
+ export * from "./order/index.ts";
33
+ export * from "./repair/index.ts";
@@ -0,0 +1,65 @@
1
+ import { fnv1a32 } from "../bench/screen/hash.ts";
2
+ import { rng } from "../fixtures.ts";
3
+ import type { Edge, Graph, Result, Router } from "../model.ts";
4
+ import { defineStrategy, type Strategy } from "../strategies/model.ts";
5
+
6
+ /** This function compares two edges by the hash of their ids, and then by their ids. Thus the order of two edges does not depend on the input. */
7
+ function byHash(a: { readonly h: number; readonly e: Edge }, b: { readonly h: number; readonly e: Edge }): number {
8
+ if (a.h !== b.h) return a.h - b.h;
9
+ return a.e.id < b.e.id ? -1 : a.e.id > b.e.id ? 1 : 0;
10
+ }
11
+
12
+ /**
13
+ * This function gives the graph with its edges in the stable order of WILD-053: by the FNV-1a hash of the id, then by the id.
14
+ * The nodes and the edges do not change. Two orders of the same edges give the same graph.
15
+ * The hash spreads the edges, so the stable order does not favor the edges of one node or one collection.
16
+ */
17
+ export function stableOrder(graph: Graph): Graph {
18
+ const edges = graph.edges.map((e) => ({ h: fnv1a32(e.id), e })).sort(byHash).map((x) => x.e);
19
+ return { ...graph, edges };
20
+ }
21
+
22
+ /** This function gives the graph with its edges in a random order of the seed. The same graph and seed always give the same order. */
23
+ export function shuffleEdges(graph: Graph, seed: number): Graph {
24
+ const edges = [...graph.edges];
25
+ const r = rng(seed);
26
+ for (let i = edges.length - 1; i > 0; i--) {
27
+ const j = Math.floor(r() * (i + 1));
28
+ [edges[i], edges[j]] = [edges[j]!, edges[i]!];
29
+ }
30
+ return { ...graph, edges };
31
+ }
32
+
33
+ /**
34
+ * This function routes the graph in the stable order of `stableOrder`, with the function `route`.
35
+ * The routes come back in the order of the input edges, so the caller sees no change of order.
36
+ * The bundles, the runs and the levels come from the stable order. Thus each order of the input gives the same data.
37
+ */
38
+ export function routeInStableOrder(graph: Graph, route: (graph: Graph) => Result): Result {
39
+ const result = route(stableOrder(graph));
40
+ const index = new Map(graph.edges.map((e, i) => [e.id, i] as const));
41
+ const at = (edge: string): number => index.get(edge) ?? Number.MAX_SAFE_INTEGER;
42
+ const routes = [...result.routes].sort((a, b) => at(a.edge) - at(b.edge));
43
+ return { ...result, routes };
44
+ }
45
+
46
+ /** This function gives a router that routes each graph in the stable order of WILD-053. It keeps the name and the `maxNodes` of the router. */
47
+ export function withStableOrder(router: Router): Router {
48
+ return { ...router, route: (graph: Graph): Result => routeInStableOrder(graph, (g) => router.route(g)) };
49
+ }
50
+
51
+ /**
52
+ * This function gives a router strategy that runs `strategy` in the stable order of WILD-053. The output does not depend on the order of the input edges.
53
+ * The new strategy has the name `<name>+stable`, and the options and the schema of `strategy`. The strategy `strategy` does not change.
54
+ */
55
+ export function stableOrderStrategy<O extends object>(strategy: Strategy<"router", O>): Strategy<"router", O> {
56
+ return defineStrategy<"router", O>({
57
+ step: "router",
58
+ name: `${strategy.name}+stable`,
59
+ describe: `${strategy.describe} The edges go in a stable order: by a hash of their ids.`,
60
+ defaults: { ...strategy.defaults } as O,
61
+ ...(strategy.schema === undefined ? {} : { schema: strategy.schema }),
62
+ ...(strategy.maxNodes === undefined ? {} : { maxNodes: strategy.maxNodes }),
63
+ run: (graph: Graph, options: Readonly<O>): Result => routeInStableOrder(graph, (g) => strategy.run(g, options)),
64
+ });
65
+ }
@@ -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
+ }