@peter.prib/renderengine 0.2.6 → 0.2.10
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 +70 -9
- package/REFERENCE.md +303 -2
- package/dist/renderengine-workspace.umd.js +8851 -3305
- package/dist/renderengine.umd.js +5621 -1382
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +251 -19
- package/package.json +1 -1
- package/src/bian.js +182 -0
- package/src/charting/DiagramEditor.js +5 -26
- package/src/charting/DiagramTypeRegistry.js +8 -2
- package/src/charting/FormDialog.js +398 -385
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/RowsField.js +127 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/ClassificationLens.js +33 -0
- package/src/charting/datamodel/ClassificationScheme.js +98 -0
- package/src/charting/datamodel/DataModel.js +574 -12
- package/src/charting/datamodel/DataModelChecker.js +338 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +40 -10
- package/src/charting/datamodel/DataModelEditCommand.js +47 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +55 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +64 -0
- package/src/charting/datamodel/ServiceDomain.js +186 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/SqlDdl.js +80 -0
- package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +21 -5
- package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
- package/src/charting/datamodel/naming/NameStyle.js +57 -0
- package/src/charting/datamodel/naming/NamingStandard.js +132 -0
- package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
- package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
- package/src/charting/datamodel/naming/index.js +8 -0
- package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
- package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
- package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
- package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
- package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
- package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
- package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
- package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
- package/src/charting/datamodel/scorecard/index.js +8 -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 +4 -11
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +19 -5
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +173 -158
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/toolbar/ToolbarSelect.js +6 -0
- package/src/datamodel.js +402 -4
- package/src/index.js +6 -4
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
- package/src/workspace/parts/DataModelPart.js +534 -33
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A ratio as a whole score out of 100 (none: null). */
|
|
2
|
+
export const percent = (part, whole) => (whole ? Math.round((100 * part) / whole) : null);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ScorecardCategory — one of the Data Model Scorecard's ten categories (Steve
|
|
6
|
+
* Hoberman, *Data Model Scorecard*, 2015): its `key`, `label` and the question
|
|
7
|
+
* it asks. This base is one a reviewer scores (`measure` says nothing); a
|
|
8
|
+
* subclass measures it from the model, a score out of 100 and notes on why,
|
|
9
|
+
* or null when the model cannot say. A reviewer's score overrides either.
|
|
10
|
+
*/
|
|
11
|
+
export class ScorecardCategory {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} key - 'correctness'
|
|
14
|
+
* @param {string} label - 'Correctness'
|
|
15
|
+
* @param {string} question - What it asks of the model
|
|
16
|
+
*/
|
|
17
|
+
constructor(key, label, question) {
|
|
18
|
+
Object.assign(this, { key, label, question });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the model says how it scores (else a reviewer must). */
|
|
22
|
+
get measured() {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Its score from the model: `{ score: 0..100 | null, notes: string[] }`.
|
|
28
|
+
* @param {DataModel} model
|
|
29
|
+
* @param {Array<Object>} findings - The model check's findings (DataModelChecker#run)
|
|
30
|
+
*/
|
|
31
|
+
measure(model, findings) {
|
|
32
|
+
return { score: null, notes: ['for a reviewer to score'] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A category measured from the model: a subclass says how. */
|
|
37
|
+
export class MeasuredCategory extends ScorecardCategory {
|
|
38
|
+
get measured() {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @abstract */
|
|
43
|
+
measure(model, findings) {
|
|
44
|
+
throw new Error(`${this.constructor.name} must implement measure()`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MeasuredCategory, percent } from './ScorecardCategory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standards: are names as the model's naming standard says? Measured as the
|
|
5
|
+
* share of entity and attribute names that keep it; without a standard the
|
|
6
|
+
* model cannot say (a reviewer may).
|
|
7
|
+
*/
|
|
8
|
+
export class StandardsCategory extends MeasuredCategory {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('standards', 'Standards', 'Are names written as the naming standard says?');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
measure(model) {
|
|
14
|
+
const standard = model.namingStandard;
|
|
15
|
+
if (!standard) return { score: null, notes: ['no naming standard: set one to measure this'] };
|
|
16
|
+
let names = 0, kept = 0;
|
|
17
|
+
for (const level of model.constructor.LEVELS) {
|
|
18
|
+
for (const e of model.level(level).entities) {
|
|
19
|
+
names++;
|
|
20
|
+
if (!standard.check(level, 'entities', e.name).length) kept++;
|
|
21
|
+
for (const a of e.attributes ?? []) {
|
|
22
|
+
names++;
|
|
23
|
+
if (!standard.check(level, 'attributes', a.name).length) kept++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names ? { score: percent(kept, names), notes: [`${kept} of ${names} names keep the naming standard`] } : { score: null, notes: ['no names'] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MeasuredCategory, percent } from './ScorecardCategory.js';
|
|
2
|
+
|
|
3
|
+
/** The model check's findings that are about structure. */
|
|
4
|
+
const STRUCTURAL = new Set([
|
|
5
|
+
'no-primary-key', 'reference-missing', 'foreign-key-without-relationship', 'foreign-key-unrelated',
|
|
6
|
+
'many-to-many', 'repeating-group', 'generalization-cycle',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Structure: is the model sound? Measured as the share of logical entities and
|
|
11
|
+
* tables the model check finds nothing structural on (keys, foreign keys,
|
|
12
|
+
* many-to-many, repeating groups, generalizations going round).
|
|
13
|
+
*/
|
|
14
|
+
export class StructureCategory extends MeasuredCategory {
|
|
15
|
+
static STRUCTURAL = STRUCTURAL;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super('structure', 'Structure', 'Is it sound: keys, foreign keys, no repeating groups or unresolved many-to-many?');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
measure(model, findings) {
|
|
22
|
+
const entities = ['logical', 'physical'].flatMap(level => model.level(level).entities.map(e => `${level}:${e.id}`));
|
|
23
|
+
if (!entities.length) return { score: null, notes: ['no logical entities or tables'] };
|
|
24
|
+
const structural = findings.filter(f => STRUCTURAL.has(f.code));
|
|
25
|
+
const touched = new Set(structural.flatMap(f => (f.ids ?? []).map(id => `${f.level}:${id}`)).filter(k => entities.includes(k)));
|
|
26
|
+
const counts = structural.reduce((m, f) => ((m[f.code] = (m[f.code] ?? 0) + 1), m), {});
|
|
27
|
+
return {
|
|
28
|
+
score: percent(entities.length - touched.size, entities.length),
|
|
29
|
+
notes: [`${entities.length - touched.size} of ${entities.length} logical entities and tables sound`, ...Object.entries(counts).map(([code, n]) => `${n} ${code}`)],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ScorecardCategory, MeasuredCategory } from './ScorecardCategory.js';
|
|
2
|
+
export { CompletenessCategory } from './CompletenessCategory.js';
|
|
3
|
+
export { StructureCategory } from './StructureCategory.js';
|
|
4
|
+
export { StandardsCategory } from './StandardsCategory.js';
|
|
5
|
+
export { DefinitionsCategory } from './DefinitionsCategory.js';
|
|
6
|
+
export { ConsistencyCategory } from './ConsistencyCategory.js';
|
|
7
|
+
export { ReadabilityCategory } from './ReadabilityCategory.js';
|
|
8
|
+
export { ModelScorecard } from './ModelScorecard.js';
|
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import { Command } from '../CommandHistory.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MergeModelCommand — things from another model brought into this one
|
|
5
|
-
* (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
|
|
6
|
-
* order given (what is needed first). One undo step; undo takes each back,
|
|
7
|
-
* last first.
|
|
8
|
-
*/
|
|
9
|
-
export class MergeModelCommand extends Command {
|
|
10
|
-
/** A merge is no edit on one view: undoing it shows no other. */
|
|
11
|
-
viewId = null;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {ViewSetModel} model - Merged into
|
|
15
|
-
* @param {ViewSetModel} from - Merged from
|
|
16
|
-
* @param {Array<{kind: ComparedKind, item: Object}>} steps
|
|
17
|
-
* @param {string} [label]
|
|
18
|
-
*/
|
|
19
|
-
constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
|
|
20
|
-
super(label);
|
|
21
|
-
this.model = model;
|
|
22
|
-
this.from = from;
|
|
23
|
-
this.steps = steps;
|
|
24
|
-
this._undos = [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
execute() {
|
|
28
|
-
this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
|
|
29
|
-
this.
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
undo() {
|
|
33
|
-
for (const undo of [...this._undos].reverse()) undo();
|
|
34
|
-
this._undos = [];
|
|
35
|
-
this.
|
|
36
|
-
}
|
|
37
|
-
|
|
1
|
+
import { Command } from '../CommandHistory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MergeModelCommand — things from another model brought into this one
|
|
5
|
+
* (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
|
|
6
|
+
* order given (what is needed first). One undo step; undo takes each back,
|
|
7
|
+
* last first.
|
|
8
|
+
*/
|
|
9
|
+
export class MergeModelCommand extends Command {
|
|
10
|
+
/** A merge is no edit on one view: undoing it shows no other. */
|
|
11
|
+
viewId = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {ViewSetModel} model - Merged into
|
|
15
|
+
* @param {ViewSetModel} from - Merged from
|
|
16
|
+
* @param {Array<{kind: ComparedKind, item: Object}>} steps
|
|
17
|
+
* @param {string} [label]
|
|
18
|
+
*/
|
|
19
|
+
constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
|
|
20
|
+
super(label);
|
|
21
|
+
this.model = model;
|
|
22
|
+
this.from = from;
|
|
23
|
+
this.steps = steps;
|
|
24
|
+
this._undos = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
execute() {
|
|
28
|
+
this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
|
|
29
|
+
this._changed();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
undo() {
|
|
33
|
+
for (const undo of [...this._undos].reverse()) undo();
|
|
34
|
+
this._undos = [];
|
|
35
|
+
this._changed();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The model changed: a view set tells its listeners; a model without views (a DataModel) says it was edited. @private */
|
|
39
|
+
_changed() {
|
|
40
|
+
this.model._notify?.();
|
|
41
|
+
this.model.edited?.();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenViewList — the views of a model that have a tab, in tab order, as a
|
|
3
|
+
* browser keeps its tabs: closing a tab leaves the view in the model, and a
|
|
4
|
+
* view is opened by going to it (from the tree, a search, a new view). It
|
|
5
|
+
* starts with the view in front only, so a model opened or imported with many
|
|
6
|
+
* views does not fill the tab strip.
|
|
7
|
+
*
|
|
8
|
+
* It follows the model: `sync()` drops views since deleted and opens the one in
|
|
9
|
+
* front, unless paused (`while`: a report passing through every view opens none).
|
|
10
|
+
*/
|
|
11
|
+
export class OpenViewList {
|
|
12
|
+
/**
|
|
13
|
+
* @param {ViewSetModel} model
|
|
14
|
+
* @param {Iterable<string>} [ids] - Those open to start with (default: the view in front)
|
|
15
|
+
*/
|
|
16
|
+
constructor(model, ids = null) {
|
|
17
|
+
this.model = model;
|
|
18
|
+
this.ids = ids ? [...ids] : (model.currentViewId ? [model.currentViewId] : []);
|
|
19
|
+
this._paused = 0;
|
|
20
|
+
this.sync();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Follow the model: deleted views gone, the one in front open (last). Returns the open views. */
|
|
24
|
+
sync() {
|
|
25
|
+
this.ids = this.ids.filter(id => this.model.getView(id));
|
|
26
|
+
const current = this.model.currentViewId;
|
|
27
|
+
if (!this._paused && current && !this.ids.includes(current)) this.ids.push(current);
|
|
28
|
+
return this.views();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The open views, in tab order. */
|
|
32
|
+
views() {
|
|
33
|
+
return this.ids.map(id => this.model.getView(id)).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isOpen(id) {
|
|
37
|
+
return this.ids.includes(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Open a view's tab (if not open) and bring it to the front. */
|
|
41
|
+
open(id) {
|
|
42
|
+
if (!this.model.getView(id)) return false;
|
|
43
|
+
if (!this.ids.includes(id)) this.ids.push(id);
|
|
44
|
+
if (this.model.currentViewId !== id) this.model.switchTo(id);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether a tab may be closed: one always stays open. */
|
|
49
|
+
canClose(id) {
|
|
50
|
+
return this.ids.includes(id) && this.ids.length > 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Close a view's tab; the view stays in the model. Closing the tab in front
|
|
55
|
+
* brings its neighbour (the next, else the one before) to the front.
|
|
56
|
+
* @returns {boolean} whether it closed
|
|
57
|
+
*/
|
|
58
|
+
close(id) {
|
|
59
|
+
if (!this.canClose(id)) return false;
|
|
60
|
+
const index = this.ids.indexOf(id);
|
|
61
|
+
this.ids.splice(index, 1);
|
|
62
|
+
if (this.model.currentViewId === id) this.model.switchTo(this.ids[Math.min(index, this.ids.length - 1)]);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run `fn` with the list paused: going through views (a report's pictures) opens none. */
|
|
67
|
+
while(fn) {
|
|
68
|
+
this._paused++;
|
|
69
|
+
try { return fn(); } finally { this._paused--; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toJSON() {
|
|
73
|
+
return [...this.ids];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Every finding. @returns {Array<Object>} */
|
|
24
|
-
run() {
|
|
25
|
-
return this.checks().flatMap(check => check.call(this));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Views with nothing on them (a template is left out: an empty one is still a start). */
|
|
29
|
-
emptyViews() {
|
|
30
|
-
return this.model.views()
|
|
31
|
-
.filter(v => !v.template && v.isEmpty())
|
|
32
|
-
.map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
import { ModelChecker } from '../ModelChecker.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ViewSetChecker — checks over any ViewSetModel (a template editor's
|
|
5
|
+
* DiagramModel, an ArchimateModel): what is wrong or worth a look across its
|
|
6
|
+
* views (a ModelChecker: findings, checks listed by `checks()`). A notation
|
|
7
|
+
* adds its own checks in a subclass (ArchimateModelChecker). `kind` in a
|
|
8
|
+
* finding is 'view' here; a subclass adds its own.
|
|
9
|
+
*/
|
|
10
|
+
export class ViewSetChecker extends ModelChecker {
|
|
11
|
+
/** The checks to run, in the order their findings are listed. */
|
|
12
|
+
checks() {
|
|
13
|
+
return [this.emptyViews];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Views with nothing on them (a template is left out: an empty one is still a start). */
|
|
17
|
+
emptyViews() {
|
|
18
|
+
return this.model.views()
|
|
19
|
+
.filter(v => !v.template && v.isEmpty())
|
|
20
|
+
.map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -94,8 +94,8 @@ export class DataModelDiagram extends Diagram {
|
|
|
94
94
|
* note to what it is about, or (the `links` option) a mapping between levels.
|
|
95
95
|
* @param {{id?: string, source: string, target: string, label?: string}} link
|
|
96
96
|
*/
|
|
97
|
-
static buildLink({ id, source, target, label }) {
|
|
98
|
-
return { ...(id && { id }), source, target, ...(label && { label }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
|
|
97
|
+
static buildLink({ id, source, target, label, color }) {
|
|
98
|
+
return { ...(id && { id }), source, target, ...(label && { label }), ...(color && { color }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
static typeGroup(type) {
|
|
@@ -146,15 +146,8 @@ export class DataModelDiagram extends Diagram {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** A relationship's edge in a notation, as its kind draws it (DataModelNotation.relationshipKind). */
|
|
149
150
|
static buildEdge(relationship, notation) {
|
|
150
|
-
return
|
|
151
|
-
...relationship,
|
|
152
|
-
label: relationship.name,
|
|
153
|
-
lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
|
|
154
|
-
arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
|
|
155
|
-
arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
|
|
156
|
-
sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
|
|
157
|
-
targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
|
|
158
|
-
};
|
|
151
|
+
return DataModelNotation.relationshipKind(relationship).edge(relationship, notation);
|
|
159
152
|
}
|
|
160
153
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DataModelNotation } from '../../datamodel.js';
|
|
1
2
|
/**
|
|
2
3
|
* Strategy interface for turning data-model relationships into diagram nodes
|
|
3
4
|
* and edges. Crow's Foot and UML keep relationships as direct edges; Chen
|
|
@@ -42,7 +43,7 @@ export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
|
|
|
42
43
|
/**
|
|
43
44
|
* Where a diamond between two places goes: their midpoint when it is clear
|
|
44
45
|
* of everything in `occupied`, else the nearest clear spot stepping out
|
|
45
|
-
* across the line, one side then the other;
|
|
46
|
+
* across the line, one side then the other; when none is clear, the least covered.
|
|
46
47
|
* @returns {{x: number, y: number}}
|
|
47
48
|
*/
|
|
48
49
|
static spotFor(from, to, occupied) {
|
|
@@ -50,22 +51,35 @@ export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
|
|
|
50
51
|
const mid = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
|
|
51
52
|
const length = Math.hypot(to.x - from.x, to.y - from.y) || 1;
|
|
52
53
|
const across = { x: -(to.y - from.y) / length, y: (to.x - from.x) / length };
|
|
53
|
-
|
|
54
|
+
// How much of a diamond at a spot is covered by what is there (0: clear).
|
|
55
|
+
const covered = ({ x, y }) => occupied.reduce((sum, o) => {
|
|
56
|
+
const w = Math.min(x + width / 2, o.x1) - Math.max(x - width / 2, o.x0);
|
|
57
|
+
const h = Math.min(y + height / 2, o.y1) - Math.max(y - height / 2, o.y0);
|
|
58
|
+
return sum + (w > 0 && h > 0 ? w * h : 0);
|
|
59
|
+
}, 0);
|
|
60
|
+
let best = mid, least = Infinity;
|
|
54
61
|
for (let k = 0; k <= RelationshipAsNodeStrategy.TRIES; k++) {
|
|
55
62
|
for (const side of k ? [1, -1] : [0]) {
|
|
56
63
|
const at = { x: mid.x + across.x * side * k * RelationshipAsNodeStrategy.STEP, y: mid.y + across.y * side * k * RelationshipAsNodeStrategy.STEP };
|
|
57
|
-
|
|
64
|
+
const c = covered(at);
|
|
65
|
+
if (c === 0) return at;
|
|
66
|
+
if (c < least) { best = at; least = c; }
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
|
-
return
|
|
69
|
+
return best; // none clear: the least covered
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
apply(relationships, { notation, entity = () => undefined, occupied = [] }) {
|
|
72
|
+
apply(relationships, { notation, buildEdge, entity = () => undefined, occupied = [] }) {
|
|
64
73
|
const nodes = [];
|
|
65
74
|
const edges = [];
|
|
66
75
|
const taken = [...occupied];
|
|
67
76
|
const { width, height } = RelationshipAsNodeStrategy.DIAMOND;
|
|
68
77
|
relationships.forEach((relationship, i) => {
|
|
78
|
+
// A kind drawn as a line in this notation (a generalization) is one.
|
|
79
|
+
if (!DataModelNotation.relationshipKind(relationship).drawnAsNode(notation)) {
|
|
80
|
+
edges.push(buildEdge(relationship, notation));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
69
83
|
const relId = relationship.id || `${relationship.source}__${relationship.target}__rel${i}`;
|
|
70
84
|
const lineStyle = relationship.identifying === false ? 'dashed' : 'solid';
|
|
71
85
|
const [from, to] = [entity(relationship.source), entity(relationship.target)];
|