@mark1russell7/highways 0.9.2 → 0.9.4

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.
@@ -1,12 +1,14 @@
1
1
  // The corpus families of highways on the shared registry of `@mark1russell7/layout-contract` (CORPUS-GEN, L45).
2
2
  import { createFamilyRegistry, type FamilyRegistry } from "@mark1russell7/layout-contract";
3
3
  import { HUB_CHAIN, MANY_GROUPS, PACKED_GRID } from "./cases.ts";
4
+ import { COVERAGE_FAMILIES } from "./coverage.ts";
4
5
  import { DENSE_GROUPS, SMALL_CLUSTERED, TINY_UNIFORM } from "./fixtures.ts";
5
6
  import type { HighwaysBoard, HighwaysFamily } from "./model.ts";
6
7
 
7
8
  export * from "./model.ts";
8
9
  export { DENSE_GROUPS, FIXTURE_LAYOUTS, FIXTURE_LOCALITY, fixtureParams, fixtureSpecOf, SMALL_CLUSTERED, TINY_UNIFORM } from "./fixtures.ts";
9
10
  export { HUB_CHAIN, MANY_GROUPS, PACKED_GRID } from "./cases.ts";
11
+ export { BIPARTITE, BIPARTITE_LAYOUTS, COVERAGE_FAMILIES, FAN_OUT, FRAME_ROWS, LOCAL_MESH, PIPELINE, SPARSE_LONG, THIN_BARS, TWO_HUBS, UNEVEN_SIZES } from "./coverage.ts";
10
12
  // The parts of the shared registry that a host of the families uses. The site imports only `@mark1russell7/highways`.
