diff --git a/src/main/java/com/powsybl/forcelayout/ForceLayout.java b/src/main/java/com/powsybl/forcelayout/ForceLayout.java index 65f48899..05c54f47 100644 --- a/src/main/java/com/powsybl/forcelayout/ForceLayout.java +++ b/src/main/java/com/powsybl/forcelayout/ForceLayout.java @@ -53,7 +53,8 @@ public class ForceLayout { private static final Logger LOGGER = LoggerFactory.getLogger(ForceLayout.class); - private final Random random = new Random(3L); // deterministic randomness + /** Deterministic randomness */ + private final Random random = new Random(3L); private static final int DEFAULT_MAX_STEPS = 1000; private static final double DEFAULT_MIN_ENERGY_THRESHOLD = 0.001; @@ -61,7 +62,8 @@ public class ForceLayout { private static final double DEFAULT_REPULSION = 800.0; private static final double DEFAULT_FRICTION = 500; private static final double DEFAULT_MAX_SPEED = 100; - private static final double DEFAULT_SPRING_REPULSION_FACTOR = 0.0; // Disabled by default + /** Spring repulsion is disabled by default */ + private static final double DEFAULT_SPRING_REPULSION_FACTOR = 0.0; private int maxSteps; private double minEnergyThreshold; @@ -70,6 +72,10 @@ public class ForceLayout { private double friction; private double maxSpeed; private double springRepulsionFactor; + /** Initial location for some nodes */ + private Map initialPoints = Collections.emptyMap(); + /** The location of these nodes should not be modified by the layout */ + private Set fixedNodes = Collections.emptySet(); private final Graph graph; private final Map points = new LinkedHashMap<>(); @@ -85,7 +91,6 @@ public ForceLayout(Graph graph) { this.friction = DEFAULT_FRICTION; this.maxSpeed = DEFAULT_MAX_SPEED; this.springRepulsionFactor = DEFAULT_SPRING_REPULSION_FACTOR; - this.graph = Objects.requireNonNull(graph); } @@ -124,9 +129,31 @@ public ForceLayout setSpringRepulsionFactor(double springRepulsionFactor) return this; } + public ForceLayout setInitialPoints(Map initialPoints) { + this.initialPoints = Objects.requireNonNull(initialPoints); + return this; + } + + public ForceLayout setFixedPoints(Map fixedPoints) { + this.initialPoints = Objects.requireNonNull(fixedPoints); + setFixedNodes(fixedPoints.keySet()); + return this; + } + + public ForceLayout setFixedNodes(Set fixedNodes) { + this.fixedNodes = Objects.requireNonNull(fixedNodes); + return this; + } + private void initializePoints() { for (V vertex : graph.vertexSet()) { - points.put(vertex, new Point(random.nextDouble(), random.nextDouble())); + Point p; + if (initialPoints.containsKey(vertex)) { + p = initialPoints.get(vertex); + } else { + p = new Point(random.nextDouble(), random.nextDouble()); + } + points.put(vertex, p); } } @@ -264,7 +291,15 @@ private void updateVelocity() { } private void updatePosition() { - for (Point point : points.values()) { + // TODO do not compute forces or update velocities for fixed nodes + // We have computed forces and velocities for all nodes, even for the fixed ones + // We can optimize calculations by ignoring fixed nodes in those calculations + // Here we only update the position for the nodes that do not have fixed positions + for (Map.Entry vertexPoint : points.entrySet()) { + if (fixedNodes.contains(vertexPoint.getKey())) { + continue; + } + Point point = vertexPoint.getValue(); Vector position = point.getPosition().add(point.getVelocity().multiply(deltaTime)); point.setPosition(position); } diff --git a/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java b/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java index e0336bd3..5d218d69 100644 --- a/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java +++ b/src/main/java/com/powsybl/nad/NetworkAreaDiagram.java @@ -61,6 +61,10 @@ public NetworkAreaDiagram(Network network, Predicate voltageLevelF this.voltageLevelFilter = Objects.requireNonNull(voltageLevelFilter); } + public Network getNetwork() { + return network; + } + public void draw(Path svgFile) { draw(svgFile, new SvgParameters()); } diff --git a/src/main/java/com/powsybl/nad/layout/AbstractLayout.java b/src/main/java/com/powsybl/nad/layout/AbstractLayout.java index 94eaa8e7..cee1c330 100644 --- a/src/main/java/com/powsybl/nad/layout/AbstractLayout.java +++ b/src/main/java/com/powsybl/nad/layout/AbstractLayout.java @@ -1,12 +1,16 @@ package com.powsybl.nad.layout; import com.powsybl.nad.model.*; +import org.jgrapht.alg.util.Pair; -import java.util.Objects; +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); @@ -19,9 +23,53 @@ public void run(Graph graph, LayoutParameters 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 abstract void busNodesLayout(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); diff --git a/src/main/java/com/powsybl/nad/layout/BasicFixedLayout.java b/src/main/java/com/powsybl/nad/layout/BasicFixedLayout.java new file mode 100644 index 00000000..2eab5f56 --- /dev/null +++ b/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/src/main/java/com/powsybl/nad/layout/BasicFixedLayoutFactory.java b/src/main/java/com/powsybl/nad/layout/BasicFixedLayoutFactory.java new file mode 100644 index 00000000..341e28b9 --- /dev/null +++ b/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/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java b/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java index 6adb966d..a6bcbd16 100644 --- a/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java +++ b/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java @@ -8,27 +8,40 @@ import com.powsybl.forcelayout.ForceLayout; import com.powsybl.forcelayout.Vector; -import com.powsybl.nad.model.*; -import org.jgrapht.alg.util.Pair; +import com.powsybl.nad.model.Edge; +import com.powsybl.nad.model.Graph; +import com.powsybl.nad.model.Node; -import java.util.Comparator; -import java.util.List; +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(100 * p.getX(), 100 * p.getY()); + node.setPosition(SCALE * p.getX(), SCALE * p.getY()); }); if (!layoutParameters.isTextNodesForceLayout()) { @@ -36,23 +49,16 @@ protected void nodesLayout(Graph graph, LayoutParameters layoutParameters) { } } - protected void busNodesLayout(Graph graph, LayoutParameters layoutParameters) { - 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()); - } - }); - } - - private void fixedTextNodeLayout(Pair nodes, LayoutParameters layoutParameters) { - Point fixedShift = layoutParameters.getTextNodeFixedShift(); - Point textPos = nodes.getFirst().getPosition().shift(fixedShift.getX(), fixedShift.getY()); - nodes.getSecond().setPosition(textPos); + 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.forcelayout.Point( + nodePosition.getValue().getX() / SCALE, + nodePosition.getValue().getY() / SCALE) + )); + forceLayout.setInitialPoints(initialPoints); } } diff --git a/src/main/java/com/powsybl/nad/layout/Layout.java b/src/main/java/com/powsybl/nad/layout/Layout.java index 65977d83..fd86e68e 100644 --- a/src/main/java/com/powsybl/nad/layout/Layout.java +++ b/src/main/java/com/powsybl/nad/layout/Layout.java @@ -7,10 +7,22 @@ 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/src/main/java/com/powsybl/nad/model/Graph.java b/src/main/java/com/powsybl/nad/model/Graph.java index 7f91ab8f..100a6985 100644 --- a/src/main/java/com/powsybl/nad/model/Graph.java +++ b/src/main/java/com/powsybl/nad/model/Graph.java @@ -313,4 +313,13 @@ public boolean containsNode(String 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/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java b/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java new file mode 100644 index 00000000..ff1d1ecb --- /dev/null +++ b/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java @@ -0,0 +1,48 @@ +/** + * 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.iidm.network.Network; +import com.powsybl.nad.build.iidm.NetworkGraphBuilder; +import com.powsybl.nad.build.iidm.VoltageLevelFilter; +import com.powsybl.nad.model.Graph; +import com.powsybl.nad.model.Point; +import com.powsybl.nad.svg.NetworkTestFactory; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Luma Zamarreno + */ +class FixedLayoutTest { + + @Test + void testCurrentLimits() { + Network network = NetworkTestFactory.createTwoVoltageLevels(); + + Map expected = Map.of( + "vl1", new Point(1, 0), + "vl2", new Point(2, 1)); + Graph graph = new NetworkGraphBuilder(network, VoltageLevelFilter.NO_FILTER).buildGraph(); + Layout fixedLayout = new BasicFixedLayoutFactory(expected).create(); + fixedLayout.run(graph, new LayoutParameters()); + Map actual = graph.getNodePositions(); + + assertEquals(expected.keySet(), actual.keySet()); + expected.keySet().forEach(k -> { + Point pexpected = expected.get(k); + Point pactual = actual.get(k); + assertNotNull(pactual); + assertEquals(pexpected.getX(), pactual.getX()); + assertEquals(pexpected.getY(), pactual.getY()); + }); + } +} diff --git a/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java b/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java index 2054a592..d092d580 100644 --- a/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java +++ b/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java @@ -45,7 +45,7 @@ protected LabelProvider getLabelProvider(Network network) { void testDiamondNoSpringRepulsionFactor() { assertEquals( toString("/diamond-spring-repulsion-factor-0.0.svg"), - generateSvgString(createDiamondNetwork(), "/diamond-spring-repulsion-factor-0.0.svg")); + generateSvgString(LayoutNetworkFactory.createDiamond(), "/diamond-spring-repulsion-factor-0.0.svg")); } @Test @@ -53,118 +53,6 @@ void testDiamondSmallSpringRepulsionFactor() { getLayoutParameters().setSpringRepulsionFactorForceLayout(0.2); assertEquals( toString("/diamond-spring-repulsion-factor-0.2.svg"), - generateSvgString(createDiamondNetwork(), "/diamond-spring-repulsion-factor-0.2.svg")); - } - - static Network createDiamondNetwork() { - Network network = NetworkFactory.findDefault().createNetwork("diamond", "manual"); - network.setName("diamond"); - - Substation subA = network.newSubstation().setId("A").add(); - Bus subA400 = createBus(subA, 400); - Bus subA230 = createBus(subA, 230); - createTransformer(subA400, subA230); - - Substation subB = network.newSubstation().setId("B").add(); - Bus subB230 = createBus(subB, 230); - createLine(subA230, subB230); - - Substation subC = network.newSubstation().setId("C").add(); - Bus subC230 = createBus(subC, 230); - Bus subC66 = createBus(subC, 66); - Bus subC20 = createBus(subC, 20); - createTransformer(subC230, subC66); - createTransformer(subC66, subC20); - createLine(subB230, subC230); - - Substation subD = network.newSubstation().setId("D").add(); - Bus subD66 = createBus(subD, 66); - Bus subD10 = createBus(subD, 10); - createTransformer(subD66, subD10); - createLine(subC66, subD66); - - Substation subE = network.newSubstation().setId("E").add(); - Bus subE10 = createBus(subE, 10); - createLine(subD10, subE10); - - Bus subF10 = createBus(network, "F", 10); - Bus subG10 = createBus(network, "G", 10); - Bus subH10 = createBus(network, "H", 10); - Bus subI10 = createBus(network, "I", 10); - Bus subJ10 = createBus(network, "J", 10); - Bus subK10 = createBus(network, "K", 10); - - createLine(subE10, subF10); - createLine(subF10, subG10); - createLine(subG10, subH10); - createLine(subH10, subD10); - - createLine(subF10, subI10); - createLine(subI10, subJ10); - createLine(subJ10, subK10); - createLine(subK10, subD10); - - return network; - } - - private static Bus createBus(Network network, String substationId, double nominalVoltage) { - Substation substation = network.newSubstation().setId(substationId).add(); - return createBus(substation, nominalVoltage); - } - - private static Bus createBus(Substation substation, double nominalVoltage) { - String vlId = String.format("%s %.0f", substation.getId(), nominalVoltage); - String busId = String.format("%s %s", vlId, "Bus"); - return substation.newVoltageLevel() - .setId(vlId) - .setNominalV(nominalVoltage) - .setTopologyKind(TopologyKind.BUS_BREAKER) - .add() - .getBusBreakerView() - .newBus() - .setId(busId) - .add(); - } - - private static void createTransformer(Bus bus1, Bus bus2) { - Substation substation = bus1.getVoltageLevel().getSubstation().orElseThrow(); - String id = String.format("%s %.0f %.0f", - substation.getId(), - bus1.getVoltageLevel().getNominalV(), - bus2.getVoltageLevel().getNominalV()); - substation.newTwoWindingsTransformer().setId(id) - .setR(0.0) - .setX(1.0) - .setG(0.0) - .setB(0.0) - .setVoltageLevel1(bus1.getVoltageLevel().getId()) - .setVoltageLevel2(bus2.getVoltageLevel().getId()) - .setConnectableBus1(bus1.getId()) - .setConnectableBus2(bus2.getId()) - .setRatedU1(bus1.getVoltageLevel().getNominalV()) - .setRatedU2(bus2.getVoltageLevel().getNominalV()) - .setBus1(bus1.getId()) - .setBus2(bus2.getId()) - .add(); - } - - private static void createLine(Bus bus1, Bus bus2) { - String id = String.format("%s - %s", - bus1.getVoltageLevel().getSubstation().orElseThrow().getId(), - bus2.getVoltageLevel().getSubstation().orElseThrow().getId()); - bus1.getNetwork().newLine().setId(id) - .setR(0.0) - .setX(1.0) - .setG1(0.0) - .setB1(0.0) - .setG2(0.0) - .setB2(0.0) - .setVoltageLevel1(bus1.getVoltageLevel().getId()) - .setVoltageLevel2(bus2.getVoltageLevel().getId()) - .setConnectableBus1(bus1.getId()) - .setConnectableBus2(bus2.getId()) - .setBus1(bus1.getId()) - .setBus2(bus2.getId()) - .add(); + generateSvgString(LayoutNetworkFactory.createDiamond(), "/diamond-spring-repulsion-factor-0.2.svg")); } } diff --git a/src/test/java/com/powsybl/nad/layout/LayoutNetworkFactory.java b/src/test/java/com/powsybl/nad/layout/LayoutNetworkFactory.java new file mode 100644 index 00000000..8889e21c --- /dev/null +++ b/src/test/java/com/powsybl/nad/layout/LayoutNetworkFactory.java @@ -0,0 +1,131 @@ +/** + * 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.iidm.network.*; + +/** + * @author Luma Zamarreno + */ +public final class LayoutNetworkFactory { + + private LayoutNetworkFactory() { + // Empty + } + + public static Network createDiamond() { + Network network = com.powsybl.iidm.network.NetworkFactory.findDefault().createNetwork("diamond", "manual"); + network.setName("diamond"); + + Substation subA = network.newSubstation().setId("A").add(); + Bus subA400 = createBus(subA, 400); + Bus subA230 = createBus(subA, 230); + createTransformer(subA400, subA230); + + Substation subB = network.newSubstation().setId("B").add(); + Bus subB230 = createBus(subB, 230); + createLine(subA230, subB230); + + Substation subC = network.newSubstation().setId("C").add(); + Bus subC230 = createBus(subC, 230); + Bus subC66 = createBus(subC, 66); + Bus subC20 = createBus(subC, 20); + createTransformer(subC230, subC66); + createTransformer(subC66, subC20); + createLine(subB230, subC230); + + Substation subD = network.newSubstation().setId("D").add(); + Bus subD66 = createBus(subD, 66); + Bus subD10 = createBus(subD, 10); + createTransformer(subD66, subD10); + createLine(subC66, subD66); + + Substation subE = network.newSubstation().setId("E").add(); + Bus subE10 = createBus(subE, 10); + createLine(subD10, subE10); + + Bus subF10 = createBus(network, "F", 10); + Bus subG10 = createBus(network, "G", 10); + Bus subH10 = createBus(network, "H", 10); + Bus subI10 = createBus(network, "I", 10); + Bus subJ10 = createBus(network, "J", 10); + Bus subK10 = createBus(network, "K", 10); + + createLine(subE10, subF10); + createLine(subF10, subG10); + createLine(subG10, subH10); + createLine(subH10, subD10); + + createLine(subF10, subI10); + createLine(subI10, subJ10); + createLine(subJ10, subK10); + createLine(subK10, subD10); + + return network; + } + + private static Bus createBus(Network network, String substationId, double nominalVoltage) { + Substation substation = network.newSubstation().setId(substationId).add(); + return createBus(substation, nominalVoltage); + } + + private static Bus createBus(Substation substation, double nominalVoltage) { + String vlId = String.format("%s %.0f", substation.getId(), nominalVoltage); + String busId = String.format("%s %s", vlId, "Bus"); + return substation.newVoltageLevel() + .setId(vlId) + .setNominalV(nominalVoltage) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add() + .getBusBreakerView() + .newBus() + .setId(busId) + .add(); + } + + private static void createTransformer(Bus bus1, Bus bus2) { + Substation substation = bus1.getVoltageLevel().getSubstation().orElseThrow(); + String id = String.format("%s %.0f %.0f", + substation.getId(), + bus1.getVoltageLevel().getNominalV(), + bus2.getVoltageLevel().getNominalV()); + substation.newTwoWindingsTransformer().setId(id) + .setR(0.0) + .setX(1.0) + .setG(0.0) + .setB(0.0) + .setVoltageLevel1(bus1.getVoltageLevel().getId()) + .setVoltageLevel2(bus2.getVoltageLevel().getId()) + .setConnectableBus1(bus1.getId()) + .setConnectableBus2(bus2.getId()) + .setRatedU1(bus1.getVoltageLevel().getNominalV()) + .setRatedU2(bus2.getVoltageLevel().getNominalV()) + .setBus1(bus1.getId()) + .setBus2(bus2.getId()) + .add(); + } + + private static void createLine(Bus bus1, Bus bus2) { + String id = String.format("%s - %s", + bus1.getVoltageLevel().getSubstation().orElseThrow().getId(), + bus2.getVoltageLevel().getSubstation().orElseThrow().getId()); + bus1.getNetwork().newLine().setId(id) + .setR(0.0) + .setX(1.0) + .setG1(0.0) + .setB1(0.0) + .setG2(0.0) + .setB2(0.0) + .setVoltageLevel1(bus1.getVoltageLevel().getId()) + .setVoltageLevel2(bus2.getVoltageLevel().getId()) + .setConnectableBus1(bus1.getId()) + .setConnectableBus2(bus2.getId()) + .setBus1(bus1.getId()) + .setBus2(bus2.getId()) + .add(); + } +} diff --git a/src/test/java/com/powsybl/nad/layout/LayoutWithInitialPositionsTest.java b/src/test/java/com/powsybl/nad/layout/LayoutWithInitialPositionsTest.java new file mode 100644 index 00000000..4c363054 --- /dev/null +++ b/src/test/java/com/powsybl/nad/layout/LayoutWithInitialPositionsTest.java @@ -0,0 +1,223 @@ +/** + * 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.commons.PowsyblException; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.VoltageLevel; +import com.powsybl.nad.AbstractTest; +import com.powsybl.nad.NetworkAreaDiagram; +import com.powsybl.nad.build.iidm.VoltageLevelFilter; +import com.powsybl.nad.model.Graph; +import com.powsybl.nad.model.Point; +import com.powsybl.nad.svg.LabelProvider; +import com.powsybl.nad.svg.StyleProvider; +import com.powsybl.nad.svg.SvgParameters; +import com.powsybl.nad.svg.iidm.DefaultLabelProvider; +import com.powsybl.nad.svg.iidm.NominalVoltageStyleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Luma Zamarreno + */ +class LayoutWithInitialPositionsTest extends AbstractTest { + + @BeforeEach + public void setup() { + setLayoutParameters(new LayoutParameters()); + setSvgParameters(new SvgParameters() + .setInsertNameDesc(true) + .setSvgWidthAndHeightAdded(true) + .setFixedWidth(800)); + } + + @Override + protected StyleProvider getStyleProvider(Network network) { + return new NominalVoltageStyleProvider(network); + } + + @Override + protected LabelProvider getLabelProvider(Network network) { + return new DefaultLabelProvider(network, getSvgParameters()); + } + + @Test + void testDiamond() { + checkLayoutWithInitialPositions(LayoutNetworkFactory.createDiamond()); + } + + private void checkLayoutWithInitialPositions(Network network) { + Predicate filter = vl -> vl.getNominalV() >= 100; + + // Perform an initial layout with only a few voltage levels of the network + NetworkAreaDiagram initialDiagram = new NetworkAreaDiagram(network, filter); + Map initialPositions = layoutResult(initialDiagram); + + // Check initial points contains an entry for all voltage levels filtered + network.getVoltageLevelStream().filter(filter).forEach(vl -> assertTrue(initialPositions.containsKey(vl.getId()))); + // Check we have voltage levels in the network that are not filtered and thus will not have an initial positions + assertTrue(network.getVoltageLevelStream().anyMatch(filter.negate())); + network.getVoltageLevelStream().filter(filter.negate()).forEach(vl -> assertFalse(initialPositions.containsKey(vl.getId()))); + + checkAllInitialPositionsFixed(network, initialPositions); + checkOnlySomeInitialPositionsFixed(network, initialPositions); + } + + private void checkAllInitialPositionsFixed(Network network, Map initialPositions) { + // Perform a global layout with all the voltage levels in the network, + // giving fixed positions for some equipment + NetworkAreaDiagram completeNetworkDiagram = new NetworkAreaDiagram(network, VoltageLevelFilter.NO_FILTER); + Map allPositions = layoutResult(completeNetworkDiagram, initialPositions); + + // Check positions of initial layout have been preserved in global layout + for (Map.Entry l : initialPositions.entrySet()) { + String equipmentId = l.getKey(); + Point expected = l.getValue(); + Point actual = allPositions.get(equipmentId); + assertNotNull(actual); + assertEquals(expected.getX(), actual.getX()); + assertEquals(expected.getY(), actual.getY()); + } + } + + private void checkOnlySomeInitialPositionsFixed(Network network, Map initialPositions) { + // Perform a global layout with all the voltage levels in the network, + // giving initial positions for some equipment, + // and fixing the position for only some equipment + NetworkAreaDiagram completeNetworkDiagram = new NetworkAreaDiagram(network, VoltageLevelFilter.NO_FILTER); + // Only consider fixed the first one in the initial layout + Set fixedNodes = Set.of(initialPositions.keySet().iterator().next()); + Map allPositions = layoutResult(completeNetworkDiagram, initialPositions, fixedNodes); + + // Check positions of initial layout have been preserved in global layout + for (Map.Entry l : initialPositions.entrySet()) { + String equipmentId = l.getKey(); + Point expected = l.getValue(); + Point actual = allPositions.get(equipmentId); + assertNotNull(actual); + if (fixedNodes.contains(equipmentId)) { + assertEquals(expected.getX(), actual.getX()); + assertEquals(expected.getY(), actual.getY()); + } else { + // We expect that the nodes with initial position but that have not been fixed have been moved + assertTrue(expected.getX() != actual.getX() || expected.getY() != actual.getY()); + } + } + } + + private Map layoutResult(NetworkAreaDiagram nad) { + return layoutResult(nad, Collections.emptyMap(), Collections.emptySet(), Collections.emptyMap()); + } + + private Map layoutResult(NetworkAreaDiagram nad, Map initialNodePositions, Set nodesWithFixedPositions) { + return layoutResult(nad, initialNodePositions, nodesWithFixedPositions, Collections.emptyMap()); + } + + private Map layoutResult(NetworkAreaDiagram nad, Map fixedNodePositions) { + return layoutResult(nad, Collections.emptyMap(), Collections.emptySet(), fixedNodePositions); + } + + private Map layoutResult(NetworkAreaDiagram nad, + Map initialNodePositions, + Set nodesWithFixedPositions, + Map fixedNodePositions + ) { + LayoutFactory delegateLayoutFactory = new BasicForceLayoutFactory(); + PositionsLayoutFactory positionsLayoutFactory = new PositionsLayoutFactory( + delegateLayoutFactory, + initialNodePositions, + nodesWithFixedPositions, + fixedNodePositions); + StringWriter writer = new StringWriter(); + nad.draw(writer, + getSvgParameters(), + getLayoutParameters(), + getStyleProvider(nad.getNetwork()), + getLabelProvider(nad.getNetwork()), + positionsLayoutFactory); + return positionsLayoutFactory.getLayoutResult().positions; + } + + static class PositionsLayoutFactory implements LayoutFactory { + static class LayoutResult { + Map positions; + } + + private final LayoutFactory delegateLayoutFactory; + private final LayoutResult layoutResult = new LayoutResult(); + private final Map initialNodePositions; + private final Set nodesWithFixedPositions; + private final Map fixedNodePositions; + + PositionsLayoutFactory(LayoutFactory delegateLayoutFactory, + Map initialNodePositions, + Set nodesWithFixedPositions, + Map fixedNodePositions + ) { + this.delegateLayoutFactory = delegateLayoutFactory; + this.initialNodePositions = initialNodePositions; + this.nodesWithFixedPositions = nodesWithFixedPositions; + this.fixedNodePositions = fixedNodePositions; + } + + public LayoutResult getLayoutResult() { + return layoutResult; + } + + @Override + public Layout create() { + final Layout delegateLayout = delegateLayoutFactory.create(); + return new Layout() { + @Override + public void run(Graph graph, LayoutParameters layoutParameters) { + if (!initialNodePositions.isEmpty()) { + delegateLayout.setInitialNodePositions(initialNodePositions); + } + if (!nodesWithFixedPositions.isEmpty()) { + delegateLayout.setNodesWithFixedPosition(nodesWithFixedPositions); + } + // only if not empty, + // setting nodes with fixed node positions will invalidate previous nodes with fixed positions + if (!fixedNodePositions.isEmpty() && delegateLayout instanceof AbstractLayout) { + ((AbstractLayout) delegateLayout).setFixedNodePositions(fixedNodePositions); + } + delegateLayout.run(graph, layoutParameters); + layoutResult.positions = graph.getNodePositions(); + } + + @Override + public void setInitialNodePositions(Map initialNodePositions) { + throw new PowsyblException("not implemented"); + } + + @Override + public void setNodesWithFixedPosition(Set nodesWithFixedPosition) { + throw new PowsyblException("not implemented"); + } + + @Override + public Map getInitialNodePositions() { + throw new PowsyblException("not implemented"); + } + + @Override + public Set getNodesWithFixedPosition() { + throw new PowsyblException("not implemented"); + } + }; + } + } +}