Browse Source

Load project from zip file

include namespacetable in exported nodesets
fix reference export
store mapping as mapping.yaml in project.zip
translate nodeids to local nodeset-id before export
fix displayname export
Martin Kunz 1 year ago
parent
commit
43349e7dff

+ 15 - 1
package-lock.json

@@ -13,7 +13,8 @@
         "fast-xml-parser": "^4.3.1",
         "jszip": "^3.10.1",
         "pinia": "^2.1.6",
-        "vue": "^3.3.4"
+        "vue": "^3.3.4",
+        "yaml": "^2.3.2"
       },
       "devDependencies": {
         "@rushstack/eslint-patch": "^1.4.0",
@@ -4037,6 +4038,14 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "node_modules/yaml": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
+      "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -6801,6 +6810,11 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "yaml": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
+      "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg=="
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

+ 2 - 1
package.json

@@ -16,7 +16,8 @@
     "fast-xml-parser": "^4.3.1",
     "jszip": "^3.10.1",
     "pinia": "^2.1.6",
-    "vue": "^3.3.4"
+    "vue": "^3.3.4",
+    "yaml": "^2.3.2"
   },
   "devDependencies": {
     "@rushstack/eslint-patch": "^1.4.0",

+ 5 - 3
src/components/TheEditor.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { useStore, type IMappingValue } from '@/util/store'
+import { useStore } from '@/util/store'
 const store = useStore()
 import { computed, shallowRef } from 'vue'
 
@@ -11,6 +11,7 @@ import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"
 import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"
 import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"
 import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"
+import type { IMappingValue } from '@/ua/AddressSpace';
 self.MonacoEnvironment = {
   getWorker(_, label) {
     if (label === "json") {
@@ -44,7 +45,7 @@ const code = computed(():string => {
       return "";
     if(!node.value.nodeId.value)
       return "";
-    const m=store.mapping.get(node.value.nodeId.value.toString());
+    const m=store.addressSpace?.mapping.get(node.value.nodeId.toString());
     if(m)
       return m.read;
     return "";
@@ -56,12 +57,13 @@ const handleMount = (editor: any) => {
 
 }
 
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const onChange = (value: string | undefined, event: monaco.editor.IModelContentChangedEvent) => {
   if(!store.selectedNode)
     return;
   if(!value)
     return;
-  store.mapping.set(store.selectedNode.nodeId.value.toString(), {read: value, write: ""} as IMappingValue)
+  store.addressSpace?.mapping.set(store.selectedNode.nodeId.value.toString(), {path: store.selectedNode.nodeId.toString(), read: value, write: ""} as IMappingValue)
 }
 </script>
 

+ 32 - 5
src/components/TheModels.vue

@@ -1,10 +1,15 @@
 <!-- eslint-disable no-fallthrough -->
 <script setup lang="ts">
-import { AddressSpace } from '@/ua/AddressSpace';
+import { AddressSpace, type IMappingValue } from '@/ua/AddressSpace';
 import { UANodeSet } from '@/ua/UANodeSet';
 import { useStore } from '@/util/store'
 import { ref } from 'vue';
 import VDialog from './VDialog.vue'
+import JSZip from "jszip";
+import YAML from 'yaml'
+import { stringify } from 'querystring';
+
+
 
 const store = useStore()
 const newDialogOpen = ref(false);
@@ -14,7 +19,7 @@ const projectType = ref("ua");
 async function exportProject() {
   const blob = await store.addressSpace?.exportProject();
   if (blob)
-    downloadBlob(blob, "proj.zip");
+    downloadBlob(blob, "project.zip");
 }
 
 function downloadBlob(blob: Blob, filename: string) {
@@ -52,10 +57,32 @@ async function newProject() {
 async function handleDrop(e: DragEvent) {
   if (!e.dataTransfer)
     return;
-  for (let file of e.dataTransfer.files) {
-    let xmlString = await file.text();
-    store.addNodeset(await UANodeSet.parse(xmlString, file.name));
+  if(e.dataTransfer.files.length==1 && e.dataTransfer.files[0].name.endsWith(".zip")) {
+    loadZip(e.dataTransfer.files[0]);
+  }
+  else {
+    for (const file of e.dataTransfer.files) {
+      const xmlString = await file.text();
+      store.addNodeset(await UANodeSet.parse(xmlString, file.name));
+    }
+  }
+}
+
+async function  loadZip(file: File){
+  const zip= await JSZip.loadAsync(file.arrayBuffer())
+  const filenames=JSON.parse(await zip.files['project.json'].async("string"));
+  let nodesets: UANodeSet[]=[];
+  for(const fileName of filenames) {
+    nodesets.push(await UANodeSet.parse(await zip.files[fileName].async("string"), fileName));
   }
+  const as=new AddressSpace(nodesets);
+  const mlist=YAML.parse(await zip.files['mapping.yaml'].async("string")) as IMappingValue[];
+  const mapping=new Map<string, IMappingValue>();
+  for(const entry of mlist) {
+    mapping.set(entry.path, entry);
+  }
+  as.mapping=mapping;
+  store.setAddressSpace(as);
 }
 </script>
 

+ 15 - 2
src/ua/AddressSpace.ts

@@ -1,6 +1,7 @@
 import { UANodeSet } from "./UANodeSet";
 import { UABaseNode } from "./UABaseNode";
 import { NamespaceTable } from "./NameSpaceTable";
+import YAML from 'yaml'
 import JSZip from "jszip";
 
 export class AddressSpace{
@@ -8,11 +9,14 @@ export class AddressSpace{
     nodeMap:  Map<string, UABaseNode>;
     nst: NamespaceTable;
     nodesets: UANodeSet[];
+    mapping:  Map<string, IMappingValue>;
+
 
     constructor(nodesets: UANodeSet[]) {
         this.nst=new NamespaceTable();
         this.nodeMap=new Map();
         this.nodesets=[];
+        this.mapping=new Map();
         for(const nodeset of nodesets) {
             this.addNodeset(nodeset);
         }
@@ -48,9 +52,18 @@ export class AddressSpace{
         const fileNames:string[]=[];
         for(const ns of this.nodesets) {
             fileNames.push(ns.fileName);
-            zip.file(ns.fileName, ns.toXML().toString());
+            zip.file(ns.fileName, ns.toXML(ns.nameSpaceTable, this.nst).toString());
         }
         zip.file("project.json", JSON.stringify(fileNames));
+        const mapString=YAML.stringify(this.mapping.values());
+        zip.file("mapping.yaml", mapString)
         return zip.generateAsync({type:'blob'});
     }
-}
+}
+
+
+export interface IMappingValue {
+    path: string;
+    read: string;
+    write: string;
+  }

+ 11 - 1
src/ua/UABaseNode.ts

@@ -132,9 +132,19 @@ export class UABaseNode implements IToXML{
         return ua;
     }
 
-    toXML(): XMLElem {
+    toXML(lnst: NamespaceTable, gnst: NamespaceTable): XMLElem {
         throw new Error("UABaseNode has no xml rep; implement in subtype.");
     }
+
+    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;
+    }
 }
 
 

+ 7 - 5
src/ua/UAMethod.ts

@@ -1,5 +1,6 @@
 import { XMLElem } from "@/util/XmlElem";
 import { UABaseNode, type UABaseNodeOptions } from "./UABaseNode";
+import type { NamespaceTable } from "./NameSpaceTable";
 
 export class UAMethod extends UABaseNode {
     constructor(options: UAMethodNodeOptions) {
@@ -15,15 +16,16 @@ export class UAMethod extends UABaseNode {
                             references: bn.references});
     }
 
-
-    toXML(): XMLElem {
+    toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
+        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
         const elem =new XMLElem('UAMethod');
-        elem.attr('NodeID', this.nodeId.toString())
+        elem.attr('NodeId', nid.toString())
             .attr('BrowseName', this.browseName)
-            .attr('DisplayName', this.displayName);
+            .elem('DisplayName', this.displayName);
         const refs=elem.add(new XMLElem('References'))
         for(const ref of this.references) {
-            refs.add(ref.toXML());
+            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));
         }
         return elem;
     }

+ 9 - 2
src/ua/UANodeSet.ts

@@ -31,14 +31,21 @@ export class UANodeSet implements IToXML{
         }    
     }
 
-    toXML(): XMLElem {
+    toXML(lnst: NamespaceTable, gnst: NamespaceTable): XMLElem {
         const xmlUANodeSet =new XMLElem('UANodeSet');
+        const xmlNameSpaceUris=xmlUANodeSet.add(new XMLElem('NamespaceUris'));
+
+        for(let i=0;i<this.nameSpaceTable.nsMap.size(); i++) {
+            const uri=this.nameSpaceTable.nsMap.get(i);
+            xmlNameSpaceUris.elem("Uri", uri);
+        }
+
         const xmlModels=xmlUANodeSet.add(new XMLElem('Models'));
         for(const model of this.models) {
             xmlModels.add(model.toXML())
         }
         for(const node of this.nodes) {
-            xmlUANodeSet.add(node.toXML())
+            xmlUANodeSet.add(node.toXML(lnst, gnst))
         }
         return xmlUANodeSet;
     }

+ 8 - 6
src/ua/UAObject.ts

@@ -1,5 +1,6 @@
 import { XMLElem } from "@/util/XmlElem";
 import { UABaseNode, type UABaseNodeOptions } from "./UABaseNode";
+import type { NamespaceTable } from "./NameSpaceTable";
 
 export class UAObject extends UABaseNode {
     constructor(options: UAObjectNodeOptions) {
@@ -15,15 +16,16 @@ export class UAObject extends UABaseNode {
                             references: bn.references});
     }
 
-
-    toXML(): XMLElem {
-        const elem =new XMLElem('UAVariable');
-        elem.attr('NodeID', this.nodeId.toString())
+    toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
+        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
+        const elem =new XMLElem('UAObject');
+        elem.attr('NodeId', nid.toString())
             .attr('BrowseName', this.browseName)
-            .attr('DisplayName', this.displayName);
+            .elem('DisplayName', this.displayName);
         const refs=elem.add(new XMLElem('References'))
         for(const ref of this.references) {
-            refs.add(ref.toXML());
+            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));
         }
         return elem;
     }

+ 3 - 2
src/ua/UAReference.ts

@@ -22,8 +22,9 @@ export class UAReference implements IToXML{
         this.toRef.namespace=newIndex;    
     }
 
-    toXML(): XMLElem {
-        return new XMLElem('Reference', this.toRef.toString())
+    toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
+        const nid=UABaseNode.localNodeId(this.toRef, lnst, gnst);
+        return new XMLElem('Reference', nid.toString())
             .attr('ReferenceType', this.referenceType.toString())
             .attr('IsForward',this.isForward);       
     }

+ 7 - 4
src/ua/UAVariable.ts

@@ -1,5 +1,6 @@
 import { XMLElem } from "@/util/XmlElem";
 import { UABaseNode, type UABaseNodeOptions } from "./UABaseNode";
+import type { NamespaceTable } from "./NameSpaceTable";
 
 export class UAVariable extends UABaseNode {
     constructor(options: UAVariableNodeOptions) {
@@ -15,14 +16,16 @@ export class UAVariable extends UABaseNode {
                                 references: bn.references});
     }
 
-    toXML(): XMLElem {
+    toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
+        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
         const elem =new XMLElem('UAVariable');
-        elem.attr('NodeID', this.nodeId.toString())
+        elem.attr('NodeId', nid.toString())
             .attr('BrowseName', this.browseName)
-            .attr('DisplayName', this.displayName);
+            .elem('DisplayName', this.displayName);
         const refs=elem.add(new XMLElem('References'))
         for(const ref of this.references) {
-            refs.add(ref.toXML());
+            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));
         }
         return elem;
     }

+ 10 - 1
src/util/XmlElem.ts

@@ -1,3 +1,5 @@
+import type { NamespaceTable } from "@/ua/NameSpaceTable";
+
 export class XMLElem {
     name: string;
     value: string;
@@ -21,6 +23,13 @@ export class XMLElem {
         return this;
     }
 
+    public elem(name:string, value: string|boolean|undefined|null): XMLElem {
+        if(value===undefined||value===null)
+            return this; //skip undefined/null values
+        this.elements.push(new XMLElem(name, value?.toString()));
+        return this;
+    }
+
     public toString(level: number=0): string {
         let s=" ".repeat(level+1) + `<${this.name} `;
         for(const attr of this.attributes) {
@@ -64,5 +73,5 @@ class XmlAttr {
 }
 
 export interface IToXML {
-    toXML() :XMLElem;
+    toXML(lnst: NamespaceTable, gnst: NamespaceTable) :XMLElem;
 }

+ 4 - 0
src/util/bi-map.ts

@@ -73,4 +73,8 @@ export class BiMap<K = any, V = any> {
 		this.primaryMap.clear();
 		this.secondaryMap.clear();
 	}
+
+	public size(): number {
+		return this.primaryMap.size;
+	}
 }

+ 1 - 8
src/util/store.ts

@@ -7,9 +7,8 @@ import { ref } from 'vue'
 export const useStore = defineStore('user', {
   state: () => ({
       addressSpace: null as AddressSpace | null,
-      rootNode: null as UABaseNode | null,
+      rootNode: ref<UABaseNode | null>(null),
       selectedNode: ref<UABaseNode | null>(null),
-      mapping: new Map<string, IMappingValue>()
   }),
   actions: {
     setAddressSpace(as: AddressSpace) {
@@ -23,9 +22,3 @@ export const useStore = defineStore('user', {
     }
   }
 })
-
-export interface IMappingValue {
-  path: string;
-  read: string;
-  write: string;
-}