@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,808 @@
|
|
|
1
|
+
import { assert } from "../assert.ts";
|
|
2
|
+
import {
|
|
3
|
+
type Edge,
|
|
4
|
+
EdgeDirectionAlongSegment,
|
|
5
|
+
type GraphViolation,
|
|
6
|
+
GraphViolationType,
|
|
7
|
+
type NodeId,
|
|
8
|
+
type Segment,
|
|
9
|
+
type SegmentId,
|
|
10
|
+
type SmartGraph,
|
|
11
|
+
} from "../types.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @desc Error to throw when a cycle is found setting a parent.
|
|
15
|
+
*/
|
|
16
|
+
export const CYCLE_SETTING_PARENT = "Setting parent that will cause a cycle";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ID for the root node. Implicitly present in _all_ graphs, used internally as
|
|
20
|
+
* a sigil and never returned to users.
|
|
21
|
+
*/
|
|
22
|
+
export const GRAPH_ROOT = "⌀"; // \u2300
|
|
23
|
+
|
|
24
|
+
/** Delimiter between parts of an edge's id. */
|
|
25
|
+
export const EDGE_KEY_DELIM = "→"; // \u2192
|
|
26
|
+
const DEFAULT_EDGE_NAME = "⌑"; // \u2311
|
|
27
|
+
const SEGMENT_DELIM = "~";
|
|
28
|
+
|
|
29
|
+
type EdgeId = string;
|
|
30
|
+
|
|
31
|
+
// Safer set of constants, but less fun.
|
|
32
|
+
// const GRAPH_NODE = '\x00';
|
|
33
|
+
// const EDGE_KEY_DELIM = '\x01';
|
|
34
|
+
// const DEFAULT_EDGE_NAME = '\x00';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A class to manage a directed graph.
|
|
38
|
+
*/
|
|
39
|
+
export class TsabreGraph<NodeValue = {}, EdgeValue = {}>
|
|
40
|
+
implements SmartGraph<NodeValue, EdgeValue>
|
|
41
|
+
{
|
|
42
|
+
get violations(): ReadonlyArray<GraphViolation> {
|
|
43
|
+
return [...this.graphViolations];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get nodeCount(): number {
|
|
47
|
+
return this.nodesMap.size;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get edgeCount(): number {
|
|
51
|
+
return this.edgeMap.size;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get segmentCount(): number {
|
|
55
|
+
return this.allSegments.size;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private readonly graphViolations: GraphViolation[] = [];
|
|
59
|
+
|
|
60
|
+
private readonly nodesMap = new Map<NodeId, NodeValue>();
|
|
61
|
+
private readonly parentMap = new Map<NodeId, NodeId>();
|
|
62
|
+
private readonly childrenMap = new Map<NodeId, Set<NodeId>>();
|
|
63
|
+
readonly leaves = new Set<NodeId>();
|
|
64
|
+
private readonly predMap = new Map<NodeId, Map<NodeId, number>>();
|
|
65
|
+
private readonly sucsMap = new Map<NodeId, Map<NodeId, number>>();
|
|
66
|
+
private readonly inMap = new Map<NodeId, Map<EdgeId, Edge>>();
|
|
67
|
+
private readonly outMap = new Map<NodeId, Map<EdgeId, Edge>>();
|
|
68
|
+
private readonly edgeMap = new Map<EdgeId, Edge>();
|
|
69
|
+
private readonly edgeLabelMap = new Map<EdgeId, EdgeValue>();
|
|
70
|
+
readonly allSegments = new Set<Segment<EdgeValue>>();
|
|
71
|
+
|
|
72
|
+
private readonly trackSegments: boolean;
|
|
73
|
+
private readonly edgeSegmentsMap = new Map<EdgeId, Set<Segment<EdgeValue>>>();
|
|
74
|
+
private readonly nodeSegmentsMap = new Map<NodeId, Set<Segment<EdgeValue>>>();
|
|
75
|
+
private readonly childSegmentsMap = new Map<NodeId, Set<Segment<EdgeValue>>>();
|
|
76
|
+
// Map from sourceid :: targetid :: path :: segment
|
|
77
|
+
private readonly pathSegmentsMap = new Map<
|
|
78
|
+
NodeId,
|
|
79
|
+
Map<NodeId, Map<string, Segment<EdgeValue>>>
|
|
80
|
+
>();
|
|
81
|
+
|
|
82
|
+
get nodes(): Set<NodeId> {
|
|
83
|
+
return new Set(this.nodesMap.keys());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get allNodes(): Iterable<NodeValue> {
|
|
87
|
+
return this.nodesMap.values();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
constructor({ trackSegments = true }: { trackSegments?: boolean } = {}) {
|
|
91
|
+
this.trackSegments = trackSegments;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
node(id?: NodeId): NodeValue | undefined {
|
|
95
|
+
return id ? this.nodesMap.get(id) : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
hasNode(id?: NodeId): boolean {
|
|
99
|
+
return id ? this.nodesMap.has(id) : false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setNode(id: NodeId, value: NodeValue): this {
|
|
103
|
+
if (this.nodesMap.has(id)) {
|
|
104
|
+
this.nodesMap.set(id, value);
|
|
105
|
+
} else {
|
|
106
|
+
this.nodesMap.set(id, value);
|
|
107
|
+
this.parentMap.set(id, GRAPH_ROOT);
|
|
108
|
+
this.childrenMap.set(id, new Set<NodeId>());
|
|
109
|
+
this.children(GRAPH_ROOT).add(id);
|
|
110
|
+
this.leaves.add(id);
|
|
111
|
+
}
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
removeNode(id: NodeId): this {
|
|
116
|
+
if (!this.hasNode(id)) {
|
|
117
|
+
return this.addViolation({
|
|
118
|
+
violation: GraphViolationType.REMOVE_NODE_VIOLATION,
|
|
119
|
+
node: id,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const pid = this.parent(id);
|
|
124
|
+
this.children(pid).delete(id);
|
|
125
|
+
this.parentMap.delete(id);
|
|
126
|
+
for (const child of this.children(id)) {
|
|
127
|
+
this.setParent(child, pid);
|
|
128
|
+
}
|
|
129
|
+
this.childrenMap.delete(id);
|
|
130
|
+
this.leaves.delete(id);
|
|
131
|
+
|
|
132
|
+
if (this.inMap.has(id)) {
|
|
133
|
+
for (const edge of this.inMap.get(id)!.values()) {
|
|
134
|
+
this.removeEdge(edge);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this.outMap.has(id)) {
|
|
139
|
+
for (const edge of this.outMap.get(id)!.values()) {
|
|
140
|
+
this.removeEdge(edge);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.inMap.delete(id);
|
|
144
|
+
this.outMap.delete(id);
|
|
145
|
+
this.predMap.delete(id);
|
|
146
|
+
this.sucsMap.delete(id);
|
|
147
|
+
this.nodesMap.delete(id);
|
|
148
|
+
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get allEdges(): Iterable<EdgeValue> {
|
|
153
|
+
return this.edgeLabelMap.values();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
edges(id?: NodeId, neighbor?: NodeId): Iterable<Edge> {
|
|
157
|
+
if (id) {
|
|
158
|
+
return [...this.inEdges(id, neighbor), ...this.outEdges(id, neighbor)];
|
|
159
|
+
} else {
|
|
160
|
+
return this.edgeMap.values();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
inEdges(id: NodeId, source?: NodeId): Iterable<Edge> {
|
|
165
|
+
const ins = this.inMap.get(id) || new Map<EdgeId, Edge>();
|
|
166
|
+
let edges = Array.from(ins.values());
|
|
167
|
+
if (source !== undefined) {
|
|
168
|
+
edges = edges.filter((e) => e.source === source);
|
|
169
|
+
}
|
|
170
|
+
return edges;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
outEdges(id: NodeId, target?: NodeId): Iterable<Edge> {
|
|
174
|
+
const outs = this.outMap.get(id) || new Map<EdgeId, Edge>();
|
|
175
|
+
let edges = Array.from(outs.values());
|
|
176
|
+
if (target !== undefined) {
|
|
177
|
+
edges = edges.filter((e) => e.target === target);
|
|
178
|
+
}
|
|
179
|
+
return edges;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private edgeIds(id: NodeId): Iterable<Edge> {
|
|
183
|
+
const outs = this.outMap.get(id) || new Map<EdgeId, Edge>();
|
|
184
|
+
const ins = this.inMap.get(id) || new Map<EdgeId, Edge>();
|
|
185
|
+
|
|
186
|
+
const edgeIds = new Set<Edge>();
|
|
187
|
+
for (const edge of outs.values()) edgeIds.add(edge);
|
|
188
|
+
for (const edge of ins.values()) edgeIds.add(edge);
|
|
189
|
+
return edgeIds;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
edge(e: Edge): EdgeValue | undefined {
|
|
193
|
+
const id = edgeId(e);
|
|
194
|
+
return this.edgeLabelMap.get(id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
hasEdge(e: Edge): boolean {
|
|
198
|
+
const id = edgeId(e);
|
|
199
|
+
return this.edgeLabelMap.has(id);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setEdge(e: Edge, value: EdgeValue): this {
|
|
203
|
+
if (!this.hasNode(e.source) || !this.hasNode(e.target)) {
|
|
204
|
+
return this.addViolation({
|
|
205
|
+
violation: GraphViolationType.EDGE_VIOLATION,
|
|
206
|
+
source: e.source,
|
|
207
|
+
target: e.target,
|
|
208
|
+
name: e.name,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const id = edgeId(e);
|
|
213
|
+
if (this.edgeLabelMap.has(id)) {
|
|
214
|
+
this.edgeLabelMap.set(id, value);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.edgeLabelMap.set(id, value);
|
|
219
|
+
|
|
220
|
+
this.edgeMap.set(id, e);
|
|
221
|
+
incEntry(this.preds(e.target), e.source);
|
|
222
|
+
incEntry(this.sucs(e.source), e.target);
|
|
223
|
+
this.in(e.target).set(id, e);
|
|
224
|
+
this.out(e.source).set(id, e);
|
|
225
|
+
|
|
226
|
+
this.updateSegmentsForEdge(e);
|
|
227
|
+
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
removeEdge(e: Edge): this {
|
|
232
|
+
const id = edgeId(e);
|
|
233
|
+
if (!this.edgeMap.has(id)) {
|
|
234
|
+
return this.addViolation({
|
|
235
|
+
violation: GraphViolationType.REMOVE_EDGE_VIOLATION,
|
|
236
|
+
source: e.source,
|
|
237
|
+
target: e.target,
|
|
238
|
+
name: e.name,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.removeSegmentsForEdge(e);
|
|
243
|
+
this.edgeSegmentsMap.delete(id);
|
|
244
|
+
|
|
245
|
+
this.edgeMap.delete(id);
|
|
246
|
+
this.edgeLabelMap.delete(id);
|
|
247
|
+
decEntry(this.preds(e.target), e.source);
|
|
248
|
+
decEntry(this.sucs(e.source), e.target);
|
|
249
|
+
if (this.inMap.has(e.target)) {
|
|
250
|
+
this.inMap.get(e.target)!.delete(id);
|
|
251
|
+
}
|
|
252
|
+
if (this.outMap.has(e.source)) {
|
|
253
|
+
this.outMap.get(e.source)!.delete(id);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get sources(): Set<NodeId> {
|
|
260
|
+
return new Set(Array.from(this.nodes).filter((id) => this.in(id).size === 0));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get sinks(): Set<NodeId> {
|
|
264
|
+
return new Set(Array.from(this.nodes).filter((id) => this.out(id).size === 0));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
parent(id: NodeId): NodeId {
|
|
268
|
+
return this.parentMap.get(id) || GRAPH_ROOT;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
setParent(id: NodeId, parent: NodeId = GRAPH_ROOT): this {
|
|
272
|
+
if (!this.hasNode(id)) {
|
|
273
|
+
return this.addViolation({
|
|
274
|
+
violation: GraphViolationType.CHILD_NODE_VIOLATION,
|
|
275
|
+
node: id,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (parent !== GRAPH_ROOT && !this.hasNode(parent)) {
|
|
279
|
+
return this.addViolation({
|
|
280
|
+
violation: GraphViolationType.PARENT_NODE_VIOLATION,
|
|
281
|
+
node: id,
|
|
282
|
+
parent,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (parent !== GRAPH_ROOT) {
|
|
286
|
+
for (let ancestor = parent; ancestor !== GRAPH_ROOT; ancestor = this.parent(ancestor)) {
|
|
287
|
+
if (ancestor === id) {
|
|
288
|
+
throw new Error(CYCLE_SETTING_PARENT);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.children(this.parent(id)).delete(id);
|
|
294
|
+
this.parentMap.set(id, parent);
|
|
295
|
+
this.children(parent).add(id);
|
|
296
|
+
this.leaves.delete(parent);
|
|
297
|
+
|
|
298
|
+
// Update all edges to nodes who have changed trees
|
|
299
|
+
if (this.trackSegments) {
|
|
300
|
+
for (const descendant of [id, ...this.descendants(id)]) {
|
|
301
|
+
for (const edge of this.edgeIds(descendant)) {
|
|
302
|
+
this.updateSegmentsForEdge(edge);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
children(id: NodeId = GRAPH_ROOT): Set<NodeId> {
|
|
310
|
+
const children = this.childrenMap.get(id) || new Set();
|
|
311
|
+
this.childrenMap.set(id, children);
|
|
312
|
+
return children;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
hasChildren(id: NodeId = GRAPH_ROOT): boolean {
|
|
316
|
+
return this.children(id).size > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
hasParent(id: NodeId): boolean {
|
|
320
|
+
return this.parent(id) !== GRAPH_ROOT;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
descendants(id: NodeId | undefined = GRAPH_ROOT): Set<NodeId> {
|
|
324
|
+
const stack = [];
|
|
325
|
+
// Separate loop rather than initializing stack to id because this does not
|
|
326
|
+
// include the id. That allows [...ancestors, ...descendants] to not double
|
|
327
|
+
// cound id.
|
|
328
|
+
for (const child of this.children(id)) {
|
|
329
|
+
stack.push(child);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const nodes = new Set<NodeId>();
|
|
333
|
+
while ((id = stack.pop())) {
|
|
334
|
+
nodes.add(id);
|
|
335
|
+
for (const child of this.children(id)) {
|
|
336
|
+
stack.push(child);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return nodes;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
ancestors(id: NodeId = GRAPH_ROOT): NodeId[] {
|
|
343
|
+
const ancestors = [];
|
|
344
|
+
while (this.hasNode(id)) {
|
|
345
|
+
ancestors.push(id);
|
|
346
|
+
id = this.parent(id);
|
|
347
|
+
}
|
|
348
|
+
return ancestors;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private in(id: NodeId): Map<EdgeId, Edge> {
|
|
352
|
+
const map = this.inMap.get(id) || new Map<EdgeId, Edge>();
|
|
353
|
+
this.inMap.set(id, map);
|
|
354
|
+
return map;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private out(id: NodeId): Map<EdgeId, Edge> {
|
|
358
|
+
const map = this.outMap.get(id) || new Map<EdgeId, Edge>();
|
|
359
|
+
this.outMap.set(id, map);
|
|
360
|
+
return map;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Utility to get a map, with keys for predecessor (incoming) nodeId, and
|
|
365
|
+
* and value being the count of edges with that node as its source.
|
|
366
|
+
*/
|
|
367
|
+
private preds(id: NodeId): Map<NodeId, number> {
|
|
368
|
+
const preds = this.predMap.get(id) || new Map<EdgeId, number>();
|
|
369
|
+
this.predMap.set(id, preds);
|
|
370
|
+
return preds;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
predecessors(id: NodeId): Set<NodeId> {
|
|
374
|
+
return new Set(this.preds(id).keys());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Utility to get a map, with keys for successor (ioutgoing) nodeId, and
|
|
379
|
+
* and value being the count of edges with that node as its target.
|
|
380
|
+
*/
|
|
381
|
+
private sucs(id: NodeId): Map<NodeId, number> {
|
|
382
|
+
const sucs = this.sucsMap.get(id) || new Map<EdgeId, number>();
|
|
383
|
+
this.sucsMap.set(id, sucs);
|
|
384
|
+
return sucs;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
successors(id: NodeId): Set<NodeId> {
|
|
388
|
+
return new Set(this.sucs(id).keys());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
neighbors(id: NodeId): Set<NodeId> {
|
|
392
|
+
const s = new Set<NodeId>();
|
|
393
|
+
for (const node of this.predecessors(id)) s.add(node);
|
|
394
|
+
for (const node of this.successors(id)) s.add(node);
|
|
395
|
+
return s;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
isLeaf(id: NodeId): boolean {
|
|
399
|
+
return this.successors(id).size === 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
hasSegment(s: Segment<EdgeValue>): boolean {
|
|
403
|
+
return this.pathSegmentsMap.has(s.source) && this.pathSegmentsMap.get(s.source)!.has(s.target);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
isTopSegment(s: Segment<EdgeValue>): boolean {
|
|
407
|
+
if (!this.hasSegment(s)) return false;
|
|
408
|
+
// A node is top-level if the source does not have a parent.
|
|
409
|
+
return !this.hasParent(s.source);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
topSegment(source: NodeId, target: NodeId): Segment<EdgeValue> | undefined {
|
|
413
|
+
if (target < source) {
|
|
414
|
+
[source, target] = [target, source];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const segA = this.pathSegmentsMap.get(source);
|
|
418
|
+
if (!segA) return undefined;
|
|
419
|
+
const segB = segA.get(target);
|
|
420
|
+
if (!segB) return undefined;
|
|
421
|
+
const path = pathId([source, target]);
|
|
422
|
+
return segB.get(path);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
nodeSegments(id: NodeId): Iterable<Segment<EdgeValue>> {
|
|
426
|
+
return this.nodeSegmentsMap.get(id) || new Set();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
childSegments(id = GRAPH_ROOT): Iterable<Segment<EdgeValue>> {
|
|
430
|
+
return this.childSegmentsMap.get(id) || new Set();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
get topSegments(): Iterable<Segment<EdgeValue>> {
|
|
434
|
+
return this.childSegments();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
edgeSegments(edge: Edge): Iterable<Segment<EdgeValue>> {
|
|
438
|
+
const eid = edgeId(edge);
|
|
439
|
+
return this.edgeSegmentsMap.get(eid) || new Set();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
segmentsUnderNode(nodeId = GRAPH_ROOT): Iterable<Segment<EdgeValue>> {
|
|
443
|
+
const segments = new Set<Segment<EdgeValue>>();
|
|
444
|
+
|
|
445
|
+
// Include segments pointing up to this node
|
|
446
|
+
for (const segment of this.nodeSegments(nodeId)) {
|
|
447
|
+
if (segment.target === nodeId && this.parent(segment.source) === nodeId) {
|
|
448
|
+
segments.add(segment);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Include segments between children of this node
|
|
453
|
+
for (const id of this.children(nodeId)) {
|
|
454
|
+
for (const segment of this.nodeSegments(id)) {
|
|
455
|
+
if (this.parent(segment.source) === nodeId && this.parent(segment.target) === nodeId) {
|
|
456
|
+
segments.add(segment);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return segments;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
successorSegments(id: NodeId): Iterable<Segment<EdgeValue>> {
|
|
465
|
+
return this.getSegmentNeighbors(id, this.outMap);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
predecessorSegments(id: NodeId): Iterable<Segment<EdgeValue>> {
|
|
469
|
+
return this.getSegmentNeighbors(id, this.inMap);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
segmentNeighborhood(id: NodeId): Iterable<Segment<EdgeValue>> {
|
|
473
|
+
const neighborhood = new Set<Segment<EdgeValue>>();
|
|
474
|
+
for (const segment of this.successorSegments(id)) {
|
|
475
|
+
neighborhood.add(segment);
|
|
476
|
+
}
|
|
477
|
+
for (const segment of this.predecessorSegments(id)) {
|
|
478
|
+
neighborhood.add(segment);
|
|
479
|
+
}
|
|
480
|
+
return neighborhood;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
edgeDirectionAlongSegment(edge: Edge, segment: Segment<EdgeValue>): EdgeDirectionAlongSegment {
|
|
484
|
+
const spid = this.parent(segment.source);
|
|
485
|
+
const tpid = this.parent(segment.target);
|
|
486
|
+
|
|
487
|
+
const edgeSourceChain = this.ancestors(edge.source);
|
|
488
|
+
const edgeTargetChain = this.ancestors(edge.target);
|
|
489
|
+
|
|
490
|
+
if (spid === tpid) {
|
|
491
|
+
// This is a segment between two nodes with the same parent. Because
|
|
492
|
+
// segments are normalized alphabetically, check whether the segment
|
|
493
|
+
// source and target line up with the edge source and segment
|
|
494
|
+
// (SOURCE_TARGET) or if the segment is reversed from the edge.
|
|
495
|
+
if (edgeSourceChain.includes(segment.source) && edgeTargetChain.includes(segment.target)) {
|
|
496
|
+
return EdgeDirectionAlongSegment.SOURCE_TARGET;
|
|
497
|
+
}
|
|
498
|
+
if (edgeSourceChain.includes(segment.target) && edgeTargetChain.includes(segment.source)) {
|
|
499
|
+
return EdgeDirectionAlongSegment.TARGET_SOURCE;
|
|
500
|
+
}
|
|
501
|
+
return EdgeDirectionAlongSegment.NOT_CROSSING;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// If the segment is not between siblings, check the direction of each
|
|
505
|
+
// ancestor chain.
|
|
506
|
+
|
|
507
|
+
const sourceChainSourceIndex = edgeSourceChain.indexOf(segment.source);
|
|
508
|
+
const sourceChainTargetIndex = edgeSourceChain.indexOf(segment.target);
|
|
509
|
+
if (sourceChainSourceIndex > -1 && sourceChainTargetIndex > -1) {
|
|
510
|
+
// At this point, because of the rules of segments, scti and scsi are
|
|
511
|
+
// apart by 1.
|
|
512
|
+
const direction = sourceChainTargetIndex - sourceChainSourceIndex;
|
|
513
|
+
assert(direction === 1 || direction === -1, `Invalid segment positions.`);
|
|
514
|
+
if (direction === 1) return EdgeDirectionAlongSegment.SOURCE_TARGET;
|
|
515
|
+
if (direction === -1) return EdgeDirectionAlongSegment.TARGET_SOURCE;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const targetChainSourceIndex = edgeTargetChain.indexOf(segment.source);
|
|
519
|
+
const targetChainTargetIndex = edgeTargetChain.indexOf(segment.target);
|
|
520
|
+
if (targetChainSourceIndex > -1 && targetChainTargetIndex > -1) {
|
|
521
|
+
// At this point, because of the rules of segments, scti and scsi are
|
|
522
|
+
// apart by 1.
|
|
523
|
+
const direction = targetChainTargetIndex - targetChainSourceIndex;
|
|
524
|
+
assert(direction === 1 || direction === -1, `Invalid segment positions.`);
|
|
525
|
+
if (direction === 1) return EdgeDirectionAlongSegment.TARGET_SOURCE;
|
|
526
|
+
if (direction === -1) return EdgeDirectionAlongSegment.SOURCE_TARGET;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// If the edge and segment haven't coincided at this point, the edge is not
|
|
530
|
+
// part of the segment. Should this be an error case?
|
|
531
|
+
|
|
532
|
+
return EdgeDirectionAlongSegment.NOT_CROSSING;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private getSegmentNeighbors(id: NodeId, map: Map<NodeId, Map<EdgeId, Edge>>) {
|
|
536
|
+
const segments = this.getAdjacentSegments(map.get(id));
|
|
537
|
+
for (const descendant of this.descendants(id)) {
|
|
538
|
+
for (const segment of this.getAdjacentSegments(map.get(descendant))) {
|
|
539
|
+
segments.add(segment);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return segments;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private getAdjacentSegments(map = new Map<EdgeId, Edge>()) {
|
|
546
|
+
const edges = map.values();
|
|
547
|
+
|
|
548
|
+
const neighborhood = new Set<Segment<EdgeValue>>();
|
|
549
|
+
for (const edge of edges) {
|
|
550
|
+
for (const segment of this.edgeSegments(edge)) {
|
|
551
|
+
neighborhood.add(segment);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return neighborhood;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* For a given edge, clear any segments it's already part of. Find or create
|
|
560
|
+
* fresh segments, then add the edge to those. Overall, O(depth of edge).
|
|
561
|
+
*/
|
|
562
|
+
private updateSegmentsForEdge(e: Edge) {
|
|
563
|
+
if (!this.trackSegments) return;
|
|
564
|
+
|
|
565
|
+
const eid = edgeId(e);
|
|
566
|
+
if (this.edgeSegmentsMap.has(eid)) {
|
|
567
|
+
this.removeSegmentsForEdge(e);
|
|
568
|
+
} else {
|
|
569
|
+
this.edgeSegmentsMap.set(eid, new Set());
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const segments = this.buildSegmentsForEdge(e);
|
|
573
|
+
|
|
574
|
+
const edgeSegments = this.edgeSegmentsMap.get(eid)!;
|
|
575
|
+
for (const segment of segments) {
|
|
576
|
+
this.allSegments.add(segment);
|
|
577
|
+
edgeSegments.add(segment);
|
|
578
|
+
segment.edges.add(this.edge(e)!);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private buildSegmentsForEdge(e: Edge): Array<Segment<EdgeValue>> {
|
|
583
|
+
const segments =
|
|
584
|
+
this.parent(e.source) === this.parent(e.target)
|
|
585
|
+
? [this.createSiblingSegment(e)]
|
|
586
|
+
: this.createPathSegments(e);
|
|
587
|
+
return segments;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private createSiblingSegment(e: Edge): Segment<EdgeValue> {
|
|
591
|
+
// Normalize source and target alphabetically for segments between siblings.
|
|
592
|
+
const sid = e.source;
|
|
593
|
+
const tid = e.target;
|
|
594
|
+
const [source, target] = sid > tid ? [tid, sid] : [sid, tid];
|
|
595
|
+
const segment = this.findOrCreateSegment(source, target, [source, target]);
|
|
596
|
+
|
|
597
|
+
const pid = this.parent(sid);
|
|
598
|
+
const children = this.childSegmentsMap.get(pid) || new Set<Segment<EdgeValue>>();
|
|
599
|
+
children.add(segment);
|
|
600
|
+
this.childSegmentsMap.set(pid, children);
|
|
601
|
+
|
|
602
|
+
return segment;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Create all segments up both legs of the path, as well as a sibling
|
|
607
|
+
* segment.
|
|
608
|
+
*/
|
|
609
|
+
private createPathSegments(e: Edge): Array<Segment<EdgeValue>> {
|
|
610
|
+
const sourcePath = this.ancestors(e.source);
|
|
611
|
+
const targetPath = this.ancestors(e.target);
|
|
612
|
+
|
|
613
|
+
// Remove path segments while they're at the same level
|
|
614
|
+
while (sourcePath[sourcePath.length - 1] === targetPath[targetPath.length - 1]) {
|
|
615
|
+
sourcePath.pop();
|
|
616
|
+
targetPath.pop();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const sourceTop = sourcePath[sourcePath.length - 1] || e.source;
|
|
620
|
+
const targetTop = targetPath[targetPath.length - 1] || e.target;
|
|
621
|
+
|
|
622
|
+
sourcePath.push(targetTop);
|
|
623
|
+
targetPath.push(sourceTop);
|
|
624
|
+
|
|
625
|
+
// Begin by creating all path segments which are _not_ sibling segments.
|
|
626
|
+
// However, the path for the segments must go "over" the top most node.
|
|
627
|
+
// Given `b { a } d { c } e; a -> c; a -> e` the necessary segments are
|
|
628
|
+
// `a ~ b -> c`, `a ~ b -> e`, `d ~ c -> b, `b ~ d`, and `b ~ e`. To ensure
|
|
629
|
+
// a distinction between `a ~ b -> c` and `a ~ b -> e`, the paths include
|
|
630
|
+
// the final sibling destination, and then segmentsForPath only connects
|
|
631
|
+
// segments on the path from [0, -2]. After creating the path segments,
|
|
632
|
+
// they get joined with the topSegment from the sibling nodes, below.
|
|
633
|
+
const sourceSegments = this.segmentsForPath(sourcePath);
|
|
634
|
+
const targetSegments = this.segmentsForPath(targetPath);
|
|
635
|
+
|
|
636
|
+
const topSegment = this.createSiblingSegment({
|
|
637
|
+
source: sourceTop,
|
|
638
|
+
target: targetTop,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// Link source path segments and target path segments with topSegment
|
|
642
|
+
if (sourceSegments.length > 0) {
|
|
643
|
+
const topSourceSegment = sourceSegments[sourceSegments.length - 1];
|
|
644
|
+
topSegment.previous.add(topSourceSegment);
|
|
645
|
+
topSourceSegment.next = topSegment;
|
|
646
|
+
}
|
|
647
|
+
if (targetSegments.length > 0) {
|
|
648
|
+
const topTargetSegment = targetSegments[targetSegments.length - 1];
|
|
649
|
+
topTargetSegment.next = topSegment;
|
|
650
|
+
topSegment.previous.add(topTargetSegment);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const segments = [...sourceSegments, ...targetSegments, topSegment];
|
|
654
|
+
return segments;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Create all segments from a node up to the root. */
|
|
658
|
+
private segmentsForPath(path: NodeId[]): Array<Segment<EdgeValue>> {
|
|
659
|
+
const segments: Array<Segment<EdgeValue>> = [];
|
|
660
|
+
|
|
661
|
+
// Source is always lower in the hierarchy than target.
|
|
662
|
+
const height = path.length - 1; // The
|
|
663
|
+
for (let i = 0; i < height - 1; i++) {
|
|
664
|
+
segments.push(this.findOrCreateSegment(path[i], path[i + 1], path.slice(i + 2)));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
for (let i = 0; i < segments.length; i++) {
|
|
668
|
+
const next = segments[i + 1];
|
|
669
|
+
const prev = segments[i - 1];
|
|
670
|
+
if (prev) segments[i].previous.add(prev);
|
|
671
|
+
if (next) segments[i].next = next;
|
|
672
|
+
}
|
|
673
|
+
return segments;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Given a source, target, and path, find the segment that it uniquely
|
|
678
|
+
* identifies, or create it if it doesn't exist yet.
|
|
679
|
+
*/
|
|
680
|
+
private findOrCreateSegment(
|
|
681
|
+
source: NodeId,
|
|
682
|
+
target: NodeId,
|
|
683
|
+
path: NodeId[] = [],
|
|
684
|
+
): Segment<EdgeValue> {
|
|
685
|
+
if (!this.pathSegmentsMap.has(source)) {
|
|
686
|
+
this.pathSegmentsMap.set(source, new Map<string, Map<string, Segment<EdgeValue>>>());
|
|
687
|
+
}
|
|
688
|
+
const sourceMap = this.pathSegmentsMap.get(source)!;
|
|
689
|
+
|
|
690
|
+
if (!sourceMap.has(target)) {
|
|
691
|
+
sourceMap.set(target, new Map<string, Segment<EdgeValue>>());
|
|
692
|
+
}
|
|
693
|
+
const targetMap = sourceMap.get(target)!;
|
|
694
|
+
|
|
695
|
+
const pid = pathId(path);
|
|
696
|
+
|
|
697
|
+
const segment: Segment<EdgeValue> = (targetMap.get(pid) || {
|
|
698
|
+
source,
|
|
699
|
+
target,
|
|
700
|
+
edges: new Set<EdgeValue>(),
|
|
701
|
+
previous: new Set<Segment<EdgeValue>>(),
|
|
702
|
+
}) as Segment<EdgeValue>;
|
|
703
|
+
targetMap.set(pid, segment);
|
|
704
|
+
|
|
705
|
+
if (!this.nodeSegmentsMap.has(source)) {
|
|
706
|
+
this.nodeSegmentsMap.set(source, new Set());
|
|
707
|
+
}
|
|
708
|
+
this.nodeSegmentsMap.get(source)!.add(segment);
|
|
709
|
+
|
|
710
|
+
if (!this.nodeSegmentsMap.has(target)) {
|
|
711
|
+
this.nodeSegmentsMap.set(target, new Set());
|
|
712
|
+
}
|
|
713
|
+
this.nodeSegmentsMap.get(target)!.add(segment);
|
|
714
|
+
|
|
715
|
+
return targetMap.get(pid) as Segment<EdgeValue>;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private removeSegmentsForEdge(e: Edge) {
|
|
719
|
+
for (const segment of this.edgeSegments(e)) {
|
|
720
|
+
segment.edges.delete(this.edge(e)!);
|
|
721
|
+
if (segment.edges.size > 0) {
|
|
722
|
+
// The segment has other edges left in it, so don't remove it yet.
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const parent = this.parent(segment.source);
|
|
726
|
+
if (!this.hasNode(parent) || parent === segment.target) {
|
|
727
|
+
const children = this.childSegmentsMap.get(parent);
|
|
728
|
+
if (children) children.delete(segment);
|
|
729
|
+
}
|
|
730
|
+
this.allSegments.delete(segment);
|
|
731
|
+
if (this.nodeSegmentsMap.has(segment.source)) {
|
|
732
|
+
this.nodeSegmentsMap.get(segment.source)!.delete(segment);
|
|
733
|
+
}
|
|
734
|
+
if (this.nodeSegmentsMap.has(segment.target)) {
|
|
735
|
+
this.nodeSegmentsMap.get(segment.target)!.delete(segment);
|
|
736
|
+
}
|
|
737
|
+
if (this.pathSegmentsMap.has(segment.source)) {
|
|
738
|
+
const pathSourceMap = this.pathSegmentsMap.get(segment.source)!;
|
|
739
|
+
if (pathSourceMap.has(segment.target)) {
|
|
740
|
+
const pathTargetMap = pathSourceMap.get(segment.target)!;
|
|
741
|
+
const pid = pathId(segmentPath(segment));
|
|
742
|
+
if (pathTargetMap.has(pid)) {
|
|
743
|
+
pathTargetMap.delete(pid);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private addViolation(violation: GraphViolation): this {
|
|
751
|
+
this.graphViolations.push(violation);
|
|
752
|
+
return this;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Given an edge, return a unique string ID for it. */
|
|
757
|
+
export function edgeId({ source, target, name = DEFAULT_EDGE_NAME }: Edge): EdgeId {
|
|
758
|
+
return `${source}${EDGE_KEY_DELIM}${target}${EDGE_KEY_DELIM}${name}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Join a segment path of NodeIds into a unique string. */
|
|
762
|
+
export function pathId(path: NodeId[]): string {
|
|
763
|
+
return path.join(SEGMENT_DELIM);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Return a list of all node ids in order from the source of the first segment,
|
|
768
|
+
* up the parent chain, and across the sibling.
|
|
769
|
+
*/
|
|
770
|
+
export function segmentPath(segment: Segment): NodeId[] {
|
|
771
|
+
const path: NodeId[] = [segment.source];
|
|
772
|
+
let last: Segment | undefined;
|
|
773
|
+
let next: Segment | undefined = segment;
|
|
774
|
+
while (next) {
|
|
775
|
+
if (!next.next && last && next.target === last.target) {
|
|
776
|
+
// At the top of the path, possibly un-reverse segments.
|
|
777
|
+
path.push(next.source);
|
|
778
|
+
} else {
|
|
779
|
+
path.push(next.target);
|
|
780
|
+
}
|
|
781
|
+
last = next;
|
|
782
|
+
next = next.next;
|
|
783
|
+
}
|
|
784
|
+
return path;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Returns a string suitable to uniquely identify a segment.
|
|
789
|
+
*/
|
|
790
|
+
export function segmentId(segment: Segment): SegmentId {
|
|
791
|
+
return segmentPath(segment).join(SEGMENT_DELIM);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Increment a count in a map, defaulting to 0 if unset. */
|
|
795
|
+
function incEntry<T>(map: Map<T, number>, key: T) {
|
|
796
|
+
const value = map.get(key) || 0;
|
|
797
|
+
map.set(key, value + 1);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** Decrement a count in a map, removing it if $t's empty. */
|
|
801
|
+
function decEntry<T>(map: Map<T, number>, key: T) {
|
|
802
|
+
const value = (map.get(key) || 0) - 1;
|
|
803
|
+
if (value <= 0) {
|
|
804
|
+
map.delete(key);
|
|
805
|
+
} else {
|
|
806
|
+
map.set(key, value);
|
|
807
|
+
}
|
|
808
|
+
}
|