- ${typeIcon(this.element)}
+ ${getIcon(this.element)}
${this.readonly
? html``
: html`
(
window.addEventListener('keydown', moveToTarget, true);
}
+/**
+ * Get the correct icon for a specific Conducting Equipment.
+ * @param condEq - The Conducting Equipment to search the icon for.
+ * @returns The icon.
+ */
+export function getIcon(condEq: Element): TemplateResult {
+ return typeIcons[typeStr(condEq)] ?? generalConductingEquipmentIcon;
+}
+
+function typeStr(condEq: Element): string {
+ if (
+ condEq.getAttribute('type') === 'DIS' &&
+ condEq.querySelector('Terminal')?.getAttribute('cNodeName') === 'grounded'
+ ) {
+ return 'ERS';
+ } else {
+ return condEq.getAttribute('type') ?? '';
+ }
+}
+
+const typeIcons: Partial> = {
+ CBR: circuitBreakerIcon,
+ DIS: disconnectorIcon,
+ CTR: currentTransformerIcon,
+ VTR: voltageTransformerIcon,
+ ERS: earthSwitchIcon,
+};
+
// Substation element hierarchy
const substationPath = [
':root',
@@ -231,19 +266,11 @@ export const selectors = >(
/** Common `CSS` styles used by substation subeditors */
export const styles = css`
- :host {
- transition: opacity 200ms linear;
- }
-
abbr {
text-decoration: none;
border-bottom: none;
}
- .moving {
- opacity: 0.3;
- }
-
#iedcontainer {
display: grid;
grid-gap: 12px;
diff --git a/test/testfiles/valid2007B4withSubstationXY.scd b/test/testfiles/valid2007B4withSubstationXY.scd
new file mode 100644
index 0000000000..2182810ede
--- /dev/null
+++ b/test/testfiles/valid2007B4withSubstationXY.scd
@@ -0,0 +1,716 @@
+
+
+
+ TrainingIEC61850
+
+
+
+
+
+
+
+
+
+
+
+
+
+ b80686f1-a514-477b-a83b-78f1cbe8a582
+ 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100.0
+
+
+ 192.168.210.111
+ 255.255.255.0
+ 192.168.210.1
+ 1,3,9999,23
+ 23
+ 00000001
+ 0001
+ 0001
+
+
+
+ 01-0C-CD-01-00-10
+ 005
+ 4
+ 0010
+
+
+
+ RJ45
+
+
+
+
+
+
+ 192.168.0.112
+ 255.255.255.0
+ 192.168.210.1
+ 1,3,9999,23
+ 23
+ 00000001
+ 0001
+ 0001
+
+
+
+
+ 192.168.0.113
+ 255.255.255.0
+ 192.168.210.1
+ 1,3,9999,23
+ 23
+ 00000001
+ 0001
+ 0001
+
+
+
+ 01-0C-CD-04-00-20
+ 007
+ 4
+ 4002
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IED2
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+ 1
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+ direct-with-normal-security
+
+
+
+
+
+
+ sbo-with-normal-security
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+ direct-with-enhanced-security
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IEC 61850-8-1:2003
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+ direct-with-normal-security
+ sbo-with-normal-security
+ direct-with-enhanced-security
+ sbo-with-enhanced-security
+
+
+ on
+ blocked
+ test
+ test/blocked
+ off
+
+
+ Ok
+ Warning
+ Alarm
+
+
+ not-supported
+ bay-control
+ station-control
+ remote-control
+ automatic-bay
+ automatic-station
+ automatic-remote
+ maintenance
+ process
+
+
+
diff --git a/test/unit/editors/singlelinediagram/foundation.test.ts b/test/unit/editors/singlelinediagram/foundation.test.ts
new file mode 100644
index 0000000000..e9d4425798
--- /dev/null
+++ b/test/unit/editors/singlelinediagram/foundation.test.ts
@@ -0,0 +1,120 @@
+import { expect } from '@open-wc/testing';
+import {
+ getRelativeCoordinates,
+ getDescriptionAttribute,
+ getNameAttribute,
+ getPathNameAttribute,
+ isBusBar,
+ getConnectedTerminals,
+ calculateConnectivityNodeCoordinates,
+} from '../../../../src/editors/singlelinediagram/foundation.js';
+
+describe('Single Line Diagram foundation', () => {
+ let doc: Document;
+ beforeEach(async () => {
+ doc = await fetch('/base/test/testfiles/valid2007B4withSubstationXY.scd')
+ .then(response => response.text())
+ .then(str => new DOMParser().parseFromString(str, 'application/xml'));
+ });
+
+ describe('defines a getNameAttribute function that', () => {
+ it('returns the correct name for an element.', () => {
+ const element = doc.querySelector('Bay[desc="Feld A"]');
+ expect(getNameAttribute(element!)).to.eql('Bay A');
+ });
+ it('returns undefined for an element without a name.', () => {
+ const element = doc.querySelector('VoltageLevel[name="J1"] > Voltage');
+ expect(getNameAttribute(element!)).to.be.undefined;
+ });
+ });
+
+ describe('defines a getDescriptionAttribute function that', () => {
+ it('returns the correct description for an element.', () => {
+ const element = doc.querySelector('Bay[name="Bay A"]');
+ expect(getDescriptionAttribute(element!)).to.eql('Feld A');
+ });
+ it('returns undefined for an element without a description.', () => {
+ const element = doc.querySelector('VoltageLevel[name="J1"] > Voltage');
+ expect(getDescriptionAttribute(element!)).to.be.undefined;
+ });
+ });
+
+ describe('defines a getPathName function that', () => {
+ it('returns the correct path name for an element.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConnectivityNode[name="L1"]'
+ );
+ expect(getPathNameAttribute(element!)).to.eql('AA1/J1/Bay A/L1');
+ });
+ it('returns undefined for an element without a pathName.', () => {
+ const element = doc.querySelector('VoltageLevel[name="J1"] > Voltage');
+ expect(getPathNameAttribute(element!)).to.be.undefined;
+ });
+ });
+
+ describe('defines a getRelativeCoordinates function that', () => {
+ it('returns the correct x and y coordinates for an element.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConductingEquipment[name="QB1"]'
+ );
+ expect(getRelativeCoordinates(element!)).to.eql({ x: 1, y: 1 });
+ });
+ it("returns {x: 0, y: 0} coordinates for an element that hasn't got any coordinates.", () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConnectivityNode[name="L1"]'
+ );
+ expect(getRelativeCoordinates(element!)).to.eql({ x: 0, y: 0 });
+ });
+ });
+
+ describe('defines an isBusBar function that', () => {
+ it('returns true if an element is a bus bar.', () => {
+ const element = doc.querySelector('Bay[name="BusBar A"]');
+ expect(isBusBar(element!)).to.be.true;
+ });
+ it('returns false if an element is not a bus bar.', () => {
+ const element = doc.querySelector('Bay[name="Bay A"]');
+ expect(isBusBar(element!)).to.be.false;
+ });
+ });
+
+ describe('defines a getConnectedTerminals function that', () => {
+ it('calculates the total number of connected terminals for a single element within the same bay.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConnectivityNode[name="L1"]'
+ );
+ expect(getConnectedTerminals(element!).length).to.eql(3);
+ });
+ it('calculates the total number of connected terminals for a single element connected to multiple bays.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="BusBar A"] > ConnectivityNode[name="L1"]'
+ );
+ expect(getConnectedTerminals(element!).length).to.eql(4);
+ });
+ });
+
+ describe('defines a calculateConnectivityNodeCoordinates function that', () => {
+ it(
+ 'calculates the x and y coordinates of an element without defined coordinates,' +
+ 'based on the coordinates of connected elements.',
+ () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConnectivityNode[name="L1"]'
+ );
+ expect(calculateConnectivityNodeCoordinates(element!)).to.eql({
+ x: Math.round((4 + 5 + 4) / 3),
+ y: Math.round((10 + 10 + 12) / 3),
+ });
+ }
+ );
+ it("returns a default {x:0, y:0} for elements that aren't Connectivity Nodes", () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConductingEquipment[name="QB1"]'
+ );
+ expect(calculateConnectivityNodeCoordinates(element!)).to.eql({
+ x: 0,
+ y: 0,
+ });
+ });
+ });
+});
diff --git a/test/unit/editors/singlelinediagram/sld-drawing.test.ts b/test/unit/editors/singlelinediagram/sld-drawing.test.ts
new file mode 100644
index 0000000000..d5fb72e0a0
--- /dev/null
+++ b/test/unit/editors/singlelinediagram/sld-drawing.test.ts
@@ -0,0 +1,92 @@
+import { expect } from '@open-wc/testing';
+import {
+ createPowerTransformerElement,
+ getAbsolutePosition,
+ getBusBarLength,
+ getParentElementName,
+ SVG_GRID_SIZE,
+} from '../../../../src/editors/singlelinediagram/sld-drawing.js';
+
+describe('Single Line Diagram drawing', () => {
+ let doc: Document;
+ beforeEach(async () => {
+ doc = await fetch('/base/test/testfiles/valid2007B4withSubstationXY.scd')
+ .then(response => response.text())
+ .then(str => new DOMParser().parseFromString(str, 'application/xml'));
+ });
+
+ describe('defines a getAbsolutePosition function that', () => {
+ it('returns the correct absolute position for an element with a Bay as parent.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConductingEquipment[name="QB1"]'
+ );
+ // Absolute position of QB1 should be x=(1 + 1 + 1), and y=(1 + 6 + 3), if looking at the coordinates of all the parents.
+ // Times the SVG_GRID_SIZE to get the absolute position on the svg.
+ expect(getAbsolutePosition(element!)).to.eql({
+ x: 3 * SVG_GRID_SIZE,
+ y: 10 * SVG_GRID_SIZE,
+ });
+ });
+
+ it('returns the correct absolute position for an element with a VoltageLevel as parent.', () => {
+ const element = doc.querySelector('Bay[name="BusBar A"]');
+ // Absolute position of Busbar A should be x=(1 + 1), and y=(1 + 3), if looking at the coordinates of all the parents.
+ // Times the SVG_GRID_SIZE to get the absolute position on the svg.
+ expect(getAbsolutePosition(element!)).to.eql({
+ x: 2 * SVG_GRID_SIZE,
+ y: 4 * SVG_GRID_SIZE,
+ });
+ });
+
+ it('returns relative position elements without legal parent.', () => {
+ const element = doc.querySelector('VoltageLevel[name="J1"]');
+ const copiedElement = element?.cloneNode();
+
+ expect(getAbsolutePosition(copiedElement!)).to.eql({
+ x: 1 * SVG_GRID_SIZE,
+ y: 3 * SVG_GRID_SIZE,
+ });
+ });
+
+ it('returns default for invalid elements.', () => {
+ const element = doc.querySelector('LDevice');
+
+ expect(getAbsolutePosition(element!)).to.eql({
+ x: 0,
+ y: 0,
+ });
+ });
+ });
+
+ describe('defines a getParentElementName function that', () => {
+ it('returns the correct parent of an element.', () => {
+ const element = doc.querySelector(
+ 'Bay[name="Bay A"] > ConnectivityNode[name="L2"]'
+ );
+ expect(getParentElementName(element!)).to.eql('Bay A');
+ });
+ it('returns undefined for an element without a parent.', () => {
+ const element = doc.querySelector('Substation');
+ expect(getParentElementName(element!)).to.be.undefined;
+ });
+ });
+
+ describe('defines a getBusBarLength function that', () => {
+ it('returns a correct length for the bus bar given voltage level as root', () => {
+ const element = doc.querySelector('VoltageLevel[name="J1"]') ?? doc;
+ expect(getBusBarLength(element)).to.eql(
+ 18 * SVG_GRID_SIZE + SVG_GRID_SIZE
+ );
+ });
+ it('returns a correct length for the bus bar given XMLDocument as root', () => {
+ expect(getBusBarLength(doc)).to.eql(18 * SVG_GRID_SIZE + SVG_GRID_SIZE);
+ });
+ });
+
+ describe('creates a group element for every given PowerTransformer element that', () => {
+ it('looks like its latest snapshot', () => {
+ const pTrans = doc.querySelector('PowerTransformer')!;
+ expect(createPowerTransformerElement(pTrans)).to.equalSnapshot();
+ });
+ });
+});