DefaultEdgeRendering.java
/**
* Copyright (c) 2022, 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.svg;
import com.powsybl.nad.model.*;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class DefaultEdgeRendering implements EdgeRendering {
@Override
public void run(Graph graph, SvgParameters svgParameters) {
graph.getNonMultiBranchEdgesStream().forEach(edge -> computeSingleBranchEdgeCoordinates(graph, edge, svgParameters));
graph.getMultiBranchEdgesStream().forEach(edges -> computeMultiBranchEdgesCoordinates(graph, edges, svgParameters));
graph.getLoopBranchEdgesMap().forEach((node, edges) -> loopEdgesLayout(graph, node, edges, svgParameters));
graph.getThreeWtNodesStream().forEach(threeWtNode -> computeThreeWtEdgeCoordinates(graph, threeWtNode, svgParameters));
graph.getTextEdgesMap().forEach((edge, nodes) -> computeTextEdgeLayoutCoordinates(nodes.getFirst(), nodes.getSecond(), edge));
}
private void computeTextEdgeLayoutCoordinates(Node node1, TextNode node2, TextEdge edge) {
edge.setPoints(node1.getPosition(), node2.getEdgeConnection());
}
private void computeSingleBranchEdgeCoordinates(Graph graph, BranchEdge edge, SvgParameters svgParameters) {
Node node1 = graph.getBusGraphNode1(edge);
Node node2 = graph.getBusGraphNode2(edge);
Point direction1 = getDirection(node2, () -> graph.getNode2(edge));
Point edgeStart1 = computeEdgeStart(node1, direction1, graph.getVoltageLevelNode1(edge), svgParameters);
Point direction2 = getDirection(node1, () -> graph.getNode1(edge));
Point edgeStart2 = computeEdgeStart(node2, direction2, graph.getVoltageLevelNode2(edge), svgParameters);
Point middle = Point.createMiddlePoint(edgeStart1, edgeStart2);
if (edge.isTransformerEdge()) {
double radius = svgParameters.getTransformerCircleRadius();
edge.setPoints1(edgeStart1, middle.atDistance(1.5 * radius, direction2));
edge.setPoints2(edgeStart2, middle.atDistance(1.5 * radius, direction1));
} else {
edge.setPoints1(edgeStart1, middle);
edge.setPoints2(edgeStart2, middle);
}
}
private Point getDirection(Node directionBusGraphNode, Supplier<Node> vlNodeSupplier) {
if (directionBusGraphNode == BusNode.UNKNOWN) {
return vlNodeSupplier.get().getPosition();
}
return directionBusGraphNode.getPosition();
}
private Point computeEdgeStart(Node node, Point direction, VoltageLevelNode vlNode, SvgParameters svgParameters) {
// If edge not connected to a bus node on that side, we use corresponding voltage level with specific extra radius
if (node == BusNode.UNKNOWN && vlNode != null) {
double unknownBusRadius = SvgWriter.getVoltageLevelCircleRadius(vlNode, svgParameters) + svgParameters.getUnknownBusNodeExtraRadius();
return vlNode.getPosition().atDistance(unknownBusRadius, direction);
}
Point edgeStart = node.getPosition();
if (node instanceof BusNode && vlNode != null) {
double busAnnulusOuterRadius = SvgWriter.getBusAnnulusOuterRadius((BusNode) node, vlNode, svgParameters);
edgeStart = edgeStart.atDistance(busAnnulusOuterRadius - svgParameters.getEdgeStartShift(), direction);
}
return edgeStart;
}
private void computeMultiBranchEdgesCoordinates(Graph graph, List<BranchEdge> edges, SvgParameters svgParameters) {
BranchEdge firstEdge = edges.iterator().next();
VoltageLevelNode nodeA = graph.getVoltageLevelNode1(firstEdge);
VoltageLevelNode nodeB = graph.getVoltageLevelNode2(firstEdge);
Point pointA = nodeA.getPosition();
Point pointB = nodeB.getPosition();
double dx = pointB.getX() - pointA.getX();
double dy = pointB.getY() - pointA.getY();
double angle = Math.atan2(dy, dx);
int nbForks = edges.size();
double forkAperture = Math.toRadians(svgParameters.getEdgesForkAperture());
double forkLength = svgParameters.getEdgesForkLength();
double angleStep = forkAperture / (nbForks - 1);
int i = 0;
for (BranchEdge edge : edges) {
if (2 * i + 1 == nbForks) { // in the middle, hence alpha = 0
computeSingleBranchEdgeCoordinates(graph, edge, svgParameters);
} else {
double alpha = -forkAperture / 2 + i * angleStep;
double angleForkA = angle - alpha;
double angleForkB = angle + Math.PI + alpha;
Point forkA = pointA.shift(forkLength * Math.cos(angleForkA), forkLength * Math.sin(angleForkA));
Point forkB = pointB.shift(forkLength * Math.cos(angleForkB), forkLength * Math.sin(angleForkB));
Point middle = Point.createMiddlePoint(forkA, forkB);
BranchEdge.Side sideA = graph.getNode1(edge) == nodeA ? BranchEdge.Side.ONE : BranchEdge.Side.TWO;
computeHalfForkCoordinates(graph, svgParameters, nodeA, edge, forkA, middle, sideA);
computeHalfForkCoordinates(graph, svgParameters, nodeB, edge, forkB, middle, sideA.getOpposite());
}
i++;
}
}
private void computeHalfForkCoordinates(Graph graph, SvgParameters svgParameters, VoltageLevelNode node, BranchEdge edge, Point fork, Point middle, BranchEdge.Side side) {
Node busNodeA = side == BranchEdge.Side.ONE ? graph.getBusGraphNode1(edge) : graph.getBusGraphNode2(edge);
Point edgeStart = computeEdgeStart(busNodeA, fork, node, svgParameters);
Point endFork = edge.isTransformerEdge()
? middle.atDistance(1.5 * svgParameters.getTransformerCircleRadius(), fork)
: middle;
edge.setPoints(side, edgeStart, fork, endFork);
}
private void loopEdgesLayout(Graph graph, VoltageLevelNode node, List<BranchEdge> loopEdges, SvgParameters svgParameters) {
List<Double> angles = computeLoopAngles(graph, loopEdges, node, svgParameters);
int i = 0;
for (BranchEdge edge : loopEdges) {
double angle = angles.get(i++);
Point middle = node.getPosition().atDistance(svgParameters.getLoopDistance(), angle);
loopEdgesHalfLayout(graph, node, svgParameters, edge, BranchEdge.Side.ONE, angle, middle);
loopEdgesHalfLayout(graph, node, svgParameters, edge, BranchEdge.Side.TWO, angle, middle);
}
}
private void loopEdgesHalfLayout(Graph graph, VoltageLevelNode node, SvgParameters svgParameters,
BranchEdge edge, BranchEdge.Side side, double angle, Point middle) {
int sideSign = side == BranchEdge.Side.ONE ? -1 : 1;
double startAngle = angle + sideSign * Math.toRadians(svgParameters.getLoopEdgesAperture() / 2);
double radius = svgParameters.getTransformerCircleRadius();
double controlsDist = svgParameters.getLoopControlDistance();
boolean isTransformer = edge.isTransformerEdge();
double endAngle = angle + sideSign * Math.PI / 2;
Point fork = node.getPosition().atDistance(svgParameters.getEdgesForkLength(), startAngle);
Point edgeStart = computeEdgeStart(graph.getBusGraphNode(edge, side), fork, node, svgParameters);
Point control1a = fork.atDistance(controlsDist, startAngle);
Point middle1 = isTransformer ? middle.atDistance(1.5 * radius, endAngle) : middle;
Point control1b = middle1.atDistance(isTransformer ? Math.max(0, controlsDist - 1.5 * radius) : controlsDist, endAngle);
edge.setPoints(side, edgeStart, fork, control1a, control1b, middle1);
}
private List<Double> computeLoopAngles(Graph graph, List<BranchEdge> loopEdges, Node node, SvgParameters svgParameters) {
int nbLoops = loopEdges.size();
List<Double> anglesOtherEdges = graph.getBranchEdgeStream(node)
.filter(e -> !loopEdges.contains(e))
.mapToDouble(e -> getAngle(e, graph, node))
.sorted().boxed().collect(Collectors.toList());
List<Double> loopAngles = new ArrayList<>();
if (!anglesOtherEdges.isEmpty()) {
anglesOtherEdges.add(anglesOtherEdges.get(0) + 2 * Math.PI);
double apertureWithMargin = Math.toRadians(svgParameters.getLoopEdgesAperture() * 1.2);
double[] deltaAngles = new double[anglesOtherEdges.size() - 1];
int nbSeparatedSlots = 0;
int nbSharedSlots = 0;
for (int i = 0; i < anglesOtherEdges.size() - 1; i++) {
deltaAngles[i] = anglesOtherEdges.get(i + 1) - anglesOtherEdges.get(i);
nbSeparatedSlots += deltaAngles[i] > apertureWithMargin ? 1 : 0;
nbSharedSlots += Math.floor(deltaAngles[i] / apertureWithMargin);
}
List<Integer> sortedIndices = IntStream.range(0, deltaAngles.length)
.boxed().sorted(Comparator.comparingDouble(i -> deltaAngles[i]))
.collect(Collectors.toList());
if (nbLoops <= nbSeparatedSlots) {
// Place loops in "slots" separated by non-loop edges
computeLoopAnglesWhenEnoughSeparatedSlotsPresent(sortedIndices, nbLoops, anglesOtherEdges, loopAngles);
} else if (nbLoops <= nbSharedSlots) {
// Place the maximum of loops in "slots" separated by non-loop edges, and put the excessive ones in the bigger "slots"
int nbExcessiveRemaining = nbLoops - nbSeparatedSlots;
computeLoopAnglesWhenEnoughSharedSlotsPresent(nbExcessiveRemaining, sortedIndices, deltaAngles, apertureWithMargin, svgParameters, anglesOtherEdges, loopAngles);
} else {
// Not enough place in the slots: dividing the circle in nbLoops, starting in the middle of the biggest slot
int iMaxDelta = sortedIndices.get(sortedIndices.size() - 1);
double startAngle = (anglesOtherEdges.get(iMaxDelta) + anglesOtherEdges.get(iMaxDelta + 1)) / 2;
IntStream.range(0, nbLoops).mapToDouble(i -> startAngle + i * 2 * Math.PI / nbLoops).forEach(loopAngles::add);
}
} else {
// No other edges: dividing the circle in nbLoops
IntStream.range(0, nbLoops).mapToDouble(i -> i * 2 * Math.PI / nbLoops).forEach(loopAngles::add);
}
return loopAngles;
}
private void computeLoopAnglesWhenEnoughSeparatedSlotsPresent(List<Integer> sortedIndices, int nbLoops, List<Double> anglesOtherEdges,
List<Double> loopAngles) {
for (int i = sortedIndices.size() - nbLoops; i < sortedIndices.size(); i++) {
int iSorted = sortedIndices.get(i);
loopAngles.add((anglesOtherEdges.get(iSorted) + anglesOtherEdges.get(iSorted + 1)) / 2);
}
}
private void computeLoopAnglesWhenEnoughSharedSlotsPresent(int initNbExcessiveRemaining, List<Integer> sortedIndices,
double[] deltaAngles, double apertureWithMargin,
SvgParameters svgParameters, List<Double> anglesOtherEdges,
List<Double> loopAngles) {
int nbExcessiveRemaining = initNbExcessiveRemaining;
for (int i = sortedIndices.size() - 1; i >= 0; i--) {
int iSorted = sortedIndices.get(i);
int nbAvailableSlots = (int) Math.floor(deltaAngles[iSorted] / apertureWithMargin);
if (nbAvailableSlots == 0) {
break;
}
int nbLoopsInDelta = Math.min(nbAvailableSlots, nbExcessiveRemaining + 1);
double extraSpace = deltaAngles[iSorted] - Math.toRadians(svgParameters.getLoopEdgesAperture()) * nbLoopsInDelta; // extra space without margins
double intraSpace = extraSpace / (nbLoopsInDelta + 1); // space between two loops and between non-loop edges and first/last loop
double angleStep = (anglesOtherEdges.get(iSorted + 1) - anglesOtherEdges.get(iSorted) - intraSpace) / nbLoopsInDelta;
double startAngle = anglesOtherEdges.get(iSorted) + intraSpace / 2 + angleStep / 2;
IntStream.range(0, nbLoopsInDelta).mapToDouble(iLoop -> startAngle + iLoop * angleStep).forEach(loopAngles::add);
nbExcessiveRemaining -= nbLoopsInDelta - 1;
}
}
private double getAngle(BranchEdge edge, Graph graph, Node node) {
BranchEdge.Side side = graph.getNode1(edge) == node ? BranchEdge.Side.ONE : BranchEdge.Side.TWO;
return edge.getEdgeStartAngle(side);
}
private void computeThreeWtEdgeCoordinates(Graph graph, ThreeWtNode threeWtNode, SvgParameters svgParameters) {
// The 3wt edges are computed by finding the "leading" edge and then placing the other edges at 120��
// The leading edge is chosen to be the opposite edge of the smallest aperture.
List<ThreeWtEdge> edges = graph.getThreeWtEdgeStream(threeWtNode).collect(Collectors.toList());
List<Double> angles = edges.stream()
.map(edge -> computeEdgeStart(graph.getBusGraphNode(edge), threeWtNode.getPosition(), graph.getVoltageLevelNode(edge), svgParameters))
.map(edgeStart -> threeWtNode.getPosition().getAngle(edgeStart))
.collect(Collectors.toList());
List<Integer> sortedIndices = IntStream.range(0, 3)
.boxed().sorted(Comparator.comparingDouble(angles::get))
.collect(Collectors.toList());
int leadingSortedIndex = getSortedIndexMaximumAperture(angles);
double leadingAngle = angles.get(sortedIndices.get(leadingSortedIndex));
List<ThreeWtEdge> edgesSorted = IntStream.range(0, 3)
.map(i -> (leadingSortedIndex + i) % 3)
.map(sortedIndices::get)
.mapToObj(edges::get)
.collect(Collectors.toList());
double dNodeToAnchor = svgParameters.getTransformerCircleRadius() * 1.6;
for (int i = 0; i < edgesSorted.size(); i++) {
ThreeWtEdge edge = edgesSorted.get(i);
Point edgeStart = computeEdgeStart(graph.getBusGraphNode(edge), threeWtNode.getPosition(), graph.getVoltageLevelNode(edge), svgParameters);
double anchorAngle = leadingAngle + i * 2 * Math.PI / 3;
Point threeWtAnchor = threeWtNode.getPosition().shiftRhoTheta(dNodeToAnchor, anchorAngle);
edge.setPoints(edgeStart, threeWtAnchor);
}
}
private int getSortedIndexMaximumAperture(List<Double> angles) {
// Sorting the given angles
List<Double> sortedAngles = angles.stream().sorted().collect(Collectors.toList());
// Then calculating the apertures
sortedAngles.add(sortedAngles.get(0) + 2 * Math.PI);
double[] deltaAngles = new double[3];
for (int i = 0; i < 3; i++) {
deltaAngles[i] = sortedAngles.get(i + 1) - sortedAngles.get(i);
}
// Returning the (sorted) index of the angle facing the minimal aperture
int minDeltaIndex = IntStream.range(0, 3)
.boxed().min(Comparator.comparingDouble(i -> deltaAngles[i]))
.orElse(0);
return ((minDeltaIndex - 1) + 3) % 3;
}
}