at.tuwien.ifs.somtoolbox.apps.viewer.GeneralUnitPNode.java Source code

Java tutorial

Introduction

Here is the source code for at.tuwien.ifs.somtoolbox.apps.viewer.GeneralUnitPNode.java

Source

/*
 * 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.apps.viewer;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.io.ObjectStreamException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.logging.Logger;

import at.tuwien.ifs.somtoolbox.layers.grid.GridGeometry;
import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolo.nodes.PText;
import edu.umd.cs.piccolo.util.PPaintContext;

import at.tuwien.ifs.somtoolbox.apps.viewer.PieChartPNode.PieChartLabelMode;
import at.tuwien.ifs.somtoolbox.apps.viewer.controls.MapOverviewPane;
import at.tuwien.ifs.somtoolbox.data.SOMLibClassInformation;
import at.tuwien.ifs.somtoolbox.data.SOMLibDataInformation;
import at.tuwien.ifs.somtoolbox.layers.Label;
import at.tuwien.ifs.somtoolbox.layers.Unit;
import at.tuwien.ifs.somtoolbox.layers.quality.QualityMeasure;
import at.tuwien.ifs.somtoolbox.layers.quality.QualityMeasureNotFoundException;
import at.tuwien.ifs.somtoolbox.util.StringUtils;
import org.apache.commons.math.geometry.Vector3D;

/**
 * The graphical representation of one SOM Unit, including labels, data items and class pie charts. This class makes use
 * of the <a href="http://www.cs.umd.edu/hcil/jazz/" target="_blank">Piccolo framework</a> and is the top {@link PNode}
 * for all unit-level visualisations. This PNode has one of the four child nodes, depending on the current zoom level of
 * the {@link SOMViewer} application - {@link GeneralUnitPNode#DETAIL_LEVEL_NO},
 * {@link GeneralUnitPNode#DETAIL_LEVEL_LOW}, {@link GeneralUnitPNode#DETAIL_LEVEL_MEDIUM} and
 * {@link GeneralUnitPNode#DETAIL_LEVEL_HIGH}. Each of the four different PNodes represents four different levels of
 * information details displayed. Each those detail nodes has potentially other child nodes:
 * <ul>
 * <li>The pie-charts representing class information - {@link PieChartPNode}</li>
 * <li>A {@link PNode} for holding the Labels, which are {@link PText} objects</li>
 * <li>A {@link PNode} for holding the data item names, which are {@link PText} objects</li>
 * <li>A {@link PNode} for holding the quality measure value, which is a {@link PText} object</li>
 * </ul>
 * 
 * @author Michael Dittenbach
 * @author Rudolf Mayer
 * @version $Id: GeneralUnitPNode.java 4109 2011-02-05 21:54:36Z mayer $
 */
public class GeneralUnitPNode extends PNode {
    private static final long serialVersionUID = 1L;

    public static final int DETAIL_LEVEL_NO = 0;

    public static final int DETAIL_LEVEL_LOW = 1;

    public static final int DETAIL_LEVEL_MEDIUM = 2;

    public static final int DETAIL_LEVEL_HIGH = 3;

    public static final int NUMBER_OF_DETAIL_LEVELS = 4;

    /**
     * Names for the different zoom/scale levels, corresponding to {@link #DETAIL_LEVEL_NO}, {@link #DETAIL_LEVEL_LOW},
     * ..
     */
    public static final String[] detailLevelNames = { "none", "low", "medium", "high" };

    public static final int DATA_DISPLAY_VARIANT_INPUTOBJECT = 0;

    public static final int DATA_DISPLAY_VARIANT_INPUTOBJECTSHIFTED = 1;

    public static final int DATA_DISPLAY_VARIANT_TEXT = 2;

    public static final int DATA_DISPLAY_VARIANTS = 3;

    private static int[] NUMBER_OF_LABELS = { 1, 2, 3, -1 };

    private static int[] NUMBER_OF_COLUMNS = { 1, 1, 1, 3 };

    private static int[] FONT_SIZE_LABELS = { 25, 20, 12, 6 };

    private static int[] MAX_LABEL_LENGTH = { 9, 9, 20, 10 };

    private static int[] FONT_SIZE_DATA = { 40, 40, 36, 4 };

    private int currentDetailLevel = DETAIL_LEVEL_NO;

    private Unit u = null;

    private double X = 0;

    private double Y = 0;

    private double width = 0;

    private double height = 0;

    private boolean drawBorder = true;

    private Rectangle2D border = null;

    private final Color borderColor = Color.gray;

    private final BasicStroke borderStroke = new BasicStroke(1.0f);

    private int[] selectedClassIndices = null;

