/* * This file is part of Caliph & Emir. * * Caliph & Emir is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Caliph & Emir is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Caliph & Emir; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Copyright statement: * -------------------- * (c) 2002-2005 by Mathias Lux (mathias@juggle.at) * http://www.juggle.at, http://caliph-emir.sourceforge.net */ package at.lux.graphviz; import java.util.*; /** * Date: 14.09.2004 * Time: 21:05:09 * * @author Mathias Lux, mathias@juggle.at */ public class SpringEmbedder { HashMap<Node, HashSet<Node>> node2nodes; List<? extends Node> nodeList; List<? extends Edge> edgeList; // ----------------------------------------------- // Parameters // ----------------------------------------------- // force function params private double c1, c2, c3, c4; // when to stop: private double stopCondition = 0.00005; // or maximum steps: private int maxSteps = 5000; // with additional steps for finetuning: private int additionalSteps = 25; // has attracting force in the center private boolean hasInvisibleCenterNode = true; private boolean edgesRepelNodes = false; // Should the space be scaled to [0,1]^2 after each step? private boolean scaleDownSpace = false; // Should the movement vector for each node be normalized? private boolean normalizeMovementVector = false; // ----------------------------------------------- // internal fields for calculation // ----------------------------------------------- private int countSteps, additionalStepCountdown; private double overallMovement; public SpringEmbedder(List<? extends Node> nodeList, List<? extends Edge> edgeList) { this.nodeList = nodeList; this.edgeList = edgeList; overallMovement = Double.MAX_VALUE; additionalStepCountdown = additionalSteps; countSteps = 0; init(); } private void init() { // init params: c1 = 2.0; c2 = 1.0; c3 = 1.0; c4 = 0.1; countSteps = 0; // init lookup: node2nodes = new HashMap<Node, HashSet<Node>>(nodeList.size()); for (Iterator<? extends Node> iterator = nodeList.iterator(); iterator.hasNext();) { node2nodes.put(iterator.next(), new HashSet<Node>()); } for (Iterator<? extends Edge> iterator = edgeList.iterator(); iterator.hasNext();) { Edge e = iterator.next(); node2nodes.get(e.getStartNode()).add(e.getEndNode()); node2nodes.get(e.getEndNode()).add(e.getStartNode()); } } private HashMap<Node, Vector2D> calculateForces() { HashMap<Node, Vector2D> result = new HashMap<Node, Vector2D>(nodeList.size()); overallMovement = 0.0; for (Iterator<? extends Node> iterator = nodeList.iterator(); iterator.hasNext();) { double force = 0.0; Node nodeA = iterator.next(); Vector2D f = new Vector2D(0.0, 0.0); DefaultNode centerNode = new DefaultNode(0.0, 0.0); if (hasInvisibleCenterNode) { // attracting force from center ... double v = (nodeA.distance(centerNode) / c2); force = v * v; Vector2D toCenter = nodeA.direction(centerNode); toCenter.normalize(); toCenter.multiply(force); f.addVector2D(toCenter); } if (edgesRepelNodes) { List<Node> border = new LinkedList<Node>(); border.add(new DefaultNode(0.0, nodeA.getY())); border.add(new DefaultNode(1.0, nodeA.getY())); border.add(new DefaultNode(nodeA.getX(), 0.0)); border.add(new DefaultNode(nodeA.getX(), 1.0)); for (Iterator<Node> itBorder = border.iterator(); itBorder.hasNext();) { Node nodeB = itBorder.next(); force = 0.0; // these two nodes repel each other force = -c3 / Math.sqrt(nodeA.distance(nodeB)); Vector2D vector = nodeA.direction(centerNode); vector.normalize(); vector.multiply(force); f.addVector2D(vector); } } for (Iterator<? extends Node> it1 = nodeList.iterator(); it1.hasNext();) { Node nodeB = it1.next(); if (!nodeA.equals(nodeB)) { force = 0.0; if (node2nodes.get(nodeA).contains(nodeB)) { // these two nodes attract each other force = c1 * Math.log(nodeA.distance(nodeB) / c2); } else { // these two nodes repel each other force = -c3 / Math.sqrt(nodeA.distance(nodeB)); } Vector2D vector = nodeA.direction(nodeB); vector.normalize(); vector.multiply(force); f.addVector2D(vector); } } if (normalizeMovementVector) { f.normalize(); } // here goes the step into the right direction: f.multiply(c4); overallMovement += f.getLength(); result.put(nodeA, f); } // System.out.println(overallMovement); return result; } private void moveVertices(HashMap<Node, Vector2D> forces) { for (Iterator<Node> iterator = forces.keySet().iterator(); iterator.hasNext();) { Node node = iterator.next(); node.move(forces.get(node)); } } public int step() { double lastMovement = overallMovement; moveVertices(calculateForces()); if (scaleDownSpace) { scaleDown(); } countSteps++; // stop when movement is small enough, there are additional steps for // for hopping over local minima if (Math.abs(overallMovement - lastMovement) < stopCondition) { additionalStepCountdown--; } else { additionalStepCountdown = additionalSteps; } if (additionalStepCountdown < 0 || countSteps > maxSteps) return -1; return countSteps; } private void scaleDown() { double xMin = 1.0, xMax = 0.0, yMin = 1.0, yMax = 0.0; for (Iterator<? extends Node> iterator = nodeList.iterator(); iterator.hasNext();) { Node node = iterator.next(); if (node.getX() < xMin) xMin = node.getX(); if (node.getX() > xMax) xMax = node.getX(); if (node.getY() < yMin) yMin = node.getY(); if (node.getY() > yMax) yMax = node.getY(); } for (Iterator<? extends Node> iterator = nodeList.iterator(); iterator.hasNext();) { Node node = iterator.next(); node.setX((node.getX() - xMin) / (xMax - xMin)); node.setY((node.getY() - yMin) / (yMax - yMin)); assert(node.getX() >= 0.0); assert(node.getX() <= 1.0); assert(node.getY() >= 0.0); assert(node.getY() <= 1.0); } } public List<? extends Node> getNodeList() { return nodeList; } public List<? extends Edge> getEdgeList() { return edgeList; } /** * Initial values: c1 = 2.0; c2 = 1.0; c3 = 1.0; c4 = 0.1; * @param c1 is multiplied with the logarithmic attraction force: force = c1 * Math.log(nodeA.distance(nodeB) / c2); * @param c2 defines how much nodes attract each other: divisor of the distance: force = c1 * Math.log(nodeA.distance(nodeB) / c2); * @param c3 defines how much nodes repel each other: force = -c3 / Math.sqrt(nodeA.distance(nodeB)); * @param c4 defines how wide to step (the length of the movement vector) * @param hasInvisibleCenterNode */ public void setEmbeddingParameters(double c1, double c2, double c3, double c4, boolean hasInvisibleCenterNode) { this.c1 = c1; this.c2 = c2; this.c3 = c3; this.c4 = c4; this.hasInvisibleCenterNode = hasInvisibleCenterNode; } public void setHasInvisibleCenterNode(boolean hasInvisibleCenterNode) { this.hasInvisibleCenterNode = hasInvisibleCenterNode; } public void setNormalizeMovementVector(boolean normalizeMovementVector) { this.normalizeMovementVector = normalizeMovementVector; } public void setScaleDownSpace(boolean scaleDownSpace) { this.scaleDownSpace = scaleDownSpace; } public double getC4() { return c4; } public void setC4(double c4) { this.c4 = c4; } }