/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2011, Open Source Geospatial Foundation (OSGeo)
* (C) 2008-2011 TOPP - www.openplans.org.
*
* 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.feature.gs;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
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.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.collection.DecoratingSimpleFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.GeometryClipper;
import org.geotools.process.ProcessException;
import org.geotools.referencing.CRS;
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.FilterFactory;
import org.opengis.filter.spatial.BBOX;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* A process clipping the geometries in the input feature collection to a specified area
*
* @author Andrea Aime - GeoSolutions
*
* @source $URL$
*/
@DescribeProcess(title = "rectangularClip", description = "Clips the features to the specified geometry")
public class ClipProcess implements GSProcess {
@DescribeResult(name = "result", description = "The feature collection bounds")
public SimpleFeatureCollection execute(
@DescribeParameter(name = "features", description = "The feature collection to be simplified") SimpleFeatureCollection features,
@DescribeParameter(name = "clip", description = "The clipping area (in the same SRS as the feature collection") Geometry clip)
throws ProcessException {
// only get the geometries in the bbox of the clip
Envelope box = clip.getEnvelopeInternal();
FilterFactory ff = CommonFactoryFinder.getFilterFactory(null);
String srs = null;
if(features.getSchema().getCoordinateReferenceSystem() != null) {
srs = CRS.toSRS(features.getSchema().getCoordinateReferenceSystem());
}
BBOX bboxFilter = ff.bbox("", box.getMinX(), box.getMinY(), box.getMaxX(), box.getMaxY(), srs);
// return dynamic collection clipping geometries on the fly
return new ClippingFeatureCollection(features.subCollection(bboxFilter), clip);
}
static class ClippingFeatureCollection extends DecoratingSimpleFeatureCollection {
Geometry clip;
SimpleFeatureType targetSchema;
public ClippingFeatureCollection(SimpleFeatureCollection delegate, Geometry clip) {
super(delegate);
this.clip = clip;
this.targetSchema = buildTargetSchema(delegate.getSchema());
}
/**
* When clipping lines and polygons can turn into multilines and multipolygons
*/
private SimpleFeatureType buildTargetSchema(SimpleFeatureType schema) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
for (AttributeDescriptor ad : schema.getAttributeDescriptors()) {
if(ad instanceof GeometryDescriptor) {
GeometryDescriptor gd = (GeometryDescriptor) ad;
Class<?> binding = ad.getType().getBinding();
if(Point.class.isAssignableFrom(binding) || GeometryCollection.class.isAssignableFrom(binding)) {
tb.add(ad);
} else {
Class target;
if(LineString.class.isAssignableFrom(binding)) {
target = MultiLineString.class;
} else if(Polygon.class.isAssignableFrom(binding)) {
target = MultiPolygon.class;
} else {
throw new RuntimeException("Don't know how to handle geometries of type "
+ binding.getCanonicalName());
}
tb.minOccurs(ad.getMinOccurs());
tb.maxOccurs(ad.getMaxOccurs());
tb.nillable(ad.isNillable());
tb.add(ad.getLocalName(), target, gd.getCoordinateReferenceSystem());
}
} else {
tb.add(ad);
}
}
tb.setName(schema.getName());
return tb.buildFeatureType();
}
@Override
public SimpleFeatureType getSchema() {
return targetSchema;
}
@Override
public SimpleFeatureIterator features() {
return new ClippingFeatureIterator(delegate.features(), clip, getSchema());
}
@Override
public Iterator<SimpleFeature> iterator() {
return new WrappingIterator(features());
}
@Override
public void close(Iterator<SimpleFeature> close) {
if (close instanceof WrappingIterator) {
((WrappingIterator) close).close();
}
}
}
static class ClippingFeatureIterator implements SimpleFeatureIterator {
SimpleFeatureIterator delegate;
GeometryClipper clipper;
boolean preserveTopology;
SimpleFeatureBuilder fb;
SimpleFeature next;
Geometry clip;
public ClippingFeatureIterator(SimpleFeatureIterator delegate, Geometry clip,
SimpleFeatureType schema) {
this.delegate = delegate;
// can we use the fast clipper?
if(clip.getEnvelope().equals(clip)) {
this.clipper = new GeometryClipper(clip.getEnvelopeInternal());
} else {
this.clip = clip;
}
fb = new SimpleFeatureBuilder(schema);
}
public void close() {
delegate.close();
}
public boolean hasNext() {
while (next == null && delegate.hasNext()) {
boolean clippedOut = false;
// try building the clipped feature out of the original feature, if the
// default geometry is clipped out, skip it
SimpleFeature f = delegate.next();
for (AttributeDescriptor ad : f.getFeatureType().getAttributeDescriptors()) {
Object attribute = f.getAttribute(ad.getName());
if (ad instanceof GeometryDescriptor) {
Class target = ad.getType().getBinding();
attribute = clipGeometry((Geometry) attribute, target);
if (attribute == null && f.getFeatureType().getGeometryDescriptor() == ad) {
// the feature has been clipped out
fb.reset();
clippedOut = true;
break;
}
}
fb.add(attribute);
}
if(!clippedOut) {
// build the next feature
next = fb.buildFeature(f.getID());
}
fb.reset();
}
return next != null;
}
public SimpleFeature next() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException("hasNext() returned false!");
}
SimpleFeature result = next;
next = null;
return result;
}
private Object clipGeometry(Geometry geom, Class target) {
// first off, clip
Geometry clipped = null;
if(clipper != null) {
clipped = clipper.clip(geom, true);
} else {
if(geom.getEnvelopeInternal().intersects(clip.getEnvelopeInternal())) {
clipped = clip.intersection(geom);
}
}
// empty intersection?
if(clipped == null || clipped.getNumGeometries() == 0) {
return null;
}
// map the result to the target output type, removing the spurious lower dimensional
// elements that might result out of the intersection
if(Point.class.isAssignableFrom(target) || MultiPoint.class.isAssignableFrom(target)
|| GeometryCollection.class.equals(target)) {
return clipped;
} else if(MultiLineString.class.isAssignableFrom(target)) {
final List<LineString> geoms = new ArrayList<LineString>();
clipped.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if(geom instanceof LineString) {
geoms.add((LineString) geom);
}
}
});
LineString[] lsArray = (LineString[]) geoms.toArray(new LineString[geoms.size()]);
return geom.getFactory().createMultiLineString(lsArray);
} else if(MultiPolygon.class.isAssignableFrom(target)) {
final List<Polygon> geoms = new ArrayList<Polygon>();
clipped.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if(geom instanceof Polygon) {
geoms.add((Polygon) geom);
}
}
});
Polygon[] lsArray = (Polygon[]) geoms.toArray(new Polygon[geoms.size()]);
return geom.getFactory().createMultiPolygon(lsArray);
} else {
throw new RuntimeException("Unrecognized target type " + target.getCanonicalName());
}
}
}
}