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 index 110ff0ce7..83358e9eb 100644 --- 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 @@ -20,10 +20,7 @@ public abstract class AbstractLayout implements Layout { private Map initialNodePositions = Collections.emptyMap(); private Set nodesWithFixedPosition = Collections.emptySet(); - private Map textNodesWithFixedPosition = new HashMap<>(); - - record TextPosition(Point topLeftPosition, Point edgeConnection) { - } + private final Map textNodesWithFixedPosition = new HashMap<>(); @Override public void run(Graph graph, LayoutParameters layoutParameters) { diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/FixedLayoutFactory.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/FixedLayoutFactory.java index cf596d531..ff5040992 100644 --- a/network-area-diagram/src/main/java/com/powsybl/nad/layout/FixedLayoutFactory.java +++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/FixedLayoutFactory.java @@ -18,13 +18,23 @@ public class FixedLayoutFactory implements LayoutFactory { private final Map fixedPositions; private final LayoutFactory layoutFactory; + private final Map textNodesWithFixedPosition; public FixedLayoutFactory(Map fixedPositions) { this(fixedPositions, BasicFixedLayout::new); } public FixedLayoutFactory(Map fixedPositions, LayoutFactory layoutFactory) { + this(fixedPositions, Map.of(), layoutFactory); + } + + public FixedLayoutFactory(Map fixedPositions, Map textNodesWithFixedPosition) { + this(fixedPositions, textNodesWithFixedPosition, BasicFixedLayout::new); + } + + public FixedLayoutFactory(Map fixedPositions, Map textNodesWithFixedPosition, LayoutFactory layoutFactory) { this.fixedPositions = Objects.requireNonNull(fixedPositions); + this.textNodesWithFixedPosition = Objects.requireNonNull(textNodesWithFixedPosition); this.layoutFactory = Objects.requireNonNull(layoutFactory); } @@ -33,6 +43,7 @@ public Layout create() { Layout layout = layoutFactory.create(); layout.setInitialNodePositions(fixedPositions); layout.setNodesWithFixedPosition(fixedPositions.keySet()); + textNodesWithFixedPosition.forEach((k, v) -> layout.setTextNodeFixedPosition(k, v.topLeftPosition(), v.edgeConnection())); return layout; } } diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactoryUtils.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactoryUtils.java new file mode 100644 index 000000000..fcb931df3 --- /dev/null +++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/LayoutFactoryUtils.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.nad.layout; + +import com.powsybl.nad.svg.metadata.DiagramMetadata; + +import java.io.InputStream; +import java.io.Reader; +import java.nio.file.Path; + +/** + * @author Massimo Ferraro {@literal } + */ +public final class LayoutFactoryUtils { + + private LayoutFactoryUtils() { + } + + private static FixedLayoutFactory createLayoutFactory(LayoutFactory layoutFactory, DiagramMetadata diagramMetadata) { + return new FixedLayoutFactory(diagramMetadata.getFixedPositions(), diagramMetadata.getFixedTextPositions(), layoutFactory); + } + + private static FixedLayoutFactory createLayoutFactory(DiagramMetadata diagramMetadata) { + return new FixedLayoutFactory(diagramMetadata.getFixedPositions(), diagramMetadata.getFixedTextPositions()); + } + + public static FixedLayoutFactory create(InputStream metadataIs, LayoutFactory layoutFactory) { + return createLayoutFactory(layoutFactory, DiagramMetadata.parseJson(metadataIs)); + } + + public static FixedLayoutFactory create(InputStream metadataIs) { + return createLayoutFactory(DiagramMetadata.parseJson(metadataIs)); + } + + public static FixedLayoutFactory create(Path metadataFile, LayoutFactory layoutFactory) { + return createLayoutFactory(layoutFactory, DiagramMetadata.parseJson(metadataFile)); + } + + public static FixedLayoutFactory create(Path metadataFile) { + return createLayoutFactory(DiagramMetadata.parseJson(metadataFile)); + } + + public static FixedLayoutFactory create(Reader metadataReader, LayoutFactory layoutFactory) { + return createLayoutFactory(layoutFactory, DiagramMetadata.parseJson(metadataReader)); + } + + public static FixedLayoutFactory create(Reader metadataReader) { + return createLayoutFactory(DiagramMetadata.parseJson(metadataReader)); + } +} diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/layout/TextPosition.java b/network-area-diagram/src/main/java/com/powsybl/nad/layout/TextPosition.java new file mode 100644 index 000000000..7ce4b5837 --- /dev/null +++ b/network-area-diagram/src/main/java/com/powsybl/nad/layout/TextPosition.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2024, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.nad.layout; + +import com.powsybl.nad.model.Point; + +/** + * @author Florian Dupuy {@literal } + */ +public record TextPosition(Point topLeftPosition, Point edgeConnection) { +} diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/DiagramMetadata.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/DiagramMetadata.java index 7d8bb51ff..ca6a0e1ac 100644 --- a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/DiagramMetadata.java +++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/DiagramMetadata.java @@ -7,6 +7,18 @@ */ package com.powsybl.nad.svg.metadata; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.json.JsonUtil; +import com.powsybl.diagram.metadata.AbstractMetadata; +import com.powsybl.nad.layout.LayoutParameters; +import com.powsybl.nad.layout.TextPosition; +import com.powsybl.nad.model.Graph; +import com.powsybl.nad.model.Point; +import com.powsybl.nad.svg.SvgParameters; + import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -15,16 +27,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.powsybl.commons.json.JsonUtil; -import com.powsybl.diagram.metadata.AbstractMetadata; -import com.powsybl.nad.layout.LayoutParameters; -import com.powsybl.nad.model.Graph; -import com.powsybl.nad.svg.SvgParameters; +import java.util.stream.Collectors; /** * @author Thomas Adam {@literal } @@ -145,6 +150,7 @@ private double round(double number) { } public static DiagramMetadata parseJson(Path file) { + Objects.requireNonNull(file); try (Reader reader = Files.newBufferedReader(file)) { return parseJson(reader); } catch (IOException e) { @@ -171,4 +177,14 @@ public static DiagramMetadata parseJson(Reader reader) { throw new UncheckedIOException(e); } } + + @JsonIgnore + public Map getFixedPositions() { + return nodesMetadata.stream().collect(Collectors.toMap(NodeMetadata::getEquipmentId, NodeMetadata::getPosition)); + } + + @JsonIgnore + public Map getFixedTextPositions() { + return textNodesMetadata.stream().collect(Collectors.toMap(TextNodeMetadata::getEquipmentId, TextNodeMetadata::getTextPosition)); + } } diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/NodeMetadata.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/NodeMetadata.java index ccdea86b9..43670e89e 100644 --- a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/NodeMetadata.java +++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/NodeMetadata.java @@ -7,8 +7,10 @@ */ package com.powsybl.nad.svg.metadata; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.powsybl.nad.model.Point; /** * @author Luma ZamarreƱo {@literal } @@ -43,4 +45,9 @@ public double getY() { public boolean isFictitious() { return fictitious; } + + @JsonIgnore + public Point getPosition() { + return new Point(x, y); + } } diff --git a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/TextNodeMetadata.java b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/TextNodeMetadata.java index dc317f83a..9c0cfa573 100644 --- a/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/TextNodeMetadata.java +++ b/network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/TextNodeMetadata.java @@ -7,8 +7,11 @@ */ package com.powsybl.nad.svg.metadata; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.powsybl.nad.layout.TextPosition; +import com.powsybl.nad.model.Point; /** * @author Massimo Ferraro {@literal } @@ -61,4 +64,9 @@ public double getConnectionShiftX() { public double getConnectionShiftY() { return connectionShiftY; } + + @JsonIgnore + public TextPosition getTextPosition() { + return new TextPosition(new Point(shiftX, shiftY), new Point(connectionShiftX, connectionShiftY)); + } } diff --git a/network-area-diagram/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java b/network-area-diagram/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java index af44bb4bb..ce05e9d91 100644 --- a/network-area-diagram/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java +++ b/network-area-diagram/src/test/java/com/powsybl/nad/layout/FixedLayoutTest.java @@ -13,8 +13,16 @@ import com.powsybl.nad.build.iidm.VoltageLevelFilter; import com.powsybl.nad.model.Graph; import com.powsybl.nad.model.Point; + import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -98,4 +106,85 @@ void testBasicFixedLayoutFallback() { assertEquals(0, actual.get("VL8").getX()); assertEquals(0, actual.get("VL8").getY()); } + + @Test + void testMetadataUtils() throws URISyntaxException, IOException { + Network network = Networks.createTwoVoltageLevels(); + Path metadataFile = Paths.get(getClass().getResource("/two-voltage-levels_metadata.json").toURI()); + + Layout layout = new FixedLayoutFactory(new HashMap<>()).create(); + testEmptyLayout(layout, network); + + layout = LayoutFactoryUtils.create(metadataFile).create(); + testMetadataLayout(layout, network); + layout = LayoutFactoryUtils.create(metadataFile, BasicFixedLayout::new).create(); + testMetadataLayout(layout, network); + + try (InputStream metadataIS = Files.newInputStream(metadataFile)) { + layout = LayoutFactoryUtils.create(metadataIS).create(); + testMetadataLayout(layout, network); + } + try (InputStream metadataIS = Files.newInputStream(metadataFile)) { + layout = LayoutFactoryUtils.create(metadataIS, BasicFixedLayout::new).create(); + testMetadataLayout(layout, network); + } + + try (Reader metadataReader = Files.newBufferedReader(metadataFile)) { + layout = LayoutFactoryUtils.create(metadataReader).create(); + testMetadataLayout(layout, network); + } + try (Reader metadataReader = Files.newBufferedReader(metadataFile)) { + layout = LayoutFactoryUtils.create(metadataReader, BasicFixedLayout::new).create(); + testMetadataLayout(layout, network); + } + } + + void testEmptyLayout(Layout layout, Network network) { + Graph graph = new NetworkGraphBuilder(network, VoltageLevelFilter.NO_FILTER).buildGraph(); + layout.run(graph, new LayoutParameters()); + Map nodePositions = graph.getNodePositions(); + checkNodePosition(nodePositions.get("dl1"), 0, 0); + checkNodePosition(nodePositions.get("vl1"), 0, 0); + checkNodePosition(nodePositions.get("vl2"), 0, 0); + Map textNodesPositions = getTextNodesPositions(graph); + checkNodeShift(nodePositions.get("vl1"), textNodesPositions.get("vl1").topLeftPosition(), 100, -40); + checkNodeShift(nodePositions.get("vl1"), textNodesPositions.get("vl1").edgeConnection(), 100, -15); + checkNodeShift(nodePositions.get("vl2"), textNodesPositions.get("vl2").topLeftPosition(), 100, -40); + checkNodeShift(nodePositions.get("vl2"), textNodesPositions.get("vl2").edgeConnection(), 100, -15); + } + + void testMetadataLayout(Layout layout, Network network) { + Graph graph = new NetworkGraphBuilder(network, VoltageLevelFilter.NO_FILTER).buildGraph(); + layout.run(graph, new LayoutParameters()); + Map nodePositions = graph.getNodePositions(); + checkNodePosition(nodePositions.get("dl1"), -49.12, 317.14); + checkNodePosition(nodePositions.get("vl1"), -56.06, -318.7); + checkNodePosition(nodePositions.get("vl2"), -230.42, 1.18); + Map textNodesPositions = getTextNodesPositions(graph); + checkNodeShift(nodePositions.get("vl1"), textNodesPositions.get("vl1").topLeftPosition(), 80, -30); + checkNodeShift(nodePositions.get("vl1"), textNodesPositions.get("vl1").edgeConnection(), 80, -5); + checkNodeShift(nodePositions.get("vl2"), textNodesPositions.get("vl2").topLeftPosition(), 80, -30); + checkNodeShift(nodePositions.get("vl2"), textNodesPositions.get("vl2").edgeConnection(), 80, -5); + } + + Map getTextNodesPositions(Graph graph) { + Map textNodesPositions = new HashMap<>(); + graph.getTextEdgesMap() + .values() + .forEach(nodePair -> textNodesPositions.put(nodePair.getFirst().getEquipmentId(), + new TextPosition(nodePair.getSecond().getPosition(), + nodePair.getSecond().getEdgeConnection()))); + return textNodesPositions; + } + + void checkNodePosition(Point point, double x, double y) { + assertEquals(x, point.getX()); + assertEquals(y, point.getY()); + } + + void checkNodeShift(Point point, Point shiftedPoint, double shiftX, double shiftY) { + Point expectedPoint = point.shift(shiftX, shiftY); + assertEquals(expectedPoint.getX(), shiftedPoint.getX()); + assertEquals(expectedPoint.getY(), shiftedPoint.getY()); + } } diff --git a/network-area-diagram/src/test/resources/two-voltage-levels_metadata.json b/network-area-diagram/src/test/resources/two-voltage-levels_metadata.json new file mode 100644 index 000000000..70c80f935 --- /dev/null +++ b/network-area-diagram/src/test/resources/two-voltage-levels_metadata.json @@ -0,0 +1,127 @@ +{ + "layoutParameters" : { + "textNodesForceLayout" : false, + "springRepulsionFactorForceLayout" : 0.0, + "textNodeFixedShift" : { + "x" : 100.0, + "y" : -40.0 + }, + "maxSteps" : 1000, + "textNodeEdgeConnectionYShift" : 25.0 + }, + "svgParameters" : { + "diagramPadding" : { + "left" : 200.0, + "top" : 200.0, + "right" : 200.0, + "bottom" : 200.0 + }, + "insertNameDesc" : false, + "svgWidthAndHeightAdded" : false, + "cssLocation" : "INSERTED_IN_SVG", + "sizeConstraint" : "FIXED_SCALE", + "fixedWidth" : -1, + "fixedHeight" : -1, + "fixedScale" : 0.2, + "arrowShift" : 30.0, + "arrowLabelShift" : 19.0, + "converterStationWidth" : 70.0, + "voltageLevelCircleRadius" : 30.0, + "fictitiousVoltageLevelCircleRadius" : 15.0, + "transformerCircleRadius" : 20.0, + "nodeHollowWidth" : 15.0, + "edgesForkLength" : 80.0, + "edgesForkAperture" : 60.0, + "edgeStartShift" : 0.0, + "unknownBusNodeExtraRadius" : 10.0, + "loopDistance" : 120.0, + "loopEdgesAperture" : 60.0, + "loopControlDistance" : 40.0, + "edgeInfoAlongEdge" : true, + "edgeNameDisplayed" : false, + "interAnnulusSpace" : 5.0, + "svgPrefix" : "", + "idDisplayed" : false, + "substationDescriptionDisplayed" : false, + "arrowHeight" : 10.0, + "busLegend" : true, + "voltageLevelDetails" : false, + "languageTag" : "en", + "voltageValuePrecision" : 1, + "powerValuePrecision" : 0, + "angleValuePrecision" : 1, + "currentValuePrecision" : 0, + "edgeInfoDisplayed" : "ACTIVE_POWER", + "pstArrowHeadSize" : 8.0, + "undefinedValueSymbol" : "" + }, + "busNodes" : [ { + "svgId" : "1", + "equipmentId" : "vl1_0", + "nbNeighbours" : 0, + "index" : 0, + "vlNode" : "0" + }, { + "svgId" : "3", + "equipmentId" : "vl2_0", + "nbNeighbours" : 0, + "index" : 0, + "vlNode" : "2" + }, { + "svgId" : "6", + "equipmentId" : "dl1", + "nbNeighbours" : 0, + "index" : 0, + "vlNode" : "5" + } ], + "nodes" : [ { + "svgId" : "0", + "equipmentId" : "vl1", + "x" : -56.06, + "y" : -318.7 + }, { + "svgId" : "2", + "equipmentId" : "vl2", + "x" : -230.42, + "y" : 1.18 + }, { + "svgId" : "5", + "equipmentId" : "dl1", + "x" : -49.12, + "y" : 317.14 + } ], + "edges" : [ { + "svgId" : "4", + "equipmentId" : "l1", + "node1" : "0", + "node2" : "2", + "busNode1" : "1", + "busNode2" : "3", + "type" : "LineEdge" + }, { + "svgId" : "7", + "equipmentId" : "dl1", + "node1" : "2", + "node2" : "5", + "busNode1" : "3", + "busNode2" : "6", + "type" : "DanglingLineEdge" + } ], + "textNodes" : [ { + "svgId" : "0-textnode", + "equipmentId" : "vl1", + "vlNode" : "0", + "shiftX" : 80.0, + "shiftY" : -30.0, + "connectionShiftX" : 80.0, + "connectionShiftY" : -5.0 + }, { + "svgId" : "2-textnode", + "equipmentId" : "vl2", + "vlNode" : "2", + "shiftX" : 80.0, + "shiftY" : -30.0, + "connectionShiftX" : 80.0, + "connectionShiftY" : -5.0 + } ] +}