diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.component.html b/projects/aas-lib/src/lib/aas-table/aas-table.component.html index 21ffa59d..b57640e9 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.component.html +++ b/projects/aas-lib/src/lib/aas-table/aas-table.component.html @@ -99,7 +99,7 @@
@if (row.isLeaf) { -
+
}@else { @if (row.expanded) { diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.component.ts b/projects/aas-lib/src/lib/aas-table/aas-table.component.ts index ef5afc34..78399d74 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.component.ts +++ b/projects/aas-lib/src/lib/aas-table/aas-table.component.ts @@ -29,7 +29,8 @@ export class AASTableComponent implements OnInit, OnChanges, OnDestroy { private readonly store: Store; private readonly subscription: Subscription = new Subscription(); private _selected: AASDocument[] = []; - private _filter: Observable | null = null; + private filter$: Observable | null = null; + private filterSubscription?: Subscription; private shiftKey = false; private altKey = false; @@ -72,17 +73,20 @@ export class AASTableComponent implements OnInit, OnChanges, OnDestroy { @Input() public get filter(): Observable | null { - return this._filter; + return this.filter$; } public set filter(value: Observable | null) { - if (value !== this._filter) { - this._filter = value; - if (this._filter) { - this.subscription.add( - this._filter.subscribe(value => this.store.dispatch(AASTableActions.setFilter({ filter: value }))), - ); - } + if (this.filterSubscription) { + this.filterSubscription.unsubscribe(); + this.filterSubscription = undefined; + } + + this.filter$ = value; + if (value) { + this.filterSubscription = value.subscribe(value => + this.store.dispatch(AASTableActions.setFilter({ filter: value })), + ); } } @@ -130,6 +134,7 @@ export class AASTableComponent implements OnInit, OnChanges, OnDestroy { public ngOnDestroy(): void { this.subscription.unsubscribe(); + this.filterSubscription?.unsubscribe(); this.window.removeEventListener('keyup', this.keyup); this.window.removeEventListener('keydown', this.keydown); } diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.effects.ts b/projects/aas-lib/src/lib/aas-table/aas-table.effects.ts index 11740689..5e014c2b 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.effects.ts +++ b/projects/aas-lib/src/lib/aas-table/aas-table.effects.ts @@ -83,7 +83,7 @@ export class AASTableEffects { } private addRoot(nodes: AASDocument[], rows: AASTableRow[]): AASTableRow[] { - const root = nodes.find(node => node.parent === null); + const root = nodes.find(node => !node.parentId); const index = findLastIndex(rows, row => row.level === 0); if (root) { const children = nodes.filter(node => this.isChild(root, node)); @@ -107,36 +107,43 @@ export class AASTableEffects { rows[index] = previous; } - this.traverse(root, nodes, rows, 1); + this.traverse(root, nodes, rows, 0, 1); } return rows; } - private traverse(parent: AASDocument, nodes: AASDocument[], rows: AASTableRow[], level: number): void { + private traverse( + parent: AASDocument, + nodes: AASDocument[], + rows: AASTableRow[], + parentIndex: number, + level: number, + ): void { let previous: AASTableRow | null = null; const children = nodes.filter(node => this.isChild(parent, node)); for (const child of children) { const row = new AASTableRow( child, - rows.length - 1, + parentIndex, false, false, false, - children.length === 0, + !nodes.some(node => this.isChild(child, node)), level, -1, -1, ); + const index = rows.length; rows.push(row); if (previous) { - previous.nextSibling = rows.length - 1; + previous.nextSibling = index; } if (children.length > 0) { row.firstChild = rows.length; - this.traverse(child, nodes, rows, level + 1); + this.traverse(child, nodes, rows, index, level + 1); } previous = row; @@ -144,11 +151,11 @@ export class AASTableEffects { } private isChild(parent: AASDocument, node: AASDocument): boolean { - if (!node.parent) { + if (!node.parentId) { return false; } - return node.parent.endpoint === parent.endpoint && node.parent.id === parent.id; + return node.parentId === parent.id; } private clone(row: AASTableRow): AASTableRow { diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.selectors.ts b/projects/aas-lib/src/lib/aas-table/aas-table.selectors.ts index bc279776..0cf0003c 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.selectors.ts +++ b/projects/aas-lib/src/lib/aas-table/aas-table.selectors.ts @@ -8,7 +8,7 @@ import { createSelector } from '@ngrx/store'; import { AASDocument } from 'common'; -import { AASTableRow, AASTableFeatureState } from './aas-table.state'; +import { AASTableRow, AASTableFeatureState, AASTableTree } from './aas-table.state'; import { TranslateService } from '@ngx-translate/core'; import { AASTableFilter } from './aas-table.filter'; @@ -31,11 +31,15 @@ export const selectEverySelected = createSelector(getRows, (rows: AASTableRow[]) export const selectRows = (translate: TranslateService) => { return createSelector(getState, state => { - if (state.filter) { - const filter = new AASTableFilter(state.filter, translate.currentLang); - return state.rows.filter(row => filter.match(row.document)); + if (state.viewMode === 'list') { + if (state.filter) { + const filter = new AASTableFilter(state.filter, translate.currentLang); + return state.rows.filter(row => filter.match(row.document)); + } + + return state.rows; + } else { + return new AASTableTree(state.rows).expanded; } - - return state.rows; }); }; diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.html b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.html index 0cb34b69..f301ac14 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree.component.html +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.component.html @@ -126,12 +126,3 @@ } - - \ No newline at end of file diff --git a/projects/aas-lib/src/lib/aas-tree/aas-tree.state.ts b/projects/aas-lib/src/lib/aas-tree/aas-tree.state.ts index 5a26f145..f3d3fd98 100644 --- a/projects/aas-lib/src/lib/aas-tree/aas-tree.state.ts +++ b/projects/aas-lib/src/lib/aas-tree/aas-tree.state.ts @@ -212,6 +212,8 @@ class TreeInitialize { private getChildren(referable: aas.Referable): aas.Referable[] { switch (referable.modelType) { + case 'AnnotatedRelationshipElement': + return (referable as aas.AnnotatedRelationshipElement).annotations ?? []; case 'AssetAdministrationShell': { const shell = referable as aas.AssetAdministrationShell; const children: aas.Referable[] = []; @@ -226,14 +228,14 @@ class TreeInitialize { return children; } + case 'Entity': + return (referable as aas.Entity).statements ?? []; case 'Submodel': return (referable as aas.Submodel).submodelElements ?? []; case 'SubmodelElementCollection': return (referable as aas.SubmodelElementCollection).value ?? []; case 'SubmodelElementList': return (referable as aas.SubmodelElementList).value ?? []; - case 'Entity': - return (referable as aas.Entity).statements ?? []; default: return []; } diff --git a/projects/aas-portal/src/app/about/about.component.html b/projects/aas-portal/src/app/about/about.component.html index ba740c96..82ee9bed 100644 --- a/projects/aas-portal/src/app/about/about.component.html +++ b/projects/aas-portal/src/app/about/about.component.html @@ -10,7 +10,7 @@

AASPortal

The AASPortal is a web portal for the visualization and management of Asset Administration Shells (AAS) licensed under the Apache License 2.0.

The implementation uses the concepts of the document "Details of the Asset Administration Shell" published on www.plattform-i40.de which is licensed under Creative Commons CC BY 4.0.

-

Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft +

Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V.

Internet: {{homepage}}
diff --git a/projects/aas-portal/src/app/start/favorites.service.ts b/projects/aas-portal/src/app/start/favorites.service.ts index 032eb1c2..80a53832 100644 --- a/projects/aas-portal/src/app/start/favorites.service.ts +++ b/projects/aas-portal/src/app/start/favorites.service.ts @@ -15,33 +15,35 @@ import { AuthService } from 'aas-lib'; providedIn: 'root', }) export class FavoritesService { - private _lists: FavoritesList[]; + private _lists?: FavoritesList[]; - public constructor(private readonly auth: AuthService) { - const value = auth.getCookie('.Favorites'); - this._lists = value ? JSON.parse(value) : []; - } + public constructor(private readonly auth: AuthService) {} public get lists(): FavoritesList[] { + if (!this._lists) { + const value = this.auth.getCookie('.Favorites'); + this._lists = value ? (JSON.parse(value) as FavoritesList[]) : []; + } + return this._lists; } public has(name: string): boolean { - return this._lists.some(list => list.name === name); + return this.lists.some(list => list.name === name); } public get(name: string): FavoritesList | undefined { - return this._lists.find(list => list.name === name); + return this.lists.find(list => list.name === name); } public delete(name: string): void { - this._lists = this._lists.filter(list => list.name !== name); + this._lists = this.lists.filter(list => list.name !== name); this.auth.setCookie('.Favorites', JSON.stringify(this._lists)); } public add(documents: AASDocument[], name: string, newName?: string): void { - const i = this._lists.findIndex(list => list.name === name); - const lists = [...this._lists]; + const i = this.lists.findIndex(list => list.name === name); + const lists = [...this.lists]; let list: FavoritesList; if (i < 0) { list = { name, documents: [] }; @@ -66,7 +68,7 @@ export class FavoritesService { } public remove(documents: AASDocument[], name: string): void { - const i = this._lists.findIndex(list => list.name === name); + const i = this.lists.findIndex(list => list.name === name); if (i < 0) return; const lists = [...this.lists]; diff --git a/projects/aas-portal/src/app/start/start.selectors.ts b/projects/aas-portal/src/app/start/start.selectors.ts index e3e31828..ed986353 100644 --- a/projects/aas-portal/src/app/start/start.selectors.ts +++ b/projects/aas-portal/src/app/start/start.selectors.ts @@ -11,14 +11,13 @@ import { StartFeatureState } from './start.state'; import { ViewMode } from 'aas-lib'; const getState = (state: StartFeatureState) => state.start; -const getFilter = (state: StartFeatureState) => state.start.filter; const getViewMode = (state: StartFeatureState) => state.start.viewMode; const getLimit = (state: StartFeatureState) => state.start.limit; const getDocuments = (state: StartFeatureState) => state.start.documents; export const selectState = createSelector(getState, state => state); -export const selectFilter = createSelector(getFilter, filter => filter); +export const selectFilter = createSelector(getState, state => (state.favorites === '-' ? '' : state.filter)); export const selectViewMode = createSelector(getViewMode, viewMode => viewMode); diff --git a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts index f71538bb..634b5708 100644 --- a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts +++ b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts @@ -174,12 +174,12 @@ export class MySqlIndex extends AASIndex { } public override async find(endpoint: string | undefined, id: string): Promise { - const document = endpoint ? await this.getEndpointDocument(endpoint, id) : await this.getDocument(id); - if (document) { - return this.toDocument(document); + const document = endpoint ? await this.selectEndpointDocument(endpoint, id) : await this.selectDocument(id); + if (!document) { + return undefined; } - return undefined; + return this.toDocument(document); } public override async remove(endpointName: string, id: string): Promise { @@ -347,7 +347,7 @@ export class MySqlIndex extends AASIndex { }; } - private async getEndpointDocument(endpoint: string, id: string): Promise { + private async selectEndpointDocument(endpoint: string, id: string): Promise { const [results] = await ( await this.connection ).query('SELECT * FROM `documents` WHERE endpoint = ? AND (id = ? OR assetId = ?)', [ @@ -357,19 +357,19 @@ export class MySqlIndex extends AASIndex { ]); if (results.length === 0) { - throw new Error(`A document with the id "${id}" does not exist in "${endpoint}".`); + return undefined; } return results[0]; } - private async getDocument(id: string): Promise { + private async selectDocument(id: string): Promise { const [results] = await ( await this.connection ).query('SELECT * FROM `documents` WHERE (id = ? OR assetId = ?)', [id, id]); if (results.length === 0) { - throw new Error(`A document with the id "${id}" does not exist.`); + return undefined; } return results[0]; diff --git a/projects/aas-server/src/app/aas-provider/aas-provider.ts b/projects/aas-server/src/app/aas-provider/aas-provider.ts index 1edc75c7..cdc69edf 100644 --- a/projects/aas-server/src/app/aas-provider/aas-provider.ts +++ b/projects/aas-server/src/app/aas-provider/aas-provider.ts @@ -22,6 +22,7 @@ import { AASEndpoint, ApplicationError, getChildren, + isReferenceElement, } from 'common'; import { ImageProcessing } from '../image-processing.js'; @@ -364,7 +365,7 @@ export class AASProvider { */ public async getHierarchyAsync(endpoint: string, id: string): Promise { const document = await this.index.get(endpoint, id); - const root: AASDocument = { ...document, parent: null, content: null }; + const root: AASDocument = { ...document, parentId: null, content: null }; const nodes: AASDocument[] = [root]; await this.collectDescendants(root, nodes); return nodes; @@ -529,44 +530,50 @@ export class AASProvider { } private async collectDescendants(parent: AASDocument, nodes: AASDocument[]): Promise { - const content = await this.getDocumentContentAsync(parent); - const hierarchicalStructures = content.submodels.filter(sm => - HierarchicalStructure.isHierarchicalStructure(sm), - ); - - if (hierarchicalStructures.length > 0) { - } else { - for (const reference of this.whereReferenceElement(content.submodels)) { - if (reference.value) { - const childId = reference.value.keys[0].value; - const child = - (await this.index.find(parent.endpoint, childId)) ?? - (await this.index.find(undefined, childId)); - - if (child) { - const node: AASDocument = { ...child, parent: { ...parent }, content: null }; - nodes.push(node); - await this.collectDescendants(node, nodes); - } + const content = parent.content ?? (await this.getDocumentContentAsync(parent)); + + for (const submodel of this.whereHierarchicalStructure(content.submodels)) { + const assetIds = await new HierarchicalStructure(parent, content, submodel).getChildren(); + for (const assetId of assetIds) { + const child = await this.index.find(undefined, assetId); + if (child) { + const node: AASDocument = { ...child, parentId: parent.id, content: null }; + nodes.push(node); + await this.collectDescendants(node, nodes); } } } + + for (const reference of this.whereAASReference(content.submodels)) { + const childId = reference.keys[0].value; + const child = + (await this.index.find(parent.endpoint, childId)) ?? (await this.index.find(undefined, childId)); + + if (child) { + const node: AASDocument = { ...child, parentId: parent.id, content: null }; + nodes.push(node); + await this.collectDescendants(node, nodes); + } + } + } + + private *whereHierarchicalStructure(submodels: aas.Submodel[]): Generator { + for (const submodel of submodels) { + if (HierarchicalStructure.isHierarchicalStructure(submodel)) { + yield submodel; + } + } } - private *whereReferenceElement(elements: aas.Referable[]): Generator { + private *whereAASReference(elements: aas.Referable[]): Generator { const stack: aas.Referable[][] = []; stack.push(elements); while (stack.length > 0) { const children = stack.pop() as aas.Referable[]; for (const child of children) { - if (child.modelType === 'ReferenceElement') { - const value = child as aas.ReferenceElement; - if ( - value && - value.value && - value.value.keys.some(item => item.type === 'AssetAdministrationShell') - ) { - yield value; + if (isReferenceElement(child)) { + if (child.value && child.value.keys.some(item => item.type === 'AssetAdministrationShell')) { + yield child.value; } } diff --git a/projects/aas-server/src/app/aas-provider/hierarchical-structure.ts b/projects/aas-server/src/app/aas-provider/hierarchical-structure.ts index f9f72b5d..8bfdcd3c 100644 --- a/projects/aas-server/src/app/aas-provider/hierarchical-structure.ts +++ b/projects/aas-server/src/app/aas-provider/hierarchical-structure.ts @@ -1,4 +1,5 @@ -import { aas } from 'common'; +import { AASDocument, aas, isEntity, isRelationshipElement, selectReferable } from 'common'; +import { AASIndex } from '../aas-index/aas-index.js'; export type ArcheType = 'Full' | 'OneDown' | 'OneUp'; @@ -41,63 +42,21 @@ export abstract class HierarchicalStructureElement { } } -export class EntryNode extends HierarchicalStructureElement { - public constructor( - public readonly archeType: ArcheType, - private readonly entity: aas.Entity, - ) { - super(); - } - - public get parent(): Node | null { - return null; - } - - public get children(): Node[] { - if (this.archeType === 'OneDown') { - } - - return []; - } -} - -export class Node extends HierarchicalStructureElement { +export class HierarchicalStructure extends HierarchicalStructureElement { public constructor( - private readonly entryNode: EntryNode, - private readonly entity: aas.Entity, + private readonly document: AASDocument, + private readonly env: aas.Environment, + private readonly submodel: aas.Submodel, ) { super(); - this.bulkCount = this.initBulkCount(); - } - - public readonly bulkCount: number; - - private initBulkCount(): number { - const property = this.getProperty( - this.entity.statements, - 'https://admin-shell.io/idta/HierarchicalStructures/BulkCount/1/0', - ); - - if (!property || !property.value) { - throw new Error('Missing BulkCount Property.'); - } - - return Number(property.value); - } -} - -export class HierarchicalStructure extends HierarchicalStructureElement { - public constructor(private readonly submodel: aas.Submodel) { - super(); - this.archeType = this.initArcheType(); - this.entryNode = this.initEntryNode(this.archeType); + this.entryNode = this.initEntryNode(); } public readonly archeType: ArcheType; - public readonly entryNode: EntryNode; + public readonly entryNode: aas.Entity; public static isHierarchicalStructure(submodel: aas.Submodel): boolean { return ( @@ -106,6 +65,32 @@ export class HierarchicalStructure extends HierarchicalStructureElement { ); } + public async getChildren(): Promise { + if (this.archeType !== 'OneDown') { + return []; + } + + const children: string[] = []; + for (const hasPart of this.getHasParts(this.entryNode)) { + const first = selectReferable(this.env, hasPart.first); + const second = selectReferable(this.env, hasPart.second); + if (isEntity(first) && isEntity(second)) { + let globalAssetId: string | undefined; + if (this.isNode(first)) { + globalAssetId = first.globalAssetId; + } else if (this.isNode(second)) { + globalAssetId = second.globalAssetId; + } + + if (globalAssetId) { + children.push(globalAssetId); + } + } + } + + return children; + } + private initArcheType(): ArcheType { const property = this.getProperty( this.submodel.submodelElements, @@ -119,7 +104,7 @@ export class HierarchicalStructure extends HierarchicalStructureElement { return property.value as ArcheType; } - private initEntryNode(archeType: ArcheType): EntryNode { + private initEntryNode(): aas.Entity { const entity = this.getEntity( this.submodel.submodelElements, 'https://admin-shell.io/idta/HierarchicalStructures/EntryNode/1/0', @@ -129,6 +114,45 @@ export class HierarchicalStructure extends HierarchicalStructureElement { throw new Error('Missing EntryNode Entity.'); } - return new EntryNode(archeType, entity); + return entity; + } + + private getHasParts(node: aas.Entity): aas.RelationshipElement[] { + if (!node.statements) { + return []; + } + + const hasParts: aas.RelationshipElement[] = []; + for (const statement of node.statements) { + if ( + isRelationshipElement(statement) && + HierarchicalStructureElement.getSemanticId(statement.semanticId) === + 'https://admin-shell.io/idta/HierarchicalStructures/HasPart/1/0' + ) { + hasParts.push(statement); + } + } + + return hasParts; + } + + private getBulkCount(node: aas.Entity): number { + const property = this.getProperty( + node.statements, + 'https://admin-shell.io/idta/HierarchicalStructures/BulkCount/1/0', + ); + + if (!property || !property.value) { + throw new Error('Missing BulkCount Property.'); + } + + return Number(property.value); + } + + private isNode(entity: aas.Entity): boolean { + return ( + HierarchicalStructureElement.getSemanticId(entity.semanticId) === + 'https://admin-shell.io/idta/HierarchicalStructures/Node/1/0' + ); } } diff --git a/projects/common/src/lib/index.ts b/projects/common/src/lib/index.ts index 467ef510..4ab1623d 100644 --- a/projects/common/src/lib/index.ts +++ b/projects/common/src/lib/index.ts @@ -10,12 +10,14 @@ import { AASEndpointType } from './types.js'; import { AssetAdministrationShell, Blob, + Entity, Environment, Identifiable, MultiLanguageProperty, Property, Referable, ReferenceElement, + RelationshipElement, Submodel, SubmodelElement, SubmodelElementCollection, @@ -244,6 +246,24 @@ export function isSubmodelElementList(referable: unknown): referable is Submodel return (referable as Referable)?.modelType === 'SubmodelElementList'; } +/** + * Determines whether the specified referable represents a relationship element. + * @param value The current referable. + * @returns `true` if the specified referable represents a `RelationshipElement`; otherwise, `false`. + */ +export function isRelationshipElement(referable: unknown): referable is RelationshipElement { + return (referable as Referable)?.modelType === 'RelationshipElement'; +} + +/** + * Determines whether the specified referable represents an entity. + * @param value The current referable. + * @returns `true` if the specified referable represents a `Entity`; otherwise, `false`. + */ +export function isEntity(referable: unknown): referable is Entity { + return (referable as Referable)?.modelType === 'Entity'; +} + /** * Checks if the specified string is url-safe-base64 encoded * @param s The string to test. diff --git a/projects/common/src/lib/types.ts b/projects/common/src/lib/types.ts index 794d0eea..8ae3b74d 100644 --- a/projects/common/src/lib/types.ts +++ b/projects/common/src/lib/types.ts @@ -86,8 +86,8 @@ export interface AASDocument extends AASDocumentId { modified?: boolean; /** Indicates whether communication can be established with the system represented by the AAS. */ onlineReady?: boolean; - /** The parent AAS in a hierarchy. */ - parent?: AASDocument | null; + /** The identifier of the parent AAS in a hierarchy. */ + parentId?: string | null; /** Indicates whether the document can be edited. */ readonly: boolean; /** A thumbnail. */