@peter.prib/renderengine 0.14.0 → 0.15.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 +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { treeLayerOf } from './ModelTreePanel.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NavigatorModel — pure, DOM-free graph traversal and navigation engine for an ArchiMate model.
|
|
5
|
+
*
|
|
6
|
+
* Allows exploring relationships step-by-step from any focused root element to arbitrary depth,
|
|
7
|
+
* with cycle detection, navigation history (back/forward), direction filters, path-finding
|
|
8
|
+
* between elements, and transitive impact analysis.
|
|
9
|
+
*/
|
|
10
|
+
export class NavigatorModel {
|
|
11
|
+
/**
|
|
12
|
+
* @param {ArchimateModel} model
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {string} [options.rootId] - Initial focused element id
|
|
15
|
+
* @param {'both'|'outgoing'|'incoming'} [options.direction='both']
|
|
16
|
+
* @param {Iterable<string>} [options.allowedKinds] - Optional relationship kinds to include
|
|
17
|
+
*/
|
|
18
|
+
constructor(model, { rootId = null, direction = 'both', allowedKinds = null } = {}) {
|
|
19
|
+
this.model = model;
|
|
20
|
+
this.direction = direction;
|
|
21
|
+
this.allowedKinds = allowedKinds ? new Set(allowedKinds) : null;
|
|
22
|
+
this.pinned = false;
|
|
23
|
+
|
|
24
|
+
this.history = [];
|
|
25
|
+
this.historyIndex = -1;
|
|
26
|
+
this.currentId = null;
|
|
27
|
+
|
|
28
|
+
this.expandedPaths = new Set();
|
|
29
|
+
this._listeners = new Set();
|
|
30
|
+
|
|
31
|
+
if (rootId && model.getElement(rootId)) {
|
|
32
|
+
this.focus(rootId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Register a listener called on state changes (focus, history, expansion, pin). */
|
|
37
|
+
onChange(listener) {
|
|
38
|
+
this._listeners.add(listener);
|
|
39
|
+
return () => this._listeners.delete(listener);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_notify() {
|
|
43
|
+
for (const listener of this._listeners) {
|
|
44
|
+
try {
|
|
45
|
+
listener(this);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn('NavigatorModel listener error:', err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Navigation & History ──
|
|
53
|
+
|
|
54
|
+
/** Whether back navigation is available. */
|
|
55
|
+
get canBack() {
|
|
56
|
+
return this.historyIndex > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether forward navigation is available. */
|
|
60
|
+
get canForward() {
|
|
61
|
+
return this.historyIndex < this.history.length - 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Focus on an element as the new root of the navigator.
|
|
66
|
+
* Pushes to history unless currently pinned or already focused.
|
|
67
|
+
* @param {string} elementId
|
|
68
|
+
* @param {Object} [options]
|
|
69
|
+
* @param {boolean} [options.force=false] - Ignore pinned state
|
|
70
|
+
* @returns {boolean} whether focus changed
|
|
71
|
+
*/
|
|
72
|
+
focus(elementId, { force = false } = {}) {
|
|
73
|
+
if (!force && this.pinned && this.currentId) return false;
|
|
74
|
+
if (!elementId || elementId === this.currentId) return false;
|
|
75
|
+
const element = this.model.getElement(elementId);
|
|
76
|
+
if (!element) return false;
|
|
77
|
+
|
|
78
|
+
// Truncate forward history if we navigated from mid-stack
|
|
79
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
80
|
+
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.history.push(elementId);
|
|
84
|
+
this.historyIndex = this.history.length - 1;
|
|
85
|
+
this.currentId = elementId;
|
|
86
|
+
this.expandedPaths.clear();
|
|
87
|
+
this.expandedPaths.add(elementId); // root is expanded by default
|
|
88
|
+
|
|
89
|
+
this._notify();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Navigate back in history. */
|
|
94
|
+
back() {
|
|
95
|
+
if (!this.canBack) return false;
|
|
96
|
+
this.historyIndex--;
|
|
97
|
+
this.currentId = this.history[this.historyIndex];
|
|
98
|
+
this.expandedPaths.clear();
|
|
99
|
+
this.expandedPaths.add(this.currentId);
|
|
100
|
+
this._notify();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Navigate forward in history. */
|
|
105
|
+
forward() {
|
|
106
|
+
if (!this.canForward) return false;
|
|
107
|
+
this.historyIndex++;
|
|
108
|
+
this.currentId = this.history[this.historyIndex];
|
|
109
|
+
this.expandedPaths.clear();
|
|
110
|
+
this.expandedPaths.add(this.currentId);
|
|
111
|
+
this._notify();
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Toggle pin state. When pinned, canvas selection changes won't re-root the navigator. */
|
|
116
|
+
togglePin() {
|
|
117
|
+
this.pinned = !this.pinned;
|
|
118
|
+
this._notify();
|
|
119
|
+
return this.pinned;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setDirection(direction) {
|
|
123
|
+
if (direction !== 'both' && direction !== 'outgoing' && direction !== 'incoming') return;
|
|
124
|
+
this.direction = direction;
|
|
125
|
+
this._notify();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setAllowedKinds(kinds) {
|
|
129
|
+
this.allowedKinds = kinds ? new Set(kinds) : null;
|
|
130
|
+
this._notify();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Expansion State ──
|
|
134
|
+
|
|
135
|
+
isExpanded(pathKey) {
|
|
136
|
+
return this.expandedPaths.has(pathKey);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
toggleExpand(pathKey) {
|
|
140
|
+
if (this.expandedPaths.has(pathKey)) {
|
|
141
|
+
this.expandedPaths.delete(pathKey);
|
|
142
|
+
} else {
|
|
143
|
+
this.expandedPaths.add(pathKey);
|
|
144
|
+
}
|
|
145
|
+
this._notify();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expand(pathKey) {
|
|
149
|
+
if (!this.expandedPaths.has(pathKey)) {
|
|
150
|
+
this.expandedPaths.add(pathKey);
|
|
151
|
+
this._notify();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
collapse(pathKey) {
|
|
156
|
+
if (this.expandedPaths.has(pathKey)) {
|
|
157
|
+
this.expandedPaths.delete(pathKey);
|
|
158
|
+
this._notify();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
collapseAll() {
|
|
163
|
+
this.expandedPaths.clear();
|
|
164
|
+
if (this.currentId) this.expandedPaths.add(this.currentId);
|
|
165
|
+
this._notify();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Tree Construction ──
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the multi-step navigation tree from the focused element,
|
|
172
|
+
* following expanded branches with cycle detection.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} [rootId=this.currentId]
|
|
175
|
+
* @returns {Object|null}
|
|
176
|
+
*/
|
|
177
|
+
buildTree(rootId = this.currentId) {
|
|
178
|
+
if (!rootId) return null;
|
|
179
|
+
const element = this.model.getElement(rootId);
|
|
180
|
+
if (!element) return null;
|
|
181
|
+
|
|
182
|
+
return this._buildNode(element, {
|
|
183
|
+
pathKey: rootId,
|
|
184
|
+
visitedInPath: new Set([rootId]),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @private */
|
|
189
|
+
_buildNode(element, { pathKey, visitedInPath }) {
|
|
190
|
+
const { incoming, outgoing } = this.neighbors(element.id);
|
|
191
|
+
|
|
192
|
+
const buildBranch = (rel, otherElement, dir) => {
|
|
193
|
+
const relKey = `${pathKey}:${dir}:${rel.id}:${otherElement.id}`;
|
|
194
|
+
const cycle = visitedInPath.has(otherElement.id);
|
|
195
|
+
const expanded = !cycle && this.isExpanded(relKey);
|
|
196
|
+
|
|
197
|
+
let children = null;
|
|
198
|
+
if (expanded) {
|
|
199
|
+
const nextVisited = new Set(visitedInPath);
|
|
200
|
+
nextVisited.add(otherElement.id);
|
|
201
|
+
children = this._buildNode(otherElement, {
|
|
202
|
+
pathKey: relKey,
|
|
203
|
+
visitedInPath: nextVisited,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
pathKey: relKey,
|
|
209
|
+
direction: dir,
|
|
210
|
+
relationship: rel,
|
|
211
|
+
element: otherElement,
|
|
212
|
+
layer: treeLayerOf(otherElement),
|
|
213
|
+
cycle,
|
|
214
|
+
expanded,
|
|
215
|
+
children,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const incomingBranches = (this.direction === 'both' || this.direction === 'incoming')
|
|
220
|
+
? incoming.map(({ relationship, element: source }) => buildBranch(relationship, source, 'incoming'))
|
|
221
|
+
: [];
|
|
222
|
+
|
|
223
|
+
const outgoingBranches = (this.direction === 'both' || this.direction === 'outgoing')
|
|
224
|
+
? outgoing.map(({ relationship, element: target }) => buildBranch(relationship, target, 'outgoing'))
|
|
225
|
+
: [];
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
pathKey,
|
|
229
|
+
element,
|
|
230
|
+
layer: treeLayerOf(element),
|
|
231
|
+
views: this.model.viewsContaining(element.id),
|
|
232
|
+
incoming: incomingBranches,
|
|
233
|
+
outgoing: outgoingBranches,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get immediate connected elements and relationships for an element.
|
|
239
|
+
* @param {string} elementId
|
|
240
|
+
* @returns {{ incoming: Array<{relationship: Object, element: Object}>, outgoing: Array<{relationship: Object, element: Object}> }}
|
|
241
|
+
*/
|
|
242
|
+
neighbors(elementId) {
|
|
243
|
+
const { incoming, outgoing } = this.model.relationshipsFor(elementId);
|
|
244
|
+
const matchKind = (r) => !this.allowedKinds || this.allowedKinds.has(r.kind);
|
|
245
|
+
|
|
246
|
+
const inItems = incoming.filter(matchKind).map(rel => {
|
|
247
|
+
const source = this.model.getElement(rel.source);
|
|
248
|
+
return source ? { relationship: rel, element: source } : null;
|
|
249
|
+
}).filter(Boolean);
|
|
250
|
+
|
|
251
|
+
const outItems = outgoing.filter(matchKind).map(rel => {
|
|
252
|
+
const target = this.model.getElement(rel.target);
|
|
253
|
+
return target ? { relationship: rel, element: target } : null;
|
|
254
|
+
}).filter(Boolean);
|
|
255
|
+
|
|
256
|
+
return { incoming: inItems, outgoing: outItems };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Graph Analysis: Path Finding & Impact Analysis ──
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Find all simple paths between startId and endId up to maxDepth.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} startId
|
|
265
|
+
* @param {string} endId
|
|
266
|
+
* @param {Object} [options]
|
|
267
|
+
* @param {number} [options.maxDepth=5]
|
|
268
|
+
* @param {'outgoing'|'both'|'incoming'} [options.direction='both']
|
|
269
|
+
* @param {Iterable<string>} [options.allowedKinds]
|
|
270
|
+
* @returns {Array<{ elements: Array<Object>, relationships: Array<Object>, length: number }>}
|
|
271
|
+
*/
|
|
272
|
+
findPaths(startId, endId, { maxDepth = 5, direction = 'both', allowedKinds = null } = {}) {
|
|
273
|
+
const paths = [];
|
|
274
|
+
const start = this.model.getElement(startId);
|
|
275
|
+
const target = this.model.getElement(endId);
|
|
276
|
+
if (!start || !target || startId === endId) return paths;
|
|
277
|
+
|
|
278
|
+
const kinds = allowedKinds ? new Set(allowedKinds) : this.allowedKinds;
|
|
279
|
+
const matchKind = (r) => !kinds || kinds.has(r.kind);
|
|
280
|
+
|
|
281
|
+
const search = (currentId, currentElements, currentRels, visited) => {
|
|
282
|
+
if (currentElements.length - 1 > maxDepth) return;
|
|
283
|
+
if (currentId === endId) {
|
|
284
|
+
paths.push({
|
|
285
|
+
elements: [...currentElements],
|
|
286
|
+
relationships: [...currentRels],
|
|
287
|
+
length: currentRels.length,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { incoming, outgoing } = this.model.relationshipsFor(currentId);
|
|
293
|
+
|
|
294
|
+
if (direction === 'both' || direction === 'outgoing') {
|
|
295
|
+
for (const rel of outgoing) {
|
|
296
|
+
if (!matchKind(rel)) continue;
|
|
297
|
+
const nextId = rel.target;
|
|
298
|
+
if (!visited.has(nextId)) {
|
|
299
|
+
const nextElem = this.model.getElement(nextId);
|
|
300
|
+
if (!nextElem) continue;
|
|
301
|
+
visited.add(nextId);
|
|
302
|
+
currentElements.push(nextElem);
|
|
303
|
+
currentRels.push(rel);
|
|
304
|
+
search(nextId, currentElements, currentRels, visited);
|
|
305
|
+
currentRels.pop();
|
|
306
|
+
currentElements.pop();
|
|
307
|
+
visited.delete(nextId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (direction === 'both' || direction === 'incoming') {
|
|
313
|
+
for (const rel of incoming) {
|
|
314
|
+
if (!matchKind(rel)) continue;
|
|
315
|
+
const nextId = rel.source;
|
|
316
|
+
if (!visited.has(nextId)) {
|
|
317
|
+
const nextElem = this.model.getElement(nextId);
|
|
318
|
+
if (!nextElem) continue;
|
|
319
|
+
visited.add(nextId);
|
|
320
|
+
currentElements.push(nextElem);
|
|
321
|
+
currentRels.push(rel);
|
|
322
|
+
search(nextId, currentElements, currentRels, visited);
|
|
323
|
+
currentRels.pop();
|
|
324
|
+
currentElements.pop();
|
|
325
|
+
visited.delete(nextId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const visited = new Set([startId]);
|
|
332
|
+
search(startId, [start], [], visited);
|
|
333
|
+
|
|
334
|
+
// Sort by path length ascending
|
|
335
|
+
return paths.sort((a, b) => a.length - b.length);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Compute transitive impact or dependencies from an element.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} elementId
|
|
342
|
+
* @param {Object} [options]
|
|
343
|
+
* @param {'outgoing'|'incoming'|'both'} [options.direction='outgoing']
|
|
344
|
+
* @param {number} [options.maxDepth=10]
|
|
345
|
+
* @param {Iterable<string>} [options.allowedKinds]
|
|
346
|
+
* @returns {{ root: Object, elements: Array<Object>, relationships: Array<Object>, depths: Map<string, number>, layers: Set<string> }}
|
|
347
|
+
*/
|
|
348
|
+
traceImpact(elementId, { direction = 'outgoing', maxDepth = 10, allowedKinds = null } = {}) {
|
|
349
|
+
const root = this.model.getElement(elementId);
|
|
350
|
+
if (!root) return { root: null, elements: [], relationships: [], depths: new Map(), layers: new Set() };
|
|
351
|
+
|
|
352
|
+
const kinds = allowedKinds ? new Set(allowedKinds) : this.allowedKinds;
|
|
353
|
+
const matchKind = (r) => !kinds || kinds.has(r.kind);
|
|
354
|
+
|
|
355
|
+
const depths = new Map([[elementId, 0]]);
|
|
356
|
+
const elementsMap = new Map();
|
|
357
|
+
const relsMap = new Map();
|
|
358
|
+
const layers = new Set([treeLayerOf(root)]);
|
|
359
|
+
|
|
360
|
+
const queue = [{ id: elementId, depth: 0 }];
|
|
361
|
+
|
|
362
|
+
while (queue.length) {
|
|
363
|
+
const { id: currId, depth } = queue.shift();
|
|
364
|
+
if (depth >= maxDepth) continue;
|
|
365
|
+
|
|
366
|
+
const { incoming, outgoing } = this.model.relationshipsFor(currId);
|
|
367
|
+
const nextDepth = depth + 1;
|
|
368
|
+
|
|
369
|
+
if (direction === 'both' || direction === 'outgoing') {
|
|
370
|
+
for (const rel of outgoing) {
|
|
371
|
+
if (!matchKind(rel)) continue;
|
|
372
|
+
const target = this.model.getElement(rel.target);
|
|
373
|
+
if (!target) continue;
|
|
374
|
+
|
|
375
|
+
relsMap.set(rel.id, rel);
|
|
376
|
+
if (!depths.has(target.id)) {
|
|
377
|
+
depths.set(target.id, nextDepth);
|
|
378
|
+
elementsMap.set(target.id, target);
|
|
379
|
+
layers.add(treeLayerOf(target));
|
|
380
|
+
queue.push({ id: target.id, depth: nextDepth });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (direction === 'both' || direction === 'incoming') {
|
|
386
|
+
for (const rel of incoming) {
|
|
387
|
+
if (!matchKind(rel)) continue;
|
|
388
|
+
const source = this.model.getElement(rel.source);
|
|
389
|
+
if (!source) continue;
|
|
390
|
+
|
|
391
|
+
relsMap.set(rel.id, rel);
|
|
392
|
+
if (!depths.has(source.id)) {
|
|
393
|
+
depths.set(source.id, nextDepth);
|
|
394
|
+
elementsMap.set(source.id, source);
|
|
395
|
+
layers.add(treeLayerOf(source));
|
|
396
|
+
queue.push({ id: source.id, depth: nextDepth });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
root,
|
|
404
|
+
elements: [...elementsMap.values()],
|
|
405
|
+
relationships: [...relsMap.values()],
|
|
406
|
+
depths,
|
|
407
|
+
layers,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|