@mark1russell7/highways 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,236 @@
1
+ import { center, dist, polylineLength, type Box, type Point } from "../geom.ts";
2
+ import {
3
+ nodeIndex,
4
+ type Graph,
5
+ type Level,
6
+ type LevelItem,
7
+ type LevelLink,
8
+ type Node,
9
+ type Result,
10
+ type Route,
11
+ type Router,
12
+ type SharedRun,
13
+ } from "../model.ts";
14
+ import { placePlug } from "../plugs.ts";
15
+ import { allSections, buildSections, treeDepth, type Section, type SectionOptions } from "../sections.ts";
16
+ import { orderLanes } from "../segments.ts";
17
+ import { WayFinder, type WayOptions } from "../ways.ts";
18
+
19
+ /** The options of the router with sections. */
20
+ export interface SectionsOptions extends WayOptions, SectionOptions {
21
+ /** The name in the benchmark. */
22
+ readonly name: string;
23
+ /**
24
+ * The number of hubs on each side of the lowest common section that a way passes. A smaller number gives shorter ways
25
+ * and fewer shared ways. Without it, a way passes each hub from its node up.
26
+ */
27
+ readonly hubs?: number;
28
+ /** A way through the hubs can be at most this many times as long as the line between its plugs. Else the edge goes directly. The default is 2. */
29
+ readonly stretch?: number;
30
+ /** The benchmark skips a fixture with more nodes. */
31
+ readonly maxNodes?: number;
32
+ }
33
+
34
+ const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
35
+
36
+ /**
37
+ * This function makes a router with a tree of sections. Each section has a hub: the free corner nearest to the center of its nodes.
38
+ * An edge goes from its node up the hubs of its sections to the lowest common section, across, and down to the other node.
39
+ * Each way from hub to hub is found one time, so all the edges on it share its points.
40
+ * The edges in one section of the lowest level go straight around the nodes. The result gives one zoom level for each depth of the tree.
41
+ */
42
+ export function sectionsRouter(options: SectionsOptions): Router {
43
+ const router: Router = {
44
+ name: options.name,
45
+ route(graph: Graph): Result {
46
+ const stretch = options.stretch ?? 2;
47
+ const byId = nodeIndex(graph);
48
+ const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
49
+ const ways = new WayFinder(graph.nodes, options);
50
+ const corners = ways.corners;
51
+ const root = buildSections(graph.nodes, options);
52
+ const parent = new Map<Section, Section>();
53
+ const leafOf: Section[] = [];
54
+ for (const s of allSections(root)) {
55
+ for (const c of s.children) parent.set(c, s);
56
+ if (s.children.length === 0) for (const i of s.nodes) leafOf[i] = s;
57
+ }
58
+ const hubs = new Map<Section, Point>();
59
+ const hub = (s: Section): Point => {
60
+ let h = hubs.get(s);
61
+ if (!h) {
62
+ let x = 0;
63
+ let y = 0;
64
+ for (const i of s.nodes) {
65
+ const c = center(graph.nodes[i]!.box);
66
+ x += c.x;
67
+ y += c.y;
68
+ }
69
+ const c = { x: x / Math.max(1, s.nodes.length), y: y / Math.max(1, s.nodes.length) };
70
+ const j = corners.nearest(c);
71
+ h = j >= 0 ? corners.points[j]! : c;
72
+ hubs.set(s, h);
73
+ }
74
+ return h;
75
+ };
76
+ const up = (s: Section): Section[] => {
77
+ const out: Section[] = [];
78
+ for (let x: Section | undefined = s; x; x = parent.get(x)) out.push(x);
79
+ return out;
80
+ };
81
+
82
+ // The ways between two places, found one time. A place is a hub or a free point.
83
+ const pieces = new Map<string, Point[]>();
84
+ const users = new Map<string, string[]>();
85
+ const piece = (ka: string, a: Point, kb: string, b: Point, used: string[]): Point[] => {
86
+ const forward = ka <= kb;
87
+ const key = forward ? `${ka}>${kb}` : `${kb}>${ka}`;
88
+ let p = pieces.get(key);
89
+ if (!p) {
90
+ const [from, to] = forward ? [a, b] : [b, a];
91
+ const way = corners.path(from, to);
92
+ p = dedupe(way ? corners.pull(way) : [from, to]);
93
+ pieces.set(key, p);
94
+ }
95
+ if (!used.includes(key)) used.push(key);
96
+ return forward ? p : [...p].reverse();
97
+ };
98
+ const at = (p: Point): string => `${p.x},${p.y}`;
99
+
100
+ const routes: Route[] = [];
101
+ // The index in the route of each hub that the route passes, by section.
102
+ const marks = new Map<string, Map<Section, number>>();
103
+ for (const e of graph.edges) {
104
+ const s = byId.get(e.source.node);
105
+ const t = byId.get(e.target.node);
106
+ const iu = indexOfNode.get(e.source.node);
107
+ const iv = indexOfNode.get(e.target.node);
108
+ if (!s || !t || iu === undefined || iv === undefined) continue;
109
+ const ps = placePlug(s, e.source, center(t.box));
110
+ const pt = placePlug(t, e.target, center(s.box));
111
+ const lu = leafOf[iu]!;
112
+ const lv = leafOf[iv]!;
113
+ if (lu === lv) {
114
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
115
+ continue;
116
+ }
117
+ const a = up(lu);
118
+ const b = up(lv);
119
+ const common = new Set(b);
120
+ const lca = a.find((x) => common.has(x))!;
121
+ const keep = options.hubs ?? Infinity;
122
+ const side = (list: Section[]): Section[] => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
123
+ const chain = [...side(a), ...side(b).reverse()];
124
+ const out: Point[] = [ps.point];
125
+ const mark = new Map<Section, number>();
126
+ const append = (p: readonly Point[]): void => {
127
+ for (const q of p) if (dist(q, out[out.length - 1]!) > 1e-9) out.push(q);
128
+ };
129
+ const used: string[] = [];
130
+ const start = ways.out(ps);
131
+ append(piece(at(start), start, chain[0]!.id, hub(chain[0]!), used));
132
+ mark.set(chain[0]!, out.length - 1);
133
+ for (let i = 1; i < chain.length; i++) {
134
+ append(piece(chain[i - 1]!.id, hub(chain[i - 1]!), chain[i]!.id, hub(chain[i]!), used));
135
+ mark.set(chain[i]!, out.length - 1);
136
+ }
137
+ const end = ways.out(pt);
138
+ append(piece(chain[chain.length - 1]!.id, hub(chain[chain.length - 1]!), at(end), end, used));
139
+ append([pt.point]);
140
+ // A way through the hubs that is too long gives way to the direct way.
141
+ if (polylineLength(out) > stretch * dist(ps.point, pt.point) + 4 * ways.margin) {
142
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
143
+ continue;
144
+ }
145
+ for (const key of used) {
146
+ const list = users.get(key);
147
+ if (list) list.push(e.id);
148
+ else users.set(key, [e.id]);
149
+ }
150
+ routes.push({ edge: e.id, points: out });
151
+ marks.set(e.id, mark);
152
+ }
153
+
154
+ const routeOf = new Map(routes.map((r) => [r.edge, r]));
155
+ const runs: SharedRun[] = [];
156
+ for (const [key, p] of pieces) {
157
+ const list = users.get(key) ?? [];
158
+ if (list.length < 2 || p.length < 2) continue;
159
+ runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!)) });
160
+ }
161
+ return { routes, runs, levels: makeLevels(graph, root, routeOf, marks) };
162
+ },
163
+ };
164
+ return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
165
+ }
166
+
167
+ /**
168
+ * This function gives one zoom level for each depth of the tree, and a last level with each node alone.
169
+ * At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
170
+ * Its points are the part of the route of its first edge between the two items.
171
+ */
172
+ function makeLevels(
173
+ graph: Graph,
174
+ root: Section,
175
+ routeOf: ReadonlyMap<string, Route>,
176
+ marks: ReadonlyMap<string, ReadonlyMap<Section, number>>,
177
+ ): Level[] {
178
+ const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
179
+ const levels: Level[] = [];
180
+ const deepest = treeDepth(root);
181
+ for (let d = 0; d <= deepest + 1; d++) {
182
+ const items: LevelItem[] = [];
183
+ const itemOf: (Section | number)[] = [];
184
+ const boxOf = (list: readonly number[]): Box => {
185
+ let x0 = Infinity;
186
+ let y0 = Infinity;
187
+ let x1 = -Infinity;
188
+ let y1 = -Infinity;
189
+ for (const i of list) {
190
+ const b = graph.nodes[i]!.box;
191
+ x0 = Math.min(x0, b.x);
192
+ y0 = Math.min(y0, b.y);
193
+ x1 = Math.max(x1, b.x + b.w);
194
+ y1 = Math.max(y1, b.y + b.h);
195
+ }
196
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
197
+ };
198
+ const visit = (s: Section): void => {
199
+ if (s.depth >= d) {
200
+ items.push({ id: s.id, box: boxOf(s.nodes), nodes: s.nodes.map((i) => graph.nodes[i]!.id) });
201
+ for (const i of s.nodes) itemOf[i] = s;
202
+ } else if (s.children.length > 0) for (const c of s.children) visit(c);
203
+ else
204
+ for (const i of s.nodes) {
205
+ const n = graph.nodes[i]!;
206
+ items.push({ id: n.id, box: n.box, nodes: [n.id] });
207
+ itemOf[i] = i;
208
+ }
209
+ };
210
+ if (graph.nodes.length > 0) visit(root);
211
+ const links = new Map<string, { source: string; target: string; count: number; points: readonly Point[] }>();
212
+ for (const e of graph.edges) {
213
+ const iu = indexOfNode.get(e.source.node);
214
+ const iv = indexOfNode.get(e.target.node);
215
+ if (iu === undefined || iv === undefined) continue;
216
+ const a = itemOf[iu]!;
217
+ const b = itemOf[iv]!;
218
+ if (a === b) continue;
219
+ const ida = typeof a === "number" ? graph.nodes[a]!.id : a.id;
220
+ const idb = typeof b === "number" ? graph.nodes[b]!.id : b.id;
221
+ const key = `${ida}\u0000${idb}`;
222
+ const link = links.get(key);
223
+ if (link) {
224
+ link.count++;
225
+ continue;
226
+ }
227
+ const route = routeOf.get(e.id)?.points ?? [];
228
+ const mark = marks.get(e.id);
229
+ const from = typeof a === "number" ? 0 : (mark?.get(a) ?? 0);
230
+ const to = typeof b === "number" ? route.length - 1 : (mark?.get(b) ?? route.length - 1);
231
+ links.set(key, { source: ida, target: idb, count: 1, points: route.slice(from, to + 1) });
232
+ }
233
+ levels.push({ depth: d, items, links: [...links.values()] as LevelLink[] });
234
+ }
235
+ return levels;
236
+ }
@@ -0,0 +1,223 @@
1
+ import { center, dist, type Point } from "../geom.ts";
2
+ import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
3
+ import { placePlug, type Plug } from "../plugs.ts";
4
+ import { WayFinder, type WayOptions } from "../ways.ts";
5
+
6
+ /** The options of the router with shared ways. */
7
+ export interface SharedOptions extends WayOptions {
8
+ /** The name in the benchmark. */
9
+ readonly name: string;
10
+ /** The edges with one key share their ways. Without a rule, all edges share. An edge without a key shares with no edge. */
11
+ readonly rule?: GroupRule;
12
+ /** The cost of a link that an edge with the same key uses already, as a share of its length. The default is 0.5. */
13
+ readonly discount?: number;
14
+ /** The benchmark skips a fixture with more nodes. */
15
+ readonly maxNodes?: number;
16
+ }
17
+
18
+ interface Way {
19
+ readonly edge: Edge;
20
+ readonly key: string;
21
+ readonly ps: Plug;
22
+ readonly pt: Plug;
23
+ /** The way through the corner graph, from the point out of the source plug to the point out of the target plug. */
24
+ seq: Point[];
25
+ /** The index of each point of `seq` in the corner graph. The two ends get an index after the corners, one for each place. */
26
+ ids: number[];
27
+ found: boolean;
28
+ }
29
+
30
+ interface Run {
31
+ readonly key: string;
32
+ readonly first: number;
33
+ readonly last: number;
34
+ /** The points from the corner `first` to the corner `last`, without the needless corners. */
35
+ readonly points: Point[];
36
+ readonly ways: Way[];
37
+ }
38
+
39
+ const byText = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
40
+
41
+ /** The angle, from -π to π, of the turn at `base` from the direction `from` → `base` to the point `to`. */
42
+ function turn(from: Point, base: Point, to: Point): number {
43
+ const dx = base.x - from.x;
44
+ const dy = base.y - from.y;
45
+ const vx = to.x - base.x;
46
+ const vy = to.y - base.y;
47
+ return Math.atan2(dx * vy - dy * vx, dx * vx + dy * vy);
48
+ }
49
+
50
+ /**
51
+ * This function makes a router where edges share their ways (after Pupyrev, Nachmanson, Bereg and Holroyd, "Edge routing with ordered bundles").
52
+ * The edges go one by one through the corner graph. A link that an edge with the same key uses already costs less.
53
+ * Thus the edges of a key come together on common ways. Each stretch that several edges share has the same points in each route.
54
+ * The result gives each stretch as a run, with the order of its lanes.
55
+ */
56
+ export function sharedRouter(options: SharedOptions): Router {
57
+ const discount = Math.min(1, Math.max(0.05, options.discount ?? 0.5));
58
+ const keyOf = (e: Edge): string => {
59
+ if (!options.rule) return "*";
60
+ return options.rule(e) ?? `\u0000${e.id}`;
61
+ };
62
+ const router: Router = {
63
+ name: options.name,
64
+ route(graph: Graph): Result {
65
+ const byId = nodeIndex(graph);
66
+ const ways = new WayFinder(graph.nodes, options);
67
+ const corners = ways.corners;
68
+ const index = new Map<Point, number>();
69
+ corners.points.forEach((p, i) => index.set(p, i));
70
+ const M = corners.points.length + 1;
71
+ const link = (i: number, j: number): number => (i < j ? i * M + j : j * M + i);
72
+ const ends = new Map<string, number>();
73
+ const endId = (p: Point): number => {
74
+ const k = `${p.x},${p.y}`;
75
+ let id = ends.get(k);
76
+ if (id === undefined) ends.set(k, (id = corners.points.length + ends.size));
77
+ return id;
78
+ };
79
+
80
+ const all: Way[] = [];
81
+ for (const e of graph.edges) {
82
+ const s = byId.get(e.source.node);
83
+ const t = byId.get(e.target.node);
84
+ if (!s || !t) continue;
85
+ const ps = placePlug(s, e.source, center(t.box));
86
+ const pt = placePlug(t, e.target, center(s.box));
87
+ all.push({ edge: e, key: keyOf(e), ps, pt, seq: [], ids: [], found: false });
88
+ }
89
+
90
+ // The edges of one key go one after the other, the longest first, so that the short edges join the long ways.
91
+ const discounted = new Map<string, Map<number, string[]>>();
92
+ const order = [...all].sort(
93
+ (p, q) => byText(p.key, q.key) || dist(q.ps.point, q.pt.point) - dist(p.ps.point, p.pt.point) || byText(p.edge.id, q.edge.id),
94
+ );
95
+ for (const w of order) {
96
+ let used = discounted.get(w.key);
97
+ if (!used) discounted.set(w.key, (used = new Map()));
98
+ const mine = used;
99
+ const way = corners.path(ways.out(w.ps), ways.out(w.pt), (i, j, d) => (mine.has(link(i, j)) ? d * discount : d), discount);
100
+ if (!way) continue;
101
+ w.found = true;
102
+ w.seq = way;
103
+ w.ids = way.map((p, i) => (i === 0 || i === way.length - 1 ? endId(p) : index.get(p)!));
104
+ for (let i = 1; i < w.ids.length; i++) {
105
+ const a = w.ids[i - 1]!;
106
+ const b = w.ids[i]!;
107
+ if (a >= corners.points.length || b >= corners.points.length) continue;
108
+ const list = mine.get(link(a, b));
109
+ if (list) list.push(w.edge.id);
110
+ else mine.set(link(a, b), [w.edge.id]);
111
+ }
112
+ }
113
+
114
+ // The users of each link, with the links to the ends. Two edges from one plug share the link from it.
115
+ const users = new Map<string, Map<string, string[]>>();
116
+ for (const w of order) {
117
+ if (!w.found) continue;
118
+ let mine = users.get(w.key);
119
+ if (!mine) users.set(w.key, (mine = new Map()));
120
+ for (let i = 1; i < w.ids.length; i++) {
121
+ const a = w.ids[i - 1]!;
122
+ const b = w.ids[i]!;
123
+ const k = a < b ? `${a}:${b}` : `${b}:${a}`;
124
+ const list = mine.get(k);
125
+ if (list) list.push(w.edge.id);
126
+ else mine.set(k, [w.edge.id]);
127
+ }
128
+ }
129
+
130
+ // Each way breaks into stretches where the set of edges on the links stays the same.
131
+ // A shared stretch loses its needless corners one time, so it has the same points in each of its routes.
132
+ const runs = new Map<string, Run>();
133
+ const points = new Map<string, Point[]>();
134
+ for (const w of all) {
135
+ if (!w.found) continue;
136
+ const used = users.get(w.key)!;
137
+ const sig = (i: number): string => {
138
+ const a = w.ids[i]!;
139
+ const b = w.ids[i + 1]!;
140
+ const list = used.get(a < b ? `${a}:${b}` : `${b}:${a}`)!;
141
+ return list.length < 2 ? "" : [...list].sort(byText).join("\u0001");
142
+ };
143
+ const out: Point[] = [w.ps.point];
144
+ const append = (piece: readonly Point[]): void => {
145
+ for (const p of piece) if (dist(p, out[out.length - 1]!) > 1e-9) out.push(p);
146
+ };
147
+ let start = 0;
148
+ for (let i = 0; i < w.seq.length - 1; i++) {
149
+ const s = sig(i);
150
+ if (i + 1 < w.seq.length - 1 && sig(i + 1) === s) continue;
151
+ const raw = w.seq.slice(start, i + 2);
152
+ if (s === "") append(corners.pull(raw));
153
+ else {
154
+ const a = w.ids[start]!;
155
+ const b = w.ids[i + 1]!;
156
+ const id = `${s}\u0002${Math.min(a, b)}\u0002${Math.max(a, b)}`;
157
+ let run = runs.get(id);
158
+ if (!run) {
159
+ run = { key: w.key, first: Math.min(a, b), last: Math.max(a, b), points: corners.pull(a < b ? raw : [...raw].reverse()), ways: [] };
160
+ runs.set(id, run);
161
+ }
162
+ run.ways.push(w);
163
+ append(a < b ? run.points : [...run.points].reverse());
164
+ }
165
+ start = i + 1;
166
+ }
167
+ append([w.pt.point]);
168
+ points.set(w.edge.id, out);
169
+ }
170
+
171
+ const routes: Route[] = [];
172
+ for (const w of all) routes.push({ edge: w.edge.id, points: points.get(w.edge.id) ?? [w.ps.point, w.pt.point] });
173
+ const shared: SharedRun[] = [];
174
+ for (const run of runs.values()) {
175
+ shared.push({ id: `r${shared.length}`, key: run.key, points: run.points, edges: laneOrderOfRun(run) });
176
+ }
177
+ return { routes, runs: shared };
178
+ },
179
+ };
180
+ return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
181
+ }
182
+
183
+ /**
184
+ * This function gives the lane order of a run (metro-line order). Two edges of the run go together past its last corner
185
+ * until they part. The edge that turns more to the left there gets the lane more to the left.
186
+ * Two edges can part only before the first corner. Then the edge that comes from more to the left gets the lane more to the left.
187
+ */
188
+ function laneOrderOfRun(run: Run): string[] {
189
+ const oriented = new Map<Way, { seq: Point[]; ids: number[]; s: number; e: number }>();
190
+ for (const w of run.ways) {
191
+ const s0 = w.ids.indexOf(run.first);
192
+ const e0 = w.ids.indexOf(run.last);
193
+ if (s0 < e0) oriented.set(w, { seq: w.seq, ids: w.ids, s: s0, e: e0 });
194
+ else oriented.set(w, { seq: [...w.seq].reverse(), ids: [...w.ids].reverse(), s: w.seq.length - 1 - s0, e: w.seq.length - 1 - e0 });
195
+ }
196
+ const compare = (p: Way, q: Way): number => {
197
+ const P = oriented.get(p)!;
198
+ const Q = oriented.get(q)!;
199
+ for (let j = 1; ; j++) {
200
+ const a = P.seq[P.e + j];
201
+ const b = Q.seq[Q.e + j];
202
+ if (a === undefined || b === undefined) break;
203
+ if (P.ids[P.e + j] !== Q.ids[Q.e + j]) {
204
+ const d = turn(P.seq[P.e + j - 2]!, P.seq[P.e + j - 1]!, a) - turn(P.seq[P.e + j - 2]!, P.seq[P.e + j - 1]!, b);
205
+ if (d !== 0) return d;
206
+ break;
207
+ }
208
+ }
209
+ for (let j = 1; ; j++) {
210
+ const a = P.seq[P.s - j];
211
+ const b = Q.seq[Q.s - j];
212
+ if (a === undefined || b === undefined) break;
213
+ if (P.ids[P.s - j] !== Q.ids[Q.s - j]) {
214
+ // Seen backward, the left side is the right side. Thus the order turns around.
215
+ const d = turn(P.seq[P.s - j + 2]!, P.seq[P.s - j + 1]!, b) - turn(P.seq[P.s - j + 2]!, P.seq[P.s - j + 1]!, a);
216
+ if (d !== 0) return d;
217
+ break;
218
+ }
219
+ }
220
+ return byText(p.edge.id, q.edge.id);
221
+ };
222
+ return [...run.ways].sort(compare).map((w) => w.edge.id);
223
+ }
@@ -0,0 +1,37 @@
1
+ import { center, type Box, type Point } from "../geom.ts";
2
+ import { fixedPortPoint, nodeIndex, type Graph, type Result, type Route, type Router } from "../model.ts";
3
+
4
+ /** This function gives the point where the ray from the center of the box to `toward` leaves the box. */
5
+ export function boxExit(box: Box, toward: Point): Point {
6
+ const c = center(box);
7
+ const dx = toward.x - c.x;
8
+ const dy = toward.y - c.y;
9
+ if (dx === 0 && dy === 0) return c;
10
+ const tx = dx === 0 ? Infinity : box.w / 2 / Math.abs(dx);
11
+ const ty = dy === 0 ? Infinity : box.h / 2 / Math.abs(dy);
12
+ const t = Math.min(tx, ty);
13
+ return { x: c.x + dx * t, y: c.y + dy * t };
14
+ }
15
+
16
+ /**
17
+ * The control of the benchmark: each edge is one straight line between its plugs.
18
+ * A free plug goes where the line between the two centers leaves the box.
19
+ */
20
+ export const straightRouter: Router = {
21
+ name: "straight",
22
+ route(graph: Graph): Result {
23
+ const byId = nodeIndex(graph);
24
+ const routes: Route[] = [];
25
+ for (const e of graph.edges) {
26
+ const s = byId.get(e.source.node);
27
+ const t = byId.get(e.target.node);
28
+ if (!s || !t) continue;
29
+ const fs = fixedPortPoint(s, e.source);
30
+ const ft = fixedPortPoint(t, e.target);
31
+ const a = fs ?? boxExit(s.box, ft ?? center(t.box));
32
+ const b = ft ?? boxExit(t.box, fs ?? center(s.box));
33
+ routes.push({ edge: e.id, points: [a, b] });
34
+ }
35
+ return { routes };
36
+ },
37
+ };
@@ -0,0 +1,134 @@
1
+ import { center, dist, type Point } from "../geom.ts";
2
+ import { nodeIndex, type Bundle, type Edge, type Graph, type GroupRule, type Node, type Result, type Route, type Router } from "../model.ts";
3
+ import { placePlug, type Plug } from "../plugs.ts";
4
+ import { WayFinder, type WayOptions } from "../ways.ts";
5
+
6
+ /** The options of the trunk router. */
7
+ export interface TrunksOptions extends WayOptions {
8
+ /** The name in the benchmark. */
9
+ readonly name: string;
10
+ /** The grouping rule. The edges from one plug with one key share a trunk. */
11
+ readonly rule: GroupRule;
12
+ /** The split point is at this share of the way to the center of the targets. The default is 0.5. */
13
+ readonly split?: number;
14
+ /** A group with fewer edges stays unbundled. The default is 2. */
15
+ readonly minSize?: number;
16
+ /** The benchmark skips a fixture with more nodes. */
17
+ readonly maxNodes?: number;
18
+ }
19
+
20
+ /** The rule that groups the edges by the collection of their target, in `data.collection`. */
21
+ export const byCollection: GroupRule = (e) => {
22
+ const c = e.data?.["collection"];
23
+ return typeof c === "string" ? c : undefined;
24
+ };
25
+
26
+ /**
27
+ * This function groups the edges by their source plug and their key. Each group keeps the order of the edges.
28
+ * A group with fewer than `minSize` edges does not appear.
29
+ */
30
+ export function groupEdges(edges: readonly Edge[], rule: GroupRule, minSize = 2): Map<string, Edge[]> {
31
+ const all = new Map<string, Edge[]>();
32
+ for (const e of edges) {
33
+ const key = rule(e);
34
+ if (key === undefined) continue;
35
+ const id = `${e.source.node}\u0000${e.source.port ?? ""}\u0000${key}`;
36
+ const list = all.get(id);
37
+ if (list) list.push(e);
38
+ else all.set(id, [e]);
39
+ }
40
+ for (const [id, list] of all) if (list.length < minSize) all.delete(id);
41
+ return all;
42
+ }
43
+
44
+ /**
45
+ * This function gives the lane order of the branches that leave the split point `from`, with the trunk along `dir`.
46
+ * Lane 0 turns most to the left, in a frame with the y axis down. Thus two lanes do not cross at the split.
47
+ */
48
+ export function laneOrder(dir: Point, from: Point, branches: readonly { readonly edge: string; readonly next: Point }[]): string[] {
49
+ const angle = (p: Point): number => {
50
+ const vx = p.x - from.x;
51
+ const vy = p.y - from.y;
52
+ return Math.atan2(dir.x * vy - dir.y * vx, dir.x * vx + dir.y * vy);
53
+ };
54
+ return branches
55
+ .map((b) => ({ edge: b.edge, a: angle(b.next) }))
56
+ .sort((p, q) => p.a - q.a || (p.edge < q.edge ? -1 : p.edge > q.edge ? 1 : 0))
57
+ .map((x) => x.edge);
58
+ }
59
+
60
+ /**
61
+ * This function gives the split point of a bundle, on the line from `start` to the center of the targets.
62
+ * The point is at `split` of the way, but at most half of the way to the nearest target. It moves back toward `start` out of the boxes.
63
+ */
64
+ export function splitPoint(start: Point, targets: readonly Point[], split: number, free: (p: Point) => boolean, step: number): Point {
65
+ if (targets.length === 0) return start;
66
+ const c = { x: targets.reduce((s, p) => s + p.x, 0) / targets.length, y: targets.reduce((s, p) => s + p.y, 0) / targets.length };
67
+ const L = dist(start, c);
68
+ if (L === 0) return start;
69
+ let d = Math.min(split * L, 0.5 * Math.min(...targets.map((p) => dist(start, p))));
70
+ const at = (t: number): Point => ({ x: start.x + ((c.x - start.x) * t) / L, y: start.y + ((c.y - start.y) * t) / L });
71
+ while (d > 0 && !free(at(d))) d -= step;
72
+ return d > 0 ? at(d) : start;
73
+ }
74
+
75
+ const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
76
+
77
+ /**
78
+ * This function makes a router that bundles the edges by a rule. The edges of a group share one plug and one trunk
79
+ * to a split point. From there, each edge goes its own way. The router gives the bundles with their lane order.
80
+ * The edges outside the groups go as in the corner router.
81
+ */
82
+ export function trunksRouter(options: TrunksOptions): Router {
83
+ const router: Router = {
84
+ name: options.name,
85
+ route(graph: Graph): Result {
86
+ const byId = nodeIndex(graph);
87
+ const ways = new WayFinder(graph.nodes, options);
88
+ const points = new Map<string, Point[]>();
89
+ const bundles: Bundle[] = [];
90
+ for (const [id, group] of groupEdges(graph.edges, options.rule, options.minSize ?? 2)) {
91
+ const first = group[0]!;
92
+ const s = byId.get(first.source.node);
93
+ const targets = group.map((e) => byId.get(e.target.node));
94
+ if (!s || targets.some((t) => !t)) continue;
95
+ const centers = targets.map((t) => center(t!.box));
96
+ const c = { x: centers.reduce((a, p) => a + p.x, 0) / centers.length, y: centers.reduce((a, p) => a + p.y, 0) / centers.length };
97
+ const plug: Plug = placePlug(s, first.source, c);
98
+ const start = ways.out(plug);
99
+ const split = splitPoint(start, centers, options.split ?? 0.5, (p) => !ways.obstacles.inside(p, ways.margin), ways.margin);
100
+ const way = ways.fromPlug(plug, split);
101
+ if (!way) continue;
102
+ const trunk = dedupe(way);
103
+ const end = trunk[trunk.length - 1]!;
104
+ const prev = trunk.length >= 2 ? trunk[trunk.length - 2]! : plug.point;
105
+ const len = dist(prev, end);
106
+ const dir = len > 0 ? { x: (end.x - prev.x) / len, y: (end.y - prev.y) / len } : plug.normal;
107
+ const branches: { edge: string; next: Point }[] = [];
108
+ group.forEach((e, i) => {
109
+ const pt = placePlug(targets[i]!, e.target, end);
110
+ const branch = dedupe(ways.toPlug(end, pt) ?? [end, pt.point]);
111
+ points.set(e.id, dedupe([...trunk, ...branch.slice(1)]));
112
+ branches.push({ edge: e.id, next: branch[1] ?? pt.point });
113
+ });
114
+ bundles.push({ id: `b${bundles.length}`, key: id.split("\u0000")[2]!, node: s.id, edges: laneOrder(dir, end, branches), trunk });
115
+ }
116
+ const routes: Route[] = [];
117
+ for (const e of graph.edges) {
118
+ const s = byId.get(e.source.node);
119
+ const t = byId.get(e.target.node);
120
+ if (!s || !t) continue;
121
+ const known = points.get(e.id);
122
+ if (known) {
123
+ routes.push({ edge: e.id, points: known });
124
+ continue;
125
+ }
126
+ const ps = placePlug(s, e.source, center(t.box));
127
+ const pt = placePlug(t, e.target, center(s.box));
128
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
129
+ }
130
+ return { routes, bundles };
131
+ },
132
+ };
133
+ return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
134
+ }