com.googlecode.blaisemath.graph.layout.SpringLayout Maven / Gradle / Ivy
package com.googlecode.blaisemath.graph.layout;
/*
* #%L
* BlaiseGraphTheory
* --
* Copyright (C) 2009 - 2019 Elisha Peterson
* --
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import com.google.common.base.Objects;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.graph.Graph;
import com.googlecode.blaisemath.annotation.InvokedFromThread;
import com.googlecode.blaisemath.graph.GraphUtils;
import com.googlecode.blaisemath.graph.IterativeGraphLayout;
import java.awt.geom.Point2D;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Graph layout modeled after repulsive charges between nodes, and spring forces between nodes.
*
* @author Elisha Peterson
*/
public class SpringLayout implements IterativeGraphLayout {
private static final Logger LOG = Logger.getLogger(SpringLayout.class.getName());
@Override
public String toString() {
return "Spring layout algorithm";
}
@Override
public SpringLayoutState createState() {
return new SpringLayoutState();
}
@Override
public SpringLayoutParameters createParameters() {
return new SpringLayoutParameters();
}
@InvokedFromThread("unknown")
@Override
public final synchronized double iterate(Graph og, SpringLayoutState state, SpringLayoutParameters params) {
Graph g = og.isDirected() ? GraphUtils.copyUndirected(og) : og;
Set nodes = g.nodes();
Set pinned = params.getConstraints().getPinnedNodes();
Set unpinned = Sets.difference(nodes, pinned).immutableCopy();
double energy;
state.nodeLocationSync(nodes);
state.updateRegions(params.maxRepelDist);
Map forces = Maps.newHashMap();
computeNonRepulsiveForces(g, nodes, pinned, forces, state, params);
computeRepulsiveForces(pinned, forces, state, params);
checkForces(unpinned, forces);
energy = move(g, unpinned, forces, state, params);
return energy;
}
//region FORCE COMPUTATIONS
protected void computeNonRepulsiveForces(Graph g, Set nodes, Set pinned, Map forces,
SpringLayoutState state, SpringLayoutParameters params) {
for (N io : nodes) {
Point2D.Double iLoc = state.getLoc(io);
if (iLoc == null) {
iLoc = newNodeLocation(g, io, state, params);
state.putLoc(io, iLoc);
}
Point2D.Double iVel = state.getVel(io);
if (iVel == null) {
iVel = new Point2D.Double();
state.putVel(io, iVel);
}
if (!pinned.contains(io)) {
Point2D.Double netForce = new Point2D.Double();
addGlobalForce(netForce, iLoc, params);
addSpringForces(g, netForce, io, iLoc, state, params);
addAdditionalForces(g, netForce, io, iLoc, state, params);
forces.put(io, netForce);
}
}
}
@SuppressWarnings("EmptyMethod")
protected void addAdditionalForces(Graph g,
Point2D.Double sum, N io, Point2D.Double iLoc,
SpringLayoutState state, SpringLayoutParameters params) {
// hook for adding additional forces per the needs of child layouts
}
protected void computeRepulsiveForces(Set pinned, Map forces,
SpringLayoutState state, SpringLayoutParameters params) {
for (LayoutRegion[] rr : state.regions) {
for (LayoutRegion r : rr) {
for (N io : r.points()) {
if (!pinned.contains(io)) {
addRepulsiveForces(r, forces.get(io), io, r.get(io), params);
}
}
}
}
for (N io : state.oRegion.points()) {
if (!pinned.contains(io)) {
addRepulsiveForces(state.oRegion, forces.get(io), io, state.oRegion.get(io), params);
}
}
}
//endregion
//region STATIC METHODS
/**
* Get a position for a node that doesn't currently have a position.
* @param node the node to get new location of
*/
private static Point2D.Double newNodeLocation(Graph g, N node, SpringLayoutState state, SpringLayoutParameters params) {
double len = params.springL;
double sx = 0;
double sy = 0;
int n = 0;
for (N o : g.adjacentNodes(node)) {
Point2D.Double p = state.getLoc(o);
if (p != null) {
sx += p.x;
sy += p.y;
n++;
}
}
if (n == 0) {
return new Point2D.Double(sx + 2 * len * Math.random(), sy + 2 * len * Math.random());
} else if (n == 1) {
return new Point2D.Double(sx + len * Math.random(), sy + len * Math.random());
} else {
return new Point2D.Double(sx / n, sy / n);
}
}
/**
* Add a global attractive force pushing node at specified location toward the origin.
* @param sum vector representing the sum of forces (will be adjusted)
* @param iLoc location of first node
* @param params algorithm parameters
*/
private static void addGlobalForce(Point2D.Double sum, Point2D.Double iLoc, SpringLayoutParameters params) {
double dist = iLoc.distance(0, 0);
if (dist > params.minGlobalForceDist) {
sum.x += -params.globalC * iLoc.x / dist;
sum.y += -params.globalC * iLoc.y / dist;
}
}
/**
* Add all repulsive forces for a particular node.
* @param region the region for the node
* @param sum vector representing the sum of forces (will be adjusted)
* @param io the node of interest
* @param iLoc location of first node
* @param params algorithm parameters
*/
private static void addRepulsiveForces(LayoutRegion region, Point2D.Double sum, N io, Point2D.Double iLoc,
SpringLayoutParameters params) {
Point2D.Double jLoc;
double dist;
for (LayoutRegion r : region.adjacentRegions()) {
for (Entry jEntry : r.entries()) {
N jo = jEntry.getKey();
if (io != jo) {
jLoc = jEntry.getValue();
dist = iLoc.distance(jLoc);
// repulsive force from other nodes
if (dist < params.maxRepelDist) {
addRepulsiveForce(sum, iLoc, jLoc, dist, params);
}
}
}
}
}
/**
* Add repulsive force at node i1 pointing away from node i2.
* @param sum vector representing the sum of forces (will be adjusted)
* @param iLoc location of first node
* @param jLoc location of second node
* @param dist distance between nodes
* @param params algorithm parameters
*/
private static void addRepulsiveForce(Point2D.Double sum, Point2D.Double iLoc, Point2D.Double jLoc, double dist,
SpringLayoutParameters params) {
if (iLoc == jLoc) {
return;
}
if (dist == 0) {
double angle = Math.random()*2*Math.PI;
sum.x += params.repulsiveC * Math.cos(angle);
sum.y += params.repulsiveC * Math.sin(angle);
} else {
double multiplier = Math.min(params.repulsiveC/(dist*dist), params.maxForce) / dist;
sum.x += multiplier * (iLoc.x - jLoc.x);
sum.y += multiplier * (iLoc.y - jLoc.y);
}
}
/**
* Add symmetric attractive force from adjacencies.
* @param g the graph
* @param sum the total force for the current object
* @param io the node of interest
* @param iLoc position of node of interest
* @param params algorithm parameters
*/
private static void addSpringForces(Graph g, Point2D.Double sum, N io, Point2D.Double iLoc,
SpringLayoutState state, SpringLayoutParameters params) {
Point2D.Double jLoc;
double dist;
for (N o : g.adjacentNodes(io)) {
if (!Objects.equal(o, io)) {
jLoc = state.getLoc(o);
dist = iLoc.distance(jLoc);
addSpringForce(sum, io, iLoc, o, jLoc, dist, params);
}
}
}
/** Add spring force at node i1 pointing to node i2.
* @param sum vector representing the sum of forces (will be adjusted)
* @param io the node of interest
* @param iLoc location of first node
* @param jo the second node of interest
* @param jLoc location of second node
* @param dist distance between nodes
*/
private static void addSpringForce(Point2D.Double sum, N io, Point2D.Double iLoc, N jo, Point2D.Double jLoc,
double dist, SpringLayoutParameters params) {
if (dist == 0) {
LOG.log(Level.WARNING, "Distance 0 between {0} and {1}: {2}, {3}", new Object[]{io, jo, iLoc, jLoc});
sum.x += params.springC / (params.minDist * params.minDist);
sum.y += 0;
} else {
double displacement = dist - params.springL;
sum.x += params.springC * displacement * (jLoc.x - iLoc.x) / dist;
sum.y += params.springC * displacement * (jLoc.y - iLoc.y) / dist;
}
}
private static void checkForces(Set unpinned, Map forces) {
for (N io : unpinned) {
Point2D.Double netForce = forces.get(io);
boolean test = !Double.isNaN(netForce.x) && !Double.isNaN(netForce.y)
&& !Double.isInfinite(netForce.x) && !Double.isInfinite(netForce.y);
if (!test) {
LOG.log(Level.SEVERE, "Computed infinite force: {0} for {1}", new Object[]{netForce, io});
}
}
}
private static double move(Graph g, Set unpinned, Map forces,
SpringLayoutState state, SpringLayoutParameters params) {
double energy = 0;
for (N io : unpinned) {
energy += adjustVelocity(state.getVel(io), forces.get(io), g.degree(io), params);
}
for (N io : unpinned) {
adjustPosition(state.getLoc(io), state.getVel(io), params.stepT);
}
return energy;
}
/**
* Adjusts the velocity vector with the specified net force, possibly by applying damping. SpringLayout uses
* iVel = dampingC*(iVel + stepT*netForce), and caps maximum speed.
* @param iVel velocity to adjust
* @param netForce force vector to use
* @param iDeg node's degree, used to increase damping for high degree nodes
* @param params layout parameters
* @return node's energy
*/
private static double adjustVelocity(Point2D.Double iVel, Point2D.Double netForce, double iDeg, SpringLayoutParameters params) {
double maxForce = iDeg <= 15 ? params.maxForce : params.maxForce * (.2 + .8/(iDeg-15));
double fm = netForce.distance(0, 0);
if (fm > maxForce) {
netForce.x *= maxForce/fm;
netForce.y *= maxForce/fm;
}
iVel.x = params.dampingC * (iVel.x + params.stepT * netForce.x);
iVel.y = params.dampingC * (iVel.y + params.stepT * netForce.y);
double speed = iVel.x*iVel.x+iVel.y*iVel.y;
if (speed > params.maxSpeed) {
iVel.x *= params.maxSpeed/speed;
iVel.y *= params.maxSpeed/speed;
speed = params.maxSpeed;
}
return .5 * speed * speed;
}
/**
* Adjusts a node's position using specified initial position and velocity.
* SpringLayout uses iLoc += stepT*iVel
* @param iLoc position to change
* @param iVel velocity to adjust
* @param stepT step time
*/
private static void adjustPosition(Point2D.Double iLoc, Point2D.Double iVel, double stepT) {
iLoc.x += stepT * iVel.x;
iLoc.y += stepT * iVel.y;
}
//endregion
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy