/*
* ClusterLayout.java
*
* Created on October 1, 2008, 2:14 PM
*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community 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.osedu.org/licenses/ECL-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.
*/
/**
*
* @author akumar03
*/
package edu.tufts.vue.layout;
import java.io.*;
import java.net.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import javax.swing.*;
import tufts.vue.*;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;
import edu.tufts.vue.dataset.*;
public class ClusterLayout extends Layout {
public static String DEFAULT_METADATA_LABEL = "default";
public static final int MINX_RADIUS = VueResources
.getInt("layout.minx_radius");
public static final int MINY_RADIUS = VueResources
.getInt("layout.miny_radius");
public static final int X_SPACING = VueResources.getInt("layout.x_spacing");
public static final int Y_SPACING = VueResources.getInt("layout.y_spacing");
public static final double FACTOR = VueResources
.getDouble("layout.space_ratio");
public static final int MAX_COLLISION_CHECK = VueResources
.getInt("layout.check_overlap_number");
public final int clusterColumn = 3;
public final int total = 15;
public static final double AREA_INCREASE_FACTOR = 2.0;
public static final int MAX_ITERATIONS = 20;
/** Creates a new instance of ClusterLayout */
public ClusterLayout() {
}
public LWMap createMap(Dataset ds, String mapName) throws Exception {
Map<String, LWNode> nodeMap = new HashMap<String, LWNode>();
Map<String, Integer> repeatMap = new HashMap<String, Integer>();
ArrayList<String> clusterColumnList = new ArrayList<String>();
LWMap map = new LWMap(mapName);
int count = 0;
// set map size of the map
double rowCount = ds.getRowList().size();
double goodSize = (int) Math.sqrt(rowCount) * 100;
MAP_SIZE = MAP_SIZE > goodSize ? MAP_SIZE : goodSize;
for (ArrayList<String> row : ds.getRowList()) {
String node1Label = row.get(0);
LWNode node1;
node1 = new LWNode(node1Label);
for (int i = 1; i < row.size(); i++) {
String value = row.get(i);
String key = ((ds.getHeading() == null) || ds.getHeading()
.size() < i) ? DEFAULT_METADATA_LABEL : ds.getHeading()
.get(i);
// System.out.println("i="+i+" key="+key+" value ="+value);
VueMetadataElement vm = new VueMetadataElement();
vm.setKey(key);
vm.setValue(value);
vm.setType(VueMetadataElement.CATEGORY);
node1.getMetadataList().addElement(vm);
}
if (ds.getHeading().size() > 1
&& ds.getHeading().get(1).equals("resource")) {
Resource resource = node1.getResourceFactory().get(
new File(row.get(1)));
node1.setResource(resource);
}
// special hack to demo the dataset laurie baise dataset
if (ds.getHeading().size() > 6
&& ds.getHeading().get(6).equals("Actual")) {
if (row.get(6).equalsIgnoreCase("A")) {
node1.setFillColor(Color.CYAN);
}
}
if (ds.getHeading().size() > 2
&& ds.getHeading().get(2).equals("Role")) {
if (row.get(2).contains("mentor")) {
node1.setFillColor(Color.CYAN);
}
}
if (ds.getHeading().size() > 9
&& ds.getHeading().get(9).contains("Total")) {
double width = Double.parseDouble(row.get(9));
width = Math.sqrt(width);
if (width > 10)
width = 10;
node1.setStrokeWidth((float) width);
}
String clusterElement = row.get(clusterColumn);
if (!clusterColumnList.contains(clusterElement))
clusterColumnList.add(clusterElement);
nodeMap.put(node1Label, node1);
int COLUMNS = 8;
int MAP_SIZE = 5000;
double Q_SIZE = (double) MAP_SIZE / COLUMNS;
double x = (clusterColumnList.indexOf(clusterElement) % COLUMNS)
* Q_SIZE - Q_SIZE / 2;
double y = (clusterColumnList.indexOf(clusterElement) / COLUMNS)
* Q_SIZE - Q_SIZE / 2;
node1.layout();
map.add(node1);
double angle = Math.random() * Math.PI * 4;
node1.setLocation(x + Math.cos(angle) * Q_SIZE / 3, y
+ Math.sin(angle) * Q_SIZE / 3);
// node1.setLocation(MAP_SIZE*Math.random(),MAP_SIZE*Math.random());
}
return map;
}
public void layout(LWSelection selection) {
System.out.println("Applying the cluster layout");
HashMap<LWComponent, ArrayList<LWComponent>> clusterMap = new HashMap<LWComponent, ArrayList<LWComponent>>();
HashMap<LWComponent,Double> componentRadiusMap = new HashMap<LWComponent,Double>();
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxNodeWidth = X_COL_SIZE;
double maxNodeHeight = Y_COL_SIZE;
double meanNodeWidth = X_COL_SIZE;
double meanNodeHeight = Y_COL_SIZE;
double totalNodeWidth = 0.0;
double totalNodeHeight= 0.0;
int meanCount = 0;
int total = 0;
Iterator<LWComponent> i = VUE.getActiveMap().getAllDescendents(
LWContainer.ChildKind.PROPER).iterator();
// placing the cluster nodes in a hashmap with the center node as a key
while (i.hasNext()) {
LWComponent c = i.next();
if (c instanceof LWLink) {
LWLink link = (LWLink) c;
LWComponent head = link.getHead();
LWComponent tail = link.getTail();
if (selection.contains(head)) {
if (!clusterMap.containsKey(head)) {
clusterMap.put(head, new ArrayList<LWComponent>());
}
clusterMap.get(head).add(tail);
}
if (selection.contains(tail)) {
if (!clusterMap.containsKey(tail)) {
clusterMap.put(tail, new ArrayList<LWComponent>());
}
clusterMap.get(tail).add(head);
}
} else if (c instanceof LWNode) {
maxNodeWidth = maxNodeWidth > c.getWidth() ? maxNodeWidth : c
.getWidth();
maxNodeHeight = maxNodeHeight > c.getHeight() ? maxNodeHeight
: c.getHeight();
totalNodeWidth += c.getWidth();
totalNodeHeight += c.getHeight();
meanCount++;
// System.out.println("Node: "+c.getLabel()+" width:"+c.getWidth()+" max:"+maxNodeWidth);
}
}
// computing the minimum and X and Y position of selection
// TODO: use the center of selection to compute min and max instead
Iterator<LWComponent> iter = selection.iterator();
while (iter.hasNext()) {
LWComponent c = iter.next();
if (c instanceof LWNode) {
LWNode node = (LWNode) c;
minX = node.getLocation().getX() < minX ? node.getLocation()
.getX() : minX;
minY = node.getLocation().getY() < minY ? node.getLocation()
.getY() : minY;
total++;
maxNodeWidth = maxNodeWidth > c.getWidth() ? maxNodeWidth : c
.getWidth();
maxNodeHeight = maxNodeHeight > c.getHeight() ? maxNodeHeight: c.getHeight();
totalNodeWidth += c.getWidth();
totalNodeHeight += c.getHeight();
meanCount++;
}
}
if(meanCount >0) {
meanNodeWidth = totalNodeWidth/meanCount;
meanNodeHeight = totalNodeHeight/meanCount;
}
// System.out.println("Max Width: "+maxNodeWidth+" Max Height:"+maxNodeHeight);
// computing the size of largest cluster and the area to plot all clusters;
double area = 0.0;
for (LWComponent c : clusterMap.keySet()) {
if (c == null) {
System.err.println("null component in clusterMap");
continue;
}
double clusterArea = FACTOR * clusterMap.get(c).size()* meanNodeWidth * meanNodeHeight;
area += clusterArea;
// assuming width> height
// System.out.println("Total Area Needed: "+area+" "+c.getLabel()+" "+clusterMap.get(c).size());
double radius = 1.05*(c.getWidth()/2+Math.sqrt( clusterArea/ Math.PI));// increase the radius for computing cluster centers by 5%
componentRadiusMap.put(c, radius);
}
area = AREA_INCREASE_FACTOR * area;
packCircles(componentRadiusMap,area,minX,minY);
double x = minX;
double y = minY;
//TODO: need to place the central nodes better
iter = selection.iterator();
// making the clusters
while (iter.hasNext()) {
LWComponent c = iter.next();
if (c instanceof LWNode) {
LWNode node = (LWNode) c;
total++;
if(clusterMap.get(node) != null && clusterMap.get(node).size() >0) {
double radius = Math.sqrt(FACTOR * clusterMap.get(node).size()* meanNodeWidth * meanNodeHeight / Math.PI);
int countLinked = 0;
for (LWComponent linkedNode : clusterMap.get(node)) {
// LWNode nodeLinked = (LWNode)c;
double angle = Math.PI * 2 * Math.random();
x = node.getX();
y = node.getY();
double radiusX = node.getWidth()/2+ radius
* (1 - Math.pow(Math.random(), 2.0));
double radiusY =node.getWidth()/2+radius
* (1 - Math.pow(Math.random(), 2.0)) ;
double xLinkedNode = x+node.getWidth()/2 -linkedNode.getWidth()/2+ radiusX * Math.cos(angle);
double yLinkedNode =y+node.getHeight()/2-linkedNode.getHeight()/2+ radiusY * Math.sin(angle);
boolean flag = true;
int col_count = 0;
while (flag && col_count < MAX_COLLISION_CHECK) {
if ((VUE.getActiveViewer().pickNode((float) x,
(float) y) != null)
|| (VUE.getActiveViewer().pickNode(
(float) x + node.getWidth(),
(float) y + node.getHeight()) != null)
|| (VUE.getActiveViewer().pickNode((float) x,
(float) y + node.getHeight()) != null)
|| (VUE.getActiveViewer().pickNode(
(float) x + node.getWidth(), (float) y) != null)) {
angle = Math.PI * 2 * Math.random();
radiusX = node.getWidth()/2 + radius
* (1 - Math.pow(Math.random(), 2.0));
radiusY = node.getHeight()/2+radius
* (1 - Math.pow(Math.random(), 2.0));
xLinkedNode = x+node.getWidth()/2 -linkedNode.getWidth()/2 + radiusX * Math.cos(angle);
yLinkedNode = y+node.getHeight()/2-linkedNode.getHeight()/2+ radiusY * Math.sin(angle);
col_count++;
} else {
flag = false;
}
}
linkedNode.setLocation(xLinkedNode, yLinkedNode);
countLinked++;
}
}
}
}
}
private void packCircles(Map<LWComponent,Double> componentRadiusMap,double area,double minX,double minY) {
int iterationCount = 0;
boolean collide = true;
double side = Math.sqrt(area);
Set<LWComponent> nodes = componentRadiusMap.keySet();
while(iterationCount< MAX_ITERATIONS && collide) {
collide = false;
iterationCount++;
int collisionCount = 0;
for(LWComponent node1: nodes) {
for(LWComponent node2: nodes) {
if(node1 != node2) {
double r1 = componentRadiusMap.get(node1);
double r2 = componentRadiusMap.get(node2);
if(checkCollision(node1,node2,r1,r2)){
collide = true;
collisionCount++;
double distance = r1+r2;
if(r1> r2) {
if(Point2D.distance(node1.getX()+node1.getWidth()/2, node1.getY()+node1.getHeight()/2, node2.getX()+node2.getWidth()/2, node2.getY()+node2.getHeight()/2) < r2/100) {
node2.setLocation(minX+Math.random()*side,minY+Math.random()*side);
} else {
if(Math.random()> 0.5 ) {
double angle = Math.atan2(node2.getY() - node1.getY(), node2.getX() - node1.getX());
node2.setLocation(node1.getX() + distance * Math.cos(angle), node1.getY() + distance* Math.sin(angle));
} else {
node2.setLocation(minX+Math.random()*side,minY+Math.random()*side);
}
}
} else {
if(Point2D.distance(node1.getX()+node1.getWidth()/2, node1.getY()+node1.getHeight()/2, node2.getX()+node2.getWidth()/2, node2.getY()+node2.getHeight()/2) < r1/100) {
node1.setLocation(minX+Math.random()*side,minY+Math.random()*side);
} else {
if(Math.random()> 0.5 ) {
double angle = Math.atan2(node1.getY() - node2.getY(), node1.getX() - node2.getX());
node1.setLocation(node2.getX() + distance * Math.cos(angle), node2.getY() + distance* Math.sin(angle));
} else {
node1.setLocation(minX+Math.random()*side,minY+Math.random()*side);
}
}
}
}
}
}
}
// System.out.println("Iteration: "+iterationCount+" collisions:"+collisionCount);
}
}
private boolean checkCollision(LWComponent c1, LWComponent c2,double r1, double r2) {
boolean collide = false;
double distance = Point2D.distance(c1.getX()+c1.getWidth()/2, c1.getY()+c1.getHeight()/2, c2.getX()+c2.getWidth()/2, c2.getY()+c2.getHeight()/2);
if(distance<(r1+r2)) {
collide = true;
}
return collide;
}
}