@davidsouther/agathon 0.0.0-stage → 2026.11.1
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/package.json +19 -4
- package/src/assert.ts +67 -0
- package/src/geometry/box.ts +76 -0
- package/src/geometry/circle.ts +296 -0
- package/src/geometry/geometry.ts +318 -0
- package/src/geometry/path.ts +315 -0
- package/src/geometry/quadtree.ts +321 -0
- package/src/geometry/rectangle.ts +129 -0
- package/src/geometry/rngon.ts +132 -0
- package/src/geometry/types.ts +102 -0
- package/src/geometry.ts +61 -0
- package/src/highlight/highlight.ts +85 -0
- package/src/index.ts +5 -0
- package/src/layout/grid/manager.ts +55 -0
- package/src/layout/grid.ts +1 -0
- package/src/layout/hierarchic/README.md +21 -0
- package/src/layout/hierarchic/acyclic.ts +249 -0
- package/src/layout/hierarchic/children.ts +169 -0
- package/src/layout/hierarchic/debug.ts +186 -0
- package/src/layout/hierarchic/layout.ts +266 -0
- package/src/layout/hierarchic/manager.ts +65 -0
- package/src/layout/hierarchic/normalize.ts +177 -0
- package/src/layout/hierarchic/order.ts +321 -0
- package/src/layout/hierarchic/position.ts +514 -0
- package/src/layout/hierarchic/rank.ts +629 -0
- package/src/layout/hierarchic.ts +2 -0
- package/src/layout/hulls/arrange.ts +94 -0
- package/src/layout/hulls/hulls.ts +243 -0
- package/src/layout/hulls/segments.ts +174 -0
- package/src/layout/hulls.ts +3 -0
- package/src/layout/manager/child_manager.ts +42 -0
- package/src/layout/manager/drag_manager.ts +112 -0
- package/src/layout/manager/manager.ts +425 -0
- package/src/layout/manager/pan_manager.ts +146 -0
- package/src/layout/manager/visibility_manager.ts +84 -0
- package/src/layout/manager/zoom_manager.ts +263 -0
- package/src/layout/manager.ts +6 -0
- package/src/layout/properties/defaults.ts +99 -0
- package/src/layout/properties/symbol.ts +99 -0
- package/src/layout/properties/weak_map.ts +73 -0
- package/src/layout/properties.ts +111 -0
- package/src/layout/types.ts +470 -0
- package/src/layout/util.ts +444 -0
- package/src/layout.ts +6 -0
- package/src/samples/diff_graph.ts +15 -0
- package/src/samples/edge_cases.ts +168 -0
- package/src/samples/gansner_with_states.ts +67 -0
- package/src/samples/load_generator.ts +213 -0
- package/src/samples/search_graph.ts +35 -0
- package/src/tsabre/algo.ts +126 -0
- package/src/tsabre/builder.ts +97 -0
- package/src/tsabre/compress.ts +15 -0
- package/src/tsabre/dijkstra/dijkstra.ts +76 -0
- package/src/tsabre/dijkstra/heap.ts +83 -0
- package/src/tsabre/graph.ts +808 -0
- package/src/tsabre/parse.ts +634 -0
- package/src/tsabre/print.ts +188 -0
- package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
- package/src/tsabre/sourcebuilder/printable.ts +33 -0
- package/src/tsabre.ts +6 -0
- package/src/types.ts +424 -0
- package/README.md +0 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generate a large graph, with some parameters to control size.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { LayoutGraph } from "../layout/types.ts";
|
|
6
|
+
import { TsabreGraph } from "../tsabre/graph.ts";
|
|
7
|
+
import type { Edge, Node, NodeId } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
const DINO_NAMES = `Vulcanodontidae
|
|
10
|
+
Tyrannosauridae
|
|
11
|
+
Troodontidae
|
|
12
|
+
Titanosauriformes
|
|
13
|
+
Titanosauridae
|
|
14
|
+
Thyreophora
|
|
15
|
+
Theropoda
|
|
16
|
+
Therizinosauridae
|
|
17
|
+
Tetanurae
|
|
18
|
+
Stegosauria
|
|
19
|
+
Spinosauroidea
|
|
20
|
+
Spinosauridae
|
|
21
|
+
Somphospondyli
|
|
22
|
+
Scelidosauridae
|
|
23
|
+
Sauropodomorpha
|
|
24
|
+
Sauropoda
|
|
25
|
+
Psittacosauridae
|
|
26
|
+
Protoceratopsidae
|
|
27
|
+
Plateosauridae
|
|
28
|
+
Pisanosauridae
|
|
29
|
+
Pachycephalosauria
|
|
30
|
+
Ornithopoda
|
|
31
|
+
Ornithomimidae
|
|
32
|
+
Ornithischia
|
|
33
|
+
Omeisauridae
|
|
34
|
+
Nodosauridae
|
|
35
|
+
Noasauridae
|
|
36
|
+
Neovenatoridae
|
|
37
|
+
Neosauropoda
|
|
38
|
+
Neoceratosauria
|
|
39
|
+
Metriacanthosauridae
|
|
40
|
+
Megalosauridae
|
|
41
|
+
Megalosauria
|
|
42
|
+
Massospondylidae
|
|
43
|
+
Maniraptoriformes
|
|
44
|
+
Maniraptora
|
|
45
|
+
Macronaria
|
|
46
|
+
Iojasaurus
|
|
47
|
+
Iguanodontidae
|
|
48
|
+
Hypsilophodontidae
|
|
49
|
+
Heterodontosauridae
|
|
50
|
+
Herrerasauria
|
|
51
|
+
Hecodontosaurus
|
|
52
|
+
Hadrosauridae
|
|
53
|
+
Fabrosauridae
|
|
54
|
+
Euhelopodidae
|
|
55
|
+
Dromaeosauridae
|
|
56
|
+
Diplodocidae
|
|
57
|
+
Deinonychosauria
|
|
58
|
+
Coelurosauria
|
|
59
|
+
Coeluridae
|
|
60
|
+
Coelophysoidea
|
|
61
|
+
Cetiosauridae
|
|
62
|
+
Ceratosauridae
|
|
63
|
+
Ceratosauria
|
|
64
|
+
Ceratopsidae
|
|
65
|
+
Ceratopsia
|
|
66
|
+
Cerapoda
|
|
67
|
+
Carnosauria
|
|
68
|
+
Carcharodontosauridae
|
|
69
|
+
Camarasauridae
|
|
70
|
+
Brachiosauridae
|
|
71
|
+
Aves
|
|
72
|
+
Ankylosauridae
|
|
73
|
+
Ankylosauria
|
|
74
|
+
Alvarezsauridae
|
|
75
|
+
Allosauroidea
|
|
76
|
+
Allosauridae
|
|
77
|
+
Abelisauroidea
|
|
78
|
+
Abelisauridae`.split("\n");
|
|
79
|
+
|
|
80
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVQXYZabcdefghijklmnopqrstuvqxyz".slice();
|
|
81
|
+
|
|
82
|
+
/** Generate a very large graph that looks like an Istio mesh. */
|
|
83
|
+
export function generateLoadMesh(
|
|
84
|
+
size = 20,
|
|
85
|
+
serviceCount = 1,
|
|
86
|
+
workloadCount = 2,
|
|
87
|
+
workloadConnections = 3,
|
|
88
|
+
): string {
|
|
89
|
+
let services: string[] = [];
|
|
90
|
+
let workloads: string[] = [];
|
|
91
|
+
const servicesToWorkloads: string[] = [];
|
|
92
|
+
const workloadsToServices: string[] = [];
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < Math.min(size, DINO_NAMES.length); i++) {
|
|
95
|
+
const name = DINO_NAMES[i];
|
|
96
|
+
|
|
97
|
+
const newServices = [];
|
|
98
|
+
for (let j = 0; j < serviceCount; j++) {
|
|
99
|
+
newServices.push(`${name}_service_v${j}`);
|
|
100
|
+
}
|
|
101
|
+
services = services.concat(newServices);
|
|
102
|
+
|
|
103
|
+
const newWorkloads = [];
|
|
104
|
+
for (let j = 0; j < workloadCount; j++) {
|
|
105
|
+
const workload = `${name}_workload_v${j}`;
|
|
106
|
+
newWorkloads.push(workload);
|
|
107
|
+
for (const service of newServices) {
|
|
108
|
+
servicesToWorkloads.push(`${service} -> ${workload}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
workloads = workloads.concat(newWorkloads);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const workload of workloads) {
|
|
115
|
+
for (let i = 0; i < workloadConnections; i++) {
|
|
116
|
+
const service = services[(Math.random() * services.length) | 0];
|
|
117
|
+
workloadsToServices.push(`${workload} -> ${service}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const graph = `digraph {
|
|
122
|
+
[timing=true;]
|
|
123
|
+
${services.join(";\n")};
|
|
124
|
+
|
|
125
|
+
${workloads.join(";\n")};
|
|
126
|
+
|
|
127
|
+
${servicesToWorkloads.join(";\n")};
|
|
128
|
+
|
|
129
|
+
${workloadsToServices.join(";\n")};
|
|
130
|
+
}`;
|
|
131
|
+
|
|
132
|
+
return graph;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function makeAlphaId(i: number): string {
|
|
136
|
+
let id = "";
|
|
137
|
+
do {
|
|
138
|
+
const c = i % ALPHABET.length;
|
|
139
|
+
i = Math.floor(i / ALPHABET.length);
|
|
140
|
+
id = ALPHABET[c] + id;
|
|
141
|
+
} while (i >= ALPHABET.length);
|
|
142
|
+
id = ALPHABET[i] + id;
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Generate a very large mesh with similar hulls */
|
|
147
|
+
export function generateLoadGraph(
|
|
148
|
+
nodes: number,
|
|
149
|
+
depth: number,
|
|
150
|
+
children: number,
|
|
151
|
+
connections: number,
|
|
152
|
+
seed?: number,
|
|
153
|
+
): LayoutGraph {
|
|
154
|
+
const g = new TsabreGraph<Node, Edge>();
|
|
155
|
+
const grandChildren: NodeId[] = [];
|
|
156
|
+
|
|
157
|
+
function makeChildren(pid: NodeId, d: number) {
|
|
158
|
+
for (let o = 0; o < children; o++) {
|
|
159
|
+
const id = `${pid}_${ALPHABET[o]}`;
|
|
160
|
+
g.setNode(id, { id });
|
|
161
|
+
g.setParent(id, pid);
|
|
162
|
+
if (d <= 0) {
|
|
163
|
+
grandChildren.push(id);
|
|
164
|
+
} else {
|
|
165
|
+
makeChildren(id, d - 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** For testing, controllable prng */
|
|
171
|
+
function random() {
|
|
172
|
+
if (seed === undefined) return Math.random();
|
|
173
|
+
seed = (A * seed + C) % M;
|
|
174
|
+
return seed / M;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (let n = 0; n < nodes; n++) {
|
|
178
|
+
// const id = DINO_NAMES[Math.random() * DINO_NAMES.length | 0];
|
|
179
|
+
const id = makeAlphaId(n);
|
|
180
|
+
g.setNode(id, { id });
|
|
181
|
+
makeChildren(id, depth - 1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const source of grandChildren) {
|
|
185
|
+
for (let i = 0; i < connections; i++) {
|
|
186
|
+
let target = source;
|
|
187
|
+
while (target === source) {
|
|
188
|
+
target = grandChildren[(random() * grandChildren.length) | 0];
|
|
189
|
+
}
|
|
190
|
+
const edge = { source, target };
|
|
191
|
+
g.setEdge(edge, edge);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Include edges for top level segments.
|
|
196
|
+
// TODO: Remove when layout handles top-level segments.
|
|
197
|
+
for (const id of g.children()) {
|
|
198
|
+
for (const segment of g.nodeSegments(id)) {
|
|
199
|
+
for (const edge of segment.edges) {
|
|
200
|
+
const source = g.ancestors(edge.source).pop()!;
|
|
201
|
+
const target = g.ancestors(edge.target).pop()!;
|
|
202
|
+
g.setEdge({ source, target }, { source, target });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return g;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// For prng, values from vbasic, older glibc, etc.
|
|
211
|
+
const A = 1103515245;
|
|
212
|
+
const C = 12345;
|
|
213
|
+
const M = 2 ** 32;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String graph of the Cloud Topo "NetViz Graph" which is a good starting point
|
|
3
|
+
* for search tests.
|
|
4
|
+
*/
|
|
5
|
+
export const SEARCH_GRAPH = `digraph {
|
|
6
|
+
[printDebug = true;]
|
|
7
|
+
|
|
8
|
+
//compute/country/pol;
|
|
9
|
+
//compute/us-east1-c/instances/instance1;
|
|
10
|
+
//compute/us-east1-c/instances/instance2;
|
|
11
|
+
//compute/eu-west1-c/instances/instance3;
|
|
12
|
+
//compute/us-east1-c/instanceGroups/us-ig;
|
|
13
|
+
//compute/eu-west1-c/instanceGroups/eu-ig;
|
|
14
|
+
//compute/us-east1/subnet1;
|
|
15
|
+
//compute/eu-west1/subnet1;
|
|
16
|
+
//compute/us-east1/net1;
|
|
17
|
+
//compute/us-east1/net2;
|
|
18
|
+
//compute/eu-west1/net1;
|
|
19
|
+
//compute/regions/us-east1;
|
|
20
|
+
//compute/regions/eu-west1;
|
|
21
|
+
|
|
22
|
+
//compute/country/pol -> //compute/us-east1-c/instances/instance1;
|
|
23
|
+
//compute/eu-west1-c/instances/instance3 -> //compute/us-east1-c/instances/instance2;
|
|
24
|
+
//compute/us-east1-c/instanceGroups/us-ig -> //compute/us-east1-c/instances/instance1;
|
|
25
|
+
//compute/us-east1-c/instanceGroups/us-ig -> //compute/us-east1-c/instances/instance2;
|
|
26
|
+
//compute/us-east1/subnet1 -> //compute/us-east1-c/instanceGroups/us-ig;
|
|
27
|
+
//compute/us-east1/net1 -> //compute/us-east1/subnet1;
|
|
28
|
+
//compute/regions/us-east1 -> //compute/us-east1/net1;
|
|
29
|
+
//compute/regions/us-east1 -> //compute/us-east1/net2;
|
|
30
|
+
//compute/eu-west1-c/instanceGroups/eu-ig -> //compute/eu-west1-c/instances/instance3;
|
|
31
|
+
//compute/eu-west1/subnet1 -> //compute/eu-west1-c/instanceGroups/eu-ig;
|
|
32
|
+
//compute/eu-west1/net1 -> //compute/eu-west1/subnet1;
|
|
33
|
+
//compute/regions/eu-west1 -> //compute/eu-west1/net1;
|
|
34
|
+
//compute/regions/eu-west1 -> //compute/eu-west1-c/instanceGroups/eu-ig;
|
|
35
|
+
}`;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { NodeId, SmartGraph } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export * from "./dijkstra/dijkstra.ts";
|
|
4
|
+
export * from "./dijkstra/heap.ts";
|
|
5
|
+
|
|
6
|
+
/** A list of nodes in some path of a graph, be it a traversal or cycle. */
|
|
7
|
+
export type Path = NodeId[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Break the graph into sets of all connected nodes.
|
|
11
|
+
*/
|
|
12
|
+
export function components(g: SmartGraph<unknown, unknown>): Set<Set<NodeId>> {
|
|
13
|
+
const components = new Set<Set<NodeId>>();
|
|
14
|
+
const visited = new Set<NodeId>();
|
|
15
|
+
|
|
16
|
+
function dfs(id: NodeId, component: Set<NodeId>) {
|
|
17
|
+
if (visited.has(id)) return;
|
|
18
|
+
visited.add(id);
|
|
19
|
+
component.add(id);
|
|
20
|
+
for (const next of g.neighbors(id)) {
|
|
21
|
+
dfs(next, component);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const id of g.nodes) {
|
|
26
|
+
const cmp = new Set<NodeId>();
|
|
27
|
+
dfs(id, cmp);
|
|
28
|
+
if (cmp.size > 0) components.add(cmp);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return components;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Order to conduct a DFS */
|
|
35
|
+
export const Order = {
|
|
36
|
+
PRE: 0,
|
|
37
|
+
POST: 1,
|
|
38
|
+
} as const;
|
|
39
|
+
export type Order = (typeof Order)[keyof typeof Order];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return all node ids in a graph using either pre or post order search.
|
|
43
|
+
*/
|
|
44
|
+
export function dfs(
|
|
45
|
+
graph: SmartGraph<unknown, unknown>,
|
|
46
|
+
start: Iterable<NodeId>,
|
|
47
|
+
order: Order = Order.PRE,
|
|
48
|
+
): NodeId[] {
|
|
49
|
+
const visit: NodeId[] = [];
|
|
50
|
+
const visited = new Set<NodeId>();
|
|
51
|
+
for (const id of start) {
|
|
52
|
+
doDfs(id);
|
|
53
|
+
}
|
|
54
|
+
return visit;
|
|
55
|
+
|
|
56
|
+
function doDfs(id: NodeId) {
|
|
57
|
+
if (visited.has(id)) return;
|
|
58
|
+
if (!graph.hasNode(id)) return;
|
|
59
|
+
visited.add(id);
|
|
60
|
+
|
|
61
|
+
if (order === Order.PRE) visit.push(id);
|
|
62
|
+
for (const nextId of graph.successors(id)) {
|
|
63
|
+
doDfs(nextId);
|
|
64
|
+
}
|
|
65
|
+
if (order === Order.POST) visit.push(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TarjanVisit {
|
|
70
|
+
onStack: boolean;
|
|
71
|
+
lowLink: number;
|
|
72
|
+
index: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Tarjan's algorithm to find strongly connected components.
|
|
77
|
+
*/
|
|
78
|
+
export function tarjan(g: SmartGraph<unknown, unknown>): Path[] {
|
|
79
|
+
let index = 0;
|
|
80
|
+
const stack: NodeId[] = [];
|
|
81
|
+
const visited = new Map<NodeId, TarjanVisit>();
|
|
82
|
+
const results: Path[] = [];
|
|
83
|
+
|
|
84
|
+
function dfs(id: NodeId) {
|
|
85
|
+
if (visited.has(id)) return;
|
|
86
|
+
|
|
87
|
+
const entry = { onStack: true, lowLink: index, index: index++ };
|
|
88
|
+
visited.set(id, entry);
|
|
89
|
+
stack.push(id);
|
|
90
|
+
|
|
91
|
+
for (const sId of g.successors(id)) {
|
|
92
|
+
if (!visited.has(sId)) {
|
|
93
|
+
dfs(sId);
|
|
94
|
+
entry.lowLink = Math.min(entry.lowLink, visited.get(sId)!.lowLink);
|
|
95
|
+
} else if (visited.get(sId)!.onStack) {
|
|
96
|
+
entry.lowLink = Math.min(entry.lowLink, visited.get(sId)!.index);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (entry.lowLink === entry.index) {
|
|
101
|
+
const cmpt: Path = [];
|
|
102
|
+
let tId: NodeId;
|
|
103
|
+
do {
|
|
104
|
+
tId = stack.pop()!;
|
|
105
|
+
visited.get(tId)!.onStack = false;
|
|
106
|
+
cmpt.push(tId);
|
|
107
|
+
} while (id !== tId);
|
|
108
|
+
results.push(cmpt);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const id of g.nodes) {
|
|
113
|
+
dfs(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return a list of all lists of nodes involved in cycles.
|
|
121
|
+
*/
|
|
122
|
+
export function findCycles(g: SmartGraph<unknown, unknown>): Path[] {
|
|
123
|
+
return tarjan(g).filter(
|
|
124
|
+
(c) => c.length > 1 || (c.length === 1 && g.hasEdge({ source: c[0], target: c[0] })),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { assertExists } from "../assert.ts";
|
|
2
|
+
import type { Edge, GraphData, Node, NodeId, SmartGraph } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
import { GRAPH_ROOT, TsabreGraph } from "./graph.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Given a set of graph-like data, instantiate a full graph.
|
|
8
|
+
*
|
|
9
|
+
* Takes an optional trackSegments parameter which is passed on to the
|
|
10
|
+
* underlying SmartGraph. When true, the SmartGraph will create segments for
|
|
11
|
+
* edges. When false, it will not.
|
|
12
|
+
*/
|
|
13
|
+
export function fromData<N extends Node, E extends Edge>(
|
|
14
|
+
data: GraphData<N, E>,
|
|
15
|
+
trackSegments = true,
|
|
16
|
+
): SmartGraph<N, E> {
|
|
17
|
+
const graph = new TsabreGraph<N, E>({ trackSegments });
|
|
18
|
+
mergeData<N, E>(graph, data);
|
|
19
|
+
return graph;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Given a set of graph-like data, put all of it into an existing graph.
|
|
24
|
+
*/
|
|
25
|
+
export function mergeData<N extends Node, E extends Edge>(
|
|
26
|
+
graph: SmartGraph<N, E>,
|
|
27
|
+
data: GraphData<N, E>,
|
|
28
|
+
): void {
|
|
29
|
+
const nodes = new Map<string, N>([...data.nodes].map((node) => [node.id, node]));
|
|
30
|
+
const addNode = (nodeId: NodeId) => {
|
|
31
|
+
if (graph.hasNode(nodeId) || !nodes.has(nodeId)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const node = assertExists(nodes.get(nodeId));
|
|
35
|
+
graph.setNode(nodeId, node);
|
|
36
|
+
if (node.parent) {
|
|
37
|
+
addNode(node.parent);
|
|
38
|
+
graph.setParent(node.id, node.parent);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const id of nodes.keys()) {
|
|
43
|
+
addNode(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const edge of data.edges) {
|
|
47
|
+
const [source, target, name] = [edge.source, edge.target, edge.name];
|
|
48
|
+
graph.setEdge({ source, target, name }, edge);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Given a graph and a function taking a node id and value which returns a
|
|
54
|
+
* boolean, construct a new graph with only those nodes matching the
|
|
55
|
+
* filter function, and only edges between those filtered nodes. Nodes
|
|
56
|
+
* whose parent is missing will be promoted to the next parent up,
|
|
57
|
+
* possibly being the graph root.
|
|
58
|
+
*/
|
|
59
|
+
export function filterNodes<N extends Node, E extends Edge>(
|
|
60
|
+
g: SmartGraph<N, E>,
|
|
61
|
+
fn: (value: N) => boolean,
|
|
62
|
+
): SmartGraph<N, E> {
|
|
63
|
+
const graph = new TsabreGraph<N, E>();
|
|
64
|
+
// Copy nodes
|
|
65
|
+
for (const value of g.allNodes) {
|
|
66
|
+
if (fn(value)) {
|
|
67
|
+
graph.setNode(value.id, value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Copy edges
|
|
72
|
+
for (const edge of g.allEdges) {
|
|
73
|
+
if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
|
|
74
|
+
graph.setEdge(edge, g.edge(edge)!);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Copy parents
|
|
79
|
+
const parents = new Map<NodeId, NodeId>();
|
|
80
|
+
const findParent = (id: NodeId): NodeId => {
|
|
81
|
+
const parent = g.parent(id);
|
|
82
|
+
if (parent === GRAPH_ROOT || graph.hasNode(parent)) {
|
|
83
|
+
parents.set(id, parent);
|
|
84
|
+
return parent;
|
|
85
|
+
} else if (parents.has(parent)) {
|
|
86
|
+
return parents.get(parent)!;
|
|
87
|
+
} else {
|
|
88
|
+
return findParent(parent);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const id of graph.nodes) {
|
|
93
|
+
graph.setParent(id, findParent(id));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return graph;
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Functions for compressings/decompressing strings.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// DOIT(dsouther): Find a zlib impl
|
|
6
|
+
|
|
7
|
+
/** Compresses a string to a base64 representation of deflated content. */
|
|
8
|
+
export function deflate(str: string): string {
|
|
9
|
+
return Buffer.from(str, "utf8").toString("base64");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Decompresses a string from a base64 representation of deflated content. */
|
|
13
|
+
export function inflate(b64: string): string {
|
|
14
|
+
return Buffer.from(b64, "base64").toString("utf8");
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { assertExists } from "../../assert.ts";
|
|
2
|
+
import type { Edge, NodeId, SmartGraph } from "../../types.ts";
|
|
3
|
+
import { edgeId } from "../graph.ts";
|
|
4
|
+
import { Heap } from "./heap.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A node in the Dijkstra search. Will have a distance from the source node and
|
|
8
|
+
* which node would precede it on the shortest path. If the node is the source,
|
|
9
|
+
* it will have no predecessor and distance 0. If the node is unconnected, it
|
|
10
|
+
* will have no predecessor and distance POSITIVE_INFINITY.
|
|
11
|
+
*/
|
|
12
|
+
export interface PathNode {
|
|
13
|
+
distance: number;
|
|
14
|
+
predecessor?: NodeId;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* From a starting node in a graph, return a table of PathNode entries for every
|
|
19
|
+
* node in the graph. Takes an optional weight function (default to constant 1)
|
|
20
|
+
* and an optional edge function (defaults to outedges of a node).
|
|
21
|
+
*
|
|
22
|
+
* Utilizes a straightforward Dijkstra variant, tracking the unsettled nodes in
|
|
23
|
+
* a heap-based priority queue. The algorithm thus runs in O(E + NlogN), and of
|
|
24
|
+
* course utilizes O(N) space.
|
|
25
|
+
*/
|
|
26
|
+
export function dijkstra(
|
|
27
|
+
g: SmartGraph<unknown, Edge>,
|
|
28
|
+
source: NodeId,
|
|
29
|
+
weightFn: (e: Edge) => number = () => 1,
|
|
30
|
+
edgeFn: (id: NodeId) => Iterable<Edge> = (id: NodeId) => g.outEdges(id),
|
|
31
|
+
): Map<NodeId, PathNode> {
|
|
32
|
+
const results = new Map<NodeId, PathNode>();
|
|
33
|
+
const pq = new Heap<string>();
|
|
34
|
+
|
|
35
|
+
// Initialize the PQ with the source at 0 and all other nodes at infinity
|
|
36
|
+
for (const node of g.nodes) {
|
|
37
|
+
const distance = node === source ? 0 : Number.POSITIVE_INFINITY;
|
|
38
|
+
results.set(node, { distance });
|
|
39
|
+
pq.insert(distance, node);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while (!pq.isEmpty) {
|
|
43
|
+
// first iteration will always start with source, later iterations will
|
|
44
|
+
// find the next closest unsettled node.
|
|
45
|
+
const currentId = assertExists(pq.pop())[1];
|
|
46
|
+
const currentHop = assertExists(results.get(currentId));
|
|
47
|
+
|
|
48
|
+
// If the distance is infinity, this node never got visited in the search,
|
|
49
|
+
// and all other nodes left in the PQ are also unconnected. Stop early.
|
|
50
|
+
if (currentHop.distance === Number.POSITIVE_INFINITY) break;
|
|
51
|
+
|
|
52
|
+
const edges = edgeFn(currentId); // For debugging, store locally
|
|
53
|
+
for (const edge of edges) {
|
|
54
|
+
// Figure out which side is the other end of the edge, and load its hop.
|
|
55
|
+
const nextId = edge.source === currentId ? edge.target : edge.source;
|
|
56
|
+
const nextHop = assertExists(results.get(nextId), `${nextId} not in graph`);
|
|
57
|
+
|
|
58
|
+
// Negative weights break priority queues and Dijkstra.
|
|
59
|
+
const weight = weightFn(edge);
|
|
60
|
+
if (weight < 0) {
|
|
61
|
+
throw new Error(`disjkstra requires positive weights, got ${weight} on ${edgeId(edge)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Update the current hop, if this edge is shorter than its current
|
|
65
|
+
// distance.
|
|
66
|
+
const distance = currentHop.distance + weight;
|
|
67
|
+
if (distance < nextHop.distance) {
|
|
68
|
+
nextHop.distance = distance;
|
|
69
|
+
nextHop.predecessor = currentId;
|
|
70
|
+
pq.insert(distance, nextId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { assert } from "../../assert.ts";
|
|
2
|
+
|
|
3
|
+
export class Heap<Value = undefined> {
|
|
4
|
+
private index = 0;
|
|
5
|
+
private heap: Array<[number, Value]>;
|
|
6
|
+
|
|
7
|
+
get size(): number {
|
|
8
|
+
return this.index;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get isEmpty(): boolean {
|
|
12
|
+
return this.index === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor(capacity = 15) {
|
|
16
|
+
this.heap = new Array(capacity);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
insert(priority: number, value: Value): void {
|
|
20
|
+
assert(!isNaN(priority), "AssertionError: heap priority must be defined");
|
|
21
|
+
this.heap[this.index] = [priority, value];
|
|
22
|
+
|
|
23
|
+
// Shuffle up
|
|
24
|
+
let i = this.index,
|
|
25
|
+
p = parent(i);
|
|
26
|
+
while (i > 0 && this.heap[i][0] < this.heap[p][0]) {
|
|
27
|
+
const t = this.heap[p];
|
|
28
|
+
this.heap[p] = this.heap[i];
|
|
29
|
+
this.heap[i] = t;
|
|
30
|
+
i = p;
|
|
31
|
+
p = parent(i);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.index += 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
peek(): [number, Value] | undefined {
|
|
38
|
+
return this.heap[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pop(): [number, Value] | undefined {
|
|
42
|
+
if (this.isEmpty) return undefined;
|
|
43
|
+
const ret = this.heap[0];
|
|
44
|
+
this.index -= 1;
|
|
45
|
+
this.heap[0] = this.heap[this.index];
|
|
46
|
+
delete this.heap[this.index];
|
|
47
|
+
|
|
48
|
+
// Shuffle down
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < this.index) {
|
|
51
|
+
const m = this.heap[i][0];
|
|
52
|
+
const li = left(i);
|
|
53
|
+
const ri = right(i);
|
|
54
|
+
|
|
55
|
+
const l = this.heap[li]?.[0] ?? Number.POSITIVE_INFINITY;
|
|
56
|
+
const r = this.heap[ri]?.[0] ?? Number.POSITIVE_INFINITY;
|
|
57
|
+
|
|
58
|
+
const ni = m > l ? (l > r ? ri : li) : m > r ? (l > r ? ri : li) : i;
|
|
59
|
+
if (ni === i) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const t = this.heap[ni];
|
|
64
|
+
this.heap[ni] = this.heap[i];
|
|
65
|
+
this.heap[i] = t;
|
|
66
|
+
i = ni;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return ret;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parent(i: number): number {
|
|
74
|
+
return Math.floor((i - 1) / 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function left(i: number): number {
|
|
78
|
+
return i * 2 + 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function right(i: number): number {
|
|
82
|
+
return i * 2 + 2;
|
|
83
|
+
}
|