/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * 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 2 * 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.workbench.ui.renderer.style.attributeclassifications; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import org.openjump.core.attributeoperations.Classifier1D; //import org.openjump.core.ui.style.classification.CalculRange; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jump.I18N; import com.vividsolutions.jump.util.CollectionUtil; import com.vividsolutions.jump.util.Range; import com.vividsolutions.jump.workbench.ui.renderer.style.ColorScheme; import com.vividsolutions.jump.workbench.ui.renderer.style.ColorThemingStylePanel; import com.vividsolutions.jump.workbench.ui.renderer.style.ColorThemingTableModel; import com.vividsolutions.jump.workbench.ui.renderer.style.ColorThemingStylePanel.State; import com.vividsolutions.jump.workbench.ui.renderer.style.ColorThemingTableModel.AttributeValueTableModelEvent; public class QuantileColorThemingState implements ColorThemingStylePanel.State { private ColorThemingStylePanel stylePanel; private static final String RANGE_COUNT_KEY = QuantileColorThemingState.class.getName() + " - QUANTILE COUNT"; public String getAllOtherValuesDescription() { return I18N.get("ui.renderer.style.RangeColorThemingState.values-below-these-values"); } public String getAttributeValueColumnTitle() { return I18N.get("ui.renderer.style.RangeColorThemingState.minimum-attribute-values"); } private int getRangeCount() { return ((Integer) comboBox.getSelectedItem()).intValue(); } private int getMaxAttributeClassCount() { return stylePanel.getAttributeValuesCount().entrySet().size()+1; } // return a collection of lower bound values for a quantile classification. // avgClassSize is the average size of a quantile class // valuesCount is the map (attributeValue -> number of corresponding entities) private Collection getQuantileBreaks(double avgClassSize, SortedMap valuesCount){ Set filteredValues = new TreeSet(); Iterator v = valuesCount.keySet().iterator(); if (!v.hasNext()) return filteredValues; Object value = v.next(); int count= ((Integer) valuesCount.get(value)).intValue(); filteredValues.add(value); // update of classification variables int classified = count; // total classified at this point double avgClassifiedAtNextLoop = avgClassSize; // expected to be classified at next loop while(v.hasNext()) { value = v.next(); count = ((Integer) valuesCount.get(value)).intValue(); if (((double)classified)>avgClassifiedAtNextLoop) { // System.out.println("break value : " + value.toString()); filteredValues.add(value); avgClassifiedAtNextLoop = avgClassifiedAtNextLoop + avgClassSize; // expected to be classified at next loop } classified = classified + count; // total classified at this point } return filteredValues; } /** * Returns a Collection with double values that are corresponding to the class breaks. * Note: the smallest number is equal to the minValue of all attributes. */ public Collection filterAttributeValues(SortedSet attributeValues) { //-1 because one row in the table is reserved for "all other values". [Jon Aquino] //int classCount = getRangeCount() - 1; Collection filteredValues = new ArrayList(); if (attributeValues.size() == 0) { return filteredValues; } // -1 deleted because class range is false int classCount = Math.min(getRangeCount(), attributeValues.size()); //-- [sstein 15.Feb. 2009] // replaced O-Bedels code by own code to be consistent // in case somebody uses Classifier1D methods for classification // and attaches the result as attribute values (i.e. to ensure // that same values are obtained) /* int featuresCount = stylePanel.getLayer().getFeatureCollectionWrapper().size(); double avgClassSize = ((double) featuresCount) / ((double) classCount); // type double to avoid round off errors SortedMap valuesCount = stylePanel.getAttributeValuesCount(); Collection filteredValues = getQuantileBreaks(avgClassSize, valuesCount); */ //-- sstein: new code double[] data = new double[attributeValues.size()]; int i=0; boolean isInteger = false; for (Iterator iterator = attributeValues.iterator(); iterator.hasNext();) { Object val = (Object) iterator.next(); if (val instanceof Integer){ data[i] = (Integer)val; isInteger = true; } else if (val instanceof Double){ data[i] = (Double)val; } else{ data[i] = Double.NaN; } i++; } double[] breaks = Classifier1D.classifyEqualNumber(data, classCount); double minVal = org.math.array.DoubleArray.min(data); //add minVal as smallest value if(isInteger){ filteredValues.add(new Integer((int)minVal)); } else{ filteredValues.add(new Double(minVal)); } for (int j = 0; j < breaks.length; j++) { if(isInteger){ filteredValues.add(new Integer((int)breaks[j])); } else{ filteredValues.add(new Double(breaks[j])); } } //-- sstein: end return filteredValues; } private JPanel panel = new JPanel(new GridBagLayout()) { public void setEnabled(boolean enabled) { comboBox.setEnabled(enabled); label.setEnabled(enabled); reverseButton.setEnabled(enabled); super.setEnabled(enabled); } }; public QuantileColorThemingState(final ColorThemingStylePanel stylePanel) { this.stylePanel = stylePanel; addComboBoxItems(); comboBox.setSelectedItem( stylePanel.getLayer().getLayerManager().getBlackboard().get( RANGE_COUNT_KEY, new Integer(5))); //Don't add action listeners until items have been added to the //combo box. [Jon Aquino] comboBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { stylePanel.populateTable(); stylePanel.getLayer().getLayerManager().getBlackboard().put( RANGE_COUNT_KEY, comboBox.getSelectedItem()); } }); panel.add( label, new GridBagConstraints( 1, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0)); panel.add( comboBox, new GridBagConstraints( 2, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0)); panel.add( reverseButton, new GridBagConstraints( 3, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0)); reverseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { reversingColorScheme = !reversingColorScheme; stylePanel.applyColorScheme(); } }); } private JButton reverseButton = new JButton(I18N.get("ui.renderer.style.RangeColorThemingState.reverse-colors")); private void addComboBoxItems() { int maxColorSchemeSize = -1; for (Iterator i = ColorScheme.rangeColorSchemeNames().iterator(); i.hasNext(); ) { String rangeColorSchemeName = (String) i.next(); maxColorSchemeSize = Math.max( maxColorSchemeSize, ColorScheme .create(rangeColorSchemeName) .getColors() .size()); } for (int i = 3; i <= maxColorSchemeSize; i++) { comboBoxModel.addElement(new Integer(i)); } } private DefaultComboBoxModel comboBoxModel = new DefaultComboBoxModel(); private JComboBox comboBox = new JComboBox(comboBoxModel); private JLabel label = new JLabel(I18N.get("ui.renderer.style.RangeColorThemingState.range-count")); public JComponent getPanel() { return panel; } public Map fromExternalFormat(Map attributeValueToObjectMap) { //Table takes values, not ranges. [Jon Aquino] TreeMap newMap = new TreeMap(); for (Iterator i = attributeValueToObjectMap.keySet().iterator(); i.hasNext(); ) { Range range = (Range) i.next(); newMap.put( range.getMin(), attributeValueToObjectMap.get(range)); } return newMap; } public Map toExternalFormat(Map attributeValueToObjectMap) { if (attributeValueToObjectMap.isEmpty()) { return attributeValueToObjectMap; } //Turn the values into ranges. Validations have already ensured that //the values are unique and contain no nulls. [Jon Aquino] Assert.isTrue(attributeValueToObjectMap instanceof SortedMap); TreeMap newMap = new Range.RangeTreeMap(); Object previousValue = null; for (Iterator i = attributeValueToObjectMap.keySet().iterator(); i.hasNext(); ) { Object value = i.next(); try { if (previousValue == null) { //Let the default style handle values from negative infinity to //the first value. [Jon Aquino] continue; } //Make one side inclusive and the other exclusive to ensure no //overlaps. [Jon Aquino] newMap.put( new Range(previousValue, true, value, false), attributeValueToObjectMap.get(previousValue)); } finally { previousValue = value; } } newMap.put( new Range(previousValue, true, new Range.PositiveInfinity(), false), attributeValueToObjectMap.get(previousValue)); return newMap; } public void applyColorScheme(ColorScheme colorScheme) { stylePanel.tableModel().apply( new ColorScheme( null, CollectionUtil.stretch( colorScheme.getColors(), new ArrayList(), stylePanel.tableModel().getRowCount())), false); } public Collection getColorSchemeNames() { return ColorScheme.rangeColorSchemeNames(); } private TableModelListener tableModelListener = new TableModelListener() { public void tableChanged(TableModelEvent e) { if (e instanceof ColorThemingTableModel.AttributeValueTableModelEvent) { stylePanel.tableModel().sort(stylePanel.tableModel().wasLastSortAscending()); //I'd like to scroll to the row at this point, but the user probably //finished the edit by clicking on another cell, so even if I scroll //to the row, it scrolls back to where the user clicked. [Jon Aquino] } } }; private int row(Object attributeValue) { for (int i = 0; i < stylePanel.tableModel().getRowCount(); i++) { Object otherAttributeValue = stylePanel.tableModel().getValueAt( i, ColorThemingTableModel.ATTRIBUTE_COLUMN); if (attributeValue == null && otherAttributeValue == null) { return i; } if (attributeValue != null && attributeValue.equals(otherAttributeValue)) { return i; } } Assert.shouldNeverReachHere(); return -1; } public void activate() { stylePanel.tableModel().addTableModelListener(tableModelListener); } public void deactivate() { stylePanel.tableModel().removeTableModelListener(tableModelListener); } private boolean reversingColorScheme = false; public ColorScheme filterColorScheme(ColorScheme colorScheme) { if (!reversingColorScheme) { return colorScheme; } List colors = new ArrayList(colorScheme.getColors()); Collections.reverse(colors); return new ColorScheme(colorScheme.getName(), colors); } }