Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions demo/nad-created.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ <h2>Network Area Diagram SVGs created from diagram metadata</h2>
<figcaption>Edge info with double arrows from metadata</figcaption>
<div id="svg-container-nad-double-arrows-c"></div>
</figure>
<div class="break"></div>
<figure class="diagram-figure">
<figcaption>Edge info with components</figcaption>
<div id="svg-container-nad-components"></div>
</figure>
<figure class="diagram-figure">
<figcaption>Edge info with components from metadata</figcaption>
<div id="svg-container-nad-components-c"></div>
</figure>
<div class="break"></div>
<figure class="diagram-figure">
<figcaption>Custom style provider</figcaption>
<div id="svg-container-nad-pst-hvdc-custom"></div>
Expand Down
33 changes: 33 additions & 0 deletions demo/src/nad-created.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -70,6 +72,7 @@ const addCreatedNadToDemo = () => {
onToggleHoverCallback: handleToggleNadHover,
onRightClickCallback: handleRightClick,
onBendLineCallback: handleLineBending,
createSvgFromMetadata: true,
};
new NetworkAreaDiagramViewer(
document.getElementById('svg-container-nad-eurostag-c')!,
Expand Down Expand Up @@ -306,6 +309,35 @@ const addCreatedNadToDemo = () => {
nadViewerParametersOptions
);

fetch(NadSvgComponentsExample)
.then((response) => response.text())
.then((svgContent) => {
const nadViewerParametersOptions: NadViewerParametersOptions = {
Comment thread
rolnico marked this conversation as resolved.
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) => {
Expand Down Expand Up @@ -366,6 +398,7 @@ const addCreatedNadToDemo = () => {
onToggleHoverCallback: handleToggleNadHover,
onRightClickCallback: handleRightClick,
onBendLineCallback: handleLineBending,
createSvgFromMetadata: true,
};
new NetworkAreaDiagramViewer(
document.getElementById('svg-container-nad-pegase-network-c')!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;

Expand Down Expand Up @@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export class NetworkAreaDiagramViewer {

componentLibrary: LibraryComponent[] = DefaultLibraryComponents;

svgWriter: SvgWriter | undefined = undefined;

static readonly ZOOM_CLASS_PREFIX = 'nad-zoom-';

/**
Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -378,6 +384,9 @@ export class NetworkAreaDiagramViewer {
};
this.svgDraw = SVG().addTo(this.svgDiv).size(this.width, this.height).viewbox(viewBox);
this.innerSvg = <SVGElement>this.svgDraw.svg(this.svgContent).node.firstElementChild;
if (this.svgWriter != null) {
this.svgWriter.addSvgContent(<SVGSVGElement>this.innerSvg);
}
this.innerSvg.style.overflow = 'visible';

this.textNodesSection = this.getOrCreateTextNodesSection();
Expand All @@ -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
Expand Down Expand Up @@ -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 = <SVGElement>this.innerSvg?.querySelector(':scope > g.nad-text-nodes');
if (!textNodesGElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,20 +47,54 @@ export class SvgWriter {
2: 'TWO',
3: 'THREE',
};
componentLibrary: LibraryComponent[] = DefaultLibraryComponents;
urlResolver: ((fileName: string) => string) | undefined = undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
urlResolver: ((fileName: string) => string) | undefined = undefined;
urlResolver: ((fileName: string) => string) | undefined = undefined;
private abortController: AbortController | undefined = undefined;

add abortController so that each addSvgContent call aborts the previous contrtoller, cancelling in-flight detchs from any prior init() invocation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getComponentPath method in component-utils.ts should change to:

export async function getComponentPath(
    componentFilename: string,
    svgUrlResolver?: (fileName: string) => string,
    signal?: AbortSignal
): Promise<SVGPathElement> {
    const url = svgUrlResolver ? svgUrlResolver(componentFilename) : DefaultComponentSvgMapping[componentFilename];
    if (!url) throw new Error(`Unknown component file: ${componentFilename}`);
    const response = await fetch(url, { signal });
    const svgContent = await response.text();
    const path = SVG().svg(svgContent).node.firstElementChild?.firstElementChild;
    if (!path) throw new Error(`No path found in component SVG: ${componentFilename}`);
    return path as SVGPathElement;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understood, the AbortController is used to correctly handle multiple calls to the addSvgContent method. This method is called in the init method of the viewer, in turn called by the constructor. So there should not be multiple calls to the addSvgContent method, at least not for the same SvgWriter. Should be the controller added if, for some reason, the call to the addSvgContent method is put also in other point of the viewer code? Take also into account that the addSvgContent method, right now, assumes that the input SVG is empty, otherwise the resulting svg would have duplicated elements, if you call it multiple times. So, if you want, I can anyway add the controller (it should not create issues) but I'm not sure it is really necessary and useful here.


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 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vb.x} ${vb.y} ${vb.width} ${vb.height}"></svg>`;
}

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public addSvgContent(svg: SVGSVGElement) {
public addSvgContent(svg: SVGSVGElement) {
this.abortController?.abort();
this.abortController = new AbortController();
this.threeWindingsTransformers = [];
this.threeWindingsTransformerEdges = [];

reset the arrays to not duplicate 3WT nodes

// add nodes
svg.appendChild(this.getNodes());
// add edges and infos
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
Comment on lines +554 to +556

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void ComponentUtils.getComponentPath(subComponent.fileName, this.urlResolver).then((path) => {
edgeInfoComponent.appendChild(path);
});
ComponentUtils.getComponentPath(subComponent.fileName, this.urlResolver, this.abortController?.signal)
.then((path) => {
edgeInfoComponent.appendChild(path);
})
.catch((err: unknown) => {
if (err instanceof DOMException && err.name === 'AbortError') return;
console.error('[NAD] Failed to load component', subComponent.fileName, err);
});

AbortError is silently ignored while other errors log to console

});
}
return edgeInfoComponent;
}

private addEdgeInfoArrows(
gEdgeInfoElement: SVGGElement,
info: EdgeInfoMetadata,
Expand Down Expand Up @@ -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';
Expand Down
Loading