/* * Copyright (C) 2012 Dr. John Lindsay <jlindsay@uoguelph.ca> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package plugins; import java.text.DecimalFormat; import javax.imageio.ImageIO; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.geom.AffineTransform; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JMenuItem; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import java.io.*; import java.awt.print.*; import javax.print.attribute.*; import whitebox.structures.ExtensionFileFilter; import java.util.ArrayList; /** * * @author Dr. John Lindsay email: jlindsay@uoguelph.ca */ class Dendrogram extends JPanel implements ActionListener, Printable, MouseMotionListener, MouseListener { private int bottomMargin = 60; private int topMargin = 45; private int leftMargin = 60; private int rightMargin = 30; private JPopupMenu myPopup = null; private JMenuItem gridMi = null; private boolean gridOn = true; private int numClasses = 0; private int numDimensions = 0; private double[][] centroidVectors = null; private ArrayList<double[]> centres = new ArrayList<double[]>(); private long[] classSizes; private double maxDist = 0; // Constructors public Dendrogram(double[][] centroidVectors, long[] classSizes) { this.centroidVectors = centroidVectors.clone(); this.numClasses = centroidVectors.length; this.numDimensions = centroidVectors[0].length; this.setPreferredSize(new Dimension(500, 500)); this.classSizes = classSizes.clone(); setMouseMotionListener(); setMouseListener(); setUp(); } // Methods private void setUp() { try { classOrder = new int[numClasses]; createPopupMenus(); merger(); } catch (Exception e) { } } ArrayList<double[]> mergedHistory = new ArrayList<double[]>(); private void merger() { try { double[] entry; double[] entry2; double[] newEntry; double[] historyEntry; int a, b, k, numCurrentClasses; double currentClassMax = numClasses; double minDist, dist; int mergedClass1 = 0; int mergedClass2 = 0; double combinedClassSize = 0; centres.clear(); for (a = 0; a < numClasses; a++) { entry = new double[numDimensions + 2]; entry[0] = a; entry[1] = classSizes[a]; for (k = 0; k < numDimensions; k++) { entry[k + 2] = centroidVectors[a][k]; } centres.add(entry); //mergedHistory.add(new double[]{a, a, 0, a}); } do { numCurrentClasses = centres.size(); // find the closest pair of classes. minDist = Float.POSITIVE_INFINITY; for (a = 0; a < numCurrentClasses; a++) { entry = centres.get(a); for (b = 0; b < numCurrentClasses; b++) { if (b > a) { entry2 = centres.get(b); dist = 0; for (k = 2; k <= numDimensions + 1; k++) { dist += (entry[k] - entry2[k]) * (entry[k] - entry2[k]); } if (dist < minDist) { minDist = dist; mergedClass1 = a; mergedClass2 = b; } } } } entry = centres.get(mergedClass1); entry2 = centres.get(mergedClass2); historyEntry = new double[4]; historyEntry[0] = entry[0]; historyEntry[1] = entry2[0]; historyEntry[2] = Math.sqrt(minDist); historyEntry[3] = currentClassMax; if (historyEntry[2] > maxDist) { maxDist = historyEntry[2]; } mergedHistory.add(historyEntry); // now actually perform the merging newEntry = new double[numDimensions + 2]; combinedClassSize = entry[1] + entry2[1]; if (entry[1] > entry2[1]) { newEntry = entry.clone(); } else { newEntry = entry2.clone(); } newEntry[0] = currentClassMax; newEntry[1] = combinedClassSize; // for (k = 2; k <= numDimensions + 1; k++) { // newEntry[k] = entry[k] * entry[1] / combinedClassSize + entry2[k] * entry2[1] / combinedClassSize; // //newEntry[k] = (entry[k] + entry2[k]) / 2; // } currentClassMax++; if (mergedClass1 > mergedClass2) { // remove the higher class first centres.remove(mergedClass1); centres.remove(mergedClass2); } else { centres.remove(mergedClass2); centres.remove(mergedClass1); } centres.add(newEntry); } while (centres.size() > 1); yValues = new double[(int)currentClassMax]; for (a = 0; a < (int)(currentClassMax); a++) { yValues[a] = -1; } inOrder(currentClassMax - 1); } catch (Exception e) { System.out.println(e.getMessage()); } } private double[] yValues; private int[] classOrder; // exterior nodes; used for labelling the y axis. private int m = 0; private void inOrder(double root) { try { double left, right; for (int a = 0; a < mergedHistory.size(); a++) { if (mergedHistory.get(a)[3] == root) { // && mergedHistory.get(a)[0] != root left = mergedHistory.get(a)[0]; right = mergedHistory.get(a)[1]; if (left >= numClasses) { inOrder(left); } else { classOrder[m] = (int) left; yValues[(int)left] = m; m++; } if (right >= numClasses) { inOrder(right); } else { classOrder[m] = (int) right; yValues[(int)right] = m; m++; } if (yValues[(int)left] >= 0 && yValues[(int)right] >=0) { yValues[(int)root] = (yValues[(int)left] + yValues[(int)right]) / 2; } } } } catch (Exception e) { System.out.println(e.getMessage()); } } private double[] getDistance(double classNum) { try { double stX = -1; double endX = -1; if (classNum < numClasses) { stX = 0; } else { for (int a = 0; a < mergedHistory.size(); a++) { if (mergedHistory.get(a)[3] == classNum) { stX = mergedHistory.get(a)[2]; } } } for (int a = 0; a < mergedHistory.size(); a++) { if (mergedHistory.get(a)[0] == classNum || mergedHistory.get(a)[1] == classNum) { endX = mergedHistory.get(a)[2]; } } if (endX == -1) { endX = maxDist + maxDist * 0.1; } return new double[]{stX, endX}; } catch (Exception e) { System.out.println(e.getMessage()); return new double[]{-1, -1}; } } private void setMouseMotionListener() { this.addMouseMotionListener(this); } private void setMouseListener() { this.addMouseListener(this); } public void refresh() { repaint(); } @Override public void paint(Graphics g) { drawPlot(g); } int plotIndex = 0; private void drawPlot(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setColor(Color.white); g2d.fillRect(0, 0, getWidth(), getHeight()); double activeWidth = getWidth() - leftMargin - rightMargin; double activeHeight = getHeight() - topMargin - bottomMargin; int bottomY = getHeight() - bottomMargin; int rightX = getWidth() - rightMargin; double xScale = (maxDist + maxDist * 0.1) / activeWidth; double yScale = (numClasses + 1) / activeHeight; int x1, x2, y1, y2; // draw axes g2d.setColor(Color.black); g2d.drawLine(leftMargin, bottomY, rightX, bottomY); g2d.drawLine(leftMargin, bottomY, leftMargin, topMargin); g2d.drawLine(leftMargin, topMargin, rightX, topMargin); g2d.drawLine(rightX, bottomY, rightX, topMargin); // draw ticks int tickSize = 4; double range = 1; if (maxDist < 1) { range = 0.1; } else if (maxDist < 10) { range = 1; } else if (maxDist < 100) { range = 10; } else if (maxDist < 1000) { range = 100; } else { range = 1000; } int numTicks = (int)(activeWidth / range); for (int i = 0; i <= numTicks; i++) { x1 = (int)(leftMargin + (i * range / xScale)); g2d.drawLine(x1, bottomY, x1, bottomY + tickSize); } for (int i = 1; i <= numClasses; i++) { y1 = (int)(bottomY - (double)(i) / (numClasses + 1) * activeHeight); g2d.drawLine(leftMargin, y1, leftMargin - tickSize, y1); } // labels DecimalFormat df = new DecimalFormat("#,###,##0.0"); Font font = new Font("SanSerif", Font.PLAIN, 11); FontMetrics metrics = g.getFontMetrics(font); int hgt, adv; hgt = metrics.getHeight(); // x-axis labels String label; for (int i = 0; i <= numTicks; i++) { x1 = (int) (leftMargin + (i * range / xScale)); label = df.format(range * i); adv = metrics.stringWidth(label) / 2; g2d.drawString(label, x1 - adv, bottomY + hgt + 4); g2d.drawLine(x1, bottomY, x1, bottomY + tickSize); } label = "Euclidean Distance"; adv = metrics.stringWidth(label); int xAxisMidPoint = (int)(leftMargin + activeWidth / 2); g2d.drawString(label, xAxisMidPoint - adv / 2, bottomY + 2 * hgt + 9); // y-axis labels // rotate the font Font oldFont = g.getFont(); Font f = oldFont.deriveFont(AffineTransform.getRotateInstance(-Math.PI / 2.0)); g2d.setFont(f); int yAxisMidPoint = (int)(topMargin + activeHeight / 2); int offset; label = "Class"; offset = metrics.stringWidth(String.valueOf(numClasses)) + 12 + hgt; adv = metrics.stringWidth(label); g2d.drawString(label, leftMargin - offset, yAxisMidPoint + adv / 2); // replace the rotated font. g2d.setFont(oldFont); // df = new DecimalFormat("0.0"); // for (int i = 0; i <= 1000; i += 100) { // label = df.format(i / 10); // y1 = (int)(bottomY - i / 1000.0 * activeHeight); // adv = metrics.stringWidth(label); // g2d.drawString(label, leftMargin - adv - 12, y1 + hgt / 2); // } for (int i = 1; i <= numClasses; i++) { y1 = (int)(bottomY - (double)(i) / (numClasses + 1) * activeHeight); label = String.valueOf(classOrder[i - 1]); adv = metrics.stringWidth(label); g2d.drawString(label, leftMargin - adv - 12, y1 + hgt / 2); } // title // bold font oldFont = g.getFont(); font = font = new Font("SanSerif", Font.BOLD, 12); g2d.setFont(font); label = "Classification Dendrogram"; adv = metrics.stringWidth(label); g2d.drawString(label, getWidth() / 2 - adv / 2, topMargin - hgt - 5); g2d.setFont(oldFont); // draw the lines g2d.setColor(Color.red); double[] dist; // horizontal lines first for (int i = 0; i < yValues.length; i++) { dist = getDistance(i); x1 = (int)(leftMargin + dist[0] / xScale); y1 = (int)(bottomY - (yValues[i] + 1) / (numClasses + 1) * activeHeight); x2 = (int)(leftMargin + dist[1] / xScale); g2d.drawLine(x1, y1, x2, y1); } // vertical lines second int left, right; for (int i = 0; i < mergedHistory.size(); i++) { x1 = (int)(leftMargin + mergedHistory.get(i)[2] / xScale); left = (int)mergedHistory.get(i)[0]; right = (int)mergedHistory.get(i)[1]; y1 = (int)(bottomY - (yValues[left] + 1) / (numClasses + 1) * activeHeight); y2 = (int)(bottomY - (yValues[right] + 1) / (numClasses + 1) * activeHeight); g2d.drawLine(x1, y1, x1, y2); } } public boolean saveToImage(String fileName) { try { int width = (int) this.getWidth(); int height = (int) this.getHeight(); // TYPE_INT_ARGB specifies the image format: 8-bit RGBA packed // into integer pixels BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics ig = bi.createGraphics(); drawPlot(ig); int i = fileName.lastIndexOf("."); String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase(); if (!ImageIO.write(bi, extension, new File(fileName))) { return false; } return true; } catch (Exception ex) { return false; } } @Override public int print(Graphics g, PageFormat pf, int page) throws PrinterException { if (page > 0) { return NO_SUCH_PAGE; } int i = pf.getOrientation(); // get the size of the page double pageWidth = pf.getImageableWidth(); double pageHeight = pf.getImageableHeight(); double myWidth = this.getWidth();// - borderWidth * 2; double myHeight = this.getHeight();// - borderWidth * 2; double scaleX = pageWidth / myWidth; double scaleY = pageHeight / myHeight; double minScale = Math.min(scaleX, scaleY); Graphics2D g2d = (Graphics2D) g; g2d.translate(pf.getImageableX(), pf.getImageableY()); g2d.scale(minScale, minScale); drawPlot(g); return PAGE_EXISTS; } // @Override public void mouseMoved(MouseEvent e) { } private void createPopupMenus() { // menu myPopup = new JPopupMenu(); JMenuItem mi = new JMenuItem("Save"); mi.addActionListener(this); mi.setActionCommand("save"); myPopup.add(mi); mi = new JMenuItem("Print"); mi.addActionListener(this); mi.setActionCommand("print"); myPopup.add(mi); // myPopup.addSeparator(); // // gridMi = new JMenuItem("Turn Off Grid"); // gridMi.addActionListener(this); // gridMi.setActionCommand("grid"); // myPopup.add(gridMi); // myPopup.setOpaque(true); myPopup.setLightWeightPopupEnabled(true); } private void printPlot() { PrinterJob job = PrinterJob.getPrinterJob(); PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet(); //PageFormat pf = job.pageDialog(aset); job.setPrintable(this); boolean ok = job.printDialog(aset); if (ok) { try { job.print(aset); } catch (PrinterException ex) { //showFeedback("An error was encountered while printing." + ex); /* The job did not successfully complete */ } } } private void savePlotAsImage() { // get the possible image name. String imageName = "ScreePlot.png"; // Ask the user to specify a file name for saving the histo. String pathSep = File.separator; JFileChooser fc = new JFileChooser(); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); //fc.setCurrentDirectory(new File(workingDirectory + pathSep + imageName + ".png")); fc.setAcceptAllFileFilterUsed(false); //File f = new File(workingDirectory + pathSep + imageName + ".png"); //fc.setSelectedFile(f); // set the filter. ArrayList<ExtensionFileFilter> filters = new ArrayList<ExtensionFileFilter>(); String[] extensions = ImageIO.getReaderFormatNames(); //{"PNG", "JPEG", "JPG"}; String filterDescription = "Image Files (" + extensions[0]; for (int i = 1; i < extensions.length; i++) { filterDescription += ", " + extensions[i]; } filterDescription += ")"; ExtensionFileFilter eff = new ExtensionFileFilter(filterDescription, extensions); fc.setFileFilter(eff); int result = fc.showSaveDialog(this); File file = null; if (result == JFileChooser.APPROVE_OPTION) { file = fc.getSelectedFile(); // see if file has an extension. if (file.toString().lastIndexOf(".") <= 0) { String fileName = file.toString() + ".png"; file = new File(fileName); } String fileDirectory = file.getParentFile() + pathSep; // if (!fileDirectory.equals(workingDirectory)) { // workingDirectory = fileDirectory; // } // see if the file exists already, and if so, should it be overwritten? if (file.exists()) { Object[] options = {"Yes", "No"}; int n = JOptionPane.showOptionDialog(this, "The file already exists.\n" + "Would you like to overwrite it?", "Whitebox GAT Message", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, //do not use a custom Icon options, //the titles of buttons options[0]); //default button title if (n == JOptionPane.YES_OPTION) { file.delete(); } else if (n == JOptionPane.NO_OPTION) { return; } } if (!saveToImage(file.toString())) { // showFeedback("An error occurred while saving the map to the image file."); } } } @Override public void mouseDragged(MouseEvent me) { //throw new UnsupportedOperationException("Not supported yet."); } // ActionListener for events @Override public void actionPerformed(ActionEvent ae) { String actionCommand = ae.getActionCommand().toLowerCase(); if (actionCommand.equals("save")) { savePlotAsImage(); } else if (actionCommand.equals("print")) { printPlot(); } else if (actionCommand.equals("grid")) { if (gridOn) { gridOn = false; gridMi.setText("Turn On Grid"); } else if (!gridOn) { gridOn = true; gridMi.setText("Turn Off Grid"); } refresh(); } } @Override public void mouseClicked(MouseEvent me) { if (me.getButton() == 3 || me.isPopupTrigger()) { myPopup.show((Component) me.getSource(), me.getX(), me.getY()); } } @Override public void mousePressed(MouseEvent me) { } @Override public void mouseReleased(MouseEvent me) { //throw new UnsupportedOperationException("Not supported yet."); } @Override public void mouseEntered(MouseEvent me) { //throw new UnsupportedOperationException("Not supported yet."); } @Override public void mouseExited(MouseEvent me) { //throw new UnsupportedOperationException("Not supported yet."); } }