diff --git a/demo/src/diagram-viewers/add-diagrams.ts b/demo/src/diagram-viewers/add-diagrams.ts index b0f5c8d5..09afba43 100644 --- a/demo/src/diagram-viewers/add-diagrams.ts +++ b/demo/src/diagram-viewers/add-diagrams.ts @@ -34,6 +34,8 @@ import { OnMoveTextNodeCallbackType, OnSelectNodeCallbackType, OnToggleNadHoverCallbackType, + OnSaveCallbackType, + MouseMode, } from '../../../src'; export const addNadToDemo = () => { @@ -54,13 +56,13 @@ export const addNadToDemo = () => { true, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + MouseMode.MOVE ); - document - .getElementById('svg-container-nad') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + document.getElementById('svg-container-nad')?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgExample) @@ -74,19 +76,21 @@ export const addNadToDemo = () => { 600, 1000, 1200, - handleNodeMove, - handleTextNodeMove, + null, + null, handleNodeSelect, false, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + null ); document .getElementById('svg-container-nad-no-moving') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgMultibusVLNodesExample) @@ -106,13 +110,15 @@ export const addNadToDemo = () => { true, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + MouseMode.MOVE ); document .getElementById('svg-container-nad-multibus-vlnodes') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgMultibusVLNodes14Example) @@ -132,13 +138,15 @@ export const addNadToDemo = () => { true, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + MouseMode.MOVE ); document .getElementById('svg-container-nad-multibus-vlnodes14') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgPstHvdcExample) @@ -158,13 +166,15 @@ export const addNadToDemo = () => { true, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + MouseMode.MOVE ); document .getElementById('svg-container-nad-pst-hvdc') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgThreeWTDanglingLineUnknownBusExample) @@ -184,13 +194,15 @@ export const addNadToDemo = () => { true, false, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + true, + MouseMode.SELECT ); document .getElementById('svg-container-nad-threewt-dl-ub') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); fetch(NadSvgPartialNetworkExample) @@ -210,13 +222,15 @@ export const addNadToDemo = () => { true, true, null, - handleToggleNadHover + handleToggleNadHover, + handleSave, + false, + MouseMode.MOVE ); document .getElementById('svg-container-nad-partial-network') - ?.getElementsByTagName('svg')[0] - .setAttribute('style', 'border:2px; border-style:solid;'); + ?.setAttribute('style', 'border:2px; border-style:solid;'); }); }; @@ -414,3 +428,8 @@ const handleToggleNadHover: OnToggleNadHoverCallbackType = (hovered, mousePositi console.log(msg); } }; + +const handleSave: OnSaveCallbackType = (svg, metadata) => { + console.log(svg); + console.log(metadata); +}; diff --git a/src/components/network-area-diagram-viewer/diagram-utils.ts b/src/components/network-area-diagram-viewer/diagram-utils.ts index 50a2f10a..ee724140 100644 --- a/src/components/network-area-diagram-viewer/diagram-utils.ts +++ b/src/components/network-area-diagram-viewer/diagram-utils.ts @@ -3,6 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 */ import { Point } from '@svgdotjs/svg.js'; @@ -126,7 +127,7 @@ export function getLabelData(point1: Point, point2: Point, arrowLabelShift: numb } // get fork position of a multibranch edge -export function getEdgeFork(point: Point, edgeForkLength: number, angleFork: number) { +export function getEdgeFork(point: Point, edgeForkLength: number, angleFork: number): Point { return new Point(point.x + edgeForkLength * Math.cos(angleFork), point.y + edgeForkLength * Math.sin(angleFork)); } @@ -521,3 +522,53 @@ export function getTextNodeMoves( { xOrig: textNode.connectionShiftX, yOrig: textNode.connectionShiftY, xNew: connXNew, yNew: connYNew }, ]; } + +function getButton(svg: string, title: string, pressed: boolean): HTMLButtonElement { + const button = document.createElement('button'); + button.innerHTML = svg; + button.title = title; + button.style.height = '25px'; + button.style.width = '25px'; + button.style.marginRight = '1px'; + button.style.marginLeft = '3px'; + button.style.marginTop = '3px'; + button.style.padding = '0px'; + if (pressed) { + button.style.border = 'medium solid orange'; + } else { + button.style.border = 'none'; + } + return button; +} + +export function getSaveButton(): HTMLButtonElement { + return getButton( + '', + 'Save', + false + ); +} + +export function getMoveButton(pressed: boolean): HTMLButtonElement { + return getButton( + '', + 'Move', + pressed + ); +} + +export function getSelectButton(pressed: boolean): HTMLButtonElement { + return getButton( + '', + 'Select', + pressed + ); +} + +export function pressButton(button: HTMLButtonElement) { + button.style.border = 'medium solid orange'; +} + +export function releaseButton(button: HTMLButtonElement) { + button.style.border = 'none'; +} diff --git a/src/components/network-area-diagram-viewer/layout-parameters.ts b/src/components/network-area-diagram-viewer/layout-parameters.ts index fd198ce6..0f70ff6a 100644 --- a/src/components/network-area-diagram-viewer/layout-parameters.ts +++ b/src/components/network-area-diagram-viewer/layout-parameters.ts @@ -3,6 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 */ import { LayoutParametersMetadata } from './diagram-metadata'; diff --git a/src/components/network-area-diagram-viewer/network-area-diagram-viewer.test.ts b/src/components/network-area-diagram-viewer/network-area-diagram-viewer.test.ts index 1a932e9a..a24435cb 100644 --- a/src/components/network-area-diagram-viewer/network-area-diagram-viewer.test.ts +++ b/src/components/network-area-diagram-viewer/network-area-diagram-viewer.test.ts @@ -27,6 +27,9 @@ describe('Test network-area-diagram-viewer', () => { false, false, null, + null, + null, + false, null ); diff --git a/src/components/network-area-diagram-viewer/network-area-diagram-viewer.ts b/src/components/network-area-diagram-viewer/network-area-diagram-viewer.ts index 00830257..35f5ffab 100644 --- a/src/components/network-area-diagram-viewer/network-area-diagram-viewer.ts +++ b/src/components/network-area-diagram-viewer/network-area-diagram-viewer.ts @@ -3,6 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 */ import { Point, SVG, ViewBoxLike, Svg } from '@svgdotjs/svg.js'; @@ -17,6 +18,11 @@ import { debounce } from '@mui/material'; type DIMENSIONS = { width: number; height: number; viewbox: VIEWBOX }; type VIEWBOX = { x: number; y: number; width: number; height: number }; +export enum MouseMode { + MOVE, + SELECT, +} + export type OnMoveNodeCallbackType = ( equipmentId: string, nodeId: string, @@ -41,6 +47,7 @@ export type OnMoveTextNodeCallbackType = ( ) => void; export type OnSelectNodeCallbackType = (equipmentId: string, nodeId: string) => void; + export type OnToggleNadHoverCallbackType = ( hovered: boolean, mousePosition: Point | null, @@ -48,8 +55,11 @@ export type OnToggleNadHoverCallbackType = ( equipmentType: string ) => void; +export type OnSaveCallbackType = (svg: string | null, metadata: string | null) => void; + export class NetworkAreaDiagramViewer { container: HTMLElement; + svgDiv: HTMLElement; svgContent: string; diagramMetadata: DiagramMetadata | null; width: number; @@ -73,6 +83,8 @@ export class NetworkAreaDiagramViewer { onSelectNodeCallback: OnSelectNodeCallbackType | null; dynamicCssRules: CSS_RULE[]; onToggleHoverCallback: OnToggleNadHoverCallbackType | null; + onSaveCallback: OnSaveCallbackType | null; + mouseMode: MouseMode; constructor( container: HTMLElement, @@ -88,9 +100,13 @@ export class NetworkAreaDiagramViewer { enableNodeInteraction: boolean, enableLevelOfDetail: boolean, customDynamicCssRules: CSS_RULE[] | null, - onToggleHoverCallback: OnToggleNadHoverCallbackType | null + onToggleHoverCallback: OnToggleNadHoverCallbackType | null, + onSaveCallback: OnSaveCallbackType | null, + addButtons: boolean, + defaultMouseMode: MouseMode | null ) { this.container = container; + this.svgDiv = document.createElement('div'); this.svgContent = svgContent; this.diagramMetadata = diagramMetadata; this.width = 0; @@ -98,6 +114,12 @@ export class NetworkAreaDiagramViewer { this.originalWidth = 0; this.originalHeight = 0; this.dynamicCssRules = customDynamicCssRules ?? DEFAULT_DYNAMIC_CSS_RULES; + this.mouseMode = defaultMouseMode ?? MouseMode.MOVE; + this.onMoveNodeCallback = onMoveNodeCallback; + this.onMoveTextNodeCallback = onMoveTextNodeCallback; + this.onSelectNodeCallback = onSelectNodeCallback; + this.onToggleHoverCallback = onToggleHoverCallback; + this.onSaveCallback = onSaveCallback; this.init( minWidth, minHeight, @@ -105,14 +127,11 @@ export class NetworkAreaDiagramViewer { maxHeight, enableNodeInteraction, enableLevelOfDetail, - diagramMetadata !== null + diagramMetadata !== null, + addButtons ); this.svgParameters = new SvgParameters(diagramMetadata?.svgParameters); this.layoutParameters = new LayoutParameters(diagramMetadata?.layoutParameters); - this.onMoveNodeCallback = onMoveNodeCallback; - this.onMoveTextNodeCallback = onMoveTextNodeCallback; - this.onSelectNodeCallback = onSelectNodeCallback; - this.onToggleHoverCallback = onToggleHoverCallback; } public setWidth(width: number): void { @@ -185,7 +204,7 @@ export class NetworkAreaDiagramViewer { public moveNodeToCoordinates(equipmentId: string, x: number, y: number) { const nodeId = this.getNodeIdFromEquipmentId(equipmentId); if (nodeId != null) { - const elemToMove: SVGElement | null = this.container.querySelector('[id="' + nodeId + '"]'); + const elemToMove: SVGElement | null = this.svgDiv.querySelector('[id="' + nodeId + '"]'); if (elemToMove) { const newPosition = new Point(x, y); this.onDragStart(elemToMove); @@ -201,7 +220,8 @@ export class NetworkAreaDiagramViewer { maxHeight: number, enableNodeInteraction: boolean, enableLevelOfDetail: boolean, - hasMetadata: boolean + hasMetadata: boolean, + addButtons: boolean ): void { if (!this.container || !this.svgContent) { return; @@ -215,6 +235,14 @@ export class NetworkAreaDiagramViewer { // clear the previous svg in div element before replacing this.container.innerHTML = ''; + // add buttons bar + if (addButtons) { + this.container.appendChild(this.getButtonsBar(enableNodeInteraction && hasMetadata)); + } + + // add svg div + this.container.appendChild(this.svgDiv); + // set dimensions this.setOriginalWidth(dimensions.width); this.setOriginalHeight(dimensions.height); @@ -223,7 +251,7 @@ export class NetworkAreaDiagramViewer { // set the SVG this.svgDraw = SVG() - .addTo(this.container) + .addTo(this.svgDiv) .size(this.width, this.height) .viewbox(dimensions.viewbox.x, dimensions.viewbox.y, dimensions.viewbox.width, dimensions.viewbox.height); const drawnSvg: HTMLElement = this.svgDraw.svg(this.svgContent).node.firstElementChild; @@ -304,7 +332,7 @@ export class NetworkAreaDiagramViewer { if (enableNodeInteraction && hasMetadata) { // fill empty elements: unknown buses and three windings transformers - const emptyElements: NodeListOf = this.container.querySelectorAll( + const emptyElements: NodeListOf = this.svgDiv.querySelectorAll( '.nad-unknown-busnode, .nad-3wt-nodes .nad-winding' ); emptyElements.forEach((emptyElement) => { @@ -313,6 +341,48 @@ export class NetworkAreaDiagramViewer { } } + private getButtonsBar(showNodeInteractionButtons: boolean): HTMLDivElement { + const buttonsDiv = document.createElement('div'); + buttonsDiv.style.display = 'flex'; + buttonsDiv.style.alignItems = 'center'; + buttonsDiv.style.position = 'absolute'; + buttonsDiv.style.zIndex = '2'; + if (showNodeInteractionButtons) { + const moveButton = DiagramUtils.getMoveButton(this.mouseMode === MouseMode.MOVE); + buttonsDiv.appendChild(moveButton); + const selectButton = DiagramUtils.getSelectButton(this.mouseMode === MouseMode.SELECT); + buttonsDiv.appendChild(selectButton); + moveButton.addEventListener('click', () => { + this.mouseMode = MouseMode.MOVE; + DiagramUtils.pressButton(moveButton); + DiagramUtils.releaseButton(selectButton); + }); + selectButton.addEventListener('click', () => { + this.mouseMode = MouseMode.SELECT; + DiagramUtils.pressButton(selectButton); + DiagramUtils.releaseButton(moveButton); + }); + } + if (this.onSaveCallback != null) { + const saveSvgButton = DiagramUtils.getSaveButton(); + buttonsDiv.appendChild(saveSvgButton); + saveSvgButton.addEventListener('click', () => { + if (this.onSaveCallback != null) { + this.onSaveCallback(this.getSvg(), this.getJsonMetadata()); + } + }); + } + return buttonsDiv; + } + + public getSvg(): string | null { + return this.svgDraw !== undefined ? this.svgDraw.svg() : null; + } + + public getJsonMetadata(): string | null { + return JSON.stringify(this.diagramMetadata); + } + public getDimensionsFromSvg(): DIMENSIONS | null { // Dimensions are set in the main svg tag attributes. We want to parse those data without loading the whole svg in the DOM. const result = this.svgContent.match(']*>'); @@ -346,10 +416,10 @@ export class NetworkAreaDiagramViewer { private onMouseLeftDown(event: MouseEvent) { // check dragging vs. selection - if (event.shiftKey) { + if (this.mouseMode == MouseMode.SELECT) { // selecting node this.onSelectStart(DiagramUtils.getSelectableFrom(event.target as SVGElement)); - } else { + } else if (this.mouseMode == MouseMode.MOVE) { // moving node this.onDragStart(DiagramUtils.getDraggableFrom(event.target as SVGElement)); } @@ -479,7 +549,7 @@ export class NetworkAreaDiagramViewer { private dragVoltageLevelText(mousePosition: Point) { window.getSelection()?.empty(); // to avoid text highlighting in firefox - const vlNode: SVGGraphicsElement | null = this.container.querySelector( + const vlNode: SVGGraphicsElement | null = this.svgDiv.querySelector( "[id='" + DiagramUtils.getVoltageLevelNodeId(this.draggedElement?.id) + "']" ); this.moveText(this.draggedElement, vlNode, mousePosition, DiagramUtils.getTextNodeAngleFromCentre); @@ -487,7 +557,7 @@ export class NetworkAreaDiagramViewer { private dragVoltageLevelNode(mousePosition: Point) { this.moveNode(mousePosition); - const textNode: SVGGraphicsElement | null = this.container.querySelector( + const textNode: SVGGraphicsElement | null = this.svgDiv.querySelector( "[id='" + DiagramUtils.getTextNodeId(this.draggedElement?.id) + "']" ); this.moveText( @@ -542,7 +612,7 @@ export class NetworkAreaDiagramViewer { textHeight: number, textWidth: number ) { - const textEdge: SVGGraphicsElement | null = this.container.querySelector("[id='" + textEdgeId + "']"); + const textEdge: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + textEdgeId + "']"); if (textEdge != null) { // compute voltage level circle radius const busNodes: BusNodeMetadata[] | undefined = this.diagramMetadata?.busNodes.filter( @@ -574,7 +644,7 @@ export class NetworkAreaDiagramViewer { } private moveSvgElement(svgElementId: string, translation: Point) { - const svgElement: SVGGraphicsElement | null = this.container.querySelector("[id='" + svgElementId + "']"); + const svgElement: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + svgElementId + "']"); if (svgElement) { const transform = DiagramUtils.getTransform(svgElement); const totalTranslation = new Point( @@ -645,7 +715,7 @@ export class NetworkAreaDiagramViewer { // get the nodes at the sides of an edge private getEdgeNodes(edge: EdgeMetadata): [SVGGraphicsElement | null, SVGGraphicsElement | null] { const otherNodeId = this.draggedElement?.id === edge.node1 ? edge.node2 : edge.node1; - const otherNode: SVGGraphicsElement | null = this.container.querySelector("[id='" + otherNodeId + "']"); + const otherNode: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + otherNodeId + "']"); const node1 = this.draggedElement?.id === edge.node1 ? this.draggedElement : otherNode; const node2 = otherNode?.id === edge.node1 ? this.draggedElement : otherNode; return [node1, node2]; @@ -693,9 +763,7 @@ export class NetworkAreaDiagramViewer { return; } // get edge element - const edgeNode: SVGGraphicsElement | null = this.container.querySelector( - "[id='" + edge.svgId + "']" - ); + const edgeNode: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + edge.svgId + "']"); if (!edgeNode) { return; } @@ -766,7 +834,7 @@ export class NetworkAreaDiagramViewer { return; } // get edge element - const edgeNode: SVGGraphicsElement | null = this.container.querySelector("[id='" + edge.svgId + "']"); + const edgeNode: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + edge.svgId + "']"); if (!edgeNode) { return; } @@ -1094,7 +1162,7 @@ export class NetworkAreaDiagramViewer { const angleId = busNode.svgId == edge.busNode1 ? edgeId + '.1' : edgeId + '.2'; if (!this.edgeAngles.has(angleId)) { // if not yet stored in angle map -> compute and store it - const edgeNode: SVGGraphicsElement | null = this.container.querySelector("[id='" + edgeId + "']"); + const edgeNode: SVGGraphicsElement | null = this.svgDiv.querySelector("[id='" + edgeId + "']"); if (edgeNode) { const side = busNode.svgId == edge.busNode1 ? 0 : 1; const halfEdge: HTMLElement = edgeNode.children.item(side)?.firstElementChild; diff --git a/src/components/network-area-diagram-viewer/svg-parameters.ts b/src/components/network-area-diagram-viewer/svg-parameters.ts index e904d1b9..82de03bb 100644 --- a/src/components/network-area-diagram-viewer/svg-parameters.ts +++ b/src/components/network-area-diagram-viewer/svg-parameters.ts @@ -3,6 +3,7 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * SPDX-License-Identifier: MPL-2.0 */ import { SvgParametersMetadata } from './diagram-metadata'; diff --git a/src/index.ts b/src/index.ts index fe85f952..2313eec0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,8 +11,10 @@ export type { OnMoveTextNodeCallbackType, OnSelectNodeCallbackType, OnToggleNadHoverCallbackType, + OnSaveCallbackType, } from './components/network-area-diagram-viewer/network-area-diagram-viewer'; export type { DiagramMetadata } from './components/network-area-diagram-viewer/diagram-metadata'; +export { MouseMode } from './components/network-area-diagram-viewer/network-area-diagram-viewer'; export { THRESHOLD_STATUS } from './components/network-area-diagram-viewer/dynamic-css-utils'; export type { CSS_DECLARATION, CSS_RULE } from './components/network-area-diagram-viewer/dynamic-css-utils'; export { SingleLineDiagramViewer } from './components/single-line-diagram-viewer/single-line-diagram-viewer';