Skip to content

Commit 38b0ca3

Browse files
authored
NAD custom style provider (#678)
Signed-off-by: Christian Biasuzzi <christian.biasuzzi@soft.it>
1 parent 9faa82a commit 38b0ca3

5 files changed

Lines changed: 748 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://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+
import com.powsybl.nad.model.BranchEdge;
11+
import com.powsybl.nad.model.BusNode;
12+
import com.powsybl.nad.model.Edge;
13+
import com.powsybl.nad.model.ThreeWtEdge;
14+
15+
import java.util.*;
16+
import java.util.stream.Collectors;
17+
import java.util.stream.Stream;
18+
19+
/**
20+
* Enables the customization of the style of NAD elements: Bus nodes, branch-edges and three-winding-transformers edges.
21+
*
22+
* <p>
23+
* NAD elements'style data is defined in the CustomStyleProvider constructor's map parameters.
24+
*
25+
* <p>
26+
* The busNodesStyles map is indexed by the bus ID and defines the style for the bus nodes.
27+
* In the map, a node style is declared in a BusNodeStyles record: fill, edge and edgeWidth are the fill color, the edge color and the edge size for the node, respectively.
28+
*
29+
* <p>
30+
* The edgesStyles map is indexed by the branch ID and defines the style for the edges.
31+
* In the map, the edge style is declared in a EdgeStyles record: edge1, width1 and dash1 are the color, the size and a dash pattern for the first half edge, respectively.
32+
* Edge2, width2 and dash2 are the color, the size and a dash pattern for the second half edge.
33+
*
34+
* <p>
35+
* The threeWtsStyles map is index by the three-winding-transformer ID and defines the style for the transformer’s legs.
36+
* In the map, the style is declared in a ThreeWtStyles record: edge1, width1, dash1, edge2, width2 and dash2, edge3, width23 and dash3,
37+
* are the color, the size and a dash pattern for the three legs of the transformer.
38+
*
39+
* <p>
40+
* Note that the edge size is a string, it can be specified in pixel (e.g, 4px).
41+
* A dash pattern is a string with a sequence of comma and/or white space separated lengths and percentages, that specify the lengths of alternating dashes and gaps in the edge.
42+
* Elements that do not have a style specified in the parameters will be displayed with a default style.
43+
*
44+
* @author Christian Biasuzzi {@literal <christian.biasuzzi at soft.it>}
45+
*/
46+
public class CustomStyleProvider extends AbstractStyleProvider {
47+
48+
final Map<String, BusNodeStyles> busNodesStyles;
49+
final Map<String, EdgeStyles> edgesStyles;
50+
final Map<String, ThreeWtStyles> threeWtsStyles;
51+
52+
public record BusNodeStyles(String fill, String edge, String edgeWidth) {
53+
}
54+
55+
public record EdgeStyles(String edge1, String width1, String dash1, String edge2, String width2,
56+
String dash2) {
57+
}
58+
59+
public record ThreeWtStyles(String edge1, String width1, String dash1, String edge2, String width2,
60+
String dash2, String edge3, String width3, String dash3) {
61+
}
62+
63+
private record EdgeStyle(String stroke, String strokeWidth, String dash) {
64+
}
65+
66+
public CustomStyleProvider(Map<String, BusNodeStyles> busNodesStyles, Map<String, EdgeStyles> edgesStyles,
67+
Map<String, ThreeWtStyles> threeWtsStyles) {
68+
this.busNodesStyles = Objects.requireNonNull(busNodesStyles);
69+
this.edgesStyles = Objects.requireNonNull(edgesStyles);
70+
this.threeWtsStyles = Objects.requireNonNull(threeWtsStyles);
71+
}
72+
73+
@Override
74+
public List<String> getCssFilenames() {
75+
return Collections.singletonList("customStyle.css");
76+
}
77+
78+
@Override
79+
public String getBusNodeStyle(BusNode busNode) {
80+
BusNodeStyles style = busNodesStyles.get(busNode.getEquipmentId());
81+
if (style != null) {
82+
List<String> parts = new ArrayList<>();
83+
if (style.fill() != null) {
84+
parts.add(String.format("background:%s; fill:%s;", style.fill(), style.fill()));
85+
}
86+
if (style.edge() != null) {
87+
parts.add(String.format("stroke:%s;", style.edge()));
88+
parts.add(String.format("border: solid %s %s;", style.edge(), style.edgeWidth() != null ? style.edgeWidth() : "1px"));
89+
}
90+
if (style.edgeWidth() != null) {
91+
parts.add(String.format("stroke-width:%s;", style.edgeWidth()));
92+
}
93+
return parts.isEmpty() ? null : String.join(" ", parts);
94+
}
95+
return null;
96+
}
97+
98+
private EdgeStyle getEdgeStyle(EdgeStyles styles, BranchEdge.Side side) {
99+
return (side == BranchEdge.Side.ONE)
100+
? new EdgeStyle(styles.edge1(), styles.width1(), styles.dash1())
101+
: new EdgeStyle(styles.edge2(), styles.width2(), styles.dash2());
102+
}
103+
104+
private String formatEdgeStyle(EdgeStyle lineStyle) {
105+
return Stream.of(
106+
Optional.ofNullable(lineStyle.stroke()).map(stroke -> String.format("stroke:%s;", stroke)).orElse(null),
107+
Optional.ofNullable(lineStyle.strokeWidth()).map(width -> String.format("stroke-width:%s;", width)).orElse(null),
108+
Optional.ofNullable(lineStyle.dash()).map(dash -> String.format("stroke-dasharray:%s;", dash)).orElse(null)
109+
)
110+
.filter(Objects::nonNull)
111+
.collect(Collectors.joining(" "));
112+
}
113+
114+
private EdgeStyle getThreeWtStyle(ThreeWtStyles styles, ThreeWtEdge.Side side) {
115+
return switch (side) {
116+
case ONE -> new EdgeStyle(styles.edge1(), styles.width1(), styles.dash1());
117+
case TWO -> new EdgeStyle(styles.edge2(), styles.width2(), styles.dash2());
118+
case THREE -> new EdgeStyle(styles.edge3(), styles.width3(), styles.dash3());
119+
};
120+
}
121+
122+
@Override
123+
public String getSideEdgeStyle(BranchEdge edge, BranchEdge.Side side) {
124+
return Optional.ofNullable(edgesStyles.get(edge.getEquipmentId()))
125+
.map(styles -> formatEdgeStyle(getEdgeStyle(styles, side)))
126+
.orElse(null);
127+
}
128+
129+
@Override
130+
public String getThreeWtEdgeStyle(ThreeWtEdge threeWtEdge) {
131+
ThreeWtEdge.Side side = threeWtEdge.getSide();
132+
return Optional.ofNullable(threeWtsStyles.get(threeWtEdge.getEquipmentId()))
133+
.map(styles -> formatEdgeStyle(getThreeWtStyle(styles, side)))
134+
.orElse(null);
135+
}
136+
137+
@Override
138+
public List<String> getEdgeInfoStyleClasses(EdgeInfo info) {
139+
List<String> styles = new LinkedList<>();
140+
info.getDirection().ifPresent(direction -> styles.add(
141+
CLASSES_PREFIX + (direction == EdgeInfo.Direction.OUT ? "state-out" : "state-in")));
142+
return styles;
143+
}
144+
145+
@Override
146+
protected boolean isDisconnected(ThreeWtEdge threeWtEdge) {
147+
return false;
148+
}
149+
150+
@Override
151+
protected boolean isDisconnected(BranchEdge branchEdge) {
152+
return false;
153+
}
154+
155+
@Override
156+
protected boolean isDisconnected(BranchEdge edge, BranchEdge.Side side) {
157+
return false;
158+
}
159+
160+
@Override
161+
protected Optional<String> getBaseVoltageStyle(Edge edge) {
162+
return Optional.empty();
163+
}
164+
165+
@Override
166+
protected Optional<String> getBaseVoltageStyle(BranchEdge edge, BranchEdge.Side side) {
167+
return Optional.empty();
168+
}
169+
170+
@Override
171+
protected Optional<String> getBaseVoltageStyle(ThreeWtEdge threeWtEdge) {
172+
return Optional.empty();
173+
}
174+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.nad-branch-edges .nad-edge-path, .nad-3wt-edges .nad-edge-path {stroke: grey; stroke-width: 5; fill: none}
2+
.nad-branch-edges .nad-winding, .nad-3wt-nodes .nad-winding {stroke: grey; stroke-width: 5; fill: none}
3+
.nad-text-edges {stroke: black; stroke-width: 3; stroke-dasharray: 6,7}
4+
.nad-vl-nodes .nad-busnode {fill: lightgrey}
5+
.nad-vl-nodes circle.nad-unknown-busnode {stroke: #808080; stroke-width: 5; stroke-dasharray: 5,5; fill: none}
6+
.nad-hvdc-edge polyline.nad-hvdc {stroke: grey; stroke-width: 40}
7+
.nad-branch-edges .nad-tie-line-edge .nad-edge-path {stroke-width: 7}
8+
.nad-pst-arrow {stroke: #6a6a6a; stroke-width: 4; stroke-linecap: round; fill: none}
9+
path.nad-arrow-out:not(.nad-state-out .nad-arrow-out) {visibility: hidden}
10+
path.nad-arrow-in:not(.nad-state-in .nad-arrow-in) {visibility: hidden}
11+
.nad-text-background {flood-color: #90a4aeaa}
12+
.nad-text-nodes {font: 25px serif; fill: black; dominant-baseline: central}
13+
.nad-text-nodes foreignObject {overflow: visible; color: black}
14+
.nad-label-box {background-color: #6c6c6c20; width: max-content; padding: 10px; border-radius: 10px;}
15+
.nad-legend-square {width: 20px; height: 20px; background: lightgrey;}
16+
.nad-edge-infos text, .nad-edge-label text {font: 20px serif; dominant-baseline:middle; stroke: #FFFFFFAA; stroke-width: 10; stroke-linejoin:round; paint-order: stroke}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://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+
import com.powsybl.diagram.test.Networks;
11+
import com.powsybl.iidm.network.Network;
12+
import com.powsybl.nad.AbstractTest;
13+
import com.powsybl.nad.layout.LayoutParameters;
14+
import com.powsybl.nad.svg.CustomStyleProvider.BusNodeStyles;
15+
import com.powsybl.nad.svg.CustomStyleProvider.EdgeStyles;
16+
import com.powsybl.nad.svg.CustomStyleProvider.ThreeWtStyles;
17+
import com.powsybl.nad.svg.iidm.DefaultLabelProvider;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
/**
25+
* @author Christian Biasuzzi {@literal <christian.biasuzzi at soft.it>}
26+
*/
27+
class CustomStyleProviderTest extends AbstractTest {
28+
StyleProvider styleProvider;
29+
30+
@BeforeEach
31+
void setup() {
32+
setLayoutParameters(new LayoutParameters());
33+
setSvgParameters(new SvgParameters()
34+
.setSvgWidthAndHeightAdded(true)
35+
.setFixedWidth(800)
36+
.setEdgeNameDisplayed(true)
37+
.setVoltageLevelDetails(true)
38+
.setBusLegend(true));
39+
}
40+
41+
@Override
42+
protected StyleProvider getStyleProvider(Network network) {
43+
return styleProvider;
44+
}
45+
46+
@Override
47+
protected LabelProvider getLabelProvider(Network network) {
48+
return new DefaultLabelProvider(network, getSvgParameters());
49+
50+
}
51+
52+
@Test
53+
void testCustomStyleProvider() {
54+
Network network = Networks.createNodeBreakerNetworkWithBranchStatus("TestNodeDecorators", "test");
55+
56+
Map<String, BusNodeStyles> busNodesStyles = new HashMap<>();
57+
busNodesStyles.put("VL1_10", new BusNodeStyles("yellow", null, null));
58+
busNodesStyles.put("VL2_30", new BusNodeStyles("red", "black", "4px"));
59+
60+
Map<String, EdgeStyles> edgesStyles = new HashMap<>();
61+
edgesStyles.put("L11", new EdgeStyles("blue", "2px", null, "blue", "2px", null));
62+
edgesStyles.put("L12", new EdgeStyles("green", "4px", "8,4", "green", "4px", "8,4"));
63+
edgesStyles.put("T11", new EdgeStyles("red", "8px", null, "brown", "8px", null));
64+
edgesStyles.put("T12", new EdgeStyles("orange", null, null, "orange", null, null));
65+
66+
Map<String, ThreeWtStyles> threeWtsStyles = new HashMap<>();
67+
threeWtsStyles.put("T3_12",
68+
new ThreeWtStyles(
69+
"gray", "4px", null,
70+
"purple", "4px", "4,4",
71+
"pink", "6px", null
72+
)
73+
);
74+
75+
styleProvider = new CustomStyleProvider(busNodesStyles, edgesStyles, threeWtsStyles);
76+
assertSvgEquals("/custom_style_provider.svg", network);
77+
}
78+
79+
@Test
80+
void testCustomStyleProviderEmpty() {
81+
Network network = Networks.createNodeBreakerNetworkWithBranchStatus("TestNodeDecorators", "test");
82+
styleProvider = new CustomStyleProvider(new HashMap<>(), new HashMap<>(), new HashMap<>());
83+
assertSvgEquals("/custom_style_provider_empty.svg", network);
84+
}
85+
86+
}

0 commit comments

Comments
 (0)