/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache 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.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* 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.
*/
package at.tuwien.ifs.somtoolbox.visualization.clustering;
import at.tuwien.ifs.somtoolbox.apps.viewer.GeneralUnitPNode;
import at.tuwien.ifs.somtoolbox.layers.Label;
import at.tuwien.ifs.somtoolbox.layers.Unit;
import at.tuwien.ifs.somtoolbox.util.StringUtils;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.nodes.PPath;
import org.apache.commons.lang.ArrayUtils;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.*;
/**
* Class containing one node in the cluster tree.
*
* @author Angela Roiger
* @version $Id: ClusterNode.java 3938 2010-11-17 15:15:25Z mayer $
*/
public class ClusterNode implements Serializable {
public static final Color INTIAL_BORDER_COLOUR = Color.BLACK;
private Color borderColor = INTIAL_BORDER_COLOUR;
private Color selectedBorderColor = Color.RED;
// increment serialVersionUID if class changes so it is incompatible with previous versions (-> [de]serialization )
private static final long serialVersionUID = 2L;
private GeneralUnitPNode[] unitNodes;
private BorderPNode border = null;
private ColoredClusterPNode colorNode = new ColoredClusterPNode(this);
private PNode labelNode = null;
private int level;
private ClusterNode child1 = null;
private ClusterNode child2 = null;
private boolean labelContainsValues = false;
private double factorValue = Double.NaN;
private ClusterLabel[] labels = null;
private double mergeCost;
int numberOfInputs;
private double centroidX = 0;
private double centroidY = 0;
// to store centroid of the weight vectors
private double[] mean = null;
// bounding rectangle:
private double x;
private double y;
private double width;
private double height;
private boolean isSelected;
/**
* Returns the mean vector of the cluster's weight vectors. Calculates it if it is not set yet.
*
* @return the mean vector of the cluster's weight vectors
*/
public double[] getMeanVector() {
int weightVectorLength = getUnitNodes()[0].getUnit().getWeightVector().length;
if (mean == null) {
mean = new double[weightVectorLength];
for (int j = 0; j < weightVectorLength; j++) {
double sum = 0;
for (GeneralUnitPNode unitNode : getUnitNodes()) {
sum = sum + unitNode.getUnit().getWeightVector()[j];
}
mean[j] = sum / getUnitNodes().length;
}
}
return mean;
}
/**
* @return the Centroid of the cluster on the map
*/
public Point2D.Double getCentroid() {
Point2D.Double centroid;
if (centroidX == 0.0d && centroidY == 0.0d) {
double x = 0;
double y = 0;
int i;
for (i = 0; i < getUnitNodes().length; i++) {
x = x + getUnitNodes()[i].getX();
y = y + getUnitNodes()[i].getY();
}
x = x / i + getUnitNodes()[0].getWidth() / 2;
y = y / i + getUnitNodes()[0].getHeight() / 2;
centroid = new Point2D.Double(x, y);
centroidX = x;
centroidY = y;
} else {
centroid = new Point2D.Double(centroidX, centroidY);
}
return centroid;
}
public void setLabelNode(PNode n) {
this.labelNode = n;
}
public PNode getLabelNode() {
return this.labelNode;
}
/**
* Connects two ClusterNodes to one cluster
*
* @param level The level of the new cluster.
*/
public ClusterNode(ClusterNode n1, ClusterNode n2, int level) {
this.level = level;
this.setUnitNodes(new GeneralUnitPNode[n1.getNodes().length + n2.getNodes().length]);
for (int i = 0; i < n1.getNodes().length; i++) {
this.getUnitNodes()[i] = n1.getNodes()[i];
}
for (int i = 0; i < n2.getNodes().length; i++) {
this.getUnitNodes()[i + n1.getNodes().length] = n2.getNodes()[i];
}
this.child1 = n1;
this.child2 = n2;
this.border = makeBorder();
// Adding border as child of the clusternode... makes problems with colored clusters
// addChild(border);
// Bounds - for label placement
// bounds = rectangle around cluster nodes
if (n1.getX() < n2.getX()) {
this.setX(n1.getX());
} else {
this.setX(n2.getX());
}
if (n1.getY() < n2.getY()) {
this.setY(n1.getY());
} else {
this.setY(n2.getY());
}
this.setWidth(Math.max(n1.getX() + n1.getWidth(), n2.getX() + n2.getWidth()) - Math.min(n1.getX(), n2.getX()));
this.setHeight((Math.max(n1.getY() + n1.getHeight(), n2.getY() + n2.getHeight()) - Math.min(n1.getY(),
n2.getY())));
this.colorNode.setBounds(this.getX(), this.getY(), this.getWidth(), this.getHeight());
this.numberOfInputs = n1.getNumberOfInputs() + n2.getNumberOfInputs();
}
public ClusterNode(ClusterNode n1, ClusterNode n2, int level, double mergeCost) {
this(n1, n2, level);
this.mergeCost = mergeCost;
}
/**
* Sets this.label and this.numberOfInputs for this cluster. Used only during initialisation when there is only one
* node per cluster.
*
* @param node the GeneralUnitPNode inside this "Cluster"
*/
private void writeLabelInfos(GeneralUnitPNode node) {
Unit u = node.getUnit();
this.numberOfInputs = u.getNumberOfMappedInputs();
}
/**
* Creates an initial ClusterNode with only one Node inside.
*
* @param leaf the GeneralUnitPNode to be put in the cluster
* @param level a number >= the number of total units.
*/
public ClusterNode(GeneralUnitPNode leaf, int level) {
setUnitNodes(new GeneralUnitPNode[1]);
getUnitNodes()[0] = leaf;
border = makeBorder();
this.level = level;
this.setX(leaf.getX());
this.setY(leaf.getY());
this.setHeight(leaf.getHeight());
this.setWidth(leaf.getWidth());
this.colorNode.setBounds(this.getX(), this.getY(), this.getWidth(), this.getHeight());
writeLabelInfos(leaf);
}
/**
* Calculates and sets new Labels with the given parameters. After calling this function you have to set factorValue
* and labelContainsValues.
*
* @param factorValue A double value between 0 and 1 to determine how much the mean value should influence the
* label.
* @param factorQe A double value between 0 and 1 to determine how much the qe value should influence the label.
* @param factorNumber Currently unused. Should determine the influence of the number of input vectors of the
* clusters.
* @param withValue set to true if you want a label with values
*/
private void calcLabel(double factorValue, double factorQe, double factorNumber, boolean withValue) {
// parameter?? anz inputs(nicht verwendet)
TreeMap<String, Label> allLabels = new TreeMap<String, Label>();
double maxVal = 0;
double maxQe = 0;
int count = 0; // number of units containing inputs
Label[] tmpLabels;// = new Label[5];
for (GeneralUnitPNode unitNode : getUnitNodes()) {
int num = unitNode.getUnit().getNumberOfMappedInputs();
if (num > 0) {
count++;
}
tmpLabels = unitNode.getLabels(Unit.LABELSOM);// change if other types of Labels should also be used
if (tmpLabels != null) {
Label tmpLabel;
for (Label tmpLabel2 : tmpLabels) {
tmpLabel = new Label(tmpLabel2.getName(), tmpLabel2.getValue(), tmpLabel2.getQe());
if (tmpLabel.getValue() > maxVal) {
maxVal = tmpLabel.getValue();
}
if (tmpLabel.getQe() > maxQe) {
maxQe = tmpLabel.getQe();
}
if (allLabels.containsKey(tmpLabel.getName())) {
Label duplicate = allLabels.remove(tmpLabel.getName());
tmpLabel = new Label(tmpLabel.getName(), (tmpLabel.getValue() + duplicate.getValue()),
(tmpLabel.getQe() + duplicate.getQe()) / 2);
}
allLabels.put(tmpLabel.getName(), tmpLabel);
}
}
}
//
// beruecksichigen? anz inputs oder anz units mit inputs
TreeSet<ClusterLabel> tmp = new TreeSet<ClusterLabel>();
for (Label l : allLabels.values()) {
if (!withValue) {
tmp.add(new ClusterLabel(l.getName(), l.getValue() / count, l.getQe() / count, 0 - (l.getValue()
/ (count * maxVal) * factorValue + (1 - l.getQe() / maxQe) * factorQe)));
} else {
tmp.add(new ClusterLabel(l.getName() + "\n" + StringUtils.format(l.getValue() / count, 3), l.getValue()
/ count, l.getQe() / count, 0 - (l.getValue() * factorValue / (count * maxVal) + (1 - l.getQe()
/ maxQe)
* factorQe)));
}
}
this.labels = tmp.toArray(new ClusterLabel[tmp.size()]);
}
/**
* returns current label or null in case label is not set
*/
public ClusterLabel[] getLabels() {
return labels;
}
/**
* calculates and sets the current labels in case label is not set yet or the values have changed and returns it
*/
public ClusterLabel[] getLabels(double fValue, boolean withValue) {
if (labels == null) {
calcLabel(fValue, 1 - fValue, 1, withValue);
}
if (this.factorValue != fValue) {
calcLabel(fValue, 1 - fValue, 1, withValue);
}
if (withValue != this.labelContainsValues) {
calcLabel(fValue, 1 - fValue, 1, withValue);
}
return labels;
}
/**
* Returns the number of input vectors inside this cluster
*
* @return the number of input vectors
*/
public int getNumberOfInputs() {
return numberOfInputs;
}
public double getFactorValue() {
return this.factorValue;
}
/**
* Does the Label of this Cluster contain a value
*/
public boolean getWithValue() {
return this.labelContainsValues;
}
/**
* Set the factor for the value in calculation of the labels - beween 0 and 1. The factor for the qe is
* automatically calculated as 1-d
*
* @param d the factor for the value
*/
public void setFactorValue(double d) {
this.factorValue = d;
}
/**
* Should the label contain a value in the text.
*/
public void setWithValue(boolean b) {
this.labelContainsValues = b;
}
/**
* returns the first child cluster
*/
public ClusterNode getChild1() {
return child1;
}
/**
* returns the second child cluster
*/
public ClusterNode getChild2() {
return child2;
}
/**
* in which level of the clustering tree is this node (1 = top node)
*/
public int getLevel() {
return this.level;
}
/**
* Returns all the {@link GeneralUnitPNode}s contained in this cluster
*/
public GeneralUnitPNode[] getNodes() {
return this.getUnitNodes();
}
public boolean containsNode(GeneralUnitPNode node) {
return ArrayUtils.contains(this.getUnitNodes(), node);
}
public boolean containsAllNodes(Collection<GeneralUnitPNode> nodes) {
if (nodes.size() != getUnitNodes().length) {
return false;
}
for (GeneralUnitPNode node : nodes) {
if (!ArrayUtils.contains(this.getUnitNodes(), node)) {
return false;
}
}
return true;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
/**
* Returns a border for this cluster
*
* @return PNode containing the borders as children
*/
private BorderPNode makeBorder() {
ArrayList<Rectangle2D> lines = new ArrayList<Rectangle2D>();
for (GeneralUnitPNode u : getUnitNodes()) {
double left = u.getX();
double right = u.getX() + u.getWidth();
double top = u.getY();
double bottom = u.getY() + u.getHeight();
xorBorderLine(lines, left, top, right, top); // top line
xorBorderLine(lines, left, bottom, right, bottom); // bottom line
xorBorderLine(lines, left, top, left, bottom); // left line
xorBorderLine(lines, right, top, right, bottom); // right line
}
BorderPNode border = new BorderPNode();
for (Rectangle2D rect : lines) {
PPath line = PPath.createLine((float) rect.getMinX(), (float) rect.getMinY(), (float) rect.getMaxX(),
(float) rect.getMaxY());
line.setStrokePaint(isSelected ? selectedBorderColor : borderColor);
border.addChild(line);
}
return border;
}
public PNode getBorder(Color borderColor) {
this.borderColor = borderColor;
this.border = makeBorder();
return border;
}
/**
* @return the border elements of this cluster in a PNode
*/
public PNode getBorder() {
if (this.border == null) {
this.border = makeBorder();
}
return this.border;
}
// add the line to "lines" if it does not exist yet, otherwise remove it (i.e. do the XOR)
private static void xorBorderLine(ArrayList<Rectangle2D> lines, double x1, double y1, double x2, double y2) {
Rectangle2D rect = new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
if (lines.contains(rect)) {
lines.remove(rect);
} else {
lines.add(rect);
}
}
/**
* Sets the color of the cluster.
*
* @param newPaint The new color of the cluster
*/
public void setPaint(Paint newPaint) {
colorNode.setPaint(newPaint);
}
public ColoredClusterPNode getColoredCluster() {
return this.colorNode;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public double getHeight() {
return height;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public void setHeight(double height) {
this.height = height;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public double getWidth() {
return width;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public void setWidth(double width) {
this.width = width;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public double getX() {
return x;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public void setX(double x) {
this.x = x;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public double getY() {
return y;
}
/**
* get/set Height/Width/X/Y: used for the bounding rectangle of the cluster.
*/
public void setY(double y) {
this.y = y;
}
/** Changes the colour of the cluster borders */
public void setBorderColor(Color c) {
this.borderColor = c;
}
/** @return Returns the mergeCost. */
public double getMergeCost() {
return mergeCost;
}
/**
* @param unitNodes The unitNodes to set.
*/
public void setUnitNodes(GeneralUnitPNode[] unitNodes) {
this.unitNodes = unitNodes;
}
/**
* @return Returns the unitNodes.
*/
public GeneralUnitPNode[] getUnitNodes() {
return unitNodes;
}
}