/*
* The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
* for visualizing and manipulating spatial features with geometry and attributes.
*
* JUMP is Copyright (C) 2003 Vivid Solutions
*
* This class implements extensions to JUMP and is
* Copyright (C) Stefan Steiniger.
*
* 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:
* Stefan Steiniger
* perriger@gmx.de
*/
/***********************************************
* created on 19.10.2007
* last modified:
*
* author: sstein
***********************************************/
package org.openjump.core.ui.plugin.tools.statistics;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JInternalFrame;
import org.openjump.core.apitools.FeatureSchemaTools;
import org.openjump.core.attributeoperations.Classifier1D;
import org.openjump.core.ui.plot.Plot2DPanelOJ;
import com.vividsolutions.jump.I18N;
import com.vividsolutions.jump.feature.AttributeType;
import com.vividsolutions.jump.feature.Feature;
import com.vividsolutions.jump.feature.FeatureCollection;
import com.vividsolutions.jump.feature.FeatureDataset;
import com.vividsolutions.jump.feature.FeatureSchema;
import com.vividsolutions.jump.task.TaskMonitor;
import com.vividsolutions.jump.workbench.WorkbenchContext;
import com.vividsolutions.jump.workbench.model.Layer;
import com.vividsolutions.jump.workbench.model.LayerManager;
import com.vividsolutions.jump.workbench.model.StandardCategoryNames;
import com.vividsolutions.jump.workbench.plugin.AbstractPlugIn;
import com.vividsolutions.jump.workbench.plugin.EnableCheckFactory;
import com.vividsolutions.jump.workbench.plugin.MultiEnableCheck;
import com.vividsolutions.jump.workbench.plugin.PlugInContext;
import com.vividsolutions.jump.workbench.plugin.ThreadedPlugIn;
import com.vividsolutions.jump.workbench.ui.GUIUtil;
import com.vividsolutions.jump.workbench.ui.GenericNames;
import com.vividsolutions.jump.workbench.ui.MenuNames;
import com.vividsolutions.jump.workbench.ui.MultiInputDialog;
import com.vividsolutions.jump.workbench.ui.plugin.FeatureInstaller;
//TODO: remove for other classification plugins the "save" option (taken from histogram plugin)
public class ClassifyAttributesPlugIn extends AbstractPlugIn implements ThreadedPlugIn{
private static String pluginname = "classifyplot";
private MultiInputDialog dialog;
private String sideBarText = "Classifies attribute data with the chosen method.\n" +
"The result is added as new field to the attribute table.";
private String CLASSIFIER = "select classification method";
private String T2 ="number of classes";
private String CLAYER = "select layer";
private String ATTRIBUTE = "select attribute";
private String OPTIMIZEWITHKMEANS = "optimize with k-means" ;
private String PROCESSNULLASZERO = "process null as zero";
private String sClassbreaks = "class breaks";
private String sDatapoints = "data points";
private String sCount = "count";
private String sHistogram = "Histogram";
private String sCalculateBreaks = "Calculate breaks";
private String sDisplayBreaks = "Display Breaks";
private String sClassifying = "classifying";
private String sAddingField = "adding field";
private Layer selLayer = null;
private int ranges = 7;
private FeatureCollection fc = null;
private String selAttribute = null;
private String selClassifier = null;
private Boolean useKmeans = false;
private boolean nullAsZero = false;
public LayerManager currentLM = null;
private String sName = "Classify Attributes";
private String sWarning = "problems appeared";
private String sNotEnoughValuesWarning = "valid values is not enough";
private String sWrongDataType = "Wrong datatype of chosen attribute";
private String sNoAttributeChoosen = "No attribute choosen";
/**
* this method is called on the startup by JUMP/OpenJUMP.
* We set here the menu entry for calling the function.
*/
public void initialize(PlugInContext context) throws Exception {
sideBarText = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.descriptiontext");
CLASSIFIER = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Select-classification-method");
T2 = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Number-of-classes");
CLAYER = GenericNames.SELECT_LAYER;
ATTRIBUTE = GenericNames.SELECT_ATTRIBUTE;
OPTIMIZEWITHKMEANS = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Optimize-with-k-means");
PROCESSNULLASZERO = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Process-null-as-zero");
sClassbreaks = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.class-breaks");
sDatapoints = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.data-points");
sCount = I18N.get("org.openjump.core.ui.plugin.tools.statistics.CreateHistogramPlugIn.count");
sHistogram = I18N.get("org.openjump.core.ui.plugin.tools.statistics.CreateHistogramPlugIn.Histogram-Plot");
sCalculateBreaks = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Calculating-Breaks");
sDisplayBreaks = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Displaying-Breaks");
sClassifying = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.classifying");
sAddingField = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.create-output-field");
sName = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Classify-Attribute");
sWarning = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Error-during-classification");
sNotEnoughValuesWarning = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.Not-enough-values");
sWrongDataType = I18N.get("org.openjump.core.ui.plugin.tools.statistics.CreateBarPlotPlugIn.Wrong-datatype-of-chosen-attribute");
sNoAttributeChoosen = I18N.get("org.openjump.core.ui.plugin.tools.statistics.ClassifyAttributesPlugin.No-attribute-choosen");
FeatureInstaller featureInstaller = new FeatureInstaller(context.getWorkbenchContext());
featureInstaller.addMainMenuItem(
this, //exe
//new String[] {MenuNames.TOOLS, MenuNames.TOOLS_ANALYSIS}, //menu path
new String[] {MenuNames.TOOLS, MenuNames.STATISTICS},
this.sName + "...", //name methode .getName recieved by AbstractPlugIn
false, //checkbox
null, //icon
createEnableCheck(context.getWorkbenchContext())); //enable check
}
/**
* This method is used to define when the menu entry is activated or
* disabled. In this example we allow the menu entry to be usable only
* if one layer exists.
*/
public static MultiEnableCheck createEnableCheck(WorkbenchContext workbenchContext) {
EnableCheckFactory checkFactory = new EnableCheckFactory(workbenchContext);
return new MultiEnableCheck()
.add(checkFactory.createAtLeastNLayersMustExistCheck(1))
//.add(checkFactory.createAtLeastNLayersMustBeEditableCheck(1))
.add(checkFactory.createTaskWindowMustBeActiveCheck());
}
/**
* this function is called by JUMP/OpenJUMP if one clicks on the menu entry.
* It is called before the "run" method and useful to do all the GUI /user-input things
* In this example we call two additional methods {@link #setDialogValues(MultiInputDialog, PlugInContext)}
* and {@link #getDialogValues(MultiInputDialog)} to obtain the Layer and the buffer radius by the user.
*/
public boolean execute(PlugInContext context) throws Exception{
this.reportNothingToUndoYet(context);
dialog = new MultiInputDialog(
context.getWorkbenchFrame(), sName, true);
this.setDialogValues(dialog, context);
GUIUtil.centreOnWindow(dialog);
dialog.setVisible(true);
if (! dialog.wasOKPressed()) { return false; }
this.getDialogValues(dialog);
return true;
}
public void run(TaskMonitor monitor, PlugInContext context) throws Exception {
//-- get the LM because when the Histogram will be shown, the app. focus
// will change and context.addLayer will not work (null pointer exc.)
this.currentLM = context.getLayerManager();
monitor.allowCancellationRequests();
// if (this.selLayer.isEditable() == true){
if (this.selAttribute == null) {
context.getWorkbenchFrame().warnUser(I18N.get(sNoAttributeChoosen));
return;
}
FeatureDataset result = classifyAndCreatePlot(monitor, context);
if (result == null) {
context.getWorkbenchFrame().warnUser(I18N.get(sNotEnoughValuesWarning));
}
else if(result.size() > 0){
String name = this.selAttribute + "_" + this.selClassifier;
this.currentLM.addLayer(StandardCategoryNames.WORKING, name, result);
}
else{
context.getWorkbenchFrame().warnUser(sWarning);
}
// }
// else{
// context.getWorkbenchFrame().warnUser("Layer not Editable");
// }
}
private void setDialogValues(MultiInputDialog dialog, PlugInContext context)
{
dialog.setSideBarDescription(sideBarText);
dialog.addLayerComboBox(CLAYER, context.getCandidateLayer(0), context.getLayerManager());
List listNumAttributes = FeatureSchemaTools.getFieldsFromLayerWithoutGeometryAndString(context.getCandidateLayer(0));
Object valAttribute = listNumAttributes.size()>0?listNumAttributes.iterator().next():null;
final JComboBox jcb_attribute = dialog.addComboBox(this.ATTRIBUTE, valAttribute, listNumAttributes, this.ATTRIBUTE);
if (listNumAttributes.size() == 0) jcb_attribute.setEnabled(false);
List listClassifiers = Classifier1D.getAvailableClassificationMethods();
Object valClassifier = listNumAttributes.size()>0?listNumAttributes.iterator().next():null;
final JComboBox jcb_classifier = dialog.addComboBox(this.CLASSIFIER, valClassifier, listClassifiers, this.CLASSIFIER);
dialog.addIntegerField(T2, this.ranges, 6, T2);
dialog.addCheckBox(this.OPTIMIZEWITHKMEANS, false);
dialog.addCheckBox(this.PROCESSNULLASZERO, false);
dialog.getComboBox(CLAYER).addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
List list = getFieldsFromLayerWithoutGeometryAndString();
if (list.size() == 0) {
jcb_attribute.setModel(new DefaultComboBoxModel(new String[0]));
jcb_attribute.setEnabled(false);
}
else {
jcb_attribute.setModel(new DefaultComboBoxModel(list.toArray(new String[0])));
jcb_attribute.setEnabled(true);
}
}
});
}
private void getDialogValues(MultiInputDialog dialog) {
this.ranges = dialog.getInteger(T2);
this.selLayer = dialog.getLayer(CLAYER);
this.fc = this.selLayer.getFeatureCollectionWrapper();
this.selAttribute = dialog.getText(ATTRIBUTE);
this.selClassifier = dialog.getText(this.CLASSIFIER);
this.useKmeans = dialog.getBoolean(this.OPTIMIZEWITHKMEANS);
this.nullAsZero = dialog.getBoolean(this.PROCESSNULLASZERO);
}
private FeatureDataset classifyAndCreatePlot(TaskMonitor monitor, final PlugInContext context) throws Exception {
monitor.report(this.sCalculateBreaks);
//=============== get DATA and prepare ==============/
FeatureSchema fs = this.fc.getFeatureSchema();
AttributeType type = null;
if ((fs.getAttributeType(this.selAttribute) == AttributeType.DOUBLE) ||
(fs.getAttributeType(this.selAttribute) == AttributeType.INTEGER)){
//-- move on
type = fs.getAttributeType(this.selAttribute);
}
else{
//System.out.println("ClassifyAttributesPlugIn: wrong datatype of chosen attribute");
context.getWorkbenchFrame().warnUser(sWrongDataType);
return null;
}
int size = getFeatureCollectionSize(this.fc, this.selAttribute, this.nullAsZero);
if (size < 3) {
return null;
}
this.ranges = Math.min(this.ranges, size);
double[] data = new double[size];
double[][] plotdata = new double[2][size]; //for drawing 1-D scatter plot
int[] fID = new int[size];
int i = 0;
for (Iterator iter = fc.iterator(); iter.hasNext();) {
Feature f = (Feature) iter.next();
if (f.getAttribute(this.selAttribute)==null && !nullAsZero) continue;
fID[i] = f.getID();
plotdata[1][i] = 1;
Object val = f.getAttribute(this.selAttribute);
if (type == AttributeType.DOUBLE){
if (val == null) data[i] = 0.0;
else data[i] = ((Double)val).doubleValue();
}
else if (type == AttributeType.INTEGER){
if (val == null) data[i] = 0;
else data[i] = ((Integer)val).intValue();
}
plotdata[0][i] = data[i];
i++;
}
/*
//-- some testdata
double[][] plotdata2 = new double[2][8];
double[] data2 = { -2, 4, 6, 5, 0, 10, 7, 1 };
double[] axis2 = { 1, 1, 1, 1, 1, 1, 1, 1 };
plotdata2[0] = data2;
plotdata2[1] = axis2;
*/
if(monitor.isCancelRequested()){
return null;
}
//=============== find breaks according to chosen method ==============/
double[] limits = null;
if (this.useKmeans == false){
if (this.selClassifier == Classifier1D.EQUAL_NUMBER){
limits = Classifier1D.classifyEqualNumber(data, this.ranges);
}
else if(this.selClassifier == Classifier1D.EQUAL_RANGE){
limits = Classifier1D.classifyEqualRange(data, this.ranges);
}
else if(this.selClassifier == Classifier1D.MEAN_STDEV){
limits = Classifier1D.classifyMeanStandardDeviation(data, this.ranges);
}
else if(this.selClassifier == Classifier1D.MAX_BREAKS){
limits = Classifier1D.classifyMaxBreaks(data, this.ranges);
}
else if(this.selClassifier == Classifier1D.JENKS_BREAKS){
limits = Classifier1D.classifyNaturalBreaks(data, this.ranges);
}
}
else{
if (this.selClassifier == Classifier1D.EQUAL_NUMBER){
limits = Classifier1D.classifyKMeansOnExistingBreaks(data, this.ranges, 3);
}
else if(this.selClassifier == Classifier1D.EQUAL_RANGE){
limits = Classifier1D.classifyKMeansOnExistingBreaks(data, this.ranges, 2 );
}
else if(this.selClassifier == Classifier1D.MEAN_STDEV){
limits = Classifier1D.classifyKMeansOnExistingBreaks(data, this.ranges, 4 );
}
else if(this.selClassifier == Classifier1D.MAX_BREAKS){
limits = Classifier1D.classifyKMeansOnExistingBreaks(data, this.ranges, 1 );
}
else if(this.selClassifier == Classifier1D.JENKS_BREAKS){
limits = Classifier1D.classifyKMeansOnExistingBreaks(data, this.ranges, 5 );
}
}
if(monitor.isCancelRequested()){
return null;
}
monitor.report(this.sDisplayBreaks);
//=============== plot data and class breaks ==============/
//-- do display here - in case we later want to allow interactive editing of the limits
//-- reformat limits
double[][] limits2show = new double[2][limits.length];
//-- due to bug in jmathplot add limits twice if only three classes = 2breaks are sought
if (limits.length == 2){
limits2show = new double[2][limits.length*2];
}
for (int j = 0; j < limits.length; j++) {
limits2show[0][j]= limits[j]; //x-axis
limits2show[1][j]= Math.floor(i/(4.0*this.ranges)); //y-axis, estimate height of "bar" from number of items
//limits2show[1][j]= 1;
//-- due to bug in jmathplot add limits twice if only three classes are sought
if (limits.length == 2){
limits2show[0][limits.length+j]= limits[j];
limits2show[1][limits.length+j]= Math.floor(i/(4.0*this.ranges));
}
}
//=============== plot data and class breaks ==============/
//-- create plots
final Plot2DPanelOJ plot = new Plot2DPanelOJ();
plot.addHistogramPlotOJ(this.selAttribute, data, this.ranges*3, context, selLayer, this.selAttribute);
plot.addScatterPlotOJ(this.sDatapoints, plotdata, fID, context, this.selLayer);
plot.addBarPlot(this.sClassbreaks, limits2show);
plot.plotToolBar.setVisible(true);
plot.setAxisLabel(0, this.selAttribute);
plot.setAxisLabel(1, this.sCount);
plot.addLegend("SOUTH");
JInternalFrame frame = new JInternalFrame(this.sHistogram);
frame.setLayout(new BorderLayout());
frame.add(plot, BorderLayout.CENTER);
frame.setClosable(true);
frame.setResizable(true);
frame.setMaximizable(true);
frame.setSize(450, 450);
frame.setVisible(true);
context.getWorkbenchFrame().addInternalFrame(frame);
//=============== classify data ==============/
if(monitor.isCancelRequested()){
return null;
}
monitor.report(this.sClassifying);
int[] classes = Classifier1D.classifyData(data, limits);
//double[] classes = org.math.array.StatisticSample.one(data.length);
//context.getWorkbenchFrame().warnUser("classification not yet implemented");
//=============== add field ==============/
if(monitor.isCancelRequested()){
return null;
}
monitor.report(sAddingField);
FeatureDataset fd = null;
ArrayList outData = new ArrayList();
FeatureSchema targetFSnew = null;
int count=0;
Iterator iterp = fc.iterator();
String attname = this.selAttribute + "_" + this.selClassifier;
while(iterp.hasNext()){
//count=count+1;
// if(monitor != null){
// monitor.report("item: " + count + " of " + size);
// }
Feature p = (Feature)iterp.next();
Object val = p.getAttribute(this.selAttribute);
if (val == null && !this.nullAsZero) continue;
else count++;
if (count == 1){
FeatureSchema targetFs = p.getSchema();
targetFSnew = FeatureSchemaTools.copyFeatureSchema(targetFs);
if (targetFSnew.hasAttribute(attname)){
//attribute will be overwriten
}
else{
//add attribute
targetFSnew.addAttribute(attname, AttributeType.INTEGER);
}
}
//-- evaluate value for every polygon
Feature fcopy = FeatureSchemaTools.copyFeature(p, targetFSnew);
//fcopy.setAttribute(this.selClassifier, new Integer(classes[count-1]));
fcopy.setAttribute(attname, new Integer(classes[count-1]));
outData.add(fcopy);
}
fd = new FeatureDataset(targetFSnew);
fd.addAll(outData);
return fd;
}
private int getFeatureCollectionSize(FeatureCollection fc, String attribute, boolean nullAsZero) {
int size = 0;
for (Iterator it = fc.iterator(); it.hasNext();) {
Feature f = (Feature) it.next();
if (nullAsZero || f.getAttribute(attribute)!=null) size++;
}
return size;
}
private List getFieldsFromLayerWithoutGeometryAndString() {
return FeatureSchemaTools.getFieldsFromLayerWithoutGeometryAndString(dialog.getLayer(CLAYER));
}
}