@oml/language 0.13.0 → 0.14.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.
Files changed (41) hide show
  1. package/out/oml/index.d.ts +3 -1
  2. package/out/oml/index.js +19 -1
  3. package/out/oml/index.js.map +1 -1
  4. package/out/oml/oml-candidates.d.ts +3 -3
  5. package/out/oml/oml-candidates.js +1 -1
  6. package/out/oml/oml-candidates.js.map +1 -1
  7. package/out/oml/oml-diagram.d.ts +17 -0
  8. package/out/oml/oml-diagram.js +1549 -0
  9. package/out/oml/oml-diagram.js.map +1 -0
  10. package/out/oml/oml-document.d.ts +5 -0
  11. package/out/oml/oml-document.js +12 -1
  12. package/out/oml/oml-document.js.map +1 -1
  13. package/out/oml/oml-index.d.ts +2 -1
  14. package/out/oml/oml-index.js +90 -9
  15. package/out/oml/oml-index.js.map +1 -1
  16. package/out/oml/oml-scope.js +4 -3
  17. package/out/oml/oml-scope.js.map +1 -1
  18. package/out/oml/oml-search.d.ts +24 -0
  19. package/out/oml/oml-search.js +95 -0
  20. package/out/oml/oml-search.js.map +1 -0
  21. package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
  22. package/out/oml/{oml-edit.js → oml-update.js} +256 -56
  23. package/out/oml/oml-update.js.map +1 -0
  24. package/out/oml/oml-utils.d.ts +1 -1
  25. package/out/oml/oml-utils.js +3 -4
  26. package/out/oml/oml-utils.js.map +1 -1
  27. package/out/oml/oml-validator.d.ts +1 -0
  28. package/out/oml/oml-validator.js +17 -2
  29. package/out/oml/oml-validator.js.map +1 -1
  30. package/package.json +4 -2
  31. package/src/oml/index.ts +32 -1
  32. package/src/oml/oml-candidates.ts +4 -4
  33. package/src/oml/oml-diagram.ts +1708 -0
  34. package/src/oml/oml-document.ts +13 -1
  35. package/src/oml/oml-index.ts +87 -9
  36. package/src/oml/oml-scope.ts +4 -3
  37. package/src/oml/oml-search.ts +132 -0
  38. package/src/oml/{oml-edit.ts → oml-update.ts} +302 -55
  39. package/src/oml/oml-utils.ts +3 -4
  40. package/src/oml/oml-validator.ts +17 -2
  41. package/out/oml/oml-edit.js.map +0 -1
