Skip to content

Commit

Permalink
fix: supervision updates after ied rename (#1338)
Browse files Browse the repository at this point in the history
* fix: supervision updates after ied rename

Signed-off-by: Stef3st <[email protected]>

* chore: improve efficiency and tidy up

Signed-off-by: Stef3st <[email protected]>

---------

Signed-off-by: Stef3st <[email protected]>
  • Loading branch information
Stef3st authored Nov 2, 2023
1 parent 1c2ef60 commit 2066e4c
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 76 deletions.
246 changes: 181 additions & 65 deletions packages/open-scd/src/wizards/foundation/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import {
Delete,
getNameAttribute,
isPublic,
Replace
} from "../../foundation.js";
Replace,
} from '../../foundation.js';

const referenceInfoTags = ['IED', 'Substation', 'VoltageLevel', 'Bay'] as const;
type ReferencesInfoTag = typeof referenceInfoTags[number];

type FilterFunction = (element: Element, attributeName: string | null, oldName: string | null) => string;
type FilterFunction = (
element: Element,
attributeName: string | null,
oldName: string | null
) => string;

/*
* For every supported tag a list of information about which elements to search for and which attribute value
Expand All @@ -23,60 +27,82 @@ const referenceInfos: Record<
filter: FilterFunction;
}[]
> = {
IED:
[{
IED: [
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`Association`)
}, {
filter: simpleAttributeFilter(`Association`),
},
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`ClientLN`)
}, {
filter: simpleAttributeFilter(`ClientLN`),
},
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`ConnectedAP`)
}, {
filter: simpleAttributeFilter(`ConnectedAP`),
},
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`ExtRef`)
}, {
filter: simpleAttributeFilter(`ExtRef`),
},
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`KDC`)
}, {
filter: simpleAttributeFilter(`KDC`),
},
{
attributeName: 'iedName',
filter: simpleAttributeFilter(`LNode`)
}, {
filter: simpleAttributeFilter(`LNode`),
},
{
attributeName: null,
filter: simpleTextContentFilter(`GSEControl > IEDName`)
}, {
filter: simpleTextContentFilter(`GSEControl > IEDName`),
},
{
attributeName: null,
filter: simpleTextContentFilter(`SampledValueControl > IEDName`)
}],
Substation:
[{
filter: simpleTextContentFilter(`SampledValueControl > IEDName`),
},
{
attributeName: null,
filter: simpleTextContentFilter(`LN > DOI > DAI > Val`),
},
],
Substation: [
{
attributeName: 'substationName',
filter: simpleAttributeFilter(`Terminal`)
}],
VoltageLevel:
[{
filter: simpleAttributeFilter(`Terminal`),
},
],
VoltageLevel: [
{
attributeName: 'voltageLevelName',
filter: attributeFilterWithParentNameAttribute(`Terminal`,
{'Substation': 'substationName'})
}],
Bay:
[{
filter: attributeFilterWithParentNameAttribute(`Terminal`, {
Substation: 'substationName',
}),
},
],
Bay: [
{
attributeName: 'bayName',
filter: attributeFilterWithParentNameAttribute(`Terminal`,
{'Substation': 'substationName', 'VoltageLevel': 'voltageLevelName'})
}],
}
filter: attributeFilterWithParentNameAttribute(`Terminal`, {
Substation: 'substationName',
VoltageLevel: 'voltageLevelName',
}),
},
],
};

/**
* Simple function to create a filter to find Elements where the value of an attribute equals the old name.
*
* @param tagName - The tagName of the elements to search for.
*/
function simpleAttributeFilter(tagName: string) {
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
return function filter(
element: Element,
attributeName: string | null,
oldName: string | null
): string {
return `${tagName}[${attributeName}="${oldName}"]`;
}
};
}

