/* * 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.controls; import at.tuwien.ifs.commons.gui.controls.TitledCollapsiblePanel; import at.tuwien.ifs.somtoolbox.apps.viewer.CommonSOMViewerStateData; import at.tuwien.ifs.somtoolbox.apps.viewer.SOMPane; import at.tuwien.ifs.somtoolbox.apps.viewer.fileutils.ExportUtils; import at.tuwien.ifs.somtoolbox.apps.viewer.fileutils.LabelXmlUtils; import at.tuwien.ifs.somtoolbox.data.SOMLibClassInformation; import at.tuwien.ifs.somtoolbox.layers.quality.EntropyAndPurityCalculator; import at.tuwien.ifs.somtoolbox.util.GridBagConstraintsIFS; import at.tuwien.ifs.somtoolbox.util.UiUtils; import at.tuwien.ifs.somtoolbox.visualization.Palette; import at.tuwien.ifs.somtoolbox.visualization.clustering.*; import edu.uci.ics.jung.algorithms.layout.TreeLayout; import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.graph.Tree; import edu.uci.ics.jung.graph.util.Context; import edu.uci.ics.jung.visualization.GraphZoomScrollPane; import edu.uci.ics.jung.visualization.VisualizationViewer; import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse; import edu.uci.ics.jung.visualization.control.GraphMouseListener; import edu.uci.ics.jung.visualization.control.ModalGraphMouse; import edu.uci.ics.jung.visualization.decorators.EdgeShape; import edu.umd.cs.piccolo.PNode; import edu.umd.cs.piccolo.util.PObjectOutputStream; import org.apache.commons.collections15.PredicateUtils; import org.apache.commons.collections15.Transformer; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Hashtable; import java.util.ListIterator; import java.util.SortedMap; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * The control panel for the clustering functionality. * * @author Angela Roiger * @author Rudolf Mayer * @version $Id: ClusteringControl.java 3970 2010-12-15 13:17:59Z mayer $ */ public class ClusteringControl extends AbstractViewerControl { private static final long serialVersionUID = 1L; private static final FileNameExtensionFilter clusteringFilter = new FileNameExtensionFilter( "Clustering and Labels (*.clustering)", "clustering"); private static final FileNameExtensionFilter xmlFilter = new FileNameExtensionFilter("Labels as xml (*.xml)", "xml"); private JSpinner spinnerNoCluster; private JSpinner labelSpinner; private GridBagConstraintsIFS c = new GridBagConstraintsIFS(GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH).setWeights( 1, 1); private int numClusters = 1; private SOMPane mapPane; private JCheckBox colorCluster; private JCheckBox showValues; private JSlider valueQe; private JCheckBox sticky = new JCheckBox("fix", false);; private JPanel kmeansInitialisationPanel = new JPanel(); private boolean st = false; private int numLabels = 0; private int maxCluster; private JButton buttonColour; private JButton qualityMeasureButton; private JLabel entropyLabel; private JLabel purityLabel; private JPanel dendogramPanel; public ClusteringControl(String title, CommonSOMViewerStateData state, SOMPane mappane) { super(title, state, new GridBagLayout()); this.mapPane = mappane; init(); updateControlDisplay(); } public void init() { maxCluster = state.growingLayer.getXSize() * state.growingLayer.getYSize(); spinnerNoCluster = new JSpinner(new SpinnerNumberModel(1, 1, maxCluster, 1)); spinnerNoCluster.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { numClusters = (Integer) ((JSpinner) e.getSource()).getValue(); SortedMap<Integer, ClusterElementsStorage> m = mapPane.getMap().getCurrentClusteringTree().getAllClusteringElements(); if (m.containsKey(numClusters)) { st = m.get(numClusters).sticky; } else { st = false; } sticky.setSelected(st); redrawClustering(); } }); JPanel clusterPanel = UiUtils.makeBorderedPanel(new FlowLayout(FlowLayout.LEFT, 10, 0), "Clusters"); sticky.setToolTipText("Marks this number of clusters as sticky for a certain leve; the next set of clusters will have a smaller boundary"); sticky.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { st = sticky.isSelected(); SortedMap<Integer, ClusterElementsStorage> m = mapPane.getMap().getCurrentClusteringTree().getAllClusteringElements(); if (m.containsKey(numClusters)) { ClusterElementsStorage c = m.get(numClusters); c.sticky = st; // System.out.println("test"); // ((ClusterElementsStorageNode)m.get(numClusters)).sticky = st; redrawClustering(); } } }); colorCluster = new JCheckBox("colour", state.colorClusters); colorCluster.setToolTipText("Fill the clusters in colours"); colorCluster.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { state.colorClusters = colorCluster.isSelected(); // TODO: Palette anzeigen (?) redrawClustering(); } }); UiUtils.fillPanel(clusterPanel, new JLabel("#"), spinnerNoCluster, sticky, colorCluster); getContentPane().add(clusterPanel, c.nextRow()); dendogramPanel = UiUtils.makeBorderedPanel(new GridLayout(0, 1), "Dendogram"); getContentPane().add(dendogramPanel, c.nextRow()); JPanel numLabelPanel = UiUtils.makeBorderedPanel(new GridBagLayout(), "Labels"); GridBagConstraintsIFS gcLabels = new GridBagConstraintsIFS(); labelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1)); labelSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { numLabels = (Integer) ((JSpinner) e.getSource()).getValue(); state.clusterWithLabels = numLabels; redrawClustering(); } }); this.showValues = new JCheckBox("values", state.labelsWithValues); this.showValues.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { state.labelsWithValues = showValues.isSelected(); redrawClustering(); } }); int start = new Double((1 - state.clusterByValue) * 100).intValue(); valueQe = new JSlider(0, 100, start); valueQe.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { int byValue = ((JSlider) e.getSource()).getValue(); state.clusterByValue = 1 - byValue / 100; redrawClustering(); } }); numLabelPanel.add(new JLabel("# Labels"), gcLabels); numLabelPanel.add(labelSpinner, gcLabels.nextCol()); numLabelPanel.add(showValues, gcLabels.nextCol()); numLabelPanel.add(valueQe, gcLabels.nextRow().setGridWidth(3).setFill(GridBagConstraints.HORIZONTAL)); getContentPane().add(numLabelPanel, c.nextRow()); Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>(); labelTable.put(0, new JLabel("by Value")); labelTable.put(100, new JLabel("by Qe")); valueQe.setToolTipText("Method how to select representative labels - by QE, or the attribute values"); valueQe.setLabelTable(labelTable); valueQe.setPaintLabels(true); final JComboBox initialisationBox = new JComboBox(KMeans.InitType.values()); initialisationBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Object o = mapPane.getMap().getClusteringTreeBuilder(); if (o instanceof KMeansTreeBuilder) { ((KMeansTreeBuilder) o).reInit((KMeans.InitType) initialisationBox.getSelectedItem()); // FIXME: is this call needed? mapPane.getMap().getCurrentClusteringTree().getAllClusteringElements(); redrawClustering(); } } }); getContentPane().add( UiUtils.fillPanel(kmeansInitialisationPanel, new JLabel("k-Means initialisation"), initialisationBox), c.nextRow()); JPanel borderPanel = new TitledCollapsiblePanel("Border", new GridLayout(1, 4), true); JSpinner borderSpinner = new JSpinner(new SpinnerNumberModel( ClusteringTree.INITIAL_BORDER_WIDTH_MAGNIFICATION_FACTOR, 0.1, 5, 0.1)); borderSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { state.clusterBorderWidthMagnificationFactor = ((Double) ((JSpinner) e.getSource()).getValue()).floatValue(); redrawClustering(); } }); borderPanel.add(new JLabel("width")); borderPanel.add(borderSpinner); borderPanel.add(new JLabel("colour")); buttonColour = new JButton(""); buttonColour.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { new ClusterBoderColorChooser(state.parentFrame, state.clusterBorderColour, ClusteringControl.this); } }); buttonColour.setBackground(state.clusterBorderColour); borderPanel.add(buttonColour); getContentPane().add(borderPanel, c.nextRow()); JPanel evaluationPanel = new TitledCollapsiblePanel("Cluster Evaluation", new GridLayout(3, 2), true); evaluationPanel.add(new JLabel("quality measure")); qualityMeasureButton = new JButton(); qualityMeasureButton.setText("ent/pur calc"); qualityMeasureButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("doing entropy here"); ClusteringTree clusteringTree = state.mapPNode.getClusteringTree(); if (clusteringTree == null) { // we have not clustered yet return; } // System.out.println(clusteringTree.getChildrenCount()); // FIXME put this in a method and call it on numcluster.change() SOMLibClassInformation classInfo = state.inputDataObjects.getClassInfo(); ArrayList<ClusterNode> clusters = clusteringTree.getNodesAtLevel(numClusters); System.out.println(clusters.size()); // EntropyMeasure.computeEntropy(clusters, classInfo); EntropyAndPurityCalculator eapc = new EntropyAndPurityCalculator(clusters, classInfo); // FIXME round first entropyLabel.setText(String.valueOf(eapc.getEntropy())); purityLabel.setText(String.valueOf(eapc.getPurity())); } }); evaluationPanel.add(qualityMeasureButton); GridBagConstraintsIFS gcEval = new GridBagConstraintsIFS(); evaluationPanel.add(new JLabel("entropy"), gcEval); evaluationPanel.add(new JLabel("purity"), gcEval.nextCol()); entropyLabel = new JLabel("n/a"); purityLabel = new JLabel("n/a"); evaluationPanel.add(entropyLabel, gcEval.nextRow()); evaluationPanel.add(purityLabel, gcEval.nextCol()); getContentPane().add(evaluationPanel, c.nextRow()); JPanel panelButtons = new JPanel(new GridLayout(1, 4)); JButton saveButton = new JButton("Save"); saveButton.setFont(smallFont); saveButton.setMargin(SMALL_INSETS); saveButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JFileChooser fileChooser; if (state.fileChooser.getSelectedFile() != null) { fileChooser = new JFileChooser(state.fileChooser.getSelectedFile().getPath()); } else { fileChooser = new JFileChooser(); } fileChooser.addChoosableFileFilter(clusteringFilter); fileChooser.addChoosableFileFilter(xmlFilter); File filePath = ExportUtils.getFilePath(ClusteringControl.this, fileChooser, "Save Clustering and Labels"); if (filePath != null) { if (xmlFilter.accept(filePath)) { LabelXmlUtils.saveLabelsToFile(state.mapPNode, filePath); } else { try { FileOutputStream fos = new FileOutputStream(filePath); GZIPOutputStream gzipOs = new GZIPOutputStream(fos); PObjectOutputStream oos = new PObjectOutputStream(gzipOs); oos.writeObjectTree(mapPane.getMap().getCurrentClusteringTree()); oos.writeObjectTree(mapPane.getMap().getManualLabels()); oos.writeInt(state.clusterWithLabels); oos.writeBoolean(state.labelsWithValues); oos.writeDouble(state.clusterByValue); oos.writeObject(spinnerNoCluster.getValue()); oos.close(); } catch (Exception ex) { ex.printStackTrace(); } } // keep the selected path for future references state.fileChooser.setSelectedFile(filePath.getParentFile()); } } }); panelButtons.add(saveButton); JButton loadButton = new JButton("Load"); loadButton.setFont(smallFont); loadButton.setMargin(SMALL_INSETS); loadButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = getFileChooser(); fileChooser.setName("Open Clustering"); fileChooser.addChoosableFileFilter(xmlFilter); fileChooser.addChoosableFileFilter(clusteringFilter); int returnVal = fileChooser.showDialog(ClusteringControl.this, "Open"); if (returnVal == JFileChooser.APPROVE_OPTION) { File filePath = fileChooser.getSelectedFile(); if (xmlFilter.accept(filePath)) { PNode restoredLabels = null; try { restoredLabels = LabelXmlUtils.restoreLabelsFromFile(fileChooser.getSelectedFile()); PNode manual = state.mapPNode.getManualLabels(); ArrayList<PNode> tmp = new ArrayList<PNode>(); for (ListIterator<?> iter = restoredLabels.getChildrenIterator(); iter.hasNext();) { PNode element = (PNode) iter.next(); tmp.add(element); } manual.addChildren(tmp); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Successfully loaded cluster labels."); } catch (Exception e1) { e1.printStackTrace(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info( "Error loading cluster labels: " + e1.getMessage()); } } else { try { FileInputStream fis = new FileInputStream(filePath); GZIPInputStream gzipIs = new GZIPInputStream(fis); ObjectInputStream ois = new ObjectInputStream(gzipIs); ClusteringTree tree = (ClusteringTree) ois.readObject(); PNode manual = (PNode) ois.readObject(); PNode all = mapPane.getMap().getManualLabels(); ArrayList<PNode> tmp = new ArrayList<PNode>(); for (ListIterator<?> iter = manual.getChildrenIterator(); iter.hasNext();) { PNode element = (PNode) iter.next(); tmp.add(element); } all.addChildren(tmp); state.clusterWithLabels = ois.readInt(); labelSpinner.setValue(state.clusterWithLabels); state.labelsWithValues = ois.readBoolean(); showValues.setSelected(state.labelsWithValues); state.clusterByValue = ois.readDouble(); valueQe.setValue(new Double((1 - state.clusterByValue) * 100).intValue()); mapPane.getMap().buildTree(tree); spinnerNoCluster.setValue(ois.readObject()); ois.close(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Successfully loaded clustering."); } catch (Exception ex) { ex.printStackTrace(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info( "Error loading clustering: " + ex.getMessage()); } } // keep the selected path for future references state.fileChooser.setSelectedFile(filePath.getParentFile()); } } }); panelButtons.add(loadButton); JButton exportImages = new JButton("Export"); exportImages.setFont(smallFont); exportImages.setMargin(SMALL_INSETS); exportImages.setToolTipText("Export labels as images (not yet working)"); exportImages.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); if (CommonSOMViewerStateData.fileNamePrefix != null && !CommonSOMViewerStateData.fileNamePrefix.equals("")) { fileChooser.setCurrentDirectory(new File(CommonSOMViewerStateData.fileNamePrefix)); } else { fileChooser.setCurrentDirectory(state.getFileChooser().getCurrentDirectory()); } fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); if (fileChooser.getSelectedFile() != null) { // reusing the dialog fileChooser.setSelectedFile(null); } fileChooser.setName("Choose path"); int returnVal = fileChooser.showDialog(ClusteringControl.this, "Choose path"); if (returnVal == JFileChooser.APPROVE_OPTION) { // save images String path = fileChooser.getSelectedFile().getAbsolutePath(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Writing label images to " + path); } } }); panelButtons.add(exportImages); JButton deleteManual = new JButton("Delete"); deleteManual.setFont(smallFont); deleteManual.setMargin(SMALL_INSETS); deleteManual.setToolTipText("Delete manually added labels."); deleteManual.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { state.mapPNode.getManualLabels().removeAllChildren(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Manual Labels deleted."); } }); panelButtons.add(deleteManual); c.anchor = GridBagConstraints.NORTH; this.getContentPane().add(panelButtons, c.nextRow()); this.setVisible(true); } // update allClusteringElements of the current clustering tree and show them private void redrawClustering() { // BasicStroke bs = new BasicStroke(12.0f); this.mapPane.getState().numClusters = numClusters; this.mapPane.getMap().showClusters(this.numClusters, sticky.isSelected()); } private JFileChooser getFileChooser() { JFileChooser fileChooser; if (state.fileChooser.getSelectedFile() != null) { fileChooser = new JFileChooser(state.fileChooser.getSelectedFile().getPath()); } else { fileChooser = new JFileChooser(); } return fileChooser; } public void updateClusterColourSelection(Color colour) { buttonColour.setBackground(colour); state.clusterBorderColour = colour; redrawClustering(); } /** Makes sure all controls are displayed as needed. Currently deals with the {@link #kmeansInitialisationPanel} */ public void updateControlDisplay() { kmeansInitialisationPanel.setVisible(mapPane.getMap().getCurrentClusteringTree() != null && mapPane.getMap().getClusteringTreeBuilder() instanceof KMeansTreeBuilder); ClusteringTree clusteringTree = mapPane.getMap().getCurrentClusteringTree(); if(clusteringTree == null) return; Tree<ClusterNode, Integer> tree = clusteringTree.getJUNGTree(); TreeLayout<ClusterNode, Integer> layout = new TreeLayout<ClusterNode, Integer>(tree, 30, 100); final double maxMergeCost = clusteringTree.getMaxMergeCost(); final double minMergeCost = clusteringTree.getMinMergeCost(); final VisualizationViewer<ClusterNode, Integer> vv = new VisualizationViewer<ClusterNode, Integer>(layout); // setup edge rendering vv.getRenderContext().setEdgeShapeTransformer(new EdgeShape.Line<ClusterNode, Integer>()); vv.getRenderContext().setEdgeStrokeTransformer(new Transformer<Integer, Stroke>() { @Override public Stroke transform(Integer integer) { return new BasicStroke(1.0f); } }); vv.getRenderContext().setEdgeArrowPredicate( PredicateUtils.<Context<Graph<ClusterNode, Integer>, Integer>>falsePredicate()); vv.getRenderContext().setEdgeDrawPaintTransformer(new Transformer<Integer, Paint>() { @Override public Paint transform(Integer edge) { return Color.BLACK; } }); // setup vertex rendering vv.getRenderContext().setVertexLabelTransformer(new Transformer<ClusterNode, String>() { @Override public String transform(ClusterNode clusterNode) { Point2D.Double centroid = clusterNode.getCentroid(); return String.format("%d @ (%f, %f)", clusterNode.getNodes().length, centroid.getX(), centroid.getY()); } }); vv.getRenderContext().setVertexFillPaintTransformer(new Transformer<ClusterNode, Paint>() { @Override public Paint transform(ClusterNode clusterNode) { Palette palette = mapPane.getState().getSOMViewer().getCurrentlySelectedPalette(); double pos = clusterNode.getMergeCost() - minMergeCost; pos /= maxMergeCost - minMergeCost; pos *= palette.getNumberOfColours() - 1; return palette.getColor((int) pos); } }); vv.getRenderContext().setVertexStrokeTransformer(new Transformer<ClusterNode, Stroke>() { @Override public Stroke transform(ClusterNode clusterNode) { if (vv.getPickedVertexState().isPicked(clusterNode)) return new BasicStroke(3.0f); else return new BasicStroke(1.0f); } }); vv.setVertexToolTipTransformer(new Transformer<ClusterNode, String>() { @Override public String transform(ClusterNode clusterNode) { StringBuilder result = new StringBuilder(); result.append("Level: ").append(clusterNode.getLevel()).append("\r\n"); result.append("Merge-cost: ").append(String.format("%.2f", clusterNode.getMergeCost())).append("\r\n"); result.append("Centroid: ").append(String.format("%.2f", clusterNode.getCentroid().getX())). append(", ").append(String.format("%.2f", clusterNode.getCentroid().getY())).append("\r\n"); result.append("Factor-value: ").append(String.format("%.2f", clusterNode.getFactorValue())).append ("\r\n"); result.append("#Nodes: ").append(clusterNode.getUnitNodes().length).append("\r\n"); result.append("Mean-vector: "); for(double d:clusterNode.getMeanVector()) result.append(String.format("%.2f", d)).append(", "); result.append("\r\n"); result.append("Bounds: (x=").append(String.format("%.2f", clusterNode.getX())) .append(", y=").append(String.format("%.2f", clusterNode.getY())) .append(", w=").append(String.format("%.2f", clusterNode.getWidth())) .append(", h=").append(String.format("%.2f", clusterNode.getHeight())) .append(")\r\n"); return result.toString(); } }); GraphZoomScrollPane vv2 = new GraphZoomScrollPane(vv); vv2.setPreferredSize(new Dimension(dendogramPanel.getParent().getWidth(), 200)); vv2.setVisible(true); DefaultModalGraphMouse<ClusterNode, Integer> graphMouse = new DefaultModalGraphMouse<ClusterNode, Integer>(); vv.setGraphMouse(graphMouse); graphMouse.setMode(ModalGraphMouse.Mode.PICKING); vv.addGraphMouseListener(new GraphMouseListener<ClusterNode>() { private ClusterNode previouslySelected; @Override public void graphClicked(ClusterNode clusterNode, MouseEvent me) { if(previouslySelected != null) previouslySelected.setSelected(false); clusterNode.setSelected(true); previouslySelected = clusterNode; numClusters = clusterNode.getLevel(); SortedMap<Integer, ClusterElementsStorage> m = mapPane.getMap().getCurrentClusteringTree().getAllClusteringElements(); if (m.containsKey(numClusters)) { st = m.get(numClusters).sticky; } else { st = false; } sticky.setSelected(st); redrawClustering(); } @Override public void graphPressed(ClusterNode clusterNode, MouseEvent me) { } @Override public void graphReleased(ClusterNode clusterNode, MouseEvent me) { } }); dendogramPanel.removeAll(); dendogramPanel.add(vv2); getContentPane().validate(); } }