    private boolean showOnlySelectedClasses = false;

    private boolean selected = false;

    private PPath selectionMarker = null;

    private ArrayList<ArrowPNode> arrowsFromThisUnit = new ArrayList<ArrowPNode>();

    private double oldScale = 1;

    private double currentScale = 1;

    /** Nodes for details at different levels */
    private PNode[] detailNodes = new PNode[NUMBER_OF_DETAIL_LEVELS];

    /**
     * Data input nodes for different levels, length of {@link #NUMBER_OF_DETAIL_LEVELS}, contains a {@link PText} at
     * {@link #DATA_DISPLAY_VARIANT_TEXT}, and {@link InputPNode}s at {@link #DATA_DISPLAY_VARIANT_INPUTOBJECT} and
     * {@link #DATA_DISPLAY_VARIANT_INPUTOBJECTSHIFTED}.
     */
    private PNode[][][] dataDetail = new PNode[NUMBER_OF_DETAIL_LEVELS][DATA_DISPLAY_VARIANTS][];

    private PNode[] dataCountDetail = new PNode[NUMBER_OF_DETAIL_LEVELS];

    /** Label nodes for different levels */
    private PNode[] labelDetailNodes = new PNode[NUMBER_OF_DETAIL_LEVELS];

    /** PieChart nodes for different levels */
    private PieChartPNode[] pieChartDetailNodes = new PieChartPNode[NUMBER_OF_DETAIL_LEVELS];

    private SOMLibClassInformation classInfo = null;

    private SOMLibDataInformation dataInfo = null;

    private boolean classInfoSelectionChanged = false;

    private PPath queryResultMarker;

    private Point[][] locations;

    private CommonSOMViewerStateData state;

    /**
     * Constructor for mnemonic (sparse) SOMs. Initialises an empty cell with no unit attached.
     */
    public GeneralUnitPNode(int x, int y, double width, double height) {
        setPickable(false);
        setChildrenPickable(false);
        X = x * width;
        Y = y * height;
        initPNodeProperties(width, height);
        addChild(detailNodes[DETAIL_LEVEL_NO]);
    }

    public GeneralUnitPNode(Unit u, CommonSOMViewerStateData state, SOMLibClassInformation classInfo,
            SOMLibDataInformation dataInfo, double width, double height) {
        this(u, state, classInfo, dataInfo, null, width, height);
    }

    public GeneralUnitPNode(Unit u, CommonSOMViewerStateData state, SOMLibClassInformation classInfo,
            SOMLibDataInformation dataInfo, Point[][] locations, double width, double height) {
        this.state = state;
        this.u = u;

        GridGeometry helper = state.growingSOM.getLayer().getGridGeometry();
        Point p = helper.getShapeBorderPointTopLeft(u.getXPos(), u.getYPos(), width, height);
        X = p.x;
        Y = p.y;

        initPNodeProperties(width, height);

        this.classInfo = classInfo;
        this.dataInfo = dataInfo;
        this.locations = locations;

        if (classInfo != null) { // class information present, generate pie chart
            initClassPieCharts(u, classInfo, width, height);
        }
        for (int i = 0; i < detailNodes.length; i++) {
            if (detailNodes[i] == null) {
                detailNodes[i] = new PNode();
            }
        }

        if (u.getNumberOfMappedInputs() > 0) {
            initDetails();
        }
        addChild(detailNodes[DETAIL_LEVEL_MEDIUM]);
    }

    public GeneralUnitPNode(Unit u, GeneralUnitPNode clone) {
        this(u, clone.state, clone.classInfo, clone.dataInfo, clone.locations, clone.width, clone.height);
    }

    private void addDataChildren() {
        PNode[] nodes = dataDetail[currentDetailLevel][getDataInputVariant()];
        if (state.dataVisibilityMode) {
            if (nodes != null) {
                for (PNode node2 : nodes) {
                    PNode node = new PNode();
                    node.addChild(node2);
                    addChild(node);
                }
            }
        }
    }

    public void reInitUnitDetails() {
        if (u.getNumberOfMappedInputs() > 0) {
            removeDetailNodes();
            for (PNode detailNode : detailNodes) {
                detailNode.removeAllChildren();
                removeAllChildren();
            }
            initDetails();
            addChild(detailNodes[currentDetailLevel]);
            addDataChildren();
        }
    }

