@peter.prib/renderengine 0.2.5 → 0.2.7
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 +219 -213
- package/REFERENCE.md +272 -6
- package/dist/renderengine-workspace.umd.js +11980 -7421
- package/dist/renderengine.umd.js +8991 -5784
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +534 -0
- package/package.json +70 -68
- package/src/bian.js +182 -0
- package/src/charting/CheckboxTreeSelector.js +373 -304
- package/src/charting/DiagramEditor.js +53 -32
- package/src/charting/DiagramTypeRegistry.js +13 -3
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
- package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/DataModel.js +645 -0
- package/src/charting/datamodel/DataModelChecker.js +92 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +108 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +129 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +51 -0
- package/src/charting/datamodel/LevelMapping.js +38 -0
- package/src/charting/datamodel/LevelSchema.js +41 -0
- package/src/charting/datamodel/ServiceDomain.js +177 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +15 -0
- package/src/charting/diagramModel/MergeModelCommand.js +43 -37
- package/src/charting/diagramModel/OpenViewList.js +75 -0
- package/src/charting/diagramModel/ViewSetChecker.js +22 -34
- package/src/charting/diagrams/DataModelDiagram.js +160 -86
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +169 -158
- package/src/charting/diagrams/nesting.js +31 -0
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/modelTree/ModelTree.js +202 -164
- package/src/charting/modelTree/ModelTreePanel.js +36 -3
- package/src/charting/modelTree/TreeViewMode.js +131 -110
- package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
- package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
- package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
- package/src/datamodel.js +132 -122
- package/src/diagramCatalog.js +35 -30
- package/src/index.js +2666 -2659
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/index.js +1 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
- package/src/workspace/parts/ChartPart.js +6 -1
- package/src/workspace/parts/DataModelPart.js +544 -0
- package/src/workspace/parts/DiagramEditorPart.js +2 -8
- package/src/workspace/parts/EditorPart.js +461 -407
- package/src/workspace/parts/ModelFile.js +216 -187
- package/src/workspace/parts/ModelsPart.js +483 -192
- package/src/workspace/storage/BackupFolder.js +145 -0
- package/src/workspace/storage/KeyValueStore.js +92 -0
- package/src/workspace/storage/ModelLibrary.js +65 -0
- package/src/workspace/workArea.js +417 -413
- package/src/yaml/YamlReader.js +372 -0
|
@@ -1,195 +1,208 @@
|
|
|
1
|
-
import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
|
|
2
|
-
import { GlyphTemplate } from '../templates/GlyphTemplate.js';
|
|
3
|
-
import { Glyph } from '../glyphs/Glyph.js';
|
|
4
|
-
import { glyphFor } from '../glyphs/index.js';
|
|
5
|
-
import { ArchimateDiagram } from '../diagrams/ArchimateDiagram.js';
|
|
6
|
-
import { LabelHelper } from '../diagrams/LabelHelper.js';
|
|
7
|
-
|
|
8
|
-
/** Paint a view can set on one occurrence: template property → appearance key (same names). */
|
|
9
|
-
const APPEARANCE = ['fillColor', 'fillOpacity', 'strokeColor', 'strokeWidth', 'textColor', ...TEXT_STYLE_KEYS];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* ArchimateElementTemplate — one element (or note, or image) on an ArchiMate
|
|
13
|
-
* view, as an editor template. It keeps no copy of its own: position, size,
|
|
14
|
-
* stacking and paint are read from and written to its ElementInstance, and
|
|
15
|
-
* what it looks like comes from the model element through the same
|
|
16
|
-
* enrichment the graph engine uses (ArchimateDiagram.buildNode: layer colour,
|
|
17
|
-
* corner style, notation shape, type badge, label position). So every editor
|
|
18
|
-
* command that moves, resizes, restacks or repaints a template edits the view
|
|
19
|
-
* itself, and its undo does too.
|
|
20
|
-
*
|
|
21
|
-
* The shape is the node's glyph: a box (sharp, rounded or chamfered by
|
|
22
|
-
* aspect) with the type's glyph as a corner badge, or the full notation shape
|
|
23
|
-
* when distinct shapes are on; notes and images draw their own glyph.
|
|
24
|
-
*/
|
|
25
|
-
export class ArchimateElementTemplate extends GlyphTemplate {
|
|
26
|
-
/** ArchiMate notation is not rotated. */
|
|
27
|
-
static rotatable = false;
|
|
28
|
-
|
|
29
|
-
/** The format painter copies its appearance (what the view's instance holds). */
|
|
30
|
-
static formatKeys = Object.freeze([...APPEARANCE]);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {ElementInstance} instance - What this template shows, and where
|
|
34
|
-
* @param {ArchimateModel} model
|
|
35
|
-
* @param {Object} [options]
|
|
36
|
-
* @param {Object} [options.nodeOptions] - ArchimateDiagram.buildNode options
|
|
37
|
-
* (archimateVersion, useDistinctShapes, layerColors, elementWidth, ...)
|
|
38
|
-
*/
|
|
39
|
-
constructor(instance, model, { nodeOptions = {} } = {}) {
|
|
40
|
-
super(instance.id, undefined, {});
|
|
41
|
-
this.instance = instance;
|
|
42
|
-
this.model = model;
|
|
43
|
-
this.nodeOptions = nodeOptions;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** State lives on the instance and the model element, not here. @protected */
|
|
47
|
-
_initState() {}
|
|
48
|
-
|
|
49
|
-
/** The element as the ArchiMate notation draws it on this view. */
|
|
50
|
-
get node() {
|
|
51
|
-
const projected = this.instance.project(this.model) ?? { id: this.instance.id, name: this.instance.elementId };
|
|
52
|
-
return ArchimateDiagram.buildNode(projected, this.nodeOptions);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get name() { return this.instance.getDisplayName(this.model); }
|
|
56
|
-
/** Renaming one occurrence labels it on this view; the element's own name is left alone. */
|
|
57
|
-
set name(value) {
|
|
58
|
-
const own = this.instance.resolve(this.model)?.name;
|
|
59
|
-
this.instance.labelOverride = value && value !== own ? value : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get x() { return this.instance.x; }
|
|
63
|
-
set x(value) { this.instance.x = value; }
|
|
64
|
-
get y() { return this.instance.y; }
|
|
65
|
-
set y(value) { this.instance.y = value; }
|
|
66
|
-
get zIndex() { return this.instance.zIndex; }
|
|
67
|
-
set zIndex(value) { this.instance.zIndex = value ?? 0; }
|
|
68
|
-
get visible() { return this.instance.visible; }
|
|
69
|
-
set visible(value) { this.instance.visible = value !== false; }
|
|
70
|
-
|
|
71
|
-
get width() { return this.node.width; }
|
|
72
|
-
set width(value) { this._setSize('width', value); }
|
|
73
|
-
get height() { return this.node.height; }
|
|
74
|
-
set height(value) { this._setSize('height', value); }
|
|
75
|
-
|
|
76
|
-
get fillColor() { return this.node.color; }
|
|
77
|
-
set fillColor(value) { this._setAppearance('fillColor', value); }
|
|
78
|
-
get fillOpacity() { return this.instance.appearance.fillOpacity ?? 1; }
|
|
79
|
-
set fillOpacity(value) { this._setAppearance('fillOpacity', value); }
|
|
80
|
-
get strokeColor() { return this.node.strokeColor; }
|
|
81
|
-
set strokeColor(value) { this._setAppearance('strokeColor', value); }
|
|
82
|
-
get strokeWidth() { return this.node.strokeWidth; }
|
|
83
|
-
set strokeWidth(value) { this._setAppearance('strokeWidth', value); }
|
|
84
|
-
get textColor() { return this.instance.appearance.textColor ?? '#222'; }
|
|
85
|
-
set textColor(value) { this._setAppearance('textColor', value); }
|
|
86
|
-
get fontFamily() { return this.instance.appearance.fontFamily; }
|
|
87
|
-
set fontFamily(value) { this._setAppearance('fontFamily', value); }
|
|
88
|
-
get fontSize() { return this.instance.appearance.fontSize; }
|
|
89
|
-
set fontSize(value) { this._setAppearance('fontSize', value); }
|
|
90
|
-
get fontWeight() { return this.instance.appearance.fontWeight; }
|
|
91
|
-
set fontWeight(value) { this._setAppearance('fontWeight', value); }
|
|
92
|
-
get fontStyle() { return this.instance.appearance.fontStyle; }
|
|
93
|
-
set fontStyle(value) { this._setAppearance('fontStyle', value); }
|
|
94
|
-
get textDecoration() { return this.instance.appearance.textDecoration; }
|
|
95
|
-
set textDecoration(value) { this._setAppearance('textDecoration', value); }
|
|
96
|
-
get textAlign() { return this.instance.appearance.textAlign; }
|
|
97
|
-
set textAlign(value) { this._setAppearance('textAlign', value); }
|
|
98
|
-
get textPosition() { return this.instance.appearance.textPosition; }
|
|
99
|
-
set textPosition(value) { this._setAppearance('textPosition', value); }
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* What this occurrence sets itself for a property (undefined when it shows
|
|
103
|
-
* the element's own), for commands that must undo to "unset".
|
|
104
|
-
*/
|
|
105
|
-
ownProperty(key) {
|
|
106
|
-
if (APPEARANCE.includes(key)) return this.instance.appearance[key];
|
|
107
|
-
if (key === 'width' || key === 'height') return this.instance.size[key];
|
|
108
|
-
if (key === 'name') return this.instance.labelOverride ?? undefined;
|
|
109
|
-
return this[key];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** undefined puts the occurrence back on the element's own value. @private */
|
|
113
|
-
_setSize(key, value) {
|
|
114
|
-
if (value === undefined) delete this.instance.size[key];
|
|
115
|
-
else this.instance.size = { ...this.instance.size, [key]: value };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** @private */
|
|
119
|
-
_setAppearance(key, value) {
|
|
120
|
-
if (!APPEARANCE.includes(key)) throw new TypeError(`Not an appearance property: ${key}`);
|
|
121
|
-
if (value === undefined) delete this.instance.appearance[key];
|
|
122
|
-
else this.instance.appearance = { ...this.instance.appearance, [key]: value };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
glyph() {
|
|
126
|
-
return glyphFor(this.node.shape);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
glyphContext() {
|
|
130
|
-
const node = this.node;
|
|
131
|
-
return Glyph.context({
|
|
132
|
-
R: Math.max(node.width, node.height) / 2,
|
|
133
|
-
size: { w: node.width, h: node.height },
|
|
134
|
-
color: node.color,
|
|
135
|
-
cornerStyle: node.cornerStyle,
|
|
136
|
-
strokeColor: node.strokeColor,
|
|
137
|
-
strokeWidth: node.strokeWidth,
|
|
138
|
-
icon: node.icon,
|
|
139
|
-
iconUrl: node.iconUrl,
|
|
140
|
-
ownFill: this.instance.appearance.fillColor ?? null,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* The
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
import { TextStyle, TEXT_STYLE_KEYS } from '../TextStyle.js';
|
|
2
|
+
import { GlyphTemplate } from '../templates/GlyphTemplate.js';
|
|
3
|
+
import { Glyph } from '../glyphs/Glyph.js';
|
|
4
|
+
import { glyphFor } from '../glyphs/index.js';
|
|
5
|
+
import { ArchimateDiagram } from '../diagrams/ArchimateDiagram.js';
|
|
6
|
+
import { LabelHelper } from '../diagrams/LabelHelper.js';
|
|
7
|
+
|
|
8
|
+
/** Paint a view can set on one occurrence: template property → appearance key (same names). */
|
|
9
|
+
const APPEARANCE = ['fillColor', 'fillOpacity', 'strokeColor', 'strokeWidth', 'textColor', ...TEXT_STYLE_KEYS];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ArchimateElementTemplate — one element (or note, or image) on an ArchiMate
|
|
13
|
+
* view, as an editor template. It keeps no copy of its own: position, size,
|
|
14
|
+
* stacking and paint are read from and written to its ElementInstance, and
|
|
15
|
+
* what it looks like comes from the model element through the same
|
|
16
|
+
* enrichment the graph engine uses (ArchimateDiagram.buildNode: layer colour,
|
|
17
|
+
* corner style, notation shape, type badge, label position). So every editor
|
|
18
|
+
* command that moves, resizes, restacks or repaints a template edits the view
|
|
19
|
+
* itself, and its undo does too.
|
|
20
|
+
*
|
|
21
|
+
* The shape is the node's glyph: a box (sharp, rounded or chamfered by
|
|
22
|
+
* aspect) with the type's glyph as a corner badge, or the full notation shape
|
|
23
|
+
* when distinct shapes are on; notes and images draw their own glyph.
|
|
24
|
+
*/
|
|
25
|
+
export class ArchimateElementTemplate extends GlyphTemplate {
|
|
26
|
+
/** ArchiMate notation is not rotated. */
|
|
27
|
+
static rotatable = false;
|
|
28
|
+
|
|
29
|
+
/** The format painter copies its appearance (what the view's instance holds). */
|
|
30
|
+
static formatKeys = Object.freeze([...APPEARANCE]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {ElementInstance} instance - What this template shows, and where
|
|
34
|
+
* @param {ArchimateModel} model
|
|
35
|
+
* @param {Object} [options]
|
|
36
|
+
* @param {Object} [options.nodeOptions] - ArchimateDiagram.buildNode options
|
|
37
|
+
* (archimateVersion, useDistinctShapes, layerColors, elementWidth, ...)
|
|
38
|
+
*/
|
|
39
|
+
constructor(instance, model, { nodeOptions = {} } = {}) {
|
|
40
|
+
super(instance.id, undefined, {});
|
|
41
|
+
this.instance = instance;
|
|
42
|
+
this.model = model;
|
|
43
|
+
this.nodeOptions = nodeOptions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** State lives on the instance and the model element, not here. @protected */
|
|
47
|
+
_initState() {}
|
|
48
|
+
|
|
49
|
+
/** The element as the ArchiMate notation draws it on this view. */
|
|
50
|
+
get node() {
|
|
51
|
+
const projected = this.instance.project(this.model) ?? { id: this.instance.id, name: this.instance.elementId };
|
|
52
|
+
return ArchimateDiagram.buildNode(projected, this.nodeOptions);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get name() { return this.instance.getDisplayName(this.model); }
|
|
56
|
+
/** Renaming one occurrence labels it on this view; the element's own name is left alone. */
|
|
57
|
+
set name(value) {
|
|
58
|
+
const own = this.instance.resolve(this.model)?.name;
|
|
59
|
+
this.instance.labelOverride = value && value !== own ? value : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get x() { return this.instance.x; }
|
|
63
|
+
set x(value) { this.instance.x = value; }
|
|
64
|
+
get y() { return this.instance.y; }
|
|
65
|
+
set y(value) { this.instance.y = value; }
|
|
66
|
+
get zIndex() { return this.instance.zIndex; }
|
|
67
|
+
set zIndex(value) { this.instance.zIndex = value ?? 0; }
|
|
68
|
+
get visible() { return this.instance.visible; }
|
|
69
|
+
set visible(value) { this.instance.visible = value !== false; }
|
|
70
|
+
|
|
71
|
+
get width() { return this.node.width; }
|
|
72
|
+
set width(value) { this._setSize('width', value); }
|
|
73
|
+
get height() { return this.node.height; }
|
|
74
|
+
set height(value) { this._setSize('height', value); }
|
|
75
|
+
|
|
76
|
+
get fillColor() { return this.node.color; }
|
|
77
|
+
set fillColor(value) { this._setAppearance('fillColor', value); }
|
|
78
|
+
get fillOpacity() { return this.instance.appearance.fillOpacity ?? 1; }
|
|
79
|
+
set fillOpacity(value) { this._setAppearance('fillOpacity', value); }
|
|
80
|
+
get strokeColor() { return this.node.strokeColor; }
|
|
81
|
+
set strokeColor(value) { this._setAppearance('strokeColor', value); }
|
|
82
|
+
get strokeWidth() { return this.node.strokeWidth; }
|
|
83
|
+
set strokeWidth(value) { this._setAppearance('strokeWidth', value); }
|
|
84
|
+
get textColor() { return this.instance.appearance.textColor ?? '#222'; }
|
|
85
|
+
set textColor(value) { this._setAppearance('textColor', value); }
|
|
86
|
+
get fontFamily() { return this.instance.appearance.fontFamily; }
|
|
87
|
+
set fontFamily(value) { this._setAppearance('fontFamily', value); }
|
|
88
|
+
get fontSize() { return this.instance.appearance.fontSize; }
|
|
89
|
+
set fontSize(value) { this._setAppearance('fontSize', value); }
|
|
90
|
+
get fontWeight() { return this.instance.appearance.fontWeight; }
|
|
91
|
+
set fontWeight(value) { this._setAppearance('fontWeight', value); }
|
|
92
|
+
get fontStyle() { return this.instance.appearance.fontStyle; }
|
|
93
|
+
set fontStyle(value) { this._setAppearance('fontStyle', value); }
|
|
94
|
+
get textDecoration() { return this.instance.appearance.textDecoration; }
|
|
95
|
+
set textDecoration(value) { this._setAppearance('textDecoration', value); }
|
|
96
|
+
get textAlign() { return this.instance.appearance.textAlign; }
|
|
97
|
+
set textAlign(value) { this._setAppearance('textAlign', value); }
|
|
98
|
+
get textPosition() { return this.instance.appearance.textPosition; }
|
|
99
|
+
set textPosition(value) { this._setAppearance('textPosition', value); }
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What this occurrence sets itself for a property (undefined when it shows
|
|
103
|
+
* the element's own), for commands that must undo to "unset".
|
|
104
|
+
*/
|
|
105
|
+
ownProperty(key) {
|
|
106
|
+
if (APPEARANCE.includes(key)) return this.instance.appearance[key];
|
|
107
|
+
if (key === 'width' || key === 'height') return this.instance.size[key];
|
|
108
|
+
if (key === 'name') return this.instance.labelOverride ?? undefined;
|
|
109
|
+
return this[key];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** undefined puts the occurrence back on the element's own value. @private */
|
|
113
|
+
_setSize(key, value) {
|
|
114
|
+
if (value === undefined) delete this.instance.size[key];
|
|
115
|
+
else this.instance.size = { ...this.instance.size, [key]: value };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @private */
|
|
119
|
+
_setAppearance(key, value) {
|
|
120
|
+
if (!APPEARANCE.includes(key)) throw new TypeError(`Not an appearance property: ${key}`);
|
|
121
|
+
if (value === undefined) delete this.instance.appearance[key];
|
|
122
|
+
else this.instance.appearance = { ...this.instance.appearance, [key]: value };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
glyph() {
|
|
126
|
+
return glyphFor(this.node.shape);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
glyphContext() {
|
|
130
|
+
const node = this.node;
|
|
131
|
+
return Glyph.context({
|
|
132
|
+
R: Math.max(node.width, node.height) / 2,
|
|
133
|
+
size: { w: node.width, h: node.height },
|
|
134
|
+
color: node.color,
|
|
135
|
+
cornerStyle: node.cornerStyle,
|
|
136
|
+
strokeColor: node.strokeColor,
|
|
137
|
+
strokeWidth: node.strokeWidth,
|
|
138
|
+
icon: node.icon,
|
|
139
|
+
iconUrl: node.iconUrl,
|
|
140
|
+
ownFill: this.instance.appearance.fillColor ?? null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The badge's size: the same whatever the box's size (as Archi draws it), so
|
|
146
|
+
* resizing a box, or a parent growing to hold what is dropped in it, leaves
|
|
147
|
+
* it as it was; only a box too small for it gets a smaller one.
|
|
148
|
+
*/
|
|
149
|
+
static badgeSize(w, h) {
|
|
150
|
+
return Math.max(8, Math.min(ArchimateElementTemplate.BADGE_SIZE, Math.round(Math.min(w, h) * 0.4)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The type badge in the top-right corner: its glyph, or an icon (text or
|
|
155
|
+
* image), a fixed size (badgeSize). As in the graph engine, a notation shape
|
|
156
|
+
* gets no glyph badge (buildNode leaves iconShape unset) but keeps an icon the element sets.
|
|
157
|
+
*/
|
|
158
|
+
decorations(ctx) {
|
|
159
|
+
const node = this.node;
|
|
160
|
+
const { w, h } = ctx;
|
|
161
|
+
const fixed = ArchimateElementTemplate.badgeSize(w, h);
|
|
162
|
+
if (node.iconShape) {
|
|
163
|
+
const badge = fixed;
|
|
164
|
+
const inset = badge / 2 + 3;
|
|
165
|
+
const parts = glyphFor(node.iconShape).parts(Glyph.context({
|
|
166
|
+
R: badge / 2, size: { w: badge, h: badge * 0.78 }, color: 'none', strokeColor: node.strokeColor || '#000000', strokeWidth: 1,
|
|
167
|
+
}));
|
|
168
|
+
return [Glyph.group([parts], { transform: `translate(${w / 2 - inset},${-h / 2 + inset})`, 'pointer-events': 'none' })];
|
|
169
|
+
}
|
|
170
|
+
if (node.icon || node.iconUrl) {
|
|
171
|
+
const size = fixed;
|
|
172
|
+
const bx = w / 2 - (size / 2 + 2), by = -h / 2 + (size / 2 + 2);
|
|
173
|
+
return [node.iconUrl
|
|
174
|
+
? Glyph.image(node.iconUrl, bx - size / 2, by - size / 2, size, size, { 'pointer-events': 'none' })
|
|
175
|
+
: Glyph.text(node.icon, bx, by, { fill: node.strokeColor || '#fff', textAnchor: 'middle', verticalAlign: 'middle', fontSize: `${size}px`, 'pointer-events': 'none' })];
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The name inside the box (wrapped, top-left) or below a notation shape; images have none. */
|
|
181
|
+
_renderLabel(g, doc) {
|
|
182
|
+
const node = this.node;
|
|
183
|
+
if (node.shape === 'image') return;
|
|
184
|
+
const text = (x, y, content, attrs) => {
|
|
185
|
+
const t = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
186
|
+
for (const [k, v] of Object.entries({ x, y, 'pointer-events': 'none', 'font-family': 'sans-serif', ...attrs })) t.setAttribute(k, String(v));
|
|
187
|
+
t.textContent = content;
|
|
188
|
+
g.appendChild(t);
|
|
189
|
+
};
|
|
190
|
+
const { width: w, height: h } = node;
|
|
191
|
+
const inside = (node.labelPosition || (node.shape === 'note' ? 'inside' : 'below')) === 'inside';
|
|
192
|
+
if (inside) {
|
|
193
|
+
const font = TextStyle.svgAttributes(this.textStyle, { defaultPixels: 10 });
|
|
194
|
+
const PAD = 6, FONT_SIZE = font['font-size'], LINE_HEIGHT = Math.round(FONT_SIZE * 1.2);
|
|
195
|
+
const lines = LabelHelper.wrapLabelLines(this.name, w - PAD * 2, FONT_SIZE, h - PAD * 2, LINE_HEIGHT);
|
|
196
|
+
// Top left unless the occurrence says otherwise, as in Archi.
|
|
197
|
+
const at = TextStyle.block(this.textStyle, { left: this.x - w / 2, right: this.x + w / 2, top: this.y - h / 2, bottom: this.y + h / 2 },
|
|
198
|
+
lines.length, { pixels: FONT_SIZE, lineHeight: LINE_HEIGHT, pad: PAD });
|
|
199
|
+
lines.forEach((line, i) => text(at.x, at.baseline + i * LINE_HEIGHT, line,
|
|
200
|
+
{ ...font, fill: this.textColor, 'font-size': `${FONT_SIZE}px`, 'text-anchor': at.textAnchor }));
|
|
201
|
+
} else {
|
|
202
|
+
text(this.x, this.y + h / 2 + 9, this.name, { fill: '#333', 'font-size': '9px', 'text-anchor': 'middle' });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The corner badge's size in pixels (badgeSize). */
|
|
208
|
+
ArchimateElementTemplate.BADGE_SIZE = 16;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { BianNotation } from '../../bian.js';
|
|
2
|
+
import { ServiceDomain } from './ServiceDomain.js';
|
|
3
|
+
import { parseArchiFile } from '../../archimateArchive.js';
|
|
4
|
+
import { parseExchangeModel } from '../../archimateExchangeFormat.js';
|
|
5
|
+
import { ZipArchive } from '../../zip/ZipArchive.js';
|
|
6
|
+
|
|
7
|
+
/** A stereotype as compared: no case, no spaces ('Service Domain' is 'servicedomain'). */
|
|
8
|
+
const plainWord = (text) => String(text ?? '').toLowerCase().replace(/[\s_-]+/g, '');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* BianLandscapeReader — service domains from BIAN's published service
|
|
12
|
+
* landscape, an ArchiMate model (an Archi file or the Exchange Format, as
|
|
13
|
+
* parseArchiFile / parseExchangeModel give it), rather than retyped.
|
|
14
|
+
*
|
|
15
|
+
* BIAN's own model (its v14 landscape) marks each element with a stereotype:
|
|
16
|
+
* a business area is a Grouping marked BusinessArea, composed of business
|
|
17
|
+
* domains (Capabilities marked BusinessDomain), each composed of service
|
|
18
|
+
* domains (Capabilities marked ServiceDomain); other Groupings aggregate
|
|
19
|
+
* service domains as overlays (a value chain, a release wave). Where the file
|
|
20
|
+
* keeps the stereotypes (an Archi specialization, or a "Stereotype" property:
|
|
21
|
+
* `stereotypeKeys`), they say which element is which: a service domain's
|
|
22
|
+
* business domain is the container marked so, its area the one that holds that.
|
|
23
|
+
* Without them it goes by structure: a service domain is a service-domain-type
|
|
24
|
+
* element nothing of those types sits inside, its nearest container its
|
|
25
|
+
* business domain and the outermost its area.
|
|
26
|
+
*
|
|
27
|
+
* What contains what: a composition from the container to what it holds before
|
|
28
|
+
* an aggregation (an overlay's), and failing both, a box drawn inside another's
|
|
29
|
+
* in a view. A service domain's functional pattern is the one realizing it (an
|
|
30
|
+
* element named as a BIAN pattern, as BIAN's model has them), else a property
|
|
31
|
+
* (`patternKeys`); its asset a property (`assetKeys`). A pattern BIAN lacks is
|
|
32
|
+
* left for the model check to report.
|
|
33
|
+
*/
|
|
34
|
+
export class BianLandscapeReader {
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} [options]
|
|
37
|
+
* @param {string[]} [options.serviceDomainTypes] - Element types a service domain may be
|
|
38
|
+
* @param {string[]} [options.containerTypes] - Element types a business area or domain may be
|
|
39
|
+
* @param {string[]} [options.containmentTypes] - Relationship types from a container to what it holds, the first preferred
|
|
40
|
+
* @param {{serviceDomain: string, businessDomain: string, businessArea: string}} [options.stereotypes] - BIAN's stereotypes for each
|
|
41
|
+
* @param {string[]} [options.stereotypeKeys] - Property keys a stereotype may be under
|
|
42
|
+
* @param {string[]} [options.patternKeys] - Property keys naming its functional pattern
|
|
43
|
+
* @param {string[]} [options.assetKeys] - Property keys naming the asset it manages
|
|
44
|
+
*/
|
|
45
|
+
constructor({
|
|
46
|
+
serviceDomainTypes = ['Capability'],
|
|
47
|
+
containerTypes = ['Grouping', 'Capability'],
|
|
48
|
+
containmentTypes = ['CompositionRelationship', 'AggregationRelationship'],
|
|
49
|
+
stereotypes = { serviceDomain: 'ServiceDomain', businessDomain: 'BusinessDomain', businessArea: 'BusinessArea' },
|
|
50
|
+
stereotypeKeys = ['Stereotype', 'Stereotypes', 'stereotype', 'stereotypes'],
|
|
51
|
+
patternKeys = ['Functional Pattern', 'functional pattern', 'FunctionalPattern', 'pattern'],
|
|
52
|
+
assetKeys = ['Asset Type', 'asset type', 'Asset', 'asset'],
|
|
53
|
+
} = {}) {
|
|
54
|
+
Object.assign(this, { serviceDomainTypes, containerTypes, containmentTypes, stereotypes, stereotypeKeys, patternKeys, assetKeys });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* An ArchiMate model from a file: an Archi file (XML or an archive) or the
|
|
59
|
+
* Exchange Format, as parseArchiFile / parseExchangeModel give it.
|
|
60
|
+
* @param {string|Uint8Array|ArrayBuffer} data
|
|
61
|
+
*/
|
|
62
|
+
static parse(data) {
|
|
63
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
64
|
+
if (typeof bytes !== 'string' && ZipArchive.isZip(bytes)) return parseArchiFile(bytes);
|
|
65
|
+
const text = typeof bytes === 'string' ? bytes : new TextDecoder().decode(bytes);
|
|
66
|
+
return /opengroup\.org\/xsd\/archimate/.test(text.slice(0, 2000)) ? parseExchangeModel(text) : parseArchiFile(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Service domain specs (ServiceDomain's), one per service domain in the landscape.
|
|
71
|
+
* @param {{elements: Array, relationships?: Array, views?: Array}} parsed
|
|
72
|
+
* @returns {Array<Object>}
|
|
73
|
+
*/
|
|
74
|
+
read(parsed) {
|
|
75
|
+
const elements = new Map((parsed.elements ?? []).map(e => [e.id, e]));
|
|
76
|
+
const containers = this.containers(parsed, elements);
|
|
77
|
+
const stereotypeOf = this._stereotypes(parsed);
|
|
78
|
+
const is = (id, which) => stereotypeOf(elements.get(id)).includes(plainWord(this.stereotypes[which]));
|
|
79
|
+
const name = (id) => elements.get(id)?.name ?? '';
|
|
80
|
+
const marked = [...elements.keys()].some(id => is(id, 'serviceDomain'));
|
|
81
|
+
let found;
|
|
82
|
+
if (marked) {
|
|
83
|
+
// Marked so, or (as some in BIAN's own model are not) a service-domain-type element in a business domain.
|
|
84
|
+
const isServiceDomain = (e) => is(e.id, 'serviceDomain') || (this.serviceDomainTypes.includes(e.type)
|
|
85
|
+
&& !is(e.id, 'businessDomain') && !is(e.id, 'businessArea') && (containers.get(e.id) ?? []).some(c => is(c, 'businessDomain')));
|
|
86
|
+
found = [...elements.values()].filter(isServiceDomain).map(e => {
|
|
87
|
+
const domain = (containers.get(e.id) ?? []).find(c => is(c, 'businessDomain'));
|
|
88
|
+
const area = [...(containers.get(domain) ?? []), ...(containers.get(e.id) ?? [])].find(c => is(c, 'businessArea'));
|
|
89
|
+
return [e, area ? name(area) : '', domain ? name(domain) : ''];
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
const holdsAny = new Set([...containers.values()].flat());
|
|
93
|
+
found = [...elements.values()].filter(e => this.serviceDomainTypes.includes(e.type) && !holdsAny.has(e.id)).map(e => {
|
|
94
|
+
const chain = [];
|
|
95
|
+
for (let at = containers.get(e.id)?.[0]; at && !chain.includes(at); at = containers.get(at)?.[0]) chain.push(at);
|
|
96
|
+
return [e, chain.length ? name(chain[chain.length - 1]) : '', chain.length > 1 ? name(chain[0]) : ''];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// A model may hold copies of one service domain (BIAN's has some, for other diagrams): the placed one is it.
|
|
100
|
+
const placed = new Set(found.filter(([, , domain]) => domain).map(([e]) => plainWord(e.name)));
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
found = found.filter(([e, , domain]) => {
|
|
103
|
+
const key = plainWord(e.name);
|
|
104
|
+
if (seen.has(key) || (!domain && placed.has(key))) return false;
|
|
105
|
+
if (domain || !placed.has(key)) seen.add(key);
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
const realizedBy = this._patternsRealizing(parsed, elements);
|
|
109
|
+
return found.map(([e, businessArea, businessDomain]) => {
|
|
110
|
+
const pattern = realizedBy.get(e.id) ?? this._property(e, this.patternKeys);
|
|
111
|
+
return {
|
|
112
|
+
kind: ServiceDomain.kind,
|
|
113
|
+
id: e.id,
|
|
114
|
+
name: e.name ?? '',
|
|
115
|
+
...(e.documentation && { description: e.documentation }),
|
|
116
|
+
businessArea, businessDomain,
|
|
117
|
+
asset: this._property(e, this.assetKeys) ?? '',
|
|
118
|
+
pattern: BianNotation.findPattern(pattern)?.name ?? pattern ?? '',
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* What each element sits in (child id -> its containers, the likeliest
|
|
125
|
+
* first): by the containment relationships, a type's before the next's
|
|
126
|
+
* (composition before aggregation), then by being drawn inside a container
|
|
127
|
+
* in a view.
|
|
128
|
+
*/
|
|
129
|
+
containers(parsed, elements = new Map((parsed.elements ?? []).map(e => [e.id, e]))) {
|
|
130
|
+
const isContainer = (id) => this.containerTypes.includes(elements.get(id)?.type);
|
|
131
|
+
const of = new Map();
|
|
132
|
+
const add = (child, container) => {
|
|
133
|
+
if (!elements.has(child) || child === container) return;
|
|
134
|
+
const list = of.get(child) ?? [];
|
|
135
|
+
if (!list.includes(container)) of.set(child, [...list, container]);
|
|
136
|
+
};
|
|
137
|
+
for (const type of this.containmentTypes) {
|
|
138
|
+
for (const r of parsed.relationships ?? []) if (r.type === type && isContainer(r.source)) add(r.target, r.source);
|
|
139
|
+
}
|
|
140
|
+
const walk = (visuals, container) => {
|
|
141
|
+
for (const v of visuals ?? []) {
|
|
142
|
+
const id = v.archimateElement;
|
|
143
|
+
if (id && container) add(id, container);
|
|
144
|
+
walk(v.children, id && isContainer(id) ? id : container);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
for (const view of parsed.views ?? []) walk(view.children, null);
|
|
148
|
+
return of;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An element's stereotypes (compared plainly): its Archi specialization's name, and any stereotype property. @private */
|
|
152
|
+
_stereotypes(parsed) {
|
|
153
|
+
const profiles = new Map((parsed.archi?.elements ?? []).filter(e => e.tag === 'profile').map(p => [p.attributes?.id, p.attributes?.name]));
|
|
154
|
+
return (element) => {
|
|
155
|
+
if (!element) return [];
|
|
156
|
+
const profile = element.archi?.attributes?.profiles;
|
|
157
|
+
return [
|
|
158
|
+
...(profile ? String(profile).split(/\s+/).map(id => profiles.get(id)).filter(Boolean) : []),
|
|
159
|
+
...String(this._property(element, this.stereotypeKeys) ?? '').split(/[,;|]/),
|
|
160
|
+
].map(plainWord).filter(Boolean);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Each service domain's pattern by what realizes it: an element named as a BIAN functional pattern. @private */
|
|
165
|
+
_patternsRealizing(parsed, elements) {
|
|
166
|
+
const patternOf = new Map();
|
|
167
|
+
for (const r of parsed.relationships ?? []) {
|
|
168
|
+
if (r.type !== 'RealizationRelationship' || patternOf.has(r.target)) continue;
|
|
169
|
+
const pattern = BianNotation.findPattern(elements.get(r.source)?.name);
|
|
170
|
+
if (pattern) patternOf.set(r.target, pattern.name);
|
|
171
|
+
}
|
|
172
|
+
return patternOf;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The first of some property keys an element has a value for. @private */
|
|
176
|
+
_property(element, keys) {
|
|
177
|
+
const props = element.properties ?? {};
|
|
178
|
+
const pairs = Array.isArray(props) ? props.map(p => [p.key, p.value]) : Object.entries(props);
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const hit = pairs.find(([k, v]) => k === key && v);
|
|
181
|
+
if (hit) return String(hit[1]).trim();
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|