LayoutContext.java
/**
* Copyright (c) 2025, 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/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.diagram.util.layout.geometry;
import com.powsybl.diagram.util.layout.Canvas;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* @author Nathan Dissoubray {@literal <nathan.dissoubray at rte-france.com>}
*/
public class LayoutContext<V, E> {
private final Point origin = new Point(0, 0);
private static final Logger LOGGER = LoggerFactory.getLogger(LayoutContext.class);
private final SimpleGraph<V, DefaultEdge> simpleGraph;
private final Map<V, Point> movingPoints = new LinkedHashMap<>();
// this will be filled by the Setup function using fixedNodes and initialPoints
private final Map<V, Point> fixedPoints = new LinkedHashMap<>();
private Map<V, Point> initialPoints = Collections.emptyMap();
private Set<V> fixedNodes = Collections.emptySet();
public LayoutContext(Graph<V, E> graph) {
Objects.requireNonNull(graph);
SimpleGraph<V, DefaultEdge> locSimpleGraph = new SimpleGraph<>(DefaultEdge.class);
for (V vertex : graph.vertexSet()) {
locSimpleGraph.addVertex(vertex);
}
for (E edge : graph.edgeSet()) {
V source = graph.getEdgeSource(edge);
V target = graph.getEdgeTarget(edge);
if (source != target) {
locSimpleGraph.addEdge(graph.getEdgeSource(edge), graph.getEdgeTarget(edge));
}
}
this.simpleGraph = locSimpleGraph;
}
public SimpleGraph<V, DefaultEdge> getSimpleGraph() {
return simpleGraph;
}
/**
* @return a Map with vertex/point, where the points are movable in the 2D space
*/
public Map<V, Point> getMovingPoints() {
return movingPoints;
}
/**
* @return a Map with vertex/point, where the points are NOT movable in the 2D space
*/
public Map<V, Point> getFixedPoints() {
return fixedPoints;
}
/**
* @return a Map vertex/point where the points had an initial position given to them before the setup
*/
public Map<V, Point> getInitialPoints() {
return initialPoints;
}
/**
* @return a set of all the vertices where the point corresponding to those is fixed. Note that this does not give the corresponding points directly
*/
public Set<V> getFixedNodes() {
return fixedNodes;
}
/**
* @param center the center of the graph in the 2D space
*/
public void setCenter(Vector2D center) {
Objects.requireNonNull(center);
origin.setPosition(center);
}
/**
* @return the center of the graph in the 2D space
*/
public Vector2D getCenter() {
return origin.getPosition();
}
/**
* @return the center of the graph in the 2D space, but in the format of a point
*/
public Point getOrigin() {
return origin;
}
/**
* @param initialPoints the vertices with the point give the initial position of the points in the 2D space
* @return the instance of LayoutContext you used this function on, with the initialPoints changed to the given parameter
*/
public LayoutContext<V, E> setInitialPoints(Map<V, Point> initialPoints) {
Objects.requireNonNull(initialPoints);
this.initialPoints = Objects.requireNonNull(initialPoints);
return this;
}
/**
* @param fixedNodes all the vertices whose corresponding point you want to not move in the 2D space
* @return the instance of LayoutContext you used this function on, with the fixedNodes changed to the given parameter
*/
public LayoutContext<V, E> setFixedNodes(Set<V> fixedNodes) {
Objects.requireNonNull(fixedNodes);
Set<V> intersection = new HashSet<>(fixedNodes);
// only put fixed nodes that are actually in the graph
// no need to have a fixed vertex if the vertex doesn't even exist in the graph
intersection.retainAll(this.simpleGraph.vertexSet());
if (!intersection.equals(fixedNodes)) {
LOGGER.warn("Some nodes of the given fixedNodes were not nodes of the graph, those nodes have been ignored");
}
this.fixedNodes = intersection;
return this;
}
/**
* Does {@link #setInitialPoints(Map)} and {@link #setFixedNodes(Set)} at the same time
* @param fixedPoints the vertices you want to have fixed, with the point being their initial position
* @return the instance of LayoutContext you used this function on, with the fixedNodes and initialPoints changed using the given parameter
*/
public LayoutContext<V, E> setFixedPoints(Map<V, Point> fixedPoints) {
Objects.requireNonNull(fixedPoints);
Map<V, Point> intersection = new HashMap<>(fixedPoints);
intersection.keySet().retainAll(this.simpleGraph.vertexSet());
if (!intersection.keySet().equals(fixedPoints.keySet())) {
LOGGER.warn("Some (vertex, point) of the given fixedPoints were not vertex of the graph, those (vertex, point) have been ignored");
}
this.initialPoints = intersection;
setFixedNodes(intersection.keySet());
return this;
}
/**
* Write a svg in the provided writer, using the tooltip as a text appearing when hovering a given vertex of the graph in the SVG
* @param tooltip associates each vertex of the graph to a message which will be displayed when hovering the mouse over the vertex in the SVG
* @param writer the writer in which to write the SVG
*/
public void toSVG(Function<V, String> tooltip, Writer writer) {
Objects.requireNonNull(writer);
BoundingBox boundingBoxMovingPoints = BoundingBox.computeBoundingBox(movingPoints.values());
BoundingBox boundingBoxFixedPoints = BoundingBox.computeBoundingBox(fixedPoints.values());
BoundingBox boundingBox = BoundingBox.addBoundingBoxes(boundingBoxMovingPoints, boundingBoxFixedPoints);
Canvas canvas = new Canvas(boundingBox, 1000, 60);
PrintWriter printWriter = new PrintWriter(writer);
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
printWriter.printf(Locale.US, "<svg width=\"%.2f\" height=\"%.2f\" xmlns=\"http://www.w3.org/2000/svg\">%n", canvas.getWidth(), canvas.getHeight());
printWriter.println("<style>");
printWriter.println("<![CDATA[");
printWriter.printf("circle {fill: %s;}%n", "purple");
printWriter.printf("line {stroke: %s; stroke-width: 2}%n", "purple");
printWriter.println("]]>");
printWriter.println("</style>");
Stream.concat(
movingPoints.entrySet().stream(),
fixedPoints.entrySet().stream()
).forEach(entry -> entry.getValue().toSVG(printWriter, canvas, tooltip, entry.getKey()));
// use graph and not simple graph, because we want to represent multiple edges, in case of multiple lines between stations
for (DefaultEdge edge : simpleGraph.edgeSet()) {
V firstVertex = simpleGraph.getEdgeSource(edge);
V secondVertex = simpleGraph.getEdgeTarget(edge);
Optional<Point> point1Opt = getPointWithVertex(firstVertex);
Optional<Point> point2Opt = getPointWithVertex(secondVertex);
if (point1Opt.isEmpty() || point2Opt.isEmpty()) {
LOGGER.error("No point found for edge, trying to continue with other vertex: {}", edge);
continue;
}
Point point1 = point1Opt.get();
Point point2 = point2Opt.get();
// this seems incorrect (reversed point1 and point2), but this is a refactor, so I just copied what was in the Spring class
Vector2D screenPosition1 = canvas.toScreen(point2.getPosition());
Vector2D screenPosition2 = canvas.toScreen(point1.getPosition());
printWriter.printf(
Locale.US,
"<line x1=\"%.2f\" y1=\"%.2f\" x2=\"%.2f\" y2=\"%.2f\"/>%n",
screenPosition1.getX(),
screenPosition1.getY(),
screenPosition2.getX(),
screenPosition2.getY()
);
}
printWriter.println("</svg>");
printWriter.close();
}
/**
* Write a svg at the provided path, using the tooltip as a text appearing when hovering a given vertex of the graph in the SVG
* @param tooltip associates each vertex of the graph to a message which will be displayed when hovering the mouse over the vertex in the SVG
* @param path the path to write this SVG to
* @throws IOException if the path does not exist, the program is lacking permission, or other reasons for which the SVG could not be written
*/
public void toSVG(Function<V, String> tooltip, Path path) throws IOException {
try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
toSVG(tooltip, writer);
}
}
private Optional<Point> getPointWithVertex(V vertex) {
Point point = movingPoints.get(vertex);
if (point != null) {
return Optional.of(point);
} else {
return Optional.ofNullable(fixedPoints.get(vertex));
}
}
/**
* Get the position of the point associated to the vertex
* @param vertex the vertex of the graph that is in <code>layoutContext</code> of the <code>run</code>
* @return the position of the point associated with the vertex
*/
public Vector2D getStablePosition(V vertex) {
Point fixedPoint = fixedPoints.get(vertex);
if (fixedPoint != null) {
return fixedPoint.getPosition();
} else {
return movingPoints.getOrDefault(vertex, new Point(-1, -1)).getPosition();
}
}
}