diff --git a/src/editors/SingleLineDiagram.ts b/src/editors/SingleLineDiagram.ts index 1d19d0cb9b..c402fef320 100644 --- a/src/editors/SingleLineDiagram.ts +++ b/src/editors/SingleLineDiagram.ts @@ -7,8 +7,18 @@ import { state, TemplateResult, } from 'lit-element'; + import panzoom from 'panzoom'; +import { + compareNames, + getDescriptionAttribute, + getNameAttribute, + getPathNameAttribute, + identity, + newWizardEvent, + SCLTag +} from '../foundation.js'; import { getAbsolutePosition, createTerminalElement, @@ -31,6 +41,7 @@ import { isBusBar, getConnectedTerminals, } from './singlelinediagram/foundation.js'; +import { isSCLNamespace } from '../schemas.js'; import { wizards } from '../wizards/wizard-library.js'; import {SingleSelectedEvent} from "@material/mwc-list/mwc-list-foundation"; import {translate} from "lit-translate"; @@ -38,8 +49,6 @@ import {translate} from "lit-translate"; import '@material/mwc-list/mwc-list-item'; import '@material/mwc-select'; import '@material/mwc-textfield'; -import { compareNames, getDescriptionAttribute, getNameAttribute, getPathNameAttribute, identity, newWizardEvent, SCLTag } from '../foundation.js'; -import { isSCLNamespace } from '../schemas.js'; /** * Main class plugin for Single Line Diagram editor. @@ -55,7 +64,7 @@ export default class SingleLineDiagramPlugin extends LitElement { // Container for giving the panzoom to. @query('#panzoom') panzoomContainer!: HTMLElement; // The main canvas to draw everything on. - @query('#svg') svg!: SVGElement; + @query('#svg') svg!: SVGGraphicsElement; private get substations() : Element[] { return Array.from(this.doc.querySelectorAll(':root > Substation')) @@ -156,7 +165,9 @@ export default class SingleLineDiagramPlugin extends LitElement { * @param powerTransformerElement - The PowerTransformer to draw. */ private drawPowerTransformer(parentGroup: SVGElement, powerTransformerElement: Element): void { - const powerTransformerGroup = createPowerTransformerElement(powerTransformerElement); + const powerTransformerGroup = createPowerTransformerElement(powerTransformerElement, + (event: Event) => this.openEditWizard(event, powerTransformerElement!) + ); parentGroup.appendChild(powerTransformerGroup); } @@ -182,11 +193,11 @@ export default class SingleLineDiagramPlugin extends LitElement { this.getVoltageLevels(substationElement) .forEach(voltageLevelElement => { this.getBusBars(voltageLevelElement).forEach( busbarElement => { - this.drawBusBarConnections(substationElement, substationGroup, busbarElement); + this.drawBusBarConnections(substationElement, this.svg, busbarElement); }); this.getBays(voltageLevelElement).forEach( bayElement => { - this.drawBayConnections(substationElement, substationGroup, bayElement); + this.drawBayConnections(substationElement, this.svg, bayElement); }); }); } @@ -223,8 +234,8 @@ export default class SingleLineDiagramPlugin extends LitElement { terminal => terminal.getAttribute('cNodeName') !== 'grounded' ).length !== 0) .forEach(conductingEquipmentElement => { - const conductingEquipmentGroup = createConductingEquipmentElement(conductingEquipmentElement, () => - this.openEditWizard(conductingEquipmentElement!) + const conductingEquipmentGroup = createConductingEquipmentElement(conductingEquipmentElement, + (event: Event) => this.openEditWizard(event, conductingEquipmentElement!) ); bayGroup.appendChild(conductingEquipmentGroup); }); @@ -239,8 +250,8 @@ export default class SingleLineDiagramPlugin extends LitElement { this.getConnectivityNode(bayElement) .filter(cNode => getConnectedTerminals(cNode).length > 0) .forEach(cNode => { - const cNodegroup = createConnectivityNodeElement(cNode, () => - this.openEditWizard(cNode) + const cNodegroup = createConnectivityNodeElement(cNode, + (event: Event) => this.openEditWizard(event, cNode) ); bayGroup.appendChild(cNodegroup); @@ -258,11 +269,12 @@ export default class SingleLineDiagramPlugin extends LitElement { this.getConnectivityNode(bayElement) .forEach(cNode => { this.findEquipment(rootElement, getPathNameAttribute(cNode)) - .forEach(element => { - const sides = getDirections(element, cNode); + .forEach(equipmentElement => { + const commonParentElement = getCommonParentElement(cNode, equipmentElement, bayElement); + const sides = getDirections(equipmentElement, cNode); const elementsTerminalPosition = getAbsolutePositionTerminal( - element, + equipmentElement, sides.startDirection ); @@ -272,7 +284,7 @@ export default class SingleLineDiagramPlugin extends LitElement { ); rootGroup - .querySelectorAll(`g[id="${identity(bayElement)}"]`) + .querySelectorAll(`g[id="${identity(commonParentElement)}"]`) .forEach(eq => drawCNodeConnections( cNodePosition, @@ -281,18 +293,18 @@ export default class SingleLineDiagramPlugin extends LitElement { ) ); - const terminalElement = element.querySelector( + const terminalElement = equipmentElement.querySelector( `Terminal[connectivityNode="${cNode.getAttribute('pathName')}"]` ); const terminal = createTerminalElement( terminalElement!, sides.startDirection, - () => this.openEditWizard(terminalElement!) + (event: Event) => this.openEditWizard(event, terminalElement!) ); rootGroup - .querySelectorAll(`g[id="${identity(element)}"]`) + .querySelectorAll(`g[id="${identity(equipmentElement)}"]`) .forEach(eq => eq.appendChild(terminal)); }); }); @@ -315,7 +327,8 @@ export default class SingleLineDiagramPlugin extends LitElement { * @param busbarElement - The Busbar Element to draw. */ private drawBusBar(parentElement: Element, parentGroup: SVGElement, busbarElement: Element): void { - const busBarGroup = createBusBarElement(busbarElement, getBusBarLength(parentElement)); + const busBarGroup = createBusBarElement(busbarElement, getBusBarLength(parentElement), + (event: Event) => this.openEditWizard(event, busbarElement)); parentGroup.appendChild(busBarGroup); } @@ -331,6 +344,7 @@ export default class SingleLineDiagramPlugin extends LitElement { this.findEquipment(rootElement, pathName) .forEach(element => { + const parentElement = element.parentElement; const elementPosition = getAbsolutePosition(element); const elementsTerminalSide = @@ -351,7 +365,7 @@ export default class SingleLineDiagramPlugin extends LitElement { ); rootGroup - .querySelectorAll(`g[id="${identity(busbarElement)}"]`) + .querySelectorAll(`g[id="${identity(parentElement)}"]`) .forEach(eq => drawBusBarRoute( busbarTerminalPosition, @@ -363,7 +377,7 @@ export default class SingleLineDiagramPlugin extends LitElement { const terminal = createTerminalElement( terminalElement!, elementsTerminalSide, - () => this.openEditWizard(terminalElement!) + (event: Event) => this.openEditWizard(event, terminalElement!) ); rootGroup @@ -394,14 +408,29 @@ export default class SingleLineDiagramPlugin extends LitElement { * Open an Edit wizard for an element. * @param element - The element to show the wizard for. */ - openEditWizard(element: Element): void { + openEditWizard(event: Event, element: Element): void { const wizard = wizards[element.tagName].edit(element); - if (wizard) this.dispatchEvent(newWizardEvent(wizard)); + if (wizard) { + this.dispatchEvent(newWizardEvent(wizard)); + event.stopPropagation(); + } } firstUpdated(): void { - panzoom(this.panzoomContainer); this.drawSubstationElements(); + + // Set the new size of the SVG. + const bbox = this.svg.getBBox(); + this.svg.setAttribute("viewBox", (bbox.x-10)+" "+(bbox.y-10)+" "+(bbox.width+20)+" "+(bbox.height+20)); + this.svg.setAttribute("width", (bbox.width+20) + "px"); + this.svg.setAttribute("height",(bbox.height+20) + "px"); + + panzoom(this.panzoomContainer, { + zoomSpeed: 0.2, + maxZoom: 1.5, + minZoom: 0.2, + initialZoom: 0.5, + }); } onSelect(event: SingleSelectedEvent): void { @@ -465,8 +494,6 @@ export default class SingleLineDiagramPlugin extends LitElement { `; @@ -507,9 +534,11 @@ export default class SingleLineDiagramPlugin extends LitElement { pointer-events: bounding-box; } + g[type='Busbar']:hover, + g[type='ConductingEquipment']:hover, g[type='ConnectivityNode']:hover, - g[type='Terminal']:hover, - g[type='ConductingEquipment']:hover { + g[type='PowerTransformer']:hover, + g[type='Terminal']:hover { outline: 2px dashed var(--mdc-theme-primary); transition: transform 200ms linear, box-shadow 250ms linear; } diff --git a/src/editors/singlelinediagram/foundation.ts b/src/editors/singlelinediagram/foundation.ts index 8360e78823..4a1cd543b8 100644 --- a/src/editors/singlelinediagram/foundation.ts +++ b/src/editors/singlelinediagram/foundation.ts @@ -8,6 +8,8 @@ export interface Point { y: number; } +export const SCL_COORDINATES_NAMESPACE = 'http://www.iec.ch/61850/2003/SCLcoordinates'; + /** Scope factor: the ConnectivityNode allocation algorithm works better with a scale factor which is bigger than 1. */ const COORDINATES_SCALE_FACTOR = 2; @@ -18,11 +20,11 @@ const COORDINATES_SCALE_FACTOR = 2; */ export function getRelativeCoordinates(element: Element): Point { const x = element.getAttributeNS( - 'http://www.iec.ch/61850/2003/SCLcoordinates', + SCL_COORDINATES_NAMESPACE, 'x' ); const y = element.getAttributeNS( - 'http://www.iec.ch/61850/2003/SCLcoordinates', + SCL_COORDINATES_NAMESPACE, 'y' ); @@ -89,8 +91,7 @@ export function getConnectedTerminals(element: Element): Element[] { * - Get all elements that are connected to this Connectivity Node. * - Extract the SCL x and y coordinates of these Connectivity Nodes and add them up. * - Divide the final x and y numbers by the number of connected elements. This way, you get an so-called average. - * @param doc - The full SCL document to scan for connected elements. - * @param cNodePathName - The pathName of the Connectivity Node to calculate the SCL x and y coordinates. + * @param cNodeElement - The Connectivity Node to calculate the X and Y Coordinates for. * @returns The calculated SCL x and y coordinates for this Connectivity Node. */ export function calculateConnectivityNodeCoordinates( @@ -103,6 +104,7 @@ export function calculateConnectivityNodeCoordinates( const pathName = getPathNameAttribute(cNodeElement); let nrOfConnections = 0; + let nrOfXConnections = 0; let totalX = 0; let totalY = 0; @@ -119,7 +121,13 @@ export function calculateConnectivityNodeCoordinates( const { x, y } = getAbsoluteCoordinates(equipment); - totalX += x!; + // Only if the Element is in the same bay, we will use that X-value to calculate the location + // of the Connectivity Node. This will cause the Connectivity Node to stay with the boundaries + // of the Bay and not causing al kind of overlays between bays. + if (equipment.parentElement === cNodeElement.parentElement) { + nrOfXConnections++; + totalX += x!; + } totalY += y!; }); @@ -127,7 +135,18 @@ export function calculateConnectivityNodeCoordinates( if (nrOfConnections === 1) return { x: totalX + 1, y: totalY + 1 }; return { - x: Math.round(totalX / nrOfConnections), + x: Math.round(totalX / nrOfXConnections), y: Math.round(totalY / nrOfConnections), }; } + +export function getCommonParentElement(leftElement: Element, rightElement: Element, defaultParent: Element | null): Element | null { + let leftParentElement = leftElement.parentElement + while (leftParentElement) { + if (leftParentElement.contains(rightElement)) { + return leftParentElement; + } + leftParentElement = leftParentElement.parentElement; + } + return defaultParent; +} diff --git a/src/editors/singlelinediagram/sld-drawing.ts b/src/editors/singlelinediagram/sld-drawing.ts index a4b1bf00c0..3f6d5cd0e5 100644 --- a/src/editors/singlelinediagram/sld-drawing.ts +++ b/src/editors/singlelinediagram/sld-drawing.ts @@ -69,8 +69,8 @@ export function getAbsolutePositionBusBar(busbar: Element): Point { * @param connectivityNode - The SCL element ConnectivityNode to get the position for. * @returns A point containing the full x/y position in px. */ -export function getAbsolutePositionConnectivityNode(element: Element): Point { - const absoluteCoordinates = calculateConnectivityNodeCoordinates(element); +export function getAbsolutePositionConnectivityNode(connectivityNode: Element): Point { + const absoluteCoordinates = calculateConnectivityNodeCoordinates(connectivityNode); return { x: absoluteCoordinates.x! * SVG_GRID_SIZE + (SVG_GRID_SIZE - CNODE_SIZE) / 2, @@ -140,7 +140,7 @@ function absoluteOffsetTerminal( /** * Get the absolute position in py for a equipments Terminal (based on the TERMINAL_OFFSET). * @param equipment - The SCL elements ConductingEquipment or PowerTransformer. - * @param side - On which side does the terminal needs to be placed relative to the given point. + * @param direction - On which side does the terminal needs to be placed relative to the given point. */ export function getAbsolutePositionTerminal( equipment: Element, @@ -217,7 +217,7 @@ export function createVoltageLevelElement(voltageLevel: Element): SVGElement { /** * Create a Bay element. - * @param voltageLevel - The Bay from the SCL document to use. + * @param bay - The Bay from the SCL document to use. * @returns A Bay element. */ export function createBayElement(bay: Element): SVGElement { @@ -263,7 +263,7 @@ export function createTextElement( export function createTerminalElement( terminal: Element, sideToDraw: Direction, - clickAction?: () => void + clickAction?: (event: Event) => void ): SVGElement { const groupElement = createGroupElement(terminal); @@ -301,9 +301,12 @@ export function createTerminalElement( */ export function createBusBarElement( busBarElement: Element, - busbarLength: number + busbarLength: number, + clickAction?: (event: Event) => void ): SVGElement { const groupElement = createGroupElement(busBarElement); + // Overwrite the type to make a distinction between Bays and Busbars. + groupElement.setAttribute('type', 'Busbar'); const busBarName = getNameAttribute(busBarElement)!; const absolutePosition = getAbsolutePositionBusBar(busBarElement); @@ -328,6 +331,8 @@ export function createBusBarElement( ); groupElement.appendChild(text); + if (clickAction) groupElement.addEventListener('click', clickAction); + return groupElement; } @@ -338,7 +343,7 @@ export function createBusBarElement( */ export function createConductingEquipmentElement( equipmentElement: Element, - clickAction?: () => void + clickAction?: (event: Event) => void ): SVGElement { const groupElement = createGroupElement(equipmentElement); @@ -375,7 +380,8 @@ export function createConductingEquipmentElement( * @returns The Power Transformer SVG element. */ export function createPowerTransformerElement( - powerTransformerElement: Element + powerTransformerElement: Element, + clickAction?: (event: Event) => void ): SVGElement { const groupElement = createGroupElement(powerTransformerElement); @@ -401,19 +407,20 @@ export function createPowerTransformerElement( ); groupElement.appendChild(text); + if (clickAction) groupElement.addEventListener('click', clickAction); + return groupElement; } /** * Create a Connectivity Node element. * @param cNodeElement - The SCL element ConnectivityNode - * @param position - The SCL position of the Connectivity Node. * @param clickAction - The action to execute when the terminal is being clicked. * @returns The Connectivity Node SVG element. */ export function createConnectivityNodeElement( cNodeElement: Element, - clickAction?: () => void + clickAction?: (event: Event) => void ): SVGElement { const groupElement = createGroupElement(cNodeElement); diff --git a/src/editors/singlelinediagram/wizards/bay.ts b/src/editors/singlelinediagram/wizards/bay.ts new file mode 100644 index 0000000000..4a8c687c07 --- /dev/null +++ b/src/editors/singlelinediagram/wizards/bay.ts @@ -0,0 +1,45 @@ +import { TemplateResult } from 'lit-element'; +import { get } from 'lit-translate'; + +import { Wizard} from '../../../foundation.js'; + +import '../../../wizard-textfield.js'; +import { renderBayWizard } from "../../../wizards/bay.js"; +import { + getDescAttribute, + getNameAttribute, + getXCoordinateAttribute, + getYCoordinateAttribute, + updateNamingAndCoordinatesAction, + renderXYCoordinateFields +} from "./foundation.js"; + +function render( + name: string | null, + desc: string | null, + xCoordinate: string | null, + yCoordinate: string | null, +): TemplateResult[] { + return renderBayWizard(name, desc) + .concat(renderXYCoordinateFields(xCoordinate, yCoordinate)); +} + +export function editBayWizard(element: Element): Wizard { + return [ + { + title: get('bay.wizard.title.edit'), + element, + primary: { + icon: 'edit', + label: get('save'), + action: updateNamingAndCoordinatesAction(element), + }, + content: render( + getNameAttribute(element), + getDescAttribute(element), + getXCoordinateAttribute(element), + getYCoordinateAttribute(element), + ), + }, + ]; +} diff --git a/src/editors/singlelinediagram/wizards/conductingequipment.ts b/src/editors/singlelinediagram/wizards/conductingequipment.ts new file mode 100644 index 0000000000..627a597342 --- /dev/null +++ b/src/editors/singlelinediagram/wizards/conductingequipment.ts @@ -0,0 +1,61 @@ +import { TemplateResult } from 'lit-element'; +import { get } from 'lit-translate'; + +import '@material/mwc-list/mwc-list-item'; +import '@material/mwc-select'; + +import '../../../wizard-textfield.js'; +import { Wizard } from '../../../foundation.js'; +import { + getDescAttribute, + getNameAttribute, + getXCoordinateAttribute, + getYCoordinateAttribute, + updateNamingAndCoordinatesAction, + renderXYCoordinateFields +} from './foundation.js'; +import { + renderConductingEquipmentWizard, + reservedNamesConductingEquipment, + typeName +} from "../../../wizards/conductingequipment.js"; + +export function render( + name: string | null, + desc: string | null, + xCoordinate: string | null, + yCoordinate: string | null, + option: 'edit' | 'create', + type: string, + reservedNames: string[] +): TemplateResult[] { + return renderConductingEquipmentWizard(name, desc, option, type, reservedNames) + .concat(renderXYCoordinateFields(xCoordinate, yCoordinate)); +} + +export function editConductingEquipmentWizard(element: Element): Wizard { + const reservedNames = reservedNamesConductingEquipment( + element.parentNode!, + element.getAttribute('name')); + + return [ + { + title: get('conductingequipment.wizard.title.edit'), + element, + primary: { + icon: 'edit', + label: get('save'), + action: updateNamingAndCoordinatesAction(element), + }, + content: render( + getNameAttribute(element), + getDescAttribute(element), + getXCoordinateAttribute(element), + getYCoordinateAttribute(element), + 'edit', + typeName(element), + reservedNames + ), + }, + ]; +} diff --git a/src/editors/singlelinediagram/wizards/foundation.ts b/src/editors/singlelinediagram/wizards/foundation.ts new file mode 100644 index 0000000000..192cc0e47c --- /dev/null +++ b/src/editors/singlelinediagram/wizards/foundation.ts @@ -0,0 +1,92 @@ +import { html, TemplateResult } from "lit-element"; +import { translate } from "lit-translate"; + +import { + cloneElement, + EditorAction, + getValue, + WizardActor, + WizardInput, +} from '../../../foundation.js'; +import { SCL_COORDINATES_NAMESPACE } from "../foundation.js"; + +export function getNameAttribute(element: Element): string | null { + return element.getAttribute('name'); +} + +export function getDescAttribute(element: Element): string | null { + return element.getAttribute('desc'); +} + +export function getXCoordinateAttribute(element: Element): string | null { + return element.getAttributeNS(SCL_COORDINATES_NAMESPACE, 'x'); +} + +export function getYCoordinateAttribute(element: Element): string | null { + return element.getAttributeNS(SCL_COORDINATES_NAMESPACE, 'y'); +} + +export function getFixedCoordinateValue(value: string | null): string | null { + if (value === null) { + return value; + } + + let convertedValue = Number(value); + if (isNaN(convertedValue) || convertedValue < 0) { + convertedValue = 0; + } + + return convertedValue.toString(); +} + +function updateXYAttribute(element: Element, attributeName: string, value: string | null): void { + if (value === null) { + element.removeAttributeNS(SCL_COORDINATES_NAMESPACE, attributeName) + } else { + element.setAttributeNS(SCL_COORDINATES_NAMESPACE, attributeName, value); + } +} + +export function updateNamingAndCoordinatesAction(element: Element): WizardActor { + return (inputs: WizardInput[]): EditorAction[] => { + const name = getValue(inputs.find(i => i.label === 'name')!)!; + const desc = getValue(inputs.find(i => i.label === 'desc')!); + const xCoordinate = getValue(inputs.find(i => i.label === 'xCoordinate')!); + const yCoordinate = getValue(inputs.find(i => i.label === 'yCoordinate')!); + + if ( + name === getNameAttribute(element) && + desc === getDescAttribute(element) && + xCoordinate === getXCoordinateAttribute(element) && + yCoordinate === getYCoordinateAttribute(element) + ) { + return []; + } + + const newElement = cloneElement(element, { name, desc }); + updateXYAttribute(newElement, 'x', getFixedCoordinateValue(xCoordinate)); + updateXYAttribute(newElement, 'y', getFixedCoordinateValue(yCoordinate)); + + return [{ old: { element }, new: { element: newElement } }]; + }; +} + +export function renderXYCoordinateFields( + xCoordinate: string | null, + yCoordinate: string | null, +) : TemplateResult[] { + return [ + html``, + html``, + ]; +} diff --git a/src/editors/singlelinediagram/wizards/powertransformer.ts b/src/editors/singlelinediagram/wizards/powertransformer.ts new file mode 100644 index 0000000000..a74902823e --- /dev/null +++ b/src/editors/singlelinediagram/wizards/powertransformer.ts @@ -0,0 +1,50 @@ +import { TemplateResult } from 'lit-element'; +import { get } from 'lit-translate'; + +import { Wizard} from '../../../foundation.js'; + +import '../../../wizard-textfield.js'; +import { + reservedNamesPowerTransformer, + renderPowerTransformerWizard +} from "../../../wizards/powertransformer.js"; +import { + getDescAttribute, + getNameAttribute, + getXCoordinateAttribute, + getYCoordinateAttribute, + updateNamingAndCoordinatesAction, + renderXYCoordinateFields +} from "./foundation.js"; + +function render( + name: string | null, + desc: string | null, + xCoordinate: string | null, + yCoordinate: string | null, + reservedNames: string[] +): TemplateResult[] { + return renderPowerTransformerWizard(name, desc, reservedNames) + .concat(renderXYCoordinateFields(xCoordinate, yCoordinate)); +} + +export function editPowerTransformerWizard(element: Element): Wizard { + return [ + { + title: get('powertransformer.wizard.title.edit'), + element, + primary: { + icon: 'edit', + label: get('save'), + action: updateNamingAndCoordinatesAction(element), + }, + content: render( + getNameAttribute(element), + getDescAttribute(element), + getXCoordinateAttribute(element), + getYCoordinateAttribute(element), + reservedNamesPowerTransformer(element) + ), + }, + ]; +} diff --git a/src/editors/singlelinediagram/wizards/wizard-library.ts b/src/editors/singlelinediagram/wizards/wizard-library.ts new file mode 100644 index 0000000000..69411d7c4d --- /dev/null +++ b/src/editors/singlelinediagram/wizards/wizard-library.ts @@ -0,0 +1,516 @@ +import { SCLTag, Wizard } from '../../../foundation.js'; +import { emptyWizard } from '../../../wizards/wizard-library.js'; + +import { editConnectivityNodeWizard } from "../../../wizards/connectivitynode.js"; +import { editTerminalWizard } from "../../../wizards/terminal.js"; + +import { editBayWizard } from "./bay.js"; +import { editConductingEquipmentWizard } from './conductingequipment.js'; +import { editPowerTransformerWizard } from './powertransformer.js'; + +type SclElementWizard = (element: Element) => Wizard | undefined; + +export const wizards: Record< + SCLTag, + { + edit: SclElementWizard; + create: SclElementWizard; + } +> = { + AccessControl: { + edit: emptyWizard, + create: emptyWizard, + }, + AccessPoint: { + edit: emptyWizard, + create: emptyWizard, + }, + Address: { + edit: emptyWizard, + create: emptyWizard, + }, + Association: { + edit: emptyWizard, + create: emptyWizard, + }, + Authentication: { + edit: emptyWizard, + create: emptyWizard, + }, + BDA: { + edit: emptyWizard, + create: emptyWizard, + }, + BitRate: { + edit: emptyWizard, + create: emptyWizard, + }, + Bay: { + edit: editBayWizard, + create: emptyWizard, + }, + ClientLN: { + edit: emptyWizard, + create: emptyWizard, + }, + ClientServices: { + edit: emptyWizard, + create: emptyWizard, + }, + CommProt: { + edit: emptyWizard, + create: emptyWizard, + }, + Communication: { + edit: emptyWizard, + create: emptyWizard, + }, + ConductingEquipment: { + edit: editConductingEquipmentWizard, + create: emptyWizard, + }, + ConfDataSet: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfLdName: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfLNs: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfLogControl: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfReportControl: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfSG: { + edit: emptyWizard, + create: emptyWizard, + }, + ConfSigRef: { + edit: emptyWizard, + create: emptyWizard, + }, + ConnectedAP: { + edit: emptyWizard, + create: emptyWizard, + }, + ConnectivityNode: { + edit: editConnectivityNodeWizard, + create: emptyWizard, + }, + DA: { + edit: emptyWizard, + create: emptyWizard, + }, + DAI: { + edit: emptyWizard, + create: emptyWizard, + }, + DAType: { + edit: emptyWizard, + create: emptyWizard, + }, + DO: { + edit: emptyWizard, + create: emptyWizard, + }, + DOI: { + edit: emptyWizard, + create: emptyWizard, + }, + DOType: { + edit: emptyWizard, + create: emptyWizard, + }, + DataObjectDirectory: { + edit: emptyWizard, + create: emptyWizard, + }, + DataSet: { + edit: emptyWizard, + create: emptyWizard, + }, + DataSetDirectory: { + edit: emptyWizard, + create: emptyWizard, + }, + DataTypeTemplates: { + edit: emptyWizard, + create: emptyWizard, + }, + DynAssociation: { + edit: emptyWizard, + create: emptyWizard, + }, + DynDataSet: { + edit: emptyWizard, + create: emptyWizard, + }, + EnumType: { + edit: emptyWizard, + create: emptyWizard, + }, + EnumVal: { + edit: emptyWizard, + create: emptyWizard, + }, + EqFunction: { + edit: emptyWizard, + create: emptyWizard, + }, + EqSubFunction: { + edit: emptyWizard, + create: emptyWizard, + }, + ExtRef: { + edit: emptyWizard, + create: emptyWizard, + }, + FCDA: { + edit: emptyWizard, + create: emptyWizard, + }, + FileHandling: { + edit: emptyWizard, + create: emptyWizard, + }, + Function: { + edit: emptyWizard, + create: emptyWizard, + }, + GeneralEquipment: { + edit: emptyWizard, + create: emptyWizard, + }, + GetCBValues: { + edit: emptyWizard, + create: emptyWizard, + }, + GetDataObjectDefinition: { + edit: emptyWizard, + create: emptyWizard, + }, + GetDataSetValue: { + edit: emptyWizard, + create: emptyWizard, + }, + GetDirectory: { + edit: emptyWizard, + create: emptyWizard, + }, + GOOSE: { + edit: emptyWizard, + create: emptyWizard, + }, + GOOSESecurity: { + edit: emptyWizard, + create: emptyWizard, + }, + GSE: { + edit: emptyWizard, + create: emptyWizard, + }, + GSEDir: { + edit: emptyWizard, + create: emptyWizard, + }, + GSEControl: { + edit: emptyWizard, + create: emptyWizard, + }, + GSESettings: { + edit: emptyWizard, + create: emptyWizard, + }, + GSSE: { + edit: emptyWizard, + create: emptyWizard, + }, + Header: { + edit: emptyWizard, + create: emptyWizard, + }, + History: { + edit: emptyWizard, + create: emptyWizard, + }, + Hitem: { + edit: emptyWizard, + create: emptyWizard, + }, + IED: { + edit: emptyWizard, + create: emptyWizard, + }, + IEDName: { + edit: emptyWizard, + create: emptyWizard, + }, + Inputs: { + edit: emptyWizard, + create: emptyWizard, + }, + IssuerName: { + edit: emptyWizard, + create: emptyWizard, + }, + KDC: { + edit: emptyWizard, + create: emptyWizard, + }, + LDevice: { + edit: emptyWizard, + create: emptyWizard, + }, + LN: { + edit: emptyWizard, + create: emptyWizard, + }, + LN0: { + edit: emptyWizard, + create: emptyWizard, + }, + LNode: { + edit: emptyWizard, + create: emptyWizard, + }, + LNodeType: { + edit: emptyWizard, + create: emptyWizard, + }, + Line: { + edit: emptyWizard, + create: emptyWizard, + }, + Log: { + edit: emptyWizard, + create: emptyWizard, + }, + LogControl: { + edit: emptyWizard, + create: emptyWizard, + }, + LogSettings: { + edit: emptyWizard, + create: emptyWizard, + }, + MaxTime: { + edit: emptyWizard, + create: emptyWizard, + }, + McSecurity: { + edit: emptyWizard, + create: emptyWizard, + }, + MinTime: { + edit: emptyWizard, + create: emptyWizard, + }, + NeutralPoint: { + edit: emptyWizard, + create: emptyWizard, + }, + OptFields: { + edit: emptyWizard, + create: emptyWizard, + }, + P: { + edit: emptyWizard, + create: emptyWizard, + }, + PhysConn: { + edit: emptyWizard, + create: emptyWizard, + }, + PowerTransformer: { + edit: editPowerTransformerWizard, + create: emptyWizard, + }, + Private: { + edit: emptyWizard, + create: emptyWizard, + }, + Process: { + edit: emptyWizard, + create: emptyWizard, + }, + ProtNs: { + edit: emptyWizard, + create: emptyWizard, + }, + Protocol: { + edit: emptyWizard, + create: emptyWizard, + }, + ReadWrite: { + edit: emptyWizard, + create: emptyWizard, + }, + RedProt: { + edit: emptyWizard, + create: emptyWizard, + }, + ReportControl: { + edit: emptyWizard, + create: emptyWizard, + }, + ReportSettings: { + edit: emptyWizard, + create: emptyWizard, + }, + RptEnabled: { + edit: emptyWizard, + create: emptyWizard, + }, + SamplesPerSec: { + edit: emptyWizard, + create: emptyWizard, + }, + SampledValueControl: { + edit: emptyWizard, + create: emptyWizard, + }, + SecPerSamples: { + edit: emptyWizard, + create: emptyWizard, + }, + SCL: { + edit: emptyWizard, + create: emptyWizard, + }, + SDI: { + edit: emptyWizard, + create: emptyWizard, + }, + SDO: { + edit: emptyWizard, + create: emptyWizard, + }, + Server: { + edit: emptyWizard, + create: emptyWizard, + }, + ServerAt: { + edit: emptyWizard, + create: emptyWizard, + }, + Services: { + edit: emptyWizard, + create: emptyWizard, + }, + SetDataSetValue: { + edit: emptyWizard, + create: emptyWizard, + }, + SettingControl: { + edit: emptyWizard, + create: emptyWizard, + }, + SettingGroups: { + edit: emptyWizard, + create: emptyWizard, + }, + SGEdit: { + edit: emptyWizard, + create: emptyWizard, + }, + SmpRate: { + edit: emptyWizard, + create: emptyWizard, + }, + SMV: { + edit: emptyWizard, + create: emptyWizard, + }, + SmvOpts: { + edit: emptyWizard, + create: emptyWizard, + }, + SMVsc: { + edit: emptyWizard, + create: emptyWizard, + }, + SMVSecurity: { + edit: emptyWizard, + create: emptyWizard, + }, + SMVSettings: { + edit: emptyWizard, + create: emptyWizard, + }, + SubEquipment: { + edit: emptyWizard, + create: emptyWizard, + }, + SubFunction: { + edit: emptyWizard, + create: emptyWizard, + }, + SubNetwork: { + edit: emptyWizard, + create: emptyWizard, + }, + Subject: { + edit: emptyWizard, + create: emptyWizard, + }, + Substation: { + edit: emptyWizard, + create: emptyWizard, + }, + SupSubscription: { + edit: emptyWizard, + create: emptyWizard, + }, + TapChanger: { + edit: emptyWizard, + create: emptyWizard, + }, + Terminal: { + edit: editTerminalWizard, + create: emptyWizard, + }, + Text: { + edit: emptyWizard, + create: emptyWizard, + }, + TimerActivatedControl: { + edit: emptyWizard, + create: emptyWizard, + }, + TimeSyncProt: { + edit: emptyWizard, + create: emptyWizard, + }, + TransformerWinding: { + edit: emptyWizard, + create: emptyWizard, + }, + TrgOps: { + edit: emptyWizard, + create: emptyWizard, + }, + Val: { + edit: emptyWizard, + create: emptyWizard, + }, + ValueHandling: { + edit: emptyWizard, + create: emptyWizard, + }, + Voltage: { + edit: emptyWizard, + create: emptyWizard, + }, + VoltageLevel: { + edit: emptyWizard, + create: emptyWizard, + }, +}; diff --git a/src/translations/de.ts b/src/translations/de.ts index 124b5dd79b..ef1dfc2302 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -164,6 +164,15 @@ export const de: Translations = { missing: 'Kein IED vorhanden', toggleChildElements: "???" }, + powertransformer: { + wizard: { + nameHelper: '`Name des Leistungstransformators', + descHelper: 'Beschreibung des Leistungstransformators', + title: { + edit: 'Leistungstransformator bearbeiten', + }, + }, + }, voltagelevel: { name: 'Spannungsebene', wizard: { @@ -400,6 +409,10 @@ export const de: Translations = { }, sld: { substationSelector: 'Schaltanlage auswählen', + wizard: { + xCoordinateHelper: 'X-Koordinate im Einphasenersatzschaltbild', + yCoordinateHelper: 'Y-Koordinate im Einphasenersatzschaltbild', + }, }, add: 'Hinzufügen', new: 'Neu', diff --git a/src/translations/en.ts b/src/translations/en.ts index d1cf97660d..12880e30eb 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -161,6 +161,15 @@ export const en = { missing: 'No IED', toggleChildElements: "Toggle child elements" }, + powertransformer: { + wizard: { + nameHelper: 'Power transformer name', + descHelper: 'Power transformer description', + title: { + edit: 'Edit power transformer', + } + } + }, voltagelevel: { name: 'Voltage level', wizard: { @@ -397,6 +406,10 @@ export const en = { }, sld: { substationSelector: 'Select a substation', + wizard: { + xCoordinateHelper: 'X-Coordinate for Single Line Diagram', + yCoordinateHelper: 'Y-Coordinate for Single Line Diagram', + }, }, add: 'Add', new: 'New', diff --git a/src/wizards/bay.ts b/src/wizards/bay.ts index 8416f6c23d..baa415dba9 100644 --- a/src/wizards/bay.ts +++ b/src/wizards/bay.ts @@ -12,7 +12,7 @@ import { } from '../foundation.js'; import { updateNamingAction } from './foundation/actions.js'; -function render(name: string | null, desc: string | null): TemplateResult[] { +export function renderBayWizard(name: string | null, desc: string | null): TemplateResult[] { return [ html``; } -function render( +export function renderConductingEquipmentWizard( name: string | null, desc: string | null, option: 'edit' | 'create', @@ -145,12 +145,15 @@ export function createAction(parent: Element): WizardActor { }; } -export function createConductingEquipmentWizard(parent: Element): Wizard { - const reservedNames = Array.from( - parent.querySelectorAll('ConductingEquipment') - ) +export function reservedNamesConductingEquipment(parent: Element, currentName?: string | null): string[] { + return Array.from(parent.querySelectorAll('ConductingEquipment')) .filter(isPublic) - .map(condEq => condEq.getAttribute('name') ?? ''); + .map(condEq => condEq.getAttribute('name') ?? '') + .filter(name => currentName && name !== currentName); +} + +export function createConductingEquipmentWizard(parent: Element): Wizard { + const reservedNames = reservedNamesConductingEquipment(parent); return [ { @@ -161,18 +164,15 @@ export function createConductingEquipmentWizard(parent: Element): Wizard { label: get('add'), action: createAction(parent), }, - content: render('', '', 'create', '', reservedNames), + content: renderConductingEquipmentWizard('', '', 'create', '', reservedNames), }, ]; } export function editConductingEquipmentWizard(element: Element): Wizard { - const reservedNames = Array.from( - element.parentNode!.querySelectorAll('ConductingEquipment') - ) - .filter(isPublic) - .map(condEq => condEq.getAttribute('name') ?? '') - .filter(name => name !== element.getAttribute('name')); + const reservedNames = reservedNamesConductingEquipment( + element.parentNode!, + element.getAttribute('name')); return [ { @@ -183,7 +183,7 @@ export function editConductingEquipmentWizard(element: Element): Wizard { label: get('save'), action: updateNamingAction(element), }, - content: render( + content: renderConductingEquipmentWizard( element.getAttribute('name'), element.getAttribute('desc'), 'edit', diff --git a/src/wizards/powertransformer.ts b/src/wizards/powertransformer.ts new file mode 100644 index 0000000000..92407f902d --- /dev/null +++ b/src/wizards/powertransformer.ts @@ -0,0 +1,61 @@ +import { html, TemplateResult } from 'lit-element'; +import { get, translate } from 'lit-translate'; + +import { + isPublic, + Wizard, +} from '../foundation.js'; + +import { updateNamingAction } from "./foundation/actions.js"; + +export function renderPowerTransformerWizard( + name: string | null, + desc: string | null, + reservedNames: string[] +): TemplateResult[] { + return [ + html``, + html``, + ]; +} + +export function reservedNamesPowerTransformer(currentElement: Element): string[] { + return Array.from( + currentElement.parentNode!.querySelectorAll('PowerTransformer') + ) + .filter(isPublic) + .map(cNode => cNode.getAttribute('name') ?? '') + .filter(name => name !== currentElement.getAttribute('name')); +} + +export function editPowerTransformerWizard(element: Element): Wizard { + return [ + { + title: get('powertransformer.wizard.title.edit'), + element, + primary: { + icon: 'edit', + label: get('save'), + action: updateNamingAction(element), + }, + content: renderPowerTransformerWizard( + element.getAttribute('name'), + element.getAttribute('desc'), + reservedNamesPowerTransformer(element) + ), + }, + ]; +} diff --git a/src/wizards/wizard-library.ts b/src/wizards/wizard-library.ts index cb53b1d7e9..f700809f1e 100644 --- a/src/wizards/wizard-library.ts +++ b/src/wizards/wizard-library.ts @@ -1,19 +1,14 @@ import { SCLTag, Wizard } from '../foundation.js'; import { createBayWizard, editBayWizard } from './bay.js'; -import { - createConductingEquipmentWizard, - editConductingEquipmentWizard, -} from './conductingequipment.js'; +import { createConductingEquipmentWizard, editConductingEquipmentWizard} from './conductingequipment.js'; import { editConnectivityNodeWizard } from './connectivitynode.js'; import { createFCDAsWizard } from './fcda.js'; import { lNodeWizard } from './lnode.js'; import { createSubstationWizard, substationEditWizard } from './substation.js'; import { editTerminalWizard } from './terminal.js'; -import { - voltageLevelCreateWizard, - voltageLevelEditWizard, -} from './voltagelevel.js'; +import { voltageLevelCreateWizard, voltageLevelEditWizard } from './voltagelevel.js'; +import { editPowerTransformerWizard } from "./powertransformer.js"; type SclElementWizard = (element: Element) => Wizard | undefined; @@ -341,7 +336,7 @@ export const wizards: Record< create: emptyWizard, }, PowerTransformer: { - edit: emptyWizard, + edit: editPowerTransformerWizard, create: emptyWizard, }, Private: { diff --git a/test/unit/editors/singlelinediagram/foundation.test.ts b/test/unit/editors/singlelinediagram/foundation.test.ts index 162e8ddc1e..18b1646345 100644 --- a/test/unit/editors/singlelinediagram/foundation.test.ts +++ b/test/unit/editors/singlelinediagram/foundation.test.ts @@ -3,7 +3,7 @@ import { getRelativeCoordinates, isBusBar, getConnectedTerminals, - calculateConnectivityNodeCoordinates, + calculateConnectivityNodeCoordinates, getCommonParentElement, } from '../../../../src/editors/singlelinediagram/foundation.js'; import { getDescriptionAttribute, getInstanceAttribute, getNameAttribute, getPathNameAttribute } from '../../../../src/foundation.js'; @@ -128,4 +128,39 @@ describe('Single Line Diagram foundation', () => { }); }); }); + + describe('defines a getCommonParentElement function that', () => { + it("common parent between connectivity node and power transformer should be the substation", () => { + const substation = doc.querySelector('Substation[name="AA1"]')!; + const powerTransformer = doc.querySelector('PowerTransformer[name="TA1"]')!; + const connectivityNode = doc.querySelector('Bay[name="Bay A"] > ConnectivityNode[name="L1"]')!; + expect(getCommonParentElement(powerTransformer, connectivityNode, null)).to.equal(substation); + }); + + it("common parent between connectivity node and conducting equipment should be the bay", () => { + const bay = doc.querySelector('Bay[name="Bay A"]')!; + const conductingEquipment = doc.querySelector('Bay[name="Bay A"] > ConductingEquipment[name="QB1"]')!; + const connectivityNode = doc.querySelector('Bay[name="Bay A"] > ConnectivityNode[name="L1"]')!; + expect(getCommonParentElement(conductingEquipment, connectivityNode, null)).to.equal(bay); + }); + + it("common parent between two unrelated elements will be the root element", () => { + const powerTransformer = doc.querySelector('PowerTransformer[name="TA1"]')!; + const subNetwork = doc.querySelector('SubNetwork[name="StationBus"]')!; + expect(getCommonParentElement(powerTransformer, subNetwork, null)).to.equal(doc.firstElementChild); + }); + + it("when no common parent then the default element returned", async () => { + // Can only happen if from different documents, otherwise there should always be the root as common. + const otherDoc = await fetch('/test/testfiles/valid2007B4withSubstationXY.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + const substation = doc.querySelector('Substation[name="AA1"]')!; + const bay = doc.querySelector('Bay[name="Bay A"]')!; + const conductingEquipment = doc.querySelector('Bay[name="Bay A"] > ConductingEquipment[name="QB1"]')!; + const connectivityNode = otherDoc.querySelector('Bay[name="Bay A"] > ConnectivityNode[name="L1"]')!; + expect(getCommonParentElement(conductingEquipment, connectivityNode, substation)).to.equal(substation); + }); + }); }); diff --git a/test/unit/editors/singlelinediagram/sld-drawing.test.ts b/test/unit/editors/singlelinediagram/sld-drawing.test.ts index 32122f7be1..59f060aca7 100644 --- a/test/unit/editors/singlelinediagram/sld-drawing.test.ts +++ b/test/unit/editors/singlelinediagram/sld-drawing.test.ts @@ -1,4 +1,5 @@ import { expect } from '@open-wc/testing'; +import { SCL_COORDINATES_NAMESPACE } from '../../../../src/editors/singlelinediagram/foundation.js'; import { createBayElement, createBusBarElement, @@ -17,13 +18,13 @@ import { function setCoordinates(element: Element, x: number, y: number): void { element.setAttributeNS( - 'http://www.iec.ch/61850/2003/SCLcoordinates', + SCL_COORDINATES_NAMESPACE, 'x', `${x}` ); element.setAttributeNS( - 'http://www.iec.ch/61850/2003/SCLcoordinates', + SCL_COORDINATES_NAMESPACE, 'y', `${y}` ); diff --git a/test/unit/editors/singlelinediagram/wizards/__snapshots__/bay.test.snap.js b/test/unit/editors/singlelinediagram/wizards/__snapshots__/bay.test.snap.js new file mode 100644 index 0000000000..3453a7d48e --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/__snapshots__/bay.test.snap.js @@ -0,0 +1,57 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL element Bay (X/Y) looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element Bay (X/Y) looks like the latest snapshot */ + diff --git a/test/unit/editors/singlelinediagram/wizards/__snapshots__/conductingequipment.test.snap.js b/test/unit/editors/singlelinediagram/wizards/__snapshots__/conductingequipment.test.snap.js new file mode 100644 index 0000000000..15b559da76 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/__snapshots__/conductingequipment.test.snap.js @@ -0,0 +1,73 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL element Conducting Equipment (X/Y) looks like the latest snapshot"] = +` +
+ + + Disconnector + + + + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element Conducting Equipment (X/Y) looks like the latest snapshot */ + diff --git a/test/unit/editors/singlelinediagram/wizards/__snapshots__/powertransformer.test.snap.js b/test/unit/editors/singlelinediagram/wizards/__snapshots__/powertransformer.test.snap.js new file mode 100644 index 0000000000..21e016a6a1 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/__snapshots__/powertransformer.test.snap.js @@ -0,0 +1,57 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL element Power Transformer (X/Y) looks like the latest snapshot"] = +` +
+ + + + + + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element Power Transformer (X/Y) looks like the latest snapshot */ + diff --git a/test/unit/editors/singlelinediagram/wizards/bay.test.ts b/test/unit/editors/singlelinediagram/wizards/bay.test.ts new file mode 100644 index 0000000000..4788417793 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/bay.test.ts @@ -0,0 +1,81 @@ +import {expect, fixture, html} from '@open-wc/testing'; + +import '../../../../mock-wizard.js'; +import {MockWizard} from '../../../../mock-wizard.js'; +import { + executeWizardUpdateAction, + expectWizardNoUpdateAction, + fetchDoc, + setWizardTextFieldValue +} from "../../../wizards/foundation.js"; + +import {WizardTextField} from "../../../../../src/wizard-textfield.js"; +import {WizardInput} from "../../../../../src/foundation.js"; +import {editBayWizard} from "../../../../../src/editors/singlelinediagram/wizards/bay.js"; +import {updateNamingAndCoordinatesAction} from "../../../../../src/editors/singlelinediagram/wizards/foundation.js"; + +describe('Wizards for SCL element Bay (X/Y)', () => { + let doc: XMLDocument; + let bay: Element; + let element: MockWizard; + let inputs: WizardInput[]; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/valid2007B4withSubstationXY.scd'); + bay = doc.querySelector('Bay[name="BusBar A"]')!; + + element = await fixture(html``); + const wizard = editBayWizard(bay); + element.workflow.push(wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('update name should be updated in document', async function () { + await setWizardTextFieldValue(inputs[0], 'OtherBusBar A'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + expect(updateAction.old.element).to.have.attribute('name', 'BusBar A'); + expect(updateAction.new.element).to.have.attribute('name', 'OtherBusBar A'); + }); + + it('update description should be updated in document', async function () { + await setWizardTextFieldValue(inputs[1], 'Some description'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + expect(updateAction.old.element).to.not.have.attribute('desc'); + expect(updateAction.new.element).to.have.attribute('desc', 'Some description'); + }); + + it('update X-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[2], '4'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:x', '1'); + expect(updateAction.new.element).to.have.attribute('sxy:x', '4'); + }); + + it('update Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[3], '5'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '1'); + expect(updateAction.new.element).to.have.attribute('sxy:y', '5'); + }); + + it('clear Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[3], null); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '1'); + expect(updateAction.new.element).to.not.have.attribute('sxy:y'); + }); + + it('when no fields changed there will be no update action', async function () { + expectWizardNoUpdateAction(updateNamingAndCoordinatesAction(bay), inputs); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); +}); diff --git a/test/unit/editors/singlelinediagram/wizards/conductingequipment.test.ts b/test/unit/editors/singlelinediagram/wizards/conductingequipment.test.ts new file mode 100644 index 0000000000..07c7344202 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/conductingequipment.test.ts @@ -0,0 +1,81 @@ +import {expect, fixture, html} from '@open-wc/testing'; + +import '../../../../mock-wizard.js'; +import {MockWizard} from '../../../../mock-wizard.js'; +import { + executeWizardUpdateAction, + expectWizardNoUpdateAction, + fetchDoc, + setWizardTextFieldValue +} from "../../../wizards/foundation.js"; + +import {WizardTextField} from "../../../../../src/wizard-textfield.js"; +import {WizardInput} from "../../../../../src/foundation.js"; +import {editConductingEquipmentWizard} from "../../../../../src/editors/singlelinediagram/wizards/conductingequipment.js"; +import {updateNamingAndCoordinatesAction} from "../../../../../src/editors/singlelinediagram/wizards/foundation.js"; + +describe('Wizards for SCL element Conducting Equipment (X/Y)', () => { + let doc: XMLDocument; + let conductingEquipment: Element; + let element: MockWizard; + let inputs: WizardInput[]; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/valid2007B4withSubstationXY.scd'); + conductingEquipment = doc.querySelector('ConductingEquipment[name="QB1"]')!; + + element = await fixture(html``); + const wizard = editConductingEquipmentWizard(conductingEquipment); + element.workflow.push(wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('update name should be updated in document', async function () { + await setWizardTextFieldValue(inputs[1], 'OtherQB1'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + expect(updateAction.old.element).to.have.attribute('name', 'QB1'); + expect(updateAction.new.element).to.have.attribute('name', 'OtherQB1'); + }); + + it('update description should be updated in document', async function () { + await setWizardTextFieldValue(inputs[2], 'Some description'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + expect(updateAction.old.element).to.not.have.attribute('desc'); + expect(updateAction.new.element).to.have.attribute('desc', 'Some description'); + }); + + it('update X-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[3], '4'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:x', '1'); + expect(updateAction.new.element).to.have.attribute('sxy:x', '4'); + }); + + it('update Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[4], '5'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '1'); + expect(updateAction.new.element).to.have.attribute('sxy:y', '5'); + }); + + it('clear Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[4], null); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '1'); + expect(updateAction.new.element).to.not.have.attribute('sxy:y'); + }); + + it('when no fields changed there will be no update action', async function () { + expectWizardNoUpdateAction(updateNamingAndCoordinatesAction(conductingEquipment), inputs); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); +}); diff --git a/test/unit/editors/singlelinediagram/wizards/foundation.test.ts b/test/unit/editors/singlelinediagram/wizards/foundation.test.ts new file mode 100644 index 0000000000..7d13372915 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/foundation.test.ts @@ -0,0 +1,26 @@ +import { expect } from '@open-wc/testing'; +import {getFixedCoordinateValue} from "../../../../../src/editors/singlelinediagram/wizards/foundation.js"; + +describe('Single Line Diagram Wizard foundation', () => { + describe('defines a getFixedCoordinateValue function that', () => { + it("when calling with value null, null will be returned", () => { + expect(getFixedCoordinateValue(null)).to.be.null; + }); + + it("when calling with a positive number, number will be returned", () => { + expect(getFixedCoordinateValue("2")).to.be.equal("2"); + }); + + it("when calling with zero, zero will be returned", () => { + expect(getFixedCoordinateValue("0")).to.be.equal("0"); + }); + + it("when calling with a negative number, zero will be returned", () => { + expect(getFixedCoordinateValue("-2")).to.be.equal("0"); + }); + + it("when calling with a invalid number, zero will be returned", () => { + expect(getFixedCoordinateValue("A2")).to.be.equal("0"); + }); + }); +}); diff --git a/test/unit/editors/singlelinediagram/wizards/powertransformer.test.ts b/test/unit/editors/singlelinediagram/wizards/powertransformer.test.ts new file mode 100644 index 0000000000..54c2fece94 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/powertransformer.test.ts @@ -0,0 +1,81 @@ +import {expect, fixture, html} from '@open-wc/testing'; + +import '../../../../mock-wizard.js'; +import {MockWizard} from '../../../../mock-wizard.js'; +import { + executeWizardUpdateAction, + expectWizardNoUpdateAction, + fetchDoc, + setWizardTextFieldValue +} from "../../../wizards/foundation.js"; + +import {WizardTextField} from "../../../../../src/wizard-textfield.js"; +import {WizardInput} from "../../../../../src/foundation.js"; +import {editPowerTransformerWizard} from "../../../../../src/editors/singlelinediagram/wizards/powertransformer.js"; +import {updateNamingAndCoordinatesAction} from "../../../../../src/editors/singlelinediagram/wizards/foundation.js"; + +describe('Wizards for SCL element Power Transformer (X/Y)', () => { + let doc: XMLDocument; + let powerTransformer: Element; + let element: MockWizard; + let inputs: WizardInput[]; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/valid2007B4withSubstationXY.scd'); + powerTransformer = doc.querySelector('PowerTransformer[name="TA1"]')!; + + element = await fixture(html``); + const wizard = editPowerTransformerWizard(powerTransformer); + element.workflow.push(wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('update name should be updated in document', async function () { + await setWizardTextFieldValue(inputs[0], 'OtherTA1'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + expect(updateAction.old.element).to.have.attribute('name', 'TA1'); + expect(updateAction.new.element).to.have.attribute('name', 'OtherTA1'); + }); + + it('update description should be updated in document', async function () { + await setWizardTextFieldValue(inputs[1], 'Some description'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + expect(updateAction.old.element).to.not.have.attribute('desc'); + expect(updateAction.new.element).to.have.attribute('desc', 'Some description'); + }); + + it('update X-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[2], '4'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:x', '1'); + expect(updateAction.new.element).to.have.attribute('sxy:x', '4'); + }); + + it('update Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[3], '5'); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '9'); + expect(updateAction.new.element).to.have.attribute('sxy:y', '5'); + }); + + it('clear Y-Coordinate should be updated in document', async function () { + await setWizardTextFieldValue(inputs[3], null); + + const updateAction = executeWizardUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + expect(updateAction.old.element).to.have.attribute('sxy:y', '9'); + expect(updateAction.new.element).to.not.have.attribute('sxy:y'); + }); + + it('when no fields changed there will be no update action', async function () { + expectWizardNoUpdateAction(updateNamingAndCoordinatesAction(powerTransformer), inputs); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); +}); diff --git a/test/unit/editors/singlelinediagram/wizards/wizard-library.test.ts b/test/unit/editors/singlelinediagram/wizards/wizard-library.test.ts new file mode 100644 index 0000000000..6e2af6fee6 --- /dev/null +++ b/test/unit/editors/singlelinediagram/wizards/wizard-library.test.ts @@ -0,0 +1,46 @@ +import {expect} from "@open-wc/testing"; + +import {SCLTag} from "../../../../../src/foundation.js"; +import {emptyWizard} from "../../../../../src/wizards/wizard-library.js"; +import {wizards} from "../../../../../src/editors/singlelinediagram/wizards/wizard-library.js"; + +import { editConnectivityNodeWizard } from "../../../../../src/wizards/connectivitynode.js"; +import { editTerminalWizard } from "../../../../../src/wizards/terminal.js"; + +import { editBayWizard } from "../../../../../src/editors/singlelinediagram/wizards/bay.js"; +import { editConductingEquipmentWizard } from '../../../../../src/editors/singlelinediagram/wizards/conductingequipment.js'; +import { editPowerTransformerWizard } from '../../../../../src/editors/singlelinediagram/wizards/powertransformer.js'; + +describe('Wizard Library (X/Y Coordinates)', () => { + it('Check that all create wizards are empty wizards', async () => { + for (const wizardKey in wizards) { + expect(wizards[wizardKey].create).to.be.equal(emptyWizard); + } + }); + + it('Check that some edit wizards are correct SLD Editors', async () => { + for (const wizardKey in wizards) { + const editWizard = wizards[wizardKey].edit; + switch (wizardKey) { + case "Bay": + expect(editWizard).to.be.equal(editBayWizard); + break; + case "ConductingEquipment": + expect(editWizard).to.be.equal(editConductingEquipmentWizard); + break; + case "ConnectivityNode": + expect(editWizard).to.be.equal(editConnectivityNodeWizard); + break; + case "PowerTransformer": + expect(editWizard).to.be.equal(editPowerTransformerWizard); + break; + case "Terminal": + expect(editWizard).to.be.equal(editTerminalWizard); + break; + default: + expect(editWizard).to.be.equal(emptyWizard); + break; + } + } + }); +}); diff --git a/test/unit/wizards/__snapshots__/powertransformer.test.snap.js b/test/unit/wizards/__snapshots__/powertransformer.test.snap.js new file mode 100644 index 0000000000..e0bea89dde --- /dev/null +++ b/test/unit/wizards/__snapshots__/powertransformer.test.snap.js @@ -0,0 +1,45 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Wizards for SCL element Power Transformer looks like the latest snapshot"] = +` +
+ + + + +
+ + + + +
+`; +/* end snapshot Wizards for SCL element Power Transformer looks like the latest snapshot */ + diff --git a/test/unit/wizards/foundation.ts b/test/unit/wizards/foundation.ts new file mode 100644 index 0000000000..04c2d27dd8 --- /dev/null +++ b/test/unit/wizards/foundation.ts @@ -0,0 +1,39 @@ +import {expect} from "@open-wc/testing"; + +import {isUpdate, Update, WizardActor, WizardInput} from "../../../src/foundation.js"; +import {WizardTextField} from "../../../src/wizard-textfield.js"; + +const noOp = () => { + return; +}; +const newWizard = (done = noOp) => { + const element = document.createElement('mwc-dialog'); + element.close = done; + return element; +}; + +export async function setWizardTextFieldValue(field: WizardTextField, value: string | null): Promise { + if (field.nullSwitch && !field.nullSwitch.checked) { + field.nullSwitch?.click(); + } + field.maybeValue = value; + await field.requestUpdate(); +} + +export function executeWizardUpdateAction(wizardActor: WizardActor, inputs: WizardInput[]): Update { + const updateActions = wizardActor(inputs, newWizard()); + expect(updateActions.length).to.equal(1); + expect(updateActions[0]).to.satisfy(isUpdate); + return updateActions[0]; +} + +export function expectWizardNoUpdateAction(wizardActor: WizardActor, inputs: WizardInput[]): void { + const updateActions = wizardActor(inputs, newWizard()); + expect(updateActions).to.be.empty; +} + +export async function fetchDoc(docName: string): Promise { + return await fetch(docName) + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); +} diff --git a/test/unit/wizards/powertransformer.test.ts b/test/unit/wizards/powertransformer.test.ts new file mode 100644 index 0000000000..38a826d82f --- /dev/null +++ b/test/unit/wizards/powertransformer.test.ts @@ -0,0 +1,58 @@ +import {expect, fixture, html} from '@open-wc/testing'; + +import '../../mock-wizard.js'; +import {MockWizard} from '../../mock-wizard.js'; + +import {WizardTextField} from "../../../src/wizard-textfield.js"; +import {WizardInput} from "../../../src/foundation.js"; +import {editPowerTransformerWizard} from "../../../src/wizards/powertransformer.js"; +import {updateNamingAction} from "../../../src/wizards/foundation/actions.js"; + +import { + executeWizardUpdateAction, + expectWizardNoUpdateAction, + fetchDoc, + setWizardTextFieldValue +} from "./foundation.js"; + +describe('Wizards for SCL element Power Transformer', () => { + let doc: XMLDocument; + let powerTransformer: Element; + let element: MockWizard; + let inputs: WizardInput[]; + + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/valid2007B4withSubstationXY.scd'); + powerTransformer = doc.querySelector('PowerTransformer[name="TA1"]')!; + + element = await fixture(html``); + const wizard = editPowerTransformerWizard(powerTransformer); + element.workflow.push(wizard); + await element.requestUpdate(); + inputs = Array.from(element.wizardUI.inputs); + }); + + it('update name should be updated in document', async function () { + await setWizardTextFieldValue(inputs[0], 'OtherTA1'); + + const updateAction = executeWizardUpdateAction(updateNamingAction(powerTransformer), inputs); + expect(updateAction.old.element).to.have.attribute('name', 'TA1'); + expect(updateAction.new.element).to.have.attribute('name', 'OtherTA1'); + }); + + it('update description should be updated in document', async function () { + await setWizardTextFieldValue(inputs[1], 'Some description'); + + const updateAction = executeWizardUpdateAction(updateNamingAction(powerTransformer), inputs); + expect(updateAction.old.element).to.not.have.attribute('desc'); + expect(updateAction.new.element).to.have.attribute('desc', 'Some description'); + }); + + it('when no fields changed there will be no update action', async function () { + expectWizardNoUpdateAction(updateNamingAction(powerTransformer), inputs); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }); +});