@mark1russell7/highways 0.1.5 → 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 +293 -8
- package/build/index.d.ts +1774 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9307 -3090
- 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/sections.ts +121 -0
- 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 +4 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +7 -0
- package/src/levels.ts +178 -9
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- 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 +78 -55
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +93 -4
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +153 -7
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +29 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ItemPair } from "../../levels.ts";
|
|
2
|
+
import type { LevelItem } from "../../model.ts";
|
|
3
|
+
import { levelCut, treeDepth, type Section } from "../../sections.ts";
|
|
4
|
+
import { boardGaps, openGaps, roomDemand, withBends, type Boxed, type Gap, type RoomLink, type RoomOptions } from "../index.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The boards of the zoom levels of `levelRoom`.
|
|
8
|
+
*
|
|
9
|
+
* - `shared`: each level uses the board of the nearest level. No box moves between two levels. A far level gets the area of the nearest level.
|
|
10
|
+
* - `each`: each level has its own board. A far level opens only the gaps of its own links, and a nearer level adds to them.
|
|
11
|
+
* Thus the items of a far level are nearer together, but a box moves in its parent when the level changes.
|
|
12
|
+
*/
|
|
13
|
+
export type LevelBoard = "each" | "shared";
|
|
14
|
+
|
|
15
|
+
/** The options of `levelRoom`: the options of `roomDemand` for each level, and the levels that get room. */
|
|
16
|
+
export interface LevelRoomOptions extends RoomOptions {
|
|
17
|
+
/** The depths of the tree that get room for their links. The default is each depth from 1 to the depth of the deepest section. */
|
|
18
|
+
readonly depths?: readonly number[];
|
|
19
|
+
/** With `true`, the last level, with each node alone, also gets room for the links between the nodes. The default is `false`. */
|
|
20
|
+
readonly nodes?: boolean;
|
|
21
|
+
/** The board of each level. The default is `shared`. */
|
|
22
|
+
readonly board?: LevelBoard;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One zoom level of `levelRoom`: its items at their open places, its links and its demand. */
|
|
26
|
+
export interface RoomLevel<T extends Boxed> {
|
|
27
|
+
readonly depth: number;
|
|
28
|
+
/** The items of the level, with the bounding box of their nodes at the places of this level. */
|
|
29
|
+
readonly items: LevelItem[];
|
|
30
|
+
/** The links between the items of the level. A link counts the links of the nodes between its two items. */
|
|
31
|
+
readonly pairs: ItemPair[];
|
|
32
|
+
/** The demand of the level on the board that it gets from the level before it. A level without room has no gaps. */
|
|
33
|
+
readonly gaps: Gap[];
|
|
34
|
+
/** The nodes at the places of this level. */
|
|
35
|
+
readonly nodes: T[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The room of each zoom level, and the board of the nearest level. */
|
|
39
|
+
export interface LevelRoom<T extends Boxed> {
|
|
40
|
+
/** The levels from depth 0 to the level with each node alone, in order. */
|
|
41
|
+
readonly levels: RoomLevel<T>[];
|
|
42
|
+
/** The gaps of the board of the nodes. `openGaps(nodes, gaps)` gives the nodes of the nearest level. */
|
|
43
|
+
readonly gaps: Gap[];
|
|
44
|
+
/** The nodes of the nearest level. */
|
|
45
|
+
readonly items: T[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_SNAP = 0.5;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* This function gives the links between the items of a level. Each pair of items gets one link, in the order of the first link of the pair.
|
|
52
|
+
* The count of a link is the sum of the counts of its node links. A link inside one item does not count.
|
|
53
|
+
*/
|
|
54
|
+
export function levelPairs(items: readonly LevelItem[], owner: readonly number[], indexOf: ReadonlyMap<string, number>, links: readonly RoomLink[]): ItemPair[] {
|
|
55
|
+
const byPair = new Map<string, { source: string; target: string; count: number }>();
|
|
56
|
+
for (const l of links) {
|
|
57
|
+
const iu = indexOf.get(l.source);
|
|
58
|
+
const iv = indexOf.get(l.target);
|
|
59
|
+
if (iu === undefined || iv === undefined) continue;
|
|
60
|
+
const a = owner[iu]!;
|
|
61
|
+
const b = owner[iv]!;
|
|
62
|
+
if (a === b) continue;
|
|
63
|
+
const key = a < b ? `${a} ${b}` : `${b} ${a}`;
|
|
64
|
+
const known = byPair.get(key);
|
|
65
|
+
if (known) known.count += l.count ?? 1;
|
|
66
|
+
else byPair.set(key, { source: items[a]!.id, target: items[b]!.id, count: l.count ?? 1 });
|
|
67
|
+
}
|
|
68
|
+
return [...byPair.values()];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This function gives the room of each zoom level of a tree of sections. The indices of the nodes of the tree are the indices of `nodes`.
|
|
73
|
+
* The far levels open first, and each nearer level keeps their growth.
|
|
74
|
+
*
|
|
75
|
+
* At each level, `roomDemand` measures the links of that level on the board of the level before it. Then the nodes move with `openGaps`.
|
|
76
|
+
* The nodes move by one monotone shift on each axis, so each level keeps the order and the alignment of the boxes.
|
|
77
|
+
* A nearer level only adds growth, so no gap of a far level gets narrower, and a box moves with its parent.
|
|
78
|
+
*/
|
|
79
|
+
export function levelRoom<T extends Boxed>(nodes: readonly T[], links: readonly RoomLink[], root: Section, options: LevelRoomOptions = {}): LevelRoom<T> {
|
|
80
|
+
const snap = options.snap ?? DEFAULT_SNAP;
|
|
81
|
+
const deepest = treeDepth(root);
|
|
82
|
+
const depths = new Set(options.depths ?? Array.from({ length: deepest }, (_, i) => i + 1));
|
|
83
|
+
if (options.nodes) depths.add(deepest + 1);
|
|
84
|
+
const indexOf = new Map(nodes.map((n, i) => [n.id, i]));
|
|
85
|
+
const levels: RoomLevel<T>[] = [];
|
|
86
|
+
let current: T[] = [...nodes];
|
|
87
|
+
// The edges of the bends join nodes, not the items of a level. So the bends get room on the board of the nodes, at the last level.
|
|
88
|
+
const { bends, ...room } = options;
|
|
89
|
+
for (let d = 0; d <= deepest + 1; d++) {
|
|
90
|
+
let gaps: Gap[] = [];
|
|
91
|
+
let cut = levelCut(current, root, d);
|
|
92
|
+
const pairs = levelPairs(cut.items, cut.owner, indexOf, links);
|
|
93
|
+
if (depths.has(d) && pairs.length > 0) {
|
|
94
|
+
gaps = roomDemand(cut.items, pairs, room);
|
|
95
|
+
// The shift of each gap applies to the nodes, so a node moves with its item, and the items of a far level stay apart.
|
|
96
|
+
if (gaps.some((g) => g.need > g.width)) {
|
|
97
|
+
current = openGaps(current, gaps, snap);
|
|
98
|
+
cut = levelCut(current, root, d);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (bends && d === deepest + 1) {
|
|
102
|
+
const bent = withBends(current, links, boardGaps(current, snap), bends, snap);
|
|
103
|
+
if (bent.some((g) => g.need > g.width)) {
|
|
104
|
+
current = openGaps(current, bent, snap);
|
|
105
|
+
cut = levelCut(current, root, d);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
levels.push({ depth: d, items: cut.items, pairs, gaps, nodes: current });
|
|
109
|
+
}
|
|
110
|
+
const shared = (options.board ?? "shared") === "shared";
|
|
111
|
+
const out = shared ? levels.map((l) => ({ ...l, items: levelCut(current, root, l.depth).items, nodes: current })) : levels;
|
|
112
|
+
return { levels: out, gaps: nodeGaps(nodes, current, levels, snap), items: current };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* This function gives the gaps of the board of the nodes that move each node from `from` to `to`.
|
|
117
|
+
* The growth of a gap is the shift of the nodes after it, less the shift of the nodes before it. Its lanes are the most lanes of a level gap there.
|
|
118
|
+
*/
|
|
119
|
+
function nodeGaps<T extends Boxed>(from: readonly T[], to: readonly T[], levels: readonly RoomLevel<T>[], snap: number): Gap[] {
|
|
120
|
+
const indexOf = new Map(from.map((n, i) => [n.id, i]));
|
|
121
|
+
const gaps = boardGaps(from, snap);
|
|
122
|
+
const shift = (id: string, axis: "x" | "y"): number => {
|
|
123
|
+
const i = indexOf.get(id)!;
|
|
124
|
+
return to[i]!.box[axis] - from[i]!.box[axis];
|
|
125
|
+
};
|
|
126
|
+
// The lanes of each level gap go to the gap of the nodes at the left (or top) edge of its first item after it.
|
|
127
|
+
const lanes = new Map<number, number>();
|
|
128
|
+
const boundaryOf = new Map<string, number>();
|
|
129
|
+
gaps.forEach((g, k) => {
|
|
130
|
+
for (const id of g.after) boundaryOf.set(`${g.axis} ${id}`, k);
|
|
131
|
+
});
|
|
132
|
+
for (const level of levels) {
|
|
133
|
+
const byId = new Map(level.items.map((i) => [i.id, i]));
|
|
134
|
+
for (const g of level.gaps) {
|
|
135
|
+
if (g.lanes === 0) continue;
|
|
136
|
+
for (const id of g.after) {
|
|
137
|
+
const item = byId.get(id);
|
|
138
|
+
if (!item) continue;
|
|
139
|
+
// The first node of the item on the axis of the gap starts the item.
|
|
140
|
+
let first = item.nodes[0]!;
|
|
141
|
+
for (const n of item.nodes) if (from[indexOf.get(n)!]!.box[g.axis] < from[indexOf.get(first)!]!.box[g.axis]) first = n;
|
|
142
|
+
const k = boundaryOf.get(`${g.axis} ${first}`);
|
|
143
|
+
if (k !== undefined) lanes.set(k, Math.max(lanes.get(k) ?? 0, g.lanes));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const last: Record<"x" | "y", number> = { x: 0, y: 0 };
|
|
148
|
+
return gaps.map((g, k) => {
|
|
149
|
+
let s = Infinity;
|
|
150
|
+
for (const id of g.after) s = Math.min(s, shift(id, g.axis));
|
|
151
|
+
const growth = Math.max(0, s - last[g.axis]);
|
|
152
|
+
last[g.axis] = Math.max(last[g.axis], s);
|
|
153
|
+
return { ...g, lanes: lanes.get(k) ?? 0, need: g.width + growth };
|
|
154
|
+
});
|
|
155
|
+
}
|
package/src/routers/sections.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { BACKTRACK_MIN, goesOutAndBack } from "../bench/links.ts";
|
|
2
|
-
import { center, dist, pointSegmentDistance, polylineLength, type
|
|
3
|
-
import { levelLinks, type ItemPair } from "../levels.ts";
|
|
4
|
-
import { nodeIndex, type Graph, type Level, type
|
|
2
|
+
import { center, dist, pointSegmentDistance, polylineLength, type Point } from "../geom.ts";
|
|
3
|
+
import { LEVEL_LINK_DEFAULTS, levelLinks, type ItemPair, type LevelLinkOptions } from "../levels.ts";
|
|
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, treeDepth, type Section, type SectionOptions } from "../sections.ts";
|
|
7
|
-
import { orderLanes } from "../segments.ts";
|
|
8
|
-
import {
|
|
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
|
+
import type { Strategy } from "../strategies/model.ts";
|
|
9
|
+
import { WayFinder } from "../ways.ts";
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
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
|
+
* The options `cap`, `maxDepth` and `collapse` are the limits of the default grouping rule, the quadtree of `buildSections`.
|
|
14
|
+
* The options `tightMargin`, `plugInset` and `throughCost` are the limits of the level links (`levelLinks`).
|
|
15
|
+
*/
|
|
16
|
+
export interface SectionsOptions extends LevelLinkOptions, SectionOptions {
|
|
12
17
|
/** The name in the benchmark. */
|
|
13
18
|
readonly name: string;
|
|
14
19
|
/**
|
|
@@ -16,19 +21,52 @@ export interface SectionsOptions extends WayOptions, SectionOptions {
|
|
|
16
21
|
* and fewer shared ways. Without it, a way passes each hub from its node up.
|
|
17
22
|
*/
|
|
18
23
|
readonly hubs?: number;
|
|
19
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The hub limit. A way through the hubs can be at most this many times as long as the line between its plugs, plus `stretchPad` margins.
|
|
26
|
+
* Else the edge goes directly. The default is 2. The value `Infinity` keeps each way through the hubs.
|
|
27
|
+
*/
|
|
20
28
|
readonly stretch?: number;
|
|
29
|
+
/** A way through the hubs can be this many margins longer than `stretch` times the line between its plugs. The default is 4. */
|
|
30
|
+
readonly stretchPad?: number;
|
|
31
|
+
/**
|
|
32
|
+
* A way skips a hub that is more than this many units farther from the target than the nearest approach of the way before it.
|
|
33
|
+
* The default is 80, two times `BACKTRACK_MIN`. The value `Infinity` keeps each hub.
|
|
34
|
+
*/
|
|
35
|
+
readonly hubSlack?: number;
|
|
36
|
+
/**
|
|
37
|
+
* The grouping rule: a strategy of the step `group` that makes the tree of sections. The hubs and the zoom levels come from this tree.
|
|
38
|
+
* Without it, the router uses the quadtree of `buildSections` with the options `cap`, `maxDepth` and `collapse`.
|
|
39
|
+
*/
|
|
40
|
+
readonly group?: Strategy<"group">;
|
|
41
|
+
/** The options of the strategy `group`. The strategy gets its `defaults` for each option that this object does not give. */
|
|
42
|
+
readonly groupOptions?: object;
|
|
43
|
+
/**
|
|
44
|
+
* The depths of the tree that get a zoom level with groups. The last level, with each node alone, always comes after them.
|
|
45
|
+
* The default is each depth, from 0 to the depth of the deepest section.
|
|
46
|
+
*/
|
|
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;
|
|
21
50
|
/** The benchmark skips a fixture with more nodes. */
|
|
22
51
|
readonly maxNodes?: number;
|
|
23
52
|
}
|
|
24
53
|
|
|
25
|
-
|
|
54
|
+
/** The default values of the numbers of `SectionsOptions`: the limits of the hubs, of the tree and of the level links. */
|
|
55
|
+
export const SECTIONS_DEFAULTS: Readonly<Required<Pick<SectionsOptions, "cap" | "maxDepth" | "collapse" | "tightMargin" | "plugInset" | "throughCost" | "hubs" | "stretch" | "stretchPad" | "hubSlack">>> =
|
|
56
|
+
Object.freeze({
|
|
57
|
+
cap: SECTION_DEFAULTS.cap,
|
|
58
|
+
maxDepth: SECTION_DEFAULTS.maxDepth,
|
|
59
|
+
collapse: SECTION_DEFAULTS.collapse,
|
|
60
|
+
tightMargin: LEVEL_LINK_DEFAULTS.tightMargin,
|
|
61
|
+
plugInset: LEVEL_LINK_DEFAULTS.plugInset,
|
|
62
|
+
throughCost: LEVEL_LINK_DEFAULTS.throughCost,
|
|
63
|
+
hubs: Infinity,
|
|
64
|
+
stretch: 2,
|
|
65
|
+
stretchPad: 4,
|
|
66
|
+
hubSlack: 2 * BACKTRACK_MIN,
|
|
67
|
+
});
|
|
26
68
|
|
|
27
|
-
|
|
28
|
-
* A hub can be this many units farther from the target than the nearest approach of the way before it.
|
|
29
|
-
* A hub farther away goes out and back, so the way skips it.
|
|
30
|
-
*/
|
|
31
|
-
const HUB_SLACK = 2 * BACKTRACK_MIN;
|
|
69
|
+
const dedupe = (p: readonly Point[]): Point[] => p.filter((q, i) => i === 0 || dist(q, p[i - 1]!) > 1e-9);
|
|
32
70
|
|
|
33
71
|
/**
|
|
34
72
|
* 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.
|
|
@@ -45,12 +83,14 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
45
83
|
const router: Router = {
|
|
46
84
|
name: options.name,
|
|
47
85
|
route(graph: Graph): Result {
|
|
48
|
-
const stretch = options.stretch ??
|
|
86
|
+
const stretch = options.stretch ?? SECTIONS_DEFAULTS.stretch;
|
|
87
|
+
const stretchPad = options.stretchPad ?? SECTIONS_DEFAULTS.stretchPad;
|
|
88
|
+
const hubSlack = options.hubSlack ?? SECTIONS_DEFAULTS.hubSlack;
|
|
49
89
|
const byId = nodeIndex(graph);
|
|
50
90
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
51
91
|
const ways = new WayFinder(graph.nodes, options);
|
|
52
92
|
const corners = ways.corners;
|
|
53
|
-
const root =
|
|
93
|
+
const root = groupNodes(graph, options);
|
|
54
94
|
const parent = new Map<Section, Section>();
|
|
55
95
|
const leafOf: Section[] = [];
|
|
56
96
|
for (const s of allSections(root)) {
|
|
@@ -82,7 +122,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
82
122
|
let last = from;
|
|
83
123
|
for (const c of list) {
|
|
84
124
|
const h = hub(c);
|
|
85
|
-
if (dist(h, to) > near +
|
|
125
|
+
if (dist(h, to) > near + hubSlack) continue;
|
|
86
126
|
near = Math.min(near, pointSegmentDistance(to, last, h));
|
|
87
127
|
last = h;
|
|
88
128
|
kept.push(c);
|
|
@@ -132,7 +172,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
132
172
|
const b = up(lv);
|
|
133
173
|
const common = new Set(b);
|
|
134
174
|
const lca = a.find((x) => common.has(x))!;
|
|
135
|
-
const keep = options.hubs ??
|
|
175
|
+
const keep = options.hubs ?? SECTIONS_DEFAULTS.hubs;
|
|
136
176
|
const side = (list: Section[]): Section[] => list.slice(Math.max(0, list.indexOf(lca) - keep), list.indexOf(lca));
|
|
137
177
|
const end = ways.out(pt);
|
|
138
178
|
const start = ways.out(ps);
|
|
@@ -151,7 +191,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
151
191
|
append(piece(chain[chain.length - 1]!.id, hub(chain[chain.length - 1]!), at(end), end, used));
|
|
152
192
|
append([pt.point]);
|
|
153
193
|
// A way through the hubs that is too long, or that goes out and back, gives way to the direct way.
|
|
154
|
-
if (polylineLength(out) > stretch * dist(ps.point, pt.point) +
|
|
194
|
+
if (polylineLength(out) > stretch * dist(ps.point, pt.point) + stretchPad * ways.margin || goesOutAndBack(out)) {
|
|
155
195
|
routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
|
|
156
196
|
continue;
|
|
157
197
|
}
|
|
@@ -168,7 +208,7 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
168
208
|
for (const [key, p] of pieces) {
|
|
169
209
|
const list = users.get(key) ?? [];
|
|
170
210
|
if (list.length < 2 || p.length < 2) continue;
|
|
171
|
-
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) });
|
|
172
212
|
}
|
|
173
213
|
return { routes, runs, levels: makeLevels(graph, root, routeOf, options) };
|
|
174
214
|
},
|
|
@@ -176,56 +216,39 @@ export function sectionsRouter(options: SectionsOptions): Router {
|
|
|
176
216
|
return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
|
|
177
217
|
}
|
|
178
218
|
|
|
219
|
+
/** This function gives the tree of sections of the grouping rule. It examines the tree of a strategy (`checkSections`). */
|
|
220
|
+
function groupNodes(graph: Graph, options: SectionsOptions): Section {
|
|
221
|
+
if (!options.group) return buildSections(graph.nodes, options);
|
|
222
|
+
const root = options.group.run(graph, options.groupOptions);
|
|
223
|
+
checkSections(root, graph.nodes.length);
|
|
224
|
+
return root;
|
|
225
|
+
}
|
|
226
|
+
|
|
179
227
|
/**
|
|
180
228
|
* This function gives one zoom level for each depth of the tree, and a last level with each node alone.
|
|
229
|
+
* With the option `levels`, only those depths get a level with groups.
|
|
181
230
|
* At depth d, a section of depth d is one item. A link joins two items and counts the edges between them.
|
|
182
231
|
* In a level with groups, a link goes around the boxes of the other items (`levelLinks`).
|
|
183
232
|
* In the last level, the points of a link are the route of its first edge.
|
|
184
233
|
*/
|
|
185
|
-
function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options:
|
|
234
|
+
function makeLevels(graph: Graph, root: Section, routeOf: ReadonlyMap<string, Route>, options: SectionsOptions): Level[] {
|
|
186
235
|
const indexOfNode = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
187
236
|
const levels: Level[] = [];
|
|
188
237
|
const deepest = treeDepth(root);
|
|
238
|
+
const shown = options.levels && new Set(options.levels);
|
|
189
239
|
for (let d = 0; d <= deepest + 1; d++) {
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const boxOf = (list: readonly number[]): Box => {
|
|
193
|
-
let x0 = Infinity;
|
|
194
|
-
let y0 = Infinity;
|
|
195
|
-
let x1 = -Infinity;
|
|
196
|
-
let y1 = -Infinity;
|
|
197
|
-
for (const i of list) {
|
|
198
|
-
const b = graph.nodes[i]!.box;
|
|
199
|
-
x0 = Math.min(x0, b.x);
|
|
200
|
-
y0 = Math.min(y0, b.y);
|
|
201
|
-
x1 = Math.max(x1, b.x + b.w);
|
|
202
|
-
y1 = Math.max(y1, b.y + b.h);
|
|
203
|
-
}
|
|
204
|
-
return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
|
|
205
|
-
};
|
|
206
|
-
const visit = (s: Section): void => {
|
|
207
|
-
if (s.depth >= d) {
|
|
208
|
-
items.push({ id: s.id, box: boxOf(s.nodes), nodes: s.nodes.map((i) => graph.nodes[i]!.id) });
|
|
209
|
-
for (const i of s.nodes) itemOf[i] = s;
|
|
210
|
-
} else if (s.children.length > 0) for (const c of s.children) visit(c);
|
|
211
|
-
else
|
|
212
|
-
for (const i of s.nodes) {
|
|
213
|
-
const n = graph.nodes[i]!;
|
|
214
|
-
items.push({ id: n.id, box: n.box, nodes: [n.id] });
|
|
215
|
-
itemOf[i] = i;
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
if (graph.nodes.length > 0) visit(root);
|
|
240
|
+
if (shown && d <= deepest && !shown.has(d)) continue;
|
|
241
|
+
const { items, owner } = levelCut(graph.nodes, root, d);
|
|
219
242
|
const links = new Map<string, { source: string; target: string; count: number; edge: string }>();
|
|
220
243
|
for (const e of graph.edges) {
|
|
221
244
|
const iu = indexOfNode.get(e.source.node);
|
|
222
245
|
const iv = indexOfNode.get(e.target.node);
|
|
223
246
|
if (iu === undefined || iv === undefined) continue;
|
|
224
|
-
const a =
|
|
225
|
-
const b =
|
|
247
|
+
const a = owner[iu]!;
|
|
248
|
+
const b = owner[iv]!;
|
|
226
249
|
if (a === b) continue;
|
|
227
|
-
const ida =
|
|
228
|
-
const idb =
|
|
250
|
+
const ida = items[a]!.id;
|
|
251
|
+
const idb = items[b]!.id;
|
|
229
252
|
const key = `${ida}\u0000${idb}`;
|
|
230
253
|
const link = links.get(key);
|
|
231
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 };
|