/******************************************************************************* * Copyright (c) 2009-2013 CWI * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Paul Klint - Paul.Klint@cwi.nl - CWI *******************************************************************************/ package org.rascalmpl.eclipse.library.vis.figure.graph.spring; import java.util.LinkedList; import org.rascalmpl.eclipse.library.vis.figure.Figure; import org.rascalmpl.eclipse.library.vis.util.FigureMath; import org.rascalmpl.eclipse.library.vis.util.vector.Rectangle; import org.rascalmpl.eclipse.library.vis.util.vector.Vector2D; /** * A SpringGraphNode is created for each "node" constructor that occurs in the * graph. * * @author paulk * */ public class SpringGraphNode extends Figure { private final SpringGraph G; protected final String name; protected final Figure figure; double x; // Coordinates of center of node double y; double temperature; double skew; Vector2D oldImpulse; protected LinkedList<SpringGraphNode> in; protected LinkedList<SpringGraphNode> out; private static boolean debug = false; SpringGraphNode(SpringGraph springGraph, String name, Figure fig) { super(springGraph.prop); this.G = springGraph; this.name = name; this.figure = fig; this.children = new Figure[1]; this.children[0] = fig; in = new LinkedList<SpringGraphNode>(); out = new LinkedList<SpringGraphNode>(); } public void init() { x = FigureMath.random(width()/2, G.minSize.getX() - width()/2); y = FigureMath.random(height()/2, G.minSize.getY() -height()/2); temperature = G.MAX_LOCAL_TEMPERATURE; skew = 0; oldImpulse = new Vector2D(0, 0); } public void addIn(SpringGraphNode n) { if (!in.contains(n)) in.add(n); } public void addOut(SpringGraphNode n) { if (!out.contains(n)) out.add(n); } public int degree() { return in.size() + out.size(); } double width() { return figure != null ? figure.minSize.getX() : 0; } double height() { return figure != null ? figure.minSize.getY() : 0; } protected double getCenterX() { return x; } protected double getCenterY() { return y; } public Vector2D getCenter() { return new Vector2D(x, y); } // Move node during simulation protected void moveBy(double dx, double dy) { x += dx; y += dy; } // Place node after one round, update all dependent positions protected void placeCenter(double newX, double newY) { double w2 = width()/2; double h2 = height()/2; if (newX <= w2){ System.err.printf("ERROR: node %s, x outside boundary: %f\n", name, newX); this.x = w2 + 20; if(oldImpulse.getX() < 0) oldImpulse.setX(-oldImpulse.getX()); } else if(newX >= G.minSize.getX() - w2){ System.err.printf("ERROR: node %s, x outside boundary: %f\n", name, newX); this.x = G.minSize.getX() - w2 - 20; if(oldImpulse.getX() > 0) oldImpulse.setX(-oldImpulse.getX()); } else this.x = newX; if (newY <= h2){ System.err.printf("ERROR: node %s, y outside boundary: %f\n", name, newY); this.y = h2 + 20; if(oldImpulse.getY() < 0) oldImpulse.setY(-oldImpulse.getY()); } else if(newY >= G.minSize.getY() - h2){ System.err.printf("ERROR: node %s, y outside boundary: %f\n", name, newY); this.y = G.minSize.getY() - h2 - 20; if(oldImpulse.getY() > 0) oldImpulse.setY(-oldImpulse.getY()); } else this.y = newY; setElementPosition(); } public double distance(SpringGraphNode other){ return getCenter().distance(other.getCenter()); // - (radius() + other.radius()));// - Math.max(width(), height())/2 - Math.max(other.width(), other.height())/2; } public double distance2(SpringGraphNode other){ double d = distance(other); return d * d; //return getCenter().distance2(other.getCenter()); } public double radius(){ double w = width(); double h = height(); return Math.sqrt(w * w + h * h); } // Mass of this node: surface * number of connections. public double getMass(){ return 0.005 * width() * height() * (1 + degree() / 2); } @Override public void computeMinSize() { minSize.set(figure.minSize.getX(), figure.minSize.getY()); } @Override public void resizeElement(Rectangle view) { localLocation.set(0, 0); setElementPosition(); } void setElementPosition(){ figure.localLocation.set( localLocation.getX() + x - figure.minSize.getX() / 2, localLocation.getY() + y - figure.minSize.getY() / 2); figure.globalLocation.set(globalLocation); figure.globalLocation.set(figure.localLocation); figure.updateGlobalLocation(); figure.computeMinSize(); } // --------------------- private void print(String msg) { double ix = oldImpulse == null ? 0 : oldImpulse.getX(); double iy = oldImpulse == null ? 0 : oldImpulse.getY(); System.err.printf("%s: %s: (%3.2f,%3.2f), imp (%3.2f,%3.2f), temp %4.1f, skew %2.1f\n", msg, name, x, y, ix, iy, temperature, skew); } // Perform one step of the spring simulation algorithm for this node public void step() { if(debug) print("update, before"); Vector2D impulse = computeNodeImpulse(); double angle = oldImpulse.angle(impulse); adjustTemperatureAndSkew(angle); double dx = G.UPDATE_STEP * impulse.getX() * temperature; double dy = G.UPDATE_STEP * impulse.getY() * temperature; moveBy(dx, dy); // adjust local position if(debug){ print("relax, after "); System.err.printf(" imp (%3.2f, %3.2f), angle %1.2f, dx %3.2f, dy %3.2f\n", impulse.getX(), impulse.getY(), Math.toDegrees(angle), dx, dy); } oldImpulse = impulse; } /** * Compute the attractive force A with another node: * A = (this - other) * distance^2 / (EDGE_LENGTH_2 * PHI) * ATTRACT * */ public Vector2D attractiveForce(SpringGraphNode other) { double distance2 = distance2(other); Vector2D thisVector = new Vector2D(x, y); Vector2D otherVector = new Vector2D(other.x, other.y); return thisVector.sub(otherVector).mul(distance2).div(G.EDGE_LENGTH_2 * getMass()) .mul(G.ATTRACT); } /** * Compute repulsive force R with another node: * R = (this - other) * EDGE_LENGHT_2 / distance ^ 2 * */ public Vector2D repulsiveForce(SpringGraphNode other) { double distance2 = distance2(other); if (distance2 > 0) { Vector2D thisVector = new Vector2D(getCenter()); Vector2D otherVector = new Vector2D(other.getCenter()); return thisVector.sub(otherVector).mul(G.EDGE_LENGTH_2).div(distance2).mul(G.REPEL); } return (new Vector2D(0, 0)); } public Vector2D repulsiveForceWall(Vector2D wallVector) { Vector2D thisVector = new Vector2D(getCenter()); double distance2 = thisVector.distance2(wallVector); if (distance2 > 0) { return thisVector.sub(wallVector).mul(G.EDGE_LENGTH_2).div(distance2).mul(10 * G.REPEL); } return (new Vector2D(0, 0)); } private final static double Deg45 = Math.toDegrees(Math.PI/4); private final static double DegMin45 = Math.toDegrees(-Math.PI/4); private final static double Deg145 = Math.toDegrees(3*Math.PI/4); private final static double Deg225 = Math.toDegrees(5*Math.PI/4); /** * Adjust the temperature of the node according to the old temperature and * the the old impulse */ public void adjustTemperatureAndSkew(double angle) { // Angle is a positive value between 0 and 2 pi. if (angle < Deg45 || angle > DegMin45 || (angle > Deg145 && angle < Deg225)) { // between -45 and 45 degrees there is acceleration, // between 145 and 225 degree there is oscillation temperature *= (1 + G.OSCILLATION * Math.cos(angle)); } else { // in the other ranges of the angle, there is rotation: skew += G.SKEW * Math.sin(angle); temperature -= G.ROTATION * Math.abs(skew); } temperature = FigureMath.constrain(temperature, 0, G.MAX_LOCAL_TEMPERATURE); } /** * This method computes the normalized impulse F of the node by the sum of * all force vectors. F = gravity + random + sum(n in nodes, repel(n)) - * sum(n in adjacent, attract(n)) Returns norm(F) */ public Vector2D computeNodeImpulse() { // Add a random force and the gravitational force Vector2D resultForce = gravitionalForce().add(randomForce()); // Repulsive forces for (SpringGraphNode otherNode : G.nodes) { if (otherNode != this) { resultForce = resultForce.add(repulsiveForce(otherNode)); } } // Attractive forces for (SpringGraphNode otherNode : in) { resultForce = resultForce.sub(this.attractiveForce(otherNode)); } for (SpringGraphNode otherNode : out) { resultForce = resultForce.sub(attractiveForce(otherNode)); } // Repulsion of left and right wall resultForce = resultForce.add(repulsiveForceWall(new Vector2D(0, y))); resultForce = resultForce.add(repulsiveForceWall(new Vector2D(G.minSize.getX(), y))); // Repulsion of top and bottom wall resultForce = resultForce.add(repulsiveForceWall(new Vector2D(x, 0))); resultForce = resultForce.add(repulsiveForceWall(new Vector2D(x, G.minSize.getY()))); // we only need the impulse return resultForce.normalize(); } /** * Compute the random force. * We need a random impulse [-RAND_CONSTANT, ..., +RAND_CONSTANT] * Range should be approx. [-1/4 ... +1/4] of desired edge length. */ public Vector2D randomForce() { return new Vector2D(FigureMath.random(-G.RAND_DISTURB, G.RAND_DISTURB), FigureMath.random(-G.RAND_DISTURB, G.RAND_DISTURB)); } /** * This method computes the gravitational force between a node and the * barycenter of the drawing. * G = (barycenter - thisVector) * Mass * Gravity */ public Vector2D gravitionalForce() { Vector2D barycenter = new Vector2D(G.getBaryCenter()); Vector2D thisVector = new Vector2D(getCenter()); return barycenter.sub(thisVector).mul(getMass()).mul(G.GRAVITY); } }