import type { NamespaceTable } from "./NameSpaceTable"; import { coerceNodeId, NodeId } from "./NodeId"; import { UAReference } from "./UAReference"; import { assert } from "@/util/assert"; import { XMLElem, type IToXML } from "@/util/XmlElem"; import { UARolePermission } from "./UARolePermission"; import { type IAddressSpace } from "./IAddressSpace"; import { ReferenceTypeIds } from "./opcua_node_ids"; import { UALocalizedText } from "./UALocalizedText"; export class UABaseNode implements IToXML{ public static nullBaseNode=new UABaseNode({browseName: "", addressSpace: {} as IAddressSpace, nodeId: NodeId.nullNodeId}); public nodeId!: NodeId; public nodeClass="UABaseNode"; public browseName!: string; public addressSpace!: IAddressSpace public displayName: UALocalizedText[]=[]; public description: UALocalizedText[]=[]; public symbolicName?: string; public releaseStatus!: string; public hasNoPermissions!: boolean; public writeMask!: number; public userWriteMask!: number; public category: string[]=[]; public documentation?: string; public accessRestriction?: number; public rolePermissions: UARolePermission[]=[]; public references: UAReference[]=[]; constructor(options: UABaseNodeOptions) { Object.assign(this, options); } reIndex(nst: NamespaceTable, onst: NamespaceTable) { const nsName=onst.getUri(this.nodeId.namespace); assert(nsName!=undefined) const newIndex=nst.getIndex(nsName); assert(newIndex!=undefined) this.nodeId.namespace=newIndex; for(const uaref of this.references) { uaref.reIndex(nst, onst); } } getParent(): UABaseNode|null { const ref=this.getParentRef(); if(ref?.fromNode===this) return ref.toNode; if(ref?.toNode===this) return ref.fromNode; return null; } setParent(newParentNode: UABaseNode, newRefType: string) { const hierReferences=this.addressSpace.getSubTreeAsList("ns=0;i="+ReferenceTypeIds.HierarchicalReferences); let href; for(const r of this.references) { //find current href in references href=hierReferences.find((n) => {n.browseName===r.referenceType}) } for(let i=this.references.length;i--;i>=0) { //remove current href from references const r=this.references[i]; if(r.referenceType==href?.browseName) this.references.splice(i,1); } const newRefA=new UAReference(this.nodeId, newRefType, newParentNode.nodeId, true); newRefA.fromNode=this; newRefA.toNode=newParentNode; this.references.push(newRefA); const newRefB=new UAReference(newParentNode.nodeId, newRefType, this.nodeId, false); newRefB.fromNode=this; newRefB.toNode=newParentNode; this.references.push(newRefB); } getParentRef(): UAReference|null{ for(const ref of this.references) { switch(ref.referenceType) { case 'HasComponent': case 'HasOrderedComponent': case 'Organizes': case 'HasProperty': case 'HasSubtype': case 'HasAddIn': if(ref.fromNode===this&&ref.isForward===false) return ref; if(ref.toNode===this&&ref.isForward===true) return ref; } } return null; } getChildren(): UABaseNode[] { const children: UABaseNode[]=[]; for(const ref of this.references) { switch(ref.referenceType) { case 'HasComponent': case 'HasOrderedComponent': case 'Organizes': case 'HasProperty': case 'HasSubtype': case 'HasAddIn': if(ref.isForward&&ref.fromNode===this) { if(!children.includes(ref.toNode)) children.push(ref.toNode); } if(!ref.isForward&&ref.toNode===this) { if(!children.includes(ref.fromNode)) children.push(ref.fromNode); } break; } } return children; } getModellingRule(): string|undefined{ for(const ref of this.references) { if(ref.referenceType == "HasModellingRule"){ return ref.toNode.browseName; } } return undefined; } resolveReferences(nm: Map) { for(const ref of this.references) { const fromNode=nm.get(ref.fromRef.toString()) if(fromNode) ref.fromNode=fromNode; const toNode=nm.get(ref.toRef.toString()) if(!toNode) continue; //TODO: if we cant find the node; the parser is still incomplete or the nodeset is broken ref.toNode=toNode; if(ref.fromNode.nodeId.toString()===this.nodeId.toString()){ //add this reference to referenced node //Bug? when loading from filedrop; fromNode is a proxy; this is not. if(!ref.toNode.references.includes(ref)) ref.toNode.references.push(ref); } } } static fromXML(xmlObject: any, addressSpace: IAddressSpace): UABaseNode{ const references:UAReference[]=[]; const nodeId=coerceNodeId(xmlObject['@_NodeId']); for(const xmlref of xmlObject['References']?.['Reference']||[]) { references.push(UAReference.fromXML(xmlref, nodeId)); } const displayNames:UALocalizedText[]=[]; for(const xmldn of xmlObject['DisplayName']||[]) { displayNames.push(UALocalizedText.fromXML("DisplayName", xmldn)); } const descriptions:UALocalizedText[]=[]; for(const desc of xmlObject['Description']||[]) { descriptions.push(UALocalizedText.fromXML("Description", desc)); } const permissions:UARolePermission[]=[]; for(const perm of xmlObject['RolePermissions']?.['RolePermission']||[]) { permissions.push(UARolePermission.fromXML(perm)); } const ua=new UABaseNode({addressSpace: addressSpace, nodeId: nodeId, browseName: xmlObject['@_BrowseName'], writeMask: Number(xmlObject['@_WriteMask']||0), userWriteMask: Number(xmlObject['@_UserWriteMask']||0), accessRestriction: xmlObject['@_AccessRestrictions'], hasNoPermissions: xmlObject['@_HasNoPermissions']||false, symbolicName: xmlObject['@_SymbolicName'], releaseStatus: xmlObject['@_ReleaseStatus']||"Released", displayName: displayNames, description: descriptions, category: xmlObject['Category'], documentation: xmlObject['Documentation']?.['#text'], references: references, rolePermissions: permissions, }); return ua; } toXML(lnst: NamespaceTable, gnst: NamespaceTable): XMLElem { const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst); const elem =new XMLElem('UABaseNode'); elem.attr('NodeId', nid.toString()) .attr('BrowseName', this.browseName) .attr('WriteMask', this.writeMask.toString()) .attr('UserWriteMask', this.userWriteMask.toString()) .attr('AccessRestriction', this.accessRestriction?.toString()) .attr('HasNoPermissions', this.hasNoPermissions) .attr('SymbolicName', this.symbolicName) .attr('ReleaseStatus',this.releaseStatus) .attr('Documentation', this.documentation); for(const c in this.category) { elem.attr('Category', c); } const refs=new XMLElem('References'); for(const ref of this.references) { if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export refs.add(ref.toXML(lnst, gnst)); } if(refs.elements.length>0) elem.add(refs); for(const dn of this.displayName) elem.add(dn.toXML()); for(const desc of this.description) elem.add(desc.toXML()); const perms=new XMLElem('RolePermissions') for(const perm of this.rolePermissions) perms.add(perm.toXML()); if(perms.elements.length>0) elem.add(perms); return elem; } static localNodeId(nodeId: NodeId, lnst: NamespaceTable, gnst: NamespaceTable) { const ns_uri=gnst.getUri(nodeId.namespace); assert(ns_uri); const nsIdx=lnst.getIndex(ns_uri); assert(nsIdx); const tmpNode= coerceNodeId(nodeId.toString()); tmpNode.namespace=nsIdx; return tmpNode; } } export interface UABaseNodeOptions { addressSpace: IAddressSpace; nodeId: NodeId; browseName: string; writeMask?: number; userWriteMask?: number; accessRestriction?: number; hasNoPermissions?: boolean; symbolicName?: string; releaseStatus?: string; displayName?: UALocalizedText[]; description?: UALocalizedText[]; category?: string[]; documentation?: string; references?: UAReference[]; rolePermissions?: UARolePermission[]; }