/******************************************************************************* * Copyright (c) 2010 Stefan A. Tzeggai. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v2.1 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * Contributors: * Stefan A. Tzeggai - initial API and implementation ******************************************************************************/ package org.geopublishing.atlasStyler.swing; import java.awt.Component; import java.awt.Cursor; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.CellEditor; import javax.swing.DefaultComboBoxModel; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JToggleButton; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.table.DefaultTableModel; import net.miginfocom.swing.MigLayout; import org.apache.log4j.Logger; import org.geopublishing.atlasStyler.ASUtil; import org.geopublishing.atlasStyler.AtlasStyler; import org.geopublishing.atlasStyler.AtlasStylerVector; import org.geopublishing.atlasStyler.classification.CLASSIFICATION_METHOD; import org.geopublishing.atlasStyler.classification.Classification; import org.geopublishing.atlasStyler.classification.ClassificationChangeEvent; import org.geopublishing.atlasStyler.classification.ClassificationChangeEvent.CHANGETYPES; import org.geopublishing.atlasStyler.classification.ClassificationChangedAdapter; import de.schmitzm.lang.LangUtil; import de.schmitzm.swing.AtlasDialog; import de.schmitzm.swing.JPanel; import de.schmitzm.swing.SwingUtil; /** * This {@link JDialog} presents the user with descriptive statistics and a * histogram for your data. He may also choose the classification method or edit * the class breaks manually. * * @author SK * */ public abstract class ClassificationGUI extends AtlasDialog { protected final static Logger LOGGER = Logger .getLogger(ClassificationGUI.class); protected static final BufferedImage ERROR_IMAGE = new BufferedImage(400, 200, BufferedImage.TYPE_3BYTE_BGR); private static final BufferedImage WAIT_IMAGE = new BufferedImage(400, 200, BufferedImage.TYPE_3BYTE_BGR); private JPanel jContentPane = null; private JPanel jPanelLinksOben = null; private JLabel jLabelMethodSelection = null; private JLabel jLabelParameter = null; private JComboBox jComboBoxMethod = null; private JPanel jPanelDescriptiveStatistics = null; private JTable jTableStats = null; private JPanel panelLowerPart = null; private JLabel jFreeChartJLabel = null; private JToggleButton jToggleButton = null; private JTable jTableBreakValues = null; protected final Classification classifier; private JPanel jPanelHistParams = null; private JLabel jLabelHistogrammColumns = null; private JComboBox jComboBoxColumns = null; private JCheckBox jCheckBoxShowSD = null; private JCheckBox jCheckBoxShowMean = null; Integer histogramBins = 59; protected final AtlasStyler atlasStyler; public ClassificationGUI(Component owner, Classification classifier, AtlasStyler atlasStyler, String title) { super(SwingUtil.getParentWindow(owner), title); this.classifier = classifier; this.atlasStyler = atlasStyler; initialize(); SwingUtil.setRelativeFramePosition(this, owner, SwingUtil.BOUNDS_OUTER, SwingUtil.NORTHEAST); } /** * This method initializes this * * @return void */ private void initialize() { this.setModal(true); this.setContentPane(getJContentPane()); pack(); } /** * This method initializes jContentPane * * @return javax.swing.JPanel */ private JPanel getJContentPane() { if (jContentPane == null) { jContentPane = new JPanel(new MigLayout("flowy, gap 1, inset 1")); jContentPane.add(getJPanelLinksOben(), "sgx1, split 3"); jContentPane.add(getJPanelData(), "sgx1"); jContentPane.add(getJPanelHistogramParameters()); jContentPane .add(getJPanelLowerPart(), "gap related, spanx 2, wrap"); jContentPane.add(getJPanelDescriptiveStatistics(), "right, top"); } return jContentPane; } /** * This method initializes jPanel * * @return javax.swing.JPanel */ private JPanel getJPanelLinksOben() { if (jPanelLinksOben == null) { jLabelParameter = new JLabel( AtlasStylerVector.R("ComboBox.NumberOfClasses")); jLabelParameter.setToolTipText(ASUtil .R("ComboBox.NumberOfClasses.TT")); jLabelMethodSelection = new JLabel( ASUtil.R("QuantitiesClassificationGUI.Combobox.Method")); jPanelLinksOben = new JPanel( new MigLayout("wrap 2, gap 1, inset 1"), ASUtil.R("GraduatedColorQuantities.classification.BorderTitle")); jPanelLinksOben.add(jLabelMethodSelection); jPanelLinksOben.add(getJComboBoxMethod()); jPanelLinksOben.add(jLabelParameter); jPanelLinksOben.add(new NumClassesJComboBox(classifier)); } return jPanelLinksOben; } /** * Creates a {@link JComboBox} that offers to choose from one of the * classification methods. */ private JComboBox getJComboBoxMethod() { if (jComboBoxMethod == null) { jComboBoxMethod = new ClassificationMethodJComboBox(classifier); } return jComboBoxMethod; } private JPanel getJPanelDescriptiveStatistics() { if (jPanelDescriptiveStatistics == null) { jPanelDescriptiveStatistics = new JPanel(new MigLayout( "gap 1, inset 1")); jPanelDescriptiveStatistics .setBorder(BorderFactory.createTitledBorder(ASUtil .R("QuantitiesClassificationGUI.Statistics.BorderTitle"))); final JScrollPane scrollPane = new JScrollPane( getJTableStatistics()); SwingUtil.setPreferredWidth(scrollPane, 250); SwingUtil.setPreferredHeight(scrollPane, 140); jPanelDescriptiveStatistics.add(scrollPane); SwingUtil.setMinimumWidth(jPanelDescriptiveStatistics, 2); } return jPanelDescriptiveStatistics; } /** * This method initializes jTable * * @return javax.swing.JTable */ private JTable getJTableStatistics() { if (jTableStats == null) { jTableStats = new JTable(); /** * Classification.DescriptiveStatistics.Subsampled=Subsampling: * Classification.DescriptiveStatistics.Count=Count: * Classification.DescriptiveStatistics.Min=Minimum: * Classification.DescriptiveStatistics.Max=Maximum: * Classification.DescriptiveStatistics.Sum=Summe: * Classification.DescriptiveStatistics.Mean=airthm. Mittel: * Classification.DescriptiveStatistics.Median=Median: * Classification.DescriptiveStatistics.SD=Standard deviation: * Classification.DescriptiveStatistics.Excluded=Excluded: */ jTableStats.setModel(new DefaultTableModel() { final String[] fieldNames = new String[] { ASUtil.R("Classification.DescriptiveStatistics.Subsampling"), ASUtil.R("Classification.DescriptiveStatistics.Count"), ASUtil.R("Classification.DescriptiveStatistics.Min"), ASUtil.R("Classification.DescriptiveStatistics.Max"), ASUtil.R("Classification.DescriptiveStatistics.Sum"), ASUtil.R("Classification.DescriptiveStatistics.Mean"), ASUtil.R("Classification.DescriptiveStatistics.Median"), ASUtil.R("Classification.DescriptiveStatistics.SD"), ASUtil.R("Classification.DescriptiveStatistics.Excluded") }; @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 0) return String.class; if (columnIndex == 1) return Double.class; return null; } @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { return 9; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == 0) return fieldNames[rowIndex]; try { if (rowIndex == 0) { // Subsampling to every nth values if (classifier.getSubsampling() > 1) return ASUtil.R("Classification.DescriptiveStatistics.SubsamplingEveryNthValue",classifier.getSubsampling()); else return ASUtil.R("Classification.DescriptiveStatistics.SubsamplingAll"); } if (rowIndex == 1) // Count return classifier.getCount(); if (rowIndex == 2) // Min return classifier.getMin(); if (rowIndex == 3) // Max return classifier.getMax(); if (rowIndex == 4) // Sum return classifier.getSum(); if (rowIndex == 5) // Mean return classifier.getMean(); if (rowIndex == 6) // Median return classifier.getMedian(); if (rowIndex == 7) // SD return classifier.getSD(); if (rowIndex == 8) // NODATA return classifier.getNoDataValuesCount(); } catch (Exception e) { LOGGER.error("While creating the statistics:", e); } return "ERR"; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { } }); jTableStats.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); jTableStats.setTableHeader(null); jTableStats.setDefaultRenderer(Double.class, ASUtil.getDoubleCellRenderer()); /** * When the classification is recalculated, also repaint the table. * THis is actually only neede when a filter has been changed... but * its not expensive. */ classifier.addListener(new ClassificationChangedAdapter() { @Override public void classifierCalculatingStatistics( ClassificationChangeEvent e) { jTableStats.setEnabled(false); } @Override public void classifierAvailableNewClasses( ClassificationChangeEvent e) { ((DefaultTableModel) getJTableStatistics().getModel()) .fireTableStructureChanged(); } }); } return jTableStats; } /** * This method initializes jPanel3 * * @return javax.swing.JPanel */ private JPanel getJPanelLowerPart() { if (panelLowerPart == null) { panelLowerPart = new JPanel(new MigLayout("flowy, gap 1, inset 1")); JLabel jLabelBreaksTable = new JLabel( AtlasStylerVector.R("Classification.BreakValues")); jLabelBreaksTable.setToolTipText(AtlasStylerVector .R("Classification.BreakValues.TT")); JLabel jLabelBreaksExplanation = new JLabel( "<html><font size='-2'>" + AtlasStylerVector .R("Classification.BreakValues.TT") + "</html>"); panelLowerPart.add(getHistogram(), "wrap"); panelLowerPart.add(jLabelBreaksTable, "split 3"); panelLowerPart.add(jLabelBreaksExplanation); panelLowerPart.add(new JScrollPane(getJTableBreakValues())); SwingUtil.setPreferredHeight(panelLowerPart, (int) getHistogram() .getPreferredSize().getHeight()); SwingUtil.setPreferredWidth(panelLowerPart, (int) getHistogram() .getPreferredSize().getWidth() + 200); } return panelLowerPart; } /** * @return A {@link JLabel} showing the histogram. The {@link JLabel} is * updated when new classes are available * * @author <a href="mailto:skpublic@wikisquare.de">Stefan Alfons Tzeggai</a> */ private JLabel getHistogram() { if (jFreeChartJLabel == null) { jFreeChartJLabel = new JLabel(); classifier.addListener(new ClassificationChangedAdapter() { @Override public void classifierAvailableNewClasses( ClassificationChangeEvent e) { LOGGER.info("Histogram should appead now"); jFreeChartJLabel .setIcon(new ImageIcon(getHistogramImage())); jFreeChartJLabel.setCursor(null); } @Override public void classifierCalculatingStatistics( ClassificationChangeEvent e) { LOGGER.info("Histogram should vanish now"); jFreeChartJLabel.setIcon(new ImageIcon(WAIT_IMAGE)); jFreeChartJLabel.setCursor(Cursor .getPredefinedCursor(Cursor.WAIT_CURSOR)); } }); jFreeChartJLabel.setIcon(new ImageIcon(getHistogramImage())); } return jFreeChartJLabel; } protected abstract BufferedImage getHistogramImage(); /** * This method initializes jToggleButton * * @return javax.swing.JToggleButton */ private JToggleButton getJToggleButton() { if (jToggleButton == null) { jToggleButton = new JToggleButton(); jToggleButton.setText(ASUtil .R("QuantitiesClassificationGUI.ShowInPercent.Button")); jToggleButton.setToolTipText(ASUtil .R("QuantitiesClassificationGUI.ShowInPercent.Button.TT")); } return jToggleButton; } /** * This JTable shows the breaks of the classes. It is editable and has a * opup menu to add/remove class-breaks. */ private JTable getJTableBreakValues() { if (jTableBreakValues == null) { jTableBreakValues = new JTable(); jTableBreakValues .setSelectionMode(ListSelectionModel.SINGLE_SELECTION); jTableBreakValues.setModel(new DefaultTableModel() { @Override public Class<?> getColumnClass(int columnIndex) { if (columnIndex == 0) return Integer.class; if (columnIndex == 1) return Double.class; return null; } @Override public int getColumnCount() { return 2; } @Override public String getColumnName(int columnIndex) { if (columnIndex == 0) return "#"; if (columnIndex == 1) return ASUtil.R("Classification.BreakValues"); return super.getColumnName(columnIndex); } @Override public int getRowCount() { return classifier.getClassLimits().size(); } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == 1) return classifier.getClassLimits().toArray()[rowIndex]; return rowIndex; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { if (columnIndex == 1) return true; return false; } /** * This JTable is editable */ @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { if (columnIndex == 1) { try { Double aValue2 = (Double) aValue; if (classifier.getMethod() != CLASSIFICATION_METHOD.MANUAL) { classifier .setMethod(CLASSIFICATION_METHOD.MANUAL); } Object oldValue = classifier.getClassLimits() .toArray()[rowIndex]; classifier.getClassLimits().remove(oldValue); classifier.getClassLimits().add(aValue2); } catch (Exception e) { return; } finally { classifier.fireEvent(new ClassificationChangeEvent( CHANGETYPES.CLASSES_CHG)); } } } }); classifier.addListener(new ClassificationChangedAdapter() { @Override public void classifierAvailableNewClasses( ClassificationChangeEvent e) { ((DefaultTableModel) (jTableBreakValues.getModel())) .fireTableDataChanged(); getJTableBreakValues().setEnabled(true); } @Override public void classifierCalculatingStatistics( ClassificationChangeEvent e) { getJTableBreakValues().setEnabled(false); } }); jTableBreakValues.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); SwingUtil.setColumnLook(jTableBreakValues, 0, null, null, null, 30); SwingUtil.setColumnLook(jTableBreakValues, 1, ASUtil.getDoubleCellRenderer(), 30, null, null); ((JLabel) jTableBreakValues.getDefaultRenderer(Integer.class)) .setHorizontalAlignment(SwingConstants.CENTER); /** * Adding a right click mouse menu... */ jTableBreakValues.addMouseListener(new MouseAdapter() { private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger() && jTableBreakValues.isEnabled()) { Point p = new Point(e.getX(), e.getY()); // int col = jTableBreakValues.columnAtPoint(p); int row = jTableBreakValues.rowAtPoint(p); // // // translate table index to model index // int mcol = jTableBreakValues.getColumn( // jTableBreakValues.getColumnName(col)) // .getModelIndex(); if (row >= 0 && row < jTableBreakValues.getRowCount()) { cancelCellEditing(); // create popup menu... jTableBreakValues.getSelectionModel() .addSelectionInterval(row, row); JPopupMenu contextMenu = createContextMenu(row); // ... and show it if (contextMenu != null && contextMenu.getComponentCount() > 0) { contextMenu.show(jTableBreakValues, p.x, p.y); } } } } private JPopupMenu createContextMenu(final int rowIndex) { JPopupMenu contextMenu = new JPopupMenu(); /** * This action allows to add a new class-break at the * position if the mouse */ JMenuItem insertMenu = new JMenuItem(); insertMenu.setText(ASUtil .R("Classification.BreakValues.InsertNew")); insertMenu.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Double value = (Double) jTableBreakValues .getModel().getValueAt(rowIndex, 1); // Calculate #newValue that is the middle between // the selected and the next/previous class-break. // Integer secondAffectedClassNumber = null; // Integer newClassNumber = null; Double newValue; if (rowIndex == 0) { // The click was on the first class if (jTableBreakValues.getModel().getRowCount() > 1) { Double valueAfter = (Double) jTableBreakValues .getModel().getValueAt( rowIndex + 1, 1); newValue = valueAfter + ((value - valueAfter) / 2); // newClassNumber = 1; // secondAffectedClassNumber = 0; } else { // We have zero or one break newValue = value + 1; // newClassNumber = 0; } } else { // The click was between the first and the last // class Double valueBefore = (Double) jTableBreakValues .getModel().getValueAt(rowIndex - 1, 1); newValue = valueBefore + ((value - valueBefore) / 2); // if (rowIndex < classifier.getClassLimits() // .size()) { // // newClassNumber = rowIndex; // // secondAffectedClassNumber = rowIndex - 1; // } else { // // The click was on the last class // // newClassNumber = rowIndex; // // secondAffectedClassNumber = rowIndex + 1; // } } // Editing the classbreaks always turn's the // classification method to MANUAL if (classifier.getMethod() != CLASSIFICATION_METHOD.MANUAL) { classifier .setMethod(CLASSIFICATION_METHOD.MANUAL); } // The newValue has been calculated. Now insert it // into the ClassLimits. Because ClassLimits is a // sorted TreeSet, it will automatically be ordered // correctly. classifier.getClassLimits().add(newValue); classifier.fireEvent(new ClassificationChangeEvent( CHANGETYPES.NUM_CLASSES_CHG)); classifier.fireEvent(new ClassificationChangeEvent( CHANGETYPES.CLASSES_CHG)); } }); contextMenu.add(insertMenu); JMenuItem removeMenu = new JMenuItem(); removeMenu.setText(ASUtil .R("Classification.BreakValues.RemoveBreak")); removeMenu.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (jTableBreakValues.getModel().getRowCount() <= 2) { // Hey! Please don't remove the last // breaks...\nthere would be no class left. return; } if (classifier.getMethod() != CLASSIFICATION_METHOD.MANUAL) { classifier .setMethod(CLASSIFICATION_METHOD.MANUAL); } Double value = (Double) jTableBreakValues .getModel().getValueAt(rowIndex, 1); classifier.getClassLimits().remove(value); classifier.fireEvent(new ClassificationChangeEvent( CHANGETYPES.NUM_CLASSES_CHG)); classifier.fireEvent(new ClassificationChangeEvent( CHANGETYPES.CLASSES_CHG)); } }); contextMenu.add(removeMenu); JMenuItem copyMenu = new JMenuItem(); copyMenu.setText(ASUtil .R("Classification.BreakValues.Copy")); copyMenu.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Object value = jTableBreakValues.getModel() .getValueAt(rowIndex, 1); LangUtil.copyToClipboard(value == null ? "" : value .toString()); } }); contextMenu.add(copyMenu); JMenuItem pasteMenu = new JMenuItem(); pasteMenu.setText(ASUtil .R("Classification.BreakValues.Paste")); if (ASUtil.isClipboardContainingText(this) && getJTableBreakValues().getModel() .isCellEditable(rowIndex, 1)) { pasteMenu.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String value = ASUtil .getClipboardContents(ClassificationGUI.this); getJTableBreakValues().getModel().setValueAt( value, rowIndex, 1); classifier .fireEvent(new ClassificationChangeEvent( CHANGETYPES.CLASSES_CHG)); } }); } else { pasteMenu.setEnabled(false); } contextMenu.add(pasteMenu); return contextMenu; } @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } }); } return jTableBreakValues; }; private void cancelCellEditing() { CellEditor ce = getJTableBreakValues().getCellEditor(); if (ce != null) { ce.cancelCellEditing(); } } /** * This method initializes jPanelHistParams * * @return javax.swing.JPanel */ private JPanel getJPanelHistogramParameters() { if (jPanelHistParams == null) { jLabelHistogrammColumns = new JLabel( ASUtil.R("QuantitiesClassificationGUI.HistogramParameters.NoOfColums")); jLabelHistogrammColumns .setToolTipText(ASUtil .R("QuantitiesClassificationGUI.HistogramParameters.NoOfColums.TT")); jPanelHistParams = new JPanel(new MigLayout("gap 1, inset 1")); jPanelHistParams.add(jLabelHistogrammColumns); jPanelHistParams.add(getJComboBoxColumns(), "gap rel"); jPanelHistParams.add(getJCheckBoxShowSD(), "gap unrel"); jPanelHistParams.add(getJCheckBoxShowMean(), "gap unrel"); } return jPanelHistParams; } /** * This method initializes jComboBox * * @return javax.swing.JComboBox */ protected JComboBox getJComboBoxColumns() { if (jComboBoxColumns == null) { jComboBoxColumns = new JComboBox(); Integer[] items = new Integer[100]; for (Integer i = 1; i <= 100; i++) { items[i - 1] = i; } jComboBoxColumns.setModel(new DefaultComboBoxModel(items)); jComboBoxColumns.setSelectedIndex(histogramBins); jComboBoxColumns.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { histogramBins = (Integer) e.getItem(); jFreeChartJLabel.setIcon(new ImageIcon( getHistogramImage())); } } }); // Hier dreht was falsch! SwingUtil.addMouseWheelForCombobox(jComboBoxColumns, false); } return jComboBoxColumns; } /** * This method initializes jCheckBox * * @return javax.swing.JCheckBox */ protected JCheckBox getJCheckBoxShowSD() { if (jCheckBoxShowSD == null) { jCheckBoxShowSD = new JCheckBox( new AbstractAction( ASUtil.R("QuantitiesClassificationGUI.HistogramParameters.ShowSD")) { @Override public void actionPerformed(ActionEvent e) { jFreeChartJLabel.setIcon(new ImageIcon( getHistogramImage())); } }); } return jCheckBoxShowSD; } /** * This method initializes jCheckBox1 * * @return javax.swing.JCheckBox */ protected JCheckBox getJCheckBoxShowMean() { if (jCheckBoxShowMean == null) { jCheckBoxShowMean = new JCheckBox( new AbstractAction( ASUtil.R("QuantitiesClassificationGUI.HistogramParameters.ShowMean")) { @Override public void actionPerformed(ActionEvent e) { // THe getHistogrammImage() function checks the // state of // this checkbox jFreeChartJLabel.setIcon(new ImageIcon( getHistogramImage())); } }); } return jCheckBoxShowMean; } abstract protected JPanel getJPanelData(); public Classification getClassifier() { return classifier; } }