11
13
  export {
12
14
  createFamilyRegistry,
@@ -26,9 +28,10 @@ export {
26
28
 
27
29
  /**
28
30
  * The corpus families of highways, in the order of the screen sets. Each family at its defaults makes the board of its screen family.
31
+ * The first 6 families are the families of `screen-v1` and `screen-v2`. The 9 families of `COVERAGE_FAMILIES` follow them (CORPUS-FAMILIES).
29
32
  * An import registers nothing. A host makes a registry with `createFamilyRegistry(HIGHWAYS_FAMILIES)`, or with the lists of other engines too.
30
33
  */
31
- export const HIGHWAYS_FAMILIES: readonly HighwaysFamily[] = Object.freeze([TINY_UNIFORM, SMALL_CLUSTERED, DENSE_GROUPS, HUB_CHAIN, MANY_GROUPS, PACKED_GRID]);
34
+ export const HIGHWAYS_FAMILIES: readonly HighwaysFamily[] = Object.freeze([TINY_UNIFORM, SMALL_CLUSTERED, DENSE_GROUPS, HUB_CHAIN, MANY_GROUPS, PACKED_GRID, ...COVERAGE_FAMILIES]);
32
35
 
33
36
  /** This function gives a new registry of `HIGHWAYS_FAMILIES`. */
34
37
  export function highwaysRegistry(): FamilyRegistry<HighwaysBoard> {
@@ -0,0 +1,144 @@
1
+ import { dist, polylineLength, type Point } from "../../geom.ts";
2
+ import { WayFinder } from "../../ways.ts";
3
+ import { firstRoutes } from "../base.ts";
4
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
5
+ import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
6
+ import { rootOf } from "../knee/root.ts";
7
+
8
+ /** The rule that splits the targets of a tip in two: by count, by weight, or at the widest gap of the angles. */
9
+ export type BronchialSplit = "count" | "weight" | "gap";
10
+
11
+ /** The values of `BronchialSplit`. */
12
+ export const BRONCHIAL_SPLITS: readonly BronchialSplit[] = Object.freeze(["count", "weight", "gap"]);
13
+
14
+ /** The options of the highway strategy `bronchial` (NAT-019 of the nature notes). */
15
+ export interface BronchialOptions {
16
+ /**
17
+ * The rule of the split. With `count`, each half gets half of the targets. With `weight`, the two halves get near-equal sums of the distances from the tip.
18
+ * With `gap`, the cut goes at the widest gap between the angles of the targets. The default is `count`.
19
+ */
20
+ readonly split: BronchialSplit;
21
+ /** The share of the distance to the center of the targets that a branch grows, from 0.3 to 0.7. The default is 0.5. */
22
+ readonly share: number;
23
+ /** A set with this number of targets or fewer stops the splits, from 1 to 3. Each of its targets gets its own way from the tip. The default is 1. */
24
+ readonly smallest: number;
25
+ /** An edge rides the tree only when its path in the tree is this many times its first route or less. The default is 1.5. */
26
+ readonly detour: number;
27
+ /** A stretch of a tree with fewer edges than this number is not a highway. The default is 2. */
28
+ readonly minSize: number;
29
+ }
30
+
31
+ /** The default options of the strategy `bronchial`. */
32
+ export const BRONCHIAL_DEFAULTS: Readonly<BronchialOptions> = Object.freeze({ split: "count", share: 0.5, smallest: 1, detour: 1.5, minSize: 2 });
33
+
34
+ /**
35
+ * This function gives the diamond angle of the direction `(dx, dy)`: a value from 0 to 4 that grows with the angle, with no trigonometry.
36
+ * The value 0 is the direction of the positive x axis.
37
+ */
38
+ export function diamondAngle(dx: number, dy: number): number {
39
+ const s = Math.abs(dx) + Math.abs(dy);
40
+ if (s <= 1e-12) return 0;
41
+ if (dy >= 0) return dx >= 0 ? dy / s : 1 - dx / s;
42
+ return dx < 0 ? 2 - dy / s : 3 + dx / s;
43
+ }
44
+
45
+ /**
46
+ * This function splits the targets of a tip in two halves (NAT-019). The targets go in the order of their angles around the tip.
47
+ * The order starts after the widest gap of the angles, so each half is a sector. The rule of `split` then cuts the order in two.
48
+ */
49
+ export function splitTargets(tip: Point, targets: readonly Point[], ids: readonly number[], split: BronchialSplit): [number[], number[]] {
50
+ const sorted = [...ids].sort((a, b) => diamondAngle(targets[a]!.x - tip.x, targets[a]!.y - tip.y) - diamondAngle(targets[b]!.x - tip.x, targets[b]!.y - tip.y) || a - b);
51
+ const angle = (i: number): number => diamondAngle(targets[sorted[i]!]!.x - tip.x, targets[sorted[i]!]!.y - tip.y);
52
+ const n = sorted.length;
53
+ // The gap after each target, to the next target around the circle.
54
+ const gaps = sorted.map((_, i) => (i === n - 1 ? angle(0) + 4 - angle(i) : angle(i + 1) - angle(i)));
55
+ let widest = 0;
56
+ for (let i = 1; i < n; i++) if (gaps[i]! > gaps[widest]! + 1e-12) widest = i;
57
+ const order = [...sorted.slice(widest + 1), ...sorted.slice(0, widest + 1)];
58
+ let cut = Math.floor(n / 2);
59
+ if (split === "weight") {
60
+ const w = order.map((i) => dist(tip, targets[i]!));
61
+ const total = w.reduce((s, x) => s + x, 0);
62
+ let run = 0;
63
+ let best = Infinity;
64
+ for (let c = 1; c < n; c++) {
65
+ run += w[c - 1]!;
66
+ const off = Math.abs(2 * run - total);
67
+ if (off < best - 1e-9) [best, cut] = [off, c];
68
+ }
69
+ } else if (split === "gap") {
70
+ let best = -Infinity;
71
+ const at = (k: number): number => diamondAngle(targets[order[k]!]!.x - tip.x, targets[order[k]!]!.y - tip.y);
72
+ for (let c = 1; c < n; c++) {
73
+ const g = (at(c) - at(c - 1) + 4) % 4;
74
+ if (g > best + 1e-12) [best, cut] = [g, c];
75
+ }
76
+ }
77
+ return [order.slice(0, cut), order.slice(cut)];
78
+ }
79
+
80
+ /**
81
+ * This function gives the highways of the bronchial trees (NAT-019, after Kitaoka and others, 1999). The edges from one source node get one tree.
82
+ * The root of a tree is the out point of the plug nearest the center of the targets.
83
+ * From the tip, the tree grows a share of the way to the center of its targets. Then it splits the targets in two and grows a branch for each half.
84
+ * A branch that hits a box grows half as far, or not at all. A small set of targets gets its own ways from the tip, through the corner graph.
85
+ * Each edge takes its path in the tree when that path is short enough, and the stretches that two edges or more share become highways.
86
+ */
87
+ export function bronchialPlan(input: HighwayInput, o: Readonly<BronchialOptions>): HighwayPlan {
88
+ const finder = new WayFinder(input.graph.nodes);
89
+ const pad = finder.margin / 2;
90
+ const first = firstRoutes(input);
91
+ const groups = new Map<string, { edge: string; key: string; s: Point; t: Point; base: number }[]>();
92
+ for (const p of edgePlugs(input.graph)) {
93
+ const s = finder.out(p.ps);
94
+ const t = finder.out(p.pt);
95
+ const route = first.get(p.edge.id);
96
+ const group = p.edge.source.node;
97
+ const list = groups.get(group) ?? [];
98
+ list.push({ edge: p.edge.id, key: p.edge.source.node, s, t, base: route ? polylineLength(route) : dist(s, t) });
99
+ groups.set(group, list);
100
+ }
101
+ const share = Math.min(0.7, Math.max(0.3, o.share));
102
+ const smallest = Math.min(3, Math.max(1, Math.floor(o.smallest)));
103
+ const ways: KeyedWay[] = [];
104
+ for (const group of [...groups.keys()].sort()) {
105
+ const list = groups.get(group)!;
106
+ if (list.length < 2) continue;
107
+ const goals = list.map((e) => e.t);
108
+ const { root, leads } = rootOf(finder, list.map((e) => e.s), goals);
109
+ const paths: (Point[] | undefined)[] = list.map(() => undefined);
110
+ const grow = (trunk: readonly Point[], ids: readonly number[]): void => {
111
+ const tip = trunk[trunk.length - 1]!;
112
+ if (ids.length <= smallest) {
113
+ for (const i of ids) {
114
+ const way = finder.corners.path(tip, goals[i]!);
115
+ if (way) paths[i] = [...trunk, ...finder.corners.pull(way).slice(1)];
116
+ }
117
+ return;
118
+ }
119
+ const c = { x: ids.reduce((s, i) => s + goals[i]!.x, 0) / ids.length, y: ids.reduce((s, i) => s + goals[i]!.y, 0) / ids.length };
120
+ let next = trunk;
121
+ for (let f = share; f > share / 8; f /= 2) {
122
+ const p = { x: tip.x + f * (c.x - tip.x), y: tip.y + f * (c.y - tip.y) };
123
+ if (dist(p, tip) <= 1e-6) break;
124
+ if (!finder.obstacles.inside(p, pad) && finder.obstacles.clearPath([tip, p], pad)) {
125
+ next = [...trunk, p];
126
+ break;
127
+ }
128
+ }
129
+ const [a, b] = splitTargets(next[next.length - 1]!, goals, ids, o.split);
130
+ grow(next, a);
131
+ grow(next, b);
132
+ };
133
+ grow([root], list.map((_, i) => i));
134
+ list.forEach((e, i) => {
135
+ const tail = paths[i];
136
+ const lead = leads[i];
137
+ if (!tail || !lead) return;
138
+ const points = [...lead, ...tail.slice(1)];
139
+ if (points.length < 2 || polylineLength(points) > o.detour * e.base) return;
140
+ ways.push({ edge: e.edge, key: e.key, points });
141
+ });
142
+ }
143
+ return sharedStretches(ways, { minSize: Math.max(2, Math.floor(o.minSize)) });
144
+ }
@@ -102,6 +102,8 @@ export { oneSteiner, spanningTree, STEINER_DEFAULTS, STEINER_GROUPS, STEINER_MET
102
102
  export { GAP_DEFAULTS, gapPlan, type GapOptions } from "./gaps/index.ts";
103
103
  export { STREET_DEFAULTS, streetPlan, type StreetOptions } from "./streets/index.ts";
104
104
  export { SEARCH_DEFAULTS, SEARCH_MOVES, searchPlan, searchScore, type SearchMove, type SearchOptions } from "./search/index.ts";
105
+ export { KNEE_DEFAULTS, KNEE_RULES, kneeOf, kneePlan, type KneeOptions, type KneeRule, type KneeTree } from "./knee/index.ts";
106
+ export { BRONCHIAL_DEFAULTS, BRONCHIAL_SPLITS, bronchialPlan, diamondAngle, splitTargets, type BronchialOptions, type BronchialSplit } from "./bronchial/index.ts";
105
107
  export { INTERCHANGE_BASES, INTERCHANGE_DEFAULTS, interchangeCandidates, interchangePlan, type Interchange, type InterchangeBase, type InterchangeOptions } from "./interchanges/index.ts";
106
108
  export { SCALE_SPACE_DEFAULTS, scaleClusters, scaleSpaceGroups, scaleSpacePlan, type ScaleCluster, type ScaleSpaceOptions } from "./scale-space/index.ts";
107
109
  export * from "./gates/index.ts";
@@ -0,0 +1,154 @@
1
+ import { dist, polylineLength, type Point } from "../../geom.ts";
2
+ import { balancedFan, fanPath, type Fan } from "../../routers/fan.ts";
3
+ import { WayFinder } from "../../ways.ts";
4
+ import { firstRoutes } from "../base.ts";
5
+ import type { HighwayInput, HighwayPlan } from "../model.ts";
6
+ import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
7
+ import { rootOf } from "./root.ts";
8
+
9
+ /** The rule that picks one tree on the front: the knee of the chord, or the least weighted sum. */
10
+ export type KneeRule = "chord" | "weights";
11
+
12
+ /** The values of `KneeRule`. */
13
+ export const KNEE_RULES: readonly KneeRule[] = Object.freeze(["chord", "weights"]);
14
+
15
+ /** The options of the highway strategy `knee` (NAT-018 of the nature notes, on the trees of NAT-017). */
16
+ export interface KneeOptions {
17
+ /** The number of points of the sweep of the balancing factor, from 5 to 21. The default is 11. */
18
+ readonly sweep: number;
19
+ /** The rule that picks one tree on the front. The default is `chord`. */
20
+ readonly rule: KneeRule;
21
+ /** The weight of the ink in the rule `weights`, from 0 to 1. The rest of the weight goes to the path length. The default is 0.5. */
22
+ readonly ink: number;
23
+ /** An edge rides the tree only when its path in the tree is this many times its first route or less. The default is 1.5. */
24
+ readonly detour: number;
25
+ /** A stretch of a tree with fewer edges than this number is not a highway. The default is 2. */
26
+ readonly minSize: number;
27
+ }
28
+
29
+ /** The default options of the strategy `knee`. */
30
+ export const KNEE_DEFAULTS: Readonly<KneeOptions> = Object.freeze({ sweep: 11, rule: "chord", ink: 0.5, detour: 1.5, minSize: 2 });
31
+
32
+ /** One tree of the sweep: its balancing factor, its ink `L`, the sum of its path lengths `S`, and the tree. */
33
+ export interface KneeTree {
34
+ readonly bf: number;
35
+ readonly ink: number;
36
+ readonly paths: number;
37
+ readonly fan: Fan;
38
+ }
39
+
40
+ /** This function gives the ink of a fan: the sum of the lengths of its links. */
41
+ function fanInk(fan: Fan): number {
42
+ let sum = 0;
43
+ for (const p of fan.points) if (p.parent >= 0) sum += dist(p.point, fan.points[p.parent]!.point);
44
+ return sum;
45
+ }
46
+
47
+ /**
48
+ * This function adds a stub point at `stub` from the end of the last link of a way. The tree of NAT-017 joins a new branch only at an open point or on a link to one.
49
+ * The last point of a way is not open, so without the stub no branch joins the last link.
50
+ */
51
+ function withStub(way: Point[], stub: number): Point[] {
52
+ if (way.length < 2) return way;
53
+ const a = way[way.length - 2]!;
54
+ const b = way[way.length - 1]!;
55
+ const d = dist(a, b);
56
+ if (d <= 2 * stub) return way;
57
+ return [...way.slice(0, -1), { x: b.x + ((a.x - b.x) * stub) / d, y: b.y + ((a.y - b.y) * stub) / d }, b];
58
+ }
59
+
60
+ /**
61
+ * This function gives the index of the tree that the rule picks on the front of the trees (NAT-018).
62
+ * The front holds the trees that no other tree beats on the ink and on the path lengths. The two values go to the range from 0 to 1.
63
+ * With `chord`, the pick is the tree of the front farthest from the line between the two ends of the front.
64
+ * With `weights`, the pick is the tree with the least `ink × L + (1 - ink) × S`. A tie goes to the earlier tree.
65
+ */
66
+ export function kneeOf(trees: readonly Pick<KneeTree, "ink" | "paths">[], rule: KneeRule, ink: number): number {
67
+ if (trees.length === 0) return -1;
68
+ const front = trees.map((_, i) => i).filter((i) => !trees.some((q, j) => j !== i && q.ink <= trees[i]!.ink && q.paths <= trees[i]!.paths && (q.ink < trees[i]!.ink || q.paths < trees[i]!.paths)));
69
+ const lo = { x: Math.min(...front.map((i) => trees[i]!.ink)), y: Math.min(...front.map((i) => trees[i]!.paths)) };
70
+ const hi = { x: Math.max(...front.map((i) => trees[i]!.ink)), y: Math.max(...front.map((i) => trees[i]!.paths)) };
71
+ const norm = (i: number): Point => ({
72
+ x: hi.x - lo.x > 1e-9 ? (trees[i]!.ink - lo.x) / (hi.x - lo.x) : 0,
73
+ y: hi.y - lo.y > 1e-9 ? (trees[i]!.paths - lo.y) / (hi.y - lo.y) : 0,
74
+ });
75
+ let best = front[0]!;
76
+ let score = rule === "chord" ? -Infinity : Infinity;
77
+ if (rule === "weights") {
78
+ const w = Math.min(1, Math.max(0, ink));
79
+ for (const i of front) {
80
+ const p = norm(i);
81
+ const s = w * p.x + (1 - w) * p.y;
82
+ if (s < score - 1e-12) [best, score] = [i, s];
83
+ }
84
+ return best;
85
+ }
86
+ // The ends of the front: the tree with the least ink, and the tree with the least path lengths.
87
+ const a = front.reduce((u, i) => (trees[i]!.ink < trees[u]!.ink || (trees[i]!.ink === trees[u]!.ink && trees[i]!.paths < trees[u]!.paths) ? i : u));
88
+ const b = front.reduce((u, i) => (trees[i]!.paths < trees[u]!.paths || (trees[i]!.paths === trees[u]!.paths && trees[i]!.ink < trees[u]!.ink) ? i : u));
89
+ const A = norm(a);
90
+ const B = norm(b);
91
+ const len = dist(A, B);
92
+ if (len <= 1e-12) return a;
93
+ for (const i of front) {
94
+ const p = norm(i);
95
+ const s = Math.abs((B.x - A.x) * (A.y - p.y) - (A.x - p.x) * (B.y - A.y)) / len;
96
+ if (s > score + 1e-12) [best, score] = [i, s];
97
+ }
98
+ return best;
99
+ }
100
+
101
+ /**
102
+ * This function gives the highways of the knee trees (NAT-018). The edges from one source node get one tree. Its root is the out point of the plug nearest the center of the targets.
103
+ * A sweep of the balancing factor of NAT-017 gives trees from the least ink to the most direct paths.
104
+ * The tree at the knee of the front of ink and path lengths is the pick. Each edge takes its path in the tree, when that path is short enough.
105
+ * The stretches of the tree that two edges or more share become highways.
106
+ */
107
+ export function kneePlan(input: HighwayInput, o: Readonly<KneeOptions>): HighwayPlan {
108
+ const finder = new WayFinder(input.graph.nodes);
109
+ const first = firstRoutes(input);
110
+ const groups = new Map<string, { edge: string; key: string; s: Point; t: Point; base: number }[]>();
111
+ for (const p of edgePlugs(input.graph)) {
112
+ const s = finder.out(p.ps);
113
+ const t = finder.out(p.pt);
114
+ const route = first.get(p.edge.id);
115
+ const group = p.edge.source.node;
116
+ const list = groups.get(group) ?? [];
117
+ list.push({ edge: p.edge.id, key: p.edge.source.node, s, t, base: route ? polylineLength(route) : dist(s, t) });
118
+ groups.set(group, list);
119
+ }
120
+ const sweep = Math.min(21, Math.max(2, Math.floor(o.sweep)));
121
+ const ways: KeyedWay[] = [];
122
+ for (const group of [...groups.keys()].sort()) {
123
+ const list = groups.get(group)!;
124
+ if (list.length < 2) continue;
125
+ const goals = list.map((e) => e.t);
126
+ const { root, leads } = rootOf(finder, list.map((e) => e.s), goals);
127
+ const lost = new Set<number>();
128
+ const branch = (from: Point, t: number): Point[] => {
129
+ const way = finder.corners.path(from, goals[t]!);
130
+ if (way) return withStub(finder.corners.pull(way), finder.margin);
131
+ lost.add(t);
132
+ return [from, goals[t]!];
133
+ };
134
+ // The sweep: the cost `a L + (1 - a) S` for `a` from `1 / sweep` to 1, as the balancing factor `bf = (1 - a) / a`.
135
+ const trees: KneeTree[] = [];
136
+ for (let k = 0; k < sweep; k++) {
137
+ const a = (k + 1) / sweep;
138
+ const bf = (1 - a) / a;
139
+ const fan = balancedFan(root, goals, bf, branch);
140
+ let paths = 0;
141
+ for (const leaf of fan.leaf) paths += fan.points[leaf]!.pl;
142
+ trees.push({ bf, ink: fanInk(fan), paths, fan });
143
+ }
144
+ const pick = trees[kneeOf(trees, o.rule, o.ink)]!;
145
+ list.forEach((e, t) => {
146
+ const lead = leads[t];
147
+ if (lost.has(t) || !lead) return;
148
+ const points = [...lead, ...fanPath(pick.fan, pick.fan.leaf[t]!).slice(1)];
149
+ if (points.length < 2 || polylineLength(points) > o.detour * e.base) return;
150
+ ways.push({ edge: e.edge, key: e.key, points });
151
+ });
152
+ }
153
+ return sharedStretches(ways, { minSize: Math.max(2, Math.floor(o.minSize)) });
154
+ }
@@ -0,0 +1,19 @@
1
+ import { dist, type Point } from "../../geom.ts";
2
+ import type { WayFinder } from "../../ways.ts";
3
+
4
+ /**
5
+ * This function gives the root of the tree of one source node (NAT-018, NAT-019): the start point nearest the center of the targets.
6
+ * It also gives the lead of each edge, the way from its own start point to the root. A lead is `undefined` when the corner graph has no way.
7
+ */
8
+ export function rootOf(finder: WayFinder, starts: readonly Point[], goals: readonly Point[]): { root: Point; leads: (Point[] | undefined)[] } {
9
+ const c = { x: goals.reduce((s, p) => s + p.x, 0) / goals.length, y: goals.reduce((s, p) => s + p.y, 0) / goals.length };
10
+ let root = starts[0]!;
11
+ for (const s of starts) if (dist(s, c) < dist(root, c) - 1e-9) root = s;
12
+ const leads = starts.map((s) => {
13
+ if (dist(s, root) <= 1e-9) return [root];
14
+ if (finder.obstacles.clearPath([s, root], finder.margin / 2)) return [s, root];
15
+ const way = finder.corners.path(s, root);
16
+ return way ? finder.corners.pull(way) : undefined;
17
+ });
18
+ return { root, leads };
19
+ }
package/src/index.ts CHANGED
@@ -18,8 +18,12 @@ export * from "./room/index.ts";
18
18
  export * from "./room/lanes.ts";
19
19
  export * from "./room/levels/index.ts";
20
20
  export * from "./plugs/facing/index.ts";
21
+ export * from "./plugs/spread/index.ts";
22
+ export * from "./plugs/fix.ts";
21
23
  export * from "./levels/depths/index.ts";
22
24
  export * from "./whole/pipeline/index.ts";
25
+ export * from "./levels/venation/index.ts";
26
+ export * from "./whole/portfolio/index.ts";
23
27
  export * from "./bench/properties.ts";
24
28
  export * from "./bench/quality.ts";
25
29
  export * from "./bench/links.ts";
@@ -0,0 +1,191 @@
1
+ import { center, dist, polylineLength, type Box, type Point } from "../../geom.ts";
2
+ import { levelLinks, type ItemPair } from "../../levels.ts";
3
+ import type { Level, LevelItem, LevelLink } from "../../model.ts";
4
+ import type { LevelInput } from "../../strategies/model.ts";
5
+ import { depthLevels, type DepthLevelOptions } from "../depths/index.ts";
6
+
7
+ /** The graph of the backbone of a level: the relative neighbourhood graph, or the Gabriel graph of the centers of the items. */
8
+ export type VenationBackbone = "rng" | "gabriel";
9
+
10
+ /** The values of `VenationBackbone`. */
11
+ export const VENATION_BACKBONES: readonly VenationBackbone[] = Object.freeze(["rng", "gabriel"]);
12
+
13
+ /** The options of the level strategy `venation` (NAT-016 of the nature notes): the options of `depths`, and the options of the backbone. */
14
+ export interface VenationOptions extends DepthLevelOptions {
15
+ /** The graph of the backbone. The relative neighbourhood graph has fewer links than the Gabriel graph. The default is `rng`. */
16
+ readonly backbone: VenationBackbone;
17
+ /** A later level link saves this share of the cost of a backbone link that an earlier level link uses, from 0 to 0.8. The default is 0.4. */
18
+ readonly discount: number;
19
+ /** A level link takes its way on the backbone only when that way is this many times its direct link or less. The default is 1.5. */
20
+ readonly detour: number;
21
+ /** A level with more items than this number keeps the links of `depths`. The default is 400. */
22
+ readonly maxItems: number;
23
+ }
24
+
25
+ /** The default options of the level strategy `venation`. */
26
+ export const VENATION_DEFAULTS: Readonly<VenationOptions> = Object.freeze({ backbone: "rng", discount: 0.4, detour: 1.5, maxItems: 400 });
27
+
28
+ /**
29
+ * This function gives the links of the backbone of the points, as pairs of indices `i < j` (NAT-016).
30
+ * With `rng`, the link `p q` exists when no third point `r` has `max(d(p, r), d(q, r)) < d(p, q)`.
31
+ * With `gabriel`, the link exists when no third point is inside the circle with the diameter `p q`.
32
+ */
33
+ export function backboneLinks(points: readonly Point[], backbone: VenationBackbone): [number, number][] {
34
+ const out: [number, number][] = [];
35
+ const n = points.length;
36
+ const d2 = (a: Point, b: Point): number => (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
37
+ for (let i = 0; i < n; i++) {
38
+ for (let j = i + 1; j < n; j++) {
39
+ const pq = d2(points[i]!, points[j]!);
40
+ let keep = true;
41
+ for (let r = 0; r < n && keep; r++) {
42
+ if (r === i || r === j) continue;
43
+ const pr = d2(points[i]!, points[r]!);
44
+ const qr = d2(points[j]!, points[r]!);
45
+ if (backbone === "rng" ? Math.max(pr, qr) < pq - 1e-9 : pr + qr < pq - 1e-9) keep = false;
46
+ }
47
+ if (keep) out.push([i, j]);
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /**
54
+ * This function gives a way around the box `b`, inflated by `m`, from the point `a` to the point `c` of its boundary.
55
+ * The way goes along the shorter side of the inflated box. It holds the two points at `m` out from `a` and `c`, and the corners between them.
56
+ */
57
+ export function aroundBox(b: Box, m: number, a: Point, c: Point): Point[] {
58
+ const x0 = b.x - m;
59
+ const y0 = b.y - m;
60
+ const x1 = b.x + b.w + m;
61
+ const y1 = b.y + b.h + m;
62
+ const w = x1 - x0;
63
+ const h = y1 - y0;
64
+ const per = 2 * (w + h);
65
+ // The point on the boundary of the inflated box nearest to `p`, and its place along the boundary, clockwise from the top left corner.
66
+ const onBoundary = (p: Point): { p: Point; t: number } => {
67
+ const cx = Math.min(x1, Math.max(x0, p.x));
68
+ const cy = Math.min(y1, Math.max(y0, p.y));
69
+ const dl = cx - x0;
70
+ const dr = x1 - cx;
71
+ const dt = cy - y0;
72
+ const db = y1 - cy;
73
+ const least = Math.min(dl, dr, dt, db);
74
+ if (least === dt) return { p: { x: cx, y: y0 }, t: cx - x0 };
75
+ if (least === dr) return { p: { x: x1, y: cy }, t: w + (cy - y0) };
76
+ if (least === db) return { p: { x: cx, y: y1 }, t: w + h + (x1 - cx) };
77
+ return { p: { x: x0, y: cy }, t: 2 * w + h + (y1 - cy) };
78
+ };
79
+ const corners: { p: Point; t: number }[] = [
80
+ { p: { x: x0, y: y0 }, t: 0 },
81
+ { p: { x: x1, y: y0 }, t: w },
82
+ { p: { x: x1, y: y1 }, t: w + h },
83
+ { p: { x: x0, y: y1 }, t: 2 * w + h },
84
+ ];
85
+ const A = onBoundary(a);
86
+ const C = onBoundary(c);
87
+ const ahead = (C.t - A.t + per) % per;
88
+ const out: Point[] = [A.p];
89
+ if (ahead <= per / 2) {
90
+ const list = corners.map((k) => ({ ...k, s: (k.t - A.t + per) % per })).filter((k) => k.s > 1e-9 && k.s < ahead - 1e-9);
91
+ for (const k of list.sort((p, q) => p.s - q.s)) out.push(k.p);
92
+ } else {
93
+ const back = per - ahead;
94
+ const list = corners.map((k) => ({ ...k, s: (A.t - k.t + per) % per })).filter((k) => k.s > 1e-9 && k.s < back - 1e-9);
95
+ for (const k of list.sort((p, q) => p.s - q.s)) out.push(k.p);
96
+ }
97
+ out.push(C.p);
98
+ return out.filter((p, i) => i === 0 || dist(p, out[i - 1]!) > 1e-9);
99
+ }
100
+
101
+ /**
102
+ * This function gives the links of one level along a closed venation backbone (NAT-016, after Runions and others, 2005).
103
+ * The backbone links the items by their relative neighbourhood graph or their Gabriel graph. Each backbone link goes around the other items.
104
+ * The level links go in the order of their counts, the largest first. Each one takes the cheapest way on the backbone, with the cost `L (1 - discount)` for a used backbone link.
105
+ * The way goes around each item between its two ends. A way longer than `detour` times the direct link, or a direct backbone link, keeps the direct link.
106
+ */
107
+ export function venationLinks(items: readonly LevelItem[], direct: readonly LevelLink[], o: Readonly<VenationOptions>): LevelLink[] {
108
+ if (direct.length < 2 || items.length < 3 || items.length > o.maxItems) return [...direct];
109
+ const index = new Map(items.map((it, i) => [it.id, i]));
110
+ const links = backboneLinks(items.map((it) => center(it.box)), o.backbone);
111
+ const pairs: ItemPair[] = links.map(([i, j]) => ({ source: items[i]!.id, target: items[j]!.id, count: 1 }));
112
+ const routed = levelLinks(items, pairs, o);
113
+ const n = items.length;
114
+ const next: { to: number; link: number }[][] = Array.from({ length: n }, () => []);
115
+ const cost = routed.map((l) => polylineLength(l.points));
116
+ routed.forEach((l, k) => {
117
+ const i = index.get(l.source)!;
118
+ const j = index.get(l.target)!;
119
+ next[i]!.push({ to: j, link: k });
120
+ next[j]!.push({ to: i, link: k });
121
+ });
122
+ const direct2 = new Set(links.map(([i, j]) => `${i},${j}`));
123
+ const used = new Uint8Array(routed.length);
124
+ const share = 1 - Math.min(0.8, Math.max(0, o.discount));
125
+ const order = direct.map((_, k) => k).sort((p, q) => direct[q]!.count - direct[p]!.count || direct[p]!.source.localeCompare(direct[q]!.source) || direct[p]!.target.localeCompare(direct[q]!.target));
126
+ const margin = o.margin ?? 5;
127
+ const out = [...direct];
128
+ for (const k of order) {
129
+ const l = direct[k]!;
130
+ const s = index.get(l.source);
131
+ const t = index.get(l.target);
132
+ if (s === undefined || t === undefined) continue;
133
+ if (direct2.has(`${Math.min(s, t)},${Math.max(s, t)}`)) {
134
+ const at = next[s]!.find((x) => x.to === t);
135
+ if (at) used[at.link] = 1;
136
+ continue;
137
+ }
138
+ // Dijkstra on the backbone. The graph is small, so a scan for the least open item is enough.
139
+ const best = new Float64Array(n).fill(Infinity);
140
+ const from = new Int32Array(n).fill(-1);
141
+ const via = new Int32Array(n).fill(-1);
142
+ const done = new Uint8Array(n);
143
+ best[s] = 0;
144
+ for (;;) {
145
+ let u = -1;
146
+ for (let i = 0; i < n; i++) if (!done[i] && best[i]! < Infinity && (u < 0 || best[i]! < best[u]!)) u = i;
147
+ if (u < 0 || u === t) break;
148
+ done[u] = 1;
149
+ for (const x of next[u]!) {
150
+ const c = best[u]! + cost[x.link]! * (used[x.link] ? share : 1);
151
+ if (c < best[x.to]! - 1e-9) {
152
+ best[x.to] = c;
153
+ from[x.to] = u;
154
+ via[x.to] = x.link;
155
+ }
156
+ }
157
+ }
158
+ if (from[t]! < 0) continue;
159
+ const hops: { link: number; forward: boolean }[] = [];
160
+ for (let v = t; v !== s; v = from[v]!) hops.push({ link: via[v]!, forward: routed[via[v]!]!.target === items[v]!.id });
161
+ hops.reverse();
162
+ const points: Point[] = [];
163
+ hops.forEach((h, i) => {
164
+ const p = routed[h.link]!.points;
165
+ const seq = h.forward ? [...p] : [...p].reverse();
166
+ if (i > 0) {
167
+ // The way goes around the item between the end of the hop before and the start of this hop.
168
+ const item = items[index.get(h.forward ? routed[h.link]!.source : routed[h.link]!.target)!]!;
169
+ const around = aroundBox(item.box, margin, points[points.length - 1]!, seq[0]!);
170
+ // The first point stays: it is on the box of the source. A link between two items that touch has no length, so its two points are one point.
171
+ if (points.length > 1) points.pop();
172
+ points.push(...around);
173
+ seq.shift();
174
+ }
175
+ for (const q of seq) if (points.length === 0 || dist(q, points[points.length - 1]!) > 1e-9) points.push(q);
176
+ });
177
+ if (points.length < 2 || polylineLength(points) > o.detour * polylineLength(l.points)) continue;
178
+ for (const h of hops) used[h.link] = 1;
179
+ out[k] = { ...l, points };
180
+ }
181
+ return out;
182
+ }
183
+
184
+ /**
185
+ * This function gives the zoom levels of the level strategy `venation` (NAT-016). The levels and their items are the levels of `depths`.
186
+ * In each level with groups, the links take their ways along a closed venation backbone (`venationLinks`). The last level stays as in `depths`.
187
+ */
188
+ export function venationLevels(input: LevelInput, options: Readonly<VenationOptions>): Level[] {
189
+ const levels = depthLevels(input, options);
190
+ return levels.map((level, i) => (i === levels.length - 1 ? level : { ...level, links: venationLinks(level.items, level.links, options) }));
191
+ }