Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add geographical coordinates to network for NAD drawing and use GeographicalLayout #102

Merged
merged 17 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-parent-ws</artifactId>
Expand Down
157 changes: 157 additions & 0 deletions src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* 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/.
*/
package com.powsybl.nad.build.iidm;

import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.*;
import com.powsybl.nad.utils.iidm.IidmUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

// FIXME: to be removed at next powsybl-diagram upgrade.
// Copied from https://github.com/powsybl/powsybl-diagram.

/**
* @author Maissa Souissi {<maissa.souissi at rte-france.com>}
*/

public class VoltageLevelFilter implements Predicate<VoltageLevel> {
souissimai marked this conversation as resolved.
Show resolved Hide resolved

protected static final Logger LOGGER = LoggerFactory.getLogger(VoltageLevelFilter.class);

public static final Predicate<VoltageLevel> NO_FILTER = voltageLevel -> true;

private static final String UNKNOWN_VOLTAGE_LEVEL = "Unknown voltage level id '";

private final Set<VoltageLevel> voltageLevels;

public VoltageLevelFilter(Set<VoltageLevel> voltageLevels) {
this.voltageLevels = voltageLevels;
}

public int getNbVoltageLevels() {
return voltageLevels.size();
}

public Set<VoltageLevel> getVoltageLevels() {
return voltageLevels;
}

souissimai marked this conversation as resolved.
Show resolved Hide resolved
@Override
public boolean test(VoltageLevel voltageLevel) {
return voltageLevels.contains(voltageLevel);
}

public static VoltageLevelFilter createVoltageLevelsDepthFilter(Network network, List<String> voltageLevelIds, int depth) {
return createVoltageLevelFilterWithPredicate(network, voltageLevelIds, depth, NO_FILTER);
}

public static VoltageLevelFilter createVoltageLevelFilterWithPredicate(Network network, List<String> voltageLevelIds, int depth, Predicate<VoltageLevel> voltageLevelPredicate) {
Objects.requireNonNull(network);
Objects.requireNonNull(voltageLevelIds);
Set<VoltageLevel> startingSet = new HashSet<>();

for (String voltageLevelId : voltageLevelIds) {
VoltageLevel vl = network.getVoltageLevel(voltageLevelId);
if (vl == null) {
throw new PowsyblException(UNKNOWN_VOLTAGE_LEVEL + voltageLevelId + "'");
}
if (!voltageLevelPredicate.test(vl)) {
LOGGER.warn("vl '{}' does not comply with the predicate", voltageLevelId);
}
startingSet.add(vl);
}
Set<VoltageLevel> voltageLevels = new HashSet<>();
VoltageLevelFilter.traverseVoltageLevels(startingSet, depth, voltageLevels, voltageLevelPredicate);
return new VoltageLevelFilter(voltageLevels);
}

public static Collection<VoltageLevel> getNextDepthVoltageLevels(Network network, List<VoltageLevel> voltageLevels) {
List<String> voltageLevelIds = voltageLevels.stream().map(VoltageLevel::getId).collect(Collectors.toList());
VoltageLevelFilter voltageLevelFilter = createVoltageLevelsDepthFilter(network, voltageLevelIds, 1);
Set<VoltageLevel> voltageLevelSet = new HashSet<>(voltageLevelFilter.getVoltageLevels());
voltageLevels.forEach(voltageLevelSet::remove);
return voltageLevelSet;
}

private static void traverseVoltageLevels(Set<VoltageLevel> voltageLevelsDepth, int depth, Set<VoltageLevel> visitedVoltageLevels, Predicate<VoltageLevel> predicate) {
if (depth < 0) {
return;
}
Set<VoltageLevel> nextDepthVoltageLevels = new HashSet<>();
for (VoltageLevel vl : voltageLevelsDepth) {
if (!visitedVoltageLevels.contains(vl)) {
visitedVoltageLevels.add(vl);
vl.visitEquipments(new VlVisitor(nextDepthVoltageLevels, visitedVoltageLevels, predicate));
}
}
traverseVoltageLevels(nextDepthVoltageLevels, depth - 1, visitedVoltageLevels, predicate);
}

private static class VlVisitor extends DefaultTopologyVisitor {
private final Set<VoltageLevel> nextDepthVoltageLevels;
private final Set<VoltageLevel> visitedVoltageLevels;
private final Predicate<VoltageLevel> voltageLevelPredicate;

public VlVisitor(Set<VoltageLevel> nextDepthVoltageLevels, Set<VoltageLevel> visitedVoltageLevels, Predicate<VoltageLevel> voltageLevelPredicate) {
this.nextDepthVoltageLevels = nextDepthVoltageLevels;
this.visitedVoltageLevels = visitedVoltageLevels;
this.voltageLevelPredicate = voltageLevelPredicate;
}

@Override
public void visitLine(Line line, TwoSides side) {
visitBranch(line, side);
}

@Override
public void visitTwoWindingsTransformer(TwoWindingsTransformer twt, TwoSides side) {
visitBranch(twt, side);
}

@Override
public void visitThreeWindingsTransformer(ThreeWindingsTransformer twt, ThreeSides side) {
if (side == ThreeSides.ONE) {
visitTerminal(twt.getTerminal(ThreeSides.TWO));
visitTerminal(twt.getTerminal(ThreeSides.THREE));
} else if (side == ThreeSides.TWO) {
visitTerminal(twt.getTerminal(ThreeSides.ONE));
visitTerminal(twt.getTerminal(ThreeSides.THREE));
} else {
visitTerminal(twt.getTerminal(ThreeSides.ONE));
visitTerminal(twt.getTerminal(ThreeSides.TWO));
}
}

@Override
public void visitHvdcConverterStation(HvdcConverterStation<?> converterStation) {
converterStation.getOtherConverterStation().ifPresent(c -> visitTerminal(c.getTerminal()));
}

private void visitBranch(Branch<?> branch, TwoSides side) {
visitTerminal(branch.getTerminal(IidmUtils.getOpposite(side)));
}

private void visitTerminal(Terminal terminal) {
VoltageLevel voltageLevel = terminal.getVoltageLevel();
if (!visitedVoltageLevels.contains(voltageLevel) && voltageLevelPredicate.test(voltageLevel)) {
nextDepthVoltageLevels.add(voltageLevel);
}
}

@Override
public void visitDanglingLine(DanglingLine danglingLine) {
if (danglingLine.isPaired()) {
danglingLine.getTieLine().ifPresent(tieline -> visitBranch(tieline, tieline.getSide(danglingLine.getTerminal())));
}
}
}
}
66 changes: 66 additions & 0 deletions src/main/java/com/powsybl/sld/server/GeoDataService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* 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/.
*/

package com.powsybl.sld.server;

/**
* @author Maissa SOUISSI <maissa.souissi at rte-france.com>
*/

import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.List;
import java.util.UUID;

@Service
public class GeoDataService {

public static final String QUERY_PARAM_SUBSTATION_ID = "substationId";
public static final String GEO_DATA_API_VERSION = "v1";
public static final String QUERY_PARAM_VARIANT_ID = "variantId";
public static final String NETWORK_UUID = "networkUuid";
static final String SUBSTATIONS = "substations";
private static final String DELIMITER = "/";
private final RestTemplate restTemplate;
@Setter
private String geoDataServerBaseUri;

public GeoDataService(@Value("${gridsuite.services.geo-data-server.base-uri:http://geo-data-server/}") String geoDataServerBaseUri,
RestTemplate restTemplate) {
this.geoDataServerBaseUri = geoDataServerBaseUri;
this.restTemplate = restTemplate;
}

private String getGeoDataServerURI() {
return this.geoDataServerBaseUri + DELIMITER + GEO_DATA_API_VERSION + DELIMITER;
}

public String getSubstationsGraphics(UUID networkUuid, String variantId, List<String> substationsIds) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(getGeoDataServerURI() + SUBSTATIONS)
.queryParam(NETWORK_UUID, networkUuid);

if (!StringUtils.isBlank(variantId)) {
uriComponentsBuilder.queryParam(QUERY_PARAM_VARIANT_ID, variantId);
}

if (substationsIds != null) {
uriComponentsBuilder.queryParam(QUERY_PARAM_SUBSTATION_ID, substationsIds);
}

var path = uriComponentsBuilder
.buildAndExpand()
.toUriString();

return restTemplate.getForObject(path, String.class);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@
*/
package com.powsybl.sld.server;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.Substation;
import com.powsybl.iidm.network.VoltageLevel;
import com.powsybl.iidm.network.extensions.SubstationPosition;
import com.powsybl.iidm.network.extensions.SubstationPositionAdder;
import com.powsybl.nad.NadParameters;
import com.powsybl.nad.NetworkAreaDiagram;
import com.powsybl.nad.build.iidm.VoltageLevelFilter;
import com.powsybl.nad.layout.GeographicalLayoutFactory;
import com.powsybl.nad.layout.LayoutParameters;
import com.powsybl.nad.svg.SvgParameters;
import com.powsybl.nad.svg.iidm.TopologicalStyleProvider;
import com.powsybl.network.store.client.NetworkStoreService;
import com.powsybl.network.store.client.PreloadingStrategy;
import com.powsybl.sld.server.dto.Coordinate;
import com.powsybl.sld.server.dto.SubstationGeoData;
import com.powsybl.sld.server.dto.SvgAndMetadata;
import com.powsybl.sld.server.dto.VoltageLevelInfos;
import com.powsybl.sld.server.utils.DiagramUtils;
import com.powsybl.sld.server.utils.GeoDataUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
Expand All @@ -39,24 +48,34 @@ class NetworkAreaDiagramService {
@Autowired
private NetworkStoreService networkStoreService;

@Autowired
private GeoDataService geoDataService;

public SvgAndMetadata generateNetworkAreaDiagramSvg(UUID networkUuid, String variantId, List<String> voltageLevelsIds, int depth) {
Network network = DiagramUtils.getNetwork(networkUuid, variantId, networkStoreService, PreloadingStrategy.COLLECTION);
List<String> existingVLIds = voltageLevelsIds.stream().filter(vl -> network.getVoltageLevel(vl) != null).collect(Collectors.toList());
List<String> existingVLIds = voltageLevelsIds.stream().filter(vl -> network.getVoltageLevel(vl) != null).toList();
if (existingVLIds.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "no voltage level was found");
}
try (StringWriter svgWriter = new StringWriter()) {
SvgParameters svgParameters = new SvgParameters()
.setSvgWidthAndHeightAdded(true)
.setCssLocation(SvgParameters.CssLocation.EXTERNAL_NO_IMPORT);

//List of selected voltageLevels with depth
VoltageLevelFilter vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, existingVLIds, depth);
//get voltage levels' positions on depth+1 to be able to locate lines on depth
Comment on lines +65 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//List of selected voltageLevels with depth
VoltageLevelFilter vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, existingVLIds, depth);
//get voltage levels' positions on depth+1 to be able to locate lines on depth
//List of selected voltageLevels with depth N
VoltageLevelFilter vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, existingVLIds, depth);
//get voltage levels' positions on depth N+1 to be able to locate lines on depth N

List<VoltageLevel> voltageLevels = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, existingVLIds, depth + 1).getVoltageLevels().stream().toList();
assignGeoDataCoordinates(network, networkUuid, variantId, voltageLevels);

LayoutParameters layoutParameters = new LayoutParameters();
NadParameters nadParameters = new NadParameters();
nadParameters.setSvgParameters(svgParameters);
nadParameters.setLayoutParameters(layoutParameters);
nadParameters.setLayoutFactory(new GeographicalLayoutFactory(network));
nadParameters.setStyleProviderFactory(n -> new TopologicalStyleProvider(network));
var vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, existingVLIds, depth);
NetworkAreaDiagram.draw(network, svgWriter, nadParameters, vlFilter);

NetworkAreaDiagram.draw(network, svgWriter, nadParameters, vlFilter);
Map<String, Object> additionalMetadata = computeAdditionalMetadata(network, existingVLIds, depth);

return SvgAndMetadata.builder()
Expand All @@ -67,14 +86,40 @@ public SvgAndMetadata generateNetworkAreaDiagramSvg(UUID networkUuid, String var
}
}

public void assignGeoDataCoordinates(Network network, UUID networkUuid, String variantId, List<VoltageLevel> voltageLevels) {
// Geographical positions for substations related to voltageLevels
List<Substation> substations = voltageLevels.stream()
.map(VoltageLevel::getNullableSubstation)
.filter(Objects::nonNull)
.toList();

String substationsGeoDataString = geoDataService.getSubstationsGraphics(networkUuid, variantId, substations.stream().map(Substation::getId).toList());
List<SubstationGeoData> substationsGeoData = GeoDataUtils.fromStringToSubstationGeoData(substationsGeoDataString, new ObjectMapper());
Map<String, Coordinate> substationGeoDataMap = substationsGeoData.stream()
.collect(Collectors.toMap(SubstationGeoData::getId, SubstationGeoData::getCoordinate));

for (Substation substation : substations) {
if (network.getSubstation(substation.getId()).getExtension(SubstationPosition.class) == null) {
com.powsybl.sld.server.dto.Coordinate coordinate = substationGeoDataMap.get(substation.getId());
if (coordinate != null) {
network.getSubstation(substation.getId())
.newExtension(SubstationPositionAdder.class)
.withCoordinate(new com.powsybl.iidm.network.extensions.Coordinate(coordinate.getLat(), coordinate.getLon()))
.add();
}
}

}
}

private Map<String, Object> computeAdditionalMetadata(Network network, List<String> voltageLevelsIds, int depth) {

VoltageLevelFilter vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, voltageLevelsIds, depth);

List<VoltageLevelInfos> voltageLevelsInfos = voltageLevelsIds.stream()
.map(network::getVoltageLevel)
.map(VoltageLevelInfos::new)
.collect(Collectors.toList());
.toList();

Map<String, Object> metadata = new HashMap<>();
metadata.put("nbVoltageLevels", vlFilter.getNbVoltageLevels());
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/powsybl/sld/server/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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/.
*/

package com.powsybl.sld.server;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

/**
* @author Maissa SOUISSI <maissa.souissi at rte-france.com>
*/

@Configuration
public class RestTemplateConfig {

@Autowired
private ObjectMapper objectMapper;

@Bean
public RestTemplate restTemplate() {
final RestTemplate restTemplate = new RestTemplate();

//find and replace Jackson message converter with our own
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
final HttpMessageConverter<?> httpMessageConverter = restTemplate.getMessageConverters().get(i);
if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
restTemplate.getMessageConverters().set(i, mappingJackson2HttpMessageConverter());
}
}

return restTemplate;
}

private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}

}
Loading
Loading