@@ -0,0 +1,1549 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+ import * as ElkModule from 'elkjs/lib/elk.bundled.js';
3
+ import { URI } from 'langium';
4
+ import { isAspect, isConcept, isConceptInstance, isDescription, isDescriptionBundle, isEquivalenceAxiom, isImport, isInstanceEnumerationAxiom, isLiteralEnumerationAxiom, isOntology, isPropertyValueAssertion, isRelation, isRelationEntity, isRelationInstance, isScalar, isScalarProperty, isSpecializationAxiom, isUnreifiedRelation, isVocabulary, isVocabularyBundle } from './generated/ast.js';
5
+ import { getOntologyModelIndex } from './oml-index.js';
6
+ import { collectOntologyMembers, findOntologyMemberByName, getIriForNode, getModelIdForNode, getNamedElementName, humanizeTypeName, normalizeNamespace, splitIri } from './oml-utils.js';
7
+ const ElkConstructor = ElkModule.default || ElkModule;
8
+ let elkPromise;
9
+ const navigationIndex = new Map();
10
+ async function getElk() {
11
+ if (!elkPromise) {
12
+ elkPromise = (async () => {
13
+ try {
14
+ const workerModule = await import('elkjs/lib/elk-worker.js');
15
+ const ElkWorkerCtor = workerModule.Worker
16
+ || workerModule.default?.Worker
17
+ || workerModule.default
18
+ || workerModule;
19
+ return new ElkConstructor({
20
+ algorithms: ['layered'],
21
+ workerFactory: (url) => new ElkWorkerCtor(url)
22
+ });
23
+ }
24
+ catch (error) {
25
+ console.error('[OML Diagram] Failed to initialize ELK:', error);
26
+ return {
27
+ layout: async (graph) => graph
28
+ };
29
+ }
30
+ })();
31
+ }
32
+ return await elkPromise;
33
+ }
34
+ function getAstNodeLocator(shared) {
35
+ const isLocator = (candidate) => !!candidate
36
+ && typeof candidate.getAstNodePath === 'function'
37
+ && typeof candidate.getAstNode === 'function';
38
+ const direct = shared?.workspace?.AstNodeLocator ?? shared?.AstNodeLocator;
39
+ if (isLocator(direct)) {
40
+ return direct;
41
+ }
42
+ const registry = shared?.ServiceRegistry;
43
+ if (!registry) {
44
+ return undefined;
45
+ }
46
+ const all = registry.all;
47
+ const services = Array.isArray(all)
48
+ ? all
49
+ : (typeof all?.toArray === 'function' ? all.toArray() : Array.from((all ?? [])));
50
+ for (const languageServices of services) {
51
+ const locator = languageServices?.workspace?.AstNodeLocator ?? languageServices?.AstNodeLocator;
52
+ if (isLocator(locator)) {
53
+ return locator;
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+ function buildDiagramContext(root) {
59
+ const nsToPrefix = new Map();
60
+ for (const ownedImport of (root.ownedImports ?? [])) {
61
+ if (!isImport(ownedImport))
62
+ continue;
63
+ const importedOntology = ownedImport.imported?.ref;
64
+ const prefix = ownedImport.prefix;
65
+ if (!importedOntology || !prefix)
66
+ continue;
67
+ const ns = normalizeNamespace(String(importedOntology.namespace ?? '').replace(/^<|>$/g, ''));
68
+ if (!ns)
69
+ continue;
70
+ nsToPrefix.set(ns, prefix);
71
+ }
72
+ return { root, nsToPrefix };
73
+ }
74
+ function getContainingOntology(astNode) {
75
+ let current = astNode;
76
+ while (current && !isOntology(current)) {
77
+ current = current.$container;
78
+ }
79
+ return current;
80
+ }
81
+ function unwrapReferenceValue(candidate) {
82
+ if (!candidate || typeof candidate !== 'object') {
83
+ return candidate;
84
+ }
85
+ if ('$refText' in candidate && 'ref' in candidate) {
86
+ return candidate.ref;
87
+ }
88
+ return candidate;
89
+ }
90
+ function getQualifiedName(ctx, astNode) {
91
+ const value = unwrapReferenceValue(astNode);
92
+ if (!value) {
93
+ return undefined;
94
+ }
95
+ const iri = getIriForNode(value);
96
+ if (iri) {
97
+ const parts = splitIri(iri);
98
+ if (parts?.fragment) {
99
+ const prefix = ctx.nsToPrefix.get(normalizeNamespace(parts.base));
100
+ return prefix ? `${prefix}:${parts.fragment}` : parts.fragment;
101
+ }
102
+ }
103
+ if (isOntology(value)) {
104
+ const namespace = normalizeNamespace(String(value.namespace ?? '').replace(/^<|>$/g, ''));
105
+ if (!namespace) {
106
+ return undefined;
107
+ }
108
+ const prefix = value.prefix;
109
+ if (typeof prefix === 'string' && prefix.length > 0) {
110
+ return prefix;
111
+ }
112
+ return namespace;
113
+ }
114
+ const name = getNamedElementName(value);
115
+ if (!name) {
116
+ return undefined;
117
+ }
118
+ const owningOntology = getContainingOntology(value);
119
+ const namespace = normalizeNamespace(String(owningOntology?.namespace ?? '').replace(/^<|>$/g, ''));
120
+ const prefix = namespace ? ctx.nsToPrefix.get(namespace) : undefined;
121
+ return prefix ? `${prefix}:${name}` : name;
122
+ }
123
+ function getRefDisplayName(ctx, refLike) {
124
+ const direct = getQualifiedName(ctx, refLike?.ref ?? refLike);
125
+ if (direct) {
126
+ return direct;
127
+ }
128
+ const raw = typeof refLike?.$refText === 'string' ? refLike.$refText.trim() : '';
129
+ if (!raw) {
130
+ return undefined;
131
+ }
132
+ const normalized = raw.replace(/^<|>$/g, '');
133
+ const parts = splitIri(normalized);
134
+ if (!parts?.fragment) {
135
+ return normalized;
136
+ }
137
+ const prefix = ctx.nsToPrefix.get(normalizeNamespace(parts.base));
138
+ return prefix ? `${prefix}:${parts.fragment}` : parts.fragment;
139
+ }
140
+ function toSimpleName(label) {
141
+ if (!label)
142
+ return '';
143
+ const trimmed = label.trim();
144
+ if (!trimmed)
145
+ return '';
146
+ const colonIndex = trimmed.indexOf(':');
147
+ if (colonIndex >= 0 && colonIndex < trimmed.length - 1) {
148
+ return trimmed.slice(colonIndex + 1);
149
+ }
150
+ const parts = splitIri(trimmed);
151
+ return parts?.fragment || trimmed;
152
+ }
153
+ function truncateWithEllipsis(text, maxChars) {
154
+ if (maxChars <= 0)
155
+ return '';
156
+ if (text.length <= maxChars)
157
+ return text;
158
+ if (maxChars === 1)
159
+ return '…';
160
+ return `${text.slice(0, maxChars - 1)}…`;
161
+ }
162
+ function iriNamespace(iri) {
163
+ if (!iri)
164
+ return undefined;
165
+ const parts = splitIri(iri);
166
+ return parts?.base;
167
+ }
168
+ function formatHoverLikeTooltip(kind, qualified, iri) {
169
+ const parts = [kind];
170
+ if (qualified && qualified.length > 0) {
171
+ parts.push(qualified);
172
+ }
173
+ if (iri && iri.length > 0) {
174
+ const ns = iriNamespace(iri);
175
+ parts.push(ns ? `${iri}\nnamespace: ${ns}` : iri);
176
+ }
177
+ return parts.filter(Boolean).join('\n');
178
+ }
179
+ function toLiteralLabel(literal) {
180
+ if (!literal)
181
+ return '[literal]';
182
+ if (typeof literal.lexicalValue === 'string' && literal.lexicalValue.length > 0) {
183
+ return literal.lexicalValue;
184
+ }
185
+ if (typeof literal.value === 'string' && literal.value.length > 0) {
186
+ return literal.value;
187
+ }
188
+ if (typeof literal.stringValue === 'string' && literal.stringValue.length > 0) {
189
+ return literal.stringValue;
190
+ }
191
+ return '[literal]';
192
+ }
193
+ function getOneOfEntries(statement, ctx, viewId, astNodeLocator, rootUri) {
194
+ const entries = [];
195
+ if (isLiteralEnumerationAxiom(statement)) {
196
+ let index = 0;
197
+ for (const literal of (statement.literals ?? [])) {
198
+ const entryId = `${viewId}:oneOf:${index++}`;
199
+ entries.push({
200
+ id: entryId,
201
+ modelId: getModelIdForNode(statement, astNodeLocator, rootUri) ?? entryId,
202
+ text: toLiteralLabel(literal),
203
+ statement
204
+ });
205
+ }
206
+ return entries;
207
+ }
208
+ if (isInstanceEnumerationAxiom(statement)) {
209
+ let index = 0;
210
+ for (const instanceRef of (statement.instances ?? [])) {
211
+ const qName = getRefDisplayName(ctx, instanceRef) ?? (instanceRef?.$refText ?? '[instance]');
212
+ const entryId = `${viewId}:oneOf:${index++}`;
213
+ entries.push({
214
+ id: entryId,
215
+ modelId: getModelIdForNode(statement, astNodeLocator, rootUri) ?? entryId,
216
+ text: toSimpleName(qName),
217
+ qualifiedText: qName,
218
+ statement
219
+ });
220
+ }
221
+ }
222
+ return entries;
223
+ }
224
+ function getScalarPropertyLabel(property, ctx) {
225
+ const qName = getQualifiedName(ctx, property);
226
+ if (!qName) {
227
+ return undefined;
228
+ }
229
+ const ranges = (property.ranges ?? [])
230
+ .map((rangeRef) => getRefDisplayName(ctx, rangeRef))
231
+ .filter((value) => typeof value === 'string' && value.length > 0)
232
+ .map((value) => toSimpleName(value));
233
+ return ranges.length > 0 ? `${toSimpleName(qName)}: ${ranges.join(' | ')}` : toSimpleName(qName);
234
+ }
235
+ function getAssertionValueLabel(ctx, assertion) {
236
+ const parts = [];
237
+ for (const literal of (assertion.literalValues ?? [])) {
238
+ parts.push(toLiteralLabel(literal));
239
+ }
240
+ for (const referenced of (assertion.referencedValues ?? [])) {
241
+ const qName = getRefDisplayName(ctx, referenced);
242
+ parts.push(qName ? toSimpleName(qName) : '[instance]');
243
+ }
244
+ for (const contained of (assertion.containedValues ?? [])) {
245
+ const qName = getQualifiedName(ctx, contained);
246
+ parts.push(qName ? toSimpleName(qName) : '[instance]');
247
+ }
248
+ return parts.join(', ');
249
+ }
250
+ export async function computeLaidOutSModelForUri(shared, modelUri) {
251
+ const documentUri = URI.parse(modelUri);
252
+ let document = shared.workspace.LangiumDocuments.getDocument(documentUri);
253
+ if (!document) {
254
+ return { id: 'root', type: 'graph', children: [] };
255
+ }
256
+ const root = document.parseResult?.value;
257
+ if (!root || !isOntology(root)) {
258
+ return { id: 'root', type: 'graph', children: [] };
259
+ }
260
+ const graph = await computeDiagramGraph(shared, document, root);
261
+ indexDiagramMappings(shared, graph);
262
+ return await layoutAndConvertToSModel(graph);
263
+ }
264
+ async function computeDiagramGraph(shared, document, root) {
265
+ const graph = {
266
+ rootUri: document.uri.toString(),
267
+ root,
268
+ nodes: [],
269
+ edges: [],
270
+ statementsById: new Map()
271
+ };
272
+ if (isVocabulary(root)) {
273
+ computeVocabularyGraph(shared, root, graph);
274
+ return graph;
275
+ }
276
+ if (isDescription(root)) {
277
+ computeDescriptionGraph(shared, root, graph);
278
+ return graph;
279
+ }
280
+ if (isVocabularyBundle(root) || isDescriptionBundle(root)) {
281
+ await computeBundleGraph(shared, root, graph);
282
+ return graph;
283
+ }
284
+ return graph;
285
+ }
286
+ function computeVocabularyGraph(shared, root, graph) {
287
+ const astNodeLocator = getAstNodeLocator(shared);
288
+ const ctx = buildDiagramContext(root);
289
+ const nodeByQName = new Map();
290
+ const statements = root.ownedStatements ?? [];
291
+ const ensureEntityNode = (candidate) => {
292
+ const isReferenceWrapper = !!candidate
293
+ && typeof candidate === 'object'
294
+ && ('$refText' in candidate)
295
+ && ('ref' in candidate)
296
+ && !('$type' in candidate);
297
+ const statement = isReferenceWrapper ? candidate.ref : candidate;
298
+ const isConcreteEntity = !!statement
299
+ && (isConcept(statement) || isAspect(statement) || isRelationEntity(statement) || isScalar(statement));
300
+ const qName = isConcreteEntity
301
+ ? getQualifiedName(ctx, statement)
302
+ : (isReferenceWrapper ? getRefDisplayName(ctx, candidate) : undefined);
303
+ if (!qName)
304
+ return undefined;
305
+ const existing = nodeByQName.get(qName);
306
+ if (existing)
307
+ return existing;
308
+ const viewId = `ent:${qName}`;
309
+ const modelId = isConcreteEntity
310
+ ? (getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? viewId)
311
+ : viewId;
312
+ const nodeKind = isConcreteEntity && isUnreifiedRelation(statement) ? 'relation' : 'entity';
313
+ const node = {
314
+ id: viewId,
315
+ viewId,
316
+ modelId,
317
+ iri: isConcreteEntity ? getIriForNode(statement) : undefined,
318
+ label: toSimpleName(qName),
319
+ qualifiedLabel: qName,
320
+ tooltip: formatHoverLikeTooltip(isConcreteEntity ? humanizeTypeName(statement.$type ?? 'entity') : 'entity', qName, isConcreteEntity ? getIriForNode(statement) : undefined),
321
+ propertyEntries: [],
322
+ compartmentEntries: isConcreteEntity
323
+ ? getOneOfEntries(statement, ctx, viewId, astNodeLocator, graph.rootUri)
324
+ : [],
325
+ kind: nodeKind,
326
+ statement: isConcreteEntity ? statement : undefined
327
+ };
328
+ graph.nodes.push(node);
329
+ if (isConcreteEntity) {
330
+ graph.statementsById.set(viewId, statement);
331
+ }
332
+ for (const entry of (node.compartmentEntries ?? [])) {
333
+ if (entry.statement) {
334
+ graph.statementsById.set(entry.id, entry.statement);
335
+ }
336
+ }
337
+ nodeByQName.set(qName, node);
338
+ return node;
339
+ };
340
+ for (const statement of statements) {
341
+ ensureEntityNode(statement);
342
+ }
343
+ for (const statement of statements) {
344
+ if (isScalarProperty(statement)) {
345
+ const propertyLabel = getScalarPropertyLabel(statement, ctx);
346
+ if (!propertyLabel)
347
+ continue;
348
+ for (const domainRef of (statement.domains ?? [])) {
349
+ const domainNode = ensureEntityNode(domainRef);
350
+ if (!domainNode)
351
+ continue;
352
+ if (!domainNode.propertyEntries)
353
+ domainNode.propertyEntries = [];
354
+ const entryId = `${domainNode.viewId}:properties:${domainNode.propertyEntries.length}`;
355
+ domainNode.propertyEntries.push({
356
+ id: entryId,
357
+ modelId: getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? entryId,
358
+ text: propertyLabel,
359
+ qualifiedText: propertyLabel,
360
+ tooltip: formatHoverLikeTooltip('scalar property', getQualifiedName(ctx, statement), getIriForNode(statement)),
361
+ statement
362
+ });
363
+ graph.statementsById.set(entryId, statement);
364
+ }
365
+ continue;
366
+ }
367
+ if (isRelationEntity(statement)) {
368
+ const relNode = ensureEntityNode(statement);
369
+ if (!relNode)
370
+ continue;
371
+ const sources = (statement.sources ?? []).map((r) => ensureEntityNode(r)).filter(Boolean);
372
+ const targets = (statement.targets ?? []).map((r) => ensureEntityNode(r)).filter(Boolean);
373
+ let index = 0;
374
+ for (const src of sources) {
375
+ const edgeId = `relent:${relNode.id}:src:${index++}`;
376
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
377
+ graph.edges.push({
378
+ id: edgeId,
379
+ viewId: edgeId,
380
+ modelId,
381
+ iri: getIriForNode(statement),
382
+ sourceId: src.id,
383
+ targetId: relNode.id,
384
+ label: '',
385
+ kind: 'relation-source',
386
+ statement
387
+ });
388
+ graph.statementsById.set(edgeId, statement);
389
+ }
390
+ index = 0;
391
+ for (const dst of targets) {
392
+ const edgeId = `relent:${relNode.id}:dst:${index++}`;
393
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
394
+ graph.edges.push({
395
+ id: edgeId,
396
+ viewId: edgeId,
397
+ modelId,
398
+ iri: getIriForNode(statement),
399
+ sourceId: relNode.id,
400
+ targetId: dst.id,
401
+ label: '',
402
+ kind: 'relation-target',
403
+ statement
404
+ });
405
+ graph.statementsById.set(edgeId, statement);
406
+ }
407
+ continue;
408
+ }
409
+ if (!isUnreifiedRelation(statement))
410
+ continue;
411
+ const relationName = getQualifiedName(ctx, statement);
412
+ if (!relationName)
413
+ continue;
414
+ const sources = (statement.sources ?? []).map((r) => ensureEntityNode(r)).filter(Boolean);
415
+ const targets = (statement.targets ?? []).map((r) => ensureEntityNode(r)).filter(Boolean);
416
+ if (sources.length === 1 && targets.length === 1) {
417
+ const edgeId = `urel:${relationName}`;
418
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
419
+ graph.edges.push({
420
+ id: edgeId,
421
+ viewId: edgeId,
422
+ modelId,
423
+ iri: getIriForNode(statement),
424
+ sourceId: sources[0].id,
425
+ targetId: targets[0].id,
426
+ label: toSimpleName(relationName),
427
+ qualifiedLabel: relationName,
428
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(statement)),
429
+ kind: 'relation',
430
+ statement
431
+ });
432
+ graph.statementsById.set(edgeId, statement);
433
+ continue;
434
+ }
435
+ const relationNodeViewId = `rel:${relationName}`;
436
+ let relationNode = nodeByQName.get(relationName);
437
+ if (!relationNode) {
438
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? relationNodeViewId;
439
+ relationNode = {
440
+ id: relationNodeViewId,
441
+ viewId: relationNodeViewId,
442
+ modelId,
443
+ iri: getIriForNode(statement),
444
+ label: toSimpleName(relationName),
445
+ qualifiedLabel: relationName,
446
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(statement)),
447
+ kind: 'relation',
448
+ statement
449
+ };
450
+ graph.nodes.push(relationNode);
451
+ graph.statementsById.set(relationNodeViewId, statement);
452
+ nodeByQName.set(relationName, relationNode);
453
+ }
454
+ let index = 0;
455
+ for (const src of sources) {
456
+ const edgeId = `urel:${relationName}:src:${index++}`;
457
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
458
+ graph.edges.push({
459
+ id: edgeId,
460
+ viewId: edgeId,
461
+ modelId,
462
+ iri: getIriForNode(statement),
463
+ sourceId: src.id,
464
+ targetId: relationNode.id,
465
+ label: '',
466
+ tooltip: formatHoverLikeTooltip('relation source', relationName, getIriForNode(statement)),
467
+ kind: 'relation-source',
468
+ statement
469
+ });
470
+ graph.statementsById.set(edgeId, statement);
471
+ }
472
+ index = 0;
473
+ for (const dst of targets) {
474
+ const edgeId = `urel:${relationName}:dst:${index++}`;
475
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
476
+ graph.edges.push({
477
+ id: edgeId,
478
+ viewId: edgeId,
479
+ modelId,
480
+ iri: getIriForNode(statement),
481
+ sourceId: relationNode.id,
482
+ targetId: dst.id,
483
+ label: '',
484
+ tooltip: formatHoverLikeTooltip('relation target', relationName, getIriForNode(statement)),
485
+ kind: 'relation-target',
486
+ statement
487
+ });
488
+ graph.statementsById.set(edgeId, statement);
489
+ }
490
+ }
491
+ const addSpecializationEdge = (owner, axiom, index) => {
492
+ if (!isSpecializationAxiom(axiom))
493
+ return;
494
+ const superTerm = axiom.superTerm?.ref;
495
+ const subNode = ensureEntityNode(owner);
496
+ const superNode = ensureEntityNode(superTerm);
497
+ if (!subNode || !superNode)
498
+ return;
499
+ const edgeId = `spec:${subNode.id}:${superNode.id}:${index}`;
500
+ const modelId = getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? edgeId;
501
+ graph.edges.push({
502
+ id: edgeId,
503
+ viewId: edgeId,
504
+ modelId,
505
+ iri: getIriForNode(axiom),
506
+ sourceId: subNode.id,
507
+ targetId: superNode.id,
508
+ label: '',
509
+ tooltip: formatHoverLikeTooltip('specialization', getQualifiedName(ctx, owner), getIriForNode(axiom)),
510
+ kind: 'specialization',
511
+ statement: axiom
512
+ });
513
+ graph.statementsById.set(edgeId, axiom);
514
+ };
515
+ const addEquivalenceEdges = (owner, axiom, index) => {
516
+ if (!isEquivalenceAxiom(axiom))
517
+ return;
518
+ const subNode = ensureEntityNode(owner);
519
+ if (!subNode)
520
+ return;
521
+ const superTerms = (axiom.superTerms ?? []).map((ref) => ref?.ref).filter(Boolean);
522
+ if (superTerms.length === 1) {
523
+ const superNode = ensureEntityNode(superTerms[0]);
524
+ if (!superNode)
525
+ return;
526
+ const edgeId = `eq:${subNode.id}:${superNode.id}:${index}:0`;
527
+ const modelId = getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? edgeId;
528
+ graph.edges.push({
529
+ id: edgeId,
530
+ viewId: edgeId,
531
+ modelId,
532
+ iri: getIriForNode(axiom),
533
+ sourceId: subNode.id,
534
+ targetId: superNode.id,
535
+ label: '',
536
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
537
+ kind: 'equivalence',
538
+ statement: axiom
539
+ });
540
+ graph.statementsById.set(edgeId, axiom);
541
+ return;
542
+ }
543
+ if (superTerms.length > 1) {
544
+ const eqNodeId = `eqnode:${subNode.id}:${index}`;
545
+ const eqNode = {
546
+ id: eqNodeId,
547
+ viewId: eqNodeId,
548
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? eqNodeId,
549
+ iri: getIriForNode(axiom),
550
+ label: '&',
551
+ qualifiedLabel: getQualifiedName(ctx, owner),
552
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
553
+ propertyEntries: [],
554
+ compartmentEntries: [],
555
+ kind: 'equivalence',
556
+ statement: axiom
557
+ };
558
+ graph.nodes.push(eqNode);
559
+ graph.statementsById.set(eqNodeId, axiom);
560
+ const inEdgeId = `eq:${subNode.id}:${eqNodeId}:${index}:in`;
561
+ graph.edges.push({
562
+ id: inEdgeId,
563
+ viewId: inEdgeId,
564
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? inEdgeId,
565
+ iri: getIriForNode(axiom),
566
+ sourceId: subNode.id,
567
+ targetId: eqNodeId,
568
+ label: '',
569
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
570
+ kind: 'equivalence-source',
571
+ statement: axiom
572
+ });
573
+ graph.statementsById.set(inEdgeId, axiom);
574
+ let superIndex = 0;
575
+ for (const superTerm of superTerms) {
576
+ const superNode = ensureEntityNode(superTerm);
577
+ if (!superNode)
578
+ continue;
579
+ const outEdgeId = `eq:${eqNodeId}:${superNode.id}:${index}:${superIndex++}`;
580
+ graph.edges.push({
581
+ id: outEdgeId,
582
+ viewId: outEdgeId,
583
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? outEdgeId,
584
+ iri: getIriForNode(axiom),
585
+ sourceId: eqNodeId,
586
+ targetId: superNode.id,
587
+ label: '',
588
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
589
+ kind: 'equivalence',
590
+ statement: axiom
591
+ });
592
+ graph.statementsById.set(outEdgeId, axiom);
593
+ }
594
+ }
595
+ };
596
+ for (const statement of statements) {
597
+ const ownedSpecializations = statement.ownedSpecializations ?? [];
598
+ for (let i = 0; i < ownedSpecializations.length; i += 1) {
599
+ addSpecializationEdge(statement, ownedSpecializations[i], i);
600
+ }
601
+ const ownedEquivalences = statement.ownedEquivalences ?? [];
602
+ for (let i = 0; i < ownedEquivalences.length; i += 1) {
603
+ addEquivalenceEdges(statement, ownedEquivalences[i], i);
604
+ }
605
+ }
606
+ }
607
+ function computeDescriptionGraph(shared, root, graph) {
608
+ const astNodeLocator = getAstNodeLocator(shared);
609
+ const ctx = buildDiagramContext(root);
610
+ const nodeByQName = new Map();
611
+ const ensureInstanceNode = (candidate) => {
612
+ const statement = unwrapReferenceValue(candidate);
613
+ if (!statement || (!isConceptInstance(statement) && !isRelationInstance(statement)))
614
+ return undefined;
615
+ const qName = getQualifiedName(ctx, statement);
616
+ if (!qName)
617
+ return undefined;
618
+ const existing = nodeByQName.get(qName);
619
+ if (existing)
620
+ return existing;
621
+ const viewId = `ci:${qName}`;
622
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? viewId;
623
+ const typeLabels = Array.from(new Set((statement.ownedTypes ?? [])
624
+ .map((typeAssertion) => getQualifiedName(ctx, typeAssertion?.type?.ref ?? typeAssertion?.type))
625
+ .filter((label) => typeof label === 'string' && label.length > 0))).map((label) => toSimpleName(label));
626
+ const label = toSimpleName(qName);
627
+ const propertyEntries = [];
628
+ let propertyIndex = 0;
629
+ for (const assertion of (statement.ownedPropertyValues ?? [])) {
630
+ if (getContainingOntology(assertion) !== root)
631
+ continue;
632
+ if (!isPropertyValueAssertion(assertion))
633
+ continue;
634
+ const propertyRef = assertion.property?.ref;
635
+ if (!propertyRef || !isScalarProperty(propertyRef))
636
+ continue;
637
+ const propertyName = getQualifiedName(ctx, propertyRef) ?? assertion.property?.$refText ?? 'property';
638
+ const valueLabel = getAssertionValueLabel(ctx, assertion);
639
+ const text = valueLabel.length > 0
640
+ ? `${toSimpleName(propertyName)} = ${valueLabel}`
641
+ : toSimpleName(propertyName);
642
+ const entryId = `${viewId}:properties:${propertyIndex++}`;
643
+ propertyEntries.push({
644
+ id: entryId,
645
+ modelId: getModelIdForNode(assertion, astNodeLocator, graph.rootUri) ?? entryId,
646
+ text,
647
+ qualifiedText: propertyName,
648
+ statement: assertion
649
+ });
650
+ }
651
+ const node = {
652
+ id: viewId,
653
+ viewId,
654
+ modelId,
655
+ iri: getIriForNode(statement),
656
+ label,
657
+ instanceTypeLabels: typeLabels,
658
+ qualifiedLabel: qName,
659
+ tooltip: formatHoverLikeTooltip(humanizeTypeName(statement.$type ?? 'instance'), qName, getIriForNode(statement)),
660
+ propertyEntries,
661
+ kind: 'instance',
662
+ statement
663
+ };
664
+ graph.nodes.push(node);
665
+ graph.statementsById.set(viewId, statement);
666
+ for (const entry of propertyEntries) {
667
+ if (entry.statement) {
668
+ graph.statementsById.set(entry.id, entry.statement);
669
+ }
670
+ }
671
+ nodeByQName.set(qName, node);
672
+ return node;
673
+ };
674
+ const statements = root.ownedStatements ?? [];
675
+ for (const statement of statements) {
676
+ ensureInstanceNode(statement);
677
+ }
678
+ let assertionEdgeIndex = 0;
679
+ const addRelationPropertyAssertionEdges = (ownerStatement, ownerNode) => {
680
+ if (!ownerNode)
681
+ return;
682
+ const ownedPropertyValues = ownerStatement.ownedPropertyValues ?? [];
683
+ for (const assertion of ownedPropertyValues) {
684
+ if (!isPropertyValueAssertion(assertion))
685
+ continue;
686
+ const propertyRef = assertion.property?.ref;
687
+ if (!propertyRef || !isRelation(propertyRef))
688
+ continue;
689
+ const relationName = getQualifiedName(ctx, propertyRef) ?? assertion.property?.$refText ?? 'relation';
690
+ const referencedValues = assertion.referencedValues ?? [];
691
+ for (const valueRef of referencedValues) {
692
+ const targetNode = ensureInstanceNode(valueRef);
693
+ if (!targetNode)
694
+ continue;
695
+ const edgeId = `pva:${ownerNode.id}:${targetNode.id}:${assertionEdgeIndex++}`;
696
+ const modelId = getModelIdForNode(assertion, astNodeLocator, graph.rootUri) ?? edgeId;
697
+ graph.edges.push({
698
+ id: edgeId,
699
+ viewId: edgeId,
700
+ modelId,
701
+ iri: getIriForNode(propertyRef),
702
+ sourceId: ownerNode.id,
703
+ targetId: targetNode.id,
704
+ label: toSimpleName(relationName),
705
+ qualifiedLabel: relationName,
706
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(propertyRef)),
707
+ kind: 'relation',
708
+ statement: assertion
709
+ });
710
+ graph.statementsById.set(edgeId, assertion);
711
+ }
712
+ }
713
+ };
714
+ for (const statement of statements) {
715
+ const ownerNode = ensureInstanceNode(statement);
716
+ addRelationPropertyAssertionEdges(statement, ownerNode);
717
+ if (!isRelationInstance(statement))
718
+ continue;
719
+ if (!ownerNode)
720
+ continue;
721
+ const relName = getQualifiedName(ctx, statement) ?? statement.name ?? 'relation';
722
+ const sources = (statement.sources ?? []).map((r) => ensureInstanceNode(r)).filter(Boolean);
723
+ const targets = (statement.targets ?? []).map((r) => ensureInstanceNode(r)).filter(Boolean);
724
+ let index = 0;
725
+ for (const src of sources) {
726
+ const edgeId = `ri:${relName}:src:${index++}`;
727
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
728
+ graph.edges.push({
729
+ id: edgeId,
730
+ viewId: edgeId,
731
+ modelId,
732
+ iri: getIriForNode(statement),
733
+ sourceId: src.id,
734
+ targetId: ownerNode.id,
735
+ label: '',
736
+ tooltip: formatHoverLikeTooltip('relation source', relName, getIriForNode(statement)),
737
+ kind: 'relation-source',
738
+ statement
739
+ });
740
+ graph.statementsById.set(edgeId, statement);
741
+ }
742
+ index = 0;
743
+ for (const dst of targets) {
744
+ const edgeId = `ri:${relName}:dst:${index++}`;
745
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
746
+ graph.edges.push({
747
+ id: edgeId,
748
+ viewId: edgeId,
749
+ modelId,
750
+ iri: getIriForNode(statement),
751
+ sourceId: ownerNode.id,
752
+ targetId: dst.id,
753
+ label: '',
754
+ tooltip: formatHoverLikeTooltip('relation target', relName, getIriForNode(statement)),
755
+ kind: 'relation-target',
756
+ statement
757
+ });
758
+ graph.statementsById.set(edgeId, statement);
759
+ }
760
+ }
761
+ }
762
+ async function computeBundleGraph(shared, root, graph) {
763
+ const astNodeLocator = getAstNodeLocator(shared);
764
+ const queue = [root];
765
+ const seen = new Set();
766
+ const nodeByNamespace = new Map();
767
+ const directImports = new Map();
768
+ const seedImports = (root.ownedImports ?? []);
769
+ for (const ownedImport of seedImports) {
770
+ const importedOntology = await resolveImportedOntology(shared, ownedImport);
771
+ if (!importedOntology)
772
+ continue;
773
+ queue.push(importedOntology);
774
+ }
775
+ const ensureOntologyNode = (ontology, namespace) => {
776
+ const existing = nodeByNamespace.get(namespace);
777
+ if (existing)
778
+ return existing;
779
+ const nodeId = `ont:${namespace}`;
780
+ const modelId = namespace || getModelIdForNode(ontology, astNodeLocator, graph.rootUri) || nodeId;
781
+ const label = ontology.prefix || namespace.split('/').pop() || namespace;
782
+ const node = {
783
+ id: nodeId,
784
+ viewId: nodeId,
785
+ modelId,
786
+ iri: getIriForNode(ontology),
787
+ label,
788
+ kind: 'ontology',
789
+ statement: ontology
790
+ };
791
+ node.tooltip = getIriForNode(ontology);
792
+ graph.nodes.push(node);
793
+ graph.statementsById.set(nodeId, ontology);
794
+ nodeByNamespace.set(namespace, node);
795
+ return node;
796
+ };
797
+ while (queue.length > 0) {
798
+ const current = queue.shift();
799
+ const namespace = normalizeNamespace(current.namespace ?? '');
800
+ if (!namespace || seen.has(namespace))
801
+ continue;
802
+ seen.add(namespace);
803
+ ensureOntologyNode(current, namespace);
804
+ for (const ownedImport of (current.ownedImports ?? [])) {
805
+ const importedOntology = await resolveImportedOntology(shared, ownedImport);
806
+ if (!importedOntology)
807
+ continue;
808
+ const importedNamespace = normalizeNamespace(importedOntology.namespace ?? '');
809
+ if (!importedNamespace)
810
+ continue;
811
+ if (!seen.has(importedNamespace)) {
812
+ queue.push(importedOntology);
813
+ }
814
+ ensureOntologyNode(importedOntology, importedNamespace);
815
+ const existing = directImports.get(namespace) ?? [];
816
+ existing.push({ targetNamespace: importedNamespace, ownedImport });
817
+ directImports.set(namespace, existing);
818
+ }
819
+ }
820
+ const reachabilityWithoutDirectEdge = (source, target) => {
821
+ const visited = new Set([source]);
822
+ const work = [];
823
+ const firstHops = directImports.get(source) ?? [];
824
+ for (const hop of firstHops) {
825
+ if (hop.targetNamespace === target)
826
+ continue;
827
+ work.push(hop.targetNamespace);
828
+ }
829
+ while (work.length > 0) {
830
+ const current = work.shift();
831
+ if (current === target)
832
+ return true;
833
+ if (visited.has(current))
834
+ continue;
835
+ visited.add(current);
836
+ for (const next of (directImports.get(current) ?? [])) {
837
+ if (!visited.has(next.targetNamespace)) {
838
+ work.push(next.targetNamespace);
839
+ }
840
+ }
841
+ }
842
+ return false;
843
+ };
844
+ for (const [sourceNamespace, imports] of directImports.entries()) {
845
+ const sourceNode = nodeByNamespace.get(sourceNamespace);
846
+ if (!sourceNode)
847
+ continue;
848
+ for (const imported of imports) {
849
+ const targetNode = nodeByNamespace.get(imported.targetNamespace);
850
+ if (!targetNode)
851
+ continue;
852
+ if (reachabilityWithoutDirectEdge(sourceNamespace, imported.targetNamespace)) {
853
+ continue;
854
+ }
855
+ const edgeId = `imp:${sourceNamespace}->${imported.targetNamespace}`;
856
+ const edgeModelId = getModelIdForNode(imported.ownedImport, astNodeLocator, graph.rootUri) ?? edgeId;
857
+ graph.edges.push({
858
+ id: edgeId,
859
+ viewId: edgeId,
860
+ modelId: edgeModelId,
861
+ iri: getIriForNode(imported.ownedImport),
862
+ sourceId: sourceNode.id,
863
+ targetId: targetNode.id,
864
+ label: '',
865
+ tooltip: getIriForNode(imported.ownedImport),
866
+ kind: 'import',
867
+ statement: imported.ownedImport
868
+ });
869
+ graph.statementsById.set(edgeId, imported.ownedImport);
870
+ }
871
+ }
872
+ }
873
+ async function resolveImportedOntology(shared, ownedImport) {
874
+ const refText = ownedImport?.imported?.$refText ?? '';
875
+ const namespace = normalizeNamespace(String(refText).replace(/^<|>$/g, ''));
876
+ if (namespace) {
877
+ const resolved = await findOntologyByNamespace(shared, namespace);
878
+ if (resolved) {
879
+ return resolved;
880
+ }
881
+ }
882
+ const direct = ownedImport?.imported?.ref;
883
+ if (direct && isOntology(direct)) {
884
+ return direct;
885
+ }
886
+ return undefined;
887
+ }
888
+ function getNodeSize(node) {
889
+ if (node.kind === 'equivalence') {
890
+ return { width: 28, height: 28 };
891
+ }
892
+ const properties = node.propertyEntries ?? [];
893
+ const entries = node.compartmentEntries ?? [];
894
+ const maxHeaderChars = 34;
895
+ const maxBodyChars = 38;
896
+ const headerName = Math.min(node.label.length, maxHeaderChars);
897
+ const headerType = Math.min(getNodeTypeLabel(node).length, maxHeaderChars);
898
+ const widestHeaderPx = Math.max(Math.round(headerName * 8.2), Math.round(headerType * 6.9));
899
+ const widestBodyPx = Math.max(...[...properties, ...entries].map((entry) => Math.round(Math.min(entry.text.length, maxBodyChars) * 6.0)), 0);
900
+ const contentWidth = Math.max(widestHeaderPx, widestBodyPx);
901
+ const width = Math.max(120, Math.min(340, contentWidth + 28));
902
+ const headerHeight = 34;
903
+ const compartmentPadding = 10;
904
+ const entryHeight = 16;
905
+ const propertyHeight = properties.length > 0
906
+ ? (compartmentPadding * 2) + (properties.length * entryHeight)
907
+ : 0;
908
+ const oneOfHeight = entries.length > 0
909
+ ? (compartmentPadding * 2) + (entries.length * entryHeight)
910
+ : 0;
911
+ const gap = properties.length > 0 && entries.length > 0 ? 4 : 0;
912
+ const bodyHeight = propertyHeight + gap + oneOfHeight + (propertyHeight + oneOfHeight > 0 ? 0 : 22);
913
+ return { width, height: headerHeight + bodyHeight };
914
+ }
915
+ function getNodeTypeLabel(node) {
916
+ if (node.kind === 'equivalence')
917
+ return '«equivalence»';
918
+ if (node.kind === 'instance') {
919
+ const types = (node.instanceTypeLabels ?? []).filter((t) => t.length > 0);
920
+ if (types.length > 0) {
921
+ return `«${types.join(', ')}»`;
922
+ }
923
+ return '«concept instance»';
924
+ }
925
+ const explicitType = node.statement?.$type;
926
+ if (typeof explicitType === 'string' && explicitType.length > 0) {
927
+ return `«${humanizeTypeName(explicitType)}»`;
928
+ }
929
+ if (node.kind === 'ontology')
930
+ return '«ontology»';
931
+ return '«concept»';
932
+ }
933
+ async function layoutAndConvertToSModel(graph) {
934
+ if (graph.nodes.length === 0) {
935
+ return { id: 'root', type: 'graph', children: [] };
936
+ }
937
+ const selfLoopCountByNode = new Map();
938
+ for (const edge of graph.edges) {
939
+ if (edge.sourceId === edge.targetId) {
940
+ const current = selfLoopCountByNode.get(edge.sourceId) ?? 0;
941
+ selfLoopCountByNode.set(edge.sourceId, current + 1);
942
+ }
943
+ }
944
+ const layoutNodeSizeById = new Map();
945
+ for (const node of graph.nodes) {
946
+ const baseSize = getNodeSize(node);
947
+ const selfLoopCount = selfLoopCountByNode.get(node.id) ?? 0;
948
+ if (selfLoopCount === 0) {
949
+ layoutNodeSizeById.set(node.id, baseSize);
950
+ continue;
951
+ }
952
+ const labelHeight = 16;
953
+ const loopSpacing = labelHeight + 8;
954
+ const baseLoopHeight = 40;
955
+ const outermostLoopIndex = selfLoopCount - 1;
956
+ const outermostLoopHeight = baseLoopHeight + (outermostLoopIndex * loopSpacing);
957
+ const minLoopHostHeight = outermostLoopHeight + 20;
958
+ layoutNodeSizeById.set(node.id, {
959
+ width: baseSize.width,
960
+ height: Math.max(baseSize.height, minLoopHostHeight)
961
+ });
962
+ }
963
+ const elk = await getElk();
964
+ const elkGraph = {
965
+ id: 'root',
966
+ layoutOptions: {
967
+ 'elk.algorithm': 'org.eclipse.elk.layered',
968
+ 'elk.direction': 'DOWN',
969
+ 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
970
+ 'elk.spacing.nodeNode': '50',
971
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '80',
972
+ 'elk.selfLoopDistribution': 'EQUALLY_SPACED',
973
+ 'elk.selfLoopOrdering': 'STACKED',
974
+ 'elk.spacing.edgeSelfLoop': '72',
975
+ 'elk.spacing.edgeEdge': '20',
976
+ 'elk.spacing.edgeNode': '20',
977
+ 'elk.edgeRouting': 'ORTHOGONAL',
978
+ 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
979
+ 'elk.layered.unnecessaryBendpoints': 'true',
980
+ 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED',
981
+ 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
982
+ 'elk.hierarchyHandling': 'INCLUDE_CHILDREN'
983
+ },
984
+ children: graph.nodes.map((n) => {
985
+ const size = layoutNodeSizeById.get(n.id) ?? getNodeSize(n);
986
+ return { id: n.id, width: size.width, height: size.height };
987
+ }),
988
+ edges: graph.edges.map((e) => ({
989
+ id: e.id,
990
+ sources: [e.sourceId],
991
+ targets: [e.targetId],
992
+ labels: e.label
993
+ ? [{
994
+ id: `${e.id}:elk-label`,
995
+ text: e.label,
996
+ width: e.label.length * 7 + 8,
997
+ height: 14,
998
+ layoutOptions: {
999
+ 'elk.edgeLabels.inline': 'false',
1000
+ 'elk.edgeLabels.placement': e.sourceId === e.targetId ? 'TAIL' : 'CENTER'
1001
+ }
1002
+ }]
1003
+ : []
1004
+ }))
1005
+ };
1006
+ const laidOut = await elk.layout(elkGraph);
1007
+ const nodeMap = new Map((laidOut.children ?? []).map((n) => [n.id, n]));
1008
+ const edgeMap = new Map((laidOut.edges ?? []).map((e) => [e.id, e]));
1009
+ const diagramNodeById = new Map(graph.nodes.map((node) => [node.id, node]));
1010
+ const extractRoutingPoints = (elkEdge) => {
1011
+ if (!elkEdge || !Array.isArray(elkEdge.sections) || elkEdge.sections.length === 0)
1012
+ return [];
1013
+ const section = elkEdge.sections[0];
1014
+ const points = [];
1015
+ const pushPoint = (p) => {
1016
+ if (!p || typeof p.x !== 'number' || typeof p.y !== 'number')
1017
+ return;
1018
+ const x = Number(p.x);
1019
+ const y = Number(p.y);
1020
+ if (!Number.isFinite(x) || !Number.isFinite(y))
1021
+ return;
1022
+ const last = points[points.length - 1];
1023
+ if (!last || Math.abs(last.x - x) > 0.001 || Math.abs(last.y - y) > 0.001) {
1024
+ points.push({ x, y });
1025
+ }
1026
+ };
1027
+ pushPoint(section.startPoint);
1028
+ for (const bend of Array.isArray(section.bendPoints) ? section.bendPoints : []) {
1029
+ pushPoint(bend);
1030
+ }
1031
+ pushPoint(section.endPoint);
1032
+ return simplifyRoutingPoints(points);
1033
+ };
1034
+ const simplifyRoutingPoints = (points) => {
1035
+ if (points.length <= 2)
1036
+ return points;
1037
+ const simplified = [points[0]];
1038
+ for (let i = 1; i < points.length - 1; i++) {
1039
+ const prev = points[i - 1];
1040
+ const curr = points[i];
1041
+ const next = points[i + 1];
1042
+ const isHorizontalLine = Math.abs(prev.y - curr.y) < 0.001 && Math.abs(curr.y - next.y) < 0.001;
1043
+ const isVerticalLine = Math.abs(prev.x - curr.x) < 0.001 && Math.abs(curr.x - next.x) < 0.001;
1044
+ if (!isHorizontalLine && !isVerticalLine) {
1045
+ simplified.push(curr);
1046
+ }
1047
+ }
1048
+ simplified.push(points[points.length - 1]);
1049
+ return simplified;
1050
+ };
1051
+ const rectangularSelfLoopRoutingPoints = (nodeLayout, loopIndex) => {
1052
+ const leftX = nodeLayout.x;
1053
+ const centerY = nodeLayout.y + (nodeLayout.height / 2);
1054
+ const labelHeight = 16;
1055
+ const loopSpacing = labelHeight + 8;
1056
+ const baseLoopWidth = 60;
1057
+ const baseLoopHeight = 40;
1058
+ const loopWidth = baseLoopWidth + (loopIndex * loopSpacing);
1059
+ const loopHeight = baseLoopHeight + (loopIndex * loopSpacing);
1060
+ const top = centerY - (loopHeight / 2);
1061
+ const bottom = centerY + (loopHeight / 2);
1062
+ const outerX = leftX - loopWidth;
1063
+ return [
1064
+ { x: leftX, y: top },
1065
+ { x: outerX, y: top },
1066
+ { x: outerX, y: bottom },
1067
+ { x: leftX, y: bottom }
1068
+ ];
1069
+ };
1070
+ const selfLoopOrdinalByNode = new Map();
1071
+ const children = [];
1072
+ const headerHeight = 34;
1073
+ const entryHeight = 16;
1074
+ const compartmentPadding = 10;
1075
+ for (const node of graph.nodes) {
1076
+ const laid = nodeMap.get(node.viewId);
1077
+ const size = getNodeSize(node);
1078
+ const width = laid?.width ?? size.width;
1079
+ const height = laid?.height ?? size.height;
1080
+ if (node.kind === 'equivalence') {
1081
+ children.push({
1082
+ id: node.viewId,
1083
+ viewId: node.viewId,
1084
+ modelId: node.modelId,
1085
+ tooltip: node.iri,
1086
+ type: 'node:rect',
1087
+ position: { x: laid?.x ?? 0, y: laid?.y ?? 0 },
1088
+ size: { width, height },
1089
+ kind: node.kind,
1090
+ children: [
1091
+ {
1092
+ id: `${node.viewId}:name`,
1093
+ viewId: `${node.viewId}:name`,
1094
+ modelId: node.modelId,
1095
+ tooltip: node.iri,
1096
+ type: 'label',
1097
+ text: '&',
1098
+ position: { x: width / 2, y: height / 2 }
1099
+ }
1100
+ ]
1101
+ });
1102
+ continue;
1103
+ }
1104
+ const propertyEntries = node.propertyEntries ?? [];
1105
+ const entries = node.compartmentEntries ?? [];
1106
+ const maxBodyChars = Math.max(8, Math.floor((width - 24) / 6.0));
1107
+ const propertyHeight = propertyEntries.length > 0
1108
+ ? (compartmentPadding * 2) + (propertyEntries.length * entryHeight)
1109
+ : 0;
1110
+ const oneOfHeight = entries.length > 0
1111
+ ? (compartmentPadding * 2) + (entries.length * entryHeight)
1112
+ : 0;
1113
+ const gap = propertyEntries.length > 0 && entries.length > 0 ? 4 : 0;
1114
+ const nodeChildren = [
1115
+ {
1116
+ id: `${node.viewId}:header`,
1117
+ viewId: `${node.viewId}:header`,
1118
+ modelId: node.modelId,
1119
+ tooltip: node.iri,
1120
+ type: 'node:rect',
1121
+ position: { x: 0, y: 0 },
1122
+ size: { width, height: headerHeight },
1123
+ kind: 'label-box',
1124
+ children: [
1125
+ {
1126
+ id: `${node.viewId}:type`,
1127
+ viewId: `${node.viewId}:type`,
1128
+ modelId: node.modelId,
1129
+ tooltip: node.iri,
1130
+ type: 'label',
1131
+ text: getNodeTypeLabel(node),
1132
+ position: { x: width / 2, y: 10 }
1133
+ },
1134
+ {
1135
+ id: `${node.viewId}:name`,
1136
+ viewId: `${node.viewId}:name`,
1137
+ modelId: node.modelId,
1138
+ tooltip: node.iri,
1139
+ type: 'label',
1140
+ text: node.label,
1141
+ position: { x: width / 2, y: 24 }
1142
+ }
1143
+ ]
1144
+ }
1145
+ ];
1146
+ let compartmentY = headerHeight;
1147
+ if (propertyEntries.length > 0) {
1148
+ nodeChildren.push({
1149
+ id: `${node.viewId}:properties`,
1150
+ viewId: `${node.viewId}:properties`,
1151
+ modelId: node.modelId,
1152
+ tooltip: node.iri,
1153
+ type: 'node:rect',
1154
+ position: { x: 0, y: compartmentY },
1155
+ size: { width, height: propertyHeight },
1156
+ kind: 'property-compartment',
1157
+ children: propertyEntries.map((entry, index) => ({
1158
+ id: entry.id,
1159
+ viewId: entry.id,
1160
+ modelId: entry.modelId,
1161
+ tooltip: getIriForNode(entry.statement) ?? node.iri,
1162
+ type: 'label',
1163
+ text: truncateWithEllipsis(entry.text, maxBodyChars),
1164
+ position: {
1165
+ x: 12,
1166
+ y: compartmentPadding + 8 + (index * entryHeight)
1167
+ }
1168
+ }))
1169
+ });
1170
+ compartmentY += propertyHeight + gap;
1171
+ }
1172
+ if (entries.length > 0) {
1173
+ nodeChildren.push({
1174
+ id: `${node.viewId}:compartment`,
1175
+ viewId: `${node.viewId}:compartment`,
1176
+ modelId: node.modelId,
1177
+ tooltip: node.iri,
1178
+ type: 'node:rect',
1179
+ position: { x: 0, y: compartmentY },
1180
+ size: { width, height: oneOfHeight },
1181
+ kind: 'compartment',
1182
+ children: entries.map((entry, index) => ({
1183
+ id: entry.id,
1184
+ viewId: entry.id,
1185
+ modelId: entry.modelId,
1186
+ tooltip: getIriForNode(entry.statement) ?? node.iri,
1187
+ type: 'label',
1188
+ text: truncateWithEllipsis(entry.text, maxBodyChars),
1189
+ position: {
1190
+ x: 12,
1191
+ y: compartmentPadding + 8 + (index * entryHeight)
1192
+ }
1193
+ }))
1194
+ });
1195
+ }
1196
+ children.push({
1197
+ id: node.viewId,
1198
+ viewId: node.viewId,
1199
+ modelId: node.modelId,
1200
+ tooltip: node.iri,
1201
+ type: 'node:rect',
1202
+ position: { x: laid?.x ?? 0, y: laid?.y ?? 0 },
1203
+ size: { width, height },
1204
+ kind: node.kind,
1205
+ children: nodeChildren
1206
+ });
1207
+ }
1208
+ for (const edge of graph.edges) {
1209
+ const laidEdge = edgeMap.get(edge.viewId);
1210
+ const isSelfLoop = edge.sourceId === edge.targetId;
1211
+ const selfLoopIndex = isSelfLoop
1212
+ ? (selfLoopOrdinalByNode.get(edge.sourceId) ?? 0)
1213
+ : -1;
1214
+ if (isSelfLoop) {
1215
+ selfLoopOrdinalByNode.set(edge.sourceId, selfLoopIndex + 1);
1216
+ }
1217
+ const routingPoints = (() => {
1218
+ if (!isSelfLoop)
1219
+ return extractRoutingPoints(laidEdge);
1220
+ const laidNode = nodeMap.get(edge.sourceId);
1221
+ const fallbackNode = diagramNodeById.get(edge.sourceId);
1222
+ const fallbackSize = fallbackNode ? getNodeSize(fallbackNode) : { width: 120, height: 56 };
1223
+ const nodeLayout = {
1224
+ x: laidNode?.x ?? 0,
1225
+ y: laidNode?.y ?? 0,
1226
+ width: laidNode?.width ?? fallbackSize.width,
1227
+ height: laidNode?.height ?? fallbackSize.height
1228
+ };
1229
+ return rectangularSelfLoopRoutingPoints(nodeLayout, selfLoopIndex);
1230
+ })();
1231
+ const edgeChild = edge.label
1232
+ ? [(() => {
1233
+ if (isSelfLoop) {
1234
+ return {
1235
+ id: `${edge.viewId}:label`,
1236
+ viewId: `${edge.viewId}:label`,
1237
+ modelId: edge.modelId,
1238
+ tooltip: edge.iri,
1239
+ type: 'label:edge',
1240
+ text: edge.label,
1241
+ edgePlacement: {
1242
+ rotate: false,
1243
+ side: 'top',
1244
+ position: 0.75,
1245
+ offset: 8,
1246
+ moveMode: 'edge'
1247
+ }
1248
+ };
1249
+ }
1250
+ return {
1251
+ id: `${edge.viewId}:label`,
1252
+ viewId: `${edge.viewId}:label`,
1253
+ modelId: edge.modelId,
1254
+ tooltip: edge.iri,
1255
+ type: 'label:edge',
1256
+ text: edge.label,
1257
+ edgePlacement: {
1258
+ rotate: false,
1259
+ side: 'top',
1260
+ position: 0.5,
1261
+ offset: 8,
1262
+ moveMode: 'edge'
1263
+ }
1264
+ };
1265
+ })()]
1266
+ : [];
1267
+ children.push({
1268
+ id: edge.viewId,
1269
+ viewId: edge.viewId,
1270
+ modelId: edge.modelId,
1271
+ tooltip: edge.iri,
1272
+ type: `edge:${edge.kind}`,
1273
+ sourceId: edge.sourceId,
1274
+ targetId: edge.targetId,
1275
+ routerKind: 'polyline',
1276
+ routingPoints,
1277
+ kind: edge.kind,
1278
+ children: edgeChild
1279
+ });
1280
+ }
1281
+ return {
1282
+ id: 'root',
1283
+ type: 'graph',
1284
+ children
1285
+ };
1286
+ }
1287
+ function normalizeDiagramElementId(elementId) {
1288
+ return elementId.endsWith(':label') ? elementId.slice(0, -':label'.length) : elementId;
1289
+ }
1290
+ function indexDiagramMappings(shared, graph) {
1291
+ const astNodeLocator = getAstNodeLocator(shared);
1292
+ if (!astNodeLocator) {
1293
+ navigationIndex.set(graph.rootUri, new Map());
1294
+ return;
1295
+ }
1296
+ const map = new Map();
1297
+ for (const node of graph.nodes) {
1298
+ const statement = node.statement;
1299
+ if (!statement)
1300
+ continue;
1301
+ const root = statement.$document?.parseResult?.value;
1302
+ if (!root)
1303
+ continue;
1304
+ const path = astNodeLocator.getAstNodePath(statement);
1305
+ if (!path)
1306
+ continue;
1307
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1308
+ const astRef = { uri, path };
1309
+ map.set(normalizeDiagramElementId(node.viewId), astRef);
1310
+ map.set(normalizeDiagramElementId(node.modelId), astRef);
1311
+ }
1312
+ for (const edge of graph.edges) {
1313
+ const statement = edge.statement;
1314
+ if (!statement)
1315
+ continue;
1316
+ const root = statement.$document?.parseResult?.value;
1317
+ if (!root)
1318
+ continue;
1319
+ const path = astNodeLocator.getAstNodePath(statement);
1320
+ if (!path)
1321
+ continue;
1322
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1323
+ const astRef = { uri, path };
1324
+ map.set(normalizeDiagramElementId(edge.viewId), astRef);
1325
+ map.set(normalizeDiagramElementId(edge.modelId), astRef);
1326
+ }
1327
+ for (const [id, statement] of graph.statementsById.entries()) {
1328
+ const root = statement.$document?.parseResult?.value;
1329
+ if (!root)
1330
+ continue;
1331
+ const path = astNodeLocator.getAstNodePath(statement);
1332
+ if (!path)
1333
+ continue;
1334
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1335
+ map.set(normalizeDiagramElementId(id), { uri, path });
1336
+ }
1337
+ navigationIndex.set(graph.rootUri, map);
1338
+ }
1339
+ export async function navigateToElement(shared, contextUri, elementId) {
1340
+ const astNodeLocator = getAstNodeLocator(shared);
1341
+ if (!astNodeLocator) {
1342
+ return null;
1343
+ }
1344
+ const normalizedId = normalizeDiagramElementId(elementId);
1345
+ const directSeparator = normalizedId.indexOf('#');
1346
+ const directUri = directSeparator > 0 ? normalizedId.slice(0, directSeparator) : undefined;
1347
+ const directPath = directSeparator > 0 ? normalizedId.slice(directSeparator + 1) : undefined;
1348
+ const indexed = !directUri || !directPath
1349
+ ? navigationIndex.get(contextUri)?.get(normalizedId)
1350
+ : undefined;
1351
+ const resolvedUri = directUri ?? indexed?.uri;
1352
+ const resolvedPath = directPath ?? indexed?.path;
1353
+ if (!resolvedUri || !resolvedPath) {
1354
+ const ontologyIri = normalizedId.startsWith('ont:')
1355
+ ? normalizeNamespace(normalizedId.slice(4))
1356
+ : normalizeNamespace(normalizedId);
1357
+ if (ontologyIri && ontologyIri.includes('://')) {
1358
+ const ontology = await findOntologyByNamespace(shared, ontologyIri);
1359
+ if (ontology?.$document) {
1360
+ return toRange(ontology.$document, ontology);
1361
+ }
1362
+ }
1363
+ }
1364
+ if (!resolvedUri || !resolvedPath) {
1365
+ return null;
1366
+ }
1367
+ const langiumDocuments = shared.workspace.LangiumDocuments;
1368
+ const resolvedDocUri = URI.parse(resolvedUri);
1369
+ const doc = langiumDocuments.getDocument(resolvedDocUri)
1370
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1371
+ ? await langiumDocuments.getOrCreateDocument(resolvedDocUri)
1372
+ : undefined);
1373
+ if (!doc) {
1374
+ return null;
1375
+ }
1376
+ const root = doc.parseResult?.value;
1377
+ if (!root) {
1378
+ return null;
1379
+ }
1380
+ const node = astNodeLocator.getAstNode(root, resolvedPath);
1381
+ if (!node) {
1382
+ return null;
1383
+ }
1384
+ return toRange(node.$document ?? doc, node);
1385
+ }
1386
+ export async function resolveDefinition(shared, elementId, referencingUri) {
1387
+ const normalizedElementId = String(elementId ?? '').trim();
1388
+ if (!normalizedElementId) {
1389
+ return null;
1390
+ }
1391
+ const directSeparator = normalizedElementId.indexOf('#');
1392
+ const directUri = directSeparator > 0 ? normalizedElementId.slice(0, directSeparator) : undefined;
1393
+ const directPath = directSeparator > 0 ? normalizedElementId.slice(directSeparator + 1) : undefined;
1394
+ if (directUri && directPath && directPath.startsWith('/')) {
1395
+ try {
1396
+ const langiumDocuments = shared.workspace.LangiumDocuments;
1397
+ const resolvedDocUri = URI.parse(directUri);
1398
+ const doc = langiumDocuments.getDocument(resolvedDocUri)
1399
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1400
+ ? await langiumDocuments.getOrCreateDocument(resolvedDocUri)
1401
+ : undefined);
1402
+ const root = doc?.parseResult?.value;
1403
+ const astNodeLocator = getAstNodeLocator(shared);
1404
+ if (doc && root && astNodeLocator) {
1405
+ const node = astNodeLocator.getAstNode(root, directPath);
1406
+ if (node) {
1407
+ return toRange(node.$document ?? doc, node);
1408
+ }
1409
+ }
1410
+ }
1411
+ catch {
1412
+ // Fall through to IRI-based resolution when this is not a resolvable document URI.
1413
+ }
1414
+ }
1415
+ const normalizedIri = normalizedElementId.replace(/^<|>$/g, '').trim();
1416
+ if (!normalizedIri) {
1417
+ return null;
1418
+ }
1419
+ if (referencingUri) {
1420
+ const preferredRange = await resolveDefinitionByIriInModel(shared, normalizedIri, referencingUri);
1421
+ if (preferredRange) {
1422
+ return preferredRange;
1423
+ }
1424
+ }
1425
+ const parts = splitIri(normalizedIri);
1426
+ if (!parts || !parts.base || !parts.fragment) {
1427
+ return null;
1428
+ }
1429
+ const ontology = await findOntologyByNamespace(shared, parts.base);
1430
+ if (!ontology?.$document) {
1431
+ return null;
1432
+ }
1433
+ const member = findOntologyMemberByName(ontology, parts.fragment);
1434
+ if (!member) {
1435
+ return null;
1436
+ }
1437
+ return toRange(member.$document ?? ontology.$document, member);
1438
+ }
1439
+ async function resolveDefinitionByIriInModel(shared, iri, modelUri) {
1440
+ const langiumDocuments = shared.workspace.LangiumDocuments;
1441
+ const builder = shared.workspace.DocumentBuilder;
1442
+ const uri = URI.parse(modelUri);
1443
+ const document = langiumDocuments.getDocument(uri)
1444
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1445
+ ? await langiumDocuments.getOrCreateDocument(uri)
1446
+ : undefined);
1447
+ if (!document) {
1448
+ return null;
1449
+ }
1450
+ if (builder?.build) {
1451
+ await builder.build([document], { validation: false });
1452
+ }
1453
+ const root = document.parseResult?.value;
1454
+ if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
1455
+ return null;
1456
+ }
1457
+ const targetIri = normalizeIriText(iri);
1458
+ const member = collectOntologyMembers(root).find((candidate) => {
1459
+ const candidateIri = normalizeIriText(getIriForNode(candidate));
1460
+ if (candidateIri === targetIri) {
1461
+ return true;
1462
+ }
1463
+ const resolvedRefIri = normalizeIriText(getIriForNode(candidate?.ref?.ref));
1464
+ if (resolvedRefIri === targetIri) {
1465
+ return true;
1466
+ }
1467
+ const rawRefText = normalizeIriText(candidate?.ref?.$refText);
1468
+ return rawRefText === targetIri;
1469
+ });
1470
+ if (!member) {
1471
+ return null;
1472
+ }
1473
+ return toRange(member.$document ?? document, member);
1474
+ }
1475
+ function normalizeIriText(value) {
1476
+ return typeof value === 'string' ? value.replace(/^<|>$/g, '').trim() : '';
1477
+ }
1478
+ async function findOntologyByNamespace(shared, namespace) {
1479
+ const index = getOntologyModelIndex(shared);
1480
+ const modelUri = index?.resolveModelUri(normalizeNamespace(namespace));
1481
+ if (!modelUri) {
1482
+ return undefined;
1483
+ }
1484
+ const langiumDocuments = shared.workspace.LangiumDocuments;
1485
+ const uri = URI.parse(modelUri);
1486
+ const document = langiumDocuments.getDocument(uri)
1487
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1488
+ ? await langiumDocuments.getOrCreateDocument(uri)
1489
+ : undefined);
1490
+ const root = document?.parseResult?.value;
1491
+ return root && isOntology(root) ? root : undefined;
1492
+ }
1493
+ export async function resolveOntologyMemberLabels(shared, oml, modelUri) {
1494
+ const langiumDocuments = shared.workspace.LangiumDocuments;
1495
+ const builder = shared.workspace.DocumentBuilder;
1496
+ const uri = URI.parse(modelUri);
1497
+ const document = langiumDocuments.getDocument(uri)
1498
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1499
+ ? await langiumDocuments.getOrCreateDocument(uri)
1500
+ : undefined);
1501
+ if (!document) {
1502
+ return [];
1503
+ }
1504
+ if (builder?.build) {
1505
+ await builder.build([document], { validation: false });
1506
+ }
1507
+ const root = document.parseResult?.value;
1508
+ if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
1509
+ return [];
1510
+ }
1511
+ const reasoningService = oml?.reasoning?.ReasoningService;
1512
+ const labelSnapshot = reasoningService && typeof reasoningService.getContextMemberLabelSnapshot === 'function'
1513
+ ? await reasoningService.getContextMemberLabelSnapshot(modelUri)
1514
+ : {};
1515
+ const entries = [];
1516
+ for (const member of collectOntologyMembers(root)) {
1517
+ const iri = getIriForNode(member)?.trim();
1518
+ const name = getNamedElementName(member)?.trim();
1519
+ if (!iri || !name) {
1520
+ continue;
1521
+ }
1522
+ const label = labelSnapshot[iri]?.trim();
1523
+ entries.push({
1524
+ iri,
1525
+ name,
1526
+ label: label && label.length > 0 ? label : undefined,
1527
+ });
1528
+ }
1529
+ entries.sort((left, right) => left.name.localeCompare(right.name)
1530
+ || left.iri.localeCompare(right.iri));
1531
+ return entries;
1532
+ }
1533
+ function toRange(document, node) {
1534
+ if (!document?.textDocument)
1535
+ return null;
1536
+ const cstNode = node.$cstNode;
1537
+ if (!cstNode)
1538
+ return null;
1539
+ const start = document.textDocument.positionAt(cstNode.offset);
1540
+ const end = document.textDocument.positionAt(cstNode.offset + cstNode.length);
1541
+ return {
1542
+ uri: document.uri.toString(),
1543
+ startLine: start.line + 1,
1544
+ startColumn: start.character,
1545
+ endLine: end.line + 1,
1546
+ endColumn: end.character
1547
+ };
1548
+ }
1549
+ //# sourceMappingURL=oml-diagram.js.map