/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2011, Open Source Geospatial Foundation (OSGeo) * (C) 2007-2011 Refractions Research * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. */ package org.geotools.process.vector; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.feature.collection.DecoratingSimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.text.cql2.CQL; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; import org.geotools.process.ProcessException; import org.geotools.process.factory.DescribeParameter; import org.geotools.process.factory.DescribeProcess; import org.geotools.process.factory.DescribeResult; import org.geotools.process.gs.GSProcess; import org.geotools.process.gs.WrappingIterator; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.AttributeType; import org.opengis.feature.type.GeometryType; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.PropertyName; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.simplify.DouglasPeuckerSimplifier; import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier; /** * Used to transform a feature collection as defined using a series of expressions. * <p> * The definition of the output feature type can be provided as a {@link Definition} data structure * or using a simple string format: * * <pre> * the_geom=the_geom * name=name * area=area( the_geom ) * </pre> * * Attribute definitions can be delimited by line breaks and/or by semicolons. * <p> * This is a very flexible process which can be used to: * <ul> * <li>Change the order of attributes - resulting in a new feature type:<pre> * INPUT Schema DEFINITION OUTPUT Schema * the_geom: Polygon the_geom the_geom: Polygon * name: String id id: Long * id: Long name name: String * description: String description description: String * </pre></li> * <li>Rename or remove attributes - resulting in a new feature type:<pre> * INPUT Schema DEFINITION OUTPUT Schema * the_geom: Polygon the_geom the_geom: Polygon * name: String id_code=id id_code: Long * id: Long summary=description summary: String * description: String * </pre></li> * <li>Process geometry - using functions like "the_geom=simplify( the_geom, 2.0 )" or "the_geom=centriod( the_geom )":<pre> * INPUT Schema DEFINITION OUTPUT Schema * the_geom: Polygon the_geom=centriod(the_geom) the_geom: Point * name: String name name: String * id: Long id id: Long * description: String description description: String * </pre></li> * <li>Generate additional attributes using the form: area=area( the_geom ):<pre> * INPUT Schema DEFINITION OUTPUT Schema * the_geom: Polygon the_geom=centriod(the_geom) the_geom: Point * name: String name name: String * id: Long id id: Long * description: String description description: String * area=area( the_geom) area: Double * text=concatenate(name,'-',id) text: String * </pre></li> * </ul> * <p> * This is a port of the uDig "reshape" operation to the GeoTools process framework. * * @author Jody Garnett (LISAsoft) * * @source $URL$ */ @DescribeProcess(title = "Transform", description = "Computes a new feature collection from the input one by renaming, deleting, and computing new attributes. Attribute values are specified as ECQL expressions in the form name=expression.") public class TransformProcess implements VectorProcess { /** * Definition of an attribute used during transform * <p> * Note this definition is terse as we are gathering the details from the origional FeatureType. * * @author jody */ public static class Definition { /** Name of the AttribtueDescriptor to generate */ public String name; /** Expression used to generate the target value; most simply a PropertyName */ public Expression expression; /** Class binding (if known) */ public Class<?> binding; } private static final String DEF_DELIMITER = ";"; @DescribeResult(name = "result", description = "Transformed feature collection") public SimpleFeatureCollection execute( @DescribeParameter(name = "features", description = "Input feature collection") SimpleFeatureCollection features, @DescribeParameter(name = "transform", description = "The transform specification, as a list of specifiers of the form name=expression, delimited by newlines or semicolons.") String transform) throws ProcessException { if (transform == null) { return features; // no change } List<Definition> list = toDefinition(transform); return executeList(features, list); } @DescribeResult(name = "result", description = "Transformed feature collection") public SimpleFeatureCollection executeList( @DescribeParameter(name = "features", description = "Input feature collection") SimpleFeatureCollection features, @DescribeParameter(name = "transform", description = "List of Definitions for the output feature attributes") List<Definition> transform) throws ProcessException { if (transform == null) { return features; // no change } return new ReshapeFeatureCollection(features, transform); } // // helper methods made static for ease of JUnit testing // /** * Parse out a list of {@link Definition} from the provided text description. * <p> * The format expected here is one definition per line; using the format "name=...expression..". * * @param definition * @return List of definition */ public static List<Definition> toDefinition(String definition) { List<Definition> list = new ArrayList<Definition>(); HashSet<String> check = new HashSet<String>(); // clean up cross platform differences of line feed String[] defs = splitDefinitions(definition); for (String line : defs) { int mark = line.indexOf("="); if (mark != -1) { String name = line.substring(0, mark).trim(); String expressionDefinition = line.substring(mark + 1).trim(); if (check.contains(name)) { throw new IllegalArgumentException("Attribute " + name + " defined more than once"); } Expression expression; try { expression = ECQL.toExpression(expressionDefinition); } catch (CQLException e) { throw new IllegalArgumentException("Unable to parse expression " + name + "=" + expressionDefinition + " " + e, e); } Definition def = new Definition(); def.name = name; def.expression = expression; list.add(def); check.add(name); // to catch duplicates! } } return list; } /** * Splits single-string definition list into a list of definitions. * Either line breaks or ';' can be used as definition delimiters. * * @param defList the definition list string * @return the separate definitions */ private static String[] splitDefinitions(String defList) { // clean up cross platform differences of linefeed String defListLF = defList.replaceAll("\r", "\n").replaceAll("[\n\r][\n\r]", "\n"); // convert explicit delimiter to linefeed defListLF = defList.replaceAll(";", "\n"); // split on linefeed return defListLF.split("\n"); } public static SimpleFeatureType toReShapeFeatureType(SimpleFeatureCollection delegate, List<Definition> definitionList) { SimpleFeature sample = null; SimpleFeatureIterator iterator = delegate.features(); try { if( iterator.hasNext() ){ sample = iterator.next(); } } finally { iterator.close(); // good bye } SimpleFeatureTypeBuilder build = new SimpleFeatureTypeBuilder(); SimpleFeatureType origional = delegate.getSchema(); for( Definition def : definitionList ){ String name = def.name; Expression expression = def.expression; Object value = null; if( sample != null ){ value = expression.evaluate(sample); } Class<?> binding = def.binding; // make use of any default binding hint provided by user if( value == null){ if( expression instanceof PropertyName){ PropertyName propertyName = (PropertyName)expression; String path = propertyName.getPropertyName(); AttributeDescriptor descriptor = origional.getDescriptor( name ); AttributeType attributeType = descriptor.getType(); binding = attributeType.getBinding(); } } else { binding = value.getClass(); } if( binding ==null ){ // note we could consider scanning through additional samples until we get a non null hit throw new IllegalArgumentException("Unable to determine type for "+name); } if( Geometry.class.isAssignableFrom( binding )){ CoordinateReferenceSystem crs; AttributeType originalAttributeType = origional.getType(name); if( originalAttributeType != null && originalAttributeType instanceof GeometryType ) { GeometryType geometryType = (GeometryType)originalAttributeType; crs = geometryType.getCoordinateReferenceSystem(); } else { crs = origional.getCoordinateReferenceSystem(); } build.crs(crs); build.add(name, binding); } else { build.add(name, binding); } } build.setName( origional.getTypeName() ); return build.buildFeatureType(); } // // helper classes responsible for most of the work // /** * ReshapeFeatureCollection obtaining feature type by processing the list of definitions * against the origional delegate feature collection. * @author jody */ static class ReshapeFeatureCollection extends DecoratingSimpleFeatureCollection { List<Definition> definition; SimpleFeatureType schema; public ReshapeFeatureCollection(SimpleFeatureCollection delegate, List<Definition> definition) { super(delegate); this.definition = definition; this.schema = toReShapeFeatureType( delegate, definition ); } @Override public SimpleFeatureType getSchema() { return schema; } @Override public SimpleFeatureIterator features() { return new ReshapeFeatureIterator(delegate.features(), definition, schema); } } /** * Process one feature at time; obtaining values by evaulating the provided list of definitions. * * @author jody */ static class ReshapeFeatureIterator implements SimpleFeatureIterator { SimpleFeatureIterator delegate; List<Definition> definition; SimpleFeatureBuilder fb; public ReshapeFeatureIterator(SimpleFeatureIterator delegate, List<Definition> definition, SimpleFeatureType schema) { this.delegate = delegate; this.definition = definition; fb = new SimpleFeatureBuilder(schema); } public void close() { delegate.close(); } public boolean hasNext() { return delegate.hasNext(); } public SimpleFeature next() throws NoSuchElementException { SimpleFeature feature = delegate.next(); for( Definition def : definition ){ Object value = def.expression.evaluate(feature); fb.add( value ); } SimpleFeature created = fb.buildFeature(feature.getID()); return created; } } }