Skip to content

Commit 9fd49fa

Browse files
rpa-dacostarolnico
andauthored
[NAD] Display two arrows for each side of a line (#783)
* display two arrows if label parameter provided * review: do not use list to manage double arrows * review: add svgParameters to be available in metadata * review * fixed empty spaces --------- Signed-off-by: Remi DA COSTA <rpa.dacosta@soprasteria.com> Co-authored-by: Nicolas Rol <nicolas.rol@rte-france.com>
1 parent cb2280b commit 9fd49fa

26 files changed

Lines changed: 1600 additions & 55 deletions

network-area-diagram/src/main/java/com/powsybl/nad/svg/EdgeInfo.java

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,22 @@ public class EdgeInfo {
2424
public static final String NAME = "Name";
2525
public static final String VALUE_PERMANENT_LIMIT_PERCENTAGE = "PermanentLimitPercentage";
2626

27-
private final String infoTypeA;
28-
private final String infoTypeB;
29-
private final Direction arrowDirection;
30-
private final String labelA;
31-
private final String labelB;
27+
private final EdgeInfoData edgeInfoDataA;
28+
private final EdgeInfoData edgeInfoDataB;
3229
private final String componentType;
3330

31+
public EdgeInfo(String infoTypeA, String infoTypeB, Direction arrowDirectionA, Direction arrowDirectionB, String labelA, String labelB, String componentType) {
32+
edgeInfoDataA = new EdgeInfoData(infoTypeA, labelA, arrowDirectionA);
33+
edgeInfoDataB = new EdgeInfoData(infoTypeB, labelB, arrowDirectionB);
34+
this.componentType = componentType;
35+
}
36+
3437
public EdgeInfo(String infoTypeA, String infoTypeB, Direction arrowDirection, String labelA, String labelB) {
35-
this(infoTypeA, infoTypeB, arrowDirection, labelA, labelB, null);
38+
this(infoTypeA, infoTypeB, arrowDirection, null, labelA, labelB, null);
3639
}
3740

3841
public EdgeInfo(String infoTypeA, String infoTypeB, Direction arrowDirection, String labelA, String labelB, String componentType) {
39-
this.infoTypeB = infoTypeB;
40-
this.infoTypeA = infoTypeA;
41-
this.arrowDirection = arrowDirection;
42-
this.labelA = labelA;
43-
this.labelB = labelB;
44-
this.componentType = componentType;
42+
this(infoTypeA, infoTypeB, arrowDirection, null, labelA, labelB, componentType);
4543
}
4644

4745
public EdgeInfo(String infoTypeA, String infoTypeB, double referenceValue, String labelA, String labelB) {
@@ -52,6 +50,10 @@ public EdgeInfo(String infoTypeA, String infoTypeB, double referenceValue, Strin
5250
this(infoTypeA, infoTypeB, getArrowDirection(referenceValue), labelA, labelB, componentType);
5351
}
5452

53+
public EdgeInfo(String infoTypeA, String infoTypeB, double referenceValueA, double referenceValueB, String labelA, String labelB) {
54+
this(infoTypeA, infoTypeB, getArrowDirection(referenceValueA), getArrowDirection(referenceValueB), labelA, labelB, null);
55+
}
56+
5557
private static Direction getArrowDirection(double value) {
5658
if (Double.isNaN(value)) {
5759
return null;
@@ -68,23 +70,31 @@ public String getInfoType() {
6870
}
6971

7072
public String getInfoTypeB() {
71-
return infoTypeB;
73+
return edgeInfoDataB.infoType();
7274
}
7375

7476
public String getInfoTypeA() {
75-
return infoTypeA;
77+
return edgeInfoDataA.infoType();
7678
}
7779

7880
public Optional<Direction> getDirection() {
79-
return Optional.ofNullable(arrowDirection);
81+
return Optional.ofNullable(edgeInfoDataA.arrowDirection() == null ? edgeInfoDataB.arrowDirection() : edgeInfoDataA.arrowDirection());
82+
}
83+
84+
public Optional<Direction> getDirectionA() {
85+
return Optional.ofNullable(edgeInfoDataA.arrowDirection());
86+
}
87+
88+
public Optional<Direction> getDirectionB() {
89+
return Optional.ofNullable(edgeInfoDataB.arrowDirection());
8090
}
8191

8292
public Optional<String> getLabelA() {
83-
return Optional.ofNullable(labelA);
93+
return Optional.ofNullable(edgeInfoDataA.label());
8494
}
8595

8696
public Optional<String> getLabelB() {
87-
return Optional.ofNullable(labelB);
97+
return Optional.ofNullable(edgeInfoDataB.label());
8898
}
8999

90100
public Optional<String> getComponentType() {
@@ -93,10 +103,10 @@ public Optional<String> getComponentType() {
93103

94104
/**
95105
* Returns the main info type.
96-
* @return the main info type. By default, the info type of the side 2.
106+
* @return the main info type. By default, the info type of the side B.
97107
*/
98108
public String getMainInfoType() {
99-
return infoTypeB != null ? infoTypeB : infoTypeA;
109+
return edgeInfoDataB.infoType() != null ? edgeInfoDataB.infoType() : edgeInfoDataA.infoType();
100110
}
101111

102112
public enum Direction {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (c) 2026, RTE (https://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
* SPDX-License-Identifier: MPL-2.0
7+
*/
8+
package com.powsybl.nad.svg;
9+
10+
/**
11+
* @author Nicolas Rol {@literal <nicolas.rol at rte-france.com>}
12+
*/
13+
public record EdgeInfoData(String infoType, String label, EdgeInfo.Direction arrowDirection) {
14+
}

network-area-diagram/src/main/java/com/powsybl/nad/svg/LabelProviderParameters.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class LabelProviderParameters {
99
private boolean substationDescriptionDisplayed = false;
1010
private boolean idDisplayed = false;
1111
private boolean voltageLevelDetails = false;
12+
private boolean doubleArrowsDisplayed = false;
1213

1314
public boolean isBusLegend() {
1415
return isBusLegend;
@@ -45,4 +46,13 @@ public LabelProviderParameters setVoltageLevelDetails(boolean voltageLevelDetail
4546
this.voltageLevelDetails = voltageLevelDetails;
4647
return this;
4748
}
49+
50+
public boolean isDoubleArrowsDisplayed() {
51+
return doubleArrowsDisplayed;
52+
}
53+
54+
public LabelProviderParameters setDoubleArrowsDisplayed(boolean doubleArrowsDisplayed) {
55+
this.doubleArrowsDisplayed = doubleArrowsDisplayed;
56+
return this;
57+
}
4858
}

network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgParameters.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class SvgParameters {
5959
private double injectionCircleRadius = 25;
6060
private boolean voltageLevelLegendsIncluded = true;
6161
private boolean edgeInfosIncluded = true;
62+
private double doubleArrowShiftFactorArrows = 1.5;
63+
private double doubleArrowShiftFactorText = 1.8;
6264

6365
public enum CssLocation {
6466
INSERTED_IN_SVG, EXTERNAL_IMPORTED, EXTERNAL_NO_IMPORT
@@ -113,6 +115,8 @@ public SvgParameters(SvgParameters other) {
113115
this.injectionCircleRadius = other.injectionCircleRadius;
114116
this.voltageLevelLegendsIncluded = other.voltageLevelLegendsIncluded;
115117
this.edgeInfosIncluded = other.edgeInfosIncluded;
118+
this.doubleArrowShiftFactorArrows = other.doubleArrowShiftFactorArrows;
119+
this.doubleArrowShiftFactorText = other.doubleArrowShiftFactorText;
116120
}
117121

118122
public Padding getDiagramPadding() {
@@ -509,4 +513,22 @@ public SvgParameters setEdgeInfosIncluded(boolean edgeInfosIncluded) {
509513
this.edgeInfosIncluded = edgeInfosIncluded;
510514
return this;
511515
}
516+
517+
public SvgParameters setDoubleArrowShiftFactorArrows(double doubleArrowShiftFactorArrows) {
518+
this.doubleArrowShiftFactorArrows = doubleArrowShiftFactorArrows;
519+
return this;
520+
}
521+
522+
public double getDoubleArrowShiftFactorArrows() {
523+
return doubleArrowShiftFactorArrows;
524+
}
525+
526+
public SvgParameters setDoubleArrowShiftFactorText(double doubleArrowShiftFactorText) {
527+
this.doubleArrowShiftFactorText = doubleArrowShiftFactorText;
528+
return this;
529+
}
530+
531+
public double getDoubleArrowShiftFactorText() {
532+
return doubleArrowShiftFactorText;
533+
}
512534
}

network-area-diagram/src/main/java/com/powsybl/nad/svg/SvgWriter.java

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -680,15 +680,17 @@ private void drawEdgeInfo(XMLStreamWriter writer, SvgEdgeInfo svgEdgeInfo, Point
680680
if (componentType.isPresent()) {
681681
this.drawComponentOnBranchEdgeMiddle(writer, componentType.get());
682682
} else {
683-
drawArrow(writer, edgeInfo, edgeAngle);
683+
drawArrows(writer, edgeInfo, edgeAngle);
684684
}
685-
Optional<String> label2 = edgeInfo.getLabelB();
686-
if (label2.isPresent()) {
687-
drawLabel(writer, label2.get(), edgeAngle, true, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeB()));
685+
686+
double factor = edgeInfo.getDirectionA().isEmpty() || edgeInfo.getDirectionB().isEmpty() ? 1.0 : svgParameters.getDoubleArrowShiftFactorText();
687+
Optional<String> labelB = edgeInfo.getLabelB();
688+
if (labelB.isPresent()) {
689+
drawLabel(writer, labelB.get(), edgeAngle, true, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeB()), factor);
688690
}
689-
Optional<String> label1 = edgeInfo.getLabelA();
690-
if (label1.isPresent()) {
691-
drawLabel(writer, label1.get(), edgeAngle, false, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeA()));
691+
Optional<String> labelA = edgeInfo.getLabelA();
692+
if (labelA.isPresent()) {
693+
drawLabel(writer, labelA.get(), edgeAngle, false, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeA()), factor);
692694
}
693695

694696
writer.writeEndElement();
@@ -698,42 +700,65 @@ private boolean checkIfEdgeInfoIsEmpty(EdgeInfo edgeInfo) {
698700
return edgeInfo.getLabelB().isEmpty() && edgeInfo.getLabelA().isEmpty() && edgeInfo.getDirection().isEmpty();
699701
}
700702

701-
private void drawArrow(XMLStreamWriter writer, EdgeInfo edgeInfo, double edgeAngle) throws XMLStreamException {
702-
var direction = edgeInfo.getDirection();
703+
private void drawArrows(XMLStreamWriter writer, EdgeInfo edgeInfo, double edgeAngle) throws XMLStreamException {
704+
Optional<EdgeInfo.Direction> direction = edgeInfo.getDirection();
703705
if (direction.isPresent()) {
704-
double rotationAngle = edgeAngle + (edgeAngle > Math.PI / 2 ? -3 * Math.PI / 2 : Math.PI / 2);
705-
writer.writeEmptyElement(PATH_ELEMENT_NAME);
706-
writer.writeAttribute(TRANSFORM_ATTRIBUTE, getRotateString(rotationAngle));
707-
if (direction.get() == EdgeInfo.Direction.IN) {
708-
writeStyleClasses(writer, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getMainInfoType()), StyleProvider.ARROW_IN_CLASS);
709-
writer.writeAttribute(PATH_D_ATTRIBUTE, svgParameters.getArrowPathIn());
710-
} else if (direction.get() == EdgeInfo.Direction.OUT) {
711-
writeStyleClasses(writer, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getMainInfoType()), StyleProvider.ARROW_OUT_CLASS);
712-
writer.writeAttribute(PATH_D_ATTRIBUTE, svgParameters.getArrowPathOut());
706+
Optional<EdgeInfo.Direction> directionA = edgeInfo.getDirectionA();
707+
Optional<EdgeInfo.Direction> directionB = edgeInfo.getDirectionB();
708+
709+
if (directionA.isEmpty() || directionB.isEmpty()) {
710+
drawArrow(writer, edgeAngle, direction.get(), null, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getMainInfoType()));
711+
} else {
712+
drawArrow(writer, edgeAngle, directionB.get(), true, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeB()));
713+
drawArrow(writer, edgeAngle, directionA.get(), false, styleProvider.getEdgeInfoStyleClasses(edgeInfo.getInfoTypeA()));
713714
}
714715
}
715716
}
716717

717-
private void drawLabel(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes) throws XMLStreamException {
718+
private void drawArrow(XMLStreamWriter writer, double edgeAngle, EdgeInfo.Direction direction, Boolean externalLabel, List<String> classes) throws XMLStreamException {
719+
double rotationAngle = edgeAngle + (edgeAngle > Math.PI / 2 ? -3 * Math.PI / 2 : Math.PI / 2);
720+
writer.writeEmptyElement(PATH_ELEMENT_NAME);
721+
722+
// Build transform string with both rotation and translation if needed
723+
String transformValue;
724+
if (externalLabel != null) {
725+
// Invert the sign because after rotation, the coordinate systems are inverted
726+
double shift = svgParameters.getArrowLabelShift() / svgParameters.getDoubleArrowShiftFactorArrows() * (externalLabel ? -1 : 1);
727+
transformValue = getRotateString(rotationAngle) + " " + getTranslateString(0, shift);
728+
} else {
729+
transformValue = getRotateString(rotationAngle);
730+
}
731+
writer.writeAttribute(TRANSFORM_ATTRIBUTE, transformValue);
732+
733+
if (direction == EdgeInfo.Direction.IN) {
734+
writeStyleClasses(writer, classes, StyleProvider.ARROW_IN_CLASS);
735+
writer.writeAttribute(PATH_D_ATTRIBUTE, svgParameters.getArrowPathIn());
736+
} else if (direction == EdgeInfo.Direction.OUT) {
737+
writeStyleClasses(writer, classes, StyleProvider.ARROW_OUT_CLASS);
738+
writer.writeAttribute(PATH_D_ATTRIBUTE, svgParameters.getArrowPathOut());
739+
}
740+
}
741+
742+
private void drawLabel(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes, double factor) throws XMLStreamException {
718743
if (svgParameters.isEdgeInfoAlongEdge()) {
719-
drawLabelAlongEdge(writer, label, edgeAngle, externalLabel, classes);
744+
drawLabelAlongEdge(writer, label, edgeAngle, externalLabel, classes, factor);
720745
} else {
721-
drawLabelPerpendicularToEdge(writer, label, edgeAngle, externalLabel, classes);
746+
drawLabelPerpendicularToEdge(writer, label, edgeAngle, externalLabel, classes, factor);
722747
}
723748
}
724749

725-
private void drawLabelAlongEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes) throws XMLStreamException {
750+
private void drawLabelAlongEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes, double factor) throws XMLStreamException {
726751
boolean textFlipped = Math.cos(edgeAngle) < 0;
727752
String style = externalLabel == textFlipped ? "text-anchor:end" : null;
728753
double textAngle = textFlipped ? edgeAngle - Math.PI : edgeAngle;
729-
double shift = svgParameters.getArrowLabelShift() * (externalLabel ? 1 : -1);
754+
double shift = svgParameters.getArrowLabelShift() * factor * (externalLabel ? 1 : -1);
730755
drawLabel(writer, label, textFlipped ? -shift : shift, style, textAngle, X_ATTRIBUTE, classes);
731756
}
732757

733-
private void drawLabelPerpendicularToEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes) throws XMLStreamException {
758+
private void drawLabelPerpendicularToEdge(XMLStreamWriter writer, String label, double edgeAngle, boolean externalLabel, List<String> classes, double factor) throws XMLStreamException {
734759
boolean textFlipped = Math.sin(edgeAngle) > 0;
735760
double textAngle = textFlipped ? -Math.PI / 2 + edgeAngle : Math.PI / 2 + edgeAngle;
736-
double shift = svgParameters.getArrowLabelShift();
761+
double shift = svgParameters.getArrowLabelShift() * factor;
737762
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)
738763
drawLabel(writer, label, shiftAdjusted, TEXT_ANCHOR_MIDDLE, textAngle, Y_ATTRIBUTE, classes);
739764
}

network-area-diagram/src/main/java/com/powsybl/nad/svg/iidm/DefaultLabelProvider.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,23 @@ private Optional<EdgeInfo> getEdgeInfo(Terminal terminal, EdgeInfoEnum infoEnum1
129129
}
130130
Optional<String> optionalValue1 = getDisplayedValue(terminal, infoEnum1);
131131
Optional<String> optionalValue2 = getDisplayedValue(terminal, infoEnum2);
132-
double referenceValue = getReferenceValue(terminal, infoEnum2).orElse(getReferenceValue(terminal, infoEnum1).orElse(Double.NaN));
132+
133133
if (optionalValue1.isEmpty() && optionalValue2.isEmpty()) {
134134
return Optional.empty();
135135
}
136+
137+
if (parameters.isDoubleArrowsDisplayed()) {
138+
return Optional.of(new EdgeInfo(
139+
getDisplayedType(infoEnum1),
140+
getDisplayedType(infoEnum2),
141+
getReferenceValue(terminal, infoEnum1).orElse(Double.NaN),
142+
getReferenceValue(terminal, infoEnum2).orElse(Double.NaN),
143+
optionalValue1.orElse(null),
144+
optionalValue2.orElse(null)
145+
));
146+
}
147+
148+
double referenceValue = getReferenceValue(terminal, infoEnum2).orElseGet(() -> getReferenceValue(terminal, infoEnum1).orElse(Double.NaN));
136149
return Optional.of(new EdgeInfo(
137150
getDisplayedType(infoEnum1),
138151
getDisplayedType(infoEnum2),
@@ -291,6 +304,11 @@ public Builder setVoltageLevelDetails(boolean voltageLevelDetails) {
291304
return this;
292305
}
293306

307+
public Builder setDoubleArrowsDisplayed(boolean doubleArrowsDisplayed) {
308+
this.parameters.setDoubleArrowsDisplayed(doubleArrowsDisplayed);
309+
return this;
310+
}
311+
294312
public DefaultLabelProvider build(Network network, SvgParameters svgParameters) {
295313
return new DefaultLabelProvider(network,
296314
new EdgeInfoParameters(infoSideExternal, infoMiddleSide1, infoMiddleSide2, infoSideInternal),

network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/DiagramMetadata.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ private static EdgeInfoMetadata createEdgeInfoMetadata(SvgEdgeInfo svgEdgeInfo)
198198
edgeInfo.getInfoTypeA(),
199199
edgeInfo.getInfoTypeB(),
200200
edgeInfo.getDirection().map(Enum::name).orElse(null),
201+
edgeInfo.getDirectionA().map(Enum::name).orElse(null),
202+
edgeInfo.getDirectionB().map(Enum::name).orElse(null),
201203
edgeInfo.getLabelA().orElse(null),
202204
edgeInfo.getLabelB().orElse(null),
203205
edgeInfo.getComponentType().orElse(null));

network-area-diagram/src/main/java/com/powsybl/nad/svg/metadata/EdgeInfoMetadata.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class EdgeInfoMetadata {
2020
private final String infoTypeA;
2121
private final String infoTypeB;
2222
private final String direction;
23+
private final String directionA;
24+
private final String directionB;
2325
private final String labelA;
2426
private final String labelB;
2527
private final String componentType;
@@ -29,13 +31,25 @@ public EdgeInfoMetadata(@JsonProperty("svgId") String svgId,
2931
@JsonProperty("infoTypeA") String infoTypeA,
3032
@JsonProperty("infoTypeB") String infoTypeB,
3133
@JsonProperty("direction") String direction,
34+
@JsonProperty("directionA") String directionA,
35+
@JsonProperty("directionB") String directionB,
3236
@JsonProperty("labelA") String labelA,
3337
@JsonProperty("labelB") String labelB,
3438
@JsonProperty("componentType") String componentType) {
3539
this.svgId = svgId;
3640
this.infoTypeA = infoTypeA;
3741
this.infoTypeB = infoTypeB;
38-
this.direction = direction;
42+
if (directionA != null && directionB != null) {
43+
// Double arrows case
44+
this.direction = null;
45+
this.directionA = directionA;
46+
this.directionB = directionB;
47+
} else {
48+
// Single arrow case
49+
this.direction = direction;
50+
this.directionA = null;
51+
this.directionB = null;
52+
}
3953
this.labelA = labelA;
4054
this.labelB = labelB;
4155
this.componentType = componentType;
@@ -61,6 +75,16 @@ public String getDirection() {
6175
return direction;
6276
}
6377

78+
@JsonProperty("directionA")
79+
public String getDirectionA() {
80+
return directionA;
81+
}
82+
83+
@JsonProperty("directionB")
84+
public String getDirectionB() {
85+
return directionB;
86+
}
87+
6488
@JsonProperty("labelA")
6589
public String getLabelA() {
6690
return labelA;

0 commit comments

Comments
 (0)