    public void reInitUnitDetails(int detailLevel) {
        if (u.getNumberOfMappedInputs() > 0) {
            removeDetailNodes();
            // re-init changed one
            if (detailLevel >= DETAIL_LEVEL_NO && detailLevel <= DETAIL_LEVEL_HIGH) {
                detailNodes[detailLevel].removeAllChildren();
                removeAllChildren();
                for (int i = 0; i < DATA_DISPLAY_VARIANTS; i++) {
                    dataDetail[detailLevel][i] = null;
                }
            }
            initDetails(detailLevel);
            addChild(detailNodes[currentDetailLevel]);
            addDataChildren();
        }
    }

    /** remove currently added detail levels. */
    private void removeDetailNodes() {
        for (PNode detailNode : detailNodes) {
            if (detailNode.isDescendentOf(this)) {
                removeChild(detailNode);
            }
        }
    }

    /**
     * Initializes common properties for unit PNodes and empty PNodes
     */
    private void initPNodeProperties(double width, double height) {
        border = new Rectangle2D.Double();
        GridGeometry helper = state.growingLayer.getGridGeometry();
        this.width = helper.adjustUnitWidth(width, height);
        this.height = helper.adjustUnitHeight(width, height);
        border.setRect(X, Y, width, height);
        this.setBounds(X, Y, width, height);
        selectionMarker = PPath.createRectangle((float) X, (float) Y, (float) width, (float) height);
        selectionMarker.setPaint(Color.decode("#ff7505"));
        selectionMarker.setTransparency(0.5f);

        queryResultMarker = PPath.createRectangle((float) X, (float) Y, (float) width, (float) height);
        queryResultMarker.setPaint(Color.ORANGE);
        queryResultMarker.setTransparency(0.5f);
    }

    public void initClassPieCharts(Unit u, SOMLibClassInformation classInfo, double width, double height) {
        this.classInfo = classInfo;
        if (classInfo != null) { // class information present, generate pie chart
            int[] values = classInfo.computeClassDistribution(u.getMappedInputNames());
            // TODO: debug output (can be used for cluster purity calculator in the future)
            // DoubleMatrix1D dummy = new DenseDoubleMatrix1D(values);
            // System.out.println("Cluster purity for unit ("+u.getXPos()+"/"+u.getYPos()+"):");
            // for (int cc=0; cc<classInfo.numClasses(); cc++) {
            // System.out.println(" "+classInfo.classNames()[cc]+" "+(values[cc]/u.getNumberOfMappedInputs()));
            // }
            // end debug

            // FIXME: maybe reuse the PieChartPNode for each detail level?
            pieChartDetailNodes[DETAIL_LEVEL_NO] = new PieChartPNode(0, 0, width, height, values,
                    classInfo.getClassNames(), u.getNumberOfMappedInputs());
            pieChartDetailNodes[DETAIL_LEVEL_LOW] = new PieChartPNode(0, 0, width, height, values,
                    classInfo.getClassNames(), u.getNumberOfMappedInputs());
            pieChartDetailNodes[DETAIL_LEVEL_MEDIUM] = new PieChartPNode(0, 0, width, height, values,
                    classInfo.getClassNames(), u.getNumberOfMappedInputs());
        }
    }

    /**
     * Initializes text labels for this node. The labels are displayed in a virtual table like structure.
     * 
     * @param numLabels Number of labels to be shown. If '-1' the limit is set to the number of labels in unit
     *            description file.
     * @param numCol Number of table columns. Setting to '1' means row table.
     * @param fontSize Font size to use for drawing labels
     * @param length Maximum label length, longer labels will be trunkated
     */
    private PNode initLabels(int numLabels, final int numCol, final int fontSize, final int length) {
        // -1 means we want to show all labels. additionally check if we have enough labels...
        if (numLabels == -1 || u.getLabels().length < numLabels) {
            numLabels = u.getLabels().length;
        }

        PNode labelsNode = new PNode();
        if (u.getLabels() != null) {
            Font labelsFont = new Font("Sans", Font.PLAIN, fontSize);

            double xOffset[] = new double[numCol];
            for (int i = 0; i < numCol; i++) {
                xOffset[i] = 2 + i * width / numCol;
            }
            double yOffset = 2;
            int columnIndex = 0;

            for (int i = 0; i < numLabels; i++) {
                PText label = new PText(u.getLabels()[i].getName());
                label.setPickable(false);
                // TODO: find a better way to set the colour. maybe the current palette can give the preferred label
                // colour?
                if (state.exactUnitPlacement && getMapPNode().getCurrentVisualization() == null) { // white labels for
                    // sky vis
                    label.setTextPaint(Color.WHITE);
                } else {
                    label.setTextPaint(Color.BLACK);
                }
                label.setFont(labelsFont);
                label.setOffset(xOffset[columnIndex], yOffset); // (i * labelsFont.getSize()) + 2
                label.addAttribute("tooltip",
                        u.getLabels()[i].getName() + "\nqe: " + StringUtils.format(u.getLabels()[i].getQe(), 3)
                                + "\nmean: " + StringUtils.format(u.getLabels()[i].getValue(), 3));
                label.addAttribute("type", "label");
                labelsNode.addChild(label);

                if (label.getBoundsReference().getWidth() > width / numCol) {
                    label.setText(label.getText().substring(0, Math.min(length, label.getText().length())) + "...");
                }

                if ((i + 1) % numCol == 0) { // Next row
                    yOffset += labelsFont.getSize() + 2;
                    columnIndex = 0;
                } else { // Next column
                    columnIndex++;
                }
            }
        }
        return labelsNode;
    }

