/*
* uDig - User Friendly Desktop Internet GIS client
* (C) MangoSystem - www.mangosystem.com
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* (http://www.eclipse.org/legal/epl-v10.html), and the Refractions BSD
* License v1.0 (http://udig.refractions.net/files/bsd3-v10.html).
*/
package org.locationtech.udig.processingtoolbox.tools;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FilenameUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureSource;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.FunctionFactory;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;
import org.geotools.process.spatialstatistics.core.FeatureTypes;
import org.geotools.process.spatialstatistics.operations.GeneralOperation;
import org.geotools.process.spatialstatistics.operations.TextColumn;
import org.geotools.process.spatialstatistics.storage.DataStoreFactory;
import org.geotools.process.spatialstatistics.storage.IFeatureInserter;
import org.geotools.process.spatialstatistics.storage.ShapeFileEditor;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.locationtech.udig.catalog.CatalogPlugin;
import org.locationtech.udig.catalog.ICatalog;
import org.locationtech.udig.catalog.ID;
import org.locationtech.udig.catalog.IResolve.Status;
import org.locationtech.udig.catalog.IService;
import org.locationtech.udig.catalog.IServiceFactory;
import org.locationtech.udig.catalog.internal.shp.ShpGeoResourceImpl;
import org.locationtech.udig.processingtoolbox.ToolboxPlugin;
import org.locationtech.udig.processingtoolbox.ToolboxView;
import org.locationtech.udig.processingtoolbox.internal.Messages;
import org.locationtech.udig.processingtoolbox.internal.ui.WidgetBuilder;
import org.locationtech.udig.processingtoolbox.styler.MapUtils;
import org.locationtech.udig.processingtoolbox.styler.MapUtils.FieldType;
import org.locationtech.udig.project.ILayer;
import org.locationtech.udig.project.IMap;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.capability.FunctionName;
import org.opengis.filter.expression.Expression;
import org.opengis.parameter.Parameter;
import com.vividsolutions.jts.geom.Geometry;
/**
* Field Calculator Dialog
*
* @author Minpa Lee, MangoSystem
*
* @source $URL$
*/
@SuppressWarnings("nls")
public class FieldCalculatorDialog extends AbstractGeoProcessingDialog implements
IRunnableWithProgress {
protected static final Logger LOGGER = Logging.getLogger(FieldCalculatorDialog.class);
private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd_hhmmss_S");
private final String space = " ";
private IMap map = null;
private ILayer layer = null;
private SimpleFeatureCollection source = null;
private String geom_field = null;
private Button btnClear, btnTest, chkSelected;
private Table fieldTable, valueTable;
private Combo cboLayer, cboField, cboType;
private Spinner spnLen;
private Text txtExpression;
public FieldCalculatorDialog(Shell parentShell, IMap map) {
super(parentShell, map);
this.map = map;
setShellStyle(SWT.CLOSE | SWT.MIN | SWT.TITLE | SWT.BORDER | SWT.APPLICATION_MODAL
| SWT.RESIZE);
this.windowTitle = Messages.FieldCalculatorDialog_title;
this.windowDesc = Messages.FieldCalculatorDialog_description;
this.windowSize = ToolboxPlugin.rescaleSize(parentShell, 650, 600);
}
/**
* Create contents of the button bar.
*
* @param parent
*/
@Override
protected void createButtonsForButtonBar(Composite parent) {
// Clear, Test, Save..., Load..., OK, Cancel
btnClear = createButton(parent, 2000, Messages.ExpressionBuilderDialog_Clear, false);
btnTest = createButton(parent, 2001, Messages.ExpressionBuilderDialog_Test, false);
btnClear.setEnabled(false);
btnTest.setEnabled(false);
createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
btnClear.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
txtExpression.setText("");
}
});
btnTest.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
try {
Expression expression = ECQL.toExpression(txtExpression.getText());
Object evaluated = expression.evaluate(source.features().next());
String msg = "Evaluated value: " + evaluated;
MessageDialog.openInformation(getParentShell(),
Messages.ExpressionBuilderDialog_Test, msg);
} catch (CQLException e) {
MessageDialog.openInformation(getParentShell(),
Messages.ExpressionBuilderDialog_Test, e.getLocalizedMessage());
}
}
});
}
/**
* Create contents of the dialog.
*
* @param parent
*/
@Override
protected Control createDialogArea(Composite parent) {
Composite area = (Composite) super.createDialogArea(parent);
GridLayout layout = new GridLayout(1, false);
layout.marginWidth = layout.marginHeight = layout.marginRight = layout.marginBottom = 2;
area.setLayout(layout);
Composite container = new Composite(area, SWT.NONE);
container.setLayout(new GridLayout(1, false));
container.setLayoutData(new GridData(GridData.FILL_BOTH));
WidgetBuilder widget = WidgetBuilder.newInstance();
// ========================================================
// 1. layer
// ========================================================
Group grpLayer = widget.createGroup(container, Messages.FieldCalculatorDialog_Layer_Group,
false, 1);
Group grpCombo = new Group(grpLayer, SWT.SHADOW_ETCHED_IN);
grpCombo.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1));
grpCombo.setLayout(new GridLayout(2, false));
widget.createLabel(grpCombo, Messages.FieldCalculatorDialog_Layer, null, 1);
cboLayer = widget.createCombo(grpCombo, 1, true);
for (ILayer layer : map.getMapLayers()) {
if (layer.hasResource(FeatureSource.class)) {
// current, only support shapefile
if (layer.getGeoResource().canResolve(ShpGeoResourceImpl.class)) {
cboLayer.add(layer.getName());
}
}
}
cboLayer.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
if (cboLayer.getText().length() == 0) {
cboField.removeAll();
fieldTable.removeAll();
chkSelected.setSelection(false);
} else {
layer = MapUtils.getLayer(map, cboLayer.getText());
source = MapUtils.getFeatures(layer);
fillFields(cboField, source.getSchema(), FieldType.ALL);
updateFields(source.getSchema());
// check selected features
chkSelected.setSelection(layer.getFilter() != Filter.EXCLUDE);
}
}
});
widget.createLabel(grpCombo, null, null, 1);
chkSelected = widget.createCheckbox(grpCombo, Messages.FieldCalculatorDialog_Selected,
null, 1);
widget.createLabel(grpCombo, Messages.FieldCalculatorDialog_Field, null, 1);
Composite subCon = new Composite(grpCombo, SWT.NONE);
subCon.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1));
subCon.setLayout(widget.createGridLayout(5, false, 0, 0));
cboField = widget.createCombo(subCon, 1, false);
cboField.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
String fieldName = cboField.getText();
if (source.getSchema().indexOf(fieldName) != -1) {
AttributeDescriptor descriptor = source.getSchema().getDescriptor(fieldName);
spnLen.setSelection(FeatureTypes.getAttributeLength(descriptor));
cboType.setText(descriptor.getType().getBinding().getSimpleName());
}
}
});
widget.createLabel(subCon, Messages.FieldCalculatorDialog_FieldType, null, 1);
cboType = widget.createCombo(subCon, 1, false);
for (String fieldType : TextColumn.getFieldTypes(false)) {
cboType.add(fieldType);
}
widget.createLabel(subCon, Messages.FieldCalculatorDialog_FieldLen, null, 1);
spnLen = widget.createSpinner(subCon, 10, 1, 255, 0, 1, 2, 1);
// ========================================================
// 2. Fields & Functions
// ========================================================
final int defaultWidth = 200;
Group grpFields = widget.createGroup(grpLayer, Messages.FieldCalculatorDialog_Fields,
false, 1);
GridData gridDataField = new GridData(SWT.FILL, SWT.FILL, false, true, 1, 1);
gridDataField.widthHint = defaultWidth;
grpFields.setLayoutData(gridDataField);
grpFields.setLayout(new GridLayout(1, true));
fieldTable = widget.createListTable(grpFields,
new String[] { Messages.FieldCalculatorDialog_Fields }, 1, 100);
if (source != null) {
updateFields(source.getSchema());
}
fieldTable.getColumns()[0].setWidth(defaultWidth - 40);
// double click event
fieldTable.addListener(SWT.MouseDoubleClick, new Listener() {
@Override
public void handleEvent(Event event) {
String selection = "[" + fieldTable.getSelection()[0].getText() + "]";
updateExpression(selection);
}
});
// ========================================================
// filter functions
// http://docs.geotools.org/latest/userguide/library/main/filter.html
// ========================================================
Group grpValues = widget.createGroup(grpLayer, Messages.FieldCalculatorDialog_Functions,
false, 1);
grpValues.setLayout(new GridLayout(1, true));
valueTable = widget.createListTable(grpValues,
new String[] { Messages.FieldCalculatorDialog_Functions }, 1, 100);
updateFunctions();
valueTable.getColumns()[0].setWidth(340);
grpValues.setText(Messages.FieldCalculatorDialog_Functions + "("
+ valueTable.getItemCount() + ")");
// double click event
valueTable.addListener(SWT.MouseDoubleClick, new Listener() {
@Override
public void handleEvent(Event event) {
if (valueTable.getSelectionCount() > 0) {
String selection = valueTable.getSelection()[0].getText();
if (selection.contains("[geom]")) {
selection = selection.replace("[geom]", "[" + geom_field + "]");
}
updateExpression(selection);
}
}
});
// ========================================================
// 3. Operators & Expression
// ========================================================
Group grpOpr = new Group(container, SWT.SHADOW_ETCHED_IN);
grpOpr.setText(Messages.FieldCalculatorDialog_Operators);
grpOpr.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1));
grpOpr.setLayout(new GridLayout(16, true));
// operators
GridData btnLayout = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1);
final String[] oprs = new String[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+",
"-", "*", "/", "(", ")" };
final List<String> exceptions = Arrays.asList(new String[] { "+", "-", "*", "/" });
Button[] btnOp = new Button[oprs.length];
for (int idx = 0; idx < oprs.length; idx++) {
btnOp[idx] = widget.createButton(grpOpr, oprs[idx], oprs[idx], btnLayout);
btnOp[idx].addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
Button current = (Button) event.widget;
if (exceptions.contains(current.getText())) {
updateExpression(space + current.getText() + space);
} else {
updateExpression(current.getText());
}
}
});
}
// expression
txtExpression = new Text(grpOpr, SWT.BORDER | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
GridData txtGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 16, 1);
txtGridData.heightHint = 50;
txtExpression.setLayoutData(txtGridData);
txtExpression.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
btnClear.setEnabled(txtExpression.getText().length() > 0);
btnTest.setEnabled(btnClear.getEnabled());
}
});
if (source == null && cboLayer.getItemCount() > 0) {
cboLayer.select(0);
}
container.pack(true);
area.pack(true);
return area;
}
private void updateFields(SimpleFeatureType schema) {
fieldTable.removeAll();
for (AttributeDescriptor descriptor : schema.getAttributeDescriptors()) {
if (descriptor instanceof GeometryDescriptor) {
this.geom_field = descriptor.getLocalName();
TableItem item = new TableItem(fieldTable, SWT.NULL);
item.setText(descriptor.getLocalName());
FontData fontData = item.getFont().getFontData()[0];
fontData.setStyle(SWT.BOLD);
item.setFont(new Font(item.getFont().getDevice(), fontData));
} else {
TableItem item = new TableItem(fieldTable, SWT.NULL);
item.setText(descriptor.getLocalName());
}
}
}
private void updateExpression(String insert) {
String val = txtExpression.getText();
if (val.length() == 0) {
txtExpression.setText(insert);
txtExpression.setSelection(txtExpression.getText().length());
} else {
if (txtExpression.getSelectionCount() == 0) {
final int pos = txtExpression.getCaretPosition();
String sql = val.substring(0, pos) + insert + val.substring(pos);
txtExpression.setText(sql);
txtExpression.setSelection(pos + insert.length() + 1);
} else {
final Point pos = txtExpression.getSelection();
String sql = val.substring(0, pos.x) + insert + val.substring(pos.y);
txtExpression.setText(sql);
txtExpression.setSelection(pos.x + insert.length());
}
}
txtExpression.setFocus();
}
private void updateFunctions() {
Runnable runnable = new Runnable() {
@Override
public void run() {
Set<FunctionFactory> functionFactories = CommonFactoryFinder
.getFunctionFactories(null);
for (FunctionFactory factory : functionFactories) {
String factoryName = factory.toString();
if (factoryName
.contains("org.geotools.process.function.ProcessFunctionFactory")) {
continue;
}
List<FunctionName> functionNames = factory.getFunctionNames();
ArrayList<FunctionName> sorted = new ArrayList<FunctionName>(functionNames);
Collections.sort(sorted, new Comparator<FunctionName>() {
@Override
public int compare(FunctionName o1, FunctionName o2) {
if (o1 == null && o2 == null) {
return 0;
} else if (o1 == null && o2 != null) {
return 1;
} else if (o1 != null && o2 == null) {
return -1;
} else {
return o1.getName().compareTo(o2.getName());
}
}
});
// add table
final String regex = "^[A-Z].*";
for (FunctionName functionName : sorted) {
if (functionName.getName().matches(regex)) {
continue;
}
TableItem item = new TableItem(valueTable, SWT.NULL);
int i = 0;
StringBuffer buffer = new StringBuffer(functionName.getName() + "( ");
for (Parameter<?> argument : functionName.getArguments()) {
if (i++ > 0) {
buffer.append(", ");
}
if (Geometry.class.isAssignableFrom(argument.getType())) {
buffer.append("[geom]");
} else {
buffer.append(argument.getName());
}
}
buffer.append(" )");
item.setText(buffer.toString());
}
}
}
};
try {
BusyIndicator.showWhile(Display.getCurrent(), runnable);
} catch (Exception e) {
ToolboxPlugin.log(e);
}
}
@Override
protected void okPressed() {
if (source == null || cboField.getText().length() == 0 || cboType.getText().length() == 0
|| txtExpression.getText().length() == 0) {
openInformation(getShell(), Messages.FieldCalculatorDialog_Warning);
return;
}
try {
PlatformUI.getWorkbench().getProgressService().run(false, true, this);
openInformation(getShell(), Messages.General_Completed);
} catch (InvocationTargetException e) {
MessageDialog.openError(getShell(), Messages.General_Error, e.getMessage());
} catch (InterruptedException e) {
MessageDialog.openInformation(getShell(), Messages.General_Cancelled, e.getMessage());
}
}
@Override
public void run(IProgressMonitor monitor) throws InvocationTargetException,
InterruptedException {
monitor.beginTask(String.format(Messages.Task_Executing, windowTitle), 100);
String folder = ToolboxView.getWorkspace();
DataStore outputDataStore = DataStoreFactory.getShapefileDataStore(folder);
String outputTypeName = null;
try {
// Convert the given monitor into a progress instance
final SubMonitor progress = SubMonitor.convert(monitor, 100);
// prepare parameters
Expression expression = ECQL.toExpression(txtExpression.getText());
String field = cboField.getText();
Class<?> fieldBinding = String.class;
if (FeatureTypes.existProeprty(source.getSchema(), field)) {
AttributeDescriptor attr = source.getSchema().getDescriptor(field);
fieldBinding = attr.getType().getBinding();
} else {
try {
fieldBinding = new TextColumn().findBestBinding(cboType.getText());
} catch (Exception ee) {
ToolboxPlugin.log(ee.getMessage());
}
}
int length = spnLen.getSelection();
monitor.worked(increment);
// execute process
String outputName = getUniqueName(folder, "calc_");
FieldCalculatorOperation process = new FieldCalculatorOperation(outputName);
process.setOutputDataStore(outputDataStore);
SimpleFeatureCollection features = process.execute(source, expression, field,
fieldBinding, length, progress.newChild(70));
// post process
if (features != null) {
Date now = Calendar.getInstance().getTime();
// remove service
IService service = layer.getGeoResource().service(progress.newChild(5));
final ID id = service.getID();
final Map<java.lang.String, Serializable> params = service.getConnectionParams();
service.dispose(progress.newChild(10));
while (service.getStatus() == Status.CONNECTED) {
Thread.sleep(100);
}
// replace dbf file
String shpPath = DataUtilities.urlToFile(id.toURL()).getPath(); // .shp
File dbfFile = new File(FilenameUtils.removeExtension(shpPath) + ".dbf");
File tempFile = new File(dbfFile.getParent(), "fc_" + df.format(now) + ".dbf");
if (dbfFile.renameTo(tempFile)) {
File newFile = new File(folder, outputName + ".dbf");
org.apache.commons.io.FileUtils.copyFile(newFile, dbfFile);
tempFile.delete();
updateFields(layer.getSchema());
fillFields(cboField, layer.getSchema(), FieldType.ALL);
cboType.setText("");
} else {
throw new Exception(Messages.FieldCalculatorDialog_Failed);
}
// reload service
IServiceFactory serviceFactory = CatalogPlugin.getDefault().getServiceFactory();
ICatalog catalog = CatalogPlugin.getDefault().getLocalCatalog();
IService replacement = serviceFactory.createService(params).get(0);
catalog.replace(id, replacement);
layer.refresh(map.getViewportModel().getBounds());
monitor.worked(increment);
}
monitor.worked(increment);
} catch (Exception e) {
ToolboxPlugin.log(e.getMessage());
throw new InvocationTargetException(e.getCause(), e.getMessage());
} finally {
// finally delete temporary files
new ShapeFileEditor().remove(outputDataStore, outputTypeName);
monitor.done();
}
}
private String getUniqueName(final String directory, final String prefix) {
File[] files = new File(directory).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
name = name.toLowerCase();
return name.startsWith(prefix.toLowerCase()) && name.endsWith(".shp");
}
});
int max = 1;
if (files.length > 0) {
for (File file : files) {
try {
String name = file.getName().toLowerCase().substring(5);
name = name.substring(0, name.length() - 4);
int num = Integer.parseInt(name);
max = Math.max(max, num);
} catch (NumberFormatException e) {
LOGGER.log(Level.FINER, e.getMessage());
}
}
}
return prefix + max;
}
static final class FieldCalculatorOperation extends GeneralOperation {
protected static final Logger LOGGER = Logging.getLogger(FieldCalculatorOperation.class);
private String outputName = "Merge"; //$NON-NLS-1$
public FieldCalculatorOperation(String outputName) {
this.outputName = outputName;
}
public SimpleFeatureCollection execute(SimpleFeatureCollection features,
Expression expression, String field, Class<?> fieldBinding, int length,
IProgressMonitor monitor) throws IOException {
field = FeatureTypes.validateProperty(features.getSchema(), field);
SimpleFeatureType schema = FeatureTypes.build(features.getSchema(), outputName);
if (FeatureTypes.existProeprty(schema, field)) {
AttributeDescriptor attr = schema.getDescriptor(field);
fieldBinding = attr.getType().getBinding();
} else {
schema = FeatureTypes.add(schema, field, fieldBinding, length);
}
SubMonitor progress = SubMonitor.convert(monitor, 100);
SubMonitor loopProgress = progress.newChild(100).setWorkRemaining(features.size());
IFeatureInserter featureWriter = getFeatureWriter(schema);
SimpleFeatureIterator featureIter = null;
try {
featureIter = features.features();
while (featureIter.hasNext()) {
SimpleFeature feature = featureIter.next();
Geometry geometry = (Geometry) feature.getDefaultGeometry();
loopProgress.newChild(1);
if (geometry == null || geometry.isEmpty()) {
continue;
}
SimpleFeature newFeature = featureWriter.buildFeature();
featureWriter.copyAttributes(feature, newFeature, true);
// expression
Object value = expression.evaluate(feature);
newFeature.setAttribute(field, Converters.convert(value, fieldBinding));
featureWriter.write(newFeature);
}
} catch (Exception e) {
featureWriter.rollback(e);
} finally {
featureWriter.close(featureIter);
}
return featureWriter.getFeatureCollection();
}
}
}