-Read more at https://www.powsybl.org !
+Read more at https://www.powsybl.org!
This project and everyone participating in it is governed by the [PowSyBl Code of Conduct](https://github.com/powsybl/.github/blob/main/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior to [powsybl-tsc@lists.lfenergy.org](mailto:powsybl-tsc@lists.lfenergy.org).
@@ -90,3 +90,97 @@ We obtain the SVG below.
![Diagram demo](.github/example_n.svg)
Note that a JSON file named `n_metadata.json` is also generated in the same folder, containing all the metadata needed to interact with the diagram.
+
+
+## PowSyBl vs PowSyBl Network Area Diagram
+
+
+
+
+
+PowSyBl Network Area Diagram is a component build on top of the `Network` model available in the PowSyBl Core repository responsible for generating a concise diagram of the whole network or of a part of the network, showing in particular the interconnections between the different voltage levels.
+A network area diagram emphasizes the electrical structure of the network, and may differ substantially from the network physical geography.
+It displays the graph whose nodes are the network voltage levels, and whose edges are the lines and transformers between those voltage levels.
+
+## Getting started
+In order to generate a SVG from a given network, we need to add some Maven dependencies:
+- `powsybl-network-area-diagram` for the network area diagram itself
+- `powsybl-iidm-impl` for the network model
+- `powsybl-config-test` and `powsybl-ieee-cdf-converter` to load the `Network` example
+- `slf4j-simple` for simple logging capabilities
+
+```xml
+
+ 0.6.0
+ 4.10.0
+ 1.7.22
+
+
+
+
+ com.powsybl
+ powsybl-network-area-diagram
+ ${powsybl.nad.version}
+
+
+ com.powsybl
+ powsybl-iidm-impl
+ ${powsybl.core.version}
+
+
+ com.powsybl
+ powsybl-config-test
+ ${powsybl.core.version}
+
+
+ com.powsybl
+ powsybl-ieee-cdf-converter
+ ${powsybl.core.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+```
+
+Then we simply need to load the IEEE 30-bus example network and then generate the corresponding network area diagram SVG.
+```java
+Network network = IeeeCdfNetworkFactory.create30();
+new NetworkAreaDiagram(network).draw(Path.of("/tmp/diagram.svg"));
+```
+We obtain the following SVG:
+
+
+
+
+
+If only part of the network is wanted, we can generate a partial graph of the network, by providing
+- either a voltage level id and a depth,
+- or a list of voltage level ids and a (unique) depth.
+
+For instance let's generate the subgraph centered on voltage level `"VL25"` with a depth of `2`:
+
+```java
+new NetworkAreaDiagram(network, "VL25", 2).draw(Path.of("/tmp/partial_diagram_25.svg"));
+```
+
+This leads to following diagram:
+
+
+
+
+
+Now let's generate the subgraph with voltage levels at a maximum distance of 2 from `"VL1"` and `"VL25"`:
+
+```java
+new NetworkAreaDiagram(network, Arrays.asList("VL1", "VL25"), 2).draw(Path.of("/tmp/partial_diagram_1_25.svg"));
+```
+
+This gives us the diagram below. Note that nothing ensures that the parts displayed in resulting diagram are connected.
+That is, the voltage levels between two voltage levels which are connected in the full graph are not necessarily drawn.
+
+
+
+
diff --git a/single-line-diagram-distribution/pom.xml b/distribution-diagram/pom.xml
similarity index 86%
rename from single-line-diagram-distribution/pom.xml
rename to distribution-diagram/pom.xml
index 8b4cbf8ab..630c14c01 100644
--- a/single-line-diagram-distribution/pom.xml
+++ b/distribution-diagram/pom.xml
@@ -13,17 +13,25 @@
4.0.0
- powsybl-single-line-diagram
+ powsybl-diagramcom.powsybl
- 2.14.0-SNAPSHOT
+ 3.0.0-SNAPSHOTpom
- powsybl-single-line-diagram-distribution
- Single line diagram distribution
- Single line diagram distribution module
+ powsybl-diagram-distribution
+ PowSyBl diagram distribution
+ PowSyBl diagram distribution module
+
+ ${project.groupId}
+ powsybl-network-area-diagram
+
+
+ ${project.groupId}
+ powsybl-diagram-util-forcelayout
+ ${project.groupId}powsybl-single-line-diagram-cgmes-dl-conversion
diff --git a/network-area-diagram/pom.xml b/network-area-diagram/pom.xml
new file mode 100644
index 000000000..c84a12a21
--- /dev/null
+++ b/network-area-diagram/pom.xml
@@ -0,0 +1,111 @@
+
+
+
+ 4.0.0
+
+
+ com.powsybl
+ powsybl-diagram
+ 3.0.0-SNAPSHOT
+
+
+ powsybl-network-area-diagram
+ PowSyBl network area diagram
+ Generate the SVG corresponding to the electrical nodes graph of the whole network or of a part of the network
+ http://www.powsybl.org
+
+
+
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+
+
+ org.codehaus.mojo
+ templating-maven-plugin
+
+
+
+
+
+
+ com.powsybl
+ powsybl-diagram-util-forcelayout
+
+
+
+
+ org.jgrapht
+ jgrapht-core
+
+
+ com.powsybl
+ powsybl-iidm-api
+
+
+ com.powsybl
+ powsybl-commons
+
+
+ com.powsybl
+ powsybl-tools
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ com.powsybl
+ powsybl-iidm-impl
+ test
+
+
+ com.powsybl
+ powsybl-ieee-cdf-converter
+ test
+
+
+ com.powsybl
+ powsybl-iidm-test
+ test
+
+
+ com.powsybl
+ powsybl-ucte-converter
+ test
+
+
+ com.powsybl
+ powsybl-config-test
+ test
+
+
+ com.powsybl
+ powsybl-loadflow-api
+ test
+
+
+ com.powsybl
+ powsybl-open-loadflow
+ test
+
+
+
+
diff --git a/network-area-diagram/src/main/java-templates/com/powsybl/nad/util/PowsyblNetworkAreaDiagramVersion.java b/network-area-diagram/src/main/java-templates/com/powsybl/nad/util/PowsyblNetworkAreaDiagramVersion.java
new file mode 100644
index 000000000..e234a417c
--- /dev/null
+++ b/network-area-diagram/src/main/java-templates/com/powsybl/nad/util/PowsyblNetworkAreaDiagramVersion.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.util;
+
+import com.google.auto.service.AutoService;
+import com.powsybl.tools.*;
+
+/**
+ * @author Florian Dupuy
+ */
+@AutoService(Version.class)
+public class PowsyblNetworkAreaDiagramVersion extends AbstractVersion {
+
+ public PowsyblNetworkAreaDiagramVersion() {
+ super("powsybl-network-area-diagram", "${project.version}", "${buildNumber}", "${scmBranch}", Long.parseLong("${timestamp}"));
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java b/network-area-diagram/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java
new file mode 100644
index 000000000..5d218d69c
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java
@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad;
+
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.VoltageLevel;
+import com.powsybl.nad.build.iidm.IdProvider;
+import com.powsybl.nad.build.iidm.IntIdProvider;
+import com.powsybl.nad.build.iidm.NetworkGraphBuilder;
+import com.powsybl.nad.build.iidm.VoltageLevelFilter;
+import com.powsybl.nad.layout.BasicForceLayoutFactory;
+import com.powsybl.nad.layout.LayoutFactory;
+import com.powsybl.nad.layout.LayoutParameters;
+import com.powsybl.nad.model.Graph;
+import com.powsybl.nad.svg.LabelProvider;
+import com.powsybl.nad.svg.StyleProvider;
+import com.powsybl.nad.svg.SvgParameters;
+import com.powsybl.nad.svg.SvgWriter;
+import com.powsybl.nad.svg.iidm.DefaultLabelProvider;
+import com.powsybl.nad.svg.iidm.NominalVoltageStyleProvider;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * @author Florian Dupuy
+ */
+public class NetworkAreaDiagram {
+
+ private final Network network;
+ private final Predicate voltageLevelFilter;
+
+ public NetworkAreaDiagram(Network network) {
+ this(network, VoltageLevelFilter.NO_FILTER);
+ }
+
+ public NetworkAreaDiagram(Network network, String voltageLevelId, int depth) {
+ this(network, VoltageLevelFilter.createVoltageLevelDepthFilter(network, voltageLevelId, depth));
+ }
+
+ public NetworkAreaDiagram(Network network, List voltageLevelIds) {
+ this(network, VoltageLevelFilter.createVoltageLevelsFilter(network, voltageLevelIds));
+ }
+
+ public NetworkAreaDiagram(Network network, List voltageLevelIds, int depth) {
+ this(network, VoltageLevelFilter.createVoltageLevelsDepthFilter(network, voltageLevelIds, depth));
+ }
+
+ public NetworkAreaDiagram(Network network, Predicate voltageLevelFilter) {
+ this.network = Objects.requireNonNull(network);
+ this.voltageLevelFilter = Objects.requireNonNull(voltageLevelFilter);
+ }
+
+ public Network getNetwork() {
+ return network;
+ }
+
+ public void draw(Path svgFile) {
+ draw(svgFile, new SvgParameters());
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters) {
+ draw(svgFile, svgParameters, new LayoutParameters());
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters, LayoutParameters layoutParameters) {
+ draw(svgFile, svgParameters, layoutParameters, new NominalVoltageStyleProvider(network));
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider) {
+ draw(svgFile, svgParameters, layoutParameters, styleProvider, new DefaultLabelProvider(network, svgParameters));
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider) {
+ draw(svgFile, svgParameters, layoutParameters, styleProvider, labelProvider, new BasicForceLayoutFactory());
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider, LayoutFactory layoutFactory) {
+ draw(svgFile, svgParameters, layoutParameters, styleProvider, labelProvider, layoutFactory, new IntIdProvider());
+ }
+
+ public void draw(Path svgFile, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider, LayoutFactory layoutFactory,
+ IdProvider idProvider) {
+ Objects.requireNonNull(svgFile);
+ Objects.requireNonNull(layoutParameters);
+ Objects.requireNonNull(svgParameters);
+ Objects.requireNonNull(styleProvider);
+ Objects.requireNonNull(layoutFactory);
+ Objects.requireNonNull(idProvider);
+
+ Graph graph = new NetworkGraphBuilder(network, voltageLevelFilter, idProvider).buildGraph();
+ layoutFactory.create().run(graph, layoutParameters);
+ new SvgWriter(svgParameters, styleProvider, labelProvider).writeSvg(graph, svgFile);
+ }
+
+ public void draw(Writer writer) {
+ draw(writer, new SvgParameters());
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters) {
+ draw(writer, svgParameters, new LayoutParameters());
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters, LayoutParameters layoutParameters) {
+ draw(writer, svgParameters, layoutParameters, new NominalVoltageStyleProvider(network));
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider) {
+ draw(writer, svgParameters, layoutParameters, styleProvider, new DefaultLabelProvider(network, svgParameters));
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider) {
+ draw(writer, svgParameters, layoutParameters, styleProvider, labelProvider, new BasicForceLayoutFactory());
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider, LayoutFactory layoutFactory) {
+ draw(writer, svgParameters, layoutParameters, styleProvider, labelProvider, layoutFactory, new IntIdProvider());
+ }
+
+ public void draw(Writer writer, SvgParameters svgParameters, LayoutParameters layoutParameters,
+ StyleProvider styleProvider, LabelProvider labelProvider, LayoutFactory layoutFactory,
+ IdProvider idProvider) {
+ Graph graph = new NetworkGraphBuilder(network, voltageLevelFilter, idProvider).buildGraph();
+ layoutFactory.create().run(graph, layoutParameters);
+ new SvgWriter(svgParameters, styleProvider, labelProvider).writeSvg(graph, writer);
+ }
+
+ public String drawToString(SvgParameters svgParameters) {
+ try (StringWriter writer = new StringWriter()) {
+ draw(writer, svgParameters);
+ return writer.toString();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/build/GraphBuilder.java b/network-area-diagram/src/main/java/com/powsybl/nad/build/GraphBuilder.java
new file mode 100644
index 000000000..2088b0876
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/build/GraphBuilder.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.build;
+
+import com.powsybl.nad.model.Graph;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface GraphBuilder {
+ Graph buildGraph();
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IdProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IdProvider.java
new file mode 100644
index 000000000..9ec3d44a6
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IdProvider.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.build.iidm;
+
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.ThreeWindingsTransformer;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface IdProvider {
+
+ String createId(Identifiable> identifiable);
+
+ String createId(ThreeWindingsTransformer.Leg leg);
+
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IntIdProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IntIdProvider.java
new file mode 100644
index 000000000..9558d17d6
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/IntIdProvider.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.build.iidm;
+
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.ThreeWindingsTransformer;
+
+/**
+ * @author Florian Dupuy
+ */
+public class IntIdProvider implements IdProvider {
+ private int count;
+
+ public IntIdProvider() {
+ count = 0;
+ }
+
+ @Override
+ public String createId(Identifiable> identifiable) {
+ return nextId();
+ }
+
+ @Override
+ public String createId(ThreeWindingsTransformer.Leg leg) {
+ return nextId();
+ }
+
+ private String nextId() {
+ return String.valueOf(count++);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/NetworkGraphBuilder.java b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/NetworkGraphBuilder.java
new file mode 100644
index 000000000..b2e33275d
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/NetworkGraphBuilder.java
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.build.iidm;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.*;
+import com.powsybl.nad.build.GraphBuilder;
+import com.powsybl.nad.model.*;
+import com.powsybl.nad.utils.iidm.IidmUtils;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author Florian Dupuy
+ */
+public class NetworkGraphBuilder implements GraphBuilder {
+
+ private final Network network;
+ private final IdProvider idProvider;
+ private final Predicate voltageLevelFilter;
+
+ public NetworkGraphBuilder(Network network, Predicate voltageLevelFilter, IdProvider idProvider) {
+ this.network = Objects.requireNonNull(network);
+ this.voltageLevelFilter = voltageLevelFilter;
+ this.idProvider = Objects.requireNonNull(idProvider);
+ }
+
+ public NetworkGraphBuilder(Network network, Predicate voltageLevelFilter) {
+ this(network, voltageLevelFilter, new IntIdProvider());
+ }
+
+ public NetworkGraphBuilder(Network network) {
+ this(network, VoltageLevelFilter.NO_FILTER, new IntIdProvider());
+ }
+
+ @Override
+ public Graph buildGraph() {
+ Graph graph = new Graph();
+ List voltageLevels = network.getVoltageLevelStream()
+ .filter(voltageLevelFilter)
+ .sorted(Comparator.comparing(VoltageLevel::getId))
+ .collect(Collectors.toList());
+ voltageLevels.forEach(vl -> addVoltageLevelGraphNode(vl, graph, true));
+ voltageLevels.forEach(vl -> addGraphEdges(vl, graph));
+ return graph;
+ }
+
+ private VoltageLevelNode addVoltageLevelGraphNode(VoltageLevel vl, Graph graph, boolean visible) {
+ VoltageLevelNode vlNode = new VoltageLevelNode(idProvider.createId(vl), vl.getId(), vl.getNameOrId(), vl.isFictitious(), visible);
+ vl.getBusView().getBusStream()
+ .map(bus -> new BusNode(idProvider.createId(bus), bus.getId()))
+ .forEach(vlNode::addBusNode);
+ graph.addNode(vlNode);
+ if (visible) {
+ graph.addTextNode(vlNode);
+ }
+ return vlNode;
+ }
+
+ private void addGraphEdges(VoltageLevel vl, Graph graph) {
+ vl.getLineStream().forEach(l -> visitLine(vl, l, graph));
+ vl.getTwoWindingsTransformerStream().forEach(twt -> visitTwoWindingsTransformer(vl, twt, graph));
+ vl.getThreeWindingsTransformerStream().forEach(thwt -> visitThreeWindingsTransformer(vl, thwt, graph));
+ vl.getConnectableStream(HvdcConverterStation.class).forEach(hvdc -> visitHvdcConverterStation(hvdc, graph));
+ }
+
+ private void visitLine(VoltageLevel vl, Line line, Graph graph) {
+ addEdge(graph, line, vl, BranchEdge.LINE_EDGE);
+ }
+
+ private void visitTwoWindingsTransformer(VoltageLevel vl, TwoWindingsTransformer twt, Graph graph) {
+ addEdge(graph, twt, vl, BranchEdge.TWO_WT_EDGE);
+ }
+
+ private void visitThreeWindingsTransformer(VoltageLevel vl, ThreeWindingsTransformer thwt, Graph graph) {
+ // check if the transformer was not already added (at the other sides of the transformer)
+ if (graph.containsNode(thwt.getId())) {
+ return;
+ }
+
+ ThreeWtNode tn = new ThreeWtNode(idProvider.createId(thwt), thwt.getId(), thwt.getNameOrId());
+ graph.addNode(tn);
+
+ ThreeWindingsTransformer.Side side;
+ if (thwt.getLeg1().getTerminal().getVoltageLevel() == vl) {
+ side = ThreeWindingsTransformer.Side.ONE;
+ } else if (thwt.getLeg2().getTerminal().getVoltageLevel() == vl) {
+ side = ThreeWindingsTransformer.Side.TWO;
+ } else {
+ side = ThreeWindingsTransformer.Side.THREE;
+ }
+
+ for (ThreeWindingsTransformer.Side s : getSidesArray(side)) {
+ addThreeWtEdge(graph, thwt, tn, s);
+ }
+ }
+
+ private void visitHvdcConverterStation(HvdcConverterStation> converterStation, Graph graph) {
+ // check if the hvdc line was not already added (at the other side of the line)
+ HvdcLine hvdcLine = converterStation.getHvdcLine();
+ if (graph.containsEdge(hvdcLine.getId())) {
+ return;
+ }
+
+ HvdcLine.Side otherSide = (hvdcLine.getConverterStation1().getId().equals(converterStation.getId()))
+ ? HvdcLine.Side.TWO : HvdcLine.Side.ONE;
+
+ Terminal terminal = converterStation.getTerminal();
+ Terminal otherSideTerminal = hvdcLine.getConverterStation(otherSide).getTerminal();
+
+ addEdge(graph, terminal, otherSideTerminal, hvdcLine, BranchEdge.HVDC_LINE_EDGE, otherSide == HvdcLine.Side.ONE);
+ }
+
+ private void addEdge(Graph graph, Branch> branch, VoltageLevel vl, String edgeType) {
+ Branch.Side side = branch.getTerminal(Branch.Side.ONE).getVoltageLevel() == vl ? Branch.Side.ONE : Branch.Side.TWO;
+ // check if the edge was not already added (at the other side of the transformer)
+ if (graph.containsEdge(branch.getId())) {
+ return;
+ }
+
+ Terminal terminalA = branch.getTerminal(side);
+ Terminal terminalB = branch.getTerminal(IidmUtils.getOpposite(side));
+
+ addEdge(graph, terminalA, terminalB, branch, edgeType, side == Branch.Side.TWO);
+ }
+
+ private void addEdge(Graph graph, Terminal terminalA, Terminal terminalB, Identifiable> identifiable, String edgeType, boolean terminalsInReversedOrder) {
+ VoltageLevelNode vlNodeA = graph.getVoltageLevelNode(terminalA.getVoltageLevel().getId())
+ .orElseThrow(() -> new PowsyblException("Cannot add edge, corresponding voltage level is unknown: '" + terminalA.getVoltageLevel().getId() + "'"));
+ VoltageLevelNode vlNodeB = getOrCreateInvisibleVoltageLevelNode(graph, terminalB);
+
+ BusNode busNodeA = getBusNode(graph, terminalA);
+ BusNode busNodeB = getBusNode(graph, terminalB);
+
+ BranchEdge edge = new BranchEdge(idProvider.createId(identifiable), identifiable.getId(), identifiable.getNameOrId(), edgeType);
+ if (!terminalsInReversedOrder) {
+ graph.addEdge(vlNodeA, busNodeA, vlNodeB, busNodeB, edge);
+ } else {
+ graph.addEdge(vlNodeB, busNodeB, vlNodeA, busNodeA, edge);
+ }
+ }
+
+ private void addThreeWtEdge(Graph graph, ThreeWindingsTransformer twt, ThreeWtNode tn, ThreeWindingsTransformer.Side side) {
+ Terminal terminal = twt.getTerminal(side);
+ VoltageLevelNode vlNode = getOrCreateInvisibleVoltageLevelNode(graph, terminal);
+ ThreeWtEdge edge = new ThreeWtEdge(idProvider.createId(IidmUtils.get3wtLeg(twt, side)),
+ twt.getId(), twt.getNameOrId(), IidmUtils.getThreeWtEdgeSideFromIidmSide(side), vlNode.isVisible());
+ graph.addEdge(vlNode, getBusNode(graph, terminal), tn, edge);
+ }
+
+ private BusNode getBusNode(Graph graph, Terminal terminal) {
+ Bus connectableBusA = terminal.getBusView().getConnectableBus();
+ if (connectableBusA == null) {
+ graph.getVoltageLevelNode(terminal.getVoltageLevel().getId()).ifPresent(vlNode -> vlNode.setHasUnknownBusNode(true));
+ return BusNode.UNKNOWN;
+ }
+ return graph.getBusNode(connectableBusA.getId());
+ }
+
+ private VoltageLevelNode getOrCreateInvisibleVoltageLevelNode(Graph graph, Terminal terminal) {
+ VoltageLevel vl = terminal.getVoltageLevel();
+ return graph.getVoltageLevelNode(vl.getId()).orElseGet(() -> addVoltageLevelGraphNode(vl, graph, false));
+ }
+
+ private ThreeWindingsTransformer.Side[] getSidesArray(ThreeWindingsTransformer.Side sideA) {
+ ThreeWindingsTransformer.Side sideB;
+ ThreeWindingsTransformer.Side sideC;
+ if (sideA == ThreeWindingsTransformer.Side.ONE) {
+ sideB = ThreeWindingsTransformer.Side.TWO;
+ sideC = ThreeWindingsTransformer.Side.THREE;
+ } else if (sideA == ThreeWindingsTransformer.Side.TWO) {
+ sideB = ThreeWindingsTransformer.Side.ONE;
+ sideC = ThreeWindingsTransformer.Side.THREE;
+ } else {
+ sideB = ThreeWindingsTransformer.Side.ONE;
+ sideC = ThreeWindingsTransformer.Side.TWO;
+ }
+ return new ThreeWindingsTransformer.Side[] {sideA, sideB, sideC};
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java
new file mode 100644
index 000000000..a261200a3
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.build.iidm;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.iidm.network.*;
+import com.powsybl.nad.utils.iidm.IidmUtils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * @author Florian Dupuy
+ */
+public class VoltageLevelFilter implements Predicate {
+
+ public static final Predicate NO_FILTER = voltageLevel -> true;
+
+ private final Set voltageLevels;
+
+ public VoltageLevelFilter(Set voltageLevels) {
+ this.voltageLevels = voltageLevels;
+ }
+
+ @Override
+ public boolean test(VoltageLevel voltageLevel) {
+ return voltageLevels.contains(voltageLevel);
+ }
+
+ public static VoltageLevelFilter createVoltageLevelDepthFilter(Network network, String voltageLevelId, int depth) {
+ Objects.requireNonNull(network);
+ Objects.requireNonNull(voltageLevelId);
+
+ Set voltageLevels = new HashSet<>();
+ VoltageLevel vl = network.getVoltageLevel(voltageLevelId);
+ if (vl == null) {
+ throw new PowsyblException("Unknown voltage level id '" + voltageLevelId + "'");
+ }
+
+ Set startingSet = new HashSet<>();
+ startingSet.add(vl);
+ traverseVoltageLevels(startingSet, depth, voltageLevels);
+ return new VoltageLevelFilter(voltageLevels);
+ }
+
+ public static VoltageLevelFilter createVoltageLevelsDepthFilter(Network network, List voltageLevelIds, int depth) {
+ Objects.requireNonNull(network);
+ Objects.requireNonNull(voltageLevelIds);
+ Set startingSet = new HashSet<>();
+ for (String voltageLevelId : voltageLevelIds) {
+ VoltageLevel vl = network.getVoltageLevel(voltageLevelId);
+ if (vl == null) {
+ throw new PowsyblException("Unknown voltage level id '" + voltageLevelId + "'");
+ }
+ startingSet.add(vl);
+ }
+
+ Set voltageLevels = new HashSet<>();
+ traverseVoltageLevels(startingSet, depth, voltageLevels);
+ return new VoltageLevelFilter(voltageLevels);
+ }
+
+ public static VoltageLevelFilter createVoltageLevelsFilter(Network network, List voltageLevelIds) {
+ return createVoltageLevelsDepthFilter(network, voltageLevelIds, 0);
+ }
+
+ private static void traverseVoltageLevels(Set voltageLevelsDepth, int depth, Set visitedVoltageLevels) {
+ if (depth < 0) {
+ return;
+ }
+ Set nextDepthVoltageLevels = new HashSet<>();
+ for (VoltageLevel vl : voltageLevelsDepth) {
+ if (!visitedVoltageLevels.contains(vl)) {
+ visitedVoltageLevels.add(vl);
+ vl.visitEquipments(new VlVisitor(nextDepthVoltageLevels, visitedVoltageLevels));
+ }
+ }
+ traverseVoltageLevels(nextDepthVoltageLevels, depth - 1, visitedVoltageLevels);
+ }
+
+ private static class VlVisitor extends DefaultTopologyVisitor {
+ private final Set nextDepthVoltageLevels;
+ private final Set visitedVoltageLevels;
+
+ public VlVisitor(Set nextDepthVoltageLevels, Set visitedVoltageLevels) {
+ this.nextDepthVoltageLevels = nextDepthVoltageLevels;
+ this.visitedVoltageLevels = visitedVoltageLevels;
+ }
+
+ @Override
+ public void visitLine(Line line, Branch.Side side) {
+ visitBranch(line, side);
+ }
+
+ @Override
+ public void visitTwoWindingsTransformer(TwoWindingsTransformer twt, Branch.Side side) {
+ visitBranch(twt, side);
+ }
+
+ @Override
+ public void visitThreeWindingsTransformer(ThreeWindingsTransformer twt, ThreeWindingsTransformer.Side side) {
+ if (side == ThreeWindingsTransformer.Side.ONE) {
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.TWO));
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.THREE));
+ } else if (side == ThreeWindingsTransformer.Side.TWO) {
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.ONE));
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.THREE));
+ } else {
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.ONE));
+ visitTerminal(twt.getTerminal(ThreeWindingsTransformer.Side.TWO));
+ }
+ }
+
+ @Override
+ public void visitHvdcConverterStation(HvdcConverterStation> converterStation) {
+ converterStation.getOtherConverterStation().ifPresent(c -> visitTerminal(c.getTerminal()));
+ }
+
+ private void visitBranch(Branch> branch, Branch.Side side) {
+ visitTerminal(branch.getTerminal(IidmUtils.getOpposite(side)));
+ }
+
+ private void visitTerminal(Terminal terminal) {
+ VoltageLevel voltageLevel = terminal.getVoltageLevel();
+ if (!visitedVoltageLevels.contains(voltageLevel)) {
+ nextDepthVoltageLevels.add(voltageLevel);
+ }
+ }
+ }
+
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/AbstractLayout.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/AbstractLayout.java
new file mode 100644
index 000000000..cee1c330c
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/AbstractLayout.java
@@ -0,0 +1,99 @@
+package com.powsybl.nad.layout;
+
+import com.powsybl.nad.model.*;
+import org.jgrapht.alg.util.Pair;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+public abstract class AbstractLayout implements Layout {
+
+ private Map initialNodePositions = Collections.emptyMap();
+ private Set nodesWithFixedPosition = Collections.emptySet();
+
+ @Override
+ public void run(Graph graph, LayoutParameters layoutParameters) {
+ Objects.requireNonNull(graph);
+ Objects.requireNonNull(layoutParameters);
+
+ nodesLayout(graph, layoutParameters);
+ busNodesLayout(graph, layoutParameters);
+ edgesLayout(graph, layoutParameters);
+
+ computeSize(graph);
+ }
+
+ @Override
+ public Map getInitialNodePositions() {
+ return initialNodePositions;
+ }
+
+ @Override
+ public void setInitialNodePositions(Map initialNodePositions) {
+ Objects.requireNonNull(initialNodePositions);
+ this.initialNodePositions = initialNodePositions;
+ }
+
+ @Override
+ public void setNodesWithFixedPosition(Set nodesWithFixedPosition) {
+ this.nodesWithFixedPosition = nodesWithFixedPosition;
+ }
+
+ @Override
+ public Set getNodesWithFixedPosition() {
+ return nodesWithFixedPosition;
+ }
+
+ public void setFixedNodePositions(Map fixedNodePositions) {
+ setInitialNodePositions(fixedNodePositions);
+ setNodesWithFixedPosition(fixedNodePositions.keySet());
+ }
+
+ protected abstract void nodesLayout(Graph graph, LayoutParameters layoutParameters);
+
+ protected void busNodesLayout(Graph graph, LayoutParameters ignoredLayoutParameters) {
+ Comparator c = Comparator.comparing(bn -> graph.getBusEdges(bn).size());
+ graph.getVoltageLevelNodesStream().forEach(n -> {
+ n.sortBusNodes(c);
+ List sortedNodes = n.getBusNodes();
+ for (int i = 0; i < sortedNodes.size(); i++) {
+ BusNode busNode = sortedNodes.get(i);
+ busNode.setIndex(i);
+ busNode.setNbNeighbouringBusNodes(sortedNodes.size() - 1);
+ busNode.setPosition(n.getPosition());
+ }
+ });
+ }
+
+ protected void fixedTextNodeLayout(Pair nodes, LayoutParameters layoutParameters) {
+ Point fixedShift = layoutParameters.getTextNodeFixedShift();
+ Point textPos = nodes.getFirst().getPosition().shift(fixedShift.getX(), fixedShift.getY());
+ nodes.getSecond().setPosition(textPos);
+ }
+
+ protected void edgesLayout(Graph graph, LayoutParameters layoutParameters) {
+ Objects.requireNonNull(graph);
+ Objects.requireNonNull(layoutParameters);
+ graph.getBranchEdgeStream().forEach(edge -> {
+ setEdgeVisibility(graph.getNode1(edge), edge, BranchEdge.Side.ONE);
+ setEdgeVisibility(graph.getNode2(edge), edge, BranchEdge.Side.TWO);
+ });
+ }
+
+ private void setEdgeVisibility(Node node, BranchEdge branchEdge, BranchEdge.Side side) {
+ if (node instanceof VoltageLevelNode && !((VoltageLevelNode) node).isVisible()) {
+ branchEdge.setVisible(side, false);
+ }
+ }
+
+ private void computeSize(Graph graph) {
+ double[] dims = new double[4];
+ Stream.concat(graph.getTextNodesStream(), graph.getNodesStream()).forEach(node -> {
+ dims[0] = Math.min(dims[0], node.getX());
+ dims[1] = Math.max(dims[1], node.getX());
+ dims[2] = Math.min(dims[2], node.getY());
+ dims[3] = Math.max(dims[3], node.getY());
+ });
+ graph.setDimensions(dims[0], dims[1], dims[2], dims[3]);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayout.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayout.java
new file mode 100644
index 000000000..2eab5f56d
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayout.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+import com.powsybl.nad.model.Edge;
+import com.powsybl.nad.model.Graph;
+import com.powsybl.nad.model.Node;
+import com.powsybl.nad.model.Point;
+
+/**
+ * @author Luma Zamarreño
+ */
+public class BasicFixedLayout extends AbstractLayout {
+
+ @Override
+ protected void nodesLayout(Graph graph, LayoutParameters layoutParameters) {
+ org.jgrapht.Graph jgraphtGraph = graph.getJgraphtGraph(layoutParameters.isTextNodesForceLayout());
+
+ jgraphtGraph.vertexSet().forEach(node -> {
+ Point p = getInitialNodePositions().get(node.getEquipmentId());
+ if (p != null) {
+ node.setPosition(p.getX(), p.getY());
+ }
+ });
+
+ if (!layoutParameters.isTextNodesForceLayout()) {
+ graph.getTextEdgesMap().values().forEach(nodePair -> fixedTextNodeLayout(nodePair, layoutParameters));
+ }
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayoutFactory.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayoutFactory.java
new file mode 100644
index 000000000..341e28b9c
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicFixedLayoutFactory.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+import com.powsybl.nad.model.Point;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author Luma Zamarreño
+ */
+public class BasicFixedLayoutFactory implements LayoutFactory {
+
+ private final Map fixedPositions;
+
+ public BasicFixedLayoutFactory(Map fixedPositions) {
+ Objects.requireNonNull(fixedPositions);
+ this.fixedPositions = fixedPositions;
+ }
+
+ @Override
+ public Layout create() {
+ AbstractLayout layout = new BasicFixedLayout();
+ layout.setFixedNodePositions(fixedPositions);
+ return layout;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java
new file mode 100644
index 000000000..d8567fcbc
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+import com.powsybl.diagram.util.forcelayout.ForceLayout;
+import com.powsybl.diagram.util.forcelayout.Vector;
+import com.powsybl.nad.model.Edge;
+import com.powsybl.nad.model.Graph;
+import com.powsybl.nad.model.Node;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Florian Dupuy
+ */
+public class BasicForceLayout extends AbstractLayout {
+
+ private static final int SCALE = 100;
+
+ @Override
+ protected void nodesLayout(Graph graph, LayoutParameters layoutParameters) {
+ org.jgrapht.Graph jgraphtGraph = graph.getJgraphtGraph(layoutParameters.isTextNodesForceLayout());
+ ForceLayout forceLayout = new ForceLayout<>(jgraphtGraph);
+ forceLayout.setSpringRepulsionFactor(layoutParameters.getSpringRepulsionFactorForceLayout());
+
+ setInitialPositions(forceLayout, graph);
+ Set fixedNodes = getNodesWithFixedPosition().stream()
+ .map(graph::getNode)
+ .flatMap(Optional::stream)
+ .collect(Collectors.toSet());
+ forceLayout.setFixedNodes(fixedNodes);
+
+ forceLayout.execute();
+
+ jgraphtGraph.vertexSet().forEach(node -> {
+ Vector p = forceLayout.getStablePosition(node);
+ node.setPosition(SCALE * p.getX(), SCALE * p.getY());
+ });
+
+ if (!layoutParameters.isTextNodesForceLayout()) {
+ graph.getTextEdgesMap().values().forEach(nodePair -> fixedTextNodeLayout(nodePair, layoutParameters));
+ }
+ }
+
+ private void setInitialPositions(ForceLayout forceLayout, Graph graph) {
+ Map initialPoints = getInitialNodePositions().entrySet().stream()
+ // Only accept positions for nodes in the graph
+ .filter(nodePosition -> graph.getNode(nodePosition.getKey()).isPresent())
+ .collect(Collectors.toMap(
+ nodePosition -> graph.getNode(nodePosition.getKey()).orElseThrow(),
+ nodePosition -> new com.powsybl.diagram.util.forcelayout.Point(
+ nodePosition.getValue().getX() / SCALE,
+ nodePosition.getValue().getY() / SCALE)
+ ));
+ forceLayout.setInitialPoints(initialPoints);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayoutFactory.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayoutFactory.java
new file mode 100644
index 000000000..e470cc51d
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/BasicForceLayoutFactory.java
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+/**
+ * @author Florian Dupuy
+ */
+public class BasicForceLayoutFactory implements LayoutFactory {
+ @Override
+ public Layout create() {
+ return new BasicForceLayout();
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/Layout.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/Layout.java
new file mode 100644
index 000000000..fd86e68e7
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/Layout.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+import com.powsybl.nad.model.Graph;
+import com.powsybl.nad.model.Point;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface Layout {
+ void run(Graph graph, LayoutParameters layoutParameters);
+
+ void setInitialNodePositions(Map initialNodePositions);
+
+ void setNodesWithFixedPosition(Set nodesWithFixedPosition);
+
+ Map getInitialNodePositions();
+
+ Set getNodesWithFixedPosition();
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactory.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactory.java
new file mode 100644
index 000000000..ef2faeeb4
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactory.java
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface LayoutFactory {
+ Layout create();
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutParameters.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutParameters.java
new file mode 100644
index 000000000..47b99502a
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutParameters.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.layout;
+
+import com.powsybl.nad.model.Point;
+
+/**
+ * @author Florian Dupuy
+ */
+public class LayoutParameters {
+ private boolean textNodesForceLayout = false;
+ private double springRepulsionFactorForceLayout = 0.0;
+ private Point textNodeFixedShift = new Point(100, -15);
+
+ public LayoutParameters() {
+ }
+
+ public LayoutParameters(LayoutParameters other) {
+ this.textNodesForceLayout = other.textNodesForceLayout;
+ this.springRepulsionFactorForceLayout = other.springRepulsionFactorForceLayout;
+ this.textNodeFixedShift = new Point(other.textNodeFixedShift.getX(), other.textNodeFixedShift.getY());
+ }
+
+ public boolean isTextNodesForceLayout() {
+ return textNodesForceLayout;
+ }
+
+ public LayoutParameters setTextNodesForceLayout(boolean textNodesForceLayout) {
+ this.textNodesForceLayout = textNodesForceLayout;
+ return this;
+ }
+
+ public LayoutParameters setSpringRepulsionFactorForceLayout(double springRepulsionFactorForceLayout) {
+ this.springRepulsionFactorForceLayout = springRepulsionFactorForceLayout;
+ return this;
+ }
+
+ public double getSpringRepulsionFactorForceLayout() {
+ return springRepulsionFactorForceLayout;
+ }
+
+ public Point getTextNodeFixedShift() {
+ return textNodeFixedShift;
+ }
+
+ public LayoutParameters setTextNodeFixedShift(double textNodeFixedShiftX, double textNodeFixedShiftY) {
+ this.textNodeFixedShift = new Point(textNodeFixedShiftX, textNodeFixedShiftY);
+ return this;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractEdge.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractEdge.java
new file mode 100644
index 000000000..4544f08ae
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractEdge.java
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public abstract class AbstractEdge extends AbstractIdentifiable implements Edge {
+
+ protected AbstractEdge(String diagramId, String equipmentId, String nameOrId) {
+ super(diagramId, equipmentId, nameOrId);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractIdentifiable.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractIdentifiable.java
new file mode 100644
index 000000000..dea861dab
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractIdentifiable.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author Thomas Adam
+ */
+abstract class AbstractIdentifiable implements Identifiable {
+
+ private final String diagramId;
+ private final String equipmentId;
+ private final String name;
+
+ protected AbstractIdentifiable(String diagramId, String equipmentId, String nameOrId) {
+ this.diagramId = Objects.requireNonNull(diagramId);
+ this.equipmentId = equipmentId;
+ this.name = nameOrId;
+ }
+
+ @Override
+ public String getDiagramId() {
+ return diagramId;
+ }
+
+ @Override
+ public String getEquipmentId() {
+ return equipmentId;
+ }
+
+ @Override
+ public Optional getName() {
+ return Optional.ofNullable(name);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractNode.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractNode.java
new file mode 100644
index 000000000..80ac8e1d5
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/AbstractNode.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public abstract class AbstractNode extends AbstractIdentifiable implements Node {
+
+ private int width;
+ private int height;
+ private Point position;
+
+ protected AbstractNode(String diagramId, String equipmentId, String name) {
+ super(diagramId, equipmentId, name);
+ position = new Point();
+ width = 0;
+ height = 0;
+ }
+
+ @Override
+ public void setPosition(Point position) {
+ this.position = position;
+ }
+
+ @Override
+ public void setPosition(double x, double y) {
+ position = new Point(x, y);
+ }
+
+ @Override
+ public Point getPosition() {
+ return position;
+ }
+
+ @Override
+ public double getX() {
+ return position.getX();
+ }
+
+ @Override
+ public double getY() {
+ return position.getY();
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/BranchEdge.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/BranchEdge.java
new file mode 100644
index 000000000..8dc6657fe
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/BranchEdge.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author Florian Dupuy
+ */
+public class BranchEdge extends AbstractEdge {
+
+ public enum Side {
+ ONE, TWO;
+
+ public Side getOpposite() {
+ return this == ONE ? TWO : ONE;
+ }
+ }
+
+ public static final String TWO_WT_EDGE = "TwoWtEdge";
+ public static final String LINE_EDGE = "LineEdge";
+ public static final String HVDC_LINE_EDGE = "HvdcLineEdge";
+
+ private List points1 = Collections.emptyList();
+ private List points2 = Collections.emptyList();
+ private final boolean[] visible = new boolean[] {true, true};
+ private final String type;
+
+ public BranchEdge(String diagramId, String equipmentId, String nameOrId, String type) {
+ super(diagramId, equipmentId, nameOrId);
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public List getPoints(Side side) {
+ Objects.requireNonNull(side);
+ return side == Side.ONE ? getPoints1() : getPoints2();
+ }
+
+ public List getPoints1() {
+ return Collections.unmodifiableList(points1);
+ }
+
+ public List getPoints2() {
+ return Collections.unmodifiableList(points2);
+ }
+
+ public void setPoints(Side side, Point... points) {
+ Objects.requireNonNull(side);
+ if (side == Side.ONE) {
+ setPoints1(points);
+ } else {
+ setPoints2(points);
+ }
+ }
+
+ public void setPoints1(Point... points) {
+ Arrays.stream(points).forEach(Objects::requireNonNull);
+ this.points1 = Arrays.asList(points);
+ }
+
+ public void setPoints2(Point... points) {
+ Arrays.stream(points).forEach(Objects::requireNonNull);
+ this.points2 = Arrays.asList(points);
+ }
+
+ public boolean isVisible(Side side) {
+ Objects.requireNonNull(side);
+ return visible[side.ordinal()];
+ }
+
+ public void setVisible(Side side, boolean visible) {
+ Objects.requireNonNull(side);
+ this.visible[side.ordinal()] = visible;
+ }
+
+ public double getEdgeStartAngle(Side side) {
+ List points = getPoints(side);
+ return points.get(0).getAngle(points.get(1));
+ }
+
+ public double getEdgeEndAngle(Side side) {
+ List points = getPoints(side);
+ return points.get(points.size() - 2).getAngle(points.get(points.size() - 1));
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/BusNode.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/BusNode.java
new file mode 100644
index 000000000..2b58af4cf
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/BusNode.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public class BusNode extends AbstractNode {
+
+ public static final BusNode UNKNOWN = new BusNode("", "");
+
+ private int index;
+ private int nbNeighbouringBusNodes;
+
+ public BusNode(String diagramId, String id) {
+ super(diagramId, id, null);
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setNbNeighbouringBusNodes(int nbNeighbouringBusNodes) {
+ this.nbNeighbouringBusNodes = nbNeighbouringBusNodes;
+ }
+
+ public int getNbNeighbouringBusNodes() {
+ return nbNeighbouringBusNodes;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/Edge.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/Edge.java
new file mode 100644
index 000000000..7c6c0dc4d
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/Edge.java
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface Edge extends Identifiable {
+
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/Graph.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/Graph.java
new file mode 100644
index 000000000..100a69854
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/Graph.java
@@ -0,0 +1,325 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import org.jgrapht.alg.util.Pair;
+import org.jgrapht.graph.Pseudograph;
+import org.jgrapht.graph.WeightedPseudograph;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Florian Dupuy
+ */
+public class Graph {
+
+ private final Map nodes = new LinkedHashMap<>();
+ private final Map busNodes = new LinkedHashMap<>();
+ private final Map edges = new LinkedHashMap<>();
+ private double minX = 0;
+ private double minY = 0;
+ private double maxX = 0;
+ private double maxY = 0;
+
+ private final org.jgrapht.Graph voltageLevelGraph = new WeightedPseudograph<>(Edge.class);
+ private final org.jgrapht.Graph busGraph = new Pseudograph<>(Edge.class);
+ private final Map> textEdges = new LinkedHashMap<>();
+
+ public void addNode(Node node) {
+ Objects.requireNonNull(node);
+ nodes.put(node.getEquipmentId(), node);
+ voltageLevelGraph.addVertex(node);
+ if (node instanceof VoltageLevelNode) {
+ ((VoltageLevelNode) node).getBusNodeStream().forEach(b -> {
+ busGraph.addVertex(b);
+ busNodes.put(b.getEquipmentId(), b);
+ });
+ }
+ if (node instanceof ThreeWtNode) {
+ busGraph.addVertex(node);
+ }
+ }
+
+ public void addTextNode(VoltageLevelNode vlNode) {
+ Objects.requireNonNull(vlNode);
+ addEdge(vlNode, new TextNode(vlNode.getDiagramId() + "_text"), new TextEdge(vlNode.getDiagramId() + "_edge"));
+ }
+
+ public void addEdge(VoltageLevelNode node1, BusNode busNode1,
+ VoltageLevelNode node2, BusNode busNode2, BranchEdge edge) {
+ addVoltageLevelsEdge(node1, node2, edge);
+ addBusesEdge(busNode1, busNode2, edge);
+ }
+
+ public void addEdge(VoltageLevelNode vlNode, BusNode busNode, ThreeWtNode tNode, ThreeWtEdge edge) {
+ addVoltageLevelsEdge(vlNode, tNode, edge);
+ addBusesEdge(busNode, tNode, edge);
+ }
+
+ public void addEdge(VoltageLevelNode vlNode, TextNode textNode, TextEdge edge) {
+ Objects.requireNonNull(vlNode);
+ Objects.requireNonNull(textNode);
+ Objects.requireNonNull(edge);
+ textEdges.put(edge, Pair.of(vlNode, textNode));
+ }
+
+ private void addVoltageLevelsEdge(Node node1, Node node2, Edge edge) {
+ Objects.requireNonNull(node1);
+ Objects.requireNonNull(node2);
+ Objects.requireNonNull(edge);
+ edges.put(edge.getEquipmentId(), edge);
+ voltageLevelGraph.addEdge(node1, node2, edge);
+ }
+
+ private void addBusesEdge(BusNode node1, Node node2, Edge edge) {
+ Objects.requireNonNull(node1);
+ Objects.requireNonNull(node2);
+ Objects.requireNonNull(edge);
+ if (node1 == BusNode.UNKNOWN || node2 == BusNode.UNKNOWN) {
+ busGraph.addVertex(BusNode.UNKNOWN);
+ }
+ busGraph.addEdge(node1, node2, edge);
+ }
+
+ public Stream getBusNodesStream() {
+ return busNodes.values().stream();
+ }
+
+ public Stream getNodesStream() {
+ return voltageLevelGraph.vertexSet().stream();
+ }
+
+ public Stream getVoltageLevelNodesStream() {
+ return nodes.values().stream().filter(VoltageLevelNode.class::isInstance).map(VoltageLevelNode.class::cast);
+ }
+
+ public Stream getThreeWtNodesStream() {
+ return nodes.values().stream().filter(ThreeWtNode.class::isInstance).map(ThreeWtNode.class::cast);
+ }
+
+ public Stream getTextNodesStream() {
+ return textEdges.values().stream().map(Pair::getSecond);
+ }
+
+ public Collection> getVoltageLevelTextPairs() {
+ return Collections.unmodifiableCollection(textEdges.values());
+ }
+
+ public Stream getEdgesStream() {
+ return edges.values().stream();
+ }
+
+ public Collection getEdges() {
+ return Collections.unmodifiableCollection(voltageLevelGraph.edgeSet());
+ }
+
+ public Stream getEdgeStream(Node node) {
+ return voltageLevelGraph.edgesOf(node).stream();
+ }
+
+ public Stream getThreeWtEdgeStream(ThreeWtNode node) {
+ return voltageLevelGraph.edgesOf(node).stream().filter(ThreeWtEdge.class::isInstance).map(ThreeWtEdge.class::cast);
+ }
+
+ public Stream getBranchEdgeStream(Node node) {
+ return getEdgeStream(node)
+ .filter(BranchEdge.class::isInstance)
+ .map(BranchEdge.class::cast);
+ }
+
+ public Collection getBusEdges(BusNode busNode) {
+ return busGraph.edgesOf(busNode);
+ }
+
+ public Stream getBranchEdgeStream() {
+ return voltageLevelGraph.edgeSet().stream()
+ .filter(BranchEdge.class::isInstance)
+ .map(BranchEdge.class::cast);
+ }
+
+ public List getBranchEdges() {
+ return getBranchEdgeStream().collect(Collectors.toList());
+ }
+
+ public Stream getTextEdgesStream() {
+ return textEdges.keySet().stream();
+ }
+
+ public List getTextEdges() {
+ return getTextEdgesStream().collect(Collectors.toList());
+ }
+
+ public Map> getTextEdgesMap() {
+ return Collections.unmodifiableMap(textEdges);
+ }
+
+ public Stream getNonMultiBranchEdgesStream() {
+ return voltageLevelGraph.edgeSet().stream()
+ .filter(BranchEdge.class::isInstance)
+ .map(BranchEdge.class::cast)
+ .filter(e -> voltageLevelGraph.getAllEdges(voltageLevelGraph.getEdgeSource(e), voltageLevelGraph.getEdgeTarget(e)).size() == 1);
+ }
+
+ public Stream> getMultiBranchEdgesStream() {
+ return voltageLevelGraph.edgeSet().stream()
+ .filter(e -> !isLoop(e))
+ .map(e -> voltageLevelGraph.getAllEdges(voltageLevelGraph.getEdgeSource(e), voltageLevelGraph.getEdgeTarget(e)))
+ .filter(e -> e.size() > 1)
+ .distinct()
+ .map(e -> e.stream().filter(BranchEdge.class::isInstance).map(BranchEdge.class::cast).collect(Collectors.toList()))
+ .filter(e -> e.size() > 1);
+ }
+
+ public Map> getLoopBranchEdgesMap() {
+ return voltageLevelGraph.vertexSet().stream()
+ .map(n -> voltageLevelGraph.getAllEdges(n, n).stream()
+ .filter(BranchEdge.class::isInstance).map(BranchEdge.class::cast)
+ .collect(Collectors.toList()))
+ .filter(l -> !l.isEmpty())
+ .collect(Collectors.toMap(l -> getVoltageLevelNode1(l.get(0)), l -> l));
+ }
+
+ public Stream getThreeWtEdgesStream() {
+ return voltageLevelGraph.edgeSet().stream()
+ .filter(ThreeWtEdge.class::isInstance)
+ .map(ThreeWtEdge.class::cast);
+ }
+
+ public List getThreeWtEdges() {
+ return getThreeWtEdgesStream().collect(Collectors.toList());
+ }
+
+ public Optional getNode(String equipmentId) {
+ return Optional.ofNullable(nodes.get(equipmentId));
+ }
+
+ public Optional getVoltageLevelNode(String voltageLevelId) {
+ return getNode(voltageLevelId).filter(VoltageLevelNode.class::isInstance).map(VoltageLevelNode.class::cast);
+ }
+
+ public VoltageLevelNode getVoltageLevelNode(TextEdge textEdge) {
+ return textEdges.get(textEdge).getFirst();
+ }
+
+ public BusNode getBusNode(String busId) {
+ return busNodes.get(busId);
+ }
+
+ public org.jgrapht.Graph getJgraphtGraph(boolean includeTextNodes) {
+ if (includeTextNodes) {
+ org.jgrapht.Graph graphWithTextNodes = new WeightedPseudograph<>(Edge.class);
+ voltageLevelGraph.vertexSet().forEach(graphWithTextNodes::addVertex);
+ voltageLevelGraph.edgeSet().forEach(e -> graphWithTextNodes.addEdge(voltageLevelGraph.getEdgeSource(e), voltageLevelGraph.getEdgeTarget(e), e));
+ textEdges.values().forEach(nodePair -> graphWithTextNodes.addVertex(nodePair.getSecond()));
+ textEdges.forEach((edge, nodePair) -> {
+ graphWithTextNodes.addEdge(nodePair.getFirst(), nodePair.getSecond(), edge);
+ graphWithTextNodes.setEdgeWeight(edge, 1);
+ });
+ return graphWithTextNodes;
+ } else {
+ return voltageLevelGraph;
+ }
+ }
+
+ public double getWidth() {
+ return maxX - minX;
+ }
+
+ public double getHeight() {
+ return maxY - minY;
+ }
+
+ public double getMinX() {
+ return minX;
+ }
+
+ public double getMinY() {
+ return minY;
+ }
+
+ public double getMaxX() {
+ return maxX;
+ }
+
+ public double getMaxY() {
+ return maxY;
+ }
+
+ public void setDimensions(double minX, double maxX, double minY, double maxY) {
+ this.minX = minX;
+ this.maxX = maxX;
+ this.minY = minY;
+ this.maxY = maxY;
+ }
+
+ public Node getNode1(Edge edge) {
+ return voltageLevelGraph.getEdgeSource(edge);
+ }
+
+ public Node getNode2(Edge edge) {
+ return voltageLevelGraph.getEdgeTarget(edge);
+ }
+
+ public VoltageLevelNode getVoltageLevelNode(BranchEdge edge, BranchEdge.Side side) {
+ return side == BranchEdge.Side.ONE ? getVoltageLevelNode1(edge) : getVoltageLevelNode2(edge);
+ }
+
+ public VoltageLevelNode getVoltageLevelNode1(BranchEdge edge) {
+ return (VoltageLevelNode) voltageLevelGraph.getEdgeSource(edge);
+ }
+
+ public VoltageLevelNode getVoltageLevelNode2(BranchEdge edge) {
+ return (VoltageLevelNode) voltageLevelGraph.getEdgeTarget(edge);
+ }
+
+ public VoltageLevelNode getVoltageLevelNode(ThreeWtEdge edge) {
+ return (VoltageLevelNode) voltageLevelGraph.getEdgeSource(edge);
+ }
+
+ public ThreeWtNode getThreeWtNode(ThreeWtEdge edge) {
+ return (ThreeWtNode) voltageLevelGraph.getEdgeTarget(edge);
+ }
+
+ public BusNode getBusGraphNode(BranchEdge edge, BranchEdge.Side side) {
+ return (BusNode) (side == BranchEdge.Side.ONE ? getBusGraphNode1(edge) : getBusGraphNode2(edge));
+ }
+
+ public BusNode getBusGraphNode(ThreeWtEdge edge) {
+ return (BusNode) getBusGraphNode1(edge);
+ }
+
+ public Node getBusGraphNode1(Edge edge) {
+ return busGraph.getEdgeSource(edge);
+ }
+
+ public Node getBusGraphNode2(Edge edge) {
+ return busGraph.getEdgeTarget(edge);
+ }
+
+ public boolean containsEdge(String equipmentId) {
+ return edges.containsKey(equipmentId);
+ }
+
+ public boolean containsNode(String equipmentId) {
+ return nodes.containsKey(equipmentId);
+ }
+
+ public boolean isLoop(Edge edge) {
+ return getNode1(edge) == getNode2(edge);
+ }
+
+ public Map getNodePositions() {
+ return getVoltageLevelNodesStream()
+ .filter(VoltageLevelNode::isVisible)
+ .collect(Collectors.toMap(
+ VoltageLevelNode::getEquipmentId,
+ VoltageLevelNode::getPosition
+ ));
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/Identifiable.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/Identifiable.java
new file mode 100644
index 000000000..c42e80e41
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/Identifiable.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Optional;
+
+/**
+ * @author Thomas Adam
+ */
+public interface Identifiable {
+
+ String getDiagramId();
+
+ String getEquipmentId();
+
+ Optional getName();
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/Node.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/Node.java
new file mode 100644
index 000000000..0fdb36cf0
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/Node.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface Node extends Identifiable {
+
+ void setPosition(Point point);
+
+ Point getPosition();
+
+ void setPosition(double x, double y);
+
+ double getX();
+
+ double getY();
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/Point.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/Point.java
new file mode 100644
index 000000000..c1c8bf4b7
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/Point.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Objects;
+
+/**
+ * @author Florian Dupuy
+ */
+public class Point {
+
+ private final double x;
+ private final double y;
+
+ public Point(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public Point() {
+ this(0, 0);
+ }
+
+ public static Point createMiddlePoint(Point point1, Point point2) {
+ Objects.requireNonNull(point1);
+ Objects.requireNonNull(point2);
+ return new Point(0.5 * (point1.x + point2.x), 0.5 * (point1.y + point2.y));
+ }
+
+ public double distanceSquare(Point other) {
+ Objects.requireNonNull(other);
+ double dx = other.x - x;
+ double dy = other.y - y;
+ return dx * dx + dy * dy;
+ }
+
+ public double distance(Point other) {
+ Objects.requireNonNull(other);
+ return Math.sqrt(distanceSquare(other));
+ }
+
+ public Point shiftRhoTheta(double rho, double theta) {
+ return shift(rho * Math.cos(theta), rho * Math.sin(theta));
+ }
+
+ public Point shift(double shiftX, double shiftY) {
+ return new Point(x + shiftX, y + shiftY);
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public double getX() {
+ return x;
+ }
+
+ public Point atDistance(double dist, Point direction) {
+ double r = dist / distance(direction);
+ return new Point(x + r * (direction.x - x),
+ y + r * (direction.y - y));
+ }
+
+ public Point atDistance(double dist, double angle) {
+ return new Point(x + dist * Math.cos(angle),
+ y + dist * Math.sin(angle));
+ }
+
+ public double getAngle(Point other) {
+ return Math.atan2(other.y - y, other.x - x);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/TextEdge.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/TextEdge.java
new file mode 100644
index 000000000..acfcbe9fe
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/TextEdge.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author Florian Dupuy
+ */
+public class TextEdge extends AbstractEdge {
+
+ private Point[] points;
+
+ public TextEdge(String diagramId) {
+ super(diagramId, null, null);
+ }
+
+ public void setPoints(Point... points) {
+ this.points = points;
+ }
+
+ public List getPoints() {
+ return Arrays.asList(points);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/TextNode.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/TextNode.java
new file mode 100644
index 000000000..cdbf01e27
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/TextNode.java
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public class TextNode extends AbstractNode {
+
+ public TextNode(String diagramId) {
+ super(diagramId, null, null);
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtEdge.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtEdge.java
new file mode 100644
index 000000000..3e167b9b8
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtEdge.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Florian Dupuy
+ */
+public class ThreeWtEdge extends AbstractEdge {
+
+ public enum Side {
+ ONE, TWO, THREE;
+ }
+
+ private Side side;
+
+ private List points;
+
+ private final boolean visible;
+
+ public ThreeWtEdge(String diagramId, String equipmentId, String transformerName, Side side, boolean visible) {
+ super(diagramId, equipmentId, transformerName);
+ this.side = side;
+ this.visible = visible;
+ }
+
+ public void setPoints(Point point1, Point point2) {
+ this.points = Arrays.asList(point1, point2);
+ }
+
+ public List getPoints() {
+ return Collections.unmodifiableList(points);
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public Side getSide() {
+ return side;
+ }
+
+ public double getEdgeAngle() {
+ return points.get(0).getAngle(points.get(1));
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtNode.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtNode.java
new file mode 100644
index 000000000..8e7f54482
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/ThreeWtNode.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+/**
+ * @author Florian Dupuy
+ */
+public class ThreeWtNode extends AbstractNode {
+
+ public ThreeWtNode(String diagramId, String equipmentId, String nameOrId) {
+ super(diagramId, equipmentId, nameOrId);
+ }
+
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/model/VoltageLevelNode.java b/network-area-diagram/src/main/java/com/powsybl/nad/model/VoltageLevelNode.java
new file mode 100644
index 000000000..7289a7219
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/model/VoltageLevelNode.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.model;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+/**
+ * @author Florian Dupuy
+ */
+public class VoltageLevelNode extends AbstractNode {
+
+ private final List busNodes = new ArrayList<>();
+ private final boolean fictitious;
+ private final boolean visible;
+ private boolean hasUnknownBusNode = false;
+
+ public VoltageLevelNode(String diagramId, String equipmentId, String nameOrId, boolean fictitious) {
+ this(diagramId, equipmentId, nameOrId, fictitious, true);
+ }
+
+ public VoltageLevelNode(String diagramId, String equipmentId, String nameOrId, boolean fictitious, boolean visible) {
+ super(diagramId, equipmentId, nameOrId);
+ this.fictitious = fictitious;
+ this.visible = visible;
+ }
+
+ public void addBusNode(BusNode busNode) {
+ Objects.requireNonNull(busNode);
+ busNodes.add(busNode);
+ }
+
+ public List getBusNodes() {
+ return Collections.unmodifiableList(busNodes);
+ }
+
+ public Stream getBusNodeStream() {
+ return busNodes.stream();
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void sortBusNodes(Comparator super BusNode> c) {
+ busNodes.sort(c);
+ }
+
+ public void setHasUnknownBusNode(boolean hasUnknownBusNode) {
+ this.hasUnknownBusNode = hasUnknownBusNode;
+ }
+
+ public boolean hasUnknownBusNode() {
+ return hasUnknownBusNode;
+ }
+
+ public boolean isFictitious() {
+ return fictitious;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/AbstractStyleProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/AbstractStyleProvider.java
new file mode 100644
index 000000000..39ff89a4e
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/AbstractStyleProvider.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.commons.config.BaseVoltagesConfig;
+import com.powsybl.nad.model.*;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Florian Dupuy
+ */
+public abstract class AbstractStyleProvider implements StyleProvider {
+
+ private final BaseVoltagesConfig baseVoltagesConfig;
+
+ protected AbstractStyleProvider() {
+ this(BaseVoltagesConfig.fromPlatformConfig());
+ }
+
+ protected AbstractStyleProvider(BaseVoltagesConfig baseVoltagesConfig) {
+ this.baseVoltagesConfig = Objects.requireNonNull(baseVoltagesConfig);
+ }
+
+ @Override
+ public String getStyleDefs() {
+ StringBuilder styleSheetBuilder = new StringBuilder("\n");
+ for (URL cssUrl : getCssUrls()) {
+ try {
+ styleSheetBuilder.append(new String(IOUtils.toByteArray(cssUrl), StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new UncheckedIOException("Can't read css file " + cssUrl.getPath(), e);
+ }
+ }
+ return styleSheetBuilder.toString()
+ .replace("\r\n", "\n") // workaround for https://bugs.openjdk.java.net/browse/JDK-8133452
+ .replace("\r", "\n");
+ }
+
+ protected List getCssUrls() {
+ return getCssFilenames().stream()
+ .map(n -> getClass().getResource("/" + n))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getNodeStyleClasses(Node node) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getNodeStyleClasses(BusNode busNode) {
+ return busNode == BusNode.UNKNOWN ? Collections.singletonList(UNKNOWN_BUSNODE_CLASS) : Collections.emptyList();
+ }
+
+ @Override
+ public List getEdgeStyleClasses(Edge edge) {
+ List result = new ArrayList<>();
+ if (isDisconnected(edge)) {
+ result.add(DISCONNECTED_CLASS);
+ }
+ getBaseVoltageStyle(edge).ifPresent(result::add);
+ return result;
+ }
+
+ @Override
+ public List getSideEdgeStyleClasses(BranchEdge edge, BranchEdge.Side side) {
+ Objects.requireNonNull(side);
+ List result = new ArrayList<>();
+ if (isDisconnected(edge, side)) {
+ result.add(DISCONNECTED_CLASS);
+ }
+ getBaseVoltageStyle(edge, side).ifPresent(result::add);
+ return result;
+ }
+
+ @Override
+ public List getEdgeInfoStyles(EdgeInfo info) {
+ List styles = new LinkedList<>();
+ if (info.getInfoType().equals(EdgeInfo.ACTIVE_POWER)) {
+ styles.add(CLASSES_PREFIX + "active");
+ } else if (info.getInfoType().equals(EdgeInfo.REACTIVE_POWER)) {
+ styles.add(CLASSES_PREFIX + "reactive");
+ }
+ info.getDirection().ifPresent(direction -> styles.add(
+ CLASSES_PREFIX + (direction == EdgeInfo.Direction.IN ? "state-in" : "state-out")));
+ return styles;
+ }
+
+ @Override
+ public List getThreeWtNodeStyle(ThreeWtNode threeWtNode, ThreeWtEdge.Side side) {
+ Objects.requireNonNull(side);
+ List result = new ArrayList<>();
+ if (isDisconnected(threeWtNode, side)) {
+ result.add(DISCONNECTED_CLASS);
+ }
+ getBaseVoltageStyle(threeWtNode, side).ifPresent(result::add);
+ return result;
+ }
+
+ protected abstract boolean isDisconnected(Edge edge);
+
+ protected abstract boolean isDisconnected(BranchEdge edge, BranchEdge.Side side);
+
+ protected abstract boolean isDisconnected(ThreeWtNode threeWtNode, ThreeWtEdge.Side side);
+
+ protected abstract Optional getBaseVoltageStyle(Edge edge);
+
+ protected abstract Optional getBaseVoltageStyle(BranchEdge edge, BranchEdge.Side side);
+
+ protected abstract Optional getBaseVoltageStyle(ThreeWtNode threeWtNode, ThreeWtEdge.Side side);
+
+ protected Optional getBaseVoltageStyle(double nominalV) {
+ return baseVoltagesConfig.getBaseVoltageName(nominalV, baseVoltagesConfig.getDefaultProfile())
+ .map(bvName -> CLASSES_PREFIX + bvName);
+ }
+
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/DefaultEdgeRendering.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/DefaultEdgeRendering.java
new file mode 100644
index 000000000..b611420bc
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/DefaultEdgeRendering.java
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.nad.model.*;
+
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * @author Florian Dupuy
+ */
+public class DefaultEdgeRendering implements EdgeRendering {
+
+ @Override
+ public void run(Graph graph, SvgParameters svgParameters) {
+ graph.getNonMultiBranchEdgesStream().forEach(edge -> computeSingleBranchEdgeCoordinates(graph, edge, svgParameters));
+ graph.getMultiBranchEdgesStream().forEach(edges -> computeMultiBranchEdgesCoordinates(graph, edges, svgParameters));
+ graph.getLoopBranchEdgesMap().forEach((node, edges) -> loopEdgesLayout(graph, node, edges, svgParameters));
+ graph.getThreeWtNodesStream().forEach(threeWtNode -> computeThreeWtEdgeCoordinates(graph, threeWtNode, svgParameters));
+ graph.getTextEdgesMap().forEach((edge, nodes) -> computeTextEdgeLayoutCoordinates(nodes.getFirst(), nodes.getSecond(), edge));
+ }
+
+ private void computeTextEdgeLayoutCoordinates(Node node1, Node node2, TextEdge edge) {
+ edge.setPoints(node1.getPosition(), node2.getPosition());
+ }
+
+ private void computeSingleBranchEdgeCoordinates(Graph graph, BranchEdge edge, SvgParameters svgParameters) {
+ Node node1 = graph.getBusGraphNode1(edge);
+ Node node2 = graph.getBusGraphNode2(edge);
+
+ Point direction1 = getDirection(node2, () -> graph.getNode2(edge));
+ Point edgeStart1 = computeEdgeStart(node1, direction1, graph.getVoltageLevelNode1(edge), svgParameters);
+
+ Point direction2 = getDirection(node1, () -> graph.getNode1(edge));
+ Point edgeStart2 = computeEdgeStart(node2, direction2, graph.getVoltageLevelNode2(edge), svgParameters);
+
+ Point middle = Point.createMiddlePoint(edgeStart1, edgeStart2);
+ if (edge.getType().equals(BranchEdge.TWO_WT_EDGE)) {
+ double radius = svgParameters.getTransformerCircleRadius();
+ edge.setPoints1(edgeStart1, middle.atDistance(1.5 * radius, direction2));
+ edge.setPoints2(edgeStart2, middle.atDistance(1.5 * radius, direction1));
+ } else {
+ edge.setPoints1(edgeStart1, middle);
+ edge.setPoints2(edgeStart2, middle);
+ }
+ }
+
+ private Point getDirection(Node directionBusGraphNode, Supplier vlNodeSupplier) {
+ if (directionBusGraphNode == BusNode.UNKNOWN) {
+ return vlNodeSupplier.get().getPosition();
+ }
+ return directionBusGraphNode.getPosition();
+ }
+
+ private Point computeEdgeStart(Node node, Point direction, VoltageLevelNode vlNode, SvgParameters svgParameters) {
+ // If edge not connected to a bus node on that side, we use corresponding voltage level with specific extra radius
+ if (node == BusNode.UNKNOWN && vlNode != null) {
+ double unknownBusRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) + svgParameters.getUnknownBusNodeExtraRadius();
+ return vlNode.getPosition().atDistance(unknownBusRadius, direction);
+ }
+
+ Point edgeStart = node.getPosition();
+ if (node instanceof BusNode && vlNode != null) {
+ double busAnnulusOuterRadius = SvgWriter.getBusAnnulusOuterRadius((BusNode) node, vlNode, svgParameters);
+ edgeStart = edgeStart.atDistance(busAnnulusOuterRadius - svgParameters.getEdgeStartShift(), direction);
+ }
+ return edgeStart;
+ }
+
+ private void computeMultiBranchEdgesCoordinates(Graph graph, List edges, SvgParameters svgParameters) {
+ BranchEdge firstEdge = edges.iterator().next();
+ VoltageLevelNode nodeA = graph.getVoltageLevelNode1(firstEdge);
+ VoltageLevelNode nodeB = graph.getVoltageLevelNode2(firstEdge);
+ Point pointA = nodeA.getPosition();
+ Point pointB = nodeB.getPosition();
+
+ double dx = pointB.getX() - pointA.getX();
+ double dy = pointB.getY() - pointA.getY();
+ double angle = Math.atan2(dy, dx);
+
+ int nbForks = edges.size();
+ double forkAperture = svgParameters.getEdgesForkAperture();
+ double forkLength = svgParameters.getEdgesForkLength();
+ double angleStep = forkAperture / (nbForks - 1);
+
+ int i = 0;
+ for (BranchEdge edge : edges) {
+ if (2 * i + 1 == nbForks) { // in the middle, hence alpha = 0
+ computeSingleBranchEdgeCoordinates(graph, edge, svgParameters);
+ } else {
+ double alpha = -forkAperture / 2 + i * angleStep;
+ double angleForkA = angle - alpha;
+ double angleForkB = angle + Math.PI + alpha;
+
+ Point forkA = pointA.shift(forkLength * Math.cos(angleForkA), forkLength * Math.sin(angleForkA));
+ Point forkB = pointB.shift(forkLength * Math.cos(angleForkB), forkLength * Math.sin(angleForkB));
+ Point middle = Point.createMiddlePoint(forkA, forkB);
+ BranchEdge.Side sideA = graph.getNode1(edge) == nodeA ? BranchEdge.Side.ONE : BranchEdge.Side.TWO;
+
+ computeHalfForkCoordinates(graph, svgParameters, nodeA, edge, forkA, middle, sideA);
+ computeHalfForkCoordinates(graph, svgParameters, nodeB, edge, forkB, middle, sideA.getOpposite());
+ }
+ i++;
+ }
+ }
+
+ private void computeHalfForkCoordinates(Graph graph, SvgParameters svgParameters, VoltageLevelNode node, BranchEdge edge, Point fork, Point middle, BranchEdge.Side side) {
+ Node busNodeA = side == BranchEdge.Side.ONE ? graph.getBusGraphNode1(edge) : graph.getBusGraphNode2(edge);
+ Point edgeStart = computeEdgeStart(busNodeA, fork, node, svgParameters);
+ Point endFork = edge.getType().equals(BranchEdge.TWO_WT_EDGE)
+ ? middle.atDistance(1.5 * svgParameters.getTransformerCircleRadius(), fork)
+ : middle;
+ edge.setPoints(side, edgeStart, fork, endFork);
+ }
+
+ private void loopEdgesLayout(Graph graph, VoltageLevelNode node, List loopEdges, SvgParameters svgParameters) {
+ List angles = computeLoopAngles(graph, loopEdges, node, svgParameters);
+ int i = 0;
+ for (BranchEdge edge : loopEdges) {
+ double angle = angles.get(i++);
+ Point middle = node.getPosition().atDistance(svgParameters.getLoopDistance(), angle);
+ loopEdgesHalfLayout(graph, node, svgParameters, edge, BranchEdge.Side.ONE, angle, middle);
+ loopEdgesHalfLayout(graph, node, svgParameters, edge, BranchEdge.Side.TWO, angle, middle);
+ }
+ }
+
+ private void loopEdgesHalfLayout(Graph graph, VoltageLevelNode node, SvgParameters svgParameters,
+ BranchEdge edge, BranchEdge.Side side, double angle, Point middle) {
+
+ int sideSign = side == BranchEdge.Side.ONE ? -1 : 1;
+ double startAngle = angle + sideSign * svgParameters.getLoopEdgesAperture() / 2;
+ double radius = svgParameters.getTransformerCircleRadius();
+ double controlsDist = svgParameters.getLoopControlDistance();
+ boolean isTwoWt = edge.getType().equals(BranchEdge.TWO_WT_EDGE);
+ double endAngle = angle + sideSign * Math.PI / 2;
+
+ Point fork = node.getPosition().atDistance(svgParameters.getEdgesForkLength(), startAngle);
+ Point edgeStart = computeEdgeStart(graph.getBusGraphNode(edge, side), fork, node, svgParameters);
+ Point control1a = fork.atDistance(controlsDist, startAngle);
+ Point middle1 = isTwoWt ? middle.atDistance(1.5 * radius, endAngle) : middle;
+ Point control1b = middle1.atDistance(isTwoWt ? Math.max(0, controlsDist - 1.5 * radius) : controlsDist, endAngle);
+
+ edge.setPoints(side, edgeStart, fork, control1a, control1b, middle1);
+ }
+
+ private List computeLoopAngles(Graph graph, List loopEdges, Node node, SvgParameters svgParameters) {
+ int nbLoops = loopEdges.size();
+
+ List anglesOtherEdges = graph.getBranchEdgeStream(node)
+ .filter(e -> !loopEdges.contains(e))
+ .mapToDouble(e -> getAngle(e, graph, node))
+ .sorted().boxed().collect(Collectors.toList());
+
+ List loopAngles = new ArrayList<>();
+ if (anglesOtherEdges.size() > 0) {
+ anglesOtherEdges.add(anglesOtherEdges.get(0) + 2 * Math.PI);
+ double apertureWithMargin = svgParameters.getLoopEdgesAperture() * 1.2;
+
+ double[] deltaAngles = new double[anglesOtherEdges.size() - 1];
+ int nbSeparatedSlots = 0;
+ int nbSharedSlots = 0;
+ for (int i = 0; i < anglesOtherEdges.size() - 1; i++) {
+ deltaAngles[i] = anglesOtherEdges.get(i + 1) - anglesOtherEdges.get(i);
+ nbSeparatedSlots += deltaAngles[i] > apertureWithMargin ? 1 : 0;
+ nbSharedSlots += Math.floor(deltaAngles[i] / apertureWithMargin);
+ }
+
+ List sortedIndices = IntStream.range(0, deltaAngles.length)
+ .boxed().sorted(Comparator.comparingDouble(i -> deltaAngles[i]))
+ .collect(Collectors.toList());
+
+ if (nbLoops <= nbSeparatedSlots) {
+ // Place loops in "slots" separated by non-loop edges
+ for (int i = sortedIndices.size() - nbLoops; i < sortedIndices.size(); i++) {
+ int iSorted = sortedIndices.get(i);
+ loopAngles.add((anglesOtherEdges.get(iSorted) + anglesOtherEdges.get(iSorted + 1)) / 2);
+ }
+ } else if (nbLoops <= nbSharedSlots) {
+ // Place the maximum of loops in "slots" separated by non-loop edges, and put the excessive ones in the bigger "slots"
+ int nbExcessiveRemaining = nbLoops - nbSeparatedSlots;
+ for (int i = sortedIndices.size() - 1; i >= 0; i--) {
+ int iSorted = sortedIndices.get(i);
+ int nbAvailableSlots = (int) Math.floor(deltaAngles[iSorted] / apertureWithMargin);
+ if (nbAvailableSlots == 0) {
+ break;
+ }
+ int nbLoopsInDelta = Math.min(nbAvailableSlots, nbExcessiveRemaining + 1);
+ double extraSpace = deltaAngles[iSorted] - svgParameters.getLoopEdgesAperture() * nbLoopsInDelta; // extra space without margins
+ double intraSpace = extraSpace / (nbLoopsInDelta + 1); // space between two loops and between non-loop edges and first/last loop
+ double angleStep = (anglesOtherEdges.get(iSorted + 1) - anglesOtherEdges.get(iSorted) - intraSpace) / nbLoopsInDelta;
+ double startAngle = anglesOtherEdges.get(iSorted) + intraSpace / 2 + angleStep / 2;
+ IntStream.range(0, nbLoopsInDelta).mapToDouble(iLoop -> startAngle + iLoop * angleStep).forEach(loopAngles::add);
+ nbExcessiveRemaining -= nbLoopsInDelta - 1;
+ }
+ } else {
+ // Not enough place in the slots: dividing the circle in nbLoops, starting in the middle of the biggest slot
+ int iMaxDelta = sortedIndices.get(sortedIndices.size() - 1);
+ double startAngle = (anglesOtherEdges.get(iMaxDelta) + anglesOtherEdges.get(iMaxDelta + 1)) / 2;
+ IntStream.range(0, nbLoops).mapToDouble(i -> startAngle + i * 2 * Math.PI / nbLoops).forEach(loopAngles::add);
+ }
+
+ } else {
+ // No other edges: dividing the circle in nbLoops
+ IntStream.range(0, nbLoops).mapToDouble(i -> i * 2 * Math.PI / nbLoops).forEach(loopAngles::add);
+ }
+
+ return loopAngles;
+ }
+
+ private double getAngle(BranchEdge edge, Graph graph, Node node) {
+ BranchEdge.Side side = graph.getNode1(edge) == node ? BranchEdge.Side.ONE : BranchEdge.Side.TWO;
+ return edge.getEdgeStartAngle(side);
+ }
+
+ private void computeThreeWtEdgeCoordinates(Graph graph, ThreeWtNode threeWtNode, SvgParameters svgParameters) {
+ // The 3wt edges are computed by finding the "leading" edge and then placing the other edges at 120°
+ // The leading edge is chosen to be the opposite edge of the smallest aperture.
+ List edges = graph.getThreeWtEdgeStream(threeWtNode).collect(Collectors.toList());
+ List angles = edges.stream()
+ .map(edge -> computeEdgeStart(graph.getBusGraphNode(edge), threeWtNode.getPosition(), graph.getVoltageLevelNode(edge), svgParameters))
+ .map(edgeStart -> threeWtNode.getPosition().getAngle(edgeStart))
+ .collect(Collectors.toList());
+ List sortedIndices = IntStream.range(0, 3)
+ .boxed().sorted(Comparator.comparingDouble(angles::get))
+ .collect(Collectors.toList());
+
+ int leadingSortedIndex = getSortedIndexMaximumAperture(angles);
+ double leadingAngle = angles.get(sortedIndices.get(leadingSortedIndex));
+
+ List edgesSorted = IntStream.range(0, 3)
+ .map(i -> (leadingSortedIndex + i) % 3)
+ .map(sortedIndices::get)
+ .mapToObj(edges::get)
+ .collect(Collectors.toList());
+ double dNodeToAnchor = svgParameters.getTransformerCircleRadius() * 1.6;
+ for (int i = 0; i < edgesSorted.size(); i++) {
+ ThreeWtEdge edge = edgesSorted.get(i);
+ Point edgeStart = computeEdgeStart(graph.getBusGraphNode(edge), threeWtNode.getPosition(), graph.getVoltageLevelNode(edge), svgParameters);
+ double anchorAngle = leadingAngle + i * 2 * Math.PI / 3;
+ Point threeWtAnchor = threeWtNode.getPosition().shiftRhoTheta(dNodeToAnchor, anchorAngle);
+ edge.setPoints(edgeStart, threeWtAnchor);
+ }
+ }
+
+ private int getSortedIndexMaximumAperture(List angles) {
+ // Sorting the given angles
+ List sortedAngles = angles.stream().sorted().collect(Collectors.toList());
+
+ // Then calculating the apertures
+ sortedAngles.add(sortedAngles.get(0) + 2 * Math.PI);
+ double[] deltaAngles = new double[3];
+ for (int i = 0; i < 3; i++) {
+ deltaAngles[i] = sortedAngles.get(i + 1) - sortedAngles.get(i);
+ }
+
+ // Returning the (sorted) index of the angle facing the minimal aperture
+ int minDeltaIndex = IntStream.range(0, 3)
+ .boxed().min(Comparator.comparingDouble(i -> deltaAngles[i]))
+ .orElse(0);
+ return ((minDeltaIndex - 1) + 3) % 3;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeInfo.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeInfo.java
new file mode 100644
index 000000000..b6be5200b
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeInfo.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import java.util.Optional;
+
+/**
+ * @author Florian Dupuy
+ */
+public class EdgeInfo {
+ public static final String ACTIVE_POWER = "ActivePower";
+ public static final String REACTIVE_POWER = "ReactivePower";
+
+ private final String infoType;
+ private final Direction arrowDirection;
+ private final String internalLabel;
+ private final String externalLabel;
+
+ public EdgeInfo(String infoType, Direction arrowDirection, String internalLabel, String externalLabel) {
+ this.infoType = infoType;
+ this.arrowDirection = arrowDirection;
+ this.internalLabel = internalLabel;
+ this.externalLabel = externalLabel;
+ }
+
+ public EdgeInfo(String infoType, double value) {
+ this(infoType, value < 0 ? Direction.IN : Direction.OUT, null, String.valueOf(Math.round(value)));
+ }
+
+ public String getInfoType() {
+ return infoType;
+ }
+
+ public Optional getDirection() {
+ return Optional.ofNullable(arrowDirection);
+ }
+
+ public Optional getInternalLabel() {
+ return Optional.ofNullable(internalLabel);
+ }
+
+ public Optional getExternalLabel() {
+ return Optional.ofNullable(externalLabel);
+ }
+
+ public enum Direction {
+ IN, OUT
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeRendering.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeRendering.java
new file mode 100644
index 000000000..ab250d750
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeRendering.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.nad.model.Graph;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface EdgeRendering {
+ void run(Graph graph, SvgParameters svgParameters);
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/GraphMetadata.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/GraphMetadata.java
new file mode 100644
index 000000000..92c36fb5a
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/GraphMetadata.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.commons.xml.XmlUtil;
+import com.powsybl.nad.model.BusNode;
+import com.powsybl.nad.model.Edge;
+import com.powsybl.nad.model.Identifiable;
+import com.powsybl.nad.model.Node;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import java.io.InputStream;
+import java.util.*;
+import java.util.function.UnaryOperator;
+
+/**
+ * @author Thomas Adam
+ */
+public class GraphMetadata {
+
+ private static final String METADATA_NAMESPACE_URI = "http://www.powsybl.org/schema/nad-metadata/1_0";
+ private static final String METADATA_PREFIX = "nad";
+ private static final String METADATA_ELEMENT_NAME = "metadata";
+ private static final String METADATA_BUS_NODES_ELEMENT_NAME = "busNodes";
+ private static final String METADATA_NODES_ELEMENT_NAME = "nodes";
+ private static final String METADATA_EDGES_ELEMENT_NAME = "edges";
+ private static final String METADATA_BUS_NODE_ELEMENT_NAME = "busNode";
+ private static final String METADATA_NODE_ELEMENT_NAME = "node";
+ private static final String METADATA_EDGE_ELEMENT_NAME = "edge";
+ private static final String DIAGRAM_ID_ATTRIBUTE = "diagramId";
+ private static final String EQUIPMENT_ID_ATTRIBUTE = "equipmentId";
+
+ private final Map busNodeIdByDiagramId = new LinkedHashMap<>();
+
+ private final Map nodeIdByDiagramId = new LinkedHashMap<>();
+
+ private final Map edgeIdByDiagramId = new LinkedHashMap<>();
+
+ public static GraphMetadata parseXml(InputStream inputStream) throws XMLStreamException {
+ return parseXml(XMLInputFactory.newDefaultFactory().createXMLStreamReader(inputStream));
+ }
+
+ public static GraphMetadata parseXml(XMLStreamReader reader) throws XMLStreamException {
+ GraphMetadata metadata = new GraphMetadata();
+
+ XmlUtil.readUntilEndElement(METADATA_ELEMENT_NAME, reader, () -> {
+ String token = reader.getLocalName();
+ switch (token) {
+ case METADATA_BUS_NODES_ELEMENT_NAME:
+ XmlUtil.readUntilEndElement(token, reader, () -> {
+ if (reader.getLocalName().equals(METADATA_BUS_NODE_ELEMENT_NAME)) {
+ parseId(metadata.busNodeIdByDiagramId, reader);
+ }
+ });
+ break;
+ case METADATA_NODES_ELEMENT_NAME:
+ XmlUtil.readUntilEndElement(token, reader, () -> {
+ if (reader.getLocalName().equals(METADATA_NODE_ELEMENT_NAME)) {
+ parseId(metadata.nodeIdByDiagramId, reader);
+ }
+ });
+ break;
+ case METADATA_EDGES_ELEMENT_NAME:
+ XmlUtil.readUntilEndElement(token, reader, () -> {
+ if (reader.getLocalName().equals(METADATA_EDGE_ELEMENT_NAME)) {
+ parseId(metadata.edgeIdByDiagramId, reader);
+ }
+ });
+ break;
+ default:
+ // Not managed
+ }
+ });
+ return metadata;
+ }
+
+ private static void parseId(Map ids, XMLStreamReader reader) {
+ String diagramId = reader.getAttributeValue(null, DIAGRAM_ID_ATTRIBUTE);
+ String equipmentId = reader.getAttributeValue(null, EQUIPMENT_ID_ATTRIBUTE);
+ ids.put(diagramId, equipmentId);
+ }
+
+ public void writeXml(XMLStreamWriter writer) throws XMLStreamException {
+ // Root element
+ writer.writeStartElement(METADATA_ELEMENT_NAME);
+ writer.writeNamespace(METADATA_PREFIX, METADATA_NAMESPACE_URI);
+ // BusNodes
+ writeIdMapping(METADATA_BUS_NODES_ELEMENT_NAME, METADATA_BUS_NODE_ELEMENT_NAME, busNodeIdByDiagramId, writer);
+ // Nodes
+ writeIdMapping(METADATA_NODES_ELEMENT_NAME, METADATA_NODE_ELEMENT_NAME, nodeIdByDiagramId, writer);
+ // Edges
+ writeIdMapping(METADATA_EDGES_ELEMENT_NAME, METADATA_EDGE_ELEMENT_NAME, edgeIdByDiagramId, writer);
+ // End root element
+ writer.writeEndElement();
+ }
+
+ private void writeIdMapping(String rootElementName, String tagElementName, Map ids, XMLStreamWriter writer) throws XMLStreamException {
+ if (ids.entrySet().isEmpty()) {
+ writer.writeEmptyElement(METADATA_PREFIX, rootElementName, METADATA_NAMESPACE_URI);
+ } else {
+ writer.writeStartElement(METADATA_PREFIX, rootElementName, METADATA_NAMESPACE_URI);
+ for (Map.Entry entry : ids.entrySet()) {
+ writer.writeEmptyElement(METADATA_PREFIX, tagElementName, METADATA_NAMESPACE_URI);
+ writer.writeAttribute(DIAGRAM_ID_ATTRIBUTE, entry.getKey());
+ writer.writeAttribute(EQUIPMENT_ID_ATTRIBUTE, entry.getValue());
+ }
+ writer.writeEndElement();
+ }
+ }
+
+ public void addBusNode(BusNode node, UnaryOperator diagramIdToSvgId) {
+ addIdentifiable(busNodeIdByDiagramId, node, diagramIdToSvgId);
+ }
+
+ public void addNode(Node node, UnaryOperator diagramIdToSvgId) {
+ addIdentifiable(nodeIdByDiagramId, node, diagramIdToSvgId);
+ }
+
+ public void addEdge(Edge edge, UnaryOperator diagramIdToSvgId) {
+ addIdentifiable(edgeIdByDiagramId, edge, diagramIdToSvgId);
+ }
+
+ private void addIdentifiable(Map map, Identifiable identifiable, UnaryOperator diagramIdToSvgId) {
+ Objects.requireNonNull(identifiable);
+ Objects.requireNonNull(diagramIdToSvgId);
+ map.put(diagramIdToSvgId.apply(identifiable.getDiagramId()), identifiable.getEquipmentId());
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/LabelProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/LabelProvider.java
new file mode 100644
index 000000000..d7ca33bda
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/LabelProvider.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.nad.model.BranchEdge;
+import com.powsybl.nad.model.Graph;
+import com.powsybl.nad.model.ThreeWtEdge;
+import com.powsybl.nad.model.VoltageLevelNode;
+
+import java.util.List;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface LabelProvider {
+ List getEdgeInfos(Graph graph, BranchEdge edge, BranchEdge.Side side);
+
+ List getEdgeInfos(Graph graph, ThreeWtEdge edge);
+
+ String getArrowPathDIn();
+
+ String getArrowPathDOut();
+
+ List getVoltageLevelDescription(VoltageLevelNode voltageLevelNode);
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/Padding.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/Padding.java
new file mode 100644
index 000000000..c8d53c8b4
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/Padding.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+/**
+ * @author Florian Dupuy
+ */
+public class Padding {
+
+ private double left;
+ private double top;
+ private double right;
+ private double bottom;
+
+ public Padding(double left, double top, double right, double bottom) {
+ this.left = left;
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ public Padding(double horizontal, double vertical) {
+ this.left = horizontal;
+ this.top = vertical;
+ this.right = horizontal;
+ this.bottom = vertical;
+ }
+
+ public Padding(double padding) {
+ this.left = padding;
+ this.top = padding;
+ this.right = padding;
+ this.bottom = padding;
+ }
+
+ public double getLeft() {
+ return left;
+ }
+
+ public void setLeft(double left) {
+ this.left = left;
+ }
+
+ public double getTop() {
+ return top;
+ }
+
+ public void setTop(double top) {
+ this.top = top;
+ }
+
+ public double getRight() {
+ return right;
+ }
+
+ public void setRight(double right) {
+ this.right = right;
+ }
+
+ public double getBottom() {
+ return bottom;
+ }
+
+ public void setBottom(double bottom) {
+ this.bottom = bottom;
+ }
+}
+
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/StyleProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/StyleProvider.java
new file mode 100644
index 000000000..0478d46d6
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/StyleProvider.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.nad.model.*;
+
+import java.util.List;
+
+/**
+ * @author Florian Dupuy
+ */
+public interface StyleProvider {
+
+ String CLASSES_PREFIX = "nad-";
+ String VOLTAGE_LEVEL_NODES_CLASS = CLASSES_PREFIX + "vl-nodes";
+ String TEXT_NODES_CLASS = CLASSES_PREFIX + "text-nodes";
+ String THREE_WT_NODES_CLASS = CLASSES_PREFIX + "3wt-nodes";
+ String DISCONNECTED_CLASS = CLASSES_PREFIX + "disconnected";
+ String BRANCH_EDGES_CLASS = CLASSES_PREFIX + "branch-edges";
+ String HVDC_EDGE_CLASS = CLASSES_PREFIX + "hvdc-edge";
+ String THREE_WT_EDGES_CLASS = CLASSES_PREFIX + "3wt-edges";
+ String TEXT_EDGES_CLASS = CLASSES_PREFIX + "text-edges";
+ String EDGE_INFOS_CLASS = CLASSES_PREFIX + "edge-infos";
+ String ARROW_IN_CLASS = CLASSES_PREFIX + "arrow-in";
+ String ARROW_OUT_CLASS = CLASSES_PREFIX + "arrow-out";
+ String HVDC_CLASS = CLASSES_PREFIX + "hvdc";
+ String UNKNOWN_BUSNODE_CLASS = CLASSES_PREFIX + "unknown-busnode";
+ String TEXT_BACKGROUND_CLASS = CLASSES_PREFIX + "text-background";
+ String LINE_OVERLOADED_CLASS = CLASSES_PREFIX + "overload";
+ String VL_OVERVOLTAGE_CLASS = CLASSES_PREFIX + "overvoltage";
+ String VL_UNDERVOLTAGE_CLASS = CLASSES_PREFIX + "undervoltage";
+ String EDGE_PATH_CLASS = CLASSES_PREFIX + "edge-path";
+ String WINDING_CLASS = CLASSES_PREFIX + "winding";
+ String BUSNODE_CLASS = CLASSES_PREFIX + "busnode";
+
+ List getCssFilenames();
+
+ String getStyleDefs();
+
+ List getNodeStyleClasses(Node node);
+
+ List getNodeStyleClasses(BusNode busNode);
+
+ List getEdgeStyleClasses(Edge edge);
+
+ List getSideEdgeStyleClasses(BranchEdge edge, BranchEdge.Side side);
+
+ List getEdgeInfoStyles(EdgeInfo info);
+
+ List getThreeWtNodeStyle(ThreeWtNode threeWtNode, ThreeWtEdge.Side one);
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgParameters.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgParameters.java
new file mode 100644
index 000000000..64805371b
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgParameters.java
@@ -0,0 +1,352 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import java.util.Objects;
+
+/**
+ * @author Florian Dupuy
+ */
+public class SvgParameters {
+
+ private Padding diagramPadding = new Padding(200);
+ private boolean insertNameDesc = false;
+ private boolean svgWidthAndHeightAdded = false;
+ private CssLocation cssLocation = CssLocation.INSERTED_IN_SVG;
+ private SizeConstraint sizeConstraint = SizeConstraint.FIXED_SCALE;
+ private int fixedWidth = -1;
+ private int fixedHeight = -1;
+ private double fixedScale = 0.2;
+ private double arrowShift = 30;
+ private double arrowLabelShift = 19;
+ private double converterStationWidth = 60;
+ private double voltageLevelCircleRadius = 30;
+ private double fictitiousVoltageLevelCircleRadius = 15;
+ private double transformerCircleRadius = 20;
+ private double nodeHollowWidth = 15;
+ private double edgesForkLength = 80;
+ private double edgesForkAperture = Math.toRadians(60);
+ private double edgeStartShift = 2;
+ private double unknownBusNodeExtraRadius = 10;
+ private double loopDistance = 120;
+ private double loopEdgesAperture = Math.toRadians(60);
+ private double loopControlDistance = 40;
+ private boolean textNodeBackground = true;
+ private boolean edgeInfoAlongEdge = true;
+ private double interAnnulusSpace = 5;
+ private String svgPrefix = "";
+ private boolean idDisplayed = false;
+ private boolean substationDescriptionDisplayed;
+ private double arrowHeight = 10;
+
+ public enum CssLocation {
+ INSERTED_IN_SVG, EXTERNAL_IMPORTED, EXTERNAL_NO_IMPORT
+ }
+
+ public enum SizeConstraint {
+ NONE, FIXED_SCALE, FIXED_WIDTH, FIXED_HEIGHT
+ }
+
+ public SvgParameters() {
+ }
+
+ public SvgParameters(SvgParameters other) {
+ this.diagramPadding = other.diagramPadding;
+ this.insertNameDesc = other.insertNameDesc;
+ this.svgWidthAndHeightAdded = other.svgWidthAndHeightAdded;
+ this.cssLocation = other.cssLocation;
+ this.sizeConstraint = other.sizeConstraint;
+ this.fixedWidth = other.fixedWidth;
+ this.fixedHeight = other.fixedHeight;
+ this.fixedScale = other.fixedScale;
+ this.arrowShift = other.arrowShift;
+ this.arrowLabelShift = other.arrowLabelShift;
+ this.converterStationWidth = other.converterStationWidth;
+ this.voltageLevelCircleRadius = other.voltageLevelCircleRadius;
+ this.fictitiousVoltageLevelCircleRadius = other.fictitiousVoltageLevelCircleRadius;
+ this.transformerCircleRadius = other.transformerCircleRadius;
+ this.nodeHollowWidth = other.nodeHollowWidth;
+ this.edgesForkLength = other.edgesForkLength;
+ this.edgesForkAperture = other.edgesForkAperture;
+ this.edgeStartShift = other.edgeStartShift;
+ this.unknownBusNodeExtraRadius = other.unknownBusNodeExtraRadius;
+ this.loopDistance = other.loopDistance;
+ this.loopEdgesAperture = other.loopEdgesAperture;
+ this.loopControlDistance = other.loopControlDistance;
+ this.textNodeBackground = other.textNodeBackground;
+ this.edgeInfoAlongEdge = other.edgeInfoAlongEdge;
+ this.interAnnulusSpace = other.interAnnulusSpace;
+ this.svgPrefix = other.svgPrefix;
+ this.idDisplayed = other.idDisplayed;
+ this.substationDescriptionDisplayed = other.substationDescriptionDisplayed;
+ this.arrowHeight = other.arrowHeight;
+ }
+
+ public Padding getDiagramPadding() {
+ return diagramPadding;
+ }
+
+ public SvgParameters setDiagramPadding(Padding padding) {
+ this.diagramPadding = Objects.requireNonNull(padding);
+ return this;
+ }
+
+ public boolean isInsertNameDesc() {
+ return insertNameDesc;
+ }
+
+ public SvgParameters setInsertNameDesc(boolean insertNameDesc) {
+ this.insertNameDesc = insertNameDesc;
+ return this;
+ }
+
+ public CssLocation getCssLocation() {
+ return cssLocation;
+ }
+
+ public SvgParameters setCssLocation(CssLocation cssLocation) {
+ this.cssLocation = Objects.requireNonNull(cssLocation);
+ return this;
+ }
+
+ public int getFixedWidth() {
+ return fixedWidth;
+ }
+
+ public SvgParameters setFixedWidth(int fixedWidth) {
+ this.fixedWidth = fixedWidth;
+ sizeConstraint = SizeConstraint.FIXED_WIDTH;
+ return this;
+ }
+
+ public int getFixedHeight() {
+ return fixedHeight;
+ }
+
+ public SvgParameters setFixedHeight(int fixedHeight) {
+ this.fixedHeight = fixedHeight;
+ sizeConstraint = SizeConstraint.FIXED_HEIGHT;
+ return this;
+ }
+
+ public double getFixedScale() {
+ return fixedScale;
+ }
+
+ public SvgParameters setFixedScale(double fixedScale) {
+ this.fixedScale = fixedScale;
+ sizeConstraint = SizeConstraint.FIXED_SCALE;
+ return this;
+ }
+
+ public SizeConstraint getSizeConstraint() {
+ return sizeConstraint;
+ }
+
+ public SvgParameters setSizeConstraint(SizeConstraint sizeConstraint) {
+ this.sizeConstraint = sizeConstraint;
+ return this;
+ }
+
+ public boolean isSvgWidthAndHeightAdded() {
+ return svgWidthAndHeightAdded;
+ }
+
+ public SvgParameters setSvgWidthAndHeightAdded(boolean svgWidthAndHeightAdded) {
+ this.svgWidthAndHeightAdded = svgWidthAndHeightAdded;
+ return this;
+ }
+
+ public double getArrowShift() {
+ return arrowShift;
+ }
+
+ public SvgParameters setArrowShift(double arrowShift) {
+ this.arrowShift = arrowShift;
+ return this;
+ }
+
+ public double getArrowLabelShift() {
+ return arrowLabelShift;
+ }
+
+ public SvgParameters setArrowLabelShift(double arrowLabelShift) {
+ this.arrowLabelShift = arrowLabelShift;
+ return this;
+ }
+
+ public double getConverterStationWidth() {
+ return converterStationWidth;
+ }
+
+ public SvgParameters setConverterStationWidth(double converterStationWidth) {
+ this.converterStationWidth = converterStationWidth;
+ return this;
+ }
+
+ public double getVoltageLevelCircleRadius() {
+ return voltageLevelCircleRadius;
+ }
+
+ public SvgParameters setVoltageLevelCircleRadius(double voltageLevelCircleRadius) {
+ this.voltageLevelCircleRadius = voltageLevelCircleRadius;
+ return this;
+ }
+
+ public double getTransformerCircleRadius() {
+ return transformerCircleRadius;
+ }
+
+ public SvgParameters setTransformerCircleRadius(double transformerCircleRadius) {
+ this.transformerCircleRadius = transformerCircleRadius;
+ return this;
+ }
+
+ public double getNodeHollowWidth() {
+ return nodeHollowWidth;
+ }
+
+ public SvgParameters setNodeHollowWidth(double nodeHollowWidth) {
+ this.nodeHollowWidth = nodeHollowWidth;
+ return this;
+ }
+
+ public double getEdgesForkAperture() {
+ return edgesForkAperture;
+ }
+
+ public SvgParameters setEdgesForkAperture(double edgesForkApertureDegrees) {
+ this.edgesForkAperture = Math.toRadians(edgesForkApertureDegrees);
+ return this;
+ }
+
+ public double getLoopEdgesAperture() {
+ return loopEdgesAperture;
+ }
+
+ public SvgParameters setLoopEdgesAperture(double loopEdgesApertureDegrees) {
+ this.loopEdgesAperture = Math.toRadians(loopEdgesApertureDegrees);
+ return this;
+ }
+
+ public double getEdgesForkLength() {
+ return edgesForkLength;
+ }
+
+ public SvgParameters setEdgesForkLength(double edgesForkLength) {
+ this.edgesForkLength = edgesForkLength;
+ return this;
+ }
+
+ public double getEdgeStartShift() {
+ return edgeStartShift;
+ }
+
+ public SvgParameters setEdgeStartShift(double edgeStartShift) {
+ this.edgeStartShift = edgeStartShift;
+ return this;
+ }
+
+ public double getUnknownBusNodeExtraRadius() {
+ return unknownBusNodeExtraRadius;
+ }
+
+ public SvgParameters setUnknownBusNodeExtraRadius(double unknownBusNodeExtraRadius) {
+ this.unknownBusNodeExtraRadius = unknownBusNodeExtraRadius;
+ return this;
+ }
+
+ public double getLoopDistance() {
+ return loopDistance;
+ }
+
+ public SvgParameters setLoopDistance(double loopDistance) {
+ this.loopDistance = loopDistance;
+ return this;
+ }
+
+ public double getFictitiousVoltageLevelCircleRadius() {
+ return fictitiousVoltageLevelCircleRadius;
+ }
+
+ public SvgParameters setFictitiousVoltageLevelCircleRadius(double fictitiousVoltageLevelCircleRadius) {
+ this.fictitiousVoltageLevelCircleRadius = fictitiousVoltageLevelCircleRadius;
+ return this;
+ }
+
+ public double getLoopControlDistance() {
+ return loopControlDistance;
+ }
+
+ public SvgParameters setLoopControlDistance(double loopControlDistance) {
+ this.loopControlDistance = loopControlDistance;
+ return this;
+ }
+
+ public boolean isTextNodeBackground() {
+ return textNodeBackground;
+ }
+
+ public SvgParameters setTextNodeBackground(boolean textNodeBackground) {
+ this.textNodeBackground = textNodeBackground;
+ return this;
+ }
+
+ public boolean isEdgeInfoAlongEdge() {
+ return edgeInfoAlongEdge;
+ }
+
+ public SvgParameters setEdgeInfoAlongEdge(boolean edgeInfoAlongEdge) {
+ this.edgeInfoAlongEdge = edgeInfoAlongEdge;
+ return this;
+ }
+
+ public double getInterAnnulusSpace() {
+ return interAnnulusSpace;
+ }
+
+ public SvgParameters setInterAnnulusSpace(double interAnnulusSpace) {
+ this.interAnnulusSpace = interAnnulusSpace;
+ return this;
+ }
+
+ public String getSvgPrefix() {
+ return svgPrefix;
+ }
+
+ public SvgParameters setSvgPrefix(String svgPrefix) {
+ this.svgPrefix = svgPrefix;
+ return this;
+ }
+
+ public boolean isIdDisplayed() {
+ return idDisplayed;
+ }
+
+ public SvgParameters setIdDisplayed(boolean idDisplayed) {
+ this.idDisplayed = idDisplayed;
+ return this;
+ }
+
+ public boolean isSubstationDescriptionDisplayed() {
+ return substationDescriptionDisplayed;
+ }
+
+ public SvgParameters setSubstationDescriptionDisplayed(boolean substationDescriptionDisplayed) {
+ this.substationDescriptionDisplayed = substationDescriptionDisplayed;
+ return this;
+ }
+
+ public double getArrowHeight() {
+ return arrowHeight;
+ }
+
+ public SvgParameters setArrowHeight(double arrowHeight) {
+ this.arrowHeight = arrowHeight;
+ return this;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgWriter.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgWriter.java
new file mode 100644
index 000000000..b4ff7b352
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgWriter.java
@@ -0,0 +1,740 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg;
+
+import com.powsybl.commons.exceptions.UncheckedXmlStreamException;
+import com.powsybl.commons.xml.XmlUtil;
+import com.powsybl.nad.model.*;
+import org.apache.commons.io.output.WriterOutputStream;
+import org.jgrapht.alg.util.Pair;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Florian Dupuy
+ */
+public class SvgWriter {
+
+ private static final String INDENT = " ";
+ public static final String NAMESPACE_URI = "http://www.w3.org/2000/svg";
+ private static final String SVG_ROOT_ELEMENT_NAME = "svg";
+ private static final String STYLE_ELEMENT_NAME = "style";
+ private static final String DEFS_ELEMENT_NAME = "defs";
+ private static final String GROUP_ELEMENT_NAME = "g";
+ private static final String POLYLINE_ELEMENT_NAME = "polyline";
+ private static final String PATH_ELEMENT_NAME = "path";
+ private static final String CIRCLE_ELEMENT_NAME = "circle";
+ private static final String TEXT_ELEMENT_NAME = "text";
+ private static final String TSPAN_ELEMENT_NAME = "tspan";
+ private static final String ID_ATTRIBUTE = "id";
+ private static final String WIDTH_ATTRIBUTE = "width";
+ private static final String HEIGHT_ATTRIBUTE = "height";
+ private static final String VIEW_BOX_ATTRIBUTE = "viewBox";
+ private static final String DESCRIPTION_ATTRIBUTE = "desc";
+ private static final String CLASS_ATTRIBUTE = "class";
+ private static final String TRANSFORM_ATTRIBUTE = "transform";
+ private static final String CIRCLE_RADIUS_ATTRIBUTE = "r";
+ private static final String PATH_D_ATTRIBUTE = "d";
+ private static final String X_ATTRIBUTE = "x";
+ private static final String Y_ATTRIBUTE = "y";
+ private static final String DY_ATTRIBUTE = "dy";
+ private static final String POINTS_ATTRIBUTE = "points";
+ private static final String FILTER_ELEMENT_NAME = "filter";
+ private static final String FE_FLOOD_ELEMENT_NAME = "feFlood";
+ private static final String FE_COMPOSITE_ELEMENT_NAME = "feComposite";
+ private static final String FE_IN_ATTRIBUTE = "in";
+ private static final String FE_OPERATOR_ATTRIBUTE = "operator";
+ public static final String TEXT_BG_FILTER_ID = "textBgFilter";
+
+ private final SvgParameters svgParameters;
+ private final StyleProvider styleProvider;
+ private final LabelProvider labelProvider;
+ private final EdgeRendering edgeRendering;
+
+ public SvgWriter(SvgParameters svgParameters, StyleProvider styleProvider, LabelProvider labelProvider) {
+ this.svgParameters = Objects.requireNonNull(svgParameters);
+ this.styleProvider = Objects.requireNonNull(styleProvider);
+ this.labelProvider = Objects.requireNonNull(labelProvider);
+ this.edgeRendering = new DefaultEdgeRendering();
+ }
+
+ public void writeSvg(Graph graph, Path svgFile) {
+ Objects.requireNonNull(svgFile);
+ Path dir = svgFile.toAbsolutePath().getParent();
+ String svgFileName = svgFile.getFileName().toString();
+ if (!svgFileName.endsWith(".svg")) {
+ svgFileName = svgFileName + ".svg";
+ }
+ try (OutputStream svgOs = new BufferedOutputStream(Files.newOutputStream(dir.resolve(svgFileName)))) {
+ writeSvg(graph, svgOs);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public void writeSvg(Graph graph, Writer svgWriter) {
+ try (WriterOutputStream svgOs = new WriterOutputStream(svgWriter, StandardCharsets.UTF_8)) {
+ writeSvg(graph, svgOs);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private void writeSvg(Graph graph, OutputStream svgOs) {
+ Objects.requireNonNull(graph);
+ Objects.requireNonNull(svgOs);
+
+ // Edge coordinates need to be computed first, based on svg parameters
+ edgeRendering.run(graph, svgParameters);
+
+ try {
+ XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, svgOs);
+ addSvgRoot(graph, writer);
+ addStyle(writer);
+ addMetadata(graph, writer);
+ addDefs(writer);
+ drawVoltageLevelNodes(graph, writer);
+ drawBranchEdges(graph, writer);
+ drawThreeWtEdges(graph, writer);
+ drawThreeWtNodes(graph, writer);
+ drawTextEdges(graph, writer);
+ drawTextNodes(graph, writer);
+ writer.writeEndDocument();
+ } catch (XMLStreamException e) {
+ throw new UncheckedXmlStreamException(e);
+ }
+ }
+
+ private void drawBranchEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.BRANCH_EDGES_CLASS);
+ for (BranchEdge edge : graph.getBranchEdges()) {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(edge.getDiagramId()));
+ addStylesIfAny(writer, styleProvider.getEdgeStyleClasses(edge));
+ insertName(writer, edge::getName);
+
+ drawHalfEdge(graph, writer, edge, BranchEdge.Side.ONE);
+ drawHalfEdge(graph, writer, edge, BranchEdge.Side.TWO);
+
+ if (edge.getType().equals(BranchEdge.HVDC_LINE_EDGE)) {
+ drawConverterStation(writer, edge);
+ }
+
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawConverterStation(XMLStreamWriter writer, BranchEdge edge) throws XMLStreamException {
+ writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
+ List line1 = edge.getPoints(BranchEdge.Side.ONE);
+ List line2 = edge.getPoints(BranchEdge.Side.TWO);
+ List points = new ArrayList<>(2);
+ double halfWidth = svgParameters.getConverterStationWidth() / 2;
+ if (line1.size() > 2) {
+ points.add(line1.get(2).atDistance(halfWidth, line1.get(1)));
+ points.add(line2.get(2).atDistance(halfWidth, line2.get(1)));
+ } else {
+ points.add(line1.get(1).atDistance(halfWidth, line1.get(0)));
+ points.add(line2.get(1).atDistance(halfWidth, line2.get(0)));
+ }
+ String lineFormatted = points.stream()
+ .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
+ .collect(Collectors.joining(" "));
+ writer.writeAttribute(POINTS_ATTRIBUTE, lineFormatted);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.HVDC_CLASS);
+ }
+
+ private void drawThreeWtEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ List threeWtEdges = graph.getThreeWtEdges();
+ if (threeWtEdges.isEmpty()) {
+ return;
+ }
+
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.THREE_WT_EDGES_CLASS);
+ for (ThreeWtEdge edge : threeWtEdges) {
+ drawThreeWtEdge(graph, writer, edge);
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawHalfEdge(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side) throws XMLStreamException {
+ // the half edge is only drawn if visible, but if the edge is a TwoWtEdge, the transformer is still drawn
+ if (!edge.isVisible(side) && !(edge.getType().equals(BranchEdge.TWO_WT_EDGE))) {
+ return;
+ }
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ addStylesIfAny(writer, styleProvider.getSideEdgeStyleClasses(edge, side));
+ if (edge.isVisible(side)) {
+ if (!graph.isLoop(edge)) {
+ writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_PATH_CLASS);
+ writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge, side));
+ drawBranchEdgeInfo(graph, writer, edge, side, labelProvider.getEdgeInfos(graph, edge, side));
+ } else {
+ writer.writeEmptyElement(PATH_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_PATH_CLASS);
+ writer.writeAttribute(PATH_D_ATTRIBUTE, getLoopPathString(edge, side));
+ drawLoopEdgeInfo(writer, edge, side, labelProvider.getEdgeInfos(graph, edge, side));
+ }
+ }
+ if (edge.getType().equals(BranchEdge.TWO_WT_EDGE)) {
+ draw2WtWinding(writer, edge.getPoints(side));
+ }
+ writer.writeEndElement();
+ }
+
+ private String getPolylinePointsString(BranchEdge edge, BranchEdge.Side side) {
+ return getPolylinePointsString(edge.getPoints(side));
+ }
+
+ private String getPolylinePointsString(ThreeWtEdge edge) {
+ return getPolylinePointsString(edge.getPoints());
+ }
+
+ private String getPolylinePointsString(List points) {
+ return points.stream()
+ .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
+ .collect(Collectors.joining(" "));
+ }
+
+ private String getLoopPathString(BranchEdge edge, BranchEdge.Side side) {
+ Object[] points = edge.getPoints(side).stream().flatMap(p -> Stream.of(p.getX(), p.getY())).toArray();
+ return String.format(Locale.US, "M%.2f,%.2f L%.2f,%.2f C%.2f,%.2f %.2f,%.2f %.2f,%.2f", points);
+ }
+
+ private void drawThreeWtEdge(Graph graph, XMLStreamWriter writer, ThreeWtEdge edge) throws XMLStreamException {
+ if (!edge.isVisible()) {
+ return;
+ }
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(edge.getDiagramId()));
+ addStylesIfAny(writer, styleProvider.getEdgeStyleClasses(edge));
+ insertName(writer, edge::getName);
+ writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_PATH_CLASS);
+ writer.writeAttribute(POINTS_ATTRIBUTE, getPolylinePointsString(edge));
+ drawThreeWtEdgeInfo(graph, writer, edge, labelProvider.getEdgeInfos(graph, edge));
+ writer.writeEndElement();
+ }
+
+ private void drawThreeWtNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ List threeWtNodes = graph.getThreeWtNodesStream().collect(Collectors.toList());
+ if (threeWtNodes.isEmpty()) {
+ return;
+ }
+
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.THREE_WT_NODES_CLASS);
+ for (ThreeWtNode threeWtNode : threeWtNodes) {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ addStylesIfAny(writer, styleProvider.getNodeStyleClasses(threeWtNode));
+ List edges = graph.getThreeWtEdgeStream(threeWtNode).collect(Collectors.toList());
+ for (ThreeWtEdge edge : edges) {
+ draw3WtWinding(edge, threeWtNode, writer);
+ }
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ private void draw3WtWinding(ThreeWtEdge edge, ThreeWtNode threeWtNode, XMLStreamWriter writer) throws XMLStreamException {
+ List styles = styleProvider.getThreeWtNodeStyle(threeWtNode, edge.getSide());
+ styles.add(StyleProvider.WINDING_CLASS);
+ double radius = svgParameters.getTransformerCircleRadius();
+ Point circleCenter = edge.getPoints().get(1).atDistance(radius, threeWtNode.getPosition());
+ writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
+ addStylesIfAny(writer, styles);
+ writer.writeAttribute("cx", getFormattedValue(circleCenter.getX()));
+ writer.writeAttribute("cy", getFormattedValue(circleCenter.getY()));
+ writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(svgParameters.getTransformerCircleRadius()));
+ }
+
+ private void drawLoopEdgeInfo(XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, List edgeInfos) throws XMLStreamException {
+ drawEdgeInfo(writer, edgeInfos, edge.getPoints(side).get(1), edge.getEdgeStartAngle(side));
+ }
+
+ private void drawBranchEdgeInfo(Graph graph, XMLStreamWriter writer, BranchEdge edge, BranchEdge.Side side, List edgeInfos) throws XMLStreamException {
+ VoltageLevelNode vlNode = graph.getVoltageLevelNode(edge, side);
+ BusNode busNode = graph.getBusGraphNode(edge, side);
+ drawEdgeInfo(writer, edgeInfos, getArrowCenter(vlNode, busNode, edge.getPoints(side)), edge.getEdgeEndAngle(side));
+ }
+
+ private void drawThreeWtEdgeInfo(Graph graph, XMLStreamWriter writer, ThreeWtEdge edge, List edgeInfos) throws XMLStreamException {
+ VoltageLevelNode vlNode = graph.getVoltageLevelNode(edge);
+ BusNode busNode = graph.getBusGraphNode(edge);
+ drawEdgeInfo(writer, edgeInfos, getArrowCenter(vlNode, busNode, edge.getPoints()), edge.getEdgeAngle());
+ }
+
+ private void drawEdgeInfo(XMLStreamWriter writer, List edgeInfos, Point infoCenter, double edgeAngle) throws XMLStreamException {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.EDGE_INFOS_CLASS);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(infoCenter));
+ for (EdgeInfo info : edgeInfos) {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ addStylesIfAny(writer, styleProvider.getEdgeInfoStyles(info));
+ drawInAndOutArrows(writer, edgeAngle);
+ Optional externalLabel = info.getExternalLabel();
+ if (externalLabel.isPresent()) {
+ drawLabel(writer, externalLabel.get(), edgeAngle, true);
+ }
+ Optional internalLabel = info.getInternalLabel();
+ if (internalLabel.isPresent()) {
+ drawLabel(writer, internalLabel.get(), edgeAngle, false);
+ }
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawInAndOutArrows(XMLStreamWriter writer, double edgeAngle) throws XMLStreamException {
+ double rotationAngle = edgeAngle + (edgeAngle > Math.PI / 2 ? -3 * Math.PI / 2 : Math.PI / 2);
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getRotateString(rotationAngle));
+ writer.writeEmptyElement(PATH_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.ARROW_IN_CLASS);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getScaleString(svgParameters.getArrowHeight()));
+ writer.writeAttribute(PATH_D_ATTRIBUTE, labelProvider.getArrowPathDIn());
+ writer.writeEmptyElement(PATH_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.ARROW_OUT_CLASS);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getScaleString(svgParameters.getArrowHeight()));
+ writer.writeAttribute(PATH_D_ATTRIBUTE, labelProvider.getArrowPathDOut());
+ writer.writeEndElement();
+ }
+
+ private void drawLabel(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
+ if (svgParameters.isEdgeInfoAlongEdge()) {
+ drawLabelAlongEdge(writer, label, edgeAngle, externalLabel);
+ } else {
+ drawLabelPerpendicularToEdge(writer, label, edgeAngle, externalLabel);
+ }
+ }
+
+ private void drawLabelAlongEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
+ boolean textFlipped = Math.cos(edgeAngle) < 0;
+ String style = externalLabel == textFlipped ? "text-anchor:end" : null;
+ double textAngle = textFlipped ? edgeAngle - Math.PI : edgeAngle;
+ double shift = svgParameters.getArrowLabelShift() * (externalLabel ? 1 : -1);
+ drawLabel(writer, label, textFlipped ? -shift : shift, style, textAngle, X_ATTRIBUTE);
+ }
+
+ private void drawLabelPerpendicularToEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel) throws XMLStreamException {
+ boolean textFlipped = Math.sin(edgeAngle) > 0;
+ double textAngle = textFlipped ? -Math.PI / 2 + edgeAngle : Math.PI / 2 + edgeAngle;
+ double shift = svgParameters.getArrowLabelShift();
+ double shiftAdjusted = externalLabel == textFlipped ? shift * 1.15 : -shift; // to have a nice compact rendering, shift needs to be adjusted, because of dominant-baseline:middle (text is expected to be a number, hence not below the line)
+ drawLabel(writer, label, shiftAdjusted, "text-anchor:middle", textAngle, Y_ATTRIBUTE);
+ }
+
+ private void drawLabel(XMLStreamWriter writer, String label, double shift, String style, double textAngle, String shiftAxis) throws XMLStreamException {
+ writer.writeStartElement(TEXT_ELEMENT_NAME);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getRotateString(textAngle));
+ writer.writeAttribute(shiftAxis, getFormattedValue(shift));
+ if (style != null) {
+ writer.writeAttribute(STYLE_ELEMENT_NAME, style);
+ }
+ writer.writeCharacters(label);
+ writer.writeEndElement();
+ }
+
+ private String getRotateString(double angleRad) {
+ return "rotate(" + getFormattedValue(Math.toDegrees(angleRad)) + ")";
+ }
+
+ private String getScaleString(double scale) {
+ return "scale(" + getFormattedValue(scale) + ")";
+ }
+
+ private Point getArrowCenter(VoltageLevelNode vlNode, BusNode busNode, List line) {
+ double shift = svgParameters.getArrowShift();
+ if (line.size() == 2) { // straight line; in case of a forking line it is the middle point which is the starting point
+ double nodeOuterRadius = getVoltageLevelCircleRadius(vlNode);
+ double busAnnulusOuterRadius = getBusAnnulusOuterRadius(busNode, vlNode, svgParameters);
+ shift += nodeOuterRadius - busAnnulusOuterRadius;
+ }
+ return line.get(line.size() - 2).atDistance(shift, line.get(line.size() - 1));
+ }
+
+ private void draw2WtWinding(XMLStreamWriter writer, List half) throws XMLStreamException {
+ writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.WINDING_CLASS);
+ Point point1 = half.get(half.size() - 1); // point near 2wt
+ Point point2 = half.get(half.size() - 2); // point near voltage level, or control point for loops
+ double radius = svgParameters.getTransformerCircleRadius();
+ Point circleCenter = point1.atDistance(-radius, point2);
+ writer.writeAttribute("cx", getFormattedValue(circleCenter.getX()));
+ writer.writeAttribute("cy", getFormattedValue(circleCenter.getY()));
+ writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(radius));
+ }
+
+ private void drawVoltageLevelNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.VOLTAGE_LEVEL_NODES_CLASS);
+ for (VoltageLevelNode vlNode : graph.getVoltageLevelNodesStream().filter(VoltageLevelNode::isVisible).collect(Collectors.toList())) {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(TRANSFORM_ATTRIBUTE, getTranslateString(vlNode));
+ drawNode(graph, writer, vlNode);
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawTextNodes(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.TEXT_NODES_CLASS);
+ for (Pair nodePair : graph.getVoltageLevelTextPairs()) {
+ writeTextNode(writer, nodePair.getSecond(), labelProvider.getVoltageLevelDescription(nodePair.getFirst()));
+ }
+ writer.writeEndElement();
+ }
+
+ private String getTranslateString(Node node) {
+ return getTranslateString(node.getPosition());
+ }
+
+ private String getTranslateString(Point point) {
+ return getTranslateString(point.getX(), point.getY());
+ }
+
+ private String getTranslateString(double x, double y) {
+ return "translate(" + getFormattedValue(x) + "," + getFormattedValue(y) + ")";
+ }
+
+ private void writeTextNode(XMLStreamWriter writer, TextNode textNode, List content) throws XMLStreamException {
+ if (textNode == null) {
+ return;
+ }
+ writer.writeStartElement(TEXT_ELEMENT_NAME);
+ if (svgParameters.isTextNodeBackground()) {
+ writer.writeAttribute(FILTER_ELEMENT_NAME, "url(#" + TEXT_BG_FILTER_ID + ")");
+ }
+ writer.writeAttribute(Y_ATTRIBUTE, getFormattedValue(textNode.getY()));
+ if (content.size() == 1) {
+ writer.writeAttribute(X_ATTRIBUTE, getFormattedValue(textNode.getX()));
+ writer.writeCharacters(content.get(0));
+ } else {
+ for (int i = 0; i < content.size(); i++) {
+ String line = content.get(i);
+ writer.writeStartElement(TSPAN_ELEMENT_NAME);
+ writer.writeAttribute(X_ATTRIBUTE, getFormattedValue(textNode.getX()));
+ if (i > 0) {
+ writer.writeAttribute(DY_ATTRIBUTE, "1.1em");
+ }
+ writer.writeCharacters(line);
+ writer.writeEndElement();
+ }
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawNode(Graph graph, XMLStreamWriter writer, VoltageLevelNode vlNode) throws XMLStreamException {
+ writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(vlNode.getDiagramId()));
+ addStylesIfAny(writer, styleProvider.getNodeStyleClasses(vlNode));
+ insertName(writer, vlNode::getName);
+
+ double nodeOuterRadius = getVoltageLevelCircleRadius(vlNode);
+
+ if (vlNode.hasUnknownBusNode()) {
+ writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
+ addStylesIfAny(writer, styleProvider.getNodeStyleClasses(BusNode.UNKNOWN));
+ writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(nodeOuterRadius + svgParameters.getUnknownBusNodeExtraRadius()));
+ }
+
+ List traversingBusEdges = new ArrayList<>();
+
+ for (BusNode busNode : vlNode.getBusNodes()) {
+ double busInnerRadius = getBusAnnulusInnerRadius(busNode, vlNode, svgParameters);
+ double busOuterRadius = getBusAnnulusOuterRadius(busNode, vlNode, svgParameters);
+ if (busInnerRadius == 0) {
+ writer.writeEmptyElement(CIRCLE_ELEMENT_NAME);
+ writer.writeAttribute(CIRCLE_RADIUS_ATTRIBUTE, getFormattedValue(busOuterRadius));
+ } else {
+ writer.writeEmptyElement(PATH_ELEMENT_NAME);
+ writer.writeAttribute(PATH_D_ATTRIBUTE, getFragmentedAnnulusPath(busInnerRadius, busOuterRadius, traversingBusEdges, graph, vlNode, busNode));
+ }
+ writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(busNode.getDiagramId()));
+
+ List nodeStyleClasses = styleProvider.getNodeStyleClasses(busNode);
+ nodeStyleClasses.add(StyleProvider.BUSNODE_CLASS);
+ addStylesIfAny(writer, nodeStyleClasses);
+
+ traversingBusEdges.addAll(graph.getBusEdges(busNode));
+ }
+ }
+
+ private String getFragmentedAnnulusPath(double innerRadius, double outerRadius, List traversingBusEdges, Graph graph, VoltageLevelNode vlNode, BusNode busNode) {
+ if (traversingBusEdges.isEmpty()) {
+ String path = "M" + getCirclePath(outerRadius, 0, Math.PI, true)
+ + " M" + getCirclePath(outerRadius, Math.PI, 0, true);
+ if (innerRadius > 0) { // going the other way around (counter-clockwise) to subtract the inner circle
+ path += "M" + getCirclePath(innerRadius, 0, Math.PI, false)
+ + "M" + getCirclePath(innerRadius, Math.PI, 0, false);
+ }
+ return path;
+ }
+
+ List angles = createSortedTraversingAnglesList(traversingBusEdges, graph, vlNode, busNode);
+
+ // adding first angle to close the circle annulus, and adding 360° to keep the list ordered
+ angles.add(angles.get(0) + 2 * Math.PI);
+
+ double halfWidth = svgParameters.getNodeHollowWidth() / 2;
+ double deltaAngle0 = halfWidth / outerRadius;
+ double deltaAngle1 = halfWidth / innerRadius;
+
+ StringBuilder path = new StringBuilder();
+ for (int i = 0; i < angles.size() - 1; i++) {
+ double outerArcStart = angles.get(i) + deltaAngle0;
+ double outerArcEnd = angles.get(i + 1) - deltaAngle0;
+ double innerArcStart = angles.get(i + 1) - deltaAngle1;
+ double innerArcEnd = angles.get(i) + deltaAngle1;
+ if (outerArcEnd > outerArcStart && innerArcEnd < innerArcStart) {
+ path.append("M").append(getCirclePath(outerRadius, outerArcStart, outerArcEnd, true))
+ .append(" L").append(getCirclePath(innerRadius, innerArcStart, innerArcEnd, false))
+ .append(" Z ");
+ }
+ }
+
+ return path.toString();
+ }
+
+ private List createSortedTraversingAnglesList(List traversingBusEdges, Graph graph, VoltageLevelNode vlNode, BusNode busNode) {
+ List angles = new ArrayList<>(traversingBusEdges.size());
+ for (Edge edge : traversingBusEdges) {
+ Node node1 = graph.getNode1(edge);
+ Node node2 = graph.getNode2(edge);
+ if (node1 == node2) {
+ // For looping edges we need to consider the two angles
+ if (isBusNodeDrawn(graph.getBusGraphNode1(edge), busNode)) {
+ angles.add(getEdgeStartAngle(edge, BranchEdge.Side.ONE));
+ }
+ if (isBusNodeDrawn(graph.getBusGraphNode2(edge), busNode)) {
+ angles.add(getEdgeStartAngle(edge, BranchEdge.Side.TWO));
+ }
+ } else {
+ angles.add(getEdgeStartAngle(edge, node1 == vlNode ? BranchEdge.Side.ONE : BranchEdge.Side.TWO));
+ }
+ }
+ Collections.sort(angles);
+
+ return angles;
+ }
+
+ private boolean isBusNodeDrawn(Node busGraphNode, BusNode busNodeCurrentlyDrawn) {
+ if (busGraphNode instanceof BusNode) {
+ return ((BusNode) busGraphNode).getIndex() < busNodeCurrentlyDrawn.getIndex();
+ }
+ return true;
+ }
+
+ private double getEdgeStartAngle(Edge edge, BranchEdge.Side side) {
+ if (edge instanceof ThreeWtEdge) {
+ return ((ThreeWtEdge) edge).getEdgeAngle();
+ } else if (edge instanceof BranchEdge) {
+ return ((BranchEdge) edge).getEdgeStartAngle(side);
+ }
+ return 0;
+ }
+
+ private String getCirclePath(double radius, double angleStart, double angleEnd, boolean clockWise) {
+ double arcAngle = angleEnd - angleStart;
+ double xStart = radius * Math.cos(angleStart);
+ double yStart = radius * Math.sin(angleStart);
+ double xEnd = radius * Math.cos(angleEnd);
+ double yEnd = radius * Math.sin(angleEnd);
+ int largeArc = Math.abs(arcAngle) > Math.PI ? 1 : 0;
+ return String.format(Locale.US, "%.3f,%.3f A%.3f,%.3f %.3f %d %d %.3f,%.3f",
+ xStart, yStart, radius, radius, Math.toDegrees(arcAngle), largeArc, clockWise ? 1 : 0, xEnd, yEnd);
+ }
+
+ private void insertName(XMLStreamWriter writer, Supplier> getName) throws XMLStreamException {
+ if (svgParameters.isInsertNameDesc()) {
+ Optional nodeName = getName.get();
+ if (nodeName.isPresent()) {
+ writer.writeStartElement(DESCRIPTION_ATTRIBUTE);
+ writer.writeCharacters(nodeName.get());
+ writer.writeEndElement();
+ }
+ }
+ }
+
+ private void drawTextEdges(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement(GROUP_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.TEXT_EDGES_CLASS);
+ for (TextEdge edge : graph.getTextEdges()) {
+ drawTextEdge(writer, edge, graph.getVoltageLevelNode(edge));
+ }
+ writer.writeEndElement();
+ }
+
+ private void drawTextEdge(XMLStreamWriter writer, TextEdge edge, VoltageLevelNode vlNode) throws XMLStreamException {
+ writer.writeEmptyElement(POLYLINE_ELEMENT_NAME);
+ writer.writeAttribute(ID_ATTRIBUTE, getPrefixedId(edge.getDiagramId()));
+ addStylesIfAny(writer, styleProvider.getEdgeStyleClasses(edge));
+ List points = edge.getPoints();
+ shiftEdgeStart(points, vlNode);
+ String lineFormatted1 = points.stream()
+ .map(point -> getFormattedValue(point.getX()) + "," + getFormattedValue(point.getY()))
+ .collect(Collectors.joining(" "));
+ writer.writeAttribute(POINTS_ATTRIBUTE, lineFormatted1);
+ }
+
+ private void addStylesIfAny(XMLStreamWriter writer, List edgeStyleClasses) throws XMLStreamException {
+ if (!edgeStyleClasses.isEmpty()) {
+ writer.writeAttribute(CLASS_ATTRIBUTE, String.join(" ", edgeStyleClasses));
+ }
+ }
+
+ private void shiftEdgeStart(List points, VoltageLevelNode vlNode) {
+ double circleRadius = getVoltageLevelCircleRadius(vlNode);
+ points.set(0, points.get(0).atDistance(circleRadius, points.get(1)));
+ }
+
+ private void addSvgRoot(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement("", SVG_ROOT_ELEMENT_NAME, NAMESPACE_URI);
+ if (svgParameters.isSvgWidthAndHeightAdded()) {
+ double[] diagramDimension = getDiagramDimensions(graph);
+ writer.writeAttribute(WIDTH_ATTRIBUTE, getFormattedValue(diagramDimension[0]));
+ writer.writeAttribute(HEIGHT_ATTRIBUTE, getFormattedValue(diagramDimension[1]));
+ }
+ writer.writeAttribute(VIEW_BOX_ATTRIBUTE, getViewBoxValue(graph));
+ writer.writeDefaultNamespace(NAMESPACE_URI);
+ }
+
+ private double[] getDiagramDimensions(Graph graph) {
+ double width = getDiagramWidth(graph);
+ double height = getDiagramHeight(graph);
+ double scale;
+ switch (svgParameters.getSizeConstraint()) {
+ case FIXED_WIDTH:
+ scale = svgParameters.getFixedWidth() / width;
+ break;
+ case FIXED_HEIGHT:
+ scale = svgParameters.getFixedHeight() / height;
+ break;
+ case FIXED_SCALE:
+ scale = svgParameters.getFixedScale();
+ break;
+ default:
+ scale = 1;
+ break;
+ }
+ return new double[] {width * scale, height * scale};
+ }
+
+ private double getDiagramHeight(Graph graph) {
+ Padding diagramPadding = svgParameters.getDiagramPadding();
+ return graph.getHeight() + diagramPadding.getTop() + diagramPadding.getBottom();
+ }
+
+ private double getDiagramWidth(Graph graph) {
+ Padding diagramPadding = svgParameters.getDiagramPadding();
+ return graph.getWidth() + diagramPadding.getLeft() + diagramPadding.getRight();
+ }
+
+ private String getViewBoxValue(Graph graph) {
+ Padding diagramPadding = svgParameters.getDiagramPadding();
+ return getFormattedValue(graph.getMinX() - diagramPadding.getLeft()) + " "
+ + getFormattedValue(graph.getMinY() - diagramPadding.getTop()) + " "
+ + getFormattedValue(getDiagramWidth(graph)) + " " + getFormattedValue(getDiagramHeight(graph));
+ }
+
+ private void addStyle(XMLStreamWriter writer) throws XMLStreamException {
+ switch (svgParameters.getCssLocation()) {
+ case INSERTED_IN_SVG:
+ writer.writeStartElement(STYLE_ELEMENT_NAME);
+ writer.writeCData(styleProvider.getStyleDefs());
+ writer.writeEndElement();
+ break;
+ case EXTERNAL_IMPORTED:
+ writer.writeStartElement(STYLE_ELEMENT_NAME);
+ for (String cssFilename : styleProvider.getCssFilenames()) {
+ writer.writeCharacters("@import url(" + cssFilename + ");");
+ }
+ writer.writeEndElement();
+ break;
+ case EXTERNAL_NO_IMPORT:
+ // nothing to do
+ break;
+ }
+ }
+
+ private void addMetadata(Graph graph, XMLStreamWriter writer) throws XMLStreamException {
+ GraphMetadata metadata = new GraphMetadata();
+
+ graph.getBusNodesStream().forEach(bn -> metadata.addBusNode(bn, this::getPrefixedId));
+ graph.getNodesStream().forEach(bn -> metadata.addNode(bn, this::getPrefixedId));
+ graph.getEdgesStream().forEach(bn -> metadata.addEdge(bn, this::getPrefixedId));
+
+ metadata.writeXml(writer);
+ }
+
+ private void addDefs(XMLStreamWriter writer) throws XMLStreamException {
+ if (svgParameters.isTextNodeBackground()) {
+ writer.writeStartElement(DEFS_ELEMENT_NAME);
+ writer.writeStartElement(FILTER_ELEMENT_NAME);
+ writer.writeAttribute(ID_ATTRIBUTE, TEXT_BG_FILTER_ID);
+ writer.writeAttribute(X_ATTRIBUTE, String.valueOf(0));
+ writer.writeAttribute(Y_ATTRIBUTE, String.valueOf(0));
+ writer.writeAttribute(WIDTH_ATTRIBUTE, String.valueOf(1));
+ writer.writeAttribute(HEIGHT_ATTRIBUTE, String.valueOf(1));
+ writer.writeEmptyElement(FE_FLOOD_ELEMENT_NAME);
+ writer.writeAttribute(CLASS_ATTRIBUTE, StyleProvider.TEXT_BACKGROUND_CLASS);
+ writer.writeEmptyElement(FE_COMPOSITE_ELEMENT_NAME);
+ writer.writeAttribute(FE_IN_ATTRIBUTE, "SourceGraphic");
+ writer.writeAttribute(FE_OPERATOR_ATTRIBUTE, "over");
+ writer.writeEndElement();
+ writer.writeEndElement();
+ }
+ }
+
+ private static String getFormattedValue(double value) {
+ return String.format(Locale.US, "%.2f", value);
+ }
+
+ protected double getVoltageLevelCircleRadius(VoltageLevelNode vlNode) {
+ return getVoltageLevelCircleRadius(vlNode, svgParameters);
+ }
+
+ protected static double getVoltageLevelCircleRadius(VoltageLevelNode vlNode, SvgParameters svgParameters) {
+ if (vlNode.isFictitious()) {
+ return svgParameters.getFictitiousVoltageLevelCircleRadius();
+ }
+ int nbBuses = vlNode.getBusNodes().size();
+ return Math.min(Math.max(nbBuses, 1), 2) * svgParameters.getVoltageLevelCircleRadius();
+ }
+
+ public static double getBusAnnulusInnerRadius(BusNode node, VoltageLevelNode vlNode, SvgParameters svgParameters) {
+ if (node.getIndex() == 0) {
+ return 0;
+ }
+ int nbNeighbours = node.getNbNeighbouringBusNodes();
+ double unitaryRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) / (nbNeighbours + 1);
+ return node.getIndex() * unitaryRadius + svgParameters.getInterAnnulusSpace() / 2;
+ }
+
+ public static double getBusAnnulusOuterRadius(BusNode node, VoltageLevelNode vlNode, SvgParameters svgParameters) {
+ int nbNeighbours = node.getNbNeighbouringBusNodes();
+ double unitaryRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) / (nbNeighbours + 1);
+ return (node.getIndex() + 1) * unitaryRadius - svgParameters.getInterAnnulusSpace() / 2;
+ }
+
+ public String getPrefixedId(String id) {
+ return svgParameters.getSvgPrefix() + id;
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/AbstractVoltageStyleProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/AbstractVoltageStyleProvider.java
new file mode 100644
index 000000000..f188a21c8
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/AbstractVoltageStyleProvider.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2022, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg.iidm;
+
+import com.powsybl.commons.config.BaseVoltagesConfig;
+import com.powsybl.iidm.network.Branch;
+import com.powsybl.iidm.network.Bus;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.Terminal;
+import com.powsybl.nad.model.*;
+import com.powsybl.nad.svg.AbstractStyleProvider;
+import com.powsybl.nad.svg.StyleProvider;
+import com.powsybl.nad.utils.iidm.IidmUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Florian Dupuy
+ */
+public abstract class AbstractVoltageStyleProvider extends AbstractStyleProvider {
+
+ protected final Network network;
+
+ protected AbstractVoltageStyleProvider(Network network) {
+ this.network = network;
+ }
+
+ protected AbstractVoltageStyleProvider(Network network, BaseVoltagesConfig baseVoltageStyle) {
+ super(baseVoltageStyle);
+ this.network = network;
+ }
+
+ @Override
+ public List getNodeStyleClasses(BusNode busNode) {
+ if (busNode == BusNode.UNKNOWN) {
+ return Collections.singletonList(UNKNOWN_BUSNODE_CLASS);
+ }
+ List styles = new ArrayList<>();
+ Bus b = network.getBusView().getBus(busNode.getEquipmentId());
+ if (b != null) {
+ if (b.getV() > b.getVoltageLevel().getHighVoltageLimit()) {
+ styles.add(StyleProvider.VL_OVERVOLTAGE_CLASS);
+ } else if (b.getV() < b.getVoltageLevel().getLowVoltageLimit()) {
+ styles.add(StyleProvider.VL_UNDERVOLTAGE_CLASS);
+ }
+ }
+ return styles;
+ }
+
+ @Override
+ public List getEdgeStyleClasses(Edge edge) {
+ List styleClasses = new ArrayList<>(super.getEdgeStyleClasses(edge));
+ if (edge instanceof BranchEdge && !((BranchEdge) edge).getType().equals(BranchEdge.HVDC_LINE_EDGE)) {
+ Branch> branch = network.getBranch(edge.getEquipmentId());
+ if (branch.isOverloaded()) {
+ styleClasses.add(StyleProvider.LINE_OVERLOADED_CLASS);
+ }
+ }
+ return styleClasses;
+ }
+
+ @Override
+ protected Optional getBaseVoltageStyle(ThreeWtNode threeWtNode, ThreeWtEdge.Side side) {
+ Terminal terminal = network.getThreeWindingsTransformer(threeWtNode.getEquipmentId())
+ .getTerminal(IidmUtils.getIidmSideFromThreeWtEdgeSide(side));
+ return getBaseVoltageStyle(terminal);
+ }
+
+ @Override
+ protected boolean isDisconnected(Edge edge) {
+ if (edge instanceof ThreeWtEdge) {
+ ThreeWtEdge twtEdge = (ThreeWtEdge) edge;
+ Terminal terminal = network.getThreeWindingsTransformer(twtEdge.getEquipmentId())
+ .getTerminal(IidmUtils.getIidmSideFromThreeWtEdgeSide(twtEdge.getSide()));
+ return terminal == null || !terminal.isConnected();
+ }
+ if (edge instanceof BranchEdge) {
+ return isDisconnected((BranchEdge) edge, BranchEdge.Side.ONE) && isDisconnected((BranchEdge) edge, BranchEdge.Side.TWO);
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean isDisconnected(BranchEdge edge, BranchEdge.Side side) {
+ Terminal terminal = IidmUtils.getTerminalFromEdge(network, edge, side);
+ return terminal == null || !terminal.isConnected();
+ }
+
+ @Override
+ protected boolean isDisconnected(ThreeWtNode threeWtNode, ThreeWtEdge.Side side) {
+ Terminal terminal = network.getThreeWindingsTransformer(threeWtNode.getEquipmentId())
+ .getTerminal(IidmUtils.getIidmSideFromThreeWtEdgeSide(side));
+ return terminal == null || !terminal.isConnected();
+ }
+
+ @Override
+ protected Optional getBaseVoltageStyle(Edge edge) {
+ if (edge instanceof BranchEdge) {
+ String branchType = ((BranchEdge) edge).getType();
+ if (branchType.equals(BranchEdge.HVDC_LINE_EDGE)) {
+ return Optional.of(HVDC_EDGE_CLASS);
+ }
+ } else if (edge instanceof ThreeWtEdge) {
+ Terminal terminal = network.getThreeWindingsTransformer(edge.getEquipmentId())
+ .getTerminal(IidmUtils.getIidmSideFromThreeWtEdgeSide(((ThreeWtEdge) edge).getSide()));
+ return getBaseVoltageStyle(terminal);
+ }
+
+ return Optional.empty();
+ }
+
+ @Override
+ protected Optional getBaseVoltageStyle(BranchEdge edge, BranchEdge.Side side) {
+ Terminal terminal = IidmUtils.getTerminalFromEdge(network, edge, side);
+ return getBaseVoltageStyle(terminal);
+ }
+
+ protected abstract Optional getBaseVoltageStyle(Terminal terminal);
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/DefaultLabelProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/DefaultLabelProvider.java
new file mode 100644
index 000000000..bc1038726
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/DefaultLabelProvider.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg.iidm;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.iidm.network.*;
+import com.powsybl.nad.svg.SvgParameters;
+import com.powsybl.nad.utils.iidm.IidmUtils;
+import com.powsybl.nad.model.*;
+import com.powsybl.nad.svg.EdgeInfo;
+import com.powsybl.nad.svg.LabelProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Florian Dupuy
+ */
+public class DefaultLabelProvider implements LabelProvider {
+ private final Network network;
+ private final SvgParameters svgParameters;
+
+ public DefaultLabelProvider(Network network, SvgParameters svgParameters) {
+ this.network = network;
+ this.svgParameters = svgParameters;
+ }
+
+ @Override
+ public List getEdgeInfos(Graph graph, BranchEdge edge, BranchEdge.Side side) {
+ Terminal terminal = IidmUtils.getTerminalFromEdge(network, edge, side);
+ return getEdgeInfos(terminal);
+ }
+
+ @Override
+ public List getEdgeInfos(Graph graph, ThreeWtEdge edge) {
+ ThreeWindingsTransformer transformer = network.getThreeWindingsTransformer(edge.getEquipmentId());
+ if (transformer == null) {
+ throw new PowsyblException("Unknown three windings transformer '" + edge.getEquipmentId() + "'");
+ }
+ Terminal terminal = transformer.getTerminal(IidmUtils.getIidmSideFromThreeWtEdgeSide(edge.getSide()));
+ return getEdgeInfos(terminal);
+ }
+
+ private List getEdgeInfos(Terminal terminal) {
+ if (terminal == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(new EdgeInfo(EdgeInfo.ACTIVE_POWER, terminal.getP()),
+ new EdgeInfo(EdgeInfo.REACTIVE_POWER, terminal.getQ()));
+ }
+
+ @Override
+ public List getVoltageLevelDescription(VoltageLevelNode voltageLevelNode) {
+ VoltageLevel vl = network.getVoltageLevel(voltageLevelNode.getEquipmentId());
+ List description = new ArrayList<>();
+ description.add(svgParameters.isIdDisplayed() ? vl.getId() : vl.getNameOrId());
+ if (svgParameters.isSubstationDescriptionDisplayed()) {
+ vl.getSubstation()
+ .map(s -> svgParameters.isIdDisplayed() ? s.getId() : s.getNameOrId())
+ .ifPresent(description::add);
+ }
+ return description;
+ }
+
+ @Override
+ public String getArrowPathDIn() {
+ return "M-1 -1 H1 L0 1z";
+ }
+
+ @Override
+ public String getArrowPathDOut() {
+ return "M-1 1 H1 L0 -1z";
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/NominalVoltageStyleProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/NominalVoltageStyleProvider.java
new file mode 100644
index 000000000..f1d00d650
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/NominalVoltageStyleProvider.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg.iidm;
+
+import com.powsybl.commons.config.BaseVoltagesConfig;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.Terminal;
+import com.powsybl.nad.model.Node;
+import com.powsybl.nad.model.VoltageLevelNode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Florian Dupuy
+ */
+public class NominalVoltageStyleProvider extends AbstractVoltageStyleProvider {
+
+ public NominalVoltageStyleProvider(Network network) {
+ super(network);
+ }
+
+ public NominalVoltageStyleProvider(Network network, BaseVoltagesConfig baseVoltageStyle) {
+ super(network, baseVoltageStyle);
+ }
+
+ @Override
+ public List getCssFilenames() {
+ return Collections.singletonList("nominalStyle.css");
+ }
+
+ @Override
+ public List getNodeStyleClasses(Node node) {
+ List styles = new ArrayList<>();
+ if (node instanceof VoltageLevelNode) {
+ double nominalV = network.getVoltageLevel(node.getEquipmentId()).getNominalV();
+ getBaseVoltageStyle(nominalV).ifPresent(styles::add);
+ }
+ return styles;
+ }
+
+ @Override
+ protected Optional getBaseVoltageStyle(Terminal terminal) {
+ if (terminal == null) {
+ return Optional.empty();
+ }
+ return getBaseVoltageStyle(terminal.getVoltageLevel().getNominalV());
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/TopologicalStyleProvider.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/TopologicalStyleProvider.java
new file mode 100644
index 000000000..f404daa83
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/TopologicalStyleProvider.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2021, RTE (http://www.rte-france.com)
+ * 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/.
+ */
+package com.powsybl.nad.svg.iidm;
+
+import com.powsybl.commons.config.BaseVoltagesConfig;
+import com.powsybl.iidm.network.*;
+import com.powsybl.nad.model.BusNode;
+import com.powsybl.nad.model.Node;
+import com.powsybl.nad.utils.iidm.IidmUtils;
+
+import java.util.*;
+
+/**
+ * @author Florian Dupuy
+ */
+public class TopologicalStyleProvider extends AbstractVoltageStyleProvider {
+
+ private final Map styleMap = new HashMap<>();
+ private final Map baseVoltagesCounter = new HashMap<>();
+
+ public TopologicalStyleProvider(Network network) {
+ super(network);
+ }
+
+ public TopologicalStyleProvider(Network network, BaseVoltagesConfig baseVoltageStyle) {
+ super(network, baseVoltageStyle);
+ }
+
+ @Override
+ public List getCssFilenames() {
+ return Collections.singletonList("topologicalStyle.css");
+ }
+
+ @Override
+ public List getNodeStyleClasses(Node node) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getNodeStyleClasses(BusNode busNode) {
+ List styles = new ArrayList<>(super.getNodeStyleClasses(busNode));
+ Bus b = network.getBusView().getBus(busNode.getEquipmentId());
+ getNodeTopologicalStyle(b).ifPresent(styles::add);
+ return styles;
+ }
+
+ private Optional getNodeTopologicalStyle(Bus b) {
+ if (b == null) {
+ return Optional.empty();
+ }
+ if (styleMap.containsKey(b.getId())) {
+ return Optional.ofNullable(styleMap.get(b.getId()));
+ }
+ return getBaseVoltageStyle(b.getVoltageLevel().getNominalV())
+ .map(baseVoltageStyle -> fillStyleMap(baseVoltageStyle, b));
+ }
+
+ private String fillStyleMap(String style, Bus bus) {
+ Collection connectedBuses = getConnectedBuses(bus);
+ String topologicalStyle = createNewTopologicalStyle(style);
+ connectedBuses.forEach(b -> styleMap.put(b.getId(), topologicalStyle));
+ return topologicalStyle;
+ }
+
+ private Collection getConnectedBuses(Bus bus) {
+ Set visitedBuses = new HashSet<>();
+ findConnectedBuses(bus, visitedBuses);
+ return visitedBuses;
+ }
+
+ private String createNewTopologicalStyle(String style) {
+ Integer baseVoltageIndex = baseVoltagesCounter.compute(style, (k, v) -> v == null ? 0 : v + 1);
+ return style + "-" + baseVoltageIndex;
+ }
+
+ private void findConnectedBuses(Bus bus, Set visitedBus) {
+ if (visitedBus.contains(bus)) {
+ return;
+ }
+ visitedBus.add(bus);
+ bus.visitConnectedEquipments(new DefaultTopologyVisitor() {
+ @Override
+ public void visitLine(Line line, Branch.Side side) {
+ Terminal t = line.getTerminal(IidmUtils.getOpposite(side));
+ Bus otherBus = t.getBusView().getBus();
+ if (otherBus != null && !visitedBus.contains(otherBus)) {
+ findConnectedBuses(otherBus, visitedBus);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected Optional getBaseVoltageStyle(Terminal terminal) {
+ if (terminal == null) {
+ return Optional.empty();
+ }
+ return terminal.isConnected()
+ ? getNodeTopologicalStyle(terminal.getBusView().getBus())
+ : getNodeTopologicalStyle(terminal.getBusView().getConnectableBus());
+ }
+}
diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/utils/iidm/IidmUtils.java b/network-area-diagram/src/main/java/com/powsybl/nad/utils/iidm/IidmUtils.java
new file mode 100644
index 000000000..665d7b35c
--- /dev/null
+++ b/network-area-diagram/src/main/java/com/powsybl/nad/utils/iidm/IidmUtils.java
@@ -0,0 +1,70 @@
+package com.powsybl.nad.utils.iidm;
+
+import com.powsybl.iidm.network.*;
+import com.powsybl.nad.model.BranchEdge;
+import com.powsybl.nad.model.ThreeWtEdge;
+
+import java.util.Objects;
+
+public final class IidmUtils {
+
+ private IidmUtils() {
+ }
+
+ public static Terminal getTerminalFromEdge(Network network, BranchEdge edge, BranchEdge.Side side) {
+ if (!edge.getType().equals(BranchEdge.HVDC_LINE_EDGE)) {
+ Branch> branch = network.getBranch(edge.getEquipmentId());
+ return branch.getTerminal(IidmUtils.getIidmSideFromBranchEdgeSide(side));
+ } else {
+ HvdcLine line = network.getHvdcLine(edge.getEquipmentId());
+ return line.getConverterStation(IidmUtils.getIidmHvdcSideFromBranchEdgeSide(side)).getTerminal();
+ }
+ }
+
+ public static ThreeWindingsTransformer.Leg get3wtLeg(ThreeWindingsTransformer twt, ThreeWindingsTransformer.Side side) {
+ if (side == ThreeWindingsTransformer.Side.ONE) {
+ return twt.getLeg1();
+ } else if (side == ThreeWindingsTransformer.Side.TWO) {
+ return twt.getLeg2();
+ } else {
+ return twt.getLeg3();
+ }
+ }
+
+ public static Branch.Side getOpposite(Branch.Side side) {
+ return side == Branch.Side.ONE ? Branch.Side.TWO : Branch.Side.ONE;
+ }
+
+ public static Branch.Side getIidmSideFromBranchEdgeSide(BranchEdge.Side side) {
+ return Objects.requireNonNull(side) == BranchEdge.Side.ONE ? Branch.Side.ONE : Branch.Side.TWO;
+ }
+
+ public static HvdcLine.Side getIidmHvdcSideFromBranchEdgeSide(BranchEdge.Side side) {
+ return Objects.requireNonNull(side) == BranchEdge.Side.ONE ? HvdcLine.Side.ONE : HvdcLine.Side.TWO;
+ }
+
+ public static ThreeWindingsTransformer.Side getIidmSideFromThreeWtEdgeSide(ThreeWtEdge.Side side) {
+ switch (Objects.requireNonNull(side)) {
+ case ONE:
+ return ThreeWindingsTransformer.Side.ONE;
+ case TWO:
+ return ThreeWindingsTransformer.Side.TWO;
+ case THREE:
+ return ThreeWindingsTransformer.Side.THREE;
+ }
+ return null;
+ }
+
+ public static ThreeWtEdge.Side getThreeWtEdgeSideFromIidmSide(ThreeWindingsTransformer.Side side) {
+ switch (Objects.requireNonNull(side)) {
+ case ONE:
+ return ThreeWtEdge.Side.ONE;
+ case TWO:
+ return ThreeWtEdge.Side.TWO;
+ case THREE:
+ return ThreeWtEdge.Side.THREE;
+ }
+ return null;
+ }
+
+}
diff --git a/network-area-diagram/src/main/resources/nad_V1_0.xsd b/network-area-diagram/src/main/resources/nad_V1_0.xsd
new file mode 100644
index 000000000..4fa889a96
--- /dev/null
+++ b/network-area-diagram/src/main/resources/nad_V1_0.xsd
@@ -0,0 +1,53 @@
+
+
+
+
+