/**
Expand All @@ -88,7 +114,7 @@ function simpleAttributeFilter(tagName: string) {
function simpleTextContentFilter(elementQuery: string) {
return function filter(): string {
return `${elementQuery}`;
}
};
}

/**
Expand All @@ -105,19 +131,28 @@ function simpleTextContentFilter(elementQuery: string) {
* @param parentInfo - The records of parent to search for, the key is the tagName of the parent, the value
* is the name of the attribuet to use in the query.
*/
function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Record<string, string>) {
return function filter(element: Element, attributeName: string | null, oldName: string | null): string {
return `${tagName}${Object.entries(parentInfo)
.map(([parentTag, parentAttribute]) => {
const parentElement = element.closest(parentTag);
if (parentElement && parentElement.hasAttribute('name')) {
const name = parentElement.getAttribute('name');
return `[${parentAttribute}="${name}"]`;
}
return null;
}).join('') // Join the strings to 1 string without a separator.
function attributeFilterWithParentNameAttribute(
tagName: string,
parentInfo: Record<string, string>
) {
return function filter(
element: Element,
attributeName: string | null,
oldName: string | null
): string {
return `${tagName}${
Object.entries(parentInfo)
.map(([parentTag, parentAttribute]) => {
const parentElement = element.closest(parentTag);
if (parentElement && parentElement.hasAttribute('name')) {
const name = parentElement.getAttribute('name');
return `[${parentAttribute}="${name}"]`;
}
return null;
})
.join('') // Join the strings to 1 string without a separator.
}[${attributeName}="${oldName}"]`;
}
};
}

/**
Expand All @@ -129,7 +164,11 @@ function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Rec
* @param value - The value to set on the cloned element or if null remove the attribute.
* @returns Returns the cloned element.
*/
function cloneElement(element: Element, attributeName: string, value: string): Element {
function cloneElement(
element: Element,
attributeName: string,
value: string
): Element {
const newElement = <Element>element.cloneNode(false);
newElement.setAttribute(attributeName, value);
return newElement;
Expand All @@ -142,7 +181,10 @@ function cloneElement(element: Element, attributeName: string, value: string): E
* @param value - The value to set.
* @returns Returns the cloned element.
*/
function cloneElementAndTextContent(element: Element, value: string | null): Element {
function cloneElementAndTextContent(
element: Element,
value: string | null
): Element {
const newElement = <Element>element.cloneNode(false);
newElement.textContent = value;
return newElement;
Expand All @@ -160,7 +202,11 @@ function cloneElementAndTextContent(element: Element, value: string | null): Ele
* @param newName - The new name of the element.
* @returns Returns a list of Replace Actions that can be added to a Complex Action or returned directly for execution.
*/
export function updateReferences(element: Element, oldName: string | null, newName: string): Replace[] {
export function updateReferences(
element: Element,
oldName: string | null,
newName: string
): Replace[] {
if (oldName === null || oldName === newName) {
return [];
}
Expand All @@ -179,9 +225,13 @@ export function updateReferences(element: Element, oldName: string | null, newNa
Array.from(element.ownerDocument.querySelectorAll(`${filter}`))
.filter(isPublic)
.forEach(element => {
const newElement = cloneElement(element, info.attributeName!, newName);
actions.push({old: {element}, new: {element: newElement}});
})
const newElement = cloneElement(
element,
info.attributeName!,
newName
);
actions.push({ old: { element }, new: { element: newElement } });
});
} else {
// If the text content needs to be updated, filter on the text content can't be done in a CSS Selector.
// So we query all elements the may need to be updated and filter them afterwards.
Expand All @@ -190,10 +240,71 @@ export function updateReferences(element: Element, oldName: string | null, newNa
.filter(isPublic)
.forEach(element => {
const newElement = cloneElementAndTextContent(element, newName);
actions.push({old: {element}, new: {element: newElement}});
})
actions.push({ old: { element }, new: { element: newElement } });
});
}
})
});

if (element.tagName === 'IED')
actions.push(...updateVals(element, oldName, newName));
return actions;
}

/**
* Adds Replace actions to update supervision references.
* Only a maximum of one Val element per IED with ExtRef elements that contain src attributes will be altered.
* The Val element that needs to be altered will be found by checking if the controlBlockReference complies with this element.
* The controlBlockReference needs to contain the IED that gets renamed.
*
* @param element - The element for which the name is updated.
* @param oldName - The old name of the element.
* @param newName - The new name of the element.
*/
function updateVals(element: Element, oldName: string | null, newName: string) {
const actions: Replace[] = [];
const ieds = element.ownerDocument.querySelectorAll('IED');
ieds.forEach(ied => {
// All Val elements inside LGOS and LSVS lnClasses that starts with the IED name that needs to be changed will be gathered.
// Because of a very rare case where multiple IED start with the same name, all will be gathered.
// If none are found continue to the next IED.
const valValues: Element[] = Array.from(
ied.querySelectorAll(
`:scope > AccessPoint > Server > LDevice > LN[lnClass="LGOS"] > DOI > DAI > Val, :scope > AccessPoint > Server > LDevice > LN[lnClass="LSVS"] > DOI > DAI > Val`
)
);

if (valValues.length === 0) return;

// If atleast one extRef element contains the to-be-changed IED name and has a srcCBName, one will be gathered.
// From that extRef element a controlblockreferences will be created and compared to the Val elements.
// If a match is found, the name of that Val element will be changed accordingly and the loop will be broken, as only 1 Val element need to be changed.

const ref = ied.querySelector(
`:scope > AccessPoint > Server > LDevice > LN0 > Inputs > ExtRef[iedName="${oldName}"][srcCBName]`
);

const suffixCBReference =
ref?.getAttribute('srcLDInst') +
'/' +
ref?.getAttribute('srcLNClass') +
'.' +
ref?.getAttribute('srcCBName');

for (let value of valValues) {
if (oldName + suffixCBReference === value.textContent!.trim()) {
const newElement = cloneElementAndTextContent(
value,
newName + suffixCBReference
);
actions.push({
old: { element: value },
new: { element: newElement },
});
break;
}
}
});

return actions;
}

Expand Down Expand Up @@ -225,8 +336,8 @@ export function deleteReferences(element: Element): Delete[] {
Array.from(element.ownerDocument.querySelectorAll(`${filter}`))
.filter(isPublic)
.forEach(element => {
actions.push({old: { parent: element.parentElement!, element }});
})
actions.push({ old: { parent: element.parentElement!, element } });
});
} else {
// If the text content needs to be used for filtering, filter on the text content can't be done in a CSS Selector.
// So we query all elements the may need to be deleted and filter them afterwards.
Expand All @@ -236,10 +347,15 @@ export function deleteReferences(element: Element): Delete[] {
.forEach(element => {
// We not only need to remove the element containing the text content, but the parent of this element.
if (element.parentElement) {
actions.push({old: {parent: element.parentElement.parentElement!, element: element.parentElement}});
actions.push({
old: {
parent: element.parentElement.parentElement!,
element: element.parentElement,
},
});
}
})
});
}
})
});
return actions;
}
Loading

0 comments on commit 2066e4c

Please sign in to comment.