diff --git a/package-lock.json b/package-lock.json index a629d5d50..18c1ac85a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32719,7 +32719,7 @@ }, "packages/core": { "name": "@openscd/core", - "version": "0.1.2", + "version": "0.1.3", "license": "Apache-2.0", "dependencies": { "@lit/localize": "^0.11.4", diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index c7cd164a4..60b3a30ad 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -34,3 +34,11 @@ export type { EditCompletedEvent, EditCompletedDetail, } from './foundation/edit-completed-event.js'; + +/** @returns the cartesian product of `arrays` */ +export function crossProduct(...arrays: T[][]): T[][] { + return arrays.reduce( + (a, b) => a.flatMap(d => b.map(e => [d, e].flat())), + [[]] + ); +} diff --git a/packages/core/foundation/scl.ts b/packages/core/foundation/scl.ts new file mode 100644 index 000000000..966d47ead --- /dev/null +++ b/packages/core/foundation/scl.ts @@ -0,0 +1,89 @@ +import { crossProduct } from '../foundation.js'; + +function getDataModelChildren(parent: Element): Element[] { + if (['LDevice', 'Server'].includes(parent.tagName)) + return Array.from(parent.children).filter( + child => + child.tagName === 'LDevice' || + child.tagName === 'LN0' || + child.tagName === 'LN' + ); + + const id = + parent.tagName === 'LN' || parent.tagName === 'LN0' + ? parent.getAttribute('lnType') + : parent.getAttribute('type'); + + return Array.from( + parent.ownerDocument.querySelectorAll( + `LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA` + ) + ); +} + +export function existFcdaReference(fcda: Element, ied: Element): boolean { + const [ldInst, prefix, lnClass, lnInst, doName, daName, fc] = [ + 'ldInst', + 'prefix', + 'lnClass', + 'lnInst', + 'doName', + 'daName', + 'fc', + ].map(attr => fcda.getAttribute(attr)); + + const sinkLdInst = ied.querySelector(`LDevice[inst="${ldInst}"]`); + if (!sinkLdInst) return false; + + const prefixSelctors = prefix + ? [`[prefix="${prefix}"]`] + : ['[prefix=""]', ':not([prefix])']; + const lnInstSelectors = lnInst + ? [`[inst="${lnInst}"]`] + : ['[inst=""]', ':not([inst])']; + + const anyLnSelector = crossProduct( + ['LN0', 'LN'], + prefixSelctors, + [`[lnClass="${lnClass}"]`], + lnInstSelectors + ) + .map(strings => strings.join('')) + .join(','); + + const sinkAnyLn = ied.querySelector(anyLnSelector); + if (!sinkAnyLn) return false; + + const doNames = doName?.split('.'); + if (!doNames) return false; + + let parent: Element | undefined = sinkAnyLn; + for (const doNameAttr of doNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === doNameAttr + ); + if (!parent) return false; + } + + const daNames = daName?.split('.'); + const someFcInSink = getDataModelChildren(parent).some( + da => da.getAttribute('fc') === fc + ); + if (!daNames && someFcInSink) return true; + if (!daNames) return false; + + let sinkFc = ''; + for (const daNameAttr of daNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === daNameAttr + ); + + if (parent?.getAttribute('fc')) sinkFc = parent.getAttribute('fc')!; + + if (!parent) return false; + } + + if (sinkFc !== fc) return false; + + return true; +} diff --git a/packages/core/package.json b/packages/core/package.json index 4d6cbe777..5fe733aab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,6 +13,7 @@ ], "exports": { ".": "./dist/foundation.js", + "./foundation/scl.js": "./dist/foundation/scl.js", "./foundation/deprecated/editor.js": "./dist/foundation/deprecated/editor.js", "./foundation/deprecated/open-event.js": "./dist/foundation/deprecated/open-event.js", "./foundation/deprecated/settings.js": "./dist/foundation/deprecated/settings.js", @@ -159,4 +160,4 @@ "prettier --write" ] } -} +} \ No newline at end of file