org.jungrapht.visualization.layout.algorithms.EdgeAwareTreeLayoutAlgorithm Maven / Gradle / Ivy
package org.jungrapht.visualization.layout.algorithms;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jgrapht.Graph;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.model.Point;
import org.jungrapht.visualization.layout.model.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link TreeLayoutAlgorithm} that can evaluate {@code Comparator} and {@link Predicate} for both
* edges and vertices.
*
* During graph traversal (for tree layout) the Comparators may be spcified to help order the
* traversal to favor certain edges or vertices. Similarly, the Predicates may be used to help
* follow a desired path to a vertex when multiple alternatives appear in the graph.
*
* @author Tom Nelson
*/
public class EdgeAwareTreeLayoutAlgorithm extends TreeLayoutAlgorithm
implements TreeLayout,
EdgeAwareLayoutAlgorithm,
EdgeSorting,
EdgePredicated,
VertexSorting,
VertexPredicated {
private static final Logger log = LoggerFactory.getLogger(EdgeAwareTreeLayoutAlgorithm.class);
/**
* a Builder to create a configured instance of an EdgeAwareTreeLayoutAlgorithm
*
* @param the vertex type
* @param the edge type
* @param the type that is built
* @param the builder type
*/
public static class Builder<
V, E, T extends EdgeAwareTreeLayoutAlgorithm, B extends Builder>
extends TreeLayoutAlgorithm.Builder
implements EdgeAwareLayoutAlgorithm.Builder {
protected Predicate vertexPredicate = v -> false;
protected Predicate edgePredicate = e -> false;
protected Comparator vertexComparator = (v1, v2) -> 0;
protected Comparator edgeComparator = (e1, e2) -> 0;
protected boolean alignFavoredEdges = true;
/** {@inheritDoc} */
protected B self() {
return (B) this;
}
/**
* @param vertexPredicate {@link Predicate} to apply to vertices
* @return this Builder
*/
public B vertexPredicate(Predicate vertexPredicate) {
this.vertexPredicate = vertexPredicate;
return self();
}
/**
* @param edgePredicate {@link Predicate} to apply to edges
* @return this Builder
*/
public B edgePredicate(Predicate edgePredicate) {
this.edgePredicate = edgePredicate;
return self();
}
/**
* @param vertexComparator {@link Comparator} to sort vertices
* @return this Builder
*/
public B vertexComparator(Comparator vertexComparator) {
this.vertexComparator = vertexComparator;
return self();
}
/**
* @param edgeComparator {@link Comparator} to sort edges
* @return this Builder
*/
public B edgeComparator(Comparator edgeComparator) {
this.edgeComparator = edgeComparator;
return self();
}
public B alignFavoredEdges(boolean alignFavoredEdges) {
this.alignFavoredEdges = alignFavoredEdges;
return self();
}
/** {@inheritDoc} */
public T build() {
return (T) new EdgeAwareTreeLayoutAlgorithm<>(this);
}
}
/**
* @param vertex type
* @param edge type
* @return a Builder ready to configure
*/
public static Builder edgeAwareBuilder() {
return new Builder<>();
}
public EdgeAwareTreeLayoutAlgorithm() {
this(EdgeAwareTreeLayoutAlgorithm.edgeAwareBuilder());
}
/**
* Create an instance with the passed builder parameters
*
* @param builder the configuration
*/
protected EdgeAwareTreeLayoutAlgorithm(Builder builder) {
super(builder);
this.vertexPredicate = builder.vertexPredicate;
this.edgePredicate = builder.edgePredicate;
this.vertexComparator = builder.vertexComparator;
this.edgeComparator = builder.edgeComparator;
this.alignFavoredEdges = builder.alignFavoredEdges;
}
/** a {@link Predicate} to filter vertices */
protected Predicate vertexPredicate;
/** a {@link Predicate} to filter edges */
protected Predicate edgePredicate;
/** a {@link Comparator} to sort vertices */
protected Comparator vertexComparator;
/** a {@link Comparator} to sort edges */
protected Comparator edgeComparator;
protected boolean alignFavoredEdges;
/** @param vertexPredicate parameter to set */
@Override
public void setVertexPredicate(Predicate vertexPredicate) {
this.vertexPredicate = vertexPredicate;
}
/** @param edgePredicate parameter to set */
@Override
public void setEdgePredicate(Predicate edgePredicate) {
this.edgePredicate = edgePredicate;
}
/** @param vertexComparator parameter to set */
@Override
public void setVertexComparator(Comparator vertexComparator) {
if (vertexComparator == null) {
vertexComparator = (v1, v2) -> 0;
}
this.vertexComparator = vertexComparator;
}
/** @param edgeComparator parameter to set */
@Override
public void setEdgeComparator(Comparator edgeComparator) {
if (edgeComparator == null) {
edgeComparator = (e1, e2) -> 0;
}
this.edgeComparator = edgeComparator;
}
/**
* @param layoutModel the model to hold vertex positions
* @return the roots vertices of the tree
*/
protected Set buildTree(LayoutModel layoutModel) {
Set roots;
Graph graph = layoutModel.getGraph();
if (graph == null || graph.vertexSet().isEmpty()) {
correctOverlap = expandLayout = false;
return Collections.emptySet();
}
if (graph.vertexSet().size() == 1) {
V loner = graph.vertexSet().stream().findFirst().get();
layoutModel.set(loner, layoutModel.getWidth() / 2, layoutModel.getHeight() / 2);
correctOverlap = expandLayout = false;
return graph.vertexSet();
}
if (alignFavoredEdges) {
// builds without expanding
roots = alignBuildTree(layoutModel);
} else {
roots = super.buildTree(layoutModel);
}
return roots;
}
/**
* Build the tree (position the vertices) starting at the passed vertex and then descending
* recursively into its child vertices
*
* @param layoutModel the {@link LayoutModel} to hold the vertex positions
* @param vertex the vertex to place in position
* @param x the x position
* @param y the y position
* @param seen a set of vertices that were already 'seen' (and placed in the layoutModel)
*/
protected void buildTree(LayoutModel layoutModel, V vertex, int x, int y, Set seen) {
if (seen.add(vertex)) {
Graph graph = layoutModel.getGraph();
log.trace("buildTree placing {}", vertex);
// go one level further down
y += this.verticalVertexSpacing;
log.trace("Set vertex {} to {}", vertex, Point.of(x, y));
layoutModel.set(vertex, x, y);
merge(layoutModel, vertex);
double sizeXofCurrent = baseBounds.get(vertex).width;
x -= sizeXofCurrent / 2;
for (E edge :
graph
.outgoingEdgesOf(vertex)
.stream()
.sorted(edgeComparator)
.collect(Collectors.toCollection(LinkedHashSet::new))) {
if (edgePredicate.test(edge)
|| graph.incomingEdgesOf(graph.getEdgeTarget(edge)).stream().noneMatch(edgePredicate)) {
V v = graph.getEdgeTarget(edge);
if (!rootPredicate.test(v) && !seen.contains(v)) {
double sizeXofChild = this.baseBounds.getOrDefault(v, Rectangle.IDENTITY).width;
x += sizeXofChild / 2;
buildTree(layoutModel, v, x, y, seen);
merge(layoutModel, v);
x += sizeXofChild / 2 + horizontalVertexSpacing;
}
}
}
}
}
/**
* Calculate the overall width of the subtree rooted at the passed vertex. The edgePredicate is
* used to keep favored edges together. For example, if a non-favored edge provides a vertex
* sooner than its favored edge, the non-favored edge path will be deferred until the favored edge
* provides the same vertex. This prevents edges that span from one tree to another from
* 'stealing' a subtree that we would prefer to keep together on its favored edge path
*
* @param layoutModel the source of the Graph and its vertices
* @param vertex the vertex at the top of the current subtree
* @param seen a set of vertices that were already counted
* @return the overall width of the subtree rooted at the passed vertex
*/
protected int calculateWidth(LayoutModel layoutModel, V vertex, Set seen) {
if (seen.add(vertex)) {
Graph graph = layoutModel.getGraph();
int width =
Math.max(
0,
graph
.outgoingEdgesOf(vertex)
.stream()
.sorted(edgeComparator)
// skip over any edge that is not in the edgePredicate but also has a target with an
// incoming edge that is in the edgePredicate
.filter(
e ->
edgePredicate.test(e)
|| graph
.incomingEdgesOf(graph.getEdgeTarget(e))
.stream()
.noneMatch(edgePredicate))
.map(graph::getEdgeTarget)
.filter(v -> !rootPredicate.test(v) && !seen.contains(v))
.mapToInt(
element ->
calculateWidth(layoutModel, element, seen) + horizontalVertexSpacing)
.sum()
- horizontalVertexSpacing);
log.trace("calcWidth baseWidths put {} {}", vertex, width);
baseBounds.merge(
vertex,
Rectangle.of(0, 0, width, 0),
(r, t) -> Rectangle.of(r.x, r.y, t.width, r.height));
return width;
}
return 0;
}
/**
* Calculate the overall height of the subtree rooted at the passed vertex. The edgePredicate is
* used to keep favored edges together. For example, if a non-favored edge provides a vertex
* sooner than its favored edge, the non-favored edge path will be deferred until the favored edge
* provides the same vertex. This prevents edges that span from one tree to another from
* 'stealing' a subtree that we would prefer to keep together on its favored edge path
*
* @param layoutModel the source of the Graph and its vertices
* @param vertex the vertex at the top of the current subtree
* @param seen a set of vertices that were already counted
* @return the overall height of the subtree rooted at the passed vertex
*/
protected int calculateHeight(LayoutModel layoutModel, V vertex, Set seen) {
if (seen.add(vertex)) {
Graph graph = layoutModel.getGraph();
int height =
graph
.outgoingEdgesOf(vertex)
.stream()
.sorted(edgeComparator)
// skip over any edge that is not in the edgePredicate but also has a target with an
// incoming edge that is in the edgePredicate
.filter(
e ->
edgePredicate.test(e)
|| graph
.incomingEdgesOf(graph.getEdgeTarget(e))
.stream()
.noneMatch(edgePredicate))
.map(graph::getEdgeTarget)
.filter(v -> !rootPredicate.test(v) && !seen.contains(v))
.mapToInt(
element -> calculateHeight(layoutModel, element, seen) + verticalVertexSpacing)
.max()
.orElse(0);
baseBounds.merge(
vertex,
Rectangle.of(0, 0, 0, height),
(r, t) -> Rectangle.of(r.x, r.y, r.width, t.height));
return height;
}
return 0;
}
/**
* After the tree is configured, visit all of the vertices that are on favored edges and adjust
* their position to the left side of their children's bounding box. This helps provide a more
* linear path for favored edge endpoints
*
* @param layoutModel the source of the graph and its vertices
* @return the Set of root vertices
*/
protected Set alignBuildTree(LayoutModel layoutModel) {
Set roots = super.buildTree(layoutModel);
Graph graph = layoutModel.getGraph();
// move all the predicated vertices or vertices with adjacent predicated edges
for (V vertex : graph.vertexSet()) {
if (vertexPredicate.test(vertex)
|| graph.outgoingEdgesOf(vertex).stream().anyMatch(edgePredicate)
|| graph.incomingEdgesOf(vertex).stream().anyMatch(edgePredicate)) {
Rectangle vertexRectangle = baseBounds.getOrDefault(vertex, Rectangle.IDENTITY);
layoutModel.set(vertex, vertexRectangle.x, vertexRectangle.y);
}
}
return roots;
}
}