org.jungrapht.visualization.layout.algorithms.CircleLayoutAlgorithm Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2003, The JUNG Authors
*
* All rights reserved.
*
* This software is open-source under the BSD license; see either
* "license.txt" or
* https://github.com/tomnelson/jungrapht-visualization/blob/master/LICENSE for a description.
*/
/*
* Created on Dec 4, 2003
*/
package org.jungrapht.visualization.layout.algorithms;
import static org.jungrapht.visualization.layout.util.PropertyLoader.PREFIX;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.jgrapht.Graph;
import org.jgrapht.Graphs;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.util.NeighborCache;
import org.jgrapht.graph.builder.GraphTypeBuilder;
import org.jungrapht.visualization.layout.algorithms.util.AfterRunnable;
import org.jungrapht.visualization.layout.algorithms.util.CircleLayoutReduceEdgeCrossing;
import org.jungrapht.visualization.layout.algorithms.util.ExecutorConsumer;
import org.jungrapht.visualization.layout.algorithms.util.Threaded;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.util.PropertyLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@code Layout} implementation that positions vertices equally spaced on a regular circle.
*
* @author Masanori Harada
* @author Tom Nelson - adapted to an algorithm
*/
public class CircleLayoutAlgorithm
implements LayoutAlgorithm, AfterRunnable, Threaded, ExecutorConsumer {
private static final Logger log = LoggerFactory.getLogger(CircleLayoutAlgorithm.class);
static {
PropertyLoader.load();
}
protected static final String CIRCLE_REDUCE_EDGE_CROSSING = PREFIX + "circle.reduceEdgeCrossing";
protected static final String CIRCLE_REDUCE_EDGE_CROSSING_MAX_EDGES =
PREFIX + "circle.reduceEdgeCrossingMaxEdges";
protected static final String CIRCLE_THREADED = PREFIX + "circle.threaded";
protected LayoutModel layoutModel;
protected Executor executor;
protected double radius;
protected boolean reduceEdgeCrossing;
protected List vertexOrderedList;
protected Runnable after;
private boolean threaded;
CompletableFuture theFuture;
protected int reduceEdgeCrossingMaxEdges;
int crossingCount = -1;
protected boolean cancelled;
public static class Builder, B extends Builder>
implements LayoutAlgorithm.Builder {
protected int radius;
protected Executor executor;
protected boolean reduceEdgeCrossing =
Boolean.parseBoolean(System.getProperty(CIRCLE_REDUCE_EDGE_CROSSING, "true"));
protected int reduceEdgeCrossingMaxEdges =
Integer.getInteger(CIRCLE_REDUCE_EDGE_CROSSING_MAX_EDGES, 200);
protected Runnable after = () -> {};
protected boolean threaded = Boolean.parseBoolean(System.getProperty(CIRCLE_THREADED, "true"));
B self() {
return (B) this;
}
public B executor(Executor executor) {
this.executor = executor;
return self();
}
public B radius(int radius) {
this.radius = radius;
return self();
}
public B reduceEdgeCrossingMaxEdges(int reduceEdgeCrossingMaxEdges) {
this.reduceEdgeCrossingMaxEdges = reduceEdgeCrossingMaxEdges;
return self();
}
public B reduceEdgeCrossing(boolean reduceEdgeCrossing) {
this.reduceEdgeCrossing = reduceEdgeCrossing;
return self();
}
public B threaded(boolean threaded) {
this.threaded = threaded;
return self();
}
public B after(Runnable after) {
this.after = after;
return self();
}
public T build() {
return (T) new CircleLayoutAlgorithm(this);
}
}
public static Builder builder() {
return new Builder<>();
}
protected CircleLayoutAlgorithm(Builder builder) {
this(
builder.executor,
builder.radius,
builder.reduceEdgeCrossing,
builder.reduceEdgeCrossingMaxEdges,
builder.threaded,
builder.after);
}
private CircleLayoutAlgorithm(
Executor executor,
int radius,
boolean reduceEdgeCrossing,
int reduceEdgeCrossingMaxEdges,
boolean threaded,
Runnable after) {
this.executor = executor;
this.radius = radius;
this.reduceEdgeCrossing = reduceEdgeCrossing;
this.reduceEdgeCrossingMaxEdges = reduceEdgeCrossingMaxEdges;
this.threaded = threaded;
this.after = after;
this.reduceEdgeCrossingMaxEdges =
Integer.getInteger(PREFIX + "circle.reduceEdgeCrossingMaxEdges", 100);
}
public CircleLayoutAlgorithm() {
this(CircleLayoutAlgorithm.builder());
}
/** @return the radius of the circle. */
public double getRadius() {
return radius;
}
/**
* Sets the radius of the circle. Must be called before {@code initialize()} is called.
*
* @param radius the radius of the circle
*/
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public void setExecutor(Executor executor) {
this.executor = executor;
}
@Override
public Executor getExecutor() {
return this.executor;
}
private void computeVertexOrder(LayoutModel layoutModel) {
Graph graph = layoutModel.getGraph();
if (reduceEdgeCrossing) {
reduceEdgeCrossing = graph.edgeSet().size() < this.reduceEdgeCrossingMaxEdges;
}
if (this.reduceEdgeCrossing) {
this.vertexOrderedList = new ArrayList<>();
ReduceCrossingRunnable reduceCrossingRunnable =
new ReduceCrossingRunnable<>(graph, this.vertexOrderedList);
if (threaded) {
if (executor != null) {
theFuture =
CompletableFuture.runAsync(reduceCrossingRunnable, executor)
.thenRun(
() -> {
log.trace("ReduceEdgeCrossing done");
layoutVertices(layoutModel, true);
runAfter(); // run the after function
layoutModel.getViewChangeSupport().fireViewChanged();
// fire an event to say that the layout is done
layoutModel
.getLayoutStateChangeSupport()
.fireLayoutStateChanged(layoutModel, false);
});
} else {
theFuture =
CompletableFuture.runAsync(reduceCrossingRunnable)
.thenRun(
() -> {
log.trace("ReduceEdgeCrossing done");
layoutVertices(layoutModel, true);
runAfter(); // run the after function
layoutModel.getViewChangeSupport().fireViewChanged();
// fire an event to say that the layout is done
layoutModel
.getLayoutStateChangeSupport()
.fireLayoutStateChanged(layoutModel, false);
});
}
} else {
reduceCrossingRunnable.run();
layoutVertices(layoutModel, true);
runAfter();
layoutModel.getViewChangeSupport().fireViewChanged();
// fire an event to say that the layout is done
layoutModel.getLayoutStateChangeSupport().fireLayoutStateChanged(layoutModel, false);
}
} else {
this.vertexOrderedList = new ArrayList<>(graph.vertexSet());
layoutVertices(layoutModel, false);
runAfter();
layoutModel.getLayoutStateChangeSupport().fireLayoutStateChanged(layoutModel, false);
}
if (log.isTraceEnabled()) {
log.trace(
"crossing count {}",
CircleLayoutReduceEdgeCrossing.countCrossings(graph, (V[]) vertexOrderedList.toArray()));
}
}
public int getCrossingCount() {
if (this.crossingCount < 0) {
this.crossingCount = countCrossings();
}
return this.crossingCount;
}
@Override
public boolean isThreaded() {
return this.threaded;
}
@Override
public void setThreaded(boolean threaded) {
this.threaded = threaded;
}
/**
* Sets the order of the vertices in the layout according to the ordering of {@code vertex_list}.
*
* @param vertexList a list specifying the ordering of the vertices
*/
public void setVertexOrder(LayoutModel layoutModel, List vertexList) {
Objects.requireNonNull(
vertexList.containsAll(layoutModel.getGraph().vertexSet()),
"Supplied list must include all vertices of the graph");
this.vertexOrderedList = vertexList;
}
@Override
public void visit(LayoutModel layoutModel) {
Graph graph = layoutModel.getGraph();
if (graph == null || graph.vertexSet().isEmpty()) {
return;
}
this.layoutModel = layoutModel;
if (layoutModel != null) {
computeVertexOrder(layoutModel);
}
}
private void layoutVertices(LayoutModel layoutModel, boolean countCrossings) {
double height = layoutModel.getHeight();
double width = layoutModel.getWidth();
if (radius <= 0) {
radius = 0.35 * Math.max(width, height);
// 0.45 * (Math.min(height, width));
}
int i = 0;
for (V vertex : vertexOrderedList) {
double angle = (2 * Math.PI * i) / vertexOrderedList.size();
double posX = Math.cos(angle) * radius + width / 2;
double posY = Math.sin(angle) * radius + height / 2;
if (!layoutModel.isLocked(vertex)) {
layoutModel.set(vertex, posX, posY);
}
if (log.isTraceEnabled()) {
log.trace("set {} to {},{} ", vertex, posX, posY);
}
i++;
}
if (countCrossings) {
crossingCount = countCrossings();
}
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public void runAfter() {
if (after != null) {
after.run();
}
}
@Override
public void setAfter(Runnable after) {
this.after = after;
}
static class ReduceCrossingRunnable implements Runnable {
Graph graph;
private List vertexOrderedList;
int edgeCrossCount = 0;
NeighborCache neighborCache;
ReduceCrossingRunnable(Graph graph, List vertexOrderList) {
this.graph = graph;
this.neighborCache = new NeighborCache<>(graph);
this.vertexOrderedList = vertexOrderList;
}
@Override
public void run() {
// is this a multicomponent graph?
ConnectivityInspector connectivityInspector = new ConnectivityInspector<>(graph);
List> componentVertices = connectivityInspector.connectedSets();
List vertexOrderedList = new ArrayList<>();
if (componentVertices.size() > 1) {
for (Set vertexSet : componentVertices) {
// get back the graph for these vertices
Graph subGraph = GraphTypeBuilder.forGraph(graph).buildGraph();
vertexSet.forEach(subGraph::addVertex);
for (V v :
vertexSet
.stream()
// filter out any vertices with only loop edges
.filter(v -> !TreeLayout.isLoopVertex(subGraph, v))
.collect(Collectors.toSet())) {
// get neighbors
neighborCache.successorsOf(v).forEach(s -> subGraph.addEdge(v, s, graph.getEdge(v, s)));
neighborCache
.predecessorsOf(v)
.forEach(p -> subGraph.addEdge(p, v, graph.getEdge(p, v)));
}
CircleLayoutReduceEdgeCrossing rec = new CircleLayoutReduceEdgeCrossing<>(subGraph);
vertexOrderedList.addAll(rec.getVertexOrderedList());
}
} else {
CircleLayoutReduceEdgeCrossing rec = new CircleLayoutReduceEdgeCrossing<>(graph);
vertexOrderedList.addAll(rec.getVertexOrderedList());
}
this.vertexOrderedList.clear();
this.vertexOrderedList.addAll(vertexOrderedList);
}
}
public int countCrossings() {
if (vertexOrderedList.size() == 0) {
return -1;
}
V[] vertices = (V[]) vertexOrderedList.toArray(new Object[0]);
Map vertexListPositions = new HashMap<>();
Graph graph = this.layoutModel.getGraph();
IntStream.range(0, vertices.length).forEach(i -> vertexListPositions.put(vertices[i], i));
int numberOfCrossings = 0;
Set openEdgeList = new LinkedHashSet<>();
List verticesSeen = new ArrayList<>();
for (V v : vertices) {
log.trace("for vertex {}", v);
verticesSeen.add(v);
// sort the incident edges....
List incidentEdges = new ArrayList<>(graph.edgesOf(v));
incidentEdges.sort(
(e, f) -> {
V oppe = Graphs.getOppositeVertex(graph, e, v);
V oppf = Graphs.getOppositeVertex(graph, f, v);
int idxv = vertexListPositions.get(v);
int idxe = vertexListPositions.get(oppe);
int idxf = vertexListPositions.get(oppf);
int deltae = idxv - idxe;
if (deltae < 0) {
deltae += vertices.length;
}
int deltaf = idxv - idxf;
if (deltaf < 0) {
deltaf += vertices.length;
}
return Integer.compare(deltae, deltaf);
});
for (E e : incidentEdges) {
V opposite = Graphs.getOppositeVertex(graph, e, v);
if (!verticesSeen.contains(opposite)) {
// e is an open edge
openEdgeList.add(e);
} else {
openEdgeList.remove(e);
for (int i = verticesSeen.indexOf(opposite) + 1; i < verticesSeen.indexOf(v); i++) {
V tween = verticesSeen.get(i);
numberOfCrossings +=
graph.edgesOf(tween).stream().filter(openEdgeList::contains).count();
log.trace("numberOfCrossings now {}", numberOfCrossings);
}
}
log.trace("added edge {}", e);
}
}
return numberOfCrossings;
}
@Override
public boolean constrained() {
return false;
}
}