org.jungrapht.visualization.layout.algorithms.repulsion.StandardFA2Repulsion Maven / Gradle / Ivy
package org.jungrapht.visualization.layout.algorithms.repulsion;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jungrapht.visualization.layout.model.LayoutModel;
import org.jungrapht.visualization.layout.model.Point;
/**
* Standard implementation of repulsion. We can strongly recommend to use this class only for Graphs
* with hundreds nodes. Complexity is O(N^2).
*
* For large Graphs use BarnesHutFA2Repulsion.
*
* @see "ForceAtlas2, a Continuous Graph Layout Algorithm for Handy Network Visualization Designed
* for the Gephi Software"
* @see "https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0098679"
* @param vertex class
* @param repulsion class
* @param repulsion builder class
*/
public class StandardFA2Repulsion<
V, R extends StandardFA2Repulsion, B extends StandardFA2Repulsion.Builder>
implements StandardRepulsion {
public static class Builder<
V, R extends StandardFA2Repulsion, B extends Builder>
implements StandardRepulsion.Builder {
protected Map frVertexData;
protected Function initializer = v -> Point.ORIGIN;
protected LayoutModel layoutModel;
protected Random random = new Random();
protected double kr = 1.0;
protected Function nodeSizes = null; // Sizes for prevent overlapping
protected Map nodeMasses = null; // Masses for "Repulsion by Degree"
public B nodeData(Map frVertexData) {
this.frVertexData = frVertexData;
return (B) this;
}
public B initializer(Function initializer) {
this.initializer = initializer;
return (B) this;
}
@Override
public B layoutModel(LayoutModel layoutModel) {
this.layoutModel = layoutModel;
return (B) this;
}
@Override
public B random(Random random) {
this.random = random;
return (B) this;
}
/**
* Set node sizes. They may have fixed size or size based on centrality measure or anything
* else. By default 1.0
*
* @param nodeSizes
* @return
*/
public B nodeSizes(Function nodeSizes) {
this.nodeSizes = nodeSizes;
return (B) this;
}
/**
* Set repulsion K. By default 100.0
*
* @param kr
* @return
*/
public B repulsionK(double kr) {
this.kr = kr;
return (B) this;
}
/**
* Set node masses. This may have fixed masses or masses based on degrees or anything else. By
* default node degrees plus one
*
* @param nodeMasses
* @return
*/
public B nodeMasses(Map nodeMasses) {
this.nodeMasses = nodeMasses;
return (B) this;
}
@Override
public R build() {
return (R) new StandardFA2Repulsion<>(this);
}
}
protected Map frVertexData;
protected Function initializer = v -> Point.ORIGIN;
protected LayoutModel layoutModel;
protected Set vertexSet;
protected Random random = new Random();
protected double kr = 100.0; // 100.0 is default value in Gephi
protected Function nodeSizes; // Sizes for prevent overlapping
protected Map nodeMasses; // Masses for "Repulsion by Degree"
protected static final double epsilon = 1e-16; // Math stability
public static Builder builder() {
return new Builder();
}
@Deprecated
public static Builder standardBuilder() {
return builder();
}
protected StandardFA2Repulsion(Builder builder) {
this.frVertexData = builder.frVertexData;
this.initializer = builder.initializer;
this.layoutModel = builder.layoutModel;
this.vertexSet = layoutModel.getGraph().vertexSet();
this.random = builder.random;
this.kr = builder.kr;
if (builder.nodeSizes == null) {
this.nodeSizes = v -> 1.0;
} else {
this.nodeSizes = builder.nodeSizes;
}
if (builder.nodeMasses == null) {
this.nodeMasses =
layoutModel
.getGraph()
.vertexSet()
.stream()
.collect(Collectors.toMap(v -> v, v -> layoutModel.getGraph().degreeOf(v) + 1.0));
} else {
this.nodeMasses = builder.nodeMasses;
}
}
public void step() {}
public Random getRandom() {
return random;
}
@Override
public void calculateRepulsion() {
for (V vertex1 : vertexSet) {
Point fvd1 = Point.ORIGIN;
Point p1 = layoutModel.apply(vertex1);
double vertexOneMass = nodeMasses.get(vertex1);
double vertexOneSize = nodeSizes.apply(vertex1);
try {
for (V vertex2 : vertexSet) {
if (vertex1 != vertex2) {
Point p2 = layoutModel.apply(vertex2);
if (p1 == null || p2 == null) {
continue;
}
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
double vertex2Mass = nodeMasses.get(vertex2);
double dist = Math.max(epsilon, Math.sqrt((dx * dx) + (dy * dy)));
dist -= vertexOneSize + nodeSizes.apply(vertex2);
double force;
if (dist > 0) {
force = kr * vertexOneMass * vertex2Mass / dist / dist;
} else if (dist < 0) {
force = kr * vertexOneMass * vertex2Mass / dist;
} else {
force = 0.0;
}
if (Double.isNaN(force)) {
throw new RuntimeException(
"Unexpected mathematical result in FRLayout:calcPositions [repulsion]");
}
fvd1 = fvd1.add(dx * force, dy * force);
}
}
frVertexData.put(vertex1, fvd1);
} catch (ConcurrentModificationException cme) {
calculateRepulsion();
}
}
}
}