/* * 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.CommonSOMViewerStateData; import at.tuwien.ifs.somtoolbox.apps.viewer.GeneralUnitPNode; import at.tuwien.ifs.somtoolbox.apps.viewer.handlers.EditLabelEventListener; import edu.uci.ics.jung.graph.DelegateTree; import edu.uci.ics.jung.graph.DirectedSparseGraph; import edu.uci.ics.jung.graph.Tree; import edu.umd.cs.piccolo.PNode; import edu.umd.cs.piccolo.nodes.PText; import java.awt.*; import java.io.Serializable; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * Class for storing the clustering. * * @author Angela Roiger * @version $Id: ClusteringTree.java 3938 2010-11-17 15:15:25Z mayer $ */ public class ClusteringTree extends PNode implements Serializable { public static final float INITIAL_BORDER_WIDTH_MAGNIFICATION_FACTOR = 1.2f; private static final long serialVersionUID = 3918891774261635426l; private ClusterNode topNode; /** contains all currently shown clustering layers */ private SortedMap<Integer, ClusterElementsStorage> allClusteringElements = new TreeMap<Integer, ClusterElementsStorage>(); private static final EditLabelEventListener labelListener = new EditLabelEventListener(); private int startFontSize; private int width; public static final Font defaultFont = new Font("Sans", Font.PLAIN, 40); private Tree<ClusterNode, Integer> jungTree; private HashMap<PNode, Integer> distancesToTopNode; private double minMergeCost = Double.NaN; private double maxMergeCost = Double.NaN; /** * Initializes the tree with the given top Node. * * @param top the top Cluster * @param width the width of the map in Units. The width is needed for initializing the Font size. */ ClusteringTree(ClusterNode top, int width) { this.width = width; topNode = top; startFontSize = 6 * width; // 10-> 6 } public Tree<ClusterNode, Integer> getJUNGTree() { if (jungTree != null) { return jungTree; } jungTree = new DelegateTree<ClusterNode, Integer>(new DirectedSparseGraph<ClusterNode, Integer>()); jungTree.addVertex(topNode); buildJUNGTree(jungTree, topNode, new AtomicInteger(0)); return jungTree; } private void buildJUNGTree(Tree<ClusterNode, Integer> tree, ClusterNode curNode, AtomicInteger curEdge) { if (curNode == null) { return; } ClusterNode c1 = curNode.getChild1(); ClusterNode c2 = curNode.getChild2(); if (c1 != null) { tree.addEdge(curEdge.incrementAndGet(), curNode, c1); } if (c2 != null) { tree.addEdge(curEdge.incrementAndGet(), curNode, c2); } buildJUNGTree(tree, c1, curEdge); buildJUNGTree(tree, c2, curEdge); } public ClusterNode findNode(int lvl) { return findNode(topNode, lvl); } /** * @return null if node could not be found */ public ClusterNode findNode(ClusterNode start, int lvl) { if (start == null) { return null; } if (start.getLevel() == lvl) { return start; } ClusterNode node = findNode(start.getChild1(), lvl); if (node != null) { return node; } return findNode(start.getChild2(), lvl); } /** * Returns the borders and the Labels of the clustering into l clusters and all other currently painted cluster's * borders and labels * * @param l the number of clusters * @return a PNode containing all borders and Labels as children */ public SortedMap<Integer, ClusterElementsStorage> getClusteringInto(int l) { return getClusteringInto(l, false); } /** * Create a new allClusteringElements containing only the sticky layers from the previous version. */ private void clearClusteringElements() { SortedMap<Integer, ClusterElementsStorage> m = new TreeMap<Integer, ClusterElementsStorage>(); for (Integer i : allClusteringElements.keySet()) { ClusterElementsStorage n = allClusteringElements.get(i); if (n.sticky) { m.put(i, n); } } allClusteringElements = m; } /** * Recreates the ClusterElementsStorageNode for layer l. Replaces allClusteringElements with a new HashMap * containing all sticky layers from the previous version and the newly created layer l. * * @param l the number of clusters * @param sticky should this clustering stay visible * @return the updated allClusteringElements */ public SortedMap<Integer, ClusterElementsStorage> getClusteringInto(int l, boolean sticky) { CommonSOMViewerStateData state = CommonSOMViewerStateData.getInstance(); clearClusteringElements(); ArrayList<ClusterNode> clusterStorage = new ArrayList<ClusterNode>(); ClusterElementsStorage store = new ClusterElementsStorage(); ArrayList<PNode> allLabels = new ArrayList<PNode>(); // to draw all clusters until level l we must find all children of clusters above "level" getAllChildrenUntil(l - 1, topNode, clusterStorage); if (state.clusterWithLabels > 0) { // loop through all ClusterNodes for (ClusterNode oneCluster : clusterStorage) { ClusterLabel[] tmpLabels = oneCluster.getLabels(state.clusterByValue, state.labelsWithValues); if (tmpLabels != null && tmpLabels.length > 0) { PText labelText = null; PNode bigLabel = oneCluster.getLabelNode(); if (bigLabel == null) { int numLabelShown = Math.min(state.clusterWithLabels, tmpLabels.length); bigLabel = new PNode(); // first(big) Label labelText = new PText(tmpLabels[0].getName()); labelText.addAttribute("type", "clusterLabel"); bigLabel.addChild(labelText); for (int i = 1; i < numLabelShown; i++) { // other(smaller) labels: labelText = new PText(tmpLabels[i].getName()); labelText.addAttribute("type", "smallClusterLabel"); bigLabel.addChild(labelText); } LabelPositioning.center(oneCluster, bigLabel); bigLabel.addInputEventListener(labelListener); } else { // edit old Labels // only change labeltext if something changed... to keep manually edited labels int numLabelsShown = Math.min(state.clusterWithLabels, tmpLabels.length); int i; if (oneCluster.getWithValue() != state.labelsWithValues || oneCluster.getFactorValue() != state.clusterByValue) { for (i = 0; i < bigLabel.getChildrenCount(); i++) { if (i < bigLabel.getChildrenCount()) { labelText = (PText) bigLabel.getChild(i); labelText.setText(tmpLabels[i].getName()); } } oneCluster.setWithValue(state.labelsWithValues); oneCluster.setFactorValue(state.clusterByValue); } for (i = bigLabel.getChildrenCount(); i < numLabelsShown; i++) { labelText = new PText(tmpLabels[i].getName()); labelText.addAttribute("type", "smallClusterLabel"); bigLabel.addChild(labelText); } // remove unneeded Labels for (int j = numLabelsShown; j < bigLabel.getChildrenCount(); j++) { bigLabel.removeChild(j); } } allLabels.add(bigLabel); oneCluster.setLabelNode(bigLabel); } } } else { allLabels = null; } // move all borders and colors to Arraylists store.clusterBorders = new ArrayList<PNode>(); ClusterElementsStorage colorStore = new ClusterElementsStorage(); colorStore.clusterColors = new ArrayList<ColoredClusterPNode>(); for (ClusterNode element : clusterStorage) { store.clusterBorders.add(element.getBorder(state.clusterBorderColour)); colorStore.clusterColors.add(element.getColoredCluster()); } store.clusterLabels = allLabels; store.sticky = sticky; allClusteringElements.put(new Integer(l), store); resizeLabelsAndBorders(state.clusterBorderWidthMagnificationFactor); // store the coloring // highest integer value to make sure it will always be the last element when painting allClusteringElements.put(new Integer(Integer.MAX_VALUE), colorStore); return allClusteringElements; } private double getMaxMergeCost(ClusterNode node, double curMax) { if (node == null) return curMax; if(node.getMergeCost() > curMax) { curMax = node.getMergeCost(); } curMax = getMaxMergeCost(node.getChild1(), curMax); curMax = getMaxMergeCost(node.getChild2(), curMax); return curMax; } public double getMaxMergeCost() { if(Double.isNaN(maxMergeCost)) maxMergeCost = getMaxMergeCost(topNode, topNode.getMergeCost()); return maxMergeCost; } private double getMinMergeCost(ClusterNode node, double curMin) { if (node == null) return curMin; if(node.getMergeCost() < curMin) { curMin = node.getMergeCost(); } curMin = getMinMergeCost(node.getChild1(), curMin); curMin = getMinMergeCost(node.getChild2(), curMin); return curMin; } public double getMinMergeCost() { if(Double.isNaN(minMergeCost)) minMergeCost = getMinMergeCost(topNode, topNode.getMergeCost()); return minMergeCost; } public HashMap<PNode, Integer> getDendrogramDistanceInfo() { // Foreach of the notes calculate distance to top note distancesToTopNode = new HashMap<PNode, Integer>(); recursiveCalculateDistances(topNode, 0); return distancesToTopNode; } private void recursiveCalculateDistances(ClusterNode parent, Integer distance) { ClusterNode child1 = parent.getChild1(); ClusterNode child2 = parent.getChild2(); if (child1 != null) { recursiveCalculateDistances(child1, distance + 1); } if (child2 != null) { recursiveCalculateDistances(child2, distance + 1); } // If it's a leaf if (child1 == null && child2 == null) { for (PNode node : parent.getUnitNodes()) { distancesToTopNode.put(node, distance); } } } public int compareClusterDistanceOfPNodes(PNode node1, PNode node2) { return Math.abs(distancesToTopNode.get(node1) - distancesToTopNode.get(node2)); } /** * give some advanced debug output */ public void dumpAllClusteringElements() { System.out.println("clustering elements dump:"); for (Integer i : allClusteringElements.keySet()) { System.out.print(i + ": "); for (PNode pNode : allClusteringElements.get(i).clusterLabels) { PText label = (PText) pNode.getChild(0); System.out.print(label.getText() + " "); } System.out.println(); } } // Function for recursion in resizeLabelsAndBorders() private void resizeLabelsAndBorders(TreeSet<Integer> ts, int fontSize, float borderWidth) { Integer c = ts.first(); ts.remove(ts.first()); // remove until the last element if (!ts.isEmpty()) { resizeLabelsAndBorders(ts, fontSize * 2 / 3, borderWidth * 2 / 3); } // now change FontSize ClusterElementsStorage n = allClusteringElements.get(c); n.changeFont(defaultFont.deriveFont(new Float(fontSize).floatValue())); n.changeBorderStroke(new BasicStroke(borderWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); } /** * Resizes all labels so that labels of a lower layer are 1/3 smaller than the one above. The maximum font * size/border width is set by the constructor. */ private void resizeLabelsAndBorders(float borderWidthFactor) { TreeSet<Integer> ts = new TreeSet<Integer>(allClusteringElements.keySet()); int fontSize = startFontSize; resizeLabelsAndBorders(ts, fontSize, width * borderWidthFactor); } public SortedMap<Integer, ClusterElementsStorage> getAllClusteringElements() { return allClusteringElements; } /** * Searches the clusters that are less or equal lvl and stores their children (ClusterNodes) which are > lvl. Used * to get a clustering into lvl+1 clusters. * * @param level the level. * @param start the top ClusterNode * @param store a ArrayList to store the results */ private void getAllChildrenUntil(int level, ClusterNode start, ArrayList<ClusterNode> store) { if (start.getLevel() > level) { // I'm a level lvl child -- add myself and return store.add(start); } else { // not a level lvl child yet -- dig deeper getAllChildrenUntil(level, start.getChild1(), store); getAllChildrenUntil(level, start.getChild2(), store); } } /** Find the {@link ClusterNode} that contains the given {@link GeneralUnitPNode} at the given level. */ public ClusterNode findClusterOf(GeneralUnitPNode unitPNode, int level) { // printTree(topNode, 0); ArrayList<ClusterNode> store = new ArrayList<ClusterNode>(); getAllChildrenUntil(level - 1, topNode, store); for (ClusterNode clusterNode : store) { if (clusterNode.containsNode(unitPNode)) { return clusterNode; } } return null; } public void printTree(ClusterNode start, int x) { for (int i = 0; i < x; i++) { System.out.print(" "); } if (start != null) { System.out.println((x > 0 ? "--> " : "") + start.getLevel()); printTree(start.getChild1(), x + 1); printTree(start.getChild2(), x + 1); } else { System.out.println("--> empty"); } } /** * Adds the EditLabelEventLister to all Labels. Used after deserialization. */ public void addEditLabelEventListenerToAll() { for (ClusterElementsStorage clusterElementsStorage : allClusteringElements.values()) { ArrayList<PNode> labelsArray = clusterElementsStorage.clusterLabels; // 16.1.07 added if if (labelsArray != null) { for (PNode labels : labelsArray) { labels.addInputEventListener(labelListener); } } } } /** * Changes the colors of the tree according to the currently chosen palette. In each step the palette is split in 2 * halves and each child cluster gets one half. The color of a cluster shown on the screen is the color * "in the middle" of its palette. This means in the worst case there are 2^n colors needed to paint n clusters, but * this also means that close clusters will have more similar colors than outliers. */ public void recolorTree() { recolorTree(getPalette(), topNode); } /** * Function for recursion in recolorTree() * * @param col Color[] contaning the Palette */ private void recolorTree(Color[] col, ClusterNode n) { if(n == null) return; CommonSOMViewerStateData state = CommonSOMViewerStateData.getInstance(); int pos = (int) (((n.getMergeCost() - getMinMergeCost()) / ( getMaxMergeCost() - getMinMergeCost())) * (col.length-1)); Color curCol = state.colorClusters ? col[pos] : Color.WHITE; n.setPaint(curCol); recolorTree(col, n.getChild1()); recolorTree(col, n.getChild2()); /* if (n == null) { return; } if (state.colorClusters) { n.setPaint(col[col.length / 2]); } else { n.setPaint(null); } if (col.length > 1 && state.colorClusters) { Color[] tmp1 = new Color[col.length / 2]; Color[] tmp2 = new Color[col.length - col.length / 2]; System.arraycopy(col, 0, tmp1, 0, tmp1.length); System.arraycopy(col, tmp1.length, tmp2, 0, tmp2.length); if (n.getChild1() == null) { return; } Point2D.Double c1 = n.getChild1().getCentroid(); Point2D.Double c2 = n.getChild2().getCentroid(); if (c1.x < c2.x || c1.x == c2.x && c1.y < c2.y) { recolorTree(tmp1, n.getChild1()); recolorTree(tmp2, n.getChild2()); } else { recolorTree(tmp2, n.getChild1()); recolorTree(tmp1, n.getChild2()); } } else { recolorTree(col, n.getChild1()); recolorTree(col, n.getChild2()); } */ } /** * Gest the current palette from the state. * * @return the curren palette. */ public Color[] getPalette() { CommonSOMViewerStateData state = CommonSOMViewerStateData.getInstance(); return state.getSOMViewer().getCurrentlySelectedPalette().getColors(); } // Doris public int[][] getClusterAssignment(int level, int xSize, int ySize) { int[][] assignment = new int[xSize][ySize]; for (int a = 0; a < xSize; a++) { Arrays.fill(assignment[a], -1); } ArrayList<ClusterNode> list = new ArrayList<ClusterNode>(); getAllChildrenUntil(level - 1, topNode, list); int clusterNo = 0; for (ClusterNode curcluster : list) { GeneralUnitPNode[] unitNodes = curcluster.getNodes(); for (GeneralUnitPNode unitNode : unitNodes) { int x = unitNode.getUnit().getXPos(); int y = unitNode.getUnit().getYPos(); assignment[x][y] = clusterNo; } clusterNo++; } return assignment; } public ArrayList<ClusterNode> getNodesAtLevel(int level) { ArrayList<ClusterNode> list = new ArrayList<ClusterNode>(); getAllChildrenUntil(level - 1, topNode, list); return list; } }