Skip to content

Commit de9cca6

Browse files
committed
CountryDiagram quick and dirty
Signed-off-by: Florian Dupuy <florian.dupuy@rte-france.com>
1 parent 1bdcf73 commit de9cca6

File tree

8 files changed

+656
-0
lines changed

8 files changed

+656
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright (c) 2021, 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;
9+
10+
import com.powsybl.iidm.network.Network;
11+
import com.powsybl.nad.build.iidm.DefaultCountryStyleProvider;
12+
import com.powsybl.nad.build.iidm.CountryGraphBuilder;
13+
import com.powsybl.nad.build.iidm.DefaultCountryLabelProvider;
14+
import com.powsybl.nad.model.Graph;
15+
import com.powsybl.nad.svg.SvgParameters;
16+
import com.powsybl.nad.svg.SvgWriter;
17+
import com.powsybl.nad.svg.metadata.DiagramMetadata;
18+
import org.apache.commons.io.output.NullWriter;
19+
20+
import java.io.IOException;
21+
import java.io.StringWriter;
22+
import java.io.UncheckedIOException;
23+
import java.io.Writer;
24+
import java.nio.file.Path;
25+
import java.util.Objects;
26+
27+
/**
28+
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
29+
*/
30+
public final class CountryDiagram {
31+
32+
private CountryDiagram() {
33+
}
34+
35+
public static void draw(Network network, Path svgFile) {
36+
draw(network, svgFile, new NadParameters());
37+
}
38+
39+
public static void draw(Network network, Writer writer, Writer metadataWriter) {
40+
draw(network, writer, metadataWriter, new NadParameters());
41+
}
42+
43+
public static void draw(Network network, Path svgFile, NadParameters param) {
44+
Objects.requireNonNull(network);
45+
Objects.requireNonNull(svgFile);
46+
Objects.requireNonNull(param);
47+
48+
Graph graph = getLayoutResult(network, param);
49+
createSvgWriter(network, param).writeSvg(graph, svgFile);
50+
createMetadata(graph, param).writeJson(getMetadataPath(svgFile));
51+
}
52+
53+
public static void draw(Network network, Writer writer, Writer metadataWriter, NadParameters param) {
54+
Objects.requireNonNull(network);
55+
Objects.requireNonNull(writer);
56+
Objects.requireNonNull(metadataWriter);
57+
Objects.requireNonNull(param);
58+
59+
Graph graph = getLayoutResult(network, param);
60+
createSvgWriter(network, param).writeSvg(graph, writer);
61+
createMetadata(graph, param).writeJson(metadataWriter);
62+
}
63+
64+
private static DiagramMetadata createMetadata(Graph graph, NadParameters param) {
65+
return new DiagramMetadata(param.getLayoutParameters(), param.getSvgParameters()).addMetadata(graph);
66+
}
67+
68+
private static Graph getLayoutResult(Network network, NadParameters param) {
69+
var builder = new CountryGraphBuilder(network, param.getIdProviderFactory().create(), new DefaultCountryLabelProvider());
70+
var graph = builder.buildGraph();
71+
param.getLayoutFactory().create().run(graph, param.getLayoutParameters());
72+
return graph;
73+
}
74+
75+
private static SvgWriter createSvgWriter(Network network, NadParameters param) {
76+
return new SvgWriter(param.getSvgParameters(), new DefaultCountryStyleProvider(network),
77+
param.getComponentLibrary(), param.getEdgeRouting());
78+
}
79+
80+
private static Path getMetadataPath(Path svgPath) {
81+
Path dir = svgPath.toAbsolutePath().getParent();
82+
String svgFileName = svgPath.getFileName().toString();
83+
if (!svgFileName.endsWith(".svg")) {
84+
svgFileName = svgFileName + ".svg";
85+
}
86+
return dir.resolve(svgFileName.replace(".svg", "_metadata.json"));
87+
}
88+
89+
public static String drawToString(Network network, SvgParameters svgParameters) {
90+
try (StringWriter writer = new StringWriter()) {
91+
NadParameters nadParameters = new NadParameters().setSvgParameters(svgParameters);
92+
draw(network, writer, NullWriter.INSTANCE, nadParameters);
93+
return writer.toString();
94+
} catch (IOException e) {
95+
throw new UncheckedIOException(e);
96+
}
97+
}
98+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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.build.iidm;
9+
10+
import com.powsybl.commons.PowsyblException;
11+
import com.powsybl.iidm.network.*;
12+
import com.powsybl.nad.build.GraphBuilder;
13+
import com.powsybl.nad.model.BranchEdge;
14+
import com.powsybl.nad.model.BusNode;
15+
import com.powsybl.nad.model.Graph;
16+
import com.powsybl.nad.model.VoltageLevelNode;
17+
import com.powsybl.nad.svg.EdgeInfo;
18+
19+
import java.util.*;
20+
import java.util.stream.Collectors;
21+
22+
/**
23+
* Graph builder that creates a graph based on substation countries.
24+
* Creates one VoltageLevelNode with one BusNode for each country found in the network substations,
25+
* and BranchEdges between countries representing the existing lines between countries.
26+
*
27+
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
28+
*/
29+
public class CountryGraphBuilder implements GraphBuilder {
30+
31+
private final Network network;
32+
private final IdProvider idProvider;
33+
private final CountryLabelProvider labelProvider;
34+
35+
/**
36+
* Creates a new CountryGraphBuilder.
37+
*
38+
* @param network the network
39+
* @param idProvider the ID provider
40+
* @param labelProvider the label provider
41+
*/
42+
public CountryGraphBuilder(Network network, IdProvider idProvider, CountryLabelProvider labelProvider) {
43+
this.network = Objects.requireNonNull(network);
44+
this.idProvider = Objects.requireNonNull(idProvider);
45+
this.labelProvider = Objects.requireNonNull(labelProvider);
46+
}
47+
48+
@Override
49+
public Graph buildGraph() {
50+
Graph graph = new Graph();
51+
52+
// Get all countries from substations
53+
Set<Country> countries = getCountries();
54+
55+
// Create a VoltageLevelNode with one BusNode for each country
56+
Map<Country, VoltageLevelNode> countryToVlNode = new EnumMap<>(Country.class);
57+
58+
for (Country country : countries) {
59+
CountryLabelProvider.CountryLegend legend = labelProvider.getCountryLegend(country);
60+
VoltageLevelNode vlNode = new VoltageLevelNode(
61+
idProvider,
62+
country.name(),
63+
country.name(),
64+
false,
65+
true,
66+
legend.header(),
67+
legend.footer()
68+
);
69+
70+
BusNode busNode = new BusNode(idProvider, country.name(), Collections.emptyList(), null);
71+
72+
vlNode.addBusNode(busNode);
73+
graph.addNode(vlNode);
74+
graph.addTextNode(vlNode);
75+
76+
countryToVlNode.put(country, vlNode);
77+
}
78+
79+
// Create edges between countries based on lines
80+
createCountryConnections(graph, countryToVlNode);
81+
82+
return graph;
83+
}
84+
85+
/**
86+
* Gets all countries from substations in the network.
87+
*
88+
* @return set of countries
89+
*/
90+
private Set<Country> getCountries() {
91+
return network.getSubstationStream()
92+
.map(Substation::getNullableCountry)
93+
.filter(Objects::nonNull)
94+
.collect(Collectors.toSet());
95+
}
96+
97+
/**
98+
* Creates connections (BranchEdges) between countries based on lines in the network.
99+
*
100+
* @param graph the graph to add edges to
101+
* @param countryToVlNode mapping from country to voltage level node
102+
*/
103+
private void createCountryConnections(Graph graph,
104+
Map<Country, VoltageLevelNode> countryToVlNode) {
105+
106+
// Map to store aggregated active powers between countries
107+
Map<Border, BorderEdges> borderEdgesMap = new HashMap<>();
108+
109+
// Process all lines
110+
network.getLineStream().forEach(line -> fillBorderEdgesMap(line, borderEdgesMap));
111+
network.getTieLineStream().forEach(tieLine -> fillBorderEdgesMap(tieLine, borderEdgesMap));
112+
113+
// Process HVDC lines
114+
network.getHvdcLineStream().forEach(hvdcLine -> fillBorderEdgesMap(hvdcLine, borderEdgesMap));
115+
116+
// Create BranchEdges for each country pair with connections
117+
borderEdgesMap.forEach((border, borderEdges) -> {
118+
Country country1 = border.country1();
119+
Country country2 = border.country2();
120+
121+
VoltageLevelNode vlNode1 = countryToVlNode.get(country1);
122+
VoltageLevelNode vlNode2 = countryToVlNode.get(country2);
123+
124+
if (vlNode1 != null && vlNode2 != null) {
125+
createCountryEdge(graph, country1, country2, borderEdges, vlNode1, vlNode2);
126+
}
127+
});
128+
}
129+
130+
/**
131+
* Processes a branch to aggregate active power between countries.
132+
*/
133+
private void fillBorderEdgesMap(Branch<?> branch, Map<Border, BorderEdges> allBorderLines) {
134+
Country country1 = getCountryFromTerminal(branch.getTerminal1());
135+
Country country2 = getCountryFromTerminal(branch.getTerminal2());
136+
137+
if (country1 != null && country2 != null && country1 != country2) {
138+
Border pair = new Border(country1, country2);
139+
allBorderLines.computeIfAbsent(pair, k -> new BorderEdges())
140+
.addBranch(branch);
141+
}
142+
}
143+
144+
/**
145+
* Processes a tie line to aggregate active power between countries.
146+
*/
147+
private void fillBorderEdgesMap(HvdcLine hvdcLine, Map<Border, BorderEdges> allBorderLines) {
148+
Country country1 = getCountryFromTerminal(hvdcLine.getConverterStation1().getTerminal());
149+
Country country2 = getCountryFromTerminal(hvdcLine.getConverterStation2().getTerminal());
150+
151+
if (country1 != null && country2 != null && country1 != country2) {
152+
Border pair = new Border(country1, country2);
153+
allBorderLines.computeIfAbsent(pair, k -> new BorderEdges())
154+
.hvdcLines().add(hvdcLine);
155+
}
156+
}
157+
158+
/**
159+
* Gets the country from a terminal's substation.
160+
*/
161+
private Country getCountryFromTerminal(Terminal terminal) {
162+
return terminal.getVoltageLevel().getSubstation().map(Substation::getNullableCountry).orElse(null);
163+
}
164+
165+
/**
166+
* Creates a BranchEdge between two countries.
167+
*/
168+
private void createCountryEdge(Graph graph, Country country1, Country country2, BorderEdges borderEdges,
169+
VoltageLevelNode vlNode1, VoltageLevelNode vlNode2) {
170+
171+
String edgeId = country1.name() + "-" + country2.name();
172+
Optional<EdgeInfo> edgeInfo1 = labelProvider.getCountryEdgeInfo(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines, BranchEdge.Side.ONE);
173+
Optional<EdgeInfo> edgeInfo2 = labelProvider.getCountryEdgeInfo(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines, BranchEdge.Side.TWO);
174+
String label = labelProvider.getBranchLabel(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines);
175+
176+
BranchEdge edge = new BranchEdge(
177+
idProvider,
178+
edgeId,
179+
edgeId,
180+
BranchEdge.LINE_EDGE,
181+
edgeInfo1.orElse(null),
182+
edgeInfo2.orElse(null),
183+
label
184+
);
185+
186+
graph.addEdge(vlNode1, vlNode1.getBusNodes().getFirst(), vlNode2, vlNode2.getBusNodes().getFirst(), edge);
187+
}
188+
189+
/**
190+
* Record to represent a pair of countries, ensuring consistent ordering.
191+
*/
192+
private record Border(Country country1, Country country2) {
193+
Border {
194+
// Ensure consistent ordering to avoid duplicate pairs
195+
if (country1.compareTo(country2) > 0) {
196+
Country temp = country1;
197+
country1 = country2;
198+
country2 = temp;
199+
}
200+
}
201+
}
202+
203+
private record BorderEdges(List<Line> lines, List<TieLine> tieLines, List<HvdcLine> hvdcLines) {
204+
private BorderEdges() {
205+
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
206+
}
207+
208+
public void addBranch(Branch<?> branch) {
209+
if (branch instanceof Line line) {
210+
lines.add(line);
211+
} else if (branch instanceof TieLine tieLine) {
212+
tieLines.add(tieLine);
213+
} else {
214+
throw new PowsyblException("Unexcepted branch class: " + branch.getClass());
215+
}
216+
}
217+
}
218+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.build.iidm;
9+
10+
import com.powsybl.iidm.network.Country;
11+
import com.powsybl.iidm.network.HvdcLine;
12+
import com.powsybl.iidm.network.Line;
13+
import com.powsybl.iidm.network.TieLine;
14+
import com.powsybl.nad.model.BranchEdge;
15+
import com.powsybl.nad.svg.EdgeInfo;
16+
17+
import java.util.List;
18+
import java.util.Optional;
19+
20+
/**
21+
* Interface for providing labels and legends for countries.
22+
*
23+
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
24+
*/
25+
public interface CountryLabelProvider {
26+
/**
27+
* Gets the voltage level legend for a country.
28+
*/
29+
CountryLegend getCountryLegend(Country country);
30+
31+
/**
32+
* Gets EdgeInfo for the connection between two countries.
33+
*/
34+
Optional<EdgeInfo> getCountryEdgeInfo(Country country1, Country country2, List<Line> lines, List<TieLine> tieLines, List<HvdcLine> hvdcLines, BranchEdge.Side side);
35+
36+
/**
37+
* Gets the branch label for the connection between two countries.
38+
*/
39+
String getBranchLabel(Country country1, Country country2, List<Line> lines, List<TieLine> tieLines, List<HvdcLine> hvdcLines);
40+
41+
record CountryLegend(List<String> header, List<String> footer) {
42+
}
43+
}

0 commit comments

Comments
 (0)