/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS 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.
*
* OrbisGIS 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
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.view.toc.actions.cui.legend.ui;
import net.miginfocom.swing.MigLayout;
import org.orbisgis.commons.progress.SwingWorkerPM;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.h2gis.utilities.TableLocation;
import org.orbisgis.corejdbc.MetaData;
import org.orbisgis.corejdbc.ReadTable;
import org.orbisgis.legend.thematic.LineParameters;
import org.orbisgis.legend.thematic.map.MappedLegend;
import org.orbisgis.legend.thematic.recode.AbstractRecodedLegend;
import org.orbisgis.commons.progress.ProgressMonitor;
import org.orbisgis.sif.ComponentUtil;
import org.orbisgis.sif.UIFactory;
import org.orbisgis.sif.UIPanel;
import org.orbisgis.view.toc.actions.cui.LegendContext;
import org.orbisgis.view.toc.actions.cui.legend.components.ColorConfigurationPanel;
import org.orbisgis.view.toc.actions.cui.legend.components.ColorScheme;
import org.orbisgis.view.toc.actions.cui.legend.model.TableModelUniqueValue;
import org.orbisgis.view.toc.actions.cui.legend.panels.AbsPanel;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.EventHandler;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.TreeSet;
/**
* Root class for Value Classifications.
*
* <p>This panel proposes a way to build a classification from scratch. This
* feature comes fortunately with a ProgressMonitor that can be used to cancel
* the building. This way, if accidentally trying to build a classification on
* a field with a lot of different values, the user can still cancel the
* operation. The feeding of the underlying recoded analysis becomes in fact
* really inefficient when it manages a lot of elements.</p>
*
* @author alexis
*/
public abstract class PnlAbstractUniqueValue<U extends LineParameters> extends PnlAbstractTableAnalysis<String,U> {
private static final I18n I18N = I18nFactory.getI18n(PnlAbstractUniqueValue.class);
public static final Logger LOGGER = LoggerFactory.getLogger(PnlAbstractUniqueValue.class);
private ColorConfigurationPanel colorConfigPanel;
private JPanel classifPanel;
private static final String COMPUTED = "Computed";
protected final static String JOB_NAME = "recodeSelectDistinct";
/**
* Contructor
*
* @param lc LegendContext
* @param legend Legend
*/
public PnlAbstractUniqueValue(LegendContext lc,
AbstractRecodedLegend<U> legend) {
super(lc, legend);
}
/**
* We take the fallback configuration and copy it for each key.
* @param set A set of keys we use as a basis.
* @param pm The progress monitor that can be used to stop the process.
* @return A fresh unique value analysis.
*/
public AbstractRecodedLegend<U> createConstantClassification(TreeSet<String> set, ProgressMonitor pm) {
U lp = getLegend().getFallbackParameters();
AbstractRecodedLegend<U> newRL = (AbstractRecodedLegend<U>) getEmptyAnalysis();
newRL.setComparator(getComparator());
newRL.setFallbackParameters(lp);
int size = set.size();
double m = size == 0 ? 0 : 90.0/(double)size;
int i = 0;
int n = 0;
pm.progressTo(50);
pm.startTask(CREATE_CLASSIF, 100);
for(String s : set){
newRL.put(s, lp);
if(i*m>n){
n++;
pm.progressTo(50*i/size);
}
if(pm.isCancelled()){
pm.endTask();
return null;
}
i++;
}
pm.progressTo(100);
pm.endTask();
pm.progressTo(100);
postProcess(newRL);
return newRL;
}
/**
* Get the comparator to be used to retrieve the values sorted the best way.
* @return A comparator that can be used with the keys of the associated mapping.
*/
public Comparator<String> getComparator(){
String fieldName = getFieldName();
try(Connection connection = getDataSource().getConnection()) {
int type = MetaData.getFieldType(connection, getTable(), fieldName);
return AbstractRecodedLegend.getComparator(type);
} catch (SQLException e) {
LOGGER.warn(I18N.tr("Can't build the analysis with an accurate comparator."),e);
}
return null;
}
/**
* Disables the colour configuration.
*/
public void onFromFallback(){
ComponentUtil.setFieldState(false, colorConfigPanel);
}
/**
* Enables the colour configuration.
*/
public void onComputed(){
ComponentUtil.setFieldState(true, colorConfigPanel);
}
/**
* Gets the JPanel that gathers all the buttons and labels to create a classification from scratch;
* @return The JPanel used to create a classification from scratch.
*/
@Override
public JPanel getCreateClassificationPanel() {
if(classifPanel == null){
classifPanel = new JPanel(new MigLayout("wrap 1", "[" + AbsPanel.FIXED_WIDTH + "]"));
classifPanel.setBorder(BorderFactory.createTitledBorder(
I18N.tr(CLASSIFICATION_SETTINGS)));
// Choose between fallback color and a color scheme
JRadioButton fromFallback = new JRadioButton(I18N.tr("Use the fallback color"));
fromFallback.setActionCommand(FALLBACK);
fromFallback.addActionListener(
EventHandler.create(ActionListener.class, this, "onFromFallback"));
JRadioButton computed = new JRadioButton(I18N.tr("Use a color scheme:"));
computed.setActionCommand(COMPUTED);
computed.addActionListener(
EventHandler.create(ActionListener.class, this, "onComputed"));
ButtonGroup bg = new ButtonGroup();
bg.add(fromFallback);
bg.add(computed);
bg.setSelected(computed.getModel(), true);
classifPanel.add(fromFallback);
classifPanel.add(computed);
// Color scheme panel
ArrayList<String> names = new ArrayList<String>(ColorScheme.discreteColorSchemeNames());
names.addAll(ColorScheme.rangeColorSchemeNames());
colorConfigPanel = new ColorConfigurationPanel(names);
classifPanel.add(colorConfigPanel, "align c, growx");
// Create button
JButton createButton = new JButton(I18N.tr("Create"));
createButton.setActionCommand("click");
createButton.addActionListener(
EventHandler.create(ActionListener.class, this, "onCreateClassification", ""));
classifPanel.add(createButton, "align c");
// Enable the color config by default
onComputed();
}
return classifPanel;
}
@Override
public int getPreviewColumn(){
return TableModelUniqueValue.PREVIEW_COLUMN;
}
@Override
public int getKeyColumn(){
return TableModelUniqueValue.KEY_COLUMN;
}
@Override
public Class getPreviewClass() {
return String.class;
}
@Override
public String getTitleBorder(){
return I18N.tr("Unique value classification");
}
/**
* Called to build a classification from the given data source and field. Makes a SELECT DISTINCT field FROM tableIdentifier;
* and feeds the legend that has been cleared prior to that.
*/
public void onCreateClassification(ActionEvent e){
if(e.getActionCommand().equals("click")){
String fieldName = getFieldName();
SelectDistinctJob<U> selectDistinct = new SelectDistinctJob<>(this, fieldName);
selectDistinct.execute();
}
}
/**
* This Job can be used as a background operation to retrieve a set containing the distinct data of a specific
* field in a DataSource.
*/
public static class SelectDistinctJob<U extends LineParameters> extends SwingWorkerPM {
private final String fieldName;
private TreeSet<String> result = null;
private PnlAbstractUniqueValue<U> pnlAbstractUniqueValue;
/**
* Builds the BackgroundJob.
* @param fieldName The name of the field we want the data from.
*/
public SelectDistinctJob(PnlAbstractUniqueValue<U> pnlAbstractUniqueValue, String fieldName) {
super(I18N.tr("Creating classification..."), 1);
this.pnlAbstractUniqueValue = pnlAbstractUniqueValue;
this.fieldName = fieldName;
}
@Override
protected Object doInBackground() throws Exception {
ProgressMonitor pm = getProgressMonitor();
result = getValues(pm);
if(result != null){
AbstractRecodedLegend<U> rl;
if(pnlAbstractUniqueValue.colorConfigPanel.isEnabled() && result.size() > 0){
ColorScheme sc = pnlAbstractUniqueValue.colorConfigPanel.getColorScheme();
rl = (AbstractRecodedLegend<U>) pnlAbstractUniqueValue.createColouredClassification(result,
getProgressMonitor(), sc);
rl.setComparator(pnlAbstractUniqueValue.getComparator());
} else {
rl = pnlAbstractUniqueValue.createConstantClassification(result, pm);
}
if(rl != null){
MappedLegend<String,U> legend = pnlAbstractUniqueValue.getLegend();
rl.setLookupFieldName(legend.getLookupFieldName());
rl.setName(legend.getName());
pnlAbstractUniqueValue.setLegend(rl);
}
} else {
pm.startTask(CREATE_CLASSIF, 100);
pm.endTask();
}
return null;
}
/**
* Gathers all the distinct values of the input DataSource in a {@link HashSet}.
* @param progress Used to be able to cancel the job.
* @return The distinct values as String instances in a {@link HashSet} or null if the job has been cancelled.
*/
public TreeSet<String> getValues(ProgressMonitor progress){
Comparator<String> comparator = pnlAbstractUniqueValue.getComparator();
TreeSet<String> ret = comparator != null ? new TreeSet<>(comparator) : new TreeSet<String>();
try(Connection connection = pnlAbstractUniqueValue.getDataSource().getConnection();
Statement st = connection.createStatement()) {
PropertyChangeListener cancelPm = EventHandler.create(PropertyChangeListener.class, st, "cancel");
progress.addPropertyChangeListener(ProgressMonitor.PROP_CANCEL, cancelPm);
try(ResultSet rs = st.executeQuery("SELECT DISTINCT "+ TableLocation.quoteIdentifier(fieldName)+" FROM "+ pnlAbstractUniqueValue.getTable()
+ " WHERE "+ TableLocation.quoteIdentifier(fieldName) + " IS NOT NULL")) {
final ProgressMonitor pm = progress.startTask(I18N.tr("Retrieving classes"),
ReadTable.getRowCount(connection, pnlAbstractUniqueValue.getTable()));
final int warn = 100;
int size = 0;
while(rs.next()) {
ret.add(rs.getString(1));
size++;
if(size == warn){
final UIPanel cancel = new CancelPanel(warn);
try{
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
if(!UIFactory.showDialog(cancel,true, true)){
pm.setCancelled(true);
}
}
});
} catch (Exception ie){
LOGGER.warn(I18N.tr("The application has ended unexpectedly"));
}
}
pm.endTask();
if(pm.isCancelled()) {
return null;
}
}
} finally {
progress.removePropertyChangeListener(cancelPm);
}
} catch (SQLException e) {
LOGGER.error("IO error while handling the input data source",e);
}
return ret;
}
/**
* Gets the generated DataSource.
* @return The gathered information in a DataSource.
*/
public TreeSet<String> getResult() {
return result;
}
}
/**
* This panel is used to let the user cancel classifications on more values than a given threshold.
*/
private static class CancelPanel implements UIPanel {
private int threshold;
/**
* Builds a new CancelPanel
* @param thres The expected limit, displayed in the inner JLabel.
*/
public CancelPanel(int thres){
super();
threshold = thres;
}
@Override
public URL getIconURL() {
return UIFactory.getDefaultIcon();
}
@Override
public String getTitle() {
return I18N.tr("Continue ?");
}
@Override
public String validateInput() {
return null;
}
@Override
public Component getComponent() {
JPanel pan = new JPanel();
JLabel lab = new JLabel();
lab.setText(I18N.tr("<html><p>The analysis seems to generate more than ") + threshold +
I18N.tr(" different values...</p><p>Are you sure you want to continue ?</p></html>"));
pan.add(lab);
return pan;
}
}
}