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';