    public MapPNode getMapPNode() {
        // parent is the PNode holding all GeneralUnitPNodes, the parent of which in turn is the MapPNode
        return (MapPNode) getParent().getParent();
    }

    /**
     * Initializes textual details
     * 
     * @param threshold Displays description of mapped inputs below certain threshold (count), otherwise a count is
     *            displayed.
     * @param fontSize Text size
     * @param yInOffset Positional offset
     */
    private PNode[] initData(int threshold, int fontSize, double yInOffset, int detailLevel, int variant) {
        final String commonVectorLabelPrefix = state.growingLayer.getCommonVectorLabelPrefix();
        int nodesToDisplay = u.getNumberOfMappedInputs() * threshold / 100;

        if (variant == DATA_DISPLAY_VARIANT_INPUTOBJECT || variant == DATA_DISPLAY_VARIANT_INPUTOBJECTSHIFTED) { // Display
            // stars
            // or
            // simply
            // count
            if (locations != null) {
                PNode[] dataNodes = new PNode[nodesToDisplay];
                for (int index = 0; index < nodesToDisplay; index++) {
                    PNode star = new StarPNode();
                    String inputName = u.getMappedInputName(index);
                    String inputDistance = StringUtils.format(u.getMappedInputDistance(index), 3);
                    String inputText = "";

                    try {
                        if (dataInfo != null) {
                            inputText = URLDecoder.decode(dataInfo.getDataDisplayName(inputName), "UTF-8");
                        } else {
                            inputText = URLDecoder.decode(inputName, "UTF-8");
                        }
                    } catch (Exception e) {
                        inputText = inputName;
                    }
                    star.addAttribute("id", inputName.replaceFirst(commonVectorLabelPrefix, ""));
                    star.addAttribute("type", "data");
                    star.setPickable(true);
                    if (dataInfo != null) {
                        try {
                            star.addAttribute("tooltip", inputText + "\ndistance: " + inputDistance + "\n"
                                    + URLDecoder.decode(dataInfo.getDataLocation(inputName), "UTF-8"));
                        } catch (Exception e) {
                            Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
                                    "URLDecoder had problems reading the name of the datum '" + inputName + "'.");
                            star.addAttribute("tooltip", inputText + "\ndistance: " + inputDistance);
                        }
                        star.addAttribute("location", dataInfo.getBaseDir() + dataInfo.getDataLocation(inputName));
                    } else {
                        star.addAttribute("tooltip", inputText + "\ndistance: " + inputDistance);
                        star.addAttribute("location", inputName);
                    }
                    star.setOffset(this.X + locations[variant][index].getX(),
                            this.Y + locations[variant][index].getY());
                    dataNodes[index] = star;
                }
                return dataNodes;
            } else {
                return new PNode[0];
            }
        } else {
            PNode[] dataNodes = new PNode[nodesToDisplay];
            if (detailLevel == DETAIL_LEVEL_HIGH) {
                Font font = new Font("Sans", Font.PLAIN, fontSize);
                final int numDataCol = 3;
                double xDataOffsets[] = new double[numDataCol];
                for (int i = 0; i < numDataCol; i++) {
                    xDataOffsets[i] = i * width / numDataCol + 2;
                }
                double yOffset = yInOffset + dataCountDetail[detailLevel].getBoundsReference().getHeight() + 2;
                int numData = Math.min(dataNodes.length, u.getNumberOfMappedInputs()); // limitation by number of labels
                // in unit description file
                int x = 0;
                for (int index = 0; index < numData; index++) {
                    PText pText = null;
                    final String inputName = u.getMappedInputNames()[index];
                    String inputText = null;
                    try {
                        if (dataInfo != null) {
                            inputText = URLDecoder.decode(dataInfo.getDataDisplayName(inputName), "UTF-8");
                        } else {
                            inputText = URLDecoder.decode(inputName, "UTF-8");
                        }
                    } catch (Exception e) {
                        Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
                                "URLDecoder had problems reading the name of the datum '" + inputName + "'.");
                        inputText = inputName;
                    }
                    pText = new PText(inputText.replaceFirst(commonVectorLabelPrefix, ""));

                    pText.addAttribute("id", inputName);
                    pText.addAttribute("type", "data");
                    pText.setFont(font);
                    pText.setTextPaint(Color.BLUE);
                    pText.setOffset(this.X + xDataOffsets[x], this.Y + yOffset);
                    if (dataInfo != null) {
                        try {
                            pText.addAttribute("tooltip",
                                    inputText + "\ndistance: "
                                            + StringUtils.format(u.getMappedInputDistances()[index], 3) + "\n"
                                            + URLDecoder.decode(dataInfo.getDataLocation(inputName), "UTF-8"));
                        } catch (Exception e) {
                            Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
                                    "URLDecoder had problems reading the name of the datum '" + inputName + "'.");
                            pText.addAttribute("tooltip", pText.getText() + "\ndistance: "
                                    + StringUtils.format(u.getMappedInputDistances()[index], 3));
                        }
                        pText.addAttribute("location", dataInfo.getBaseDir() + dataInfo.getDataLocation(inputName));
                    } else {
                        pText.addAttribute("tooltip", inputText + "\ndistance: "
                                + StringUtils.format(u.getMappedInputDistances()[index], 3));
                        pText.addAttribute("location", inputName);
                    }
                    dataNodes[index] = pText;
                    if (pText.getBoundsReference().getWidth() > width / numDataCol) {
                        pText.setText(pText.getText().substring(0, Math.min(16, pText.getText().length())) + "...");
                    }
                    x++;
                    if ((index + 1) % numDataCol == 0) { // next line
                        yOffset += font.getSize() + 2; // TODO: check qualityHighYOffset!!!!
                        x = 0;
                    }
                }
                return dataNodes;
            }
        }
        return new PNode[0];
    }

    /**
     * Initializes the details node. A pie chart is generated if class info is available (except
     * {@link #DETAIL_LEVEL_HIGH}, quality measure info is added for {@link #DETAIL_LEVEL_HIGH}.
     */
    private void initDetails() {
        for (int level = 0; level < NUMBER_OF_DETAIL_LEVELS; level++) {
            initDetails(level);
        }
    }

    private void initDetails(int level) {
        detailNodes[level].setOffset(this.X, this.Y);
        double xOffset = 0;
        double yOffset = 0;

        // labels
        if (u.getLabels() != null && state.labelVisibilityMode) {
            if (labelDetailNodes[level] == null) {
                labelDetailNodes[level] = initLabels(NUMBER_OF_LABELS[level], NUMBER_OF_COLUMNS[level],
                        FONT_SIZE_LABELS[level], MAX_LABEL_LENGTH[level]);
            }
            detailNodes[level].addChild(labelDetailNodes[level]);
            yOffset = labelDetailNodes[level].getFullBoundsReference().getHeight();
            if (yOffset > state.maxLabelYOffset[level]) {
                state.maxLabelYOffset[level] = yOffset;
            }
        }

        // quality, for high level only
        if (level == DETAIL_LEVEL_HIGH) {
            QualityMeasure qm = null;
            if ((qm = u.getLayer().getQualityMeasure()) != null) {
                PNode qualityHigh = new PNode();
                Font qualityHighFont = new Font("Sans", Font.PLAIN, 4);

                try {
                    for (int i = 0; i < qm.getUnitQualityNames().length; i++) {
                        PText qmText = new PText(qm.getUnitQualityNames()[i] + ": " + StringUtils.format(
                                qm.getUnitQualities(qm.getUnitQualityNames()[i])[u.getXPos()][u.getYPos()], 3));
                        qmText.setPickable(false);
                        qmText.setFont(qualityHighFont);
                        qmText.setOffset(xOffset, 0);
                        xOffset += qmText.getWidth() + 4;
                    }
                } catch (QualityMeasureNotFoundException e) {
                    Logger.getLogger("at.tuwien.ifs.somtoolbox")
                            .severe(e.getMessage() + " Aborting. BTW: the must be a major flaw"
                                    + "in the quality measure class that has been used.");
                    System.exit(-1);
                }

                double qualityHighYOffset = height - (qualityHigh.getFullBoundsReference().getHeight() + 2);
                qualityHigh.setOffset(2, qualityHighYOffset);
                detailNodes[level].addChild(qualityHigh);
            }
        }

        if (dataCountDetail[level] == null) {
            Font font = new Font("Sans", Font.PLAIN, FONT_SIZE_DATA[level]);
            String inputCount = String.valueOf(u.getNumberOfMappedInputs());
            PText numDataText = new PText();
            if (level == DETAIL_LEVEL_NO || level == DETAIL_LEVEL_LOW || level == DETAIL_LEVEL_MEDIUM) {
                numDataText.setText(inputCount);
                // FIXME: use a calculated xOffset rather than a fixed one, e.g. as below
                // double xOffsetNumData = (width - defaultToolkit.getFontMetrics(font).stringWidth(inputCount)) / 2;
                numDataText.setOffset(2, (height - yOffset) / 2 - numDataText.getFont().getSize() / 2 + yOffset);
                dataCountDetail[level] = numDataText;
            } else if (level == DETAIL_LEVEL_HIGH) {
                numDataText.setText("Number of data items: " + inputCount);
                numDataText.setOffset(2, yOffset);
                dataCountDetail[level] = numDataText;
            }
            numDataText.setPickable(false);
            numDataText.setFont(font);
        }
        dataCountDetail[level].setVisible(state.hitsVisibilityMode);
        detailNodes[level].addChild(dataCountDetail[level]);

        // data
        for (int i = 0; i < DATA_DISPLAY_VARIANTS; i++) {
            if (dataDetail[level][i] == null) {
                dataDetail[level][i] = initData(state.thresholdInputPercentage[level], FONT_SIZE_DATA[level],
                        yOffset, level, i);
            }
        }

        // class info, not for high details
        if (level != DETAIL_LEVEL_HIGH) {
            if (classInfo != null) { // class information present, generate pie chart
                double pWidth = width - xOffset;
                double pHeight = height - (level == DETAIL_LEVEL_NO ? height / 5
                        : dataCountDetail[level].getFullBoundsReference().getHeight());
                pieChartDetailNodes[level].setBounds(xOffset, 0, pWidth, pHeight);
                if (state.getClassPiechartMode() != SOMViewer.TOGGLE_PIE_CHARTS_NONE) {
                    detailNodes[level].addChild(pieChartDetailNodes[level]);
                }
            }
        }
    }

    /** Updates the units displayed info by removing & re-creating them. */
    public void updateDetailsAfterMoving() {
        removeDetailNodes();
        for (int detailLevel = 0; detailLevel < detailNodes.length; detailLevel++) {
            detailNodes[detailLevel].removeAllChildren();
            removeAllChildren();
            for (int i = 0; i < DATA_DISPLAY_VARIANTS; i++) {
                dataDetail[detailLevel][i] = null;
                dataCountDetail[detailLevel] = null;
                pieChartDetailNodes[detailLevel] = null;
            }
            if (u.getNumberOfMappedInputs() > 0) {
                initDetails(detailLevel);
            }
        }
        addChild(detailNodes[currentDetailLevel]);
        addDataChildren();
        repaint();
    }

    /** @see edu.umd.cs.piccolo.PNode#paint(edu.umd.cs.piccolo.util.PPaintContext) */
    @Override
    protected void paint(PPaintContext paintContext) {
        Graphics2D g2d = paintContext.getGraphics();

        if (state.exactUnitPlacement && getMapPNode().getCurrentVisualization() == null) { // black background for sky
            // vis
            border.setRect(X, Y, width, height);
            g2d.setColor(Color.BLACK);
            g2d.fill(border);
        }

        if (drawBorder) {
            GridGeometry helper = state.growingSOM.getLayer().getGridGeometry();
            Vector3D[] v = helper.shapeLinePoints(X, Y, width, height);

            g2d.setStroke(borderStroke);
            g2d.setPaint(Color.CYAN);
            g2d.setColor(borderColor);
            g2d.drawLine((int) v[0].getX(), (int) v[0].getY(), (int) v[1].getX(), (int) v[1].getY());

            for (int i = 1; i < v.length; i++) {
                int i_1 = (i + 1) % v.length;
                g2d.drawLine((int) v[i].getX(), (int) v[i].getY(), (int) v[i_1].getX(), (int) v[i_1].getY());
            }

            border.setRect(helper.getBorder(X, Y, width, height));
        }

        PCamera pCam = paintContext.getCamera();
        if (!((PCanvas) pCam.getComponent()).getClass().equals(MapOverviewPane.MapOverviewCanvas.class)) { // only for
            // main
            // display
            currentScale = paintContext.getScale();
            if (currentScale != oldScale) {
                // System.out.println("SCALE: "+currentScale);
                // double os = t10.getScale();
                // t10.setScale(1/currentScale);
                // if (t10.getGlobalFullBounds().width>this.width) {
                // t10.setScale(os);
                // }

                if (currentScale < state.scaleLimits[1]) { // no information
                    if (currentDetailLevel != DETAIL_LEVEL_NO) {
                        currentDetailLevel = DETAIL_LEVEL_NO;
                        detailChanged();
                    }
                } else if (currentScale >= state.scaleLimits[1] && currentScale < state.scaleLimits[2]) { // little
                    // information
                    if (currentDetailLevel != DETAIL_LEVEL_LOW) {
                        currentDetailLevel = DETAIL_LEVEL_LOW;
                        detailChanged();
                    }
                } else if (currentScale >= state.scaleLimits[2] && currentScale < state.scaleLimits[3]) { // more labels
                    if (currentDetailLevel != DETAIL_LEVEL_MEDIUM) {
                        currentDetailLevel = DETAIL_LEVEL_MEDIUM;
                        detailChanged();
                    }
                } else if (currentScale >= state.scaleLimits[3]) { // detailed information
                    if (currentDetailLevel != DETAIL_LEVEL_HIGH) {
                        currentDetailLevel = DETAIL_LEVEL_HIGH;
                        detailChanged();
                    }
                }

                oldScale = currentScale;
            }
        }
    }

    /** @see edu.umd.cs.piccolo.PNode#setBounds(double, double, double, double) */
    @Override
    public boolean setBounds(double x, double y, double width, double height) {
        if (super.setBounds(x, y, width, height)) {
            border.setFrame(x, y, width, height);
            return true;
        }
        return false;
    }

    /**
     * Updates the class pie chart visibility.
     */
    public void updateClassPieCharts() {
        if (u.getNumberOfMappedInputs() > 0 && classInfoSelectionChanged) {
            boolean pieChartVisible = false;
            if (state.getClassPiechartMode() == SOMViewer.TOGGLE_PIE_CHARTS_NONE) { // hiding class info is selected
                pieChartVisible = false;
            } else if (selectedClassIndices == null) { // no classes selected, show pie chart in any case
                pieChartVisible = true;
            } else { // at least one class is selected
                int[] classValues = pieChartDetailNodes[DETAIL_LEVEL_LOW].getValues();
                // check whether this unit contains any mapped data items belonging to a selected class
                // determines whether we show a pie chart at all
                int sc = 0;
                while (sc < selectedClassIndices.length && pieChartVisible == false) {
                    if (classValues[selectedClassIndices[sc]] > 0) {
                        pieChartVisible = true;
                    }
                    sc++;
                }
            }
            for (int i = 0; i < detailNodes.length; i++) {
                if (detailNodes[i] != null && pieChartDetailNodes[i] != null) {
                    if (state.getClassPiechartMode() == SOMViewer.TOGGLE_PIE_CHARTS_SHOW_COUNTS) {
                        pieChartDetailNodes[i].setShowLegend(PieChartLabelMode.Count);
                    } else if (state.getClassPiechartMode() == SOMViewer.TOGGLE_PIE_CHARTS_SHOW_PERCENT) {
                        pieChartDetailNodes[i].setShowLegend(PieChartLabelMode.Percent);
                    } else if (state.getClassPiechartMode() == SOMViewer.TOGGLE_PIE_CHARTS_SHOW_CLASSNAME) {
                        pieChartDetailNodes[i].setShowLegend(PieChartLabelMode.ClassName);
                    } else {
                        pieChartDetailNodes[i].setShowLegend(PieChartLabelMode.None);
                    }
                    if (selectedClassIndices != null) {
                        Color[] cs = pieChartDetailNodes[i].getLegendColors();
                        for (int j = 0; j < cs.length; j++) {
                            boolean colorVisible = !getShowOnlySelectedClasses();
                            if (!colorVisible) {
                                for (int selectedClassIndice : selectedClassIndices) {
                                    if (j == selectedClassIndice) {
                                        colorVisible = true;
                                        break;
                                    }
                                }
                            }
                            if (colorVisible) {
                                cs[j] = new Color(cs[j].getRed(), cs[j].getGreen(), cs[j].getBlue());
                            } else {
                                cs[j] = new Color(cs[j].getRed(), cs[j].getGreen(), cs[j].getBlue(), 0);
                            }

                        }
                        pieChartDetailNodes[i].setColors(cs);
                    }
                    boolean ancestorOf = detailNodes[i].isAncestorOf(pieChartDetailNodes[i]);
                    if (state.getClassPiechartMode() != SOMViewer.TOGGLE_PIE_CHARTS_NONE && pieChartVisible) {
                        if (!ancestorOf) {
                            detailNodes[i].addChild(pieChartDetailNodes[i]);
                        }
                    } else {
                        if (ancestorOf) {
                            detailNodes[i].removeChild(pieChartDetailNodes[i]);
                        }
                    }
                }
            }
        }
        classInfoSelectionChanged = false;
    }

    public int getDataInputVariant() {
        if (state.exactUnitPlacement) {
            if (state.shiftOverlappingInputs) {
                return DATA_DISPLAY_VARIANT_INPUTOBJECTSHIFTED;
            } else {
                return DATA_DISPLAY_VARIANT_INPUTOBJECT;
            }
        } else {
            return DATA_DISPLAY_VARIANT_TEXT;
        }
    }

    /**
     * Updates child nodes to display upon change in detail level
     */
    private void detailChanged() {
        removeAllChildren();
        setSelected(selected);
        addChild(detailNodes[currentDetailLevel]);
        addDataChildren();
    }

    public boolean hasPieCharts() {
        return pieChartDetailNodes[DETAIL_LEVEL_LOW] != null;
    }

    /**
     * This implementation does not check for the pie charts ({@link #pieChartDetailNodes} to be initialised and should
     * therefore be only used if it is for sure != null.
     */
    public Color getClassLegendColorFast(int index) {
        return pieChartDetailNodes[DETAIL_LEVEL_LOW].getLegendColor(index);
    }

    public void setClassColor(int index, Color color) {
        for (PieChartPNode pieChartDetailNode : pieChartDetailNodes) {
            if (pieChartDetailNode != null) {
                pieChartDetailNode.setColor(index, color);
            }
        }
        repaint();
    }

    public void setClassColors(Color[] colors) {
        for (PieChartPNode pieChartDetailNode : pieChartDetailNodes) {
            if (pieChartDetailNode != null) {
                pieChartDetailNode.setColors(colors);
            }
        }
        repaint();
    }

    public void updateClassSelection(int[] indices) {
        selectedClassIndices = indices;
        classInfoSelectionChanged = true;
        updateClassPieCharts();
    }

    public String[] getMappedDataNames() {
        return u.getMappedInputNames();
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean sel) {
        selected = sel;
        if (selected) {
            if (!selectionMarker.isDescendentOf(this)) {
                addChild(selectionMarker);
            }
        } else {
            if (selectionMarker.isDescendentOf(this)) {
                removeChild(selectionMarker);
            }
        }
    }

    public Label[] getLabels(String type) {
        return u.getLabels(type);
    }

    public void setQueryHit() {
        addChild(queryResultMarker);
    }

    public void removeQueryHit() {
        removeChild(queryResultMarker);
    }

    /**
     * Returns the associtated SOM unit for this node
     */
    public Unit getUnit() {
        return u;
    }

    public PieChartPNode getClassPieChart(int width, int height) {
        if (classInfo != null) { // class information present, generate pie chart
            int[] values = classInfo.computeClassDistribution(u.getMappedInputNames());
            PieChartPNode pieChartPNode = new PieChartPNode(0, 0, width, height, values, classInfo.getClassNames(),
                    u.getNumberOfMappedInputs());
            Color[] colors = pieChartDetailNodes[DETAIL_LEVEL_NO].getLegendColors();
            for (int i = 0; i < colors.length; i++) {
                pieChartPNode.setColor(i, colors[i]);
            }
            return pieChartPNode;
        } else {
            return null;
        }
    }

    // Angela: used by the Serializer -- serialize another object instead
    private Object writeReplace() throws ObjectStreamException {
        return new GeneralUnitPNodeSerializer(this);
    }

    public ArrayList<ArrowPNode> getArrows() {
        return arrowsFromThisUnit;
    }

    public void setArrows(ArrayList<ArrowPNode> arrows) {
        this.arrowsFromThisUnit = arrows;
    }

    public void addArrow(ArrowPNode arrow) {
        this.arrowsFromThisUnit.add(arrow);
    }

    public void resetArrows() {
        this.arrowsFromThisUnit = new ArrayList<ArrowPNode>();
    }

    public Point[] getLocations() {
        if (state.shiftOverlappingInputs) {
            return locations[DATA_DISPLAY_VARIANT_INPUTOBJECTSHIFTED];
        } else {
            return locations[DATA_DISPLAY_VARIANT_INPUTOBJECT];
        }
    }

    public Point getPostion() {
        return new Point((int) X, (int) Y);
    }

    public boolean getShowOnlySelectedClasses() {
        return showOnlySelectedClasses;
    }

    public void setShowOnlySelectedClasses(boolean showOnlySelectedClasses) {
        this.showOnlySelectedClasses = showOnlySelectedClasses;
        classInfoSelectionChanged = true;
        updateClassPieCharts();
    }

    @Override
    public String toString() {
        return u.toString();
    }

    public int[] getSelectedClassIndices() {
        return selectedClassIndices;
    }

}