/* * 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(); } } }