@mark1russell7/highways 0.2.0 → 0.3.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.
- package/README.md +240 -8
- package/build/index.d.ts +1459 -78
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9315 -3411
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +145 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/suite.ts +781 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/index.ts +4 -0
- package/src/levels.ts +163 -5
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +129 -8
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +19 -1
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
package/src/routers/sections.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
|
-
import { center, dist, pointSegmentDistance, polylineLength, type
|
|
2
|
+
import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
|
|
3
3
|
import { LEVEL_LINK_DEFAULTS, levelLinks, type ItemPair, type LevelLinkOptions } from "../levels.ts";
|
|
4
|
-
import { nodeIndex, type Graph, type Level, type
|
|
4
|
+
import { nodeIndex, type Graph, type Level, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
5
5
|
import { placePlug } from "../plugs.ts";
|
|
6
|
-
import { allSections, buildSections, checkSections, SECTION_DEFAULTS, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
7
|
-
import { orderLanes } from "../segments.ts";
|
|
6
|
+
import { allSections, buildSections, checkSections, levelCut, SECTION_DEFAULTS, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
7
|
+
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
8
8
|
import type { Strategy } from "../strategies/model.ts";
|
|
9
9
|
import { WayFinder } from "../ways.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The options of the router with sections. Each option has a default, and the defaults give the output of version 0.1.5.
|
|
13
13
|
* The options `cap`, `maxDepth` and `collapse` are the limits of the default grouping rule, the quadtree of `buildSections`.
|
|
14
|
-
* The options `tightMargin` and `
|
|
14
|
+
* The options `tightMargin`, `plugInset` and `throughCost` are the limits of the level links (`levelLinks`).
|
|
15
15
|
*/
|
|
16
16
|
export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
17
17
|
/** The name in the benchmark. */
|
|
@@ -45,18 +45,21 @@ export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
|
45
45
|
* The default is each depth, from 0 to the depth of the deepest section.
|
|
46
46
|
*/
|
|
47
47
|
readonly levels?: readonly number[];
|
|
48
|
+
/** The lane order of the runs. With `run`, the default, each run gets its order alone. With `bundle`, two edges keep one order on each run of their common stretch. */
|
|
49
|
+
readonly laneOrder?: RunOrder;
|
|
48
50
|
/** The benchmark skips a fixture with more nodes. */
|
|
49
51
|
readonly maxNodes?: number;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** The default values of the numbers of `SectionsOptions`: the limits of the hubs, of the tree and of the level links. */
|
|
53
|
-
export const SECTIONS_DEFAULTS: Readonly<Required<Pick<SectionsOptions, "cap" | "maxDepth" | "collapse" | "tightMargin" | "plugInset" | "hubs" | "stretch" | "stretchPad" | "hubSlack">>> =
|
|
55
|
+
export const SECTIONS_DEFAULTS: Readonly<Required<Pick<SectionsOptions, "cap" | "maxDepth" | "collapse" | "tightMargin" | "plugInset" | "throughCost" | "hubs" | "stretch" | "stretchPad" | "hubSlack">>> =
|
|
54
56
|
Object.freeze({
|
|
55
57
|
cap: SECTION_DEFAULTS.cap,
|
|
56
58
|
maxDepth: SECTION_DEFAULTS.maxDepth,
|
|
57
59
|
collapse: SECTION_DEFAULTS.collapse,
|
|
58
60
|
tightMargin: LEVEL_LINK_DEFAULTS.tightMargin,
|
|
59
61
|
plugInset: LEVEL_LINK_DEFAULTS.plugInset,
|
|
62
|
+
throughCost: LEVEL_LINK_DEFAULTS.throughCost,
|
|
60
63
|
hubs: Infinity,
|
|
61
64
|
stretch: 2,
|
|
62
65
|
stretchPad: 4,
|
|
@@ -205,7 +208,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
205
208
|
for (const [key, p] of pieces) {
|
|
206
209
|
const list = users.get(key) ?? [];
|
|
207
210
|
if (list.length < 2 || p.length < 2) continue;
|
|
208
|
-
runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!)) });
|
|
211
|
+
runs.push({ id: `r${runs.length}`, key, points: p, edges: orderLanes(p, list.map((id) => routeOf.get(id)!), options.laneOrder) });
|
|
209
212
|
}
|
|
210
213
|
return { routes, runs, levels: makeLevels(graph, root, routeOf, options) };
|
|
211
214
|
},
|
|
@@ -235,45 +238,17 @@ function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Ro
|
|
|
235
238
|
const shown = options.levels && new Set(options.levels);
|
|
236
239
|
for (let d = 0; d <= deepest + 1; d++) {
|
|
237
240
|
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
238
|
-
const items
|
|
239
|
-
const itemOf: (Section | number)[] = [];
|
|
240
|
-
const boxOf = (list: readonly number[]): Box => {
|
|
241
|
-
let x0 = Infinity;
|
|
242
|
-
let y0 = Infinity;
|
|
243
|
-
let x1 = -Infinity;
|
|
244
|
-
let y1 = -Infinity;
|
|
245
|
-
for (const i of list) {
|
|
246
|
-
const b = graph.nodes[i]!.box;
|
|
247
|
-
x0 = Math.min(x0, b.x);
|
|
248
|
-
y0 = Math.min(y0, b.y);
|
|
249
|
-
x1 = Math.max(x1, b.x + b.w);
|
|
250
|
-
y1 = Math.max(y1, b.y + b.h);
|
|
251
|
-
}
|
|
252
|
-
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
253
|
-
};
|
|
254
|
-
const visit = (s: Section): void => {
|
|
255
|
-
if (s.depth >= d) {
|
|
256
|
-
items.push({ id: s.id, box: boxOf(s.nodes), nodes: s.nodes.map((i) => graph.nodes[i]!.id) });
|
|
257
|
-
for (const i of s.nodes) itemOf[i] = s;
|
|
258
|
-
} else if (s.children.length > 0) for (const c of s.children) visit(c);
|
|
259
|
-
else
|
|
260
|
-
for (const i of s.nodes) {
|
|
261
|
-
const n = graph.nodes[i]!;
|
|
262
|
-
items.push({ id: n.id, box: n.box, nodes: [n.id] });
|
|
263
|
-
itemOf[i] = i;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
if (graph.nodes.length > 0) visit(root);
|
|
241
|
+
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
267
242
|
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
268
243
|
for (const e of graph.edges) {
|
|
269
244
|
const iu = indexOfNode.get(e.source.node);
|
|
270
245
|
const iv = indexOfNode.get(e.target.node);
|
|
271
246
|
if (iu === undefined || iv === undefined) continue;
|
|
272
|
-
const a =
|
|
273
|
-
const b =
|
|
247
|
+
const a = owner[iu]!;
|
|
248
|
+
const b = owner[iv]!;
|
|
274
249
|
if (a === b) continue;
|
|
275
|
-
const ida =
|
|
276
|
-
const idb =
|
|
250
|
+
const ida = items[a]!.id;
|
|
251
|
+
const idb = items[b]!.id;
|
|
277
252
|
const key = `${ida}\u0000${idb}`;
|
|
278
253
|
const link = links.get(key);
|
|
279
254
|
if (link) {
|
package/src/routers/shared.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { center, dist, type Point } from "../geom.ts";
|
|
2
2
|
import { nodeIndex, type Edge, type Graph, type GroupRule, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
3
3
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
|
+
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
4
5
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
5
6
|
|
|
6
7
|
/** The options of the router with shared ways. */
|
|
@@ -11,6 +12,8 @@ export interface SharedOptions extends WayOptions {
|
|
|
11
12
|
readonly rule?: GroupRule;
|
|
12
13
|
/** 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
14
|
readonly discount?: number;
|
|
15
|
+
/** The lane order of the runs. With `bundle`, the default, two edges keep one order on each run of their common stretch. With `run`, each run gets its order alone. */
|
|
16
|
+
readonly laneOrder?: RunOrder;
|
|
14
17
|
/** The benchmark skips a fixture with more nodes. */
|
|
15
18
|
readonly maxNodes?: number;
|
|
16
19
|
}
|
|
@@ -171,8 +174,10 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
171
174
|
const routes: Route[] = [];
|
|
172
175
|
for (const w of all) routes.push({ edge: w.edge.id, points: points.get(w.edge.id) ?? [w.ps.point, w.pt.point] });
|
|
173
176
|
const shared: SharedRun[] = [];
|
|
177
|
+
const routeOf = new Map(routes.map((r) => [r.edge, r]));
|
|
174
178
|
for (const run of runs.values()) {
|
|
175
|
-
|
|
179
|
+
const edges = options.laneOrder === "run" ? laneOrderOfRun(run) : orderLanes(run.points, run.ways.map((w) => routeOf.get(w.edge.id)!), "bundle");
|
|
180
|
+
shared.push({ id: `r${shared.length}`, key: run.key, points: run.points, edges });
|
|
176
181
|
}
|
|
177
182
|
return { routes, runs: shared };
|
|
178
183
|
},
|
|
@@ -181,7 +186,7 @@ export function sharedRouter(options: SharedOptions): Router {
|
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
/**
|
|
184
|
-
* This function gives the lane order of a run (metro-line order). Two edges of the run go together past its last corner
|
|
189
|
+
* This function gives the lane order of a run with the option `laneOrder: "run"` (metro-line order). Two edges of the run go together past its last corner
|
|
185
190
|
* until they part. The edge that turns more to the left there gets the lane more to the left.
|
|
186
191
|
* 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
192
|
*/
|
package/src/routers/trunks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
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";
|
|
2
|
+
import { nodeIndex, type Bundle, type Edge, type Graph, type GroupRule, type Node, type Result, type Route, type Router, type SharedRun } from "../model.ts";
|
|
3
3
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
4
|
+
import { orderLanes, type RunOrder } from "../segments.ts";
|
|
4
5
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
5
6
|
|
|
6
7
|
/** The options of the trunk router. */
|
|
@@ -13,10 +14,20 @@ export interface TrunksOptions extends WayOptions {
|
|
|
13
14
|
readonly split?: number;
|
|
14
15
|
/** A group with fewer edges stays unbundled. The default is 2. */
|
|
15
16
|
readonly minSize?: number;
|
|
17
|
+
/** The lane order of a trunk. With `bundle`, the default, the branches part in lane order. With `run`, the first leg of each branch tells the order. */
|
|
18
|
+
readonly laneOrder?: RunOrder;
|
|
19
|
+
/**
|
|
20
|
+
* The place where each lane leaves the trunk. With `part`, the default, a lane leaves where its way parts from the ways of the other lanes.
|
|
21
|
+
* A lane that turns back over the trunk leaves at an earlier point of the trunk. With `split`, each lane leaves at the split point, as in version 0.2.0.
|
|
22
|
+
*/
|
|
23
|
+
readonly leave?: TrunkLeave;
|
|
16
24
|
/** The benchmark skips a fixture with more nodes. */
|
|
17
25
|
readonly maxNodes?: number;
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/** The place where the lanes of a trunk leave it: where their ways part, or at the split point. */
|
|
29
|
+
export type TrunkLeave = "part" | "split";
|
|
30
|
+
|
|
20
31
|
/** The rule that groups the edges by the collection of their target, in `data.collection`. */
|
|
21
32
|
export const byCollection: GroupRule = (e) => {
|
|
22
33
|
const c = e.data?.["collection"];
|
|
@@ -74,6 +85,83 @@ export function splitPoint(start: Point, targets: readonly Point[], split: numbe
|
|
|
74
85
|
|
|
75
86
|
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
|
|
76
87
|
|
|
88
|
+
/** The place where a branch leaves its trunk. */
|
|
89
|
+
export interface TrunkExit {
|
|
90
|
+
/** The place along the trunk: the index of a point of the trunk, plus the share of the next leg. Point `i` of the trunk is at `i`. */
|
|
91
|
+
readonly at: number;
|
|
92
|
+
/** The point where the branch leaves the trunk. */
|
|
93
|
+
readonly point: Point;
|
|
94
|
+
/** The branch past `point`, to the plug of the target. */
|
|
95
|
+
readonly rest: readonly Point[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* This function gives the place where a branch leaves its trunk. The branch starts at the last point of the trunk and ends at the plug of the target.
|
|
100
|
+
*
|
|
101
|
+
* A branch that turns back over the trunk, by more than 90 degrees, leaves at the point of the trunk before.
|
|
102
|
+
* It does so when it goes back along the trunk to that point. It also does so when `clear` tells that the line from that point is free.
|
|
103
|
+
* The first two legs of the trunk stay. When the branch still turns back, it leaves the last leg at the foot of its next point, if the line from there is free.
|
|
104
|
+
* Each step makes the route shorter or keeps its length.
|
|
105
|
+
*
|
|
106
|
+
* The function gives `undefined` when the foot on the second leg of the trunk is less than `min` past the start of that leg.
|
|
107
|
+
* Then the target is behind or beside the plug, and the edge does not go well in the bundle.
|
|
108
|
+
*/
|
|
109
|
+
export function leaveTrunk(trunk: readonly Point[], branch: readonly Point[], clear: (a: Point, b: Point) => boolean, min = 0): TrunkExit | undefined {
|
|
110
|
+
const back = (a: Point, b: Point, q: Point): boolean => (b.x - a.x) * (q.x - b.x) + (b.y - a.y) * (q.y - b.y) < 0;
|
|
111
|
+
let at = trunk.length - 1;
|
|
112
|
+
let rest = branch.slice(1);
|
|
113
|
+
while (at >= 3 && rest.length >= 2) {
|
|
114
|
+
const a = trunk[at - 1]!;
|
|
115
|
+
if (!back(a, trunk[at]!, rest[0]!)) break;
|
|
116
|
+
const next = dist(rest[0]!, a) <= 1e-9 ? rest.slice(1) : rest;
|
|
117
|
+
if (next === rest && !clear(a, rest[0]!)) break;
|
|
118
|
+
rest = next;
|
|
119
|
+
at -= 1;
|
|
120
|
+
}
|
|
121
|
+
const exit: TrunkExit = { at, point: trunk[at]!, rest };
|
|
122
|
+
if (at < 1 || rest.length < 2 || !back(trunk[at - 1]!, trunk[at]!, rest[0]!)) return exit;
|
|
123
|
+
const a = trunk[at - 1]!;
|
|
124
|
+
const b = trunk[at]!;
|
|
125
|
+
const q = rest[0]!;
|
|
126
|
+
const u = ((q.x - a.x) * (b.x - a.x) + (q.y - a.y) * (b.y - a.y)) / ((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
|
|
127
|
+
if (at <= 2 ? u * Math.hypot(b.x - a.x, b.y - a.y) < min : u <= 0) return at <= 2 ? undefined : exit;
|
|
128
|
+
const foot = { x: a.x + u * (b.x - a.x), y: a.y + u * (b.y - a.y) };
|
|
129
|
+
return at >= 2 && clear(foot, q) ? { at: at - 1 + u, point: foot, rest } : exit;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** This function gives the number of points at the start of each route that all the routes share. */
|
|
133
|
+
function commonStart(routes: readonly Route[], from: number): number {
|
|
134
|
+
const first = routes[0]!.points;
|
|
135
|
+
let n = from;
|
|
136
|
+
while (n < first.length && routes.every((r) => n < r.points.length && dist(r.points[n]!, first[n]!) <= 1e-9)) n++;
|
|
137
|
+
return n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* This function gives the shared ways of the routes of one bundle past the trunk. The routes share their first `from` points.
|
|
142
|
+
* At the last of these points, the routes part. The routes that go on to one next point share a way to the point where they part again.
|
|
143
|
+
* Each shared way has two edges or more, in the order of the routes.
|
|
144
|
+
*/
|
|
145
|
+
export function sharedWays(routes: readonly Route[], from: number): { points: Point[]; edges: string[] }[] {
|
|
146
|
+
const out: { points: Point[]; edges: string[] }[] = [];
|
|
147
|
+
const next = new Map<string, Route[]>();
|
|
148
|
+
for (const r of routes) {
|
|
149
|
+
const q = r.points[from];
|
|
150
|
+
if (!q) continue;
|
|
151
|
+
const key = `${q.x},${q.y}`;
|
|
152
|
+
const list = next.get(key);
|
|
153
|
+
if (list) list.push(r);
|
|
154
|
+
else next.set(key, [r]);
|
|
155
|
+
}
|
|
156
|
+
for (const list of next.values()) {
|
|
157
|
+
if (list.length < 2) continue;
|
|
158
|
+
const end = commonStart(list, from);
|
|
159
|
+
out.push({ points: list[0]!.points.slice(from - 1, end), edges: list.map((r) => r.edge) });
|
|
160
|
+
out.push(...sharedWays(list, end));
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
77
165
|
/**
|
|
78
166
|
* This function makes a router that bundles the edges by a rule. The edges of a group share one plug and one trunk
|
|
79
167
|
* to a split point. From there, each edge goes its own way. The router gives the bundles with their lane order.
|
|
@@ -85,8 +173,12 @@ export function trunksRouter(options: TrunksOptions): Router {
|
|
|
85
173
|
route(graph: Graph): Result {
|
|
86
174
|
const byId = nodeIndex(graph);
|
|
87
175
|
const ways = new WayFinder(graph.nodes, options);
|
|
88
|
-
const points = new Map<string, Point[]>();
|
|
176
|
+
const points = new Map<string, readonly Point[]>();
|
|
89
177
|
const bundles: Bundle[] = [];
|
|
178
|
+
const runs: SharedRun[] = [];
|
|
179
|
+
const part = (options.leave ?? "part") === "part";
|
|
180
|
+
// A branch leaves the trunk early only on a line that keeps out of the boxes as far as the corner graph does.
|
|
181
|
+
const clear = (a: Point, b: Point): boolean => ways.obstacles.clear(a, b, ways.margin / 2);
|
|
90
182
|
for (const [id, group] of groupEdges(graph.edges, options.rule, options.minSize ?? 2)) {
|
|
91
183
|
const first = group[0]!;
|
|
92
184
|
const s = byId.get(first.source.node);
|
|
@@ -105,13 +197,46 @@ export function trunksRouter(options: TrunksOptions): Router {
|
|
|
105
197
|
const len = dist(prev, end);
|
|
106
198
|
const dir = len > 0 ? { x: (end.x - prev.x) / len, y: (end.y - prev.y) / len } : plug.normal;
|
|
107
199
|
const branches: { edge: string; next: Point }[] = [];
|
|
200
|
+
const own: Route[] = [];
|
|
201
|
+
const exits: { edge: string; exit: TrunkExit }[] = [];
|
|
108
202
|
group.forEach((e, i) => {
|
|
109
203
|
const pt = placePlug(targets[i]!, e.target, end);
|
|
110
204
|
const branch = dedupe(ways.toPlug(end, pt) ?? [end, pt.point]);
|
|
111
|
-
|
|
112
|
-
|
|
205
|
+
if (!part) {
|
|
206
|
+
own.push({ edge: e.id, points: dedupe([...trunk, ...branch.slice(1)]) });
|
|
207
|
+
branches.push({ edge: e.id, next: branch[1] ?? pt.point });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// An edge whose target is behind the plug goes alone, as an edge outside the groups.
|
|
211
|
+
const exit = leaveTrunk(trunk, branch, clear, 2 * ways.margin);
|
|
212
|
+
if (exit) exits.push({ edge: e.id, exit });
|
|
113
213
|
});
|
|
114
|
-
|
|
214
|
+
const key = id.split("\u0000")[2]!;
|
|
215
|
+
if (!part) {
|
|
216
|
+
for (const r of own) points.set(r.edge, r.points);
|
|
217
|
+
const edges = options.laneOrder === "run" || trunk.length < 2 ? laneOrder(dir, end, branches) : orderLanes(trunk, own, "bundle");
|
|
218
|
+
bundles.push({ id: `b${bundles.length}`, key, node: s.id, edges, trunk });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// A group that keeps fewer than `minSize` edges stays unbundled.
|
|
222
|
+
if (exits.length < (options.minSize ?? 2)) continue;
|
|
223
|
+
// The trunk gets a point at each place where a branch leaves it. Thus each route holds the points of the trunk up to its exit.
|
|
224
|
+
const stops = new Map<number, Point>(trunk.map((p, i) => [i, p]));
|
|
225
|
+
for (const { exit } of exits) if (!stops.has(exit.at)) stops.set(exit.at, exit.point);
|
|
226
|
+
const line = [...stops].sort((x, y) => x[0] - y[0]);
|
|
227
|
+
for (const { edge, exit } of exits) {
|
|
228
|
+
const p = dedupe([...line.filter(([at]) => at <= exit.at).map(([, q]) => q), ...exit.rest]);
|
|
229
|
+
points.set(edge, p);
|
|
230
|
+
own.push({ edge, points: p });
|
|
231
|
+
}
|
|
232
|
+
// The bundle keeps the points that all the routes share. Past them, the routes that go on together share a run.
|
|
233
|
+
const order = options.laneOrder ?? "bundle";
|
|
234
|
+
const common = own[0]!.points.slice(0, commonStart(own, 0));
|
|
235
|
+
bundles.push({ id: `b${bundles.length}`, key, node: s.id, edges: orderLanes(common, own, order), trunk: common });
|
|
236
|
+
for (const way of sharedWays(own, common.length)) {
|
|
237
|
+
const users = own.filter((r) => way.edges.includes(r.edge));
|
|
238
|
+
runs.push({ id: `r${runs.length}`, key, points: way.points, edges: orderLanes(way.points, users, order) });
|
|
239
|
+
}
|
|
115
240
|
}
|
|
116
241
|
const routes: Route[] = [];
|
|
117
242
|
for (const e of graph.edges) {
|
|
@@ -127,7 +252,7 @@ export function trunksRouter(options: TrunksOptions): Router {
|
|
|
127
252
|
const pt = placePlug(t, e.target, center(s.box));
|
|
128
253
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
129
254
|
}
|
|
130
|
-
return { routes, bundles };
|
|
255
|
+
return runs.length > 0 ? { routes, bundles, runs } : { routes, bundles };
|
|
131
256
|
},
|
|
132
257
|
};
|
|
133
258
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
package/src/sections.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { center, type Box } from "./geom.ts";
|
|
2
|
-
import type { Node } from "./model.ts";
|
|
2
|
+
import type { LevelItem, Node } from "./model.ts";
|
|
3
3
|
|
|
4
4
|
/** A section of the plane: a cell of a quadtree, with the nodes whose centers are in it. */
|
|
5
5
|
export interface Section {
|
|
@@ -123,3 +123,49 @@ export function checkSections(root: Section, count: number): void {
|
|
|
123
123
|
const lost = seen.findIndex((n) => n !== 1);
|
|
124
124
|
if (lost >= 0) fail(root, `the node index ${lost} is in ${seen[lost]} leaves, not in one`);
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/** The items of one zoom level of a tree of sections, and the item of each node. */
|
|
128
|
+
export interface LevelCut {
|
|
129
|
+
/** The items of the level: a section of the depth of the level, or one node of a leaf above that depth. */
|
|
130
|
+
readonly items: LevelItem[];
|
|
131
|
+
/** The index in `items` of the item of each node, in the order of the nodes. */
|
|
132
|
+
readonly owner: readonly number[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* This function gives the items of the zoom level at `depth`. A section of that depth is one item, with the bounding box of its nodes.
|
|
137
|
+
* A node of a leaf above that depth is an item alone. The items come in the order of the walk of the tree, parents before children.
|
|
138
|
+
* A depth past the depth of the deepest section gives each node alone.
|
|
139
|
+
*/
|
|
140
|
+
export function levelCut(nodes: readonly Pick<Node, "id" | "box">[], root: Section, depth: number): LevelCut {
|
|
141
|
+
const items: LevelItem[] = [];
|
|
142
|
+
const owner: number[] = [];
|
|
143
|
+
const boxOf = (list: readonly number[]): Box => {
|
|
144
|
+
let x0 = Infinity;
|
|
145
|
+
let y0 = Infinity;
|
|
146
|
+
let x1 = -Infinity;
|
|
147
|
+
let y1 = -Infinity;
|
|
148
|
+
for (const i of list) {
|
|
149
|
+
const b = nodes[i]!.box;
|
|
150
|
+
x0 = Math.min(x0, b.x);
|
|
151
|
+
y0 = Math.min(y0, b.y);
|
|
152
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
153
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
154
|
+
}
|
|
155
|
+
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
156
|
+
};
|
|
157
|
+
const visit = (s: Section): void => {
|
|
158
|
+
if (s.depth >= depth) {
|
|
159
|
+
for (const i of s.nodes) owner[i] = items.length;
|
|
160
|
+
items.push({ id: s.id, box: boxOf(s.nodes), nodes: s.nodes.map((i) => nodes[i]!.id) });
|
|
161
|
+
} else if (s.children.length > 0) for (const c of s.children) visit(c);
|
|
162
|
+
else
|
|
163
|
+
for (const i of s.nodes) {
|
|
164
|
+
const n = nodes[i]!;
|
|
165
|
+
owner[i] = items.length;
|
|
166
|
+
items.push({ id: n.id, box: n.box, nodes: [n.id] });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (nodes.length > 0) visit(root);
|
|
170
|
+
return { items, owner };
|
|
171
|
+
}
|
package/src/segments.ts
CHANGED
|
@@ -203,13 +203,77 @@ function turn(from: Point, base: Point, to: Point): number {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* A route that does not hold the run goes last.
|
|
206
|
+
* The lane order of the shared ways of a router.
|
|
207
|
+
* With `bundle`, two edges keep one order on all the shared ways of their common stretch.
|
|
208
|
+
* With `run`, each shared way gets its order alone. Two edges can change sides from one shared way to the next.
|
|
210
209
|
*/
|
|
211
|
-
export
|
|
212
|
-
|
|
210
|
+
export type RunOrder = "bundle" | "run";
|
|
211
|
+
|
|
212
|
+
/** The place of a route on a run. The points go in the direction of the run. `s` and `e` are the indexes of the ends of the run. */
|
|
213
|
+
interface Placed {
|
|
214
|
+
readonly edge: string;
|
|
215
|
+
readonly p: readonly Point[];
|
|
216
|
+
readonly s: number;
|
|
217
|
+
readonly e: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The place where two routes part past one end of their common stretch, and the side of the first route there. */
|
|
221
|
+
interface Parting {
|
|
222
|
+
/** The last common point. */
|
|
223
|
+
readonly at: Point;
|
|
224
|
+
/** This value is below 0 when the first route goes to the left of the second route. It is above 0 when it goes to the right. */
|
|
225
|
+
readonly side: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** This function gives the place where the routes `P` and `Q` part after the last point of the run, or nothing. */
|
|
229
|
+
function partAfter(P: Placed, Q: Placed): Parting | undefined {
|
|
230
|
+
for (let j = 1; ; j++) {
|
|
231
|
+
const a = P.p[P.e + j];
|
|
232
|
+
const b = Q.p[Q.e + j];
|
|
233
|
+
if (a === undefined || b === undefined) return undefined;
|
|
234
|
+
if (dist(a, b) > SAME) {
|
|
235
|
+
const from = P.p[P.e + j - 2]!;
|
|
236
|
+
const at = P.p[P.e + j - 1]!;
|
|
237
|
+
const side = turn(from, at, a) - turn(from, at, b);
|
|
238
|
+
return side === 0 ? undefined : { at, side };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** This function gives the place where the routes `P` and `Q` part before the first point of the run, or nothing. */
|
|
244
|
+
function partBefore(P: Placed, Q: Placed): Parting | undefined {
|
|
245
|
+
for (let j = 1; ; j++) {
|
|
246
|
+
const a = P.p[P.s - j];
|
|
247
|
+
const b = Q.p[Q.s - j];
|
|
248
|
+
if (a === undefined || b === undefined) return undefined;
|
|
249
|
+
if (dist(a, b) > SAME) {
|
|
250
|
+
const from = P.p[P.s - j + 2]!;
|
|
251
|
+
const at = P.p[P.s - j + 1]!;
|
|
252
|
+
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
253
|
+
const side = turn(from, at, b) - turn(from, at, a);
|
|
254
|
+
return side === 0 ? undefined : { at, side };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const byEdge = (P: Placed, Q: Placed): number => (P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0);
|
|
260
|
+
|
|
261
|
+
/** This function puts the point `a` before the point `b`: the smaller x first, then the smaller y. */
|
|
262
|
+
const firstPoint = (a: Point, b: Point): boolean => a.x < b.x - SAME || (Math.abs(a.x - b.x) <= SAME && a.y < b.y);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This function gives the lane order of the edges on a shared way `run` (metro-line order).
|
|
266
|
+
* The routes are polylines that hold the points of the run in a row, in either direction. A route that does not hold the run goes last.
|
|
267
|
+
*
|
|
268
|
+
* Two routes go on together past each end of the run until they part. The route that turns more to the left there gets the lane more to the left.
|
|
269
|
+
*
|
|
270
|
+
* With the order `run`, the default, the end after the last point of the run tells the side first.
|
|
271
|
+
* With the order `bundle`, two routes that part on one side at both ends keep that side.
|
|
272
|
+
* When the two ends disagree, the two routes must cross. The end with the smaller x, then the smaller y, tells the side.
|
|
273
|
+
* Thus each shared way of a common stretch gives two routes the same order, and the crossing is where they part.
|
|
274
|
+
*/
|
|
275
|
+
export function orderLanes(run: readonly Point[], routes: readonly Route[], order: RunOrder = "run"): string[] {
|
|
276
|
+
const placed: Placed[] = [];
|
|
213
277
|
const lost: string[] = [];
|
|
214
278
|
for (const r of routes) {
|
|
215
279
|
let at = indexOf(r.points, run, 0);
|
|
@@ -221,30 +285,16 @@ export function orderLanes(run: readonly Point[], routes: readonly Route[]): str
|
|
|
221
285
|
else lost.push(r.edge);
|
|
222
286
|
}
|
|
223
287
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
for (let j = 1; ; j++) {
|
|
237
|
-
const a = P.p[P.s - j];
|
|
238
|
-
const b = Q.p[Q.s - j];
|
|
239
|
-
if (a === undefined || b === undefined) break;
|
|
240
|
-
if (!same(a, b)) {
|
|
241
|
-
// Seen backward, the left side is the right side. Thus the order turns around.
|
|
242
|
-
const d = turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, b) - turn(P.p[P.s - j + 2]!, P.p[P.s - j + 1]!, a);
|
|
243
|
-
if (d !== 0) return d;
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return P.edge < Q.edge ? -1 : P.edge > Q.edge ? 1 : 0;
|
|
248
|
-
});
|
|
288
|
+
if (order === "run") placed.sort((P, Q) => partAfter(P, Q)?.side ?? partBefore(P, Q)?.side ?? byEdge(P, Q));
|
|
289
|
+
else {
|
|
290
|
+
// The sort starts from the order of the edges, so that each run sorts the same edges the same way.
|
|
291
|
+
placed.sort(byEdge);
|
|
292
|
+
placed.sort((P, Q) => {
|
|
293
|
+
const after = partAfter(P, Q);
|
|
294
|
+
const before = partBefore(P, Q);
|
|
295
|
+
if (after && before && Math.sign(after.side) !== Math.sign(before.side)) return firstPoint(after.at, before.at) ? after.side : before.side;
|
|
296
|
+
return after?.side ?? before?.side ?? byEdge(P, Q);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
249
299
|
return [...placed.map((x) => x.edge), ...lost.sort()];
|
|
250
300
|
}
|