/*
* 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;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Arrays;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.ListSelectionModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import at.tuwien.ifs.somtoolbox.SOMToolboxException;
import at.tuwien.ifs.somtoolbox.layers.GrowingLayer;
import at.tuwien.ifs.somtoolbox.models.GrowingSOM;
import at.tuwien.ifs.somtoolbox.util.FileUtils;
import at.tuwien.ifs.somtoolbox.visualization.clustering.LabelCoordinates;
import at.tuwien.ifs.somtoolbox.visualization.comparison.SOMComparison;
/**
* @author Doris Baum
* @version $Id: ComparisonVisualizer.java 3883 2010-11-02 17:13:23Z frank $
*/
public class ComparisonVisualizer extends AbstractMatrixVisualizer implements BackgroundImageVisualizer { // ,
// ListSelectionListener
// {
private DefaultListModel soms = new DefaultListModel();
private double[][] meanDistances = null;
private double[][] varDistances = null;
private boolean storeValid = false;
private int oldindex = -1;
private double threshold = 0;
private int clusterNo = 5;
private TreeMap<String, double[][]> clusterDistances = null;
private final int MEAN = 0;
private final int VAR = 1;
private final int CLUSTER = 2;
private final int CLUSTERVAR = 3;
private final double MAX_DISTANCE_THRESHOLD = 100;
private final int MAX_CLUSTER_NO = 100;
public ComparisonVisualizer() {
NUM_VISUALIZATIONS = 4;
VISUALIZATION_NAMES = new String[] { "Comparison - Mean", "Comparison - Variance", "Comparison - Cluster mean",
"Comparison - Cluster variance" };
VISUALIZATION_SHORT_NAMES = new String[] { "ComparisonMean", "ComparisonVar", "ComparisonClusterMean",
"ComparisonClustVar" };
VISUALIZATION_DESCRIPTIONS = new String[] {
"Comparison between two or more SOMs - calculates the mean euclidean distance in the compared SOMs\n"
+ "between data vectors which lie on the same unit in the displayed SOM.",
"Comparison between two or more SOMs - calculates the variance of the euclidean distances in the compared SOMs\n"
+ " between data vectors which lie on the same unit in the displayed SOM.",
"Comparison between two or more SOMs - calculates the mean cluster distance in the compared SOMs\n"
+ " between data vectors which lie on the same unit in the displayed SOM.",
"Comparison between two or more SOMs - calculates the cluster distance variance in the compared SOMs\n"
+ " between data vectors which lie on the same unit in the displayed SOM." };
// don't initialise the control panel if we have no graphics environment (e.g. in server applications)
if (!GraphicsEnvironment.isHeadless()) {
controlPanel = new ComparisonControlPanel(this);
}
}
@Override
public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
BufferedImage res = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) res.getGraphics();
drawBackground(width, height, g);
// if we have no data, just make white map
if (soms == null || soms.size() == 0) {
g.setPaint(Color.white);
g.fill(new Rectangle(0, 0, width, height));
Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
"No SOMs loaded, thus not displaying any visualisation; add some SOMs and hit recalculate to generate matrices");
// else do full blown visualisation
} else {
Object[] dummy = soms.toArray();
String[] prefixes = new String[dummy.length];
for (int i = 0; i < dummy.length; i++) {
prefixes[i] = (String) dummy[i];
}
// TODO ueberpruefung ob korrekte SOM description files? wann und wo und wie moeglichst schnell, ohne die
// SOMs komplett zu laden?
// TODO frage: warum wird initguielements nicht zuende ausgefuehrt, wenn createvis unterbrochen wird
if ((oldindex == MEAN || oldindex == VAR) && (index == CLUSTER || index == CLUSTERVAR)
|| (oldindex == CLUSTER || oldindex == CLUSTERVAR) && (index == MEAN || index == VAR)) {
storeValid = false;
}
oldindex = index;
// have a sort of cache for the mean distances; if only the palette was changed, don't recalculate
// everything
if (!storeValid) {
this.calculateMeanVarDistance(gsom, prefixes, index, true);
storeValid = true;
}
int xSize = gsom.getLayer().getXSize();
int ySize = gsom.getLayer().getYSize();
int unitWidth = width / xSize;
int unitHeight = height / ySize;
int ci = 0;
for (int y = 0; y < ySize; y++) {
for (int x = 0; x < xSize; x++) {
if (index == MEAN || index == CLUSTER) {
ci = (int) Math.round((1 - meanDistances[x][y]) * palette.maxColourIndex());
} else if (index == VAR || index == CLUSTERVAR) {
ci = (int) Math.round((1 - varDistances[x][y]) * palette.maxColourIndex());
}
g.setPaint(palette.getColor(ci));
g.fill(new Rectangle(x * unitWidth, y * unitHeight, unitWidth, unitHeight));
}
}
}
return res;
}
public void calculateMeanVarDistance(GrowingSOM gsom, String[] prefixes, int index, boolean normalized)
throws SOMToolboxException {
GrowingLayer layer = gsom.getLayer();
int xSize = layer.getXSize();
int ySize = layer.getYSize();
String[] labelList = gsom.getLayer().getAllMappedDataNames(true);
// create and initialise variable for the mean distances
double[][] meandist = new double[xSize][ySize];
for (int a = 0; a < xSize; a++) {
Arrays.fill(meandist[a], 0.0);
}
// create and initialise variable for the mean distances
double[][] vardist = new double[xSize][ySize];
for (int a = 0; a < xSize; a++) {
Arrays.fill(vardist[a], 0.0);
}
// init hashmap for cluster distance arrays
clusterDistances = new TreeMap<String, double[][]>();
// go through all the SOMs that should be compared with the main one
for (String prefixe : prefixes) {
// load second SOM to compare with
GrowingSOM secondSOM = SOMComparison.loadGSOM(prefixe);
// if the vector labels aren't the same in both SOMs, then throw exeption
if (!Arrays.equals(labelList, secondSOM.getLayer().getAllMappedDataNames(true))) {
soms.removeElement(prefixe);
SOMComparison.printInputDifferenceErrorMesage(labelList, secondSOM.getLayer().getAllMappedDataNames(
true));
throw new SOMToolboxException(
"The input vector sets of the SOMs aren't equal - can't do comparison! See the logs for input vector differences.");
}
// calculate the distances between the vectors in the second SOM
double[][] dist2 = null;
if (index == MEAN || index == VAR) {
dist2 = SOMComparison.calculcateIntraSOMDistanceMatrix(SOMComparison.getLabelCoordinates(secondSOM));
} else if (index == CLUSTER || index == CLUSTERVAR) {
int[][] assignment2 = SOMComparison.calculateClusterAssignment(secondSOM, clusterNo);
LabelCoordinates[] coords2 = SOMComparison.getLabelCoordinates(secondSOM);
double[][] distances = SOMComparison.calculateClusterDistances(assignment2, clusterNo);
clusterDistances.put(prefixe, distances);
dist2 = SOMComparison.calculcateIntraSOMClusterDistanceMatrix(coords2, assignment2, clusterNo,
distances);
} else {
throw new SOMToolboxException("Invalid visualisation index: " + index);
}
// go through all units in the first SOM's layer...
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
// ... for each unit, get the labels of the vectors mapped to them
String[] unitnames = layer.getUnit(x, y).getMappedInputNames();
// ... and calculate the mean pairwise distance in SOM2 for those vectors
double currentDistance = 0;
double currentVar = 0;
int distanceCounts = 0;
if (unitnames != null) {
// compare each vector with all following vectors on the unit...
for (int n = 0; n < unitnames.length; n++) {
int indexa = Arrays.binarySearch(labelList, unitnames[n]);
for (int m = n + 1; m < unitnames.length; m++) {
int indexb = Arrays.binarySearch(labelList, unitnames[m]);
if (dist2[indexa][indexb] > threshold) {
currentDistance += dist2[indexa][indexb];
currentVar += dist2[indexa][indexb] * dist2[indexa][indexb];
}
distanceCounts++;
}
}
}
if (distanceCounts != 0) {
currentDistance = currentDistance / distanceCounts;
currentVar = currentVar / distanceCounts;
}
meandist[x][y] += currentDistance;
vardist[x][y] += currentVar;
}
}
}
// divide cumulated distance by number of SOMs to compare, to really get mean distance
// also find maximum of the mean distances
double maxDist = 0;
double maxVar = 0;
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
meandist[x][y] = meandist[x][y] / prefixes.length;
// calculate variance: Var(x) = E(X^2) - (E(X))^2
vardist[x][y] = vardist[x][y] / prefixes.length - meandist[x][y] * meandist[x][y];
if (meandist[x][y] > maxDist) {
maxDist = meandist[x][y];
}
if (vardist[x][y] > maxVar) {
maxVar = vardist[x][y];
}
}
}
// if normalization is required, normalize to maximum distance
if (normalized == true) {
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
meandist[x][y] = meandist[x][y] / maxDist;
vardist[x][y] = vardist[x][y] / maxVar;
}
}
}
if (clusterDistances.size() != 0) {
((ComparisonControlPanel) controlPanel).btShowDistances.setEnabled(true);
}
meanDistances = meandist;
varDistances = vardist;
}
public void addSOM(String fileName) {
String prefix = FileUtils.extractSOMLibInputPrefix(fileName);
if (!soms.contains(prefix)) {
soms.addElement(prefix);
} else {
Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
"Not adding already existing SOM '" + prefix + "' (extracted from '" + fileName + "')");
}
}
private class ComparisonControlPanel extends VisualizationControlPanel implements ActionListener {
private static final long serialVersionUID = 1L;
private JButton btAddSOMs = null;
private JButton btRemove = null;
private JButton btReCalculate = null;
protected JButton btShowDistances = null;
private JSpinner spThreshold = null;
private JLabel lThreshold = null;
private JList somlist = null;
private JSpinner spClusterNo = null;
private JLabel lClusterNo = null;
private final String ADD_SOMS = "addSOMs";
private final String REMOVE = "remove";
private final String RECALCULATE = "reCalculate";
private final String SHOW_DIST = "showDistances";
final static String addToolTip = "Add SOMs to the comparison; it is sufficient to add one description file per SOM, "
+ "the other two description files with the same prefix will automatically be found";
final static String showDistancesToolTip = "Shows cluster distance matrices for the SOMs in the list in an extra window.";
final static String thresholdToolTip = "Distance threshold: if the distance between two vectors is less than or equal the distance threshold, "
+ "don't take it into account for this visualization.";
final static String clusterNoToolTip = "Number of clusters to generate (using Ward's linkage); "
+ "only relevant for Cluster mean and Cluster variance visualisations.";
final static String removeToolTip = "Remove SOMs from the list.";
final static String recalculateToolTip = "Recalculate the visualisation; hit this button to see the changes from adding or remove SOMs ;-)";
private ComparisonControlPanel(ComparisonVisualizer comp) {
super("SOM Comparison Control");
this.initGUIElements(comp);
setVisible(true);
}
private void initGUIElements(ComparisonVisualizer comp) {
setName("Comparison Control");
JPanel comparisonPanel = new JPanel(new GridBagLayout());
GridBagConstraints constr = new GridBagConstraints();
constr.fill = GridBagConstraints.HORIZONTAL;
int gridy = 2;
spThreshold = new JSpinner(new SpinnerNumberModel(comp.threshold, 0, comp.MAX_DISTANCE_THRESHOLD, 0.5));
spThreshold.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JSpinner src = (JSpinner) e.getSource();
threshold = ((Double) src.getValue()).doubleValue();
}
});
lThreshold = new JLabel("Dist. threshold:");
spThreshold.setToolTipText(thresholdToolTip);
lThreshold.setToolTipText(thresholdToolTip);
constr.gridx = 0;
constr.gridy = gridy;
comparisonPanel.add(lThreshold, constr);
constr.gridx = 1;
constr.gridy = gridy;
constr.anchor = GridBagConstraints.NORTHEAST;
comparisonPanel.add(spThreshold, constr);
gridy++;
spClusterNo = new JSpinner(new SpinnerNumberModel(comp.clusterNo, 0, comp.MAX_CLUSTER_NO, 1));
spClusterNo.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JSpinner src = (JSpinner) e.getSource();
clusterNo = ((Integer) src.getValue()).intValue();
}
});
lClusterNo = new JLabel("# clusters:");
spClusterNo.setToolTipText(clusterNoToolTip);
lClusterNo.setToolTipText(clusterNoToolTip);
constr.gridx = 0;
constr.gridy = gridy;
comparisonPanel.add(lClusterNo, constr);
constr.gridx = 1;
constr.gridy = gridy;
constr.anchor = GridBagConstraints.NORTHEAST;
comparisonPanel.add(spClusterNo, constr);
gridy++;
constr.fill = GridBagConstraints.BOTH;
btAddSOMs = new JButton("Add SOMs");
btAddSOMs.setFont(smallerFont);
btAddSOMs.setActionCommand(ADD_SOMS);
btAddSOMs.addActionListener(this);
btAddSOMs.setToolTipText(addToolTip);
constr.gridx = 0;
constr.gridy = gridy;
comparisonPanel.add(btAddSOMs, constr);
btRemove = new JButton("Remove");
btRemove.setFont(smallerFont);
btRemove.setActionCommand(REMOVE);
btRemove.addActionListener(this);
btRemove.setToolTipText(removeToolTip);
constr.gridx = 1;
constr.gridy = gridy;
comparisonPanel.add(btRemove, constr);
gridy++;
btReCalculate = new JButton("Recalculate");
btReCalculate.setFont(smallerFont);
btReCalculate.setActionCommand(RECALCULATE);
btReCalculate.addActionListener(this);
btReCalculate.setToolTipText(recalculateToolTip);
constr.gridx = 0;
constr.gridy = gridy;
comparisonPanel.add(btReCalculate, constr);
btShowDistances = new JButton("Cluster distances");
btShowDistances.setFont(smallerFont);
btShowDistances.setActionCommand(SHOW_DIST);
btShowDistances.addActionListener(this);
btShowDistances.setToolTipText(showDistancesToolTip);
btShowDistances.setEnabled(false);
constr.gridx = 1;
constr.gridy = gridy;
comparisonPanel.add(btShowDistances, constr);
soms = new DefaultListModel();
somlist = new JList(soms);
somlist.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
// somlist.addListSelectionListener(comp);
somlist.setVisibleRowCount(10);
JScrollPane listScroller = new JScrollPane(somlist);
listScroller.setPreferredSize(new Dimension(50, 50));
constr.fill = GridBagConstraints.BOTH;
gridy++;
constr.gridx = 0;
constr.gridy = gridy;
constr.gridwidth = 2;
constr.weightx = 0.5;
constr.weighty = 1.0;
comparisonPanel.add(listScroller, constr);
this.add(comparisonPanel, c);
}
protected void addSOMs() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setMultiSelectionEnabled(true);
fileChooser.setCurrentDirectory(map.getState().getFileChooser().getCurrentDirectory());
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.addChoosableFileFilter(new FileUtils.SOMDescriptionFileFilter());
if (fileChooser.getSelectedFile() != null) { // reusing the dialog
fileChooser = new JFileChooser(fileChooser.getSelectedFile().getPath());
}
fileChooser.setName("Choose SOMs to compare");
int returnVal = fileChooser.showDialog(this.getParent(), "Choose SOMs");
if (returnVal == JFileChooser.APPROVE_OPTION) {
File[] selectedFiles = fileChooser.getSelectedFiles();
for (File selectedFile : selectedFiles) {
addSOM(selectedFile.getPath() + selectedFile.getName());
}
}
}
private void recalculate() {
storeValid = false;
for (String visualizationName : VISUALIZATION_SHORT_NAMES) {
invalidateCache(visualizationName);
}
if (visualizationUpdateListener != null) {
visualizationUpdateListener.updateVisualization();
}
}
private void remove() {
int[] deletedIndices = somlist.getSelectedIndices();
if (deletedIndices != null) {
for (int i = deletedIndices.length - 1; i >= 0; i--) {
soms.removeElementAt(deletedIndices[i]);
}
}
if (soms == null || soms.size() == 0) {
btShowDistances.setEnabled(false);
}
}
private void showDist() {
if (clusterDistances == null) {
Logger.getLogger("at.tuwien.ifs.somtoolbox").warning(
"No cluster distance matrices present; add some SOMs and hit recalculate to generate matrices");
} else {
final String nl = System.getProperty("line.separator");
String output = "";
for (String key : clusterDistances.keySet()) {
double[][] value = clusterDistances.get(key);
output += "SOM: " + key + nl;
output += "Distances: " + nl;
for (int i = 0; i < value.length; i++) {
for (int j = i + 1; j < value[0].length; j++) {
output += "Cluster " + i + " to cluster " + j + ": " + value[i][j] + nl;
}
}
output += nl;
}
this.makeDistancesDialog(output);
}
}
private void makeDistancesDialog(String output) {
final JDialog dialog = new JDialog(map.getState().parentFrame, "Cluster distances");
JTextArea textArea = new JTextArea(output);
textArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
JButton closeButton = new JButton("Close");
closeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dialog.setVisible(false);
dialog.dispose();
}
});
JPanel closePanel = new JPanel();
closePanel.setLayout(new BoxLayout(closePanel, BoxLayout.LINE_AXIS));
closePanel.add(Box.createHorizontalGlue());
closePanel.add(closeButton);
closePanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 5));
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.add(scrollPane, BorderLayout.CENTER);
contentPane.add(closePanel, BorderLayout.PAGE_END);
contentPane.setOpaque(true);
dialog.setContentPane(contentPane);
dialog.setSize(new Dimension(350, 400));
dialog.setLocationRelativeTo(map.getState().parentFrame);
dialog.setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand() == ADD_SOMS) {
this.addSOMs();
} else if (e.getActionCommand() == REMOVE) {
this.remove();
} else if (e.getActionCommand() == RECALCULATE) {
this.recalculate();
} else if (e.getActionCommand() == SHOW_DIST) {
this.showDist();
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(map.getState().controlElementsWidth, 400);
}
@Override
public Dimension getMinimumSize() {
return this.getPreferredSize();
}
}
}