/*
Copyright 2008-2010 Gephi
Authors : Mathieu Jacomy
Website : http://www.gephi.org
This file is part of Gephi.
Gephi is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Gephi 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Gephi. If not, see <http://www.gnu.org/licenses/>.
*/
package org.gephi.layout.plugin.fruchterman;
import java.util.ArrayList;
import java.util.List;
import org.gephi.graph.api.Edge;
import org.gephi.graph.api.Graph;
import org.gephi.graph.api.HierarchicalGraph;
import org.gephi.graph.api.Node;
import org.gephi.graph.api.NodeData;
import org.gephi.layout.plugin.AbstractLayout;
import org.gephi.layout.plugin.ForceVectorNodeLayoutData;
import org.gephi.layout.spi.Layout;
import org.gephi.layout.spi.LayoutBuilder;
import org.gephi.layout.spi.LayoutProperty;
import org.openide.util.NbBundle;
/**
*
* @author Mathieu Jacomy
*/
public class FruchtermanReingold extends AbstractLayout implements Layout {
private static final float SPEED_DIVISOR = 800;
private static final float AREA_MULTIPLICATOR = 10000;
//Graph
protected HierarchicalGraph graph;
//Properties
private float area;
private double gravity;
private double speed;
public FruchtermanReingold(LayoutBuilder layoutBuilder) {
super(layoutBuilder);
}
public void resetPropertiesValues() {
speed = 1;
area = 10000;
gravity = 10;
}
public void initAlgo() {
this.graph = graphModel.getHierarchicalGraphVisible();
for (Node n : graph.getNodes()) {
n.getNodeData().setLayoutData(new ForceVectorNodeLayoutData());
}
}
public void goAlgo() {
this.graph = graphModel.getHierarchicalGraphVisible();
graph.readLock();
Node[] nodes = graph.getNodes().toArray();
Edge[] edges = graph.getEdgesAndMetaEdges().toArray();
for (Node n : nodes) {
if (n.getNodeData().getLayoutData() == null || !(n.getNodeData().getLayoutData() instanceof ForceVectorNodeLayoutData)) {
n.getNodeData().setLayoutData(new ForceVectorNodeLayoutData());
}
ForceVectorNodeLayoutData layoutData = n.getNodeData().getLayoutData();
layoutData.dx = 0;
layoutData.dy = 0;
}
float maxDisplace = (float) (Math.sqrt(AREA_MULTIPLICATOR * area) / 10f); // Déplacement limite : on peut le calibrer...
float k = (float) Math.sqrt((AREA_MULTIPLICATOR * area) / (1f + nodes.length)); // La variable k, l'idée principale du layout.
for (Node N1 : nodes) {
for (Node N2 : nodes) { // On fait toutes les paires de noeuds
if (N1 != N2) {
float xDist = N1.getNodeData().x() - N2.getNodeData().x(); // distance en x entre les deux noeuds
float yDist = N1.getNodeData().y() - N2.getNodeData().y();
float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist); // distance tout court
if (dist > 0) {
float repulsiveF = k * k / dist; // Force de répulsion
ForceVectorNodeLayoutData layoutData = N1.getNodeData().getLayoutData();
layoutData.dx += xDist / dist * repulsiveF; // on l'applique...
layoutData.dy += yDist / dist * repulsiveF;
}
}
}
}
for (Edge E : edges) {
// Idem, pour tous les noeuds on applique la force d'attraction
Node Nf = E.getSource();
Node Nt = E.getTarget();
float xDist = Nf.getNodeData().x() - Nt.getNodeData().x();
float yDist = Nf.getNodeData().y() - Nt.getNodeData().y();
float dist = (float) Math.sqrt(xDist * xDist + yDist * yDist);
float attractiveF = dist * dist / k;
if (dist > 0) {
ForceVectorNodeLayoutData sourceLayoutData = Nf.getNodeData().getLayoutData();
ForceVectorNodeLayoutData targetLayoutData = Nt.getNodeData().getLayoutData();
sourceLayoutData.dx -= xDist / dist * attractiveF;
sourceLayoutData.dy -= yDist / dist * attractiveF;
targetLayoutData.dx += xDist / dist * attractiveF;
targetLayoutData.dy += yDist / dist * attractiveF;
}
}
// gravity
for (Node n : nodes) {
NodeData nodeData = n.getNodeData();
ForceVectorNodeLayoutData layoutData = nodeData.getLayoutData();
float d = (float) Math.sqrt(nodeData.x() * nodeData.x() + nodeData.y() * nodeData.y());
float gf = 0.01f * k * (float) gravity * d;
layoutData.dx -= gf * nodeData.x() / d;
layoutData.dy -= gf * nodeData.y() / d;
}
// speed
for (Node n : nodes) {
ForceVectorNodeLayoutData layoutData = n.getNodeData().getLayoutData();
layoutData.dx *= speed / SPEED_DIVISOR;
layoutData.dy *= speed / SPEED_DIVISOR;
}
for (Node n : nodes) {
// Maintenant on applique le déplacement calculé sur les noeuds.
// nb : le déplacement à chaque passe "instantanné" correspond à la force : c'est une sorte d'accélération.
ForceVectorNodeLayoutData layoutData = n.getNodeData().getLayoutData();
float xDist = layoutData.dx;
float yDist = layoutData.dy;
float dist = (float) Math.sqrt(layoutData.dx * layoutData.dx + layoutData.dy * layoutData.dy);
if (dist > 0 && !n.getNodeData().isFixed()) {
float limitedDist = Math.min(maxDisplace * ((float) speed / SPEED_DIVISOR), dist);
n.getNodeData().setX(n.getNodeData().x() + xDist / dist * limitedDist);
n.getNodeData().setY(n.getNodeData().y() + yDist / dist * limitedDist);
}
}
graph.readUnlock();
}
public void endAlgo() {
for (Node n : graph.getNodes()) {
n.getNodeData().setLayoutData(null);
}
}
@Override
public boolean canAlgo() {
return true;
}
public LayoutProperty[] getProperties() {
List<LayoutProperty> properties = new ArrayList<LayoutProperty>();
final String FRUCHTERMAN_REINGOLD = "Fruchterman Reingold";
try {
properties.add(LayoutProperty.createProperty(
this, Float.class,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.area.name"),
FRUCHTERMAN_REINGOLD,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.area.desc"),
"getArea", "setArea"));
properties.add(LayoutProperty.createProperty(
this, Double.class,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.gravity.name"),
FRUCHTERMAN_REINGOLD,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.gravity.desc"),
"getGravity", "setGravity"));
properties.add(LayoutProperty.createProperty(
this, Double.class,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.speed.name"),
FRUCHTERMAN_REINGOLD,
NbBundle.getMessage(FruchtermanReingold.class, "fruchtermanReingold.speed.desc"),
"getSpeed", "setSpeed"));
} catch (Exception e) {
e.printStackTrace();
}
return properties.toArray(new LayoutProperty[0]);
}
public Float getArea() {
return area;
}
public void setArea(Float area) {
this.area = area;
}
/**
* @return the gravity
*/
public Double getGravity() {
return gravity;
}
/**
* @param gravity the gravity to set
*/
public void setGravity(Double gravity) {
this.gravity = gravity;
}
/**
* @return the speed
*/
public Double getSpeed() {
return speed;
}
/**
* @param speed the speed to set
*/
public void setSpeed(Double speed) {
this.speed = speed;
}
}