/*
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.labelAdjust;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.gephi.graph.api.Graph;
import org.gephi.graph.api.Node;
import org.gephi.layout.plugin.AbstractLayout;
import org.gephi.layout.plugin.ForceVectorUtils;
import org.gephi.layout.spi.Layout;
import org.gephi.layout.spi.LayoutBuilder;
import org.gephi.layout.spi.LayoutProperty;
/**
*
* @author Mathieu Jacomy
*/
public class LabelAdjust extends AbstractLayout implements Layout {
//Graph
protected Graph graph;
//Settings
private double speed = 1;
private final double RADIUS_SCALE = 2;
private double xmin;
private double xmax;
private double ymin;
private double ymax;
public LabelAdjust(LayoutBuilder layoutBuilder) {
super(layoutBuilder);
}
public void resetPropertiesValues() {
speed = 1;
}
public void initAlgo() {
setConverged(false);
}
public void goAlgo() {
boolean somethingMoved = false;
this.graph = graphModel.getGraphVisible();
graph.readLock();
Node[] nodes = graph.getNodes().toArray();
//Reset Layout Data
for (Node n : nodes) {
if (n.getNodeData().getLayoutData() == null || !(n.getNodeData().getLayoutData() instanceof LabelAdjustLayoutData)) {
n.getNodeData().setLayoutData(new LabelAdjustLayoutData());
}
LabelAdjustLayoutData layoutData = n.getNodeData().getLayoutData();
layoutData.neighbours.clear();
layoutData.dx = 0;
layoutData.dy = 0;
}
// Get xmin, xmax, ymin, ymax
this.xmin = Double.MAX_VALUE;
this.xmax = Double.MIN_VALUE;
this.ymin = Double.MAX_VALUE;
this.ymax = Double.MIN_VALUE;
List<Node> correctNodes = new ArrayList<Node>();
for (Node n : nodes) {
float x = n.getNodeData().x();
float y = n.getNodeData().y();
float w = n.getNodeData().getTextData().getWidth();
float h = n.getNodeData().getTextData().getHeight();
float radius = n.getNodeData().getRadius();
if (w > 0 && h > 0) {
// Get the rectangle occupied by the node (size + label)
double nxmin = Math.min(x - w / 2, x - radius);
double nxmax = Math.max(x + w / 2, x + radius);
double nymin = Math.min(y - h / 2, y - radius);
double nymax = Math.max(y + h / 2, y + radius);
// Update global boundaries
this.xmin = Math.min(this.xmin, nxmin);
this.xmax = Math.max(this.xmax, nxmax);
this.ymin = Math.min(this.ymin, nymin);
this.ymax = Math.max(this.ymax, nymax);
correctNodes.add(n);
}
}
if (correctNodes.isEmpty()) {
graph.readUnlock();
return;
}
// Secure the bounds
double xwidth = this.xmax - this.xmin;
double yheight = this.ymax - this.ymin;
double xcenter = (this.xmin + this.xmax) / 2;
double ycenter = (this.ymin + this.ymax) / 2;
double ratio = 1.1;
this.xmin = xcenter - ratio * xwidth / 2;
this.xmax = xcenter + ratio * xwidth / 2;
this.ymin = ycenter - ratio * yheight / 2;
this.ymax = ycenter + ratio * yheight / 2;
//System.out.println("BOUNDS this.xmin="+this.xmin+" this.xmax="+this.xmax+" this.ymin="+this.ymin+" this.ymax="+this.ymax);
SpatialGrid grid = new SpatialGrid();
// Put nodes in their boxes
for (Node n : correctNodes) {
grid.add(n);
}
// Now we have boxes with nodes in it. Nodes that are in the same box, or in adjacent boxes, are tested for repulsion.
// But they are not repulsed several times, even if they are in several boxes...
// So we build a relation of proximity between nodes.
// Build proximities
for (int row = 0; row < grid.countRows(); row++) {
for (int col = 0; col < grid.countColumns(); col++) {
for (Node n : grid.getContent(row, col)) {
LabelAdjustLayoutData lald = n.getNodeData().getLayoutData();
// For node n in the box "box"...
// We search nodes that are in the boxes that are adjacent or the same.
for (int row2 = Math.max(0, row - 1); row2 <= Math.min(row + 1, grid.countRows() - 1); row2++) {
for (int col2 = Math.max(0, col - 1); col2 <= Math.min(col + 1, grid.countColumns() - 1); col2++) {
for (Node n2 : grid.getContent(row2, col2)) {
if (n2 != n && !lald.neighbours.contains(n2)) {
lald.neighbours.add(n2);
}
}
}
}
}
}
}
// Proximities are built !
// Apply repulsion force - along proximities...
for (Node n1 : correctNodes) {
LabelAdjustLayoutData lald = n1.getNodeData().getLayoutData();
for (Node n2 : lald.neighbours) {
float n1x = n1.getNodeData().x();
float n1y = n1.getNodeData().y();
float n2x = n2.getNodeData().x();
float n2y = n2.getNodeData().y();
float n1w = n1.getNodeData().getTextData().getWidth();
float n2w = n2.getNodeData().getTextData().getWidth();
float n1h = n1.getNodeData().getTextData().getHeight();
float n2h = n2.getNodeData().getTextData().getHeight();
// Check sizes (spheric)
double xDist = Math.abs(n1x - n2x);
double yDist = Math.abs(n1.getNodeData().y() - n2.getNodeData().y());
boolean sphereCollision = Math.sqrt(xDist * xDist + yDist * yDist) < RADIUS_SCALE * (n1.getNodeData().getRadius() + n2.getNodeData().getRadius());
if (sphereCollision) {
ForceVectorUtils.fcUniRepulsor(n1.getNodeData(), n2.getNodeData(), 0.1 * n1.getNodeData().getRadius());
somethingMoved = true;
}
// Check labels, but when no label keep a rectangle equivalent to the sphere
double n1xmin = n1x - 0.5 * n1w;
double n2xmin = n2x - 0.5 * n2w;
double n1ymin = n1y - 0.5 * n1h;
double n2ymin = n2y - 0.5 * n2h;
double n1xmax = n1x + 0.5 * n1w;
double n2xmax = n2x + 0.5 * n2w;
double n1ymax = n1y + 0.5 * n1h;
double n2ymax = n2y + 0.5 * n2h;
double upDifferential = n1ymax - n2ymin;
double downDifferential = n2ymax - n1ymin;
double labelCollisionXleft = n2xmax - n1xmin;
double labelCollisionXright = n1xmax - n2xmin;
LabelAdjustLayoutData layoutData = n2.getNodeData().getLayoutData();
if (upDifferential > 0 && downDifferential > 0) { // Potential collision
if (labelCollisionXleft > 0 && labelCollisionXright > 0) {// Collision
if (upDifferential > downDifferential) {
// N1 pushes N2 up
layoutData.dy -= 0.01 * n1h * (0.8 + 0.4 * Math.random());
somethingMoved = true;
} else {
// N1 pushes N2 down
layoutData.dy += 0.01 * n1h * (0.8 + 0.4 * Math.random());
somethingMoved = true;
}
if (labelCollisionXleft > labelCollisionXright) {
// N1 pushes N2 right
layoutData.dx += 0.01 * (n1h / 2) * (0.8 + 0.4 * Math.random());
somethingMoved = true;
} else {
// N1 pushes N2 left
layoutData.dx -= 0.01 * (n1h / 2) * (0.8 + 0.4 * Math.random());
somethingMoved = true;
}
}
}
}
}
// apply forces
for (Node n : correctNodes) {
LabelAdjustLayoutData layoutData = n.getNodeData().getLayoutData();
if (!n.getNodeData().isFixed()) {
layoutData.dx *= speed;
layoutData.dy *= speed;
float x = n.getNodeData().x() + layoutData.dx;
float y = n.getNodeData().y() + layoutData.dy;
n.getNodeData().setX(x);
n.getNodeData().setY(y);
}
}
if (!somethingMoved) {
setConverged(true);
}
graph.readUnlock();
}
public void endAlgo() {
for (Node n : graph.getNodes()) {
n.getNodeData().setLayoutData(null);
}
}
public LayoutProperty[] getProperties() {
List<LayoutProperty> properties = new ArrayList<LayoutProperty>();
final String LABELADJUST_CATEGORY = "LabelAdjust";
try {
properties.add(LayoutProperty.createProperty(
this, Double.class, "speed", LABELADJUST_CATEGORY, "speed", "getSpeed", "setSpeed"));
} catch (Exception e) {
e.printStackTrace();
}
return properties.toArray(new LayoutProperty[0]);
}
public Double getSpeed() {
return speed;
}
public void setSpeed(Double speed) {
this.speed = speed;
}
private class SpatialGrid {
//Param
private final int COLUMNS_ROWS = 20;
//Data
private Map<Cell, List<Node>> data = new HashMap<Cell, List<Node>>();
public SpatialGrid() {
for (int row = 0; row < COLUMNS_ROWS; row++) {
for (int col = 0; col < COLUMNS_ROWS; col++) {
List<Node> localnodes = new ArrayList<Node>();
data.put(new Cell(row, col), localnodes);
}
}
}
public Iterable<Node> getContent(int row, int col) {
return data.get(new Cell(row, col));
}
public int countColumns() {
return COLUMNS_ROWS;
}
public int countRows() {
return COLUMNS_ROWS;
}
public void add(Node node) {
float x = node.getNodeData().x();
float y = node.getNodeData().y();
float w = node.getNodeData().getTextData().getWidth();
float h = node.getNodeData().getTextData().getHeight();
float radius = node.getNodeData().getRadius();
// Get the rectangle occupied by the node (size + label)
double nxmin = Math.min(x - w / 2, x - radius);
double nxmax = Math.max(x + w / 2, x + radius);
double nymin = Math.min(y - h / 2, y - radius);
double nymax = Math.max(y + h / 2, y + radius);
// Get the rectangle as boxes
int minXbox = (int) Math.floor((COLUMNS_ROWS - 1) * (nxmin - xmin) / (xmax - xmin));
int maxXbox = (int) Math.floor((COLUMNS_ROWS - 1) * (nxmax - xmin) / (xmax - xmin));
int minYbox = (int) Math.floor((COLUMNS_ROWS - 1) * (nymin - ymin) / (ymax - ymin));
int maxYbox = (int) Math.floor((COLUMNS_ROWS - 1) * (nymax - ymin) / (ymax - ymin));
for (int col = minXbox; col <= maxXbox; col++) {
for (int row = minYbox; row <= maxYbox; row++) {
try {
data.get(new Cell(row, col)).add(node);
} catch (Exception e) {
//e.printStackTrace();
if (nxmin < xmin || nxmax > xmax) {
System.err.println("Xerr0r* - " + node.getId() + " - nxmin=" + nxmin + " this.xmin=" + xmin + " nxmax=" + nxmax + " this.xmax=" + xmax);
}
if (nymin < ymin || nymax > ymax) {
System.err.println("Yerr0r* - " + node.getId() + " - nymin=" + nymin + " this.ymin=" + ymin + " nymax=" + nymax + " this.ymax=" + ymax);
}
}
}
}
}
}
private static class Cell {
private final int row;
private final int col;
public Cell(int row, int col) {
this.row = row;
this.col = col;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Cell other = (Cell) obj;
if (this.row != other.row) {
return false;
}
if (this.col != other.col) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 11 * hash + this.row;
hash = 11 * hash + this.col;
return hash;
}
}
}