com.googlecode.blaisemath.graph.modules.layout.SpringLayout Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of blaise-graphtheory Show documentation
Show all versions of blaise-graphtheory Show documentation
Link graph definitions, algorithms, and visualization.
The newest version!
/*
* SpringLayout.java
* Created May 13, 2010
*/
package com.googlecode.blaisemath.graph.modules.layout;
/*
* #%L
* BlaiseGraphTheory
* --
* Copyright (C) 2009 - 2016 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.googlecode.blaisemath.annotation.InvokedFromThread;
import com.googlecode.blaisemath.graph.Graph;
import com.googlecode.blaisemath.graph.GraphUtils;
import com.googlecode.blaisemath.graph.IterativeGraphLayout;
import com.googlecode.blaisemath.util.SetSelectionModel;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
*
* Simple energy-layout engine.
*
*
* The class is thread-safe. Three state objects are shared with clients...
* the set of "locked" nodes whose positions remain fixed, the set of node
* locations, and the temporary set of node locations.
* Overriding classes may adjust how forces are applied to change node positions,
* but should be cautious because iteration is applied with this object locked.
*
*
* @param node type
*
* @author Elisha Peterson
*/
@ThreadSafe
public class SpringLayout implements IterativeGraphLayout {
/** Default distance scale */
public static final int DEFAULT_DIST_SCALE = 50;
/** # of regions away from origin in x and y directions. Region size is determined by the maximum repel distance. */
private static final int REGION_N = 5;
// STATE VARIABLES
/** Algorithm parameters object */
protected Parameters parameters;
/** Nodes whose locations are fixed */
@GuardedBy("itself")
private final SetSelectionModel pinnedNodes = new SetSelectionModel();
/** Current locations */
@GuardedBy("this")
private final Map loc = Maps.newHashMap();
/** Temporary map holding positions to be updated in a future iteration. */
@GuardedBy("itself")
private final Map tempLoc = Maps.newHashMap();
/** Whether tempLoc should contain all the nodes or not */
@GuardedBy("tempLoc")
private boolean resetNodes = false;
/** Iteration number. */
private int iteration = 0;
/** Total energy. */
private double energy = 0.0;
// INTERNAL VARIABLES
/** Current velocities. */
private final Map vel = Maps.newHashMap();
/** Regions used for localizing computation */
private Region[][] regions;
/** Points that are not in a region */
private Region oRegion;
/**
* Construct without starting locations.
*/
public SpringLayout() {
this(Collections.EMPTY_MAP, null);
}
/**
* Construct using specified starting locations.
* @param positions starting positions for spring layout
*/
public SpringLayout(Map positions) {
this(positions, null);
}
/**
* Construct using specified starting locations and parameters.
* @param positions starting positions for spring layout
* @param params starting parameters
*/
public SpringLayout(Map positions, @Nullable Parameters params) {
this.parameters = params == null ? new Parameters() : params;
for (C v : positions.keySet()) {
loc.put(v, positions.get(v) == null ? new Point2D.Double() : positions.get(v));
vel.put(v, new Point2D.Double());
}
iteration = 0;
}
//
//
// PROPERTY PATTERNS
//
/**
* Get the layout parameters object.
* @return parameters
*/
public Parameters getParameters() {
return parameters;
}
public void setParameters(Parameters parameters) {
this.parameters = parameters;
}
@Override
public double getCoolingParameter() {
return parameters.dampingC;
}
@Override
public void setCoolingParameter(double val) {
parameters.dampingC = val;
}
@Override
public double getEnergyStatus() {
return energy;
}
@Override
public int getIteration() {
return iteration;
}
public Set getLockedNodes() {
synchronized (pinnedNodes) {
return pinnedNodes.getSelection();
}
}
public void setLockedNodes(Set nodes) {
synchronized (pinnedNodes) {
pinnedNodes.setSelection(nodes);
}
}
//
//
public Map getPositionsCopy() {
synchronized (this) {
return Maps.newHashMap(loc);
}
}
public void requestPositions(Map positions, boolean resetNodes) {
synchronized (tempLoc) {
tempLoc.putAll(positions);
this.resetNodes = resetNodes;
}
}
//
//
/**
* This method returns a position for a node that doesn't currently have a position.
* @param node the node to get new location of
*/
private Point2D.Double newNodeLocation(Graph g, Object node) {
double len = parameters.springL;
double sx = 0;
double sy = 0;
int n = 0;
for (Object o : g.neighbors(node)) {
Point2D.Double p = loc.get(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);
}
}
/**
* This method is called before each iteration to see if node locations have
* been updated by a mechanism other than this layout class.
* @param nodes the nodes to check for updates
*/
private void checkForNodeUpdate(Set nodes) {
synchronized (tempLoc) {
for (Entry en : tempLoc.entrySet()) {
C n = en.getKey();
if (nodes.contains(n)) {
loc.put(n, en.getValue());
if (vel.containsKey(n)) {
vel.get(n).setLocation(0, 0);
} else {
vel.put(en.getKey(), new Point2D.Double());
}
}
}
if (resetNodes) {
Set keep = new HashSet(nodes);
keep.addAll(tempLoc.keySet());
loc.keySet().retainAll(keep);
vel.keySet().retainAll(keep);
}
resetNodes = false;
tempLoc.clear();
}
for (C v : nodes) {
if (!loc.containsKey(v)) {
loc.put(v, new Point2D.Double());
vel.put(v, new Point2D.Double());
}
}
}
@InvokedFromThread("unknown")
public final void iterate(Graph og) {
Graph g = og.isDirected() ? GraphUtils.copyAsUndirectedSparseGraph(og) : og;
Set nodes = g.nodes();
Set pinned;
synchronized (pinnedNodes) {
pinned = pinnedNodes.getSelection();
}
Set unpinned = Sets.difference(nodes, pinned).immutableCopy();
synchronized (this) {
// check for temporary location updates
loc.keySet().retainAll(nodes);
checkForNodeUpdate(nodes);
updateRegions();
energy = 0;
Map forces = Maps.newHashMap();
computeNonRepulsiveForces(g, nodes, pinned, unpinned, forces);
computeRepulsiveForces(g, pinned, forces);
checkForces(unpinned, forces);
move(g, unpinned, forces);
iteration++;
}
}
private void computeNonRepulsiveForces(Graph g, Set nodes, Set pinned, Set unpinned, Map forces) {
for (C io : nodes) {
Point2D.Double iLoc = loc.get(io);
if (iLoc == null) {
iLoc = newNodeLocation(g, io);
loc.put(io, iLoc);
}
Point2D.Double iVel = vel.get(io);
if (iVel == null) {
iVel = new Point2D.Double();
vel.put(io, iVel);
}
if (!pinned.contains(io)) {
Point2D.Double netForce = new Point2D.Double();
addGlobalForce(netForce, io, iLoc);
addSpringForces(g, netForce, io, iLoc);
addAdditionalForces(g, netForce, io, iLoc);
forces.put(io, netForce);
}
}
}
private void computeRepulsiveForces(Graph g, Set pinned, Map forces) {
for (Region[] rr : regions) {
for (Region r : rr) {
for (Object io : r.pts.keySet()) {
if (!pinned.contains(io)) {
addRepulsiveForces(g, r, forces.get(io), io, r.pts.get(io));
}
}
}
}
for (Object io : oRegion.pts.keySet()) {
if (!pinned.contains(io)) {
addRepulsiveForces(g, oRegion, forces.get(io), io, oRegion.pts.get(io));
}
}
}
private void checkForces(Set unpinned, Map forces) {
for (C 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) {
Logger.getLogger(SpringLayout.class.getName()).log(Level.SEVERE,
"Computed infinite force: {0} for {1}", new Object[]{netForce, io});
}
}
}
private void move(Graph g, Set unpinned, Map forces) {
// adjusts velocity with damping;
for (C io : unpinned) {
int deg = g.degree(io);
double maxForce = deg <= 15 ? parameters.maxForce
: parameters.maxForce * (.2 + .8/(deg-15));
adjustVelocity(vel.get(io), forces.get(io), maxForce);
}
// move nodes
for (C io : unpinned) {
adjustPosition(loc.get(io), vel.get(io));
}
}
//
//
//
// UTILITIES
//
/**
* Adds a global attractive force pushing vertex at specified location toward the origin
* @param sum vector representing the sum of forces (will be adjusted)
* @param io the node of interest
* @param iLoc location of first vertex
*/
protected void addGlobalForce(Point2D.Double sum, Object io, Point2D.Double iLoc) {
double dist = iLoc.distance(0,0);
if (dist > parameters.minGlobalForceDist) {
sum.x += -parameters.globalC * iLoc.x / dist;
sum.y += -parameters.globalC * iLoc.y / dist;
}
}
/**
* Adds all repulsive forces for a particular vertex.
* @param g the graph
* @param ireg 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 vertex
*/
protected void addRepulsiveForces(Graph g, Region ireg, Point2D.Double sum, Object io, Point2D.Double iLoc) {
Point2D.Double jLoc;
double dist;
for (Region r : ireg.adj) {
for (Entry
//
/** Return region for specified point */
private Region getRegion(Point2D.Double p) {
int ix = (int) ((p.x + REGION_N * parameters.maxRepelDist) / parameters.maxRepelDist);
int iy = (int) ((p.y + REGION_N * parameters.maxRepelDist) / parameters.maxRepelDist);
if (ix < 0 || ix > 2*REGION_N || iy < 0 || iy > 2*REGION_N) {
return oRegion;
}
return regions[ix][iy];
}
/** Generates the regions */
private void updateRegions() {
if (regions == null) {
regions = new Region[2*REGION_N+1][2*REGION_N+1];
for (int ix = -REGION_N; ix <= REGION_N; ix++) {
for (int iy = -REGION_N; iy <= REGION_N; iy++) {
regions[ix+REGION_N][iy+REGION_N] = new Region(new Rectangle2D.Double(
ix*parameters.maxRepelDist,iy*parameters.maxRepelDist,
parameters.maxRepelDist,parameters.maxRepelDist));
}
}
// set up adjacencies
for (int ix = -REGION_N; ix <= REGION_N; ix++) {
for (int iy = -REGION_N; iy <= REGION_N; iy++) {
for (int ix2 = Math.max(ix-1,-REGION_N); ix2 <= Math.min(ix+1, REGION_N); ix2++) {
for (int iy2 = Math.max(iy-1,-REGION_N); iy2 <= Math.min(iy+1, REGION_N); iy2++) {
regions[ix+REGION_N][iy+REGION_N].adj.add(regions[ix2+REGION_N][iy2+REGION_N]);
}
}
}
}
// set up adjacencies with outer region
oRegion = new Region(null);
oRegion.adj.add(oRegion);
for (int ix = -REGION_N; ix <= REGION_N; ix++) {
Region min = regions[ix+REGION_N][0];
Region max = regions[ix+REGION_N][2*REGION_N];
min.adj.add(oRegion);
max.adj.add(oRegion);
oRegion.adj.addAll(Arrays.asList(min, max));
}
for (int iy = -REGION_N+1; iy <= REGION_N-1; iy++) {
Region min = regions[0][iy+REGION_N];
Region max = regions[2*REGION_N][iy+REGION_N];
min.adj.add(oRegion);
max.adj.add(oRegion);
oRegion.adj.addAll(Arrays.asList(min, max));
}
}
// refresh points
for (int ix = -REGION_N; ix <= REGION_N; ix++) {
for (int iy = -REGION_N; iy <= REGION_N; iy++) {
regions[ix+REGION_N][iy+REGION_N].pts.clear();
}
}
oRegion.pts.clear();
for (Entry en : loc.entrySet()) {
Region r = getRegion(en.getValue());
if (r != null) {
r.pts.put(en.getKey(), en.getValue());
} else {
Logger.getLogger(SpringLayout.class.getName()).log(Level.WARNING,
"Point not in any region: {0}", en);
}
}
}
//
//
/** Parameters of the SpringLayout algorithm */
public static class Parameters {
/** Desired distance between nodes */
double distScale = DEFAULT_DIST_SCALE;
/** Global attractive constant (keeps vertices closer to origin) */
double globalC = 1;
/** Attractive constant */
double springC = .1;
/** Natural spring length */
double springL = .5*distScale;
/** Repelling constant */
double repulsiveC = distScale*distScale;
/**
* Damping constant (the "cooling" parameter. Smaller values will make
* movements less "jumpy".
*/
double dampingC = 0.7;
/** Time step per iteration */
double stepT = 1;
/** The maximum speed (movement per unit time) */
double maxSpeed = 10*distScale;
/** Distance outside which global force acts */
private double minGlobalForceDist = distScale;
/** Maximum force that can be applied between nodes */
private double maxForce = distScale*distScale/100;
/** Min distance between nodes */
private double minDist = distScale/100;
/** Max distance to apply repulsive force */
private double maxRepelDist = 2*distScale;
//
//
// PROPERTIES
//
public double getDistScale() {
return distScale;
}
public void setDistScale(double distScale) {
this.distScale = distScale;
springL = .5*distScale;
repulsiveC = distScale*distScale;
maxSpeed = 10*distScale;
minGlobalForceDist = distScale;
maxForce = distScale*distScale/100;
minDist = distScale/100;
maxRepelDist = 2*distScale;
}
public double getDampingConstant() {
return dampingC;
}
public void setDampingConstant(double dampingC) {
this.dampingC = dampingC;
}
public double getGlobalForce() {
return globalC;
}
public void setGlobalForce(double globalC) {
this.globalC = globalC;
}
public double getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = maxSpeed;
}
public double getRepulsiveForce() {
return repulsiveC;
}
public void setRepulsiveForce(double repulsiveC) {
this.repulsiveC = repulsiveC;
}
public double getSpringForce() {
return springC;
}
public void setSpringForce(double springC) {
this.springC = springC;
}
public double getSpringLength() {
return springL;
}
public void setSpringLength(double springL) {
this.springL = springL;
}
public double getStepTime() {
return stepT;
}
public void setStepTime(double stepT) {
this.stepT = stepT;
}
//
}
/** Describes a region with a subset of the graph's nodes */
private static class Region {
Rectangle2D.Double bounds;
HashMap
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy