Skip to content

Commit

Permalink
fix: supervision updates after ied rename
Browse files Browse the repository at this point in the history
Signed-off-by: Stef3st <[email protected]>
  • Loading branch information
Stef3st committed Oct 24, 2023
1 parent a737c42 commit f185a86
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 76 deletions.
258 changes: 193 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,13 +240,86 @@ 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') updateVals(element, oldName, newName, actions);

This comment has been minimized.

Copy link
@JakobVogelsang

JakobVogelsang Oct 30, 2023

Collaborator

This is a very unusual pattern within OpenSCD. I would have expected that the function is returning and action array, and you push it into actions like so actions.push(...updateVals(element, oldName, newName)). I for my part had hard time to read it.

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.
* @param actions - Array of Replace actions from the updateReferences function. The Val elements will be added to this array.
*/
function updateVals(
element: Element,
oldName: string | null,
newName: string,
actions: Replace[]
) {
// Each IED will be checked for the extRef elements. So firstly all the IEDs will be gathered with the querySelectorAll function.

This comment has been minimized.

Copy link
@JakobVogelsang

JakobVogelsang Oct 30, 2023

Collaborator

Is this comment really necessary? The line before that reads very close to what you have commented here.

const ieds = element.ownerDocument.querySelectorAll('IED');
ieds.forEach(ied => {

This comment has been minimized.

Copy link
@JakobVogelsang

JakobVogelsang Oct 30, 2023

Collaborator

flatMap might be an option that is a bit more readable.

// 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(
`LN[lnClass="LGOS"] > DOI > DAI > Val, LN[lnClass="LSVS"] > DOI > DAI > Val[textContent*="${oldName}"]`

This comment has been minimized.

Copy link
@JakobVogelsang

JakobVogelsang Oct 30, 2023

Collaborator

Why is the LSVS scoped to Val with oldName and LGOS is not?

This comment has been minimized.

Copy link
@JakobVogelsang

JakobVogelsang Oct 30, 2023

Collaborator

The information within Val is not in the attribute, but in the text content. Here you are filtering for the attribute textContent that does never exist and as a result you are not filtering at all

)
);

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

// The extRef elements are gathered that contain the to-be-changed IED name and has a srcCBName will be gathered.
// For each of these elements a controlblockreferences will be created.
const extRefs = Array.from(
ied.querySelectorAll(
'AccessPoint > Server > LDevice > LN0 > Inputs > ExtRef'
)
).filter(
ref =>
ref.getAttribute('iedName') === oldName && ref.getAttribute('srcCBName')
);

extRefLoop: for (let ref of extRefs) {
const suffixCBReference =
ref.getAttribute('srcLDInst') +
'/' +
ref.getAttribute('srcLNClass') +
'.' +
ref.getAttribute('srcCBName');

// The found Val elements will be compared by the constructed controlblockreferences.
// The textContent of the Val element will be changed to the new IED name and will be added
// to the Replace action array. Only one Val element should be changed. This means that if a match
// is found, the loop should break.
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 extRefLoop;
}
}
}
});
}

/**
* Function to create Delete actions to remove reference which point to the name of the element being removed.
* For instance the IED Name is used in other SCL Elements as attribute 'iedName' to reference the IED.
Expand Down Expand Up @@ -225,8 +348,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 +359,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 f185a86

Please sign in to comment.