@mark1russell7/highways 0.4.1 → 0.5.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 +43 -1
- package/build/index.d.ts +1377 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8202 -1515
- package/build/index.js.map +1 -1
- package/package.json +3 -1
- package/src/bench/exact/index.ts +197 -0
- package/src/bench/measures/highways.ts +93 -0
- package/src/bench/measures/index.ts +2 -0
- package/src/bench/measures/lanes.ts +304 -0
- package/src/bench/measures/levels.ts +86 -2
- package/src/bench/screen/forms.ts +11 -5
- package/src/bench/screen/measures.ts +23 -0
- package/src/bench/screen/screen.ts +42 -0
- package/src/channels.ts +56 -0
- package/src/highways/base.ts +126 -0
- package/src/highways/diffusion/index.ts +103 -0
- package/src/highways/edge-paths/index.ts +155 -0
- package/src/highways/fixtures.test-util.ts +71 -0
- package/src/highways/flow-clusters/index.ts +143 -0
- package/src/highways/flow-field/index.ts +49 -0
- package/src/highways/gates/ambiguity/index.ts +110 -0
- package/src/highways/gates/breakeven/index.ts +62 -0
- package/src/highways/gates/hysteresis/index.ts +48 -0
- package/src/highways/gates/ids/index.ts +59 -0
- package/src/highways/gates/index.ts +6 -0
- package/src/highways/gates/model.ts +129 -0
- package/src/highways/gates/stretch/index.ts +39 -0
- package/src/highways/grouped.ts +123 -0
- package/src/highways/growth/index.ts +148 -0
- package/src/highways/hdbscan/index.ts +250 -0
- package/src/highways/hough/index.ts +182 -0
- package/src/highways/hub-arcs/index.ts +181 -0
- package/src/highways/index.ts +30 -0
- package/src/highways/lanes/index.ts +189 -0
- package/src/highways/level-links/index.ts +114 -0
- package/src/highways/meet/index.ts +84 -0
- package/src/highways/mingle/index.ts +171 -0
- package/src/highways/model.ts +5 -0
- package/src/highways/nested/index.ts +143 -0
- package/src/highways/od-cells/index.ts +50 -0
- package/src/highways/quickbundles/index.ts +117 -0
- package/src/highways/ridges/index.ts +328 -0
- package/src/highways/road-classes/index.ts +74 -0
- package/src/highways/router.ts +1 -1
- package/src/highways/select/index.ts +162 -0
- package/src/highways/snap.ts +110 -0
- package/src/highways/soft-keys/index.ts +114 -0
- package/src/highways/spiral/index.ts +205 -0
- package/src/highways/stretches.ts +141 -0
- package/src/highways/stubs/index.ts +117 -0
- package/src/highways/traclus/index.ts +296 -0
- package/src/highways/weiszfeld/index.ts +105 -0
- package/src/highways/wspd/index.ts +130 -0
- package/src/highways/zipper/index.ts +168 -0
- package/src/index.ts +1 -0
- package/src/routers/shared.ts +56 -3
- package/src/strategies/builtins.ts +303 -7
- package/src/strategies/schemas.ts +300 -0
- package/src/visibility.ts +5 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { center, dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
|
|
5
|
+
|
|
6
|
+
/** The options of the highway strategy `edge-paths` (HWY-012 of the note on highways). */
|
|
7
|
+
export interface EdgePathOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
8
|
+
/** An edge takes a path of other edges only when the path is this many times its own length or less. The default is 2. */
|
|
9
|
+
readonly distortion: number;
|
|
10
|
+
/** The weight of a link in the search is its length to this power. A larger power prefers paths of many short links. The default is 2. */
|
|
11
|
+
readonly power: number;
|
|
12
|
+
/** A highway has this number of edges or more. The default is 2. */
|
|
13
|
+
readonly minSize: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The default options of the strategy `edge-paths`. */
|
|
17
|
+
export const EDGE_PATH_DEFAULTS: Readonly<Required<Omit<EdgePathOptions, "margin" | "k">>> = Object.freeze({ distortion: 2, power: 2, minSize: 2 });
|
|
18
|
+
|
|
19
|
+
/** One step of the path of an edge: the edge of the step and `true` when the path goes from its target to its source. */
|
|
20
|
+
export interface PathStep {
|
|
21
|
+
readonly edge: string;
|
|
22
|
+
readonly back: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This function gives the edge paths of the graph (Wallinger and others 2022). The edges go in the order of their lengths, the longest first.
|
|
27
|
+
* An edge looks for the shortest path of other edges between its nodes, with the weight `length ^ power`. A path is good when its length is `distortion` times the length of the edge or less.
|
|
28
|
+
* The edges of a good path lock: they keep their own ways. An edge with a good path leaves the graph of the search. The map holds the path of each such edge.
|
|
29
|
+
*/
|
|
30
|
+
export function edgePaths(input: HighwayInput, o: Readonly<Pick<EdgePathOptions, "distortion" | "power">>): Map<string, PathStep[]> {
|
|
31
|
+
const nodes = new Map(input.graph.nodes.map((n) => [n.id, center(n.box)] as const));
|
|
32
|
+
const links = input.graph.edges
|
|
33
|
+
.filter((e) => e.source.node !== e.target.node && nodes.has(e.source.node) && nodes.has(e.target.node))
|
|
34
|
+
.map((e) => ({ id: e.id, s: e.source.node, t: e.target.node, len: dist(nodes.get(e.source.node)!, nodes.get(e.target.node)!) }));
|
|
35
|
+
const around = new Map<string, number[]>();
|
|
36
|
+
links.forEach((l, i) => {
|
|
37
|
+
for (const n of [l.s, l.t]) {
|
|
38
|
+
const list = around.get(n);
|
|
39
|
+
if (list) list.push(i);
|
|
40
|
+
else around.set(n, [i]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const order = links.map((_, i) => i).sort((a, b) => links[b]!.len - links[a]!.len || (links[a]!.id < links[b]!.id ? -1 : 1));
|
|
44
|
+
const skip = new Uint8Array(links.length);
|
|
45
|
+
const lock = new Uint8Array(links.length);
|
|
46
|
+
const paths = new Map<string, PathStep[]>();
|
|
47
|
+
for (const i of order) {
|
|
48
|
+
if (lock[i]) continue;
|
|
49
|
+
const e = links[i]!;
|
|
50
|
+
skip[i] = 1;
|
|
51
|
+
const path = shortest(e.s, e.t, links, around, skip, o.power, o.distortion * e.len);
|
|
52
|
+
if (!path) {
|
|
53
|
+
skip[i] = 0;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const step of path) lock[step.index] = 1;
|
|
57
|
+
paths.set(e.id, path.map((step) => ({ edge: links[step.index]!.id, back: step.back })));
|
|
58
|
+
}
|
|
59
|
+
return paths;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** This function gives the shortest path (Dijkstra) from the node `from` to the node `to` over the links that do not skip. It gives `undefined` when the length is more than `limit`. */
|
|
63
|
+
function shortest(
|
|
64
|
+
from: string,
|
|
65
|
+
to: string,
|
|
66
|
+
links: readonly { s: string; t: string; len: number }[],
|
|
67
|
+
around: ReadonlyMap<string, readonly number[]>,
|
|
68
|
+
skip: Uint8Array,
|
|
69
|
+
power: number,
|
|
70
|
+
limit: number,
|
|
71
|
+
): { index: number; back: boolean }[] | undefined {
|
|
72
|
+
const cost = new Map<string, number>([[from, 0]]);
|
|
73
|
+
const via = new Map<string, { index: number; back: boolean; prev: string }>();
|
|
74
|
+
const done = new Set<string>();
|
|
75
|
+
const open: { node: string; c: number }[] = [{ node: from, c: 0 }];
|
|
76
|
+
while (open.length > 0) {
|
|
77
|
+
let k = 0;
|
|
78
|
+
for (let j = 1; j < open.length; j++) if (open[j]!.c < open[k]!.c || (open[j]!.c === open[k]!.c && open[j]!.node < open[k]!.node)) k = j;
|
|
79
|
+
const { node } = open.splice(k, 1)[0]!;
|
|
80
|
+
if (done.has(node)) continue;
|
|
81
|
+
done.add(node);
|
|
82
|
+
if (node === to) break;
|
|
83
|
+
for (const index of around.get(node) ?? []) {
|
|
84
|
+
if (skip[index]) continue;
|
|
85
|
+
const l = links[index]!;
|
|
86
|
+
const back = l.t === node;
|
|
87
|
+
const next = back ? l.s : l.t;
|
|
88
|
+
if (done.has(next)) continue;
|
|
89
|
+
const c = cost.get(node)! + Math.pow(Math.max(l.len, 1e-9), power);
|
|
90
|
+
if (c < (cost.get(next) ?? Infinity)) {
|
|
91
|
+
cost.set(next, c);
|
|
92
|
+
via.set(next, { index, back, prev: node });
|
|
93
|
+
open.push({ node: next, c });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!via.has(to)) return undefined;
|
|
98
|
+
const out: { index: number; back: boolean }[] = [];
|
|
99
|
+
let length = 0;
|
|
100
|
+
for (let n = to; n !== from; ) {
|
|
101
|
+
const v = via.get(n)!;
|
|
102
|
+
out.push({ index: v.index, back: v.back });
|
|
103
|
+
length += links[v.index]!.len;
|
|
104
|
+
n = v.prev;
|
|
105
|
+
}
|
|
106
|
+
if (length > limit) return undefined;
|
|
107
|
+
return out.reverse();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This function gives the highways of the edge paths (HWY-012). An edge with a good path follows the ways of the edges of its path.
|
|
112
|
+
* Each edge of the path keeps its own way through the corner graph. So the ways share their links, and each shared stretch is a highway.
|
|
113
|
+
*/
|
|
114
|
+
export function edgePathPlan(input: HighwayInput, o: Readonly<EdgePathOptions>): HighwayPlan {
|
|
115
|
+
const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
116
|
+
const corners = finder.corners;
|
|
117
|
+
const plugs = edgePlugs(input.graph);
|
|
118
|
+
const own = new Map<string, Point[]>();
|
|
119
|
+
for (const { edge, ps, pt } of plugs) {
|
|
120
|
+
const a = finder.out(ps);
|
|
121
|
+
const b = finder.out(pt);
|
|
122
|
+
own.set(edge.id, corners.path(a, b) ?? [a, b]);
|
|
123
|
+
}
|
|
124
|
+
const paths = edgePaths(input, o);
|
|
125
|
+
const ways: KeyedWay[] = [];
|
|
126
|
+
for (const { edge, ps, pt } of plugs) {
|
|
127
|
+
const path = paths.get(edge.id);
|
|
128
|
+
if (!path) {
|
|
129
|
+
ways.push({ edge: edge.id, key: "", points: own.get(edge.id)! });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const points: Point[] = [];
|
|
133
|
+
const join = (to: Point): void => {
|
|
134
|
+
const last = points[points.length - 1];
|
|
135
|
+
if (!last) {
|
|
136
|
+
points.push(to);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (dist(last, to) <= 1e-9) return;
|
|
140
|
+
const link = corners.path(last, to) ?? [last, to];
|
|
141
|
+
points.push(...link.slice(1));
|
|
142
|
+
};
|
|
143
|
+
join(finder.out(ps));
|
|
144
|
+
for (const step of path) {
|
|
145
|
+
const way = own.get(step.edge) ?? [];
|
|
146
|
+
const along = step.back ? [...way].reverse() : way;
|
|
147
|
+
if (along.length === 0) continue;
|
|
148
|
+
join(along[0]!);
|
|
149
|
+
points.push(...along.slice(1));
|
|
150
|
+
}
|
|
151
|
+
join(finder.out(pt));
|
|
152
|
+
ways.push({ edge: edge.id, key: "", points });
|
|
153
|
+
}
|
|
154
|
+
return sharedStretches(ways, { minSize: o.minSize, pull: (p) => corners.pull(p) });
|
|
155
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { checkProperties } from "../bench/properties.ts";
|
|
3
|
+
import { inputGraph } from "../bench/screen/forms.ts";
|
|
4
|
+
import { screenInputs } from "../bench/screen/set.ts";
|
|
5
|
+
import { CASES } from "../cases.ts";
|
|
6
|
+
import type { Box } from "../geom.ts";
|
|
7
|
+
import type { Graph } from "../model.ts";
|
|
8
|
+
import { STRATEGIES } from "../strategies/builtins.ts";
|
|
9
|
+
import { getStrategy } from "../strategies/model.ts";
|
|
10
|
+
import { checkPlan, type HighwayInput, type HighwayPlan } from "./model.ts";
|
|
11
|
+
import { highwayRouterStrategy } from "./router.ts";
|
|
12
|
+
|
|
13
|
+
/** This function gives a box at `x`, `y` with the size `w` by `h`. */
|
|
14
|
+
export const box = (x: number, y: number, w = 40, h = 20): Box => ({ x, y, w, h });
|
|
15
|
+
|
|
16
|
+
/** The case `case-hub-chain`. */
|
|
17
|
+
export const hub: Graph = CASES.find((c) => c.name === "case-hub-chain")!.make();
|
|
18
|
+
|
|
19
|
+
/** The input `screen-v1/small-clustered/s0`. */
|
|
20
|
+
export const clustered: Graph = inputGraph(screenInputs().find((i) => i.id === "screen-v1/small-clustered/s0")!);
|
|
21
|
+
|
|
22
|
+
/** Three edges from one area on the left to one area on the right (one of them reversed), and two edges from the top to the bottom. Two keys alternate. */
|
|
23
|
+
export const flows: Graph = {
|
|
24
|
+
nodes: [
|
|
25
|
+
...[0, 1, 2].flatMap((i) => [
|
|
26
|
+
{ id: `l${i}`, box: box(0, 30 * i) },
|
|
27
|
+
{ id: `r${i}`, box: box(600, 30 * i) },
|
|
28
|
+
]),
|
|
29
|
+
...[0, 1].flatMap((i) => [
|
|
30
|
+
{ id: `u${i}`, box: box(250 + 60 * i, -400) },
|
|
31
|
+
{ id: `d${i}`, box: box(250 + 60 * i, 400) },
|
|
32
|
+
]),
|
|
33
|
+
],
|
|
34
|
+
edges: [
|
|
35
|
+
{ id: "a", source: { node: "l0" }, target: { node: "r0" }, data: { collection: "p" } },
|
|
36
|
+
{ id: "b", source: { node: "l1" }, target: { node: "r1" }, data: { collection: "q" } },
|
|
37
|
+
{ id: "c", source: { node: "r2" }, target: { node: "l2" }, data: { collection: "p" } },
|
|
38
|
+
{ id: "x", source: { node: "u0" }, target: { node: "d0" }, data: { collection: "p" } },
|
|
39
|
+
{ id: "y", source: { node: "u1" }, target: { node: "d1" }, data: { collection: "q" } },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Three thin nodes on the left and three on the right, with three near horizontal edges between them. */
|
|
44
|
+
export const parallel: Graph = {
|
|
45
|
+
nodes: [0, 1, 2].flatMap((i) => [
|
|
46
|
+
{ id: `l${i}`, box: box(0, 8 * i, 40, 6) },
|
|
47
|
+
{ id: `r${i}`, box: box(600, 8 * i, 40, 6) },
|
|
48
|
+
]),
|
|
49
|
+
edges: [0, 1, 2].map((i) => ({ id: `e${i}`, source: { node: `l${i}` }, target: { node: `r${i}` } })),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This function adds the common tests of a highway strategy. The strategy is in the registry, and it gives a correct plan the same each time.
|
|
54
|
+
* Its routes break no hard rule after `follow`.
|
|
55
|
+
*/
|
|
56
|
+
export function commonTests<O extends object>(name: string, defaults: O, plan: (input: HighwayInput, o: O) => HighwayPlan, graphs: readonly Graph[]): void {
|
|
57
|
+
it("is in the registry, and gives a correct plan, the same each time", () => {
|
|
58
|
+
const strategy = getStrategy(STRATEGIES, "highway", name);
|
|
59
|
+
for (const graph of [...graphs, hub, clustered]) {
|
|
60
|
+
const made = strategy.run({ graph }, { ...defaults });
|
|
61
|
+
expect(checkPlan(graph, made)).toEqual([]);
|
|
62
|
+
expect(plan({ graph }, defaults)).toEqual(made);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("gives routes with no break of a hard rule after follow", () => {
|
|
67
|
+
const router = highwayRouterStrategy(getStrategy(STRATEGIES, "highway", name));
|
|
68
|
+
expect(checkProperties(hub, router.run(hub), 1_000_000)).toEqual([]);
|
|
69
|
+
expect(checkProperties(clustered, router.run(clustered), 1_000_000)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { dist, type Point } from "../../geom.ts";
|
|
2
|
+
import { groupedPlan, type EdgeGroup, type GroupSpine } from "../grouped.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { edgePlugs } from "../stretches.ts";
|
|
5
|
+
|
|
6
|
+
/** The linkage of the hierarchical clustering: the distance of two clusters from the distances of their edges. */
|
|
7
|
+
export type Linkage = "average" | "complete" | "single";
|
|
8
|
+
|
|
9
|
+
/** The values of `Linkage`. */
|
|
10
|
+
export const LINKAGES: readonly Linkage[] = Object.freeze(["average", "complete", "single"]);
|
|
11
|
+
|
|
12
|
+
/** The options of the highway strategy `flow-clusters` (HWY-023 of the note on highways). */
|
|
13
|
+
export interface FlowClusterOptions {
|
|
14
|
+
/** Two clusters join while their distance is this value or less. The distance is a share of the length of the shorter edge. The default is 0.3. */
|
|
15
|
+
readonly tau: number;
|
|
16
|
+
/** The linkage. The default is `average`. */
|
|
17
|
+
readonly linkage: Linkage;
|
|
18
|
+
/** With `true`, the source of one edge only compares with the source of the other edge. The default is `false`. */
|
|
19
|
+
readonly directed: boolean;
|
|
20
|
+
/** A cluster with this number of edges or more gets highways. The default is 2. */
|
|
21
|
+
readonly minSize: number;
|
|
22
|
+
/** The spine of each cluster. The default is `trunk`: one flow line from one area to the other area. */
|
|
23
|
+
readonly spine: GroupSpine;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The default options of the strategy `flow-clusters`. */
|
|
27
|
+
export const FLOW_CLUSTER_DEFAULTS: Readonly<FlowClusterOptions> = Object.freeze({ tau: 0.3, linkage: "average", directed: false, minSize: 2, spine: "trunk" });
|
|
28
|
+
|
|
29
|
+
/** The largest number of edges in one distance matrix. A larger graph splits into blocks of edges in the order of their sources. */
|
|
30
|
+
const BLOCK = 2500;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* This function gives the flow distance of two edges (Zhu and Guo 2014): `max(|s_e - s_f|, |t_e - t_f|) / min(len e, len f)`.
|
|
34
|
+
* Without `directed`, it is the smaller value of the two directions of `f`, and `flip` is `true` when the opposite direction is smaller.
|
|
35
|
+
*/
|
|
36
|
+
export function flowDistance(s1: Point, t1: Point, s2: Point, t2: Point, directed: boolean): { d: number; flip: boolean } {
|
|
37
|
+
const len = Math.max(1e-9, Math.min(dist(s1, t1), dist(s2, t2)));
|
|
38
|
+
const direct = Math.max(dist(s1, s2), dist(t1, t2)) / len;
|
|
39
|
+
if (directed) return { d: direct, flip: false };
|
|
40
|
+
const back = Math.max(dist(s1, t2), dist(t1, s2)) / len;
|
|
41
|
+
return back < direct ? { d: back, flip: true } : { d: direct, flip: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This function clusters the points of a distance matrix with the chain of nearest neighbours. The linkage is reducible, so the chain gives the same tree as the greedy order.
|
|
46
|
+
* The clusters join while their distance is `tau` or less. The function gives the cluster of each point, as the index of a point of the cluster.
|
|
47
|
+
*/
|
|
48
|
+
export function chainClusters(n: number, d: Float64Array, tau: number, linkage: Linkage): Int32Array {
|
|
49
|
+
const size = new Int32Array(n).fill(1);
|
|
50
|
+
const alive = new Uint8Array(n).fill(1);
|
|
51
|
+
const parent = new Int32Array(n);
|
|
52
|
+
for (let i = 0; i < n; i++) parent[i] = i;
|
|
53
|
+
// A cluster that has no neighbour within `tau` is done: under a reducible linkage, its distances only grow.
|
|
54
|
+
const done = new Uint8Array(n);
|
|
55
|
+
const find = (i: number): number => {
|
|
56
|
+
while (parent[i] !== i) i = parent[i] = parent[parent[i]!]!;
|
|
57
|
+
return i;
|
|
58
|
+
};
|
|
59
|
+
const chain: number[] = [];
|
|
60
|
+
for (;;) {
|
|
61
|
+
if (chain.length === 0) {
|
|
62
|
+
let start = -1;
|
|
63
|
+
for (let i = 0; i < n && start < 0; i++) if (alive[i] && !done[i]) start = i;
|
|
64
|
+
if (start < 0) break;
|
|
65
|
+
chain.push(start);
|
|
66
|
+
}
|
|
67
|
+
const a = chain[chain.length - 1]!;
|
|
68
|
+
let b = -1;
|
|
69
|
+
let best = Infinity;
|
|
70
|
+
for (let j = 0; j < n; j++) {
|
|
71
|
+
if (j === a || !alive[j] || done[j]) continue;
|
|
72
|
+
const v = d[a * n + j]!;
|
|
73
|
+
if (v < best || (v === best && j < b)) [best, b] = [v, j];
|
|
74
|
+
}
|
|
75
|
+
const prev = chain.length >= 2 ? chain[chain.length - 2]! : -1;
|
|
76
|
+
if (b < 0 || best > tau) {
|
|
77
|
+
done[a] = 1;
|
|
78
|
+
chain.length = 0;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (prev >= 0 && d[a * n + prev]! <= best) {
|
|
82
|
+
// `a` and `prev` are nearest neighbours of each other: they join.
|
|
83
|
+
chain.length -= 2;
|
|
84
|
+
const [i, j] = a < prev ? [a, prev] : [prev, a];
|
|
85
|
+
for (let k = 0; k < n; k++) {
|
|
86
|
+
if (!alive[k] || k === i || k === j) continue;
|
|
87
|
+
const di = d[i * n + k]!;
|
|
88
|
+
const dj = d[j * n + k]!;
|
|
89
|
+
const v = linkage === "single" ? Math.min(di, dj) : linkage === "complete" ? Math.max(di, dj) : (size[i]! * di + size[j]! * dj) / (size[i]! + size[j]!);
|
|
90
|
+
d[i * n + k] = v;
|
|
91
|
+
d[k * n + i] = v;
|
|
92
|
+
}
|
|
93
|
+
size[i] = size[i]! + size[j]!;
|
|
94
|
+
alive[j] = 0;
|
|
95
|
+
parent[j] = i;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
chain.push(b);
|
|
99
|
+
}
|
|
100
|
+
const out = new Int32Array(n);
|
|
101
|
+
for (let i = 0; i < n; i++) out[i] = find(i);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* This function gives the flow clusters of the edges (HWY-023). It clusters the edges as points in 4D, with the cut at `tau`.
|
|
107
|
+
* Each edge of a cluster gets the key of the cluster. It gets `flip` when it goes opposite to the first edge of the cluster.
|
|
108
|
+
*/
|
|
109
|
+
export function flowClusters(input: HighwayInput, o: Readonly<FlowClusterOptions>): Map<string, EdgeGroup> {
|
|
110
|
+
const ends = edgePlugs(input.graph).map(({ edge, ps, pt }) => ({ edge: edge.id, s: ps.point, t: pt.point }));
|
|
111
|
+
const groups = new Map<string, EdgeGroup>();
|
|
112
|
+
// A large graph splits into blocks of `BLOCK` edges, in the order of the x of the sources.
|
|
113
|
+
const order = ends.length > BLOCK ? [...ends].sort((a, b) => a.s.x - b.s.x || a.s.y - b.s.y || (a.edge < b.edge ? -1 : 1)) : ends;
|
|
114
|
+
for (let from = 0; from < order.length; from += BLOCK) {
|
|
115
|
+
const block = order.slice(from, from + BLOCK);
|
|
116
|
+
const n = block.length;
|
|
117
|
+
const d = new Float64Array(n * n);
|
|
118
|
+
for (let i = 0; i < n; i++)
|
|
119
|
+
for (let j = i + 1; j < n; j++) {
|
|
120
|
+
const v = flowDistance(block[i]!.s, block[i]!.t, block[j]!.s, block[j]!.t, o.directed).d;
|
|
121
|
+
d[i * n + j] = v;
|
|
122
|
+
d[j * n + i] = v;
|
|
123
|
+
}
|
|
124
|
+
const cluster = chainClusters(n, d, o.tau, o.linkage);
|
|
125
|
+
const first = new Map<number, number>();
|
|
126
|
+
block.forEach((e, i) => {
|
|
127
|
+
const c = cluster[i]!;
|
|
128
|
+
if (!first.has(c)) first.set(c, i);
|
|
129
|
+
const lead = block[first.get(c)!]!;
|
|
130
|
+
const flip = i === first.get(c) ? false : flowDistance(lead.s, lead.t, e.s, e.t, o.directed).flip;
|
|
131
|
+
groups.set(e.edge, { key: `f${from + first.get(c)!}`, flip });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return groups;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* This function gives the highways of the flow clusters (HWY-023). The clusters of `flowClusters` with `minSize` edges or more get highways.
|
|
139
|
+
* With the spine `trunk`, each cluster gets one trunk from one area to the other area. With `shared`, the edges of a cluster share their ways.
|
|
140
|
+
*/
|
|
141
|
+
export function flowClusterPlan(input: HighwayInput, o: Readonly<FlowClusterOptions>): HighwayPlan {
|
|
142
|
+
return groupedPlan(input, flowClusters(input, o), { spine: o.spine, minSize: o.minSize });
|
|
143
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Point } from "../../geom.ts";
|
|
2
|
+
import { WayFinder, type WayOptions } from "../../ways.ts";
|
|
3
|
+
import type { HighwayInput, HighwayPlan } from "../model.ts";
|
|
4
|
+
import { edgePlugs, sharedStretches, type KeyedWay } from "../stretches.ts";
|
|
5
|
+
|
|
6
|
+
/** The end of the edges that gets a field: the target of a fan-in, or the source of a fan-out. */
|
|
7
|
+
export type FieldEnd = "target" | "source";
|
|
8
|
+
|
|
9
|
+
/** The options of the highway strategy `flow-field` (EDA-046 with HWY-037 of the notes). */
|
|
10
|
+
export interface FlowFieldOptions extends Pick<WayOptions, "margin" | "k"> {
|
|
11
|
+
/** An end with this number of edges or more on one side of its box gets a field. The default is 2. */
|
|
12
|
+
readonly minFanIn: number;
|
|
13
|
+
/** The end that gets a field. The default is `target`. */
|
|
14
|
+
readonly end: FieldEnd;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The default options of the strategy `flow-field`. */
|
|
18
|
+
export const FLOW_FIELD_DEFAULTS: Readonly<Required<Omit<FlowFieldOptions, "margin" | "k">>> = Object.freeze({ minFanIn: 2, end: "target" });
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives the highways of a flow field for each target (EDA-046, HWY-037).
|
|
22
|
+
* The edges into one side of one box make a fan. The root of the fan is the point out of the median plug of the fan.
|
|
23
|
+
* Each edge of the fan goes the shortest way from the point out of its other plug to the root, through the corner graph.
|
|
24
|
+
* The shortest ways to one root make a tree, so they merge like streams and do not cross. Each stretch that two ways share is one highway.
|
|
25
|
+
* With the end `source`, the same holds for the edges out of one side of one box.
|
|
26
|
+
*/
|
|
27
|
+
export function flowFieldPlan(input: HighwayInput, o: Readonly<FlowFieldOptions>): HighwayPlan {
|
|
28
|
+
const finder = new WayFinder(input.graph.nodes, { ...(o.margin === undefined ? {} : { margin: o.margin }), ...(o.k === undefined ? {} : { k: o.k }) });
|
|
29
|
+
const fans = new Map<string, { edge: string; root: Point; far: Point; along: number }[]>();
|
|
30
|
+
for (const { edge, ps, pt } of edgePlugs(input.graph)) {
|
|
31
|
+
const [near, far, node] = o.end === "target" ? [pt, ps, edge.target.node] : [ps, pt, edge.source.node];
|
|
32
|
+
const key = `${node}\u0000${near.normal.x},${near.normal.y}`;
|
|
33
|
+
const item = { edge: edge.id, root: finder.out(near), far: finder.out(far), along: near.normal.x === 0 ? near.point.x : near.point.y };
|
|
34
|
+
const list = fans.get(key);
|
|
35
|
+
if (list) list.push(item);
|
|
36
|
+
else fans.set(key, [item]);
|
|
37
|
+
}
|
|
38
|
+
const ways: KeyedWay[] = [];
|
|
39
|
+
for (const [key, fan] of fans) {
|
|
40
|
+
if (fan.length < Math.max(2, o.minFanIn)) continue;
|
|
41
|
+
const sorted = [...fan].sort((a, b) => a.along - b.along || (a.edge < b.edge ? -1 : a.edge > b.edge ? 1 : 0));
|
|
42
|
+
const root = sorted[Math.floor((sorted.length - 1) / 2)]!.root;
|
|
43
|
+
for (const f of fan) {
|
|
44
|
+
const way = finder.corners.path(f.far, root);
|
|
45
|
+
if (way) ways.push({ edge: f.edge, key, points: o.end === "target" ? way : [...way].reverse() });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return sharedStretches(ways, { minSize: 2, pull: (p) => finder.corners.pull(p) });
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Graph } from "../../../model.ts";
|
|
2
|
+
import type { HighwayInput, HighwayPlan } from "../../model.ts";
|
|
3
|
+
import { keepMembers, type Gate } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the gate `ambiguity` (HWY-049). */
|
|
6
|
+
export interface AmbiguityGuardOptions {
|
|
7
|
+
/** Two groups of edges merge only when the merge adds this share of false pairs, or less. The default is 0.5. */
|
|
8
|
+
readonly maxAmbiguity: number;
|
|
9
|
+
/** With `node`, the default, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides. */
|
|
10
|
+
readonly link: "node" | "any";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The default options of the gate `ambiguity`. */
|
|
14
|
+
export const AMBIGUITY_GUARD_DEFAULTS: Readonly<AmbiguityGuardOptions> = Object.freeze({ maxAmbiguity: 0.5, link: "node" });
|
|
15
|
+
|
|
16
|
+
/** The adjacency of the nodes of a graph: the neighbours of each node, by the edges. */
|
|
17
|
+
function adjacency(graph: Graph): Map<string, Set<string>> {
|
|
18
|
+
const out = new Map<string, Set<string>>();
|
|
19
|
+
const add = (a: string, b: string): void => {
|
|
20
|
+
const set = out.get(a);
|
|
21
|
+
if (set) set.add(b);
|
|
22
|
+
else out.set(a, new Set([b]));
|
|
23
|
+
};
|
|
24
|
+
for (const e of graph.edges) {
|
|
25
|
+
add(e.source.node, e.target.node);
|
|
26
|
+
add(e.target.node, e.source.node);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This function gives the ambiguity of a set of nodes: the share of the pairs of distinct nodes without an edge between them.
|
|
33
|
+
* The nodes are the ends of the edges of one highway. A set with fewer than two nodes gives 0.
|
|
34
|
+
*/
|
|
35
|
+
export function nodeAmbiguity(nodes: ReadonlySet<string>, adj: ReadonlyMap<string, ReadonlySet<string>>): number {
|
|
36
|
+
const n = nodes.size;
|
|
37
|
+
if (n < 2) return 0;
|
|
38
|
+
let linked = 0;
|
|
39
|
+
for (const v of nodes) for (const w of adj.get(v) ?? []) if (w !== v && nodes.has(w)) linked++;
|
|
40
|
+
return 1 - linked / 2 / ((n * (n - 1)) / 2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A group of the members of one highway: the edges, their end nodes and the ambiguity of the nodes. */
|
|
44
|
+
interface Group {
|
|
45
|
+
readonly edges: string[];
|
|
46
|
+
readonly nodes: Set<string>;
|
|
47
|
+
readonly amb: number;
|
|
48
|
+
/** The first lane of the group, for a fixed order. */
|
|
49
|
+
readonly first: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This function applies the ambiguity guard of HWY-049 to a plan. A highway joins edges that belong together.
|
|
54
|
+
*
|
|
55
|
+
* Two nodes are a false pair when their edges share a highway and no edge joins them. The ambiguity of a group of edges is the share of false pairs among the pairs of its end nodes.
|
|
56
|
+
* In each highway, each edge starts as a group. Two groups merge only when `amb(A ∪ B) - max(amb A, amb B)` is at most `maxAmbiguity`.
|
|
57
|
+
* With the link `node`, they also must share an end node. The merge with the smallest rise comes first.
|
|
58
|
+
*
|
|
59
|
+
* The largest group stays on the highway. The other edges leave it and go alone.
|
|
60
|
+
*/
|
|
61
|
+
export function ambiguityPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<AmbiguityGuardOptions>): HighwayPlan {
|
|
62
|
+
const adj = adjacency(input.graph);
|
|
63
|
+
const ends = new Map(input.graph.edges.map((e) => [e.id, [e.source.node, e.target.node] as const]));
|
|
64
|
+
const stay = new Map<string, Set<string>>();
|
|
65
|
+
for (const h of plan.highways) {
|
|
66
|
+
let groups: Group[] = h.lanes.flatMap((edge, i): Group[] => {
|
|
67
|
+
const end = ends.get(edge);
|
|
68
|
+
if (!end) return [];
|
|
69
|
+
const nodes = new Set(end);
|
|
70
|
+
return [{ edges: [edge], nodes, amb: nodeAmbiguity(nodes, adj), first: i }];
|
|
71
|
+
});
|
|
72
|
+
for (;;) {
|
|
73
|
+
let best: { a: number; b: number; rise: number; group: Group } | undefined;
|
|
74
|
+
for (let a = 0; a < groups.length; a++)
|
|
75
|
+
for (let b = a + 1; b < groups.length; b++) {
|
|
76
|
+
const ga = groups[a]!;
|
|
77
|
+
const gb = groups[b]!;
|
|
78
|
+
if (options.link === "node" && ![...gb.nodes].some((v) => ga.nodes.has(v))) continue;
|
|
79
|
+
const nodes = new Set([...ga.nodes, ...gb.nodes]);
|
|
80
|
+
const amb = nodeAmbiguity(nodes, adj);
|
|
81
|
+
const rise = amb - Math.max(ga.amb, gb.amb);
|
|
82
|
+
if (rise > options.maxAmbiguity + 1e-12) continue;
|
|
83
|
+
if (best === undefined || rise < best.rise - 1e-12)
|
|
84
|
+
best = { a, b, rise, group: { edges: [...ga.edges, ...gb.edges], nodes, amb, first: Math.min(ga.first, gb.first) } };
|
|
85
|
+
}
|
|
86
|
+
if (!best) break;
|
|
87
|
+
const merged = best.group;
|
|
88
|
+
const { a, b } = best;
|
|
89
|
+
groups = groups.filter((_, i) => i !== a && i !== b);
|
|
90
|
+
groups.push(merged);
|
|
91
|
+
groups.sort((p, q) => p.first - q.first);
|
|
92
|
+
}
|
|
93
|
+
let top: Group | undefined;
|
|
94
|
+
for (const g of groups) if (top === undefined || g.edges.length > top.edges.length) top = g;
|
|
95
|
+
stay.set(h.id, new Set(top?.edges ?? []));
|
|
96
|
+
}
|
|
97
|
+
return keepMembers(plan, (h, m) => stay.get(h.id)?.has(m.edge) ?? false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The gate `ambiguity` (HWY-049): the ambiguity guard. A highway does not join edges whose end nodes have no edges between them. */
|
|
101
|
+
export const ambiguityGate: Gate<AmbiguityGuardOptions> = {
|
|
102
|
+
name: "ambiguity",
|
|
103
|
+
describe: "The gate `ambiguity` (HWY-049) keeps on each highway the largest group of edges that a reader can tell apart. A merge of two groups must add few false pairs of nodes.",
|
|
104
|
+
defaults: AMBIGUITY_GUARD_DEFAULTS,
|
|
105
|
+
schema: {
|
|
106
|
+
maxAmbiguity: { type: "number", min: 0, max: 1, step: 0.05, marks: [0.3, 0.5], default: 0.5, describe: "Two groups merge only when the merge adds this share of false pairs, or less." },
|
|
107
|
+
link: { type: "enum", values: ["node", "any"], default: "node", describe: "With `node`, two groups merge only when they share an end node. With `any`, only `maxAmbiguity` decides." },
|
|
108
|
+
},
|
|
109
|
+
apply: (input, plan, options) => ambiguityPlan(input, plan, options),
|
|
110
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WayOptions } from "../../../ways.ts";
|
|
2
|
+
import { lengthBetween, type HighwayInput, type HighwayPlan } from "../../model.ts";
|
|
3
|
+
import { edgeLengths, keepMembers, type Gate } from "../model.ts";
|
|
4
|
+
|
|
5
|
+
/** The options of the gate `breakeven` (HWY-047). */
|
|
6
|
+
export interface BreakEvenOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The exponent of the cost law: a stretch with `n` edges costs `n^shareAlpha` times its length. Each edge pays the share `n^(shareAlpha - 1)`.
|
|
9
|
+
* With 0, a shared stretch is one line, as the ink of B1 counts it. The default is 0.5, as the break-even floor of `corridors`.
|
|
10
|
+
*/
|
|
11
|
+
readonly shareAlpha: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The default options of the gate `breakeven`. */
|
|
15
|
+
export const BREAK_EVEN_DEFAULTS: Readonly<BreakEvenOptions> = Object.freeze({ shareAlpha: 0.5 });
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This function applies the break-even law of HWY-047 to a plan. An edge stays on its highways only when sharing saves ink.
|
|
19
|
+
*
|
|
20
|
+
* The edge pays `n^(shareAlpha - 1)` of each shared stretch, with `n` the members of the highway. Thus it saves `(1 - n^(shareAlpha - 1)) L` on a stretch of the length `L`.
|
|
21
|
+
* It adds the length of its route along the plan less the length of its route alone. An edge stays when the saving is as large as the added length.
|
|
22
|
+
* For two parallel edges at the gap `d`, this rule gives the break-even overlap `d √((2 + t) / (2 - t))` with `t = 2^shareAlpha`.
|
|
23
|
+
*
|
|
24
|
+
* An edge that does not save ink leaves each of its highways. Then the other members of those highways save less, so the test repeats until no edge leaves.
|
|
25
|
+
*/
|
|
26
|
+
export function breakEvenPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<BreakEvenOptions>, way: WayOptions = {}): HighwayPlan {
|
|
27
|
+
const lengths = edgeLengths(input.graph, plan, way);
|
|
28
|
+
let now = plan;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const saving = new Map<string, number>();
|
|
31
|
+
for (const h of now.highways) {
|
|
32
|
+
const share = 1 - Math.pow(h.members.length, options.shareAlpha - 1);
|
|
33
|
+
for (const m of h.members) saving.set(m.edge, (saving.get(m.edge) ?? 0) + share * lengthBetween(h.spine, Math.min(m.on, m.off), Math.max(m.on, m.off)));
|
|
34
|
+
}
|
|
35
|
+
const out = new Set<string>();
|
|
36
|
+
for (const [edge, saved] of saving) {
|
|
37
|
+
const l = lengths.get(edge);
|
|
38
|
+
if (l && saved < l.route - l.alone) out.add(edge);
|
|
39
|
+
}
|
|
40
|
+
if (out.size === 0) return now;
|
|
41
|
+
now = keepMembers(now, (_, m) => !out.has(m.edge));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The gate `breakeven` (HWY-047): an edge stays on a highway only when sharing saves ink. */
|
|
46
|
+
export const breakEvenGate: Gate<BreakEvenOptions> = {
|
|
47
|
+
name: "breakeven",
|
|
48
|
+
describe: "The gate `breakeven` (HWY-047) keeps an edge on a highway only when its share of the highway saves more ink than its detour adds.",
|
|
49
|
+
defaults: BREAK_EVEN_DEFAULTS,
|
|
50
|
+
schema: {
|
|
51
|
+
shareAlpha: {
|
|
52
|
+
type: "number",
|
|
53
|
+
min: 0,
|
|
54
|
+
max: 0.9,
|
|
55
|
+
step: 0.1,
|
|
56
|
+
marks: [0, 0.5],
|
|
57
|
+
default: 0.5,
|
|
58
|
+
describe: "The exponent of the cost law. An edge pays the share `n^(shareAlpha - 1)` of a stretch with `n` edges.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
apply: breakEvenPlan,
|
|
62
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HighwayInput, HighwayPlan } from "../../model.ts";
|
|
2
|
+
import { commonEdges, matchHighways, type Gate } from "../model.ts";
|
|
3
|
+
|
|
4
|
+
/** The options of the gate `hysteresis` (HWY-046). */
|
|
5
|
+
export interface HysteresisOptions {
|
|
6
|
+
/** A new highway needs this number of edges or more. The default is 3. */
|
|
7
|
+
readonly kOn: number;
|
|
8
|
+
/** A highway of the earlier plan stays while it has this number of edges or more. The default is 2. */
|
|
9
|
+
readonly kOff: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The default options of the gate `hysteresis`. */
|
|
13
|
+
export const HYSTERESIS_DEFAULTS: Readonly<HysteresisOptions> = Object.freeze({ kOn: 3, kOff: 2 });
|
|
14
|
+
|
|
15
|
+
/** Two highways of two plans are the same highway when the Jaccard index of their edges is this value or more. */
|
|
16
|
+
export const SAME_HIGHWAY = 0.5;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This function applies the count threshold with hysteresis of HWY-046 to a plan.
|
|
20
|
+
* A highway with `kOn` edges or more stays. A highway with fewer than `kOff` edges goes away.
|
|
21
|
+
* A highway between the two limits stays only when it was in the earlier plan, `input.previous`.
|
|
22
|
+
* The highway of the earlier plan with the most common edges is its match, with a Jaccard index of `SAME_HIGHWAY` or more.
|
|
23
|
+
*
|
|
24
|
+
* Thus an edit that adds or removes one edge near the limit does not make a highway come and go. Without an earlier plan, the limit is `kOn`.
|
|
25
|
+
*/
|
|
26
|
+
export function hysteresisPlan(input: HighwayInput, plan: HighwayPlan, options: Readonly<HysteresisOptions>): HighwayPlan {
|
|
27
|
+
const matched = input.previous ? matchHighways(input.previous.plan, plan, SAME_HIGHWAY, commonEdges(input.previous.graph, input.graph)) : new Map<number, number>();
|
|
28
|
+
const kOff = Math.max(2, Math.min(options.kOff, options.kOn));
|
|
29
|
+
const highways = plan.highways.filter((h, i) => {
|
|
30
|
+
const n = h.members.length;
|
|
31
|
+
if (n >= options.kOn) return true;
|
|
32
|
+
if (n < kOff) return false;
|
|
33
|
+
return matched.has(i);
|
|
34
|
+
});
|
|
35
|
+
return highways.length === plan.highways.length ? plan : { highways };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The gate `hysteresis` (HWY-046): a highway forms at `kOn` edges and goes away below `kOff` edges. */
|
|
39
|
+
export const hysteresisGate: Gate<HysteresisOptions> = {
|
|
40
|
+
name: "hysteresis",
|
|
41
|
+
describe: "The gate `hysteresis` (HWY-046) keeps a new highway with `kOn` edges or more. A highway of the earlier plan stays while it has `kOff` edges or more.",
|
|
42
|
+
defaults: HYSTERESIS_DEFAULTS,
|
|
43
|
+
schema: {
|
|
44
|
+
kOn: { type: "integer", min: 2, max: 16, step: 1, default: 3, describe: "A new highway needs this number of edges or more." },
|
|
45
|
+
kOff: { type: "integer", min: 2, max: 16, step: 1, default: 2, describe: "A highway of the earlier plan stays while it has this number of edges or more." },
|
|
46
|
+
},
|
|
47
|
+
apply: (input, plan, options) => hysteresisPlan(input, plan, options),
|
|
48
|
+
};
|