diff --git a/.vscode/settings.json b/.vscode/settings.json index 465eaa96..c571fc5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,6 +30,7 @@ "Qualifiable", "rechtlich", "selbstaendige", + "Sematics", "submodel", "tsoa", "tsyringe", diff --git a/angular.json b/angular.json index ed1dac11..5d035353 100644 --- a/angular.json +++ b/angular.json @@ -36,10 +36,7 @@ "node_modules/bootstrap-icons/font/bootstrap-icons.min.css", "projects/aas-portal/src/styles.scss" ], - "scripts": [], - "allowedCommonJsDependencies": [ - "lodash-es" - ] + "scripts": [] }, "configurations": { "production": { diff --git a/package.json b/package.json index 9682308c..c687d148 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aas-portal-project", - "version": "3.0.0-development.69", + "version": "3.0.0-development.70", "description": "Web-based visualization and control of asset administration shells.", "type": "module", "scripts": { diff --git a/projects/aas-core/src/lib/index.ts b/projects/aas-core/src/lib/index.ts index f93c38b2..8f50448f 100644 --- a/projects/aas-core/src/lib/index.ts +++ b/projects/aas-core/src/lib/index.ts @@ -13,10 +13,12 @@ import { Blob, Entity, Environment, + HasSemantics, Identifiable, MultiLanguageProperty, Property, Referable, + Reference, ReferenceElement, RelationshipElement, Submodel, @@ -121,6 +123,35 @@ export function equalArray(a: T[], b: T[]): boolean { return a.every((_, i) => a[i] === b[i]); } +/** + * Determines whether the specified value is of type `HasSemantics`. + * @param value The current value. + * @returns `true` if the specified value is of type `HasSemantics`; otherwise, `false`. + */ +export function isReference(value: unknown): value is Reference { + if (!value || typeof value !== 'object') { + return false; + } + + return typeof (value as Reference).type === 'string' && Array.isArray((value as Reference).keys); +} + +/** + * Determines whether the specified value is of type `HasSemantics`. + * @param value The current value. + * @returns `true` if the specified value is of type `HasSemantics`; otherwise, `false`. + */ +export function isHasSemantics(value: unknown): value is HasSemantics { + if (!value || typeof value !== 'object') { + return false; + } + + return ( + isReference((value as HasSemantics).semanticId) || + Array.isArray((value as HasSemantics).supplementalSemanticIds) + ); +} + /** * Determines whether the specified value represents a submodel element. * @param value The current value. diff --git a/projects/aas-core/src/test/index.spec.ts b/projects/aas-core/src/test/index.spec.ts index d94d73f6..1761b911 100644 --- a/projects/aas-core/src/test/index.spec.ts +++ b/projects/aas-core/src/test/index.spec.ts @@ -15,8 +15,10 @@ import { getEndpointType, isAssetAdministrationShell, isBlob, + isHasSemantics, isMultiLanguageProperty, isProperty, + isReference, isReferenceElement, isSubmodel, isSubmodelElement, @@ -27,6 +29,7 @@ import { isValidPassword, stringFormat, } from '../lib/index.js'; +import { HasSemantics, Reference } from '../lib/aas.js'; describe('index', () => { describe('isSubmodelElement', () => { @@ -355,4 +358,52 @@ describe('index', () => { expect(getEndpointType('file:///endpoints/samples')).toEqual('FileSystem'); }); }); + + describe('isReference', () => { + it('indicates that value is of type reference', () => { + const value: Reference = { + keys: [], + type: 'ExternalReference', + }; + + expect(isReference(value)).toBeTruthy(); + }); + + it('indicates that "{}" is not of type Reference', () => { + expect(isReference({})).toBeFalsy(); + }); + + it('indicates that "undefined" is not of type Reference', () => { + expect(isReference(undefined)).toBeFalsy(); + }); + + it('indicates that "null" is not of type Reference', () => { + expect(isReference(null)).toBeFalsy(); + }); + }); + + describe('isHasSemantics', () => { + it('indicates that value is of type HasSemantics', () => { + const value: HasSemantics = { + semanticId: { + keys: [], + type: 'ExternalReference', + }, + }; + + expect(isHasSemantics(value)).toBeTruthy(); + }); + + it('indicates that "{}" is not of type HasSemantics', () => { + expect(isHasSemantics({})).toBeFalsy(); + }); + + it('indicates that "undefined" is not of type HasSemantics', () => { + expect(isHasSemantics(undefined)).toBeFalsy(); + }); + + it('indicates that "null" is not of type HasSemantics', () => { + expect(isHasSemantics(null)).toBeFalsy(); + }); + }); }); diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts index 712d68a4..f9ca036e 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree-row.ts @@ -13,11 +13,13 @@ import { convertToString, getAbbreviation, getLocaleValue, + isAssetAdministrationShell, isBooleanType, + isHasSemantics, isIdentifiable, - isProperty, - isReferenceElement, + isMultiLanguageProperty, isSubmodel, + isSubmodelElement, mimeTypeToExtension, selectReferable, toBoolean, @@ -135,18 +137,11 @@ class TreeInitialize { case 'Submodel': case 'SubmodelElementCollection': case 'SubmodelElementList': + case 'AnnotatedRelationshipElement': + case 'Entity': + case 'Operation': isLeaf = false; break; - case 'AnnotatedRelationshipElement': { - const relationship = element as aas.AnnotatedRelationshipElement; - isLeaf = !relationship.annotations || relationship.annotations.length === 0; - break; - } - case 'Entity': { - const entity = element as aas.Entity; - isLeaf = !entity.statements || entity.statements.length === 0; - break; - } } return new AASTreeRow( @@ -212,6 +207,23 @@ class TreeInitialize { return (referable as aas.SubmodelElementCollection).value ?? []; case 'SubmodelElementList': return (referable as aas.SubmodelElementList).value ?? []; + case 'Operation': { + const operation = referable as aas.Operation; + const children: aas.Referable[] = []; + if (operation.inputVariables) { + children.push(...operation.inputVariables.filter(variable => isSubmodelElement(variable))); + } + + if (operation.inoutputVariables) { + children.push(...operation.inoutputVariables.filter(variable => isSubmodelElement(variable))); + } + + if (operation.outputVariables) { + children.push(...operation.outputVariables.filter(variable => isSubmodelElement(variable))); + } + + return children; + } default: return []; } @@ -246,46 +258,41 @@ class TreeInitialize { } private getValue(referable: aas.Referable | null, localeId: string): boolean | string | undefined { - if (referable) { - switch (referable.modelType) { - case 'Blob': { - const blob = referable as aas.Blob; - const extension = mimeTypeToExtension(blob.contentType); - return blob.contentType ? `${blob.idShort}${extension}` : this.getSemanticId(blob); - } - case 'Entity': - return (referable as aas.Entity).globalAssetId ?? '-'; - case 'File': { - const file = referable as aas.File; - return file.value ? basename(normalize(file.value)) : '-'; - } - case 'MultiLanguageProperty': - return getLocaleValue((referable as aas.MultiLanguageProperty).value, localeId) ?? '-'; - case 'Operation': - return `${referable.idShort}()`; - case 'Property': - return this.getPropertyValue(referable as aas.Property, localeId); - case 'Range': { - const range = referable as aas.Range; - return `${convertToString(range.min, localeId)} ... ${convertToString(range.max, localeId)}`; - } - case 'ReferenceElement': - return (referable as aas.ReferenceElement).value?.keys.map(item => item.value).join('/'); - default: - return '-'; - } + if (!referable) { + return ''; } - return ''; - } - - private getSemanticId(hasSemantics: aas.HasSemantics): string { - const keys = hasSemantics.semanticId?.keys; - if (keys && keys.length > 0) { - return keys[0].value; + switch (referable.modelType) { + case 'Blob': { + const blob = referable as aas.Blob; + const extension = mimeTypeToExtension(blob.contentType); + return `${blob.idShort}${extension}`; + } + case 'Entity': + return (referable as aas.Entity).globalAssetId ?? '-'; + case 'File': { + const file = referable as aas.File; + return file.value ? basename(normalize(file.value)) : '-'; + } + case 'MultiLanguageProperty': + return getLocaleValue((referable as aas.MultiLanguageProperty).value, localeId) ?? '-'; + case 'Operation': + return `${referable.idShort}()`; + case 'Property': + return this.getPropertyValue(referable as aas.Property, localeId); + case 'Range': { + const range = referable as aas.Range; + return `${convertToString(range.min, localeId)} ... ${convertToString(range.max, localeId)}`; + } + case 'ReferenceElement': + return this.referenceToString((referable as aas.ReferenceElement).value); + case 'SubmodelElementCollection': + return (referable as aas.SubmodelElementCollection).value?.length.toString() ?? '0'; + case 'SubmodelElementList': + return (referable as aas.SubmodelElementList).value?.length.toString() ?? '0'; + default: + return ''; } - - return '-'; } private hasChildren(referable: aas.Referable): boolean { @@ -314,6 +321,22 @@ class TreeInitialize { const relationship = referable as aas.AnnotatedRelationshipElement; return relationship.annotations != null && relationship.annotations.length > 0; } + case 'Operation': { + const operation = referable as aas.Operation; + if (operation.inputVariables) { + return operation.inputVariables.some(variable => isSubmodelElement(variable.value)); + } + + if (operation.inoutputVariables) { + return operation.inoutputVariables.some(variable => isSubmodelElement(variable.value)); + } + + if (operation.outputVariables) { + return operation.outputVariables.some(variable => isSubmodelElement(variable.value)); + } + + return false; + } default: return false; } @@ -328,102 +351,75 @@ class TreeInitialize { } private getTypeInfo(referable: aas.Referable | null): string { - let value: string; - if (referable) { - switch (referable.modelType) { - case 'AnnotatedRelationshipElement': - value = (referable as aas.AnnotatedRelationshipElement).annotations?.length.toString() ?? '-'; - break; - case 'AssetAdministrationShell': - value = (referable as aas.Submodel).id; - break; - case 'Blob': - value = (referable as aas.Blob).contentType; - break; - case 'File': - value = (referable as aas.File).contentType; - break; - case 'Property': - value = (referable as aas.Property).valueType; - break; - case 'Range': - value = (referable as aas.Range).valueType; - break; - case 'ReferenceElement': - { - const keys = (referable as aas.ReferenceElement).value?.keys; - value = keys && keys.length > 0 ? keys[0].type : '-'; - } - break; - case 'Submodel': - value = `Semantic ID: ${this.referenceToString((referable as aas.Submodel).semanticId)}`; - break; - case 'SubmodelElementCollection': - value = (referable as aas.SubmodelElementCollection).value?.length.toString() ?? '0'; - break; - case 'SubmodelElementList': - value = (referable as aas.SubmodelElementList).value?.length.toString() ?? '0'; - break; - case 'MultiLanguageProperty': - { - const mlp = referable as aas.MultiLanguageProperty; - value = ''; - if (mlp && Array.isArray(mlp.value)) { - value += `${mlp.value.map(item => item.language).join(', ')}`; - } - } - break; - case 'Entity': - { - const entity = referable as aas.Entity; - value = entity.entityType; - } - break; - case 'Operation': - { - const operation = referable as aas.Operation; - value = ''; - if (operation.inputVariables && operation.inputVariables.length > 0) { - value += - '(' + - operation.inputVariables.map(v => this.variableToString(v.value)).join(', ') + - ')'; - } + if (!referable) { + return '-'; + } - if (operation.outputVariables && operation.outputVariables.length === 1) { - value += `: ${this.variableToString(operation.outputVariables[0].value)}`; - } else if (operation.outputVariables && operation.outputVariables.length > 1) { - value += - ': {' + - operation.outputVariables.map(v => this.variableToString(v.value)).join(', ') + - '}'; - } - } - break; - default: - value = '-'; - break; - } - } else { - value = '-'; + if (isAssetAdministrationShell(referable)) { + return referable.id; } - return value; - } + if (isMultiLanguageProperty(referable)) { + let value = ''; + if (referable && Array.isArray(referable.value)) { + value += `${referable.value.map(item => item.language).join(', ')}`; + } - private variableToString(value: aas.SubmodelElement): string { - if (isProperty(value)) { - return `${value.idShort}: ${value.valueType}`; + return value; } - if (isReferenceElement(value)) { - return `${value.idShort}: ${value?.value?.keys.map(key => key.value).join('/')}`; + let value = ''; + if (isHasSemantics(referable)) { + const a = this.getSemanticId(referable); + value = `Semantic ID: ${a}`; + } + + switch (referable.modelType) { + case 'AnnotatedRelationshipElement': + value = (referable as aas.AnnotatedRelationshipElement).annotations?.length.toString() ?? '-'; + break; + case 'Blob': { + const contentType = (referable as aas.Blob).contentType; + if (contentType) { + value += ', ' + contentType; + } + + break; + } + case 'File': { + const contentType = (referable as aas.File).contentType; + if (contentType) { + value += ', ' + contentType; + } + + break; + } + case 'Property': { + const valueType = (referable as aas.Property).valueType; + if (valueType) { + value += ', ' + (valueType.startsWith('xs:') ? valueType.substring(3) : valueType); + } + + break; + } + case 'Range': { + const valueType = (referable as aas.Range).valueType; + if (valueType) { + value += ', ' + (valueType.startsWith('xs:') ? valueType.substring(3) : valueType); + } + + break; + } } - return `${value.idShort}: ${value.modelType}`; + return value; + } + + private getSemanticId(hasSematics: aas.HasSemantics): string { + return this.referenceToString(hasSematics?.semanticId); } - private referenceToString(reference?: aas.Reference): string { + private referenceToString(reference: aas.Reference | undefined): string { return reference?.keys.map(key => key.value).join('/') ?? '-'; } } diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree-search.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree-search.ts index 88ea191f..840c4c13 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree-search.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree-search.ts @@ -21,7 +21,7 @@ import { import { normalize } from '../convert'; import { AASTreeRow } from './aas-tree-row'; -import { AASTreeStore, Operator, SearchQuery, SearchTerm } from './aas-tree.store'; +import { AASTreeService, Operator, SearchQuery, SearchTerm } from './aas-tree.service'; @Injectable() export class AASTreeSearch { @@ -29,7 +29,7 @@ export class AASTreeSearch { private terms: SearchTerm[] = []; public constructor( - private readonly store: AASTreeStore, + private readonly store: AASTreeService, private readonly translate: TranslateService, ) {} @@ -337,7 +337,7 @@ export class AASTreeSearch { return typeof min === 'number' && typeof max === 'number' && a >= min && a <= max; } else { const d = isDate - ? parseDate(b, this.translate.currentLang)?.getTime() ?? 0 + ? (parseDate(b, this.translate.currentLang)?.getTime() ?? 0) : parseNumber(b, this.translate.currentLang); if (typeof d !== 'number') { diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts index 068752eb..1960de57 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.ts @@ -50,7 +50,7 @@ import { } from '../submodel-template/submodel-template'; import { AASTreeApiService } from './aas-tree-api.service'; -import { AASTreeStore } from './aas-tree.store'; +import { AASTreeService } from './aas-tree.service'; interface PropertyValue { property: aas.Property; @@ -63,7 +63,7 @@ interface PropertyValue { styleUrls: ['./aas-tree.component.scss'], standalone: true, imports: [NgClass, NgStyle, TranslateModule], - providers: [AASTreeSearch, AASTreeStore], + providers: [AASTreeSearch, AASTreeService], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AASTreeComponent implements OnInit, OnDestroy { @@ -76,7 +76,7 @@ export class AASTreeComponent implements OnInit, OnDestroy { private webSocketSubject?: WebSocketSubject; public constructor( - private readonly store: AASTreeStore, + private readonly store: AASTreeService, private readonly router: Router, private readonly api: AASTreeApiService, private readonly searching: AASTreeSearch, diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.service.ts similarity index 99% rename from projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts rename to projects/aas-lib/src/lib/aas-tree/aas-tree.service.ts index 039f443c..fac36460 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree.store.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.service.ts @@ -33,7 +33,7 @@ interface AASTreeState { } @Injectable() -export class AASTreeStore { +export class AASTreeService { private readonly _state = signal({ matchIndex: -1, rows: [], nodes: [] }); public constructor( diff --git a/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts b/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts index 2115de67..8ac80a1e 100644 --- a/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts +++ b/projects/aas-lib/src/test/aas-tree/aas-tree-search.spec.ts @@ -10,12 +10,12 @@ import { TestBed } from '@angular/core/testing'; import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { AASTreeSearch } from '../../lib/aas-tree/aas-tree-search'; import { sampleDocument } from '../assets/sample-document'; -import { AASTreeStore } from '../../lib/aas-tree/aas-tree.store'; +import { AASTreeService } from '../../lib/aas-tree/aas-tree.store'; import { NotifyService } from 'projects/aas-lib/dist'; describe('AASTreeSearch', function () { let search: AASTreeSearch; - let store: AASTreeStore; + let store: AASTreeService; beforeEach(async function () { await TestBed.configureTestingModule({ @@ -25,7 +25,7 @@ describe('AASTreeSearch', function () { provide: NotifyService, useValue: jasmine.createSpyObj(['error']), }, - AASTreeStore, + AASTreeService, ], imports: [ TranslateModule.forRoot({ @@ -37,7 +37,7 @@ describe('AASTreeSearch', function () { ], }); - store = TestBed.inject(AASTreeStore); + store = TestBed.inject(AASTreeService); search = new AASTreeSearch(store, TestBed.inject(TranslateService)); store.updateRows(sampleDocument); });