diff --git a/foundation/utils.ts b/foundation/utils.ts index fc4cc3c..cd8f115 100644 --- a/foundation/utils.ts +++ b/foundation/utils.ts @@ -1,3 +1,20 @@ +/** User selection of a data structure + * @example + * user selects data object `Beh` to be enum `on` and `test` + * ```ts + * { + * Beh: { + * stVal: {on: {}, test: {}} + * q: {} + * t: {} + * } + * } + * ``` + */ +export type TreeSelection = { + [name: string]: TreeSelection; +}; + /** Intent to `parent.insertBefore(node, reference)` */ export type Insert = { parent: Node; @@ -35,12 +52,12 @@ export function isInsert(edit: Edit): edit is Insert { export function createElement( doc: XMLDocument, tag: string, - attrs: Record, + attrs: Record, ): Element { const element = doc.createElementNS(doc.documentElement.namespaceURI, tag); Object.entries(attrs) // eslint-disable-next-line @typescript-eslint/no-unused-vars - .filter(([_, value]) => value !== null) + .filter(([_, value]) => typeof value === "string") .forEach(([name, value]) => element.setAttribute(name, value!)); return element; diff --git a/index.ts b/index.ts index 1172d13..3dbc7f6 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,10 @@ -export { Edit } from "./foundation/utils.js"; -export { Update } from "./foundation/utils.js"; -export { Insert } from "./foundation/utils.js"; -export { Remove } from "./foundation/utils.js"; +export { + Edit, + Update, + Insert, + Remove, + TreeSelection, +} from "./foundation/utils.js"; export { updateBay } from "./tBay/updateBay.js"; export { updateVoltageLevel } from "./tVoltageLevel/updateVoltageLevel.js"; @@ -69,10 +72,7 @@ export { sourceControlBlock } from "./tExtRef/sourceControlBlock.js"; export { isSubscribed } from "./tExtRef/isSubscribed.js"; export { importLNodeType } from "./tDataTypeTemplates/importLNodeType.js"; -export { - lNodeTypeToSelection, - TreeSelection, -} from "./tDataTypeTemplates/lNodeTypeToSelection.js"; +export { lNodeTypeToSelection } from "./tDataTypeTemplates/lNodeTypeToSelection.js"; export { LNodeDescription, @@ -80,6 +80,8 @@ export { nsdToJson, } from "./tDataTypeTemplates/nsdToJson.js"; +export { insertSelectedLNodeType } from "./tDataTypeTemplates/insertSelectedLNodeType.js"; + export { Supervision, SupervisionOptions, diff --git a/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts b/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts new file mode 100644 index 0000000..e75e9e1 --- /dev/null +++ b/tDataTypeTemplates/insertSelecetdLNodeType.spec.ts @@ -0,0 +1,188 @@ +import { expect } from "chai"; + +import { findElement } from "../foundation/helpers.test.js"; + +import { + atccSelection, + ltrkSelection, + mmxuSelection, +} from "./insertSelectedLNodeType.testdata.js"; + +import { + incompleteAtccTypes, + missingMmxuTypes, + incompleteLtrkTypes, + emptySSD, +} from "./insertSelectedDataType.testfiles.js"; + +import { insertSelectedLNodeType } from "./insertSelectedLNodeType.js"; + +const incompleteMmxu = findElement(missingMmxuTypes) as XMLDocument; +const imcompleteLtrk = findElement(incompleteLtrkTypes) as XMLDocument; +const incompleteAtcc = findElement(incompleteAtccTypes) as XMLDocument; +const missingDataTypes = findElement(emptySSD) as XMLDocument; + +describe("insertLNodeTypeSelection", () => { + it("return empty array with invlaid lnClass", () => { + expect( + insertSelectedLNodeType(incompleteMmxu, mmxuSelection, "ERRO").length, + ).to.equal(0); + }); + + it("insert MMXU LNodeType including missing sub data", () => { + const edits = insertSelectedLNodeType( + incompleteMmxu, + mmxuSelection, + "MMXU", + ); + + expect(edits.length).to.equal(6); + + const lNodeType = edits[0].node as Element; + expect(lNodeType.tagName).to.equal("LNodeType"); + expect(lNodeType.getAttribute("lnClass")).to.equal("MMXU"); + expect(lNodeType.getAttribute("id")).to.equal( + "MMXU$oscd$_3f831c4c0fa5f6bd", + ); + + const doTypeCmv = edits[1].node as Element; + expect(doTypeCmv.tagName).to.equal("DOType"); + expect(doTypeCmv.getAttribute("cdc")).to.equal("CMV"); + expect(doTypeCmv.getAttribute("id")).to.equal( + "phsB$oscd$_3a6c99c1de0ca1c1", + ); + + const doTypeWye = edits[2].node as Element; + expect(doTypeWye.tagName).to.equal("DOType"); + expect(doTypeWye.getAttribute("cdc")).to.equal("WYE"); + expect(doTypeWye.getAttribute("id")).to.equal("A$oscd$_bd01f85651a2b3ee"); + + const daTypecVal = edits[3].node as Element; + expect(daTypecVal.tagName).to.equal("DAType"); + expect(daTypecVal.getAttribute("id")).to.equal( + "cVal$oscd$_21f679e08734a896", + ); + + const daTypeSBOw = edits[4].node as Element; + expect(daTypeSBOw.tagName).to.equal("DAType"); + expect(daTypeSBOw.getAttribute("id")).to.equal( + "SBOw$oscd$_264aab113bc4c3d7", + ); + + const enumType = edits[5].node as Element; + expect(enumType.tagName).to.equal("EnumType"); + expect(enumType.getAttribute("id")).to.equal( + "stVal$oscd$_48ba16345b8e7f5b", + ); + }); + + it("insert LTRK LNodeType including missing sub data", () => { + const edits = insertSelectedLNodeType( + imcompleteLtrk, + ltrkSelection, + "LTRK", + ); + + expect(edits.length).to.equal(7); + + const lNodeType = edits[0].node as Element; + expect(lNodeType.tagName).to.equal("LNodeType"); + expect(lNodeType.getAttribute("lnClass")).to.equal("LTRK"); + expect(lNodeType.getAttribute("id")).to.equal( + "LTRK$oscd$_f8074960800758df", + ); + + const doTypeCmv = edits[1].node as Element; + expect(doTypeCmv.tagName).to.equal("DOType"); + expect(doTypeCmv.getAttribute("cdc")).to.equal("CTS"); + expect(doTypeCmv.getAttribute("id")).to.equal( + "ApcFTrk$oscd$_9039fc5f67d3778a", + ); + + const doTypeWye = edits[2].node as Element; + expect(doTypeWye.tagName).to.equal("DOType"); + expect(doTypeWye.getAttribute("cdc")).to.equal("CTS"); + expect(doTypeWye.getAttribute("id")).to.equal( + "ApcIntTrk$oscd$_0b6b0a301af5aa77", + ); + + const daTypecVal = edits[3].node as Element; + expect(daTypecVal.tagName).to.equal("DAType"); + expect(daTypecVal.getAttribute("id")).to.equal( + "ctlVal$oscd$_ed49c2f7a55ad05a", + ); + + const daTypeSBOw = edits[4].node as Element; + expect(daTypeSBOw.tagName).to.equal("DAType"); + expect(daTypeSBOw.getAttribute("id")).to.equal( + "ctlVal$oscd$_5a5af9e249dc7f84", + ); + + const enumTypeStVal = edits[5].node as Element; + expect(enumTypeStVal.tagName).to.equal("EnumType"); + expect(enumTypeStVal.getAttribute("id")).to.equal( + "stVal$oscd$_74dd2cc4b188b4ad", + ); + + const enumTypeOrCat = edits[6].node as Element; + expect(enumTypeOrCat.tagName).to.equal("EnumType"); + expect(enumTypeOrCat.getAttribute("id")).to.equal( + "orCat$oscd$_929ee017c8f9feb5", + ); + }); + + it("insert ATCC LNodeType including missing sub data", () => { + const edits = insertSelectedLNodeType( + incompleteAtcc, + atccSelection, + "ATCC", + ); + + expect(edits.length).to.equal(5); + + const lNodeType = edits[0].node as Element; + expect(lNodeType.tagName).to.equal("LNodeType"); + expect(lNodeType.getAttribute("lnClass")).to.equal("ATCC"); + expect(lNodeType.getAttribute("id")).to.equal( + "ATCC$oscd$_f9d7914eb0ce0e92", + ); + + const doTypeCmv = edits[1].node as Element; + expect(doTypeCmv.tagName).to.equal("DOType"); + expect(doTypeCmv.getAttribute("cdc")).to.equal("APC"); + expect(doTypeCmv.getAttribute("id")).to.equal( + "VolSpt$oscd$_ef3a36fd78b41086", + ); + + const daTypecVal = edits[2].node as Element; + expect(daTypecVal.tagName).to.equal("DAType"); + expect(daTypecVal.getAttribute("id")).to.equal( + "SBOw$oscd$_61d7e600207c9456", + ); + + const daTypeSBOw = edits[3].node as Element; + expect(daTypeSBOw.tagName).to.equal("DAType"); + expect(daTypeSBOw.getAttribute("id")).to.equal( + "Oper$oscd$_5b11d63fa0ade588", + ); + + const enumTypeOrCat = edits[4].node as Element; + expect(enumTypeOrCat.tagName).to.equal("EnumType"); + expect(enumTypeOrCat.getAttribute("id")).to.equal( + "ctlModel$oscd$_e975941313cb546c", + ); + }); + + it("insert DataTypeTemplates when missing", () => { + const edits = insertSelectedLNodeType( + missingDataTypes, + atccSelection, + "ATCC", + ); + + expect(edits.length).to.equal(19); + + const lNodeType = edits[0].node as Element; + expect(lNodeType.tagName).to.equal("DataTypeTemplates"); + }); +}); diff --git a/tDataTypeTemplates/insertSelectedDataType.testfiles.ts b/tDataTypeTemplates/insertSelectedDataType.testfiles.ts new file mode 100644 index 0000000..b3e2625 --- /dev/null +++ b/tDataTypeTemplates/insertSelectedDataType.testfiles.ts @@ -0,0 +1,481 @@ +export const missingMmxuTypes = ` + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + automatic-bay + + + automatic-remote + + + maintenance + + + + + not-supported + + + automatic-bay + + + automatic-remote + + + + + automatic-bay + + + automatic-station + + + automatic-remote + + + + + sbo-with-enhanced-security + + + + + on + + + + + + m + + + kg + + + s + + + A + + + K + + + mol + + + cd + + + deg + + + rad + + + sr + + + Gy + + + Bq + + + °C + + + Sv + + + F + + + C + + + S + + + H + + + V + + + ohm + + + J + + + N + + + Hz + + + lx + + + Lm + + + Wb + + + T + + + W + + + Pa + + + m² + + + m³ + + + m/s + + + m/s² + + + m³/s + + + m/m³ + + + M + + + kg/m³ + + + m²/s + + + W/m K + + + J/K + + + ppm + + + 1/s + + + rad/s + + + W/m² + + + J/m² + + + S/m + + + K/s + + + Pa/s + + + J/kg K + + + VA + + + Watts + + + VAr + + + phi + + + cos(phi) + + + Vs + + + V² + + + As + + + A² + + + A²t + + + VAh + + + Wh + + + VArh + + + V/Hz + + + Hz/s + + + char + + + char/s + + + kgm² + + + dB + + + J/Wh + + + W/s + + + l/s + + + dBm + + + h + + + min + + + Ohm/m + + + percent/s + + + A/V + + + A/Vs + + + + +`; + +export const incompleteLtrkTypes = ` + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1-of-n-control + Abortion-by-cancel + + + access-violation + access-not-allowed-in-current-state + + + Associate + Abort + + +`; + +export const incompleteAtccTypes = ` + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + automatic-remote + + + automatic-bay + + + sbo-with-enhanced-security + + + blocked + + +`; + +export const emptySSD = ` + +
+`; diff --git a/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts b/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts new file mode 100644 index 0000000..bacf3b6 --- /dev/null +++ b/tDataTypeTemplates/insertSelectedLNodeType.testdata.ts @@ -0,0 +1,355 @@ +export const mmxuSelection = { + Beh: { + q: {}, + stVal: { + off: {}, + blocked: {}, + on: {}, + test: {}, + "test/blocked": {}, + }, + t: {}, + }, + A: { + phsA: { + cVal: { + mag: { + f: {}, + }, + ang: { + f: {}, + }, + }, + q: {}, + t: {}, + }, + phsB: { + cVal: { + ang: { + f: {}, + }, + mag: { + f: {}, + }, + }, + q: {}, + t: {}, + instCVal: { + mag: { + f: {}, + }, + ang: { + f: {}, + }, + }, + units: { + SIUnit: { + "": {}, + "°C": {}, + "1/s": {}, + A: {}, + "A/V": {}, + "A/Vs": {}, + "A²": {}, + "A²t": {}, + As: {}, + Bq: {}, + C: {}, + cd: {}, + char: {}, + "char/s": {}, + "cos(phi)": {}, + dB: {}, + dBm: {}, + deg: {}, + F: {}, + Gy: {}, + h: {}, + H: {}, + Hz: {}, + "Hz/s": {}, + J: {}, + "J/K": {}, + "J/kg K": {}, + "J/m²": {}, + "J/Wh": {}, + K: {}, + "K/s": {}, + kg: {}, + "kg/m³": {}, + "kgm²": {}, + "l/s": {}, + Lm: {}, + lx: {}, + m: {}, + M: {}, + "m/m³": {}, + "m/s": {}, + "m/s²": {}, + "m²": {}, + "m²/s": {}, + "m³": {}, + "m³/s": {}, + min: {}, + mol: {}, + N: {}, + ohm: {}, + "Ohm/m": {}, + Pa: {}, + "Pa/s": {}, + "percent/s": {}, + phi: {}, + ppm: {}, + rad: {}, + "rad/s": {}, + s: {}, + S: {}, + "S/m": {}, + sr: {}, + Sv: {}, + T: {}, + V: {}, + "V/Hz": {}, + "V²": {}, + VA: {}, + VAh: {}, + VAr: {}, + VArh: {}, + Vs: {}, + W: {}, + "W/m K": {}, + "W/m²": {}, + "W/s": {}, + Watts: {}, + Wb: {}, + Wh: {}, + }, + }, + }, + phsC: { + cVal: { + mag: { + f: {}, + }, + ang: { + f: {}, + }, + }, + q: {}, + t: {}, + zeroDb: {}, + zeroDbRef: {}, + }, + }, + Mod: { + q: {}, + stVal: { + on: {}, + }, + t: {}, + ctlModel: { + "sbo-with-enhanced-security": {}, + }, + sboTimeout: {}, + SBOw: { + Check: {}, + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + "automatic-remote": {}, + "automatic-station": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + Oper: { + Check: {}, + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + "automatic-remote": {}, + "not-supported": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + Cancel: { + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + "automatic-remote": {}, + maintenance: {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + }, +}; + +export const ltrkSelection = { + Beh: { + q: {}, + stVal: { on: {} }, + t: {}, + }, + ApcFTrk: { + Check: {}, + ctlNum: {}, + ctlVal: { f: {} }, + errorCode: { + "access-not-allowed-in-current-state": {}, + "access-violation": {}, + }, + objRef: {}, + origin: { + orCat: { + "automatic-bay": {}, + "automatic-remote": {}, + }, + orIdent: {}, + }, + respAddCause: { + "1-of-n-control": {}, + "Abortion-by-cancel": {}, + }, + serviceType: { + Abort: {}, + Associate: {}, + }, + t: {}, + T: {}, + Test: {}, + }, + ApcIntTrk: { + Check: {}, + ctlNum: {}, + ctlVal: { i: {} }, + errorCode: { + "access-not-allowed-in-current-state": {}, + "access-violation": {}, + }, + objRef: {}, + origin: { + orCat: { + "automatic-bay": {}, + "automatic-remote": {}, + }, + orIdent: {}, + }, + respAddCause: { + "1-of-n-control": {}, + "Abortion-by-cancel": {}, + }, + serviceType: { + Abort: {}, + Associate: {}, + }, + t: {}, + T: {}, + Test: {}, + }, +}; + +export const atccSelection = { + Beh: { + q: {}, + stVal: { + blocked: {}, + }, + t: {}, + }, + VolSpt: { + ctlModel: { + "sbo-with-enhanced-security": {}, + }, + dbRef: {}, + Cancel: { + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + mxVal: { + f: {}, + }, + Oper: { + Check: {}, + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + SBOw: { + Check: {}, + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-bay": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + }, + BndCtrChg: { + ctlModel: { + "direct-with-enhanced-security": {}, + }, + dbRef: {}, + persistent: {}, + Cancel: { + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-remote": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + mxVal: { + i: {}, + }, + Oper: { + Check: {}, + ctlNum: {}, + ctlVal: {}, + origin: { + orCat: { + "automatic-remote": {}, + }, + orIdent: {}, + }, + T: {}, + Test: {}, + }, + }, +}; diff --git a/tDataTypeTemplates/insertSelectedLNodeType.ts b/tDataTypeTemplates/insertSelectedLNodeType.ts new file mode 100644 index 0000000..3c9a1dd --- /dev/null +++ b/tDataTypeTemplates/insertSelectedLNodeType.ts @@ -0,0 +1,410 @@ +/** + * Basis is a copy from www.github.com/openenergytools/oscd-template-generator + * originally written by ca-d, thx Chris. Code has been modified! + */ +import { nsdToJson } from "./nsdToJson.js"; + +import { createElement, Insert, TreeSelection } from "../foundation/utils.js"; + +import { getReference } from "../tBaseElement/getReference.js"; + +type Templates = { + EnumType: Element[]; + DAType: Element[]; + DOType: Element[]; + LNodeType: Element[]; +}; + +function describeEnumType(element: Element): { vals: Record } { + const vals: Record = {}; + + const sortedEnumVals = Array.from(element.children) + .filter((child) => child.tagName === "EnumVal") + .sort( + (v1, v2) => + parseInt(v1.getAttribute("ord")!, 10) - + parseInt(v2.getAttribute("ord")!, 10), + ); + for (const val of sortedEnumVals) + vals[val.getAttribute("ord")!] = val.textContent ?? ""; + + return { vals }; +} + +function describeDAType(element: Element): { + bdas: Record>; +} { + const bdas: Record> = {}; + for (const bda of Array.from(element.children) + .filter((child) => child.tagName === "BDA") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [bType, type, dchg, dupd, qchg] = [ + "bType", + "type", + "dchg", + "dupd", + "qchg", + ].map((attr) => bda.getAttribute(attr)); + bdas[bda.getAttribute("name")!] = { bType, type, dchg, dupd, qchg }; + } + return { bdas }; +} + +function describeDOType(element: Element) { + const sdos: Record> = {}; + for (const sdo of Array.from(element.children) + .filter((child) => child.tagName === "SDO") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, type, transient] = ["name", "type", "transient"].map((attr) => + sdo.getAttribute(attr), + ); + sdos[name!] = { type, transient }; + } + const das: Record> = {}; + for (const da of Array.from(element.children) + .filter((child) => child.tagName === "DA") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, fc, bType, type, dchg, dupd, qchg] = [ + "name", + "fc", + "bType", + "type", + "dchg", + "dupd", + "qchg", + ].map((attr) => da.getAttribute(attr)); + das[name!] = { + fc, + bType, + type, + dchg, + dupd, + qchg, + }; + } + return { + sdos, + das, + cdc: element.getAttribute("cdc"), + }; +} + +function describeLNodeType(element: Element) { + const dos: Record> = {}; + for (const doElement of Array.from(element.children) + .filter((child) => child.tagName === "DO") + .sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) { + const [name, type, transient] = ["name", "type", "transient"].map((attr) => + doElement.getAttribute(attr), + ); + dos[name!] = { type, transient }; + } + return { + dos, + lnClass: element.getAttribute("lnClass"), + }; +} + +const typeDescriptions = { + EnumType: describeEnumType, + DAType: describeDAType, + DOType: describeDOType, + LNodeType: describeLNodeType, +} as Partial object>>; + +function describeElement(element: Element): object { + const describe = typeDescriptions[element.tagName]!; + + return describe(element); +} + +function hashElement(element: Element): string { + /** A direct copy from www.github.com/openscd/open-scd-core/foundation/cyrb64.ts */ + + /** + * Hashes `str` using the cyrb64 variant of + * https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js + * @returns digest - a rather insecure hash, very quickly + */ + function cyrb64(str: string): string { + /* eslint-disable no-bitwise */ + let h1 = 0xdeadbeef; + let h2 = 0x41c6ce57; + /* eslint-disable-next-line no-plusplus */ + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return ( + (h2 >>> 0).toString(16).padStart(8, "0") + + (h1 >>> 0).toString(16).padStart(8, "0") + ); + /* eslint-enable no-bitwise */ + } + + return cyrb64(JSON.stringify(describeElement(element))); +} + +function data(lnData: any, path: string[]): any { + let d = lnData; + for (const slug of path.slice(0, -1)) d = d[slug].children; + + return d[path[path.length - 1]]; +} + +/** + * Creates a new data type `LNodeType` based on a user selection. + * @param doc - the XML document to add the `LNodeType` to + * @param selection - The user selection as a tree object + * @param lnClass - the logical node class of the `LNodeType` + * @returns an array of inserts for the `LNodeType` and the missing subs data + */ +export function insertSelectedLNodeType( + doc: XMLDocument, + selection: TreeSelection, + lnClass: string, +): Insert[] { + const types = new Set(); + const elements: Templates = { + LNodeType: [], + DOType: [], + DAType: [], + EnumType: [], + }; + + const lnData = nsdToJson(lnClass); + if (!lnData) return []; + + function isUnknownId(id: string): boolean { + const alreadyCreate = types.has(id); + const alreadyExist = !!doc.querySelector( + `:root > DataTypeTemplates > *[id="${id}"]`, + ); + + return !alreadyCreate && !alreadyExist; + } + + function identify(element: Element, name: string): string { + const hash = hashElement(element); + const id = `${name}$oscd$_${hash}`; + + element.setAttribute("id", id); + if (isUnknownId(id)) { + types.add(id); + elements[ + element.tagName as "LNodeType" | "DOType" | "DAType" | "EnumType" + ]?.push(element); + } + + return id; + } + + const lnType = createElement(doc, "LNodeType", { lnClass }); + + function createEnumType(path: string[], sel: TreeSelection): string { + const enumData = data(lnData, path).children; + + const vals: Element[] = []; + + for (const content of Object.keys(sel)) { + const ord = enumData[content].literalVal; + const val = createElement(doc, "EnumVal", { ord }); + val.textContent = content; + vals.push(val); + } + + vals.sort( + (v1, v2) => + parseInt(v1.getAttribute("ord")!, 10) - + parseInt(v2.getAttribute("ord")!, 10), + ); + + const enumType = createElement(doc, "EnumType", {}); + vals.forEach((val) => enumType.append(val)); + + const dataName = path[path.length - 1]; + return identify(enumType, dataName); + } + + function createDAType( + path: string[], + selection: TreeSelection, + underlyingValSel: TreeSelection = {}, // For Oper.ctlVal, SBOw.ctlVal and Cancel.ctlVal + ): string { + const { children, underlyingTypeKind, underlyingType } = data(lnData, path); + + const daType = createElement(doc, "DAType", {}); + + for (const [name, dep] of Object.entries(children) as [ + string, + { + tagName: string; + transient?: string; + fc: string; + dchg?: string; + dupd?: string; + qchg?: string; + typeKind?: "BASIC" | "ENUMERATED" | "CONSTRUCTED" | "undefined"; + type?: string; + }, + ][]) { + if (!selection[name]) continue; + + const bda = createElement(doc, "BDA", { name }); + + if (dep.typeKind === "BASIC" || !dep.typeKind) { + bda.setAttribute("bType", dep.type!); + } + + if (dep.typeKind === "ENUMERATED") { + const enumId = createEnumType(path.concat([name]), selection[name]); + bda.setAttribute("bType", "Enum"); + bda.setAttribute("type", enumId); + } + + if (dep.typeKind === "CONSTRUCTED") { + const daId = createDAType( + path.concat([name]), + selection[name], + underlyingValSel, + ); + bda.setAttribute("bType", "Struct"); + bda.setAttribute("type", daId); + } + + // For Oper, SBOw and Cancel only + if (dep.typeKind === "undefined") { + if (underlyingTypeKind === "BASIC") + // For all but APC and ENC + bda.setAttribute("bType", underlyingType); + else if (underlyingTypeKind === "ENUMERATED") { + // For ENC type -> enumeration is equal to parent stVal + const enumId = createEnumType( + path.slice(0, -1).concat(["stVal"]), + underlyingValSel, + ); + bda.setAttribute("bType", "Enum"); + bda.setAttribute("type", enumId); + } else if (underlyingTypeKind === "CONSTRUCTED") { + // For APC type -> AnalogueValue is equal to parent mxVal + const daId = createDAType( + path.slice(0, -1).concat(["mxVal"]), + underlyingValSel, + ); + bda.setAttribute("bType", "Struct"); + bda.setAttribute("type", daId); + } + } + + daType.append(bda); + } + + return identify(daType, path[path.length - 1]); + } + + function createDOType(path: string[], selection: TreeSelection): string { + const dO = data(lnData, path); + + const doType = createElement(doc, "DOType", { cdc: dO.type }); + + const deps: [ + string, + { + tagName: string; + transient?: string; + fc: string; + dchg?: string; + dupd?: string; + qchg?: string; + typeKind?: "BASIC" | "ENUMERATED" | "CONSTRUCTED" | "undefined"; + type?: string; + }, + ][] = Object.entries(dO.children); + + for (const [name, dep] of deps) { + if (!selection[name]) continue; + + if (dep.tagName === "SubDataObject") { + const { transient } = dep; + const type = createDOType(path.concat([name]), selection[name]); + const sdo = createElement(doc, "SDO", { name, transient, type }); + doType.prepend(sdo); + } else { + const { fc, dchg, dupd, qchg } = dep; + + const da = createElement(doc, "DA", { name, fc, dchg, dupd, qchg }); + + if (dep.typeKind === "BASIC" || !dep.typeKind) { + da.setAttribute("bType", dep.type!); + } + + if (dep.typeKind === "ENUMERATED") { + const enumId = createEnumType(path.concat([name]), selection[name]); + da.setAttribute("bType", "Enum"); + da.setAttribute("type", enumId); + } + + if (dep.typeKind === "CONSTRUCTED") { + const underlyingVal = selection.stVal || selection.mxVal; + const daId = createDAType( + path.concat([name]), + selection[name], + underlyingVal, + ); + da.setAttribute("bType", "Struct"); + da.setAttribute("type", daId); + } + + doType.append(da); + } + } + + return identify(doType, path[path.length - 1]); + } + + Object.keys(selection).forEach((name) => { + const type = createDOType([name], selection[name]); + + const { transient } = lnData[name]; + + const doElement = createElement(doc, "DO", { name, type, transient }); + + lnType.append(doElement); + }); + + identify(lnType, lnClass); + + const dataTypeTemplates: Element = + (doc.querySelector(":root > DataTypeTemplates") as Element) || + createElement(doc, "DataTypeTemplates", {}); + + const inserts: Insert[] = []; + if (!dataTypeTemplates.parentElement) { + inserts.push({ + parent: doc.documentElement, + node: dataTypeTemplates, + reference: getReference(doc.documentElement, "DataTypeTemplates"), + }); + } + + [ + ...elements.LNodeType, + ...elements.DOType, + ...elements.DAType, + ...elements.EnumType, + ].forEach((dataType) => { + if (!doc.querySelector(`${dataType.tagName}[id="${dataType.id}"]`)) { + const reference = getReference(dataTypeTemplates, dataType.tagName); + inserts.push({ parent: dataTypeTemplates, node: dataType, reference }); + } + }); + + return inserts; +} diff --git a/tDataTypeTemplates/lNodeTypeToSelection.ts b/tDataTypeTemplates/lNodeTypeToSelection.ts index 2088120..ce4591c 100644 --- a/tDataTypeTemplates/lNodeTypeToSelection.ts +++ b/tDataTypeTemplates/lNodeTypeToSelection.ts @@ -1,13 +1,10 @@ -/** A object to store a tree structure in */ -export type TreeSelection = { - [name: string]: TreeSelection; -}; +import { TreeSelection } from "../foundation/utils.js"; function enumeration(daOrBda: Element): TreeSelection { const selection: TreeSelection = {}; const enumType = daOrBda.ownerDocument.querySelector( - `EnumType[id="${daOrBda.getAttribute("type")}"]` + `EnumType[id="${daOrBda.getAttribute("type")}"]`, ); if (!enumType) return selection; @@ -15,7 +12,7 @@ function enumeration(daOrBda: Element): TreeSelection { (enumVal) => { const val = enumVal.textContent?.trim(); if (val) selection[val] = {}; - } + }, ); return selection; @@ -25,7 +22,7 @@ function dataAttribute(daOrBda: Element): TreeSelection { const selection: TreeSelection = {}; const doType = daOrBda.ownerDocument.querySelector( - `DAType[id="${daOrBda.getAttribute("type")}"]` + `DAType[id="${daOrBda.getAttribute("type")}"]`, ); if (!doType) return selection; @@ -47,7 +44,7 @@ function dataObject(dOorSdo: Element): TreeSelection { const selection: TreeSelection = {}; const doType = dOorSdo.ownerDocument.querySelector( - `:scope > DataTypeTemplates > DOType[id="${dOorSdo.getAttribute("type")}"]` + `:scope > DataTypeTemplates > DOType[id="${dOorSdo.getAttribute("type")}"]`, ); if (!doType) return selection; @@ -64,7 +61,7 @@ function dataObject(dOorSdo: Element): TreeSelection { selection[name] = enumeration(sDOorDA); else selection[name] = {}; } - } + }, ); return selection;