Java tutorial
/** * StaticSpringLayout.java * Created Feb 6, 2011 */ package com.googlecode.blaisemath.graph.modules.layout; /* * #%L * BlaiseGraphTheory * -- * Copyright (C) 2009 - 2015 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.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; import com.googlecode.blaisemath.graph.Graph; import com.googlecode.blaisemath.graph.GraphUtils; import com.googlecode.blaisemath.graph.OptimizedGraph; import com.googlecode.blaisemath.graph.StaticGraphLayout; import com.googlecode.blaisemath.util.Edge; import com.googlecode.blaisemath.util.Points; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Positions nodes in a graph using a force-based layout technique. * @author elisha */ public class StaticSpringLayout implements StaticGraphLayout<Double> { private double distScale = SpringLayout.DEFAULT_DIST_SCALE; private int minSteps = 100; private int maxSteps = 5000; private double coolStart = 0.65; private double coolEnd = 0.1; private double energyChangeThreshold = distScale * distScale * 1e-6; private int lastStepCount = 0; public StaticSpringLayout() { } @Override public String toString() { return "StaticSpringLayout"; } @Override public Class<Double> getParametersType() { return Double.class; } //<editor-fold defaultstate="collapsed" desc="PROPERTIES"> // // PROPERTIES // public double getDistScale() { return distScale; } public void setDistScale(double distScale) { this.distScale = distScale; this.energyChangeThreshold = distScale * distScale * 1e-6; } public int getMinSteps() { return minSteps; } public void setMinSteps(int minSteps) { this.minSteps = minSteps; } public int getMaxSteps() { return maxSteps; } public void setMaxSteps(int maxSteps) { this.maxSteps = maxSteps; } public double getEnergyChangeThreshold() { return energyChangeThreshold; } public void setEnergyChangeThreshold(double energyChangeThreshold) { this.energyChangeThreshold = energyChangeThreshold; } public double getCoolStart() { return coolStart; } public void setCoolStart(double coolStart) { this.coolStart = coolStart; } public double getCoolEnd() { return coolEnd; } public void setCoolEnd(double coolEnd) { this.coolEnd = coolEnd; } public int getLastStepCount() { return lastStepCount; } //</editor-fold> @Override public <C> Map<C, Point2D.Double> layout(Graph<C> originalGraph, Map<C, Point2D.Double> ic, Set<C> fixed, Double irad) { Logger.getLogger(StaticSpringLayout.class.getName()).log(Level.INFO, "originalGraph, |V|={0}, |E|={1}, #components={2}, degrees={3}\n", new Object[] { originalGraph.nodeCount(), originalGraph.edgeCount(), GraphUtils.components(originalGraph).size(), nicer(GraphUtils.degreeDistribution(originalGraph)) }); // reduce graph size for layout OptimizedGraph graphForInfo = new OptimizedGraph(false, originalGraph.nodes(), originalGraph.edges()); final Set<C> keepNodes = Sets.newHashSet(graphForInfo.getConnectorNodes()); keepNodes.addAll(graphForInfo.getCoreNodes()); Iterable<Edge<C>> keepEdges = Iterables.filter(graphForInfo.edges(), new Predicate<Edge<C>>() { @Override public boolean apply(Edge<C> input) { return keepNodes.contains(input.getNode1()) && keepNodes.contains(input.getNode2()); } }); OptimizedGraph<C> graphForLayout = new OptimizedGraph<C>(false, keepNodes, keepEdges); Logger.getLogger(StaticSpringLayout.class.getName()).log(Level.INFO, "graphForLayout, |V|={0}, |E|={1}, #components={2}, degrees={3}\n", new Object[] { graphForLayout.nodeCount(), graphForLayout.edgeCount(), GraphUtils.components(graphForLayout).size(), nicer(GraphUtils.degreeDistribution(graphForLayout)) }); // perform the physics-based layout Map<C, Point2D.Double> initialLocs = StaticGraphLayout.CIRCLE.layout(graphForLayout, Collections.EMPTY_MAP, Collections.EMPTY_SET, irad); SpringLayout sl = new SpringLayout(initialLocs); sl.getParameters().setDistScale(distScale); double lastEnergy = Double.MAX_VALUE; double energyChange = Double.MAX_VALUE; int step = 0; while (step < minSteps || (step < maxSteps && Math.abs(energyChange) > energyChangeThreshold)) { // adjust cooling parameter double cool01 = 1 - step * step / (maxSteps * maxSteps); sl.parameters.dampingC = coolStart * cool01 + coolEnd * (1 - cool01); sl.iterate(graphForLayout); double energy = sl.getEnergyStatus(); energyChange = energy - lastEnergy; lastEnergy = energy; step++; } // add positions of isolates and leaf nodes back in Map<C, Point2D.Double> res = sl.getPositionsCopy(); addLeafNodes(graphForInfo, res, sl.getParameters().getDistScale()); addIsolates(graphForInfo.getIsolates(), res, sl.getParameters().getDistScale()); // report and clean up lastStepCount = step; Logger.getLogger(StaticSpringLayout.class.getName()).log(Level.INFO, "StaticSpringLayout completed in {0} steps. The final energy " + "change was {1}, and the final energy was {2}", new Object[] { step, energyChange, lastEnergy }); return res; } /** * Add leaf nodes that are adjacent to the given positions. * @param og the graph * @param pos current positions * @param distScale distance between noddes * @param <C> graph node type */ public static <C> void addLeafNodes(OptimizedGraph<C> og, Map<C, Point2D.Double> pos, double distScale) { double nomSz = distScale / 2; Set<C> leafs = og.getLeafNodes(); int n = leafs.size(); if (n > 0) { Rectangle2D bounds = Points.boundingBox(pos.values(), distScale); if (bounds == null) { // no points exist, so must be all pairs double sqSide = nomSz * Math.sqrt(n); Rectangle2D pairRegion = new Rectangle2D.Double(-sqSide, -sqSide, 2 * sqSide, 2 * sqSide); LinkedHashSet orderedLeafs = orderByAdjacency(leafs, og); addPointsToBox(orderedLeafs, pairRegion, pos, nomSz, true); } else { // add close to their neighboring point Set<C> cores = Sets.newHashSet(); Set<C> pairs = Sets.newHashSet(); for (C o : leafs) { C nbr = og.getNeighborOfLeaf(o); if (leafs.contains(nbr)) { pairs.add(o); pairs.add(nbr); } else { cores.add(nbr); } } for (C o : cores) { Set<C> leaves = og.getLeavesAdjacentTo(o); Point2D.Double ctr = pos.get(o); double r = nomSz; double theta = Math.atan2(ctr.y, ctr.x); if (leaves.size() == 1) { pos.put(Iterables.getFirst(leaves, null), new Point2D.Double( ctr.getX() + r * Math.cos(theta), ctr.getY() + r * Math.sin(theta))); } else { double th0 = theta - Math.PI / 3; double dth = (2 * Math.PI / 3) / (leaves.size() - 1); for (C l : leaves) { pos.put(l, new Point2D.Double(ctr.getX() + r * Math.cos(th0), ctr.getY() + r * Math.sin(th0))); th0 += dth; } } } // put the pairs to the right side double area = n * nomSz * nomSz; double ht = Math.min(bounds.getHeight(), 2 * Math.sqrt(area)); double wid = area / ht; Rectangle2D pairRegion = new Rectangle2D.Double(bounds.getMaxX() + .1 * bounds.getWidth(), bounds.getCenterY() - ht / 2, wid, ht); LinkedHashSet orderedPairs = orderByAdjacency(pairs, og); addPointsToBox(orderedPairs, pairRegion, pos, nomSz, true); } } } private static LinkedHashSet orderByAdjacency(Set leafs, OptimizedGraph og) { LinkedHashSet res = Sets.newLinkedHashSet(); for (Object o : leafs) { if (!res.contains(o)) { res.add(o); res.add(og.getNeighborOfLeaf(o)); } } return res; } /** * Add isolate nodes in the given graph based on the current positions in the map * @param <C> graph node type * @param isolates the isolate nodes * @param pos position map * @param distScale distance between nodes */ public static <C> void addIsolates(Set<C> isolates, Map<C, Point2D.Double> pos, double distScale) { double nomSz = distScale / 2; int n = isolates.size(); if (n > 0) { Rectangle2D bounds = Points.boundingBox(pos.values(), nomSz); Rectangle2D isolateRegion; if (bounds == null) { // put them all in the middle double sqSide = nomSz * Math.sqrt(n); isolateRegion = new Rectangle2D.Double(-sqSide, -sqSide, 2 * sqSide, 2 * sqSide); } else { // put them to the right side double area = n * nomSz * nomSz; double ht = Math.min(bounds.getHeight(), 2 * Math.sqrt(area)); double wid = area / ht; isolateRegion = new Rectangle2D.Double(bounds.getMaxX() + .1 * bounds.getWidth(), bounds.getCenterY() - ht / 2, wid, ht); } addPointsToBox(isolates, isolateRegion, pos, nomSz, false); } } private static <C> void addPointsToBox(Set<C> is, Rectangle2D rect, Map<C, Point2D.Double> pos, double nomSz, boolean even) { double x = rect.getMinX(); double y = rect.getMinY(); int added = 0; for (C o : is) { pos.put(o, new Point2D.Double(x, y)); added++; x += nomSz; if (x > rect.getMaxX() && (!even || added % 2 == 0)) { x = rect.getMinX(); y += nomSz; } } } private static String nicer(Multiset set) { List<String> ss = Lists.newArrayList(); for (Object el : Sets.newTreeSet(set.elementSet())) { ss.add(el + ":" + set.count(el)); } return "[" + Joiner.on(",").join(ss) + "]"; } }