diff --git a/pom.xml b/pom.xml index 6b37aa4..21d3401 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 - + com.powsybl powsybl-parent-ws diff --git a/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java b/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java new file mode 100644 index 0000000..597b521 --- /dev/null +++ b/src/main/java/com/powsybl/nad/build/iidm/VoltageLevelFilter.java @@ -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 {} + */ + +public class VoltageLevelFilter implements Predicate { + + protected static final Logger LOGGER = LoggerFactory.getLogger(VoltageLevelFilter.class); + + public static final Predicate NO_FILTER = voltageLevel -> true; + + private static final String UNKNOWN_VOLTAGE_LEVEL = "Unknown voltage level id '"; + + private final Set voltageLevels; + + public VoltageLevelFilter(Set voltageLevels) { + this.voltageLevels = voltageLevels; + } + + public int getNbVoltageLevels() { + return voltageLevels.size(); + } + + public Set getVoltageLevels() { + return voltageLevels; + } + + @Override + public boolean test(VoltageLevel voltageLevel) { + return voltageLevels.contains(voltageLevel); + } + + public static VoltageLevelFilter createVoltageLevelsDepthFilter(Network network, List voltageLevelIds, int depth) { + return createVoltageLevelFilterWithPredicate(network, voltageLevelIds, depth, NO_FILTER); + } + + public static VoltageLevelFilter createVoltageLevelFilterWithPredicate(Network network, List voltageLevelIds, int depth, Predicate voltageLevelPredicate) { + Objects.requireNonNull(network); + Objects.requireNonNull(voltageLevelIds); + Set 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 voltageLevels = new HashSet<>(); + VoltageLevelFilter.traverseVoltageLevels(startingSet, depth, voltageLevels, voltageLevelPredicate); + return new VoltageLevelFilter(voltageLevels); + } + + public static Collection getNextDepthVoltageLevels(Network network, List voltageLevels) { + List voltageLevelIds = voltageLevels.stream().map(VoltageLevel::getId).collect(Collectors.toList()); + VoltageLevelFilter voltageLevelFilter = createVoltageLevelsDepthFilter(network, voltageLevelIds, 1); + Set voltageLevelSet = new HashSet<>(voltageLevelFilter.getVoltageLevels()); + voltageLevels.forEach(voltageLevelSet::remove); + return voltageLevelSet; + } + + private static void traverseVoltageLevels(Set voltageLevelsDepth, int depth, Set visitedVoltageLevels, Predicate predicate) { + if (depth < 0) { + return; + } + Set 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 nextDepthVoltageLevels; + private final Set visitedVoltageLevels; + private final Predicate voltageLevelPredicate; + + public VlVisitor(Set nextDepthVoltageLevels, Set visitedVoltageLevels, Predicate 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()))); + } + } + } +} diff --git a/src/main/java/com/powsybl/sld/server/GeoDataService.java b/src/main/java/com/powsybl/sld/server/GeoDataService.java new file mode 100644 index 0000000..119d6db --- /dev/null +++ b/src/main/java/com/powsybl/sld/server/GeoDataService.java @@ -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 + */ + +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 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); + } +} + diff --git a/src/main/java/com/powsybl/sld/server/NetworkAreaDiagramService.java b/src/main/java/com/powsybl/sld/server/NetworkAreaDiagramService.java index 4f962af..ec0c213 100644 --- a/src/main/java/com/powsybl/sld/server/NetworkAreaDiagramService.java +++ b/src/main/java/com/powsybl/sld/server/NetworkAreaDiagramService.java @@ -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; @@ -39,9 +48,12 @@ class NetworkAreaDiagramService { @Autowired private NetworkStoreService networkStoreService; + @Autowired + private GeoDataService geoDataService; + public SvgAndMetadata generateNetworkAreaDiagramSvg(UUID networkUuid, String variantId, List voltageLevelsIds, int depth) { Network network = DiagramUtils.getNetwork(networkUuid, variantId, networkStoreService, PreloadingStrategy.COLLECTION); - List existingVLIds = voltageLevelsIds.stream().filter(vl -> network.getVoltageLevel(vl) != null).collect(Collectors.toList()); + List existingVLIds = voltageLevelsIds.stream().filter(vl -> network.getVoltageLevel(vl) != null).toList(); if (existingVLIds.isEmpty()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "no voltage level was found"); } @@ -49,14 +61,21 @@ public SvgAndMetadata generateNetworkAreaDiagramSvg(UUID networkUuid, String var 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 + List 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 additionalMetadata = computeAdditionalMetadata(network, existingVLIds, depth); return SvgAndMetadata.builder() @@ -67,6 +86,32 @@ public SvgAndMetadata generateNetworkAreaDiagramSvg(UUID networkUuid, String var } } + public void assignGeoDataCoordinates(Network network, UUID networkUuid, String variantId, List voltageLevels) { + // Geographical positions for substations related to voltageLevels + List substations = voltageLevels.stream() + .map(VoltageLevel::getNullableSubstation) + .filter(Objects::nonNull) + .toList(); + + String substationsGeoDataString = geoDataService.getSubstationsGraphics(networkUuid, variantId, substations.stream().map(Substation::getId).toList()); + List substationsGeoData = GeoDataUtils.fromStringToSubstationGeoData(substationsGeoDataString, new ObjectMapper()); + Map 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 computeAdditionalMetadata(Network network, List voltageLevelsIds, int depth) { VoltageLevelFilter vlFilter = VoltageLevelFilter.createVoltageLevelsDepthFilter(network, voltageLevelsIds, depth); @@ -74,7 +119,7 @@ private Map computeAdditionalMetadata(Network network, List voltageLevelsInfos = voltageLevelsIds.stream() .map(network::getVoltageLevel) .map(VoltageLevelInfos::new) - .collect(Collectors.toList()); + .toList(); Map metadata = new HashMap<>(); metadata.put("nbVoltageLevels", vlFilter.getNbVoltageLevels()); diff --git a/src/main/java/com/powsybl/sld/server/RestTemplateConfig.java b/src/main/java/com/powsybl/sld/server/RestTemplateConfig.java new file mode 100644 index 0000000..e99c36c --- /dev/null +++ b/src/main/java/com/powsybl/sld/server/RestTemplateConfig.java @@ -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 + */ + +@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; + } + +} diff --git a/src/main/java/com/powsybl/sld/server/dto/Coordinate.java b/src/main/java/com/powsybl/sld/server/dto/Coordinate.java new file mode 100644 index 0000000..0b15978 --- /dev/null +++ b/src/main/java/com/powsybl/sld/server/dto/Coordinate.java @@ -0,0 +1,25 @@ +/** + * 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.dto; + +import lombok.*; + +/** + * @author Maissa SOUISSI + */ + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Coordinate { + private double lat; + private double lon; +} diff --git a/src/main/java/com/powsybl/sld/server/dto/SubstationGeoData.java b/src/main/java/com/powsybl/sld/server/dto/SubstationGeoData.java new file mode 100644 index 0000000..b1ac270 --- /dev/null +++ b/src/main/java/com/powsybl/sld/server/dto/SubstationGeoData.java @@ -0,0 +1,28 @@ +/** + * 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.dto; + +import com.powsybl.iidm.network.Country; +import lombok.*; + +/** + * @author Maissa SOUISSI + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@ToString +public class SubstationGeoData { + + private String id; + + private Country country; + + private Coordinate coordinate; +} diff --git a/src/main/java/com/powsybl/sld/server/utils/GeoDataUtils.java b/src/main/java/com/powsybl/sld/server/utils/GeoDataUtils.java new file mode 100644 index 0000000..4689214 --- /dev/null +++ b/src/main/java/com/powsybl/sld/server/utils/GeoDataUtils.java @@ -0,0 +1,38 @@ +/** + * 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.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.powsybl.commons.PowsyblException; +import com.powsybl.sld.server.dto.SubstationGeoData; + +import java.util.List; + +/** + * @author Maissa SOUISSI + */ + +public final class GeoDataUtils { + private GeoDataUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static List fromStringToSubstationGeoData(String jsonResponse, ObjectMapper objectMapper) { + try { + return objectMapper.readValue(jsonResponse, new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + throw new PowsyblException("Failed to parse JSON response", e); + } + } + +} + + + diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 4c640f6..84c7d41 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -5,3 +5,8 @@ powsybl: services: network-store-server: base-uri: http://localhost:8080 + +gridsuite: + services: + geo-data-server: + base-uri: http://localhost:8087 diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index b7adaf7..7ad3e36 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -1,3 +1,9 @@ spring: application: name: single-line-diagram-server + +gridsuite: + services: + - + name: geo-data-server + base-uri: http://geo-data-server/ \ No newline at end of file diff --git a/src/test/java/com/powsybl/sld/server/GeoDataServiceTest.java b/src/test/java/com/powsybl/sld/server/GeoDataServiceTest.java new file mode 100644 index 0000000..f22ba5c --- /dev/null +++ b/src/test/java/com/powsybl/sld/server/GeoDataServiceTest.java @@ -0,0 +1,131 @@ +package com.powsybl.sld.server; + +/* + * 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/. + */ + +import com.powsybl.iidm.network.*; +import com.powsybl.network.store.client.NetworkStoreService; +import com.powsybl.sld.server.dto.Coordinate; +import com.powsybl.sld.server.dto.SubstationGeoData; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +public class GeoDataServiceTest { + + private static final String VARIANT_1_ID = "variant_1"; + private static final String VARIANT_2_ID = "variant_2"; + private final String baseUri = "http://geo-data-server/"; + @Mock + private RestTemplate restTemplate; + @InjectMocks + private GeoDataService geoDataService; + @Mock + private NetworkStoreService networkStoreService; + + public static Network createNetwork() { + Network network = Network.create("test", "test"); + Substation substationFr1 = network.newSubstation() + .setId("subFr1") + .setCountry(Country.FR) + .setTso("RTE") + .add(); + VoltageLevel voltageLevelFr1A = substationFr1.newVoltageLevel() + .setId("vlFr1A") + .setName("vlFr1A") + .setNominalV(440.0) + .setHighVoltageLimit(400.0) + .setLowVoltageLimit(200.0) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + voltageLevelFr1A.getBusBreakerView().newBus() + .setId("busFr1A") + .setName("busFr1A") + .add(); + VoltageLevel voltageLevelFr1B = substationFr1.newVoltageLevel() + .setId("vlFr1B").setName("vlFr1B") + .setNominalV(200.0) + .setHighVoltageLimit(400.0) + .setLowVoltageLimit(200.0) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + voltageLevelFr1B.getBusBreakerView().newBus() + .setId("busFr1B") + .setName("busFr1B") + .add(); + + Substation substationFr2 = network.newSubstation() + .setId("subFr2") + .setCountry(Country.FR) + .setTso("RTE") + .add(); + VoltageLevel voltageLevelFr2A = substationFr2.newVoltageLevel() + .setId("vlFr2A") + .setName("vlFr2A") + .setNominalV(440.0) + .setHighVoltageLimit(400.0) + .setLowVoltageLimit(200.0) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + voltageLevelFr2A.getBusBreakerView().newBus() + .setId("busFr2A") + .setName("busFr2A") + .add(); + network.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, VARIANT_1_ID); + network.getVariantManager().cloneVariant(VariantManagerConstants.INITIAL_VARIANT_ID, VARIANT_2_ID); + + return network; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + geoDataService.setGeoDataServerBaseUri(baseUri); + } + + @Test + public void testGetSubstationsGraphics() { + UUID networkUuid = UUID.randomUUID(); + String variantId = "variant2"; + List substationsIds = List.of("subFr1", "subFr2"); + + String expectedResponse = "Substations graphics data"; + when(restTemplate.getForObject(ArgumentMatchers.anyString(), ArgumentMatchers.eq(String.class))) + .thenReturn(expectedResponse); + + String response = geoDataService.getSubstationsGraphics(networkUuid, variantId, substationsIds); + + assertEquals(expectedResponse, response); + } + + @Test + public void testGetSubstationsGraphicsWithoutVariantId() { + UUID networkUuid = UUID.randomUUID(); + List substationsIds = List.of("subFr1"); + SubstationGeoData substationGeoData = new SubstationGeoData(); + substationGeoData.setId("subFr1"); + substationGeoData.setCoordinate(new Coordinate(48.8588443, 2.2943506)); + substationGeoData.setCountry(Country.FR); + String expectedResponse = substationGeoData.toString(); + when(restTemplate.getForObject(ArgumentMatchers.anyString(), ArgumentMatchers.eq(String.class))) + .thenReturn(expectedResponse); + + String response = geoDataService.getSubstationsGraphics(networkUuid, null, substationsIds); + + assertEquals(expectedResponse, response); + } +} diff --git a/src/test/java/com/powsybl/sld/server/SingleLineDiagramTest.java b/src/test/java/com/powsybl/sld/server/SingleLineDiagramTest.java index 7935325..94139da 100644 --- a/src/test/java/com/powsybl/sld/server/SingleLineDiagramTest.java +++ b/src/test/java/com/powsybl/sld/server/SingleLineDiagramTest.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.ByteStreams; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import com.powsybl.commons.PowsyblException; @@ -16,6 +17,7 @@ import com.powsybl.iidm.network.extensions.BusbarSectionPositionAdder; import com.powsybl.iidm.network.extensions.ConnectablePosition; import com.powsybl.iidm.network.extensions.ConnectablePositionAdder; +import com.powsybl.iidm.network.extensions.SubstationPosition; import com.powsybl.network.store.client.NetworkStoreService; import com.powsybl.network.store.client.PreloadingStrategy; import com.powsybl.sld.SingleLineDiagram; @@ -54,6 +56,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import static com.powsybl.sld.library.ComponentTypeName.ARROW_ACTIVE; @@ -87,6 +90,8 @@ public class SingleLineDiagramTest { @MockBean private NetworkStoreService networkStoreService; + @MockBean + private GeoDataService geoDataService; private ObjectMapper objectMapper = new ObjectMapper(); @@ -281,11 +286,39 @@ public void testComponentLibraries() throws Exception { assertEquals("[\"GridSuiteAndConvergence\",\"Convergence\",\"FlatDesign\"]", result.getResponse().getContentAsString()); } + private static final String GEO_DATA_SUBSTATIONS = "/geo_data_substations.json"; + + @Test + public void testAssignSubstationGeoData() throws Exception { + UUID testNetworkId = UUID.fromString("7928181c-7977-4592-ba19-88027e4254e4"); + given(networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION)).willReturn(createNetwork()); + Network network = networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION); + + String substationGeoDataJson = "[{\"id\":\"subFr1\",\"coordinate\":{\"lat\":48.8588443,\"lon\":2.2943506}},{\"id\":\"subFr2\",\"coordinate\":{\"lat\":51.507351,\"lon\":1.127758}}]"; + given(geoDataService.getSubstationsGraphics(testNetworkId, VARIANT_1_ID, List.of("subFr2"))).willReturn(substationGeoDataJson); + + networkAreaDiagramService.assignGeoDataCoordinates(network, testNetworkId, VARIANT_1_ID, List.of(network.getVoltageLevel("vlFr2A"))); + assertEquals(network.getSubstation("subFr2").getExtension(SubstationPosition.class).getCoordinate(), new com.powsybl.iidm.network.extensions.Coordinate(51.507351, 1.127758)); + assertEquals(network.getSubstation("subFr1").getExtension(SubstationPosition.class), null); + + String faultSubstationGeoDataJson = "[{\"id\":\"subFr1\",\"coordinate\":{\"lat\":48.8588443,\"long\":2.2943506}}]"; + given(geoDataService.getSubstationsGraphics(testNetworkId, VARIANT_1_ID, List.of("subFr1"))).willReturn(faultSubstationGeoDataJson); + PowsyblException exception = assertThrows(PowsyblException.class, () -> { + networkAreaDiagramService.assignGeoDataCoordinates(network, testNetworkId, VARIANT_1_ID, List.of(network.getVoltageLevel("vlFr1A"))); + }); + + // Assert the exception message + assertEquals("Failed to parse JSON response", exception.getMessage()); + assertEquals(network.getSubstation("subFr1").getExtension(SubstationPosition.class), null); + } + @Test public void testNetworkAreaDiagram() throws Exception { UUID testNetworkId = UUID.fromString("7928181c-7977-4592-ba19-88027e4254e4"); UUID notFoundNetworkId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + given(geoDataService.getSubstationsGraphics(testNetworkId, VARIANT_2_ID, List.of("subFr1"))).willReturn(toString(GEO_DATA_SUBSTATIONS)); + given(networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION)).willReturn(createNetwork()); given(networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION)).willReturn(createNetwork()); given(networkStoreService.getNetwork(notFoundNetworkId, PreloadingStrategy.COLLECTION)).willThrow(new PowsyblException()); @@ -318,6 +351,7 @@ public void testNetworkAreaDiagramAdditionalMetadata() { UUID testNetworkId = UUID.fromString("7928181c-7977-4592-ba19-88027e4254e4"); given(networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION)).willReturn(createNetwork()); + given(geoDataService.getSubstationsGraphics(testNetworkId, VARIANT_2_ID, List.of("subFr1"))).willReturn(toString(GEO_DATA_SUBSTATIONS)); SvgAndMetadata svgAndMetadata = networkAreaDiagramService.generateNetworkAreaDiagramSvg(testNetworkId, VARIANT_2_ID, List.of("vlFr1A"), 2); Object additionalMetadata = svgAndMetadata.getAdditionalMetadata(); @@ -518,6 +552,8 @@ public void testPosisionDiagramLabelProvider() throws IOException { public void testNetworkAreaDiagramWithMissingVoltageLevel() throws Exception { UUID testNetworkId = UUID.fromString("7928181c-7977-4592-ba19-88027e4254e4"); given(networkStoreService.getNetwork(testNetworkId, PreloadingStrategy.COLLECTION)).willReturn(createNetwork()); + given(geoDataService.getSubstationsGraphics(testNetworkId, VARIANT_2_ID, List.of("subFr1"))).willReturn(toString(GEO_DATA_SUBSTATIONS)); + SvgAndMetadata svgAndMetadata = networkAreaDiagramService.generateNetworkAreaDiagramSvg(testNetworkId, VARIANT_2_ID, List.of("vlFr1A", "vlNotFound1"), 0); Object additionalMetadata = svgAndMetadata.getAdditionalMetadata(); assertNotNull(additionalMetadata); @@ -542,6 +578,14 @@ public static String toString(Path outPath) { return content; } + public String toString(String resourceName) { + try { + return new String(ByteStreams.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream(resourceName))), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + /* #TODO replace it with already configured FourSubstationsNodeBreakerWithExtensionsFactory when migrating to next powsybl release */ diff --git a/src/test/resources/geo_data_substations.json b/src/test/resources/geo_data_substations.json new file mode 100644 index 0000000..390a20f --- /dev/null +++ b/src/test/resources/geo_data_substations.json @@ -0,0 +1,10 @@ +[ + { + "id": "subFr1", + "country": "FR", + "coordinate": { + "lat": 53.19624, + "lon": 6.60964 + } + } +]