-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
Signed-off-by: Stef3st <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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}"]`; | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -88,7 +114,7 @@ function simpleAttributeFilter(tagName: string) { | |
function simpleTextContentFilter(elementQuery: string) { | ||
return function filter(): string { | ||
return `${elementQuery}`; | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -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}"]`; | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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 []; | ||
} | ||
|
@@ -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. | ||
|
@@ -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.
Sorry, something went wrong. |
||
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.
Sorry, something went wrong.
JakobVogelsang
Collaborator
|
||
const ieds = element.ownerDocument.querySelectorAll('IED'); | ||
ieds.forEach(ied => { | ||
This comment has been minimized.
Sorry, something went wrong. |
||
// 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.
Sorry, something went wrong.
JakobVogelsang
Collaborator
|
||
) | ||
); | ||
|
||
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. | ||
|
@@ -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. | ||
|
@@ -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; | ||
} |
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 soactions.push(...updateVals(element, oldName, newName))
. I for my part had hard time to read it.