AbstractLayout.java
/**
* Copyright (c) 2021, 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.layout;
import com.powsybl.commons.PowsyblException;
import com.powsybl.sld.model.cells.Cell;
import com.powsybl.sld.model.coordinate.Direction;
import com.powsybl.sld.model.coordinate.Orientation;
import com.powsybl.sld.model.coordinate.Point;
import com.powsybl.sld.model.graphs.*;
import com.powsybl.sld.model.nodes.*;
import org.jgrapht.alg.util.Pair;
import java.util.*;
import java.util.function.BooleanSupplier;
/**
* @author Franck Lecuyer {@literal <franck.lecuyer at rte-france.com>}
* @author Slimane Amar {@literal <slimane.amar at rte-france.com>}
*/
public abstract class AbstractLayout<T extends AbstractBaseGraph> implements Layout {
private final T graph;
protected AbstractLayout(T graph) {
this.graph = graph;
}
public T getGraph() {
return graph;
}
protected abstract void manageSnakeLines(LayoutParameters layoutParameters);
protected void manageSnakeLines(BaseGraph graph, LayoutParameters layoutParameters) {
for (MiddleTwtNode multiNode : graph.getMultiTermNodes()) {
List<Edge> adjacentEdges = multiNode.getAdjacentEdges();
List<Node> adjacentNodes = multiNode.getAdjacentNodes();
if (multiNode instanceof Middle2WTNode) {
List<Point> pol = calculatePolylineSnakeLine(layoutParameters, new Pair<>(adjacentNodes.get(0), adjacentNodes.get(1)), true);
List<List<Point>> pollingSplit = splitPolyline2(pol, multiNode);
((BranchEdge) adjacentEdges.get(0)).setSnakeLine(pollingSplit.get(0));
((BranchEdge) adjacentEdges.get(1)).setSnakeLine(pollingSplit.get(1));
handle2wtNodeOrientation((Middle2WTNode) multiNode, pollingSplit);
} else if (multiNode instanceof Middle3WTNode) {
List<Point> pol1 = calculatePolylineSnakeLine(layoutParameters, new Pair<>(adjacentNodes.get(0), adjacentNodes.get(1)), true);
List<Point> pol2 = calculatePolylineSnakeLine(layoutParameters, new Pair<>(adjacentNodes.get(1), adjacentNodes.get(2)), false);
List<List<Point>> pollingSplit = splitPolyline3(pol1, pol2, multiNode);
for (int i = 0; i < 3; i++) {
((BranchEdge) adjacentEdges.get(i)).setSnakeLine(pollingSplit.get(i));
}
handle3wtNodeOrientation((Middle3WTNode) multiNode, pollingSplit);
}
}
for (BranchEdge lineEdge : graph.getLineEdges()) {
List<Node> adjacentNodes = lineEdge.getNodes();
lineEdge.setSnakeLine(calculatePolylineSnakeLine(layoutParameters, new Pair<>(adjacentNodes.get(0), adjacentNodes.get(1)), true));
}
}
private void handle2wtNodeOrientation(Middle2WTNode node, List<List<Point>> pollingSplit) {
List<Point> pol1 = pollingSplit.get(0);
List<Point> pol2 = pollingSplit.get(1);
// Orientation.LEFT example:
// coord1 o-----OO-----o coord2
Point coord1 = pol1.get(pol1.size() - 2); // point linked to winding1
Point coord2 = pol2.get(pol2.size() - 2); // point linked to winding2
if (coord1.getX() == coord2.getX()) {
node.setOrientation(coord2.getY() > coord1.getY() ? Orientation.DOWN : Orientation.UP);
} else {
node.setOrientation(coord1.getX() < coord2.getX() ? Orientation.RIGHT : Orientation.LEFT);
}
}
/**
* Deduce the node orientation based on the lines coordinates supporting the svg component.
* As we are dealing with straight lines, we always have two out of three snake lines which are in line, the third
* one being perpendicular.
*/
private void handle3wtNodeOrientation(Middle3WTNode node, List<List<Point>> snakeLines) {
List<Point> snakeLineLeg1 = snakeLines.get(0); // snakeline from leg1 feeder node to 3wt
List<Point> snakeLineLeg2 = snakeLines.get(1); // snakeline with simply two points going from leg2 feeder node to 3wt
List<Point> snakeLineLeg3 = snakeLines.get(2); // snakeline from leg3 feeder node to 3wt
// Orientation.UP example:
// line going _____OO_____ line going
// to leg1 O to leg3
// |
// o leg2
Point leg1 = snakeLineLeg1.get(snakeLineLeg1.size() - 2);
Point leg2 = snakeLineLeg2.get(snakeLineLeg2.size() - 2);
Point leg3 = snakeLineLeg3.get(snakeLineLeg3.size() - 2);
if (leg1.getY() == leg3.getY()) {
// General case
node.setOrientation(leg2.getY() < leg1.getY() ? Orientation.DOWN : Orientation.UP);
setWindingOrder(node,
() -> leg1.getX() < leg3.getX(),
Arrays.asList(Middle3WTNode.Winding.UPPER_LEFT, Middle3WTNode.Winding.DOWN, Middle3WTNode.Winding.UPPER_RIGHT,
Middle3WTNode.Winding.UPPER_RIGHT, Middle3WTNode.Winding.DOWN, Middle3WTNode.Winding.UPPER_LEFT));
} else if (leg2.getX() == leg1.getX()) {
// Specific case of leg1 and leg2 facing feeder nodes with same abscissa
node.setOrientation(leg3.getX() > leg1.getX() ? Orientation.LEFT : Orientation.RIGHT);
setWindingOrder(node,
() -> leg3.getX() > leg1.getX() == leg1.getY() > leg2.getY(),
Arrays.asList(Middle3WTNode.Winding.UPPER_LEFT, Middle3WTNode.Winding.UPPER_RIGHT, Middle3WTNode.Winding.DOWN,
Middle3WTNode.Winding.UPPER_RIGHT, Middle3WTNode.Winding.UPPER_LEFT, Middle3WTNode.Winding.DOWN));
} else if (leg2.getX() == leg3.getX()) {
// Specific case of leg2 and leg3 facing feeder nodes with same abscissa
node.setOrientation(leg1.getX() > leg3.getX() ? Orientation.LEFT : Orientation.RIGHT);
setWindingOrder(node,
() -> leg1.getX() > leg3.getX() == leg2.getY() > leg3.getY(),
Arrays.asList(Middle3WTNode.Winding.DOWN, Middle3WTNode.Winding.UPPER_LEFT, Middle3WTNode.Winding.UPPER_RIGHT,
Middle3WTNode.Winding.DOWN, Middle3WTNode.Winding.UPPER_RIGHT, Middle3WTNode.Winding.UPPER_LEFT));
}
}
private void setWindingOrder(Middle3WTNode node,
BooleanSupplier cond,
List<Middle3WTNode.Winding> windings) {
if (cond.getAsBoolean()) {
node.setWindingOrder(windings.get(0), windings.get(1), windings.get(2));
} else {
node.setWindingOrder(windings.get(3), windings.get(4), windings.get(5));
}
}
protected abstract List<Point> calculatePolylineSnakeLine(LayoutParameters layoutParam, Pair<Node, Node> nodes,
boolean increment);
protected Direction getNodeDirection(Node node, int nb) {
if (node.getType() != Node.NodeType.FEEDER) {
throw new PowsyblException("Node " + nb + " is not a feeder node");
}
Direction dNode = getGraph().getCell(node).map(Cell::getDirection).orElse(Direction.TOP);
if (dNode != Direction.TOP && dNode != Direction.BOTTOM) {
throw new PowsyblException("Node " + nb + " cell direction not TOP or BOTTOM");
}
return dNode;
}
/*
* Calculate polyline points of a snakeLine
* This is a default implementation of 'calculatePolylineSnakeLine' for a horizontal layout
*/
protected List<Point> calculatePolylineSnakeLineForHorizontalLayout(LayoutParameters layoutParam,
Pair<Node, Node> nodes,
boolean increment, InfosNbSnakeLinesHorizontal infosNbSnakeLines,
double yMin, double yMax) {
Node node1 = nodes.getFirst();
Node node2 = nodes.getSecond();
List<Point> pol = new ArrayList<>();
pol.add(getGraph().getShiftedPoint(node1));
addMiddlePoints(layoutParam, nodes, infosNbSnakeLines, increment, pol, new Pair<>(yMin, yMax));
pol.add(getGraph().getShiftedPoint(node2));
return pol;
}
private void addMiddlePoints(LayoutParameters layoutParam,
Pair<Node, Node> nodes,
InfosNbSnakeLinesHorizontal infosNbSnakeLines, boolean increment,
List<Point> pol,
Pair<Double, Double> yMinMax) {
Node node1 = nodes.getFirst();
Node node2 = nodes.getSecond();
double yMin = yMinMax.getFirst();
double yMax = yMinMax.getSecond();
Direction dNode1 = getNodeDirection(node1, 1);
Direction dNode2 = getNodeDirection(node2, 2);
VoltageLevelGraph vlGraph1 = getGraph().getVoltageLevelGraph(node1);
VoltageLevelGraph vlGraph2 = getGraph().getVoltageLevelGraph(node2);
Map<Direction, Integer> nbSnakeLinesTopBottom = infosNbSnakeLines.getNbSnakeLinesTopBottom();
double x1 = node1.getX() + vlGraph1.getX();
double x2 = node2.getX() + vlGraph2.getX();
double y1 = dNode1 == Direction.BOTTOM ? yMax : yMin;
double y2 = dNode2 == Direction.BOTTOM ? yMax : yMin;
if (dNode1 == dNode2) {
if (increment) {
nbSnakeLinesTopBottom.compute(dNode1, (k, v) -> v + 1);
}
double decalV = getVerticalShift(layoutParam, dNode1, nbSnakeLinesTopBottom);
double yDecal = y1 + decalV;
pol.add(new Point(x1, yDecal));
pol.add(new Point(x2, yDecal));
} else {
if (increment) {
nbSnakeLinesTopBottom.compute(dNode1, (k, v) -> v + 1);
nbSnakeLinesTopBottom.compute(dNode2, (k, v) -> v + 1);
}
VoltageLevelGraph rightestVoltageLevel = vlGraph1.getX() > vlGraph2.getX() ? vlGraph1 : vlGraph2;
double xMaxGraph = rightestVoltageLevel.getX();
String idMaxGraph = rightestVoltageLevel.getId();
LayoutParameters.Padding vlPadding = layoutParam.getVoltageLevelPadding();
double decal1V = getVerticalShift(layoutParam, dNode1, nbSnakeLinesTopBottom);
double decal2V = getVerticalShift(layoutParam, dNode2, nbSnakeLinesTopBottom);
double xBetweenGraph = xMaxGraph - vlPadding.getLeft()
- (infosNbSnakeLines.getNbSnakeLinesVerticalBetween().compute(idMaxGraph, (k, v) -> v + 1) - 1) * layoutParam.getHorizontalSnakeLinePadding();
pol.addAll(Point.createPointsList(x1, y1 + decal1V,
xBetweenGraph, y1 + decal1V,
xBetweenGraph, y2 + decal2V,
x2, y2 + decal2V));
}
}
private static double getVerticalShift(LayoutParameters layoutParam, Direction dNode1, Map<Direction, Integer> nbSnakeLinesTopBottom) {
if (dNode1 == Direction.BOTTOM) {
return Math.max(nbSnakeLinesTopBottom.get(dNode1) - 1, 0) * layoutParam.getVerticalSnakeLinePadding() + layoutParam.getVoltageLevelPadding().getBottom();
} else {
return -Math.max(nbSnakeLinesTopBottom.get(dNode1) - 1, 0) * layoutParam.getVerticalSnakeLinePadding() - layoutParam.getVoltageLevelPadding().getTop();
}
}
protected List<List<Point>> splitPolyline2(List<Point> points, Node multiNode) {
int iMiddle0 = points.size() / 2 - 1;
int iMiddle1 = points.size() / 2;
Point pointSplit = points.get(iMiddle0).getMiddlePoint(points.get(iMiddle1));
multiNode.setCoordinates(pointSplit);
List<Point> part1 = new ArrayList<>(points.subList(0, iMiddle1));
part1.add(new Point(pointSplit));
// we need to reverse the order for the second part as the edges are always from middleTwtNode to twtLegNode
LinkedList<Point> part2 = new LinkedList<>();
points.stream().skip(iMiddle1).forEach(part2::addFirst);
part2.add(new Point(pointSplit));
return Arrays.asList(part1, part2);
}
protected List<List<Point>> splitPolyline3(List<Point> points1, List<Point> points2, Node coord) {
// for the first new edge, we keep all the original first polyline points, except the last one
List<Point> part1 = new ArrayList<>(points1.subList(0, points1.size() - 1));
// for the second new edge, we keep the last two points of the original first polyline (in reverse order
// as the edges are always from middleTwtNode to twtLegNode
// we need to create a new point to avoid having a point shared between part1 and part2
List<Point> part2 = Arrays.asList(points1.get(points1.size() - 1), new Point(points1.get(points1.size() - 2)));
// the third new edge is made with the original second polyline, except the first point (in reverse order
// as the edges are always from middleTwtNode to twtLegNode)
LinkedList<Point> part3 = new LinkedList<>();
points2.stream().skip(1).forEach(part3::addFirst);
// the fictitious node point is the second to last point of the original first polyline (or the second of the original second polyline)
coord.setCoordinates(points2.get(1));
return Arrays.asList(part1, part2, part3);
}
protected static double getWidthVerticalSnakeLines(String vlGraphId, LayoutParameters layoutParameters, InfosNbSnakeLinesHorizontal infosNbSnakeLines) {
return Math.max(infosNbSnakeLines.getNbSnakeLinesVerticalBetween().get(vlGraphId) - 1, 0) * layoutParameters.getHorizontalSnakeLinePadding();
}
protected static double getHeightSnakeLines(LayoutParameters layoutParameters, Direction top, InfosNbSnakeLinesHorizontal infosNbSnakeLines) {
return Math.max(infosNbSnakeLines.getNbSnakeLinesTopBottom().get(top) - 1, 0) * layoutParameters.getVerticalSnakeLinePadding();
}
}