diff --git a/demo/nad-created.html b/demo/nad-created.html index 036e2631..361212bc 100644 --- a/demo/nad-created.html +++ b/demo/nad-created.html @@ -113,6 +113,16 @@

Network Area Diagram SVGs created from diagram metadata

Edge info with double arrows from metadata
+
+
+
Edge info with components
+
+
+
+
Edge info with components from metadata
+
+
+
Custom style provider
diff --git a/demo/src/nad-created.ts b/demo/src/nad-created.ts index e318895f..215887e6 100644 --- a/demo/src/nad-created.ts +++ b/demo/src/nad-created.ts @@ -26,6 +26,8 @@ import NadSvgPegaseNetworkExample from './diagram-viewers/data/case1354pegase.sv import NadSvgPegaseNetworkExampleMeta from './diagram-viewers/data/case1354pegase_metadata.json'; import NadSvgDoubleArrowsExample from './diagram-viewers/data/nad-double-arrows-with-middle-values.svg'; import NadSvgDoubleArrowsExampleMeta from './diagram-viewers/data/nad-double-arrows-with-middle-values_metadata.json'; +import NadSvgComponentsExample from './diagram-viewers/data/nad-edge-info-components.svg'; +import NadSvgComponentsExampleMeta from './diagram-viewers/data/nad-edge-info-components_metadata.json'; import NadSvgPstHvdcCustomExample from './diagram-viewers/data/nad-four-substations_custom.svg'; import NadSvgPstHvdcCustomExampleMeta from './diagram-viewers/data/nad-four-substations_custom_metadata.json'; @@ -70,6 +72,7 @@ const addCreatedNadToDemo = () => { onToggleHoverCallback: handleToggleNadHover, onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, + createSvgFromMetadata: true, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-eurostag-c')!, @@ -306,6 +309,35 @@ const addCreatedNadToDemo = () => { nadViewerParametersOptions ); + fetch(NadSvgComponentsExample) + .then((response) => response.text()) + .then((svgContent) => { + const nadViewerParametersOptions: NadViewerParametersOptions = { + enableDragInteraction: true, + addButtons: true, + onMoveNodeCallback: handleNodeMove, + onMoveTextNodeCallback: handleTextNodeMove, + onSelectNodeCallback: handleNodeSelect, + onToggleHoverCallback: handleToggleNadHover, + onRightClickCallback: handleRightClick, + onBendLineCallback: handleLineBending, + adaptiveTextZoom: { enabled: true, threshold: 1100 }, + }; + new NetworkAreaDiagramViewer( + document.getElementById('svg-container-nad-components')!, + svgContent, + NadSvgComponentsExampleMeta, + nadViewerParametersOptions + ); + }); + + new NetworkAreaDiagramViewer( + document.getElementById('svg-container-nad-components-c')!, + '', + NadSvgComponentsExampleMeta, + nadViewerParametersOptions + ); + fetch(NadSvgPstHvdcCustomExample) .then((response) => response.text()) .then((svgContent) => { @@ -366,6 +398,7 @@ const addCreatedNadToDemo = () => { onToggleHoverCallback: handleToggleNadHover, onRightClickCallback: handleRightClick, onBendLineCallback: handleLineBending, + createSvgFromMetadata: true, }; new NetworkAreaDiagramViewer( document.getElementById('svg-container-nad-pegase-network-c')!, diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts index 81753b8d..a344e61d 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/nad-viewer-parameters.ts @@ -135,6 +135,9 @@ export interface NadViewerParametersOptions { // Resolver that maps an SVG filename to its URL, for use with a custom component library. // If not provided, the default library SVG files are used. svgUrlResolver?: (fileName: string) => string; + + // Whether to create the SVG from diagram metadata, instead of using the SVG content provided as input + createSvgFromMetadata?: boolean; } export class NadViewerParameters { @@ -149,6 +152,7 @@ export class NadViewerParameters { static readonly HOVER_POSITION_PRECISION_DEFAULT = 10; static readonly ENABLE_ADAPTIVE_ZOOM_DEFAULT = false; static readonly THRESHOLD_ADAPTIVE_ZOOM_DEFAULT = 3000; + static readonly CREATE_SVG_FROM_METADATA_DEFAULT = false; nadViewerParametersOptions: NadViewerParametersOptions | undefined; @@ -232,4 +236,11 @@ export class NadViewerParameters { public getSvgUrlResolver(): ((fileName: string) => string) | undefined { return this.nadViewerParametersOptions?.svgUrlResolver; } + + public getCreateSvgFromMetadata(): boolean { + return ( + this.nadViewerParametersOptions?.createSvgFromMetadata ?? + NadViewerParameters.CREATE_SVG_FROM_METADATA_DEFAULT + ); + } } diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts index bbcee811..81329d66 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/network-area-diagram-viewer.ts @@ -150,6 +150,8 @@ export class NetworkAreaDiagramViewer { componentLibrary: LibraryComponent[] = DefaultLibraryComponents; + svgWriter: SvgWriter | undefined = undefined; + static readonly ZOOM_CLASS_PREFIX = 'nad-zoom-'; /** @@ -169,11 +171,16 @@ export class NetworkAreaDiagramViewer { this.svgDiv.id = 'svg-container'; this.svgContent = svgContent; this.diagramMetadata = diagramMetadata; - if (this.diagramMetadata != null && this.svgContent.length == 0) { - const createdSvg = new SvgWriter(this.diagramMetadata).getSvg(); - this.svgContent = createdSvg; - } this.nadViewerParameters = new NadViewerParameters(nadViewerParametersOptions ?? undefined); + this.componentLibrary = this.nadViewerParameters.getComponentLibrary() ?? DefaultLibraryComponents; + if (this.nadViewerParameters.getCreateSvgFromMetadata() && this.diagramMetadata != null) { + this.svgWriter = new SvgWriter( + this.diagramMetadata, + this.componentLibrary, + this.nadViewerParameters.getSvgUrlResolver() + ); + this.svgContent = this.svgWriter.getEmptySvg(); + } this.width = 0; this.height = 0; this.originalWidth = 0; @@ -193,7 +200,6 @@ export class NetworkAreaDiagramViewer { this.init(); this.layoutParameters = new LayoutParameters(this.diagramMetadata?.layoutParameters); this.previousMaxDisplayedSize = 0; - this.componentLibrary = this.nadViewerParameters.getComponentLibrary() ?? DefaultLibraryComponents; } public setWidth(width: number): void { @@ -378,6 +384,9 @@ export class NetworkAreaDiagramViewer { }; this.svgDraw = SVG().addTo(this.svgDiv).size(this.width, this.height).viewbox(viewBox); this.innerSvg = this.svgDraw.svg(this.svgContent).node.firstElementChild; + if (this.svgWriter != null) { + this.svgWriter.addSvgContent(this.innerSvg); + } this.innerSvg.style.overflow = 'visible'; this.textNodesSection = this.getOrCreateTextNodesSection(); @@ -386,49 +395,7 @@ export class NetworkAreaDiagramViewer { // add events const hasMetadata = this.diagramMetadata !== null; - if (this.hasNodeInteraction() && hasMetadata) { - this.svgDraw.on('mousedown', (e: Event) => { - if ((e as MouseEvent).button == 0) { - this.onMouseLeftDown(e as MouseEvent); - } - }); - this.svgDraw.on('mousemove', (e: Event) => { - this.onMouseMove(e as MouseEvent); - }); - this.svgDraw.on('mouseup mouseleave', (e: Event) => { - if ((e as MouseEvent).button == 0) { - this.onMouseLeftUpOrLeave(e as MouseEvent); - } - }); - } - if (hasMetadata) { - this.svgDraw.on('mouseover', (e: Event) => { - this.onHover(e as MouseEvent); - }); - - this.svgDraw.on('mouseout', () => { - this.clearHighlights(); - this.hideEdgePreviewPoints(); - }); - } - if (this.onRightClickCallback != null && hasMetadata) { - this.svgDraw.on('mousedown', (e: Event) => { - if ((e as MouseEvent).button == 2) { - this.onMouseRightDown(e as MouseEvent); - } - }); - } - - this.svgDraw.on('panStart', () => { - this.attachCursorOverlay('move'); - }); - this.svgDraw.on('panEnd', () => { - this.detachCursorOverlay(); - //if the adaptive zoom feature is enabled, updates the diagram - if (this.nadViewerParameters.getAdaptiveTextZoom().enabled) { - this.adaptiveZoomViewboxUpdate(this.getCurrentlyMaxDisplayedSize()); - } - }); + this.addEvents(this.svgDraw, hasMetadata); // add pan and zoom to the SVG // we check if there is an "initial zoom" by checking ratio of width and height of the nad compared with viewBox sizes @@ -487,6 +454,53 @@ export class NetworkAreaDiagramViewer { } } + private addEvents(svgDraw: Svg, hasMetadata: boolean) { + if (this.hasNodeInteraction() && hasMetadata) { + svgDraw.on('mousedown', (e: Event) => { + if ((e as MouseEvent).button == 0) { + this.onMouseLeftDown(e as MouseEvent); + } + }); + svgDraw.on('mousemove', (e: Event) => { + this.onMouseMove(e as MouseEvent); + }); + svgDraw.on('mouseup mouseleave', (e: Event) => { + if ((e as MouseEvent).button == 0) { + this.onMouseLeftUpOrLeave(e as MouseEvent); + } + }); + } + if (hasMetadata) { + svgDraw.on('mouseover', (e: Event) => { + this.onHover(e as MouseEvent); + }); + + svgDraw.on('mouseout', () => { + this.clearHighlights(); + this.hideEdgePreviewPoints(); + }); + } + if (this.onRightClickCallback != null && hasMetadata) { + svgDraw.on('mousedown', (e: Event) => { + if ((e as MouseEvent).button == 2) { + this.onMouseRightDown(e as MouseEvent); + } + }); + } + + svgDraw.on('panStart', () => { + this.attachCursorOverlay('move'); + }); + svgDraw.on('panEnd', () => { + this.detachCursorOverlay(); + + //if the adaptive zoom feature is enabled, updates the diagram + if (this.nadViewerParameters.getAdaptiveTextZoom().enabled) { + this.adaptiveZoomViewboxUpdate(this.getCurrentlyMaxDisplayedSize()); + } + }); + } + private getOrCreateTextNodesSection(): SVGElement { let textNodesGElement = this.innerSvg?.querySelector(':scope > g.nad-text-nodes'); if (!textNodesGElement) { diff --git a/packages/network-viewer-core/src/network-area-diagram-viewer/svg-writer.ts b/packages/network-viewer-core/src/network-area-diagram-viewer/svg-writer.ts index 23f3a755..d3a888a8 100644 --- a/packages/network-viewer-core/src/network-area-diagram-viewer/svg-writer.ts +++ b/packages/network-viewer-core/src/network-area-diagram-viewer/svg-writer.ts @@ -14,6 +14,9 @@ import * as MetadataUtils from './metadata-utils'; import { EdgeRouter } from './edge-router'; import { EdgeType } from './diagram-types'; import * as SvgUtils from './svg-utils'; +import { LibraryComponent } from './library-component'; +import DefaultLibraryComponents from '../resources/default-library/components.json'; +import * as ComponentUtils from './component-utils'; export class SvgWriter { static readonly NODES_CLASS = 'nad-vl-nodes'; @@ -44,20 +47,54 @@ export class SvgWriter { 2: 'TWO', 3: 'THREE', }; + componentLibrary: LibraryComponent[] = DefaultLibraryComponents; + urlResolver: ((fileName: string) => string) | undefined = undefined; - constructor(diagramMetadata: DiagramMetadata) { + constructor( + diagramMetadata: DiagramMetadata, + componentLibrary?: LibraryComponent[], + urlResolver?: (fileName: string) => string + ) { this.diagramMetadata = diagramMetadata; this.svgParameters = new SvgParameters(this.diagramMetadata.svgParameters); + if (componentLibrary) { + this.componentLibrary = componentLibrary; + } + if (urlResolver) { + this.urlResolver = urlResolver; + } + // get edge router, for computing edges points + this.edgeRouter = new EdgeRouter(this.diagramMetadata); } public getSvg(textBoxSize?: { width: number; height: number }): string { - // get edge router, for computing edges points - this.edgeRouter = new EdgeRouter(this.diagramMetadata); + const baseSvg = this.createBaseSvg(textBoxSize); + this.addSvgContent(baseSvg.svg); + return new XMLSerializer().serializeToString(baseSvg.xmlDoc); + } + + public getEmptySvg(): string { + const vb = MetadataUtils.getViewBox( + this.diagramMetadata.nodes, + this.diagramMetadata.textNodes, + this.svgParameters + ); + return ``; + } + + private createBaseSvg(textBoxSize?: { width: number; height: number }): { + xmlDoc: XMLDocument; + svg: SVGSVGElement; + } { // create XML doc const xmlDoc = this.getXmlDoc(); // add SVG root element const svg = this.getSvgRootElement(textBoxSize); xmlDoc.appendChild(svg); + return { xmlDoc, svg }; + } + + public addSvgContent(svg: SVGSVGElement) { // add nodes svg.appendChild(this.getNodes()); // add edges and infos @@ -76,7 +113,6 @@ export class SvgWriter { const textNodeAndEdges = this.getTextNodesAndEdges(); svg.appendChild(textNodeAndEdges.textEdges); svg.appendChild(textNodeAndEdges.textNodes); - return new XMLSerializer().serializeToString(xmlDoc); } private getXmlDoc(): XMLDocument { @@ -469,8 +505,12 @@ export class SvgWriter { if (infoPoint) { gEdgeInfoElement.setAttribute('transform', 'translate(' + DiagramUtils.getFormattedPoint(infoPoint) + ')'); } - // add arrows - this.addEdgeInfoArrows(gEdgeInfoElement, info, this.edgeRouter?.getEdgeSideInfoAngle(edge.svgId, side)); + if (info.componentType) { + gEdgeInfoElement.appendChild(this.getEdgeInfoComponent(info.componentType)); + } else { + // add arrows + this.addEdgeInfoArrows(gEdgeInfoElement, info, this.edgeRouter?.getEdgeSideInfoAngle(edge.svgId, side)); + } // add labels const labelData = this.edgeRouter?.getEdgeSideLabelData(edge.svgId, side); if (labelData === undefined) return gEdgeInfoElement; @@ -503,6 +543,22 @@ export class SvgWriter { return gEdgeInfoElement; } + private getEdgeInfoComponent(componentType: string): SVGGElement { + const component = ComponentUtils.getComponent(this.componentLibrary, componentType); + const edgeInfoComponent = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + if (component) { + const trans = new Point(-component.size.width / 2, -component.size.height / 2); + edgeInfoComponent.setAttribute('transform', 'translate(' + DiagramUtils.getFormattedPoint(trans) + ')'); + edgeInfoComponent.classList.add(component.styleClass); + component.subComponents.forEach((subComponent) => { + void ComponentUtils.getComponentPath(subComponent.fileName, this.urlResolver).then((path) => { + edgeInfoComponent.appendChild(path); + }); + }); + } + return edgeInfoComponent; + } + private addEdgeInfoArrows( gEdgeInfoElement: SVGGElement, info: EdgeInfoMetadata, @@ -584,8 +640,12 @@ export class SvgWriter { 'translate(' + DiagramUtils.getFormattedPoint(middleInfoPoint) + ')' ); } - // add arrows - this.addEdgeInfoArrows(gEdgeMiddleInfoElement, info, this.edgeRouter?.getEdgeMiddleInfoAngle(edge.svgId)); + if (info.componentType) { + gEdgeMiddleInfoElement.appendChild(this.getEdgeInfoComponent(info.componentType)); + } else { + // add arrows + this.addEdgeInfoArrows(gEdgeMiddleInfoElement, info, this.edgeRouter?.getEdgeMiddleInfoAngle(edge.svgId)); + } // add labels let x = 0; let style: string | undefined = 'text-anchor:middle';