All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jungrapht.visualization.layout.algorithms.GEMLayoutAlgorithm Maven / Gradle / Ivy

The newest version!
package org.jungrapht.visualization.layout.algorithms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.Set;
import org.jgrapht.Graph;
import org.jgrapht.Graphs;
import org.jgrapht.graph.builder.GraphTypeBuilder;
import org.jungrapht.visualization.layout.algorithms.util.IterativeContext;
import org.jungrapht.visualization.layout.algorithms.util.Pair;
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;

/**
 * Java implementation of the gem 2D layout. 
* The algorithm needs to get various subgraphs and traversals. The recursive nature of the * algorithm is totally captured within those subgraphs and traversals. The main loop of the * algorithm is then expressed using the iterator feature, which makes it look like a simple flat * iteration over nodes. * * @author David Duke * @author Hacked by Eytan Adar for Guess * @author Hacked by taubertj for OVTK2 * @author Hacked by Tom Nelson */ public class GEMLayoutAlgorithm extends AbstractIterativeLayoutAlgorithm implements IterativeContext { private static final Logger log = LoggerFactory.getLogger(GEMLayoutAlgorithm.class); public static class Builder< V, E, T extends GEMLayoutAlgorithm, B extends Builder> extends AbstractIterativeLayoutAlgorithm.Builder implements LayoutAlgorithm.Builder { private int maxIterations = 700; private int multi = 3; private int verticalSpacing = 75; private int horizontalSpacing = 75; private boolean clustered = true; protected boolean adjustToFit = true; public B multi(int multi) { this.multi = multi; return self(); } public B maxIterations(int maxIterations) { this.maxIterations = maxIterations; return self(); } public B verticalSpacing(int verticalSpacing) { this.verticalSpacing = verticalSpacing; return self(); } public B horizontalSpacing(int horizontalSpacing) { this.horizontalSpacing = horizontalSpacing; return self(); } public B clustered(boolean clustered) { this.clustered = clustered; return self(); } /** * @param adjustToFit adjust the points to fit in the layoutModel area * @return the Builder */ public B adjustToFit(boolean adjustToFit) { this.adjustToFit = adjustToFit; return self(); } public T build() { return (T) new GEMLayoutAlgorithm(this); } } public static Builder edgeAwareBuilder() { return new Builder<>(); } public GEMLayoutAlgorithm() { this(GEMLayoutAlgorithm.edgeAwareBuilder()); } protected GEMLayoutAlgorithm(Builder builder) { super(builder); this.maxIterations = builder.maxIterations; this.multi = builder.multi; this.horizontalSpacing = builder.horizontalSpacing; this.verticalSpacing = builder.verticalSpacing; this.clustered = builder.clustered; this.adjustToFit = builder.adjustToFit; } /** * Class containing properties per node. * * @author taubertj */ private static class Properties { public int x, y; // position public int in; public int iX, iY; // impulse public float dir; // direction public float heat; // heat public float mass; // weight = nr edges public boolean mark; public Properties(int m) { x = y = 0; iX = iY = 0; dir = 0.0f; heat = 0; mass = m; mark = false; } } private boolean done; /** If the process gets cancelled */ private boolean cancelled = false; // number of nodes in the graph private int nodeCount; // use clustered approach private boolean clustered; // number of clusters private int nbClusters = 1; // vertical cluster spacing public int verticalSpacing; // horizontal cluster spacing public int horizontalSpacing; // multiplicator of largest cluster public int multi; protected int maxIterations; // // GEM Constants // private int ELEN = 128; private int ELENSQR = ELEN * ELEN; private int MAXATTRACT = 1048576; // // GEM variables // private long iteration; private long temperature; private int centerX, centerY; private long maxtemp; private float oscillation, rotation; protected boolean adjustToFit; // // GEM Default Parameter Values // private float i_maxtemp = 1.0f; private float a_maxtemp = 1.5f; private float o_maxtemp = 0.25f; private float i_starttemp = 0.3f; private float a_starttemp = 1.0f; private float o_starttemp = 1.0f; private float i_finaltemp = 0.05f; private float a_finaltemp = 0.02f; private float o_finaltemp = 1.0f; private int i_maxiter = 10; private int a_maxiter = 3; private int o_maxiter = 3; private float i_gravity = 0.05f; private float i_oscillation = 0.4f; private float i_rotation = 0.5f; private float i_shake = 0.2f; private float a_gravity = 0.1f; private float a_oscillation = 0.4f; private float a_rotation = 0.9f; private float a_shake = 0.3f; private float o_gravity = 0.1f; private float o_oscillation = 0.4f; private float o_rotation = 0.9f; private float o_shake = 0.3f; long stop_temperature; long stop_iteration; // list of properties for each node private Properties[] gemProp; // inverse map from int id to V private V[] invmap; // adjacent int ids for a given V int id private Map> adjacent; // map from V to int id private Map nodeNumbers; // randomizer used for node selection private Random rand = new Random(); // map used for current random set of nodes private int[] map; // priority queue for BFS private Queue q; private Graph graph; public void visit(LayoutModel layoutModel) { super.visit(layoutModel); this.graph = layoutModel.getGraph(); if (graph == null || graph.vertexSet().isEmpty()) { return; } this.initialize(); this.arrange(); if (adjustToFit) { expandToFill(layoutModel); // adjustToFit(); } Rectangle range = computeLayoutExtent(layoutModel); // add padding of 5% of width and height int widthPadding = (int) (range.width * 0.05); int heightPadding = (int) (range.height * 0.05); range = Rectangle.from( range.min().add(-widthPadding, -heightPadding), range.max().add(widthPadding, heightPadding)); // offset all the vertex points by widthPadding and heightPadding graph .vertexSet() .forEach(v -> layoutModel.set(v, layoutModel.apply(v).add(widthPadding, heightPadding))); int maxDimension = Math.max((int) range.width, (int) range.height); layoutModel.setSize(maxDimension, maxDimension); } private Rectangle getMaxBounds() { int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int maxY = Integer.MIN_VALUE; for (Point p : layoutModel.getLocations().values()) { if (p.x < minX) minX = (int) p.x; if (p.y < minY) minY = (int) p.y; if (p.x > maxX) maxX = (int) p.x; if (p.y > maxY) maxY = (int) p.y; } minX -= horizontalSpacing; minY -= verticalSpacing; maxX += horizontalSpacing; maxY += verticalSpacing; return Rectangle.of(minX, minY, maxX - minX, maxY - minY); } private void adjustToFit() { Rectangle bounds = getMaxBounds(); double boundsWidth = bounds.width; double boundsHeight = bounds.height; int layoutWidth = layoutModel.getWidth(); int layoutHeight = layoutModel.getHeight(); double scaleX = layoutWidth / boundsWidth; double scaleY = layoutHeight / boundsHeight; for (V v : graph.vertexSet()) { Point vp = layoutModel.apply(v); vp = Point.of(vp.x * scaleX, vp.y * scaleY); vp = vp.add(horizontalSpacing, verticalSpacing); layoutModel.set(v, vp); } } private Graph getGraph() { return this.graph; } public synchronized void step() { if (temperature > stop_temperature && iteration < stop_iteration && !cancelled) { log.trace("iteration: {}", iteration); a_round(); } else { this.done = true; } } @Override public boolean done() { if (cancelled) return true; if (done) { runAfter(); } return done; } private void a_round() { Iterator nodeSet; int v; int iX, iY, dX, dY; int n; int pX, pY; Properties p, q; for (int i = 0; i < nodeCount; i++) { v = select(); p = gemProp[v]; pX = p.x; pY = p.y; n = (int) (a_shake * ELEN); iX = rand() % (2 * n + 1) - n; iY = rand() % (2 * n + 1) - n; iX += (centerX / nodeCount - pX) * p.mass * a_gravity; iY += (centerY / nodeCount - pY) * p.mass * a_gravity; for (int u = 0; u < nodeCount; u++) { q = gemProp[u]; dX = pX - q.x; dY = pY - q.y; n = dX * dX + dY * dY; if (n > 0) { iX += dX * ELENSQR / n; iY += dY * ELENSQR / n; } } nodeSet = adjacent.get(v).iterator(); int u; while (nodeSet.hasNext()) { u = nodeSet.next(); q = gemProp[u]; dX = pX - q.x; dY = pY - q.y; n = (int) ((dX * dX + dY * dY) / p.mass); n = Math.min(n, MAXATTRACT); iX -= dX * n / ELENSQR; iY -= dY * n / ELENSQR; } displace(v, iX, iY); iteration++; } } private void arrange() { vertexdata_init(a_starttemp); oscillation = a_oscillation; rotation = a_rotation; maxtemp = (int) (a_maxtemp * ELEN); stop_temperature = (int) (a_finaltemp * a_finaltemp * ELENSQR * nodeCount); stop_iteration = a_maxiter * nodeCount * nodeCount; iteration = 0; } /** * Performs a BFS on the graph * * @param root int * @return node id */ private int bfs(int root) { Iterator nodeSet; int v, ui; if (root >= 0) { q = new LinkedList<>(); if (!gemProp[root].mark) { // root > 0 for (int vi = 0; vi < nodeCount; vi++) { gemProp[vi].in = 0; } } else gemProp[root].mark = true; // root = -root; q.add(root); gemProp[root].in = 1; } if (q.size() == 0) return -1; v = q.poll(); nodeSet = adjacent.get(v).iterator(); while (nodeSet.hasNext()) { ui = nodeSet.next(); if (gemProp[ui].in != 0) { q.add(ui); gemProp[ui].in = gemProp[v].in + 1; } } return v; } /** * Calculates actual bounds of a painted graph. * * @return min/max coordinates in a Point[] */ private Point[] calcBounds(Graph graph, Map coords) { Point[] result = new Point[2]; Point min = null; Point max = null; Iterator it = graph.vertexSet().iterator(); while (it.hasNext()) { Point point = coords.get(it.next()); if (min == null) { min = point; } if (max == null) { max = point; } min = Point.of(Math.min(min.x, point.x), Math.min(min.y, point.y)); max = Point.of(Math.max(max.x, point.x), Math.max(max.y, point.y)); } result[0] = min; result[1] = max; return result; } /** Clusters given graph into subgraphs. */ public Set> clusterGraph(Graph original) { // contains all possible subgraphs Set> subgraphs = new HashSet<>(); // sort each vertex into one subgraph Set sorted = new HashSet(); for (V n : original.vertexSet()) { // Orphan node if (!sorted.contains(n)) { // create new cluster starting at this node Graph cluster = GraphTypeBuilder.undirected() .allowingSelfLoops(true) .allowingMultipleEdges(true) .buildGraph(); subgraphs.add(cluster); // add node to new cluster and mark as sorted cluster.addVertex(n); sorted.add(n); // inspect neighbours of n do BFS Queue queue = new LinkedList(); Collection neigbours = Graphs.neighborListOf(original, n); queue.addAll(neigbours); // process queue while (!queue.isEmpty()) { V next = queue.poll(); if (!sorted.contains(next)) { // add to cluster and mark as sorted cluster.addVertex(next); sorted.add(next); // add edges to cluster Collection nextEdges = original.edgesOf(next); for (E edge : nextEdges) { cluster.addVertex(original.getEdgeSource(edge)); cluster.addVertex(original.getEdgeTarget(edge)); cluster.addEdge(original.getEdgeSource(edge), original.getEdgeTarget(edge), edge); } // proceed to next level queue.addAll(Graphs.neighborListOf(original, next)); } } } if (cancelled) return subgraphs; } return subgraphs; } private void displace(int v, int iX, int iY) { int t; int n; Properties p; if (iX != 0 || iY != 0) { n = Math.max(Math.abs(iX), Math.abs(iY)) / 16384; if (n > 1) { iX /= n; iY /= n; } p = gemProp[v]; t = (int) p.heat; n = (int) Math.sqrt(iX * iX + iY * iY); iX = iX * t / n; iY = iY * t / n; p.x += iX; p.y += iY; centerX += iX; centerY += iY; // imp = &vi[v].imp; n = t * (int) Math.sqrt(p.iX * p.iX + p.iY * p.iY); if (n > 0) { temperature -= t * t; t += t * oscillation * (iX * p.iX + iY * p.iY) / n; t = (int) Math.min(t, maxtemp); p.dir += rotation * (iX * p.iY - iY * p.iX) / n; t -= t * Math.abs(p.dir) / nodeCount; t = Math.max(t, 2); temperature += t * t; p.heat = t; } p.iX = iX; p.iY = iY; } } /* * Optimisation Code */ private int[] EVdistance(int thisNode, int thatNode, int v) { Properties thisGP = gemProp[thisNode]; Properties thatGP = gemProp[thatNode]; Properties nodeGP = gemProp[v]; int aX = thisGP.x; int aY = thisGP.y; int bX = thatGP.x; int bY = thatGP.y; int cX = nodeGP.x; int cY = nodeGP.y; long m, n; bX -= aX; bY -= aY; /* b' = b - a */ m = bX * (cX - aX) + bY * (cY - aY); /* m = = */ n = bX * bX + bY * bY; /* n = |b'|^2 = |b-a|^2 */ if (m < 0) m = 0; if (m > n) m = n = 1; if ((m >> 17) > 0) { /* prevent integer overflow */ n /= m >> 16; m /= m >> 16; } if (n != 0) { aX += (int) (bX * m / n); /* a' = m/n b' = a + m/n (b-a) */ aY += (int) (bY * m / n); } return new int[] {aX, aY}; } /** * Returns node for the graph center. * * @return int */ private int graphCenter() { Properties p; int c, u, v, w; // nodes int h; c = -1; // for a contented compiler. u = -1; h = nodeCount + 1; for (w = 0; w < nodeCount; w++) { v = bfs(w); while (v >= 0 && gemProp[v].in < h) { u = v; v = bfs(-1); // null } p = gemProp[u]; if (p.in < h) { h = p.in; c = w; } } // randomly choose a centre node if graph doesn't have a centre if (c == -1) return (int) Math.rint((nodeCount - 1) * Math.random()); return c; } /* * INSERT code from GEM */ private int[] i_impulse(int v) { Iterator nodeSet; int iX, iY, dX, dY, pX, pY; int n; Properties p, q; p = gemProp[v]; pX = p.x; pY = p.y; n = (int) (i_shake * ELEN); iX = rand() % (2 * n + 1) - n; iY = rand() % (2 * n + 1) - n; iX += (centerX / nodeCount - pX) * p.mass * i_gravity; iY += (centerY / nodeCount - pY) * p.mass * i_gravity; for (int u = 0; u < nodeCount; u++) { q = gemProp[u]; if (q.in > 0) { dX = pX - q.x; dY = pY - q.y; n = dX * dX + dY * dY; if (n > 0) { iX += dX * ELENSQR / n; iY += dY * ELENSQR / n; } } } nodeSet = adjacent.get(v).iterator(); int u; while (nodeSet.hasNext()) { u = nodeSet.next(); q = gemProp[u]; if (q.in > 0) { dX = pX - q.x; dY = pY - q.y; n = (int) ((dX * dX + dY * dY) / p.mass); n = Math.min(n, MAXATTRACT); iX -= dX * n / ELENSQR; iY -= dY * n / ELENSQR; } } return new int[] {iX, iY}; } /** Runs the layout. */ public void initialize() { cancelled = false; if (clustered) { Set> clusters = clusterGraph(getGraph()); nbClusters = clusters.size(); runClustered(clusters); } else { runNormal(getGraph()); // set location of nodes in graph for (int i = 0; i < nodeCount && !cancelled; i++) { Properties p = gemProp[i]; V n = invmap[i]; layoutModel.set(n, p.x, p.y); } } } private void insert() { Iterator nodeSet; Properties p, q; int startNode; int v, w; int d; vertexdata_init(i_starttemp); oscillation = i_oscillation; rotation = i_rotation; maxtemp = (int) (i_maxtemp * ELEN); v = graphCenter(); for (int ui = 0; ui < nodeCount; ui++) { gemProp[ui].in = 0; } gemProp[v].in = -1; startNode = -1; for (int i = 0; i < nodeCount; i++) { d = 0; for (int u = 0; u < nodeCount; u++) { if (gemProp[u].in < d) { d = gemProp[u].in; v = u; } } gemProp[v].in = 1; nodeSet = adjacent.get(v).iterator(); int u; while (nodeSet.hasNext()) { u = nodeSet.next(); if (gemProp[u].in <= 0) gemProp[u].in--; } p = gemProp[v]; p.x = p.y = 0; if (startNode >= 0) { d = 0; p = gemProp[v]; nodeSet = adjacent.get(v).iterator(); while (nodeSet.hasNext()) { w = nodeSet.next(); q = gemProp[w]; if (q.in > 0) { p.x += q.x; p.y += q.y; d++; } } if (d > 1) { p.x /= d; p.y /= d; } d = 0; while ((d++ < i_maxiter) && (p.heat > i_finaltemp * ELEN)) { int[] i_impulse = i_impulse(v); displace(v, i_impulse[0], i_impulse[1]); } } else { startNode = i; } if (cancelled) return; } } private int[] o_impulse(Graph graph, int v) { int u, w; int iX, iY, dX, dY; int n; Properties p, up, wp; int pX, pY; p = gemProp[v]; pX = p.x; pY = p.y; n = (int) (o_shake * ELEN); iX = rand() % (2 * n + 1) - n; iY = rand() % (2 * n + 1) - n; iX += (centerX / nodeCount - pX) * p.mass * o_gravity; iY += (centerY / nodeCount - pY) * p.mass * o_gravity; for (E e : graph.edgeSet()) { Pair ends = Pair.of(graph.getEdgeSource(e), graph.getEdgeTarget(e)); u = nodeNumbers.get(ends.first); w = nodeNumbers.get(ends.second); if (u != v && w != v) { up = gemProp[u]; wp = gemProp[w]; dX = (up.x + wp.x) / 2 - pX; dY = (up.y + wp.y) / 2 - pY; n = dX * dX + dY * dY; if (n < 8 * ELENSQR) { int[] evdist = EVdistance(u, w, v); // source, dest, vert dX = evdist[0]; dY = evdist[1]; dX -= pX; dY -= pY; n = dX * dX + dY * dY; } if (n > 0) { iX -= dX * ELENSQR / n; iY -= dY * ELENSQR / n; } } else { if (u == v) u = w; up = gemProp[u]; dX = pX - up.x; dY = pY - up.y; n = (int) ((dX * dX + dY * dY) / p.mass); n = Math.min(n, MAXATTRACT); iX -= dX * n / ELENSQR; iY -= dY * n / ELENSQR; } } return new int[] {iX, iY}; } private void o_round(Graph graph) { int v; for (int i = 0; i < nodeCount; i++) { v = select(); int[] o_impulse = o_impulse(graph, v); displace(v, o_impulse[0], o_impulse[1]); iteration++; } } private void optimize(Graph graph) { long stop_temperature; long stop_iteration; vertexdata_init(o_starttemp); oscillation = o_oscillation; rotation = o_rotation; maxtemp = (int) (o_maxtemp * ELEN); stop_temperature = (int) (o_finaltemp * o_finaltemp * ELENSQR * nodeCount); stop_iteration = o_maxiter * nodeCount * nodeCount; while (temperature > stop_temperature && iteration < stop_iteration) { o_round(graph); if (cancelled) return; } } /** * Random function returns an random int value. * * @return int */ private int rand() { return (int) (rand.nextDouble() * Integer.MAX_VALUE); } /** Layout subgraphs on separate places. */ public void runClustered(Set> subgraphs) { // sort subgraphs according to size Graph[] sortedSubgraphs = subgraphs.toArray(new Graph[0]); Arrays.sort(sortedSubgraphs, Comparator.comparingInt(g -> g.vertexSet().size())); // cache local layout Map, Map> localLayouts = new HashMap<>(); int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE; // apply algorithm to sorted graphs int j = 0; for (Graph subgraph : sortedSubgraphs) { j++; // set subgraph as normal and run GEM layout on it runNormal((Graph) subgraph); // set location of nodes in subgraph localLayouts.put((Graph) subgraph, new HashMap<>()); for (int i = 0; i < nodeCount; i++) { Properties p = gemProp[i]; V n = invmap[i]; Point coord = Point.of(p.x, p.y); layoutModel.set(n, coord); if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; localLayouts.get(subgraph).put(n, coord); } if (cancelled) return; } int width = (Math.abs(minX) + Math.abs(maxX)) * multi; double offsetX = 0; double offsetY = 0; double maxposY = 0; for (Graph sub : sortedSubgraphs) { Graph subgraph = (Graph) sub; Map coords = localLayouts.get(subgraph); // calculate bounds required for normalisation Point[] result = calcBounds(subgraph, coords); Point min = result[0]; // current expansion double tmpY = 0; double tmpX = 0; // offset all nodes of local layout Iterator> entries = coords.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); Point coord = entry.getValue(); // centre at 0,0 and offset double newX = offsetX + coord.x - min.x; double newY = offsetY + coord.y - min.y; // calculate maximum boundaries if (newX > tmpX) tmpX = newX; if (newY > tmpY) tmpY = newY; localLayouts.get(subgraph).put(entry.getKey(), Point.of(newX, newY)); layoutModel.set(entry.getKey(), Point.of(newX, newY)); } // shift horizontally keep track of vertical offsetX = tmpX + horizontalSpacing; if (tmpY > maxposY) { maxposY = tmpY; } // line break here if (offsetX > width) { offsetY = maxposY + verticalSpacing; offsetX = 0; maxposY = 0; } } } /** Normal bubble like GEM layout. */ private void runNormal(Graph graph) { Collection nodes = graph.vertexSet(); nodeCount = nodes.size(); // ignore empty graphs if (nodeCount == 0) return; gemProp = new Properties[nodeCount]; invmap = (V[]) new Object[nodeCount]; adjacent = new HashMap>(nodeCount); nodeNumbers = new HashMap(); // initialize node lists and gemProp Iterator nodeSet = nodes.iterator(); for (int i = 0; nodeSet.hasNext(); i++) { V n = nodeSet.next(); gemProp[i] = new Properties(graph.outgoingEdgesOf(n).size()); invmap[i] = n; nodeNumbers.put(n, i); } // fill adjacent lists Collection neighbors; for (int i = 0; i < nodeCount; i++) { neighbors = Graphs.neighborListOf(graph, invmap[i]); adjacent.put(i, new ArrayList(neighbors.size())); for (V n : neighbors) { adjacent.get(i).add(nodeNumbers.get(n)); } } if (cancelled) return; // actual layout if (i_finaltemp < i_starttemp) { insert(); if (cancelled) return; } if (a_finaltemp < a_starttemp) { arrange(); if (cancelled) return; } if (o_finaltemp < o_starttemp) { optimize(graph); if (cancelled) return; } } /** * Randomize selection of nodes. * * @return node id */ private int select() { int u; int n, v; if (iteration == 0) { map = new int[nodeCount]; for (int i = 0; i < nodeCount; i++) map[i] = i; } n = (int) (nodeCount - iteration % nodeCount); v = rand() % n; // was 1 + rand() % n due to numbering in GEM if (v == nodeCount) v--; if (n == nodeCount) n--; u = map[v]; map[v] = map[n]; map[n] = u; return u; } /** * Initialize properties of nodes. * * @param starttemp given start temperature */ private void vertexdata_init(float starttemp) { temperature = 0; centerX = centerY = 0; for (int v = 0; v < nodeCount; v++) { Properties p = gemProp[v]; p.heat = starttemp * ELEN; temperature += p.heat * p.heat; p.iX = p.iY = 0; p.dir = 0; p.mass = 1 + gemProp[v].mass / 3; centerX += p.x; centerY += p.y; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy