/* * 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.vector; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.logging.Logger; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.GeoTools; 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.process.factory.DescribeParameter; import org.geotools.process.factory.DescribeProcess; import org.geotools.process.factory.DescribeResult; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.util.logging.Logging; 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 org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeographicCRS; import com.vividsolutions.jts.algorithm.LineIntersector; import com.vividsolutions.jts.algorithm.RobustLineIntersector; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateSequence; import com.vividsolutions.jts.geom.CoordinateSequenceFilter; 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.LineSegment; 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; import com.vividsolutions.jts.geom.impl.CoordinateArraySequence; /** * Modified version that can preserve Z values after the clip * * @author Andrea Aime - GeoSolutions * * @source $URL$ */ @DescribeProcess(title = "Clip", description = "Clips (crops) features to a given geometry") public class ClipProcess implements VectorProcess { static final FilterFactory ff = CommonFactoryFinder.getFilterFactory(GeoTools.getDefaultHints()); static final Logger LOGGER = Logging.getLogger(ClipProcess.class); @DescribeResult(name = "result", description = "Clipped feature collection") public SimpleFeatureCollection execute( @DescribeParameter(name = "features", description = "Input feature collection") SimpleFeatureCollection features, @DescribeParameter(name = "clip", description = "Geometry to use for clipping (in same CRS as input features)") Geometry clip, @DescribeParameter(name = "preserveZ", min=0, description = "Attempt to preserve Z values from the original geometry (interpolate value for new points)") Boolean preserveZ) throws ProcessException { // only get the geometries in the bbox of the clip Envelope box = clip.getEnvelopeInternal(); 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); // default value for preserve Z if(preserveZ == null) { preserveZ = false; } // return dynamic collection clipping geometries on the fly return new ClippingFeatureCollection(features.subCollection(bboxFilter), clip, preserveZ); } static class ClippingFeatureCollection extends DecoratingSimpleFeatureCollection { Geometry clip; SimpleFeatureType targetSchema; private boolean preserveZ; public ClippingFeatureCollection(SimpleFeatureCollection delegate, Geometry clip, boolean preserveZ) { super(delegate); this.clip = clip; this.targetSchema = buildTargetSchema(delegate.getSchema()); this.preserveZ = preserveZ; } @Override public SimpleFeatureType getSchema() { return targetSchema; } /** * 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 SimpleFeatureIterator features() { return new ClippingFeatureIterator(delegate.features(), clip, getSchema(), preserveZ); } @Override public int size() { SimpleFeatureIterator fi = null; try { int count = 0; fi = features(); while(fi.hasNext()) { fi.next(); count++; } return count; } finally { fi.close(); } } } static class ClippingFeatureIterator implements SimpleFeatureIterator { SimpleFeatureIterator delegate; GeometryClipper clipper; boolean preserveTopology; SimpleFeatureBuilder fb; SimpleFeature next; Geometry clip; boolean preserveZ; public ClippingFeatureIterator(SimpleFeatureIterator delegate, Geometry clip, SimpleFeatureType schema, boolean preserveZ) { 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); this.preserveZ = preserveZ; } 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, ((GeometryDescriptor) ad).getCoordinateReferenceSystem()); 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, CoordinateReferenceSystem crs) { // 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.isEmpty() || 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 Geometry result; if(Point.class.isAssignableFrom(target) || MultiPoint.class.isAssignableFrom(target) || GeometryCollection.class.equals(target)) { result = clipped; } else if(MultiLineString.class.isAssignableFrom(target) || LineString.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); } } }); if(geoms.size() == 0) { result = null; } else { LineString[] lsArray = geoms.toArray(new LineString[geoms.size()]); result = geom.getFactory().createMultiLineString(lsArray); } } else if(MultiPolygon.class.isAssignableFrom(target) || Polygon.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); } } }); if(geoms.size() == 0) { result = null; } else { Polygon[] lsArray = geoms.toArray(new Polygon[geoms.size()]); result = geom.getFactory().createMultiPolygon(lsArray); } } else { throw new RuntimeException("Unrecognized target type " + target.getCanonicalName()); } // manage Z preservation if(preserveZ && !geom.equalsExact(clipped)) { // for polygons we need to go idw, for points and multipoints idw will do and will not // add much overhead (it has optimizations for points that were already in the input) if(result instanceof MultiPolygon || result instanceof MultiPoint || result instanceof Point) { result.apply(new IDWElevationInterpolator(geom, crs)); } else if(result instanceof MultiLineString) { result.apply(new LinearElevationInterpolator(geom, crs)); } } return result; } protected boolean hasElevations(CoordinateSequence seq) { return (seq instanceof CoordinateArraySequence && !Double.isNaN(((CoordinateArraySequence) seq).getCoordinate(0).z)) || (!(seq instanceof CoordinateArraySequence) && seq.getDimension() > 2); } /** * An interpolator that will copy over the Z from the original points where possible, * and will use the IDW Interpolation algorithm for the missing ones * * @author Andrea Aime - GeoSolutions */ private class IDWElevationInterpolator implements CoordinateSequenceFilter { private static final int MAX_POINTS = 12; private final int scale; private final List<PointDistance> elevations; private final CoordinateReferenceSystem crs; public IDWElevationInterpolator(Geometry geom, CoordinateReferenceSystem crs) { this.elevations = gatherElevationPointCloud(geom); this.scale = crs instanceof GeographicCRS ? 9 : 6; this.crs = crs; } List<PointDistance> gatherElevationPointCloud(Geometry geom) { final List<PointDistance> results = new ArrayList<PointDistance>(); geom.apply(new CoordinateSequenceFilter() { @Override public boolean isGeometryChanged() { return false; } @Override public boolean isDone() { return false; } @Override public void filter(CoordinateSequence seq, int i) { // we do all the collecting when called for the first ordinate if(i > 0) { return; } // collects only points with a Z if(hasElevations(seq)) { Coordinate[] coords = seq.toCoordinateArray(); for (int j = 0; j < coords.length; j++) { Coordinate c = coords[j]; // avoid adding the last element of a ring to avoid un-balancing the // weights (the fist/last coordinate would be counted twice) if((j < coords.length - 1 || !c.equals(coords[0])) && !Double.isNaN(c.z)) { results.add(new PointDistance(c)); } } } } }); if(results.size() == 0) { return null; } else { return results; } } @Override public boolean isGeometryChanged() { return true; } @Override public boolean isDone() { return elevations == null; } @Override public void filter(CoordinateSequence seq, int i) { if(elevations == null) { return; } if (seq.getDimension() < 3) { throw new IllegalArgumentException( "Expecting a 3 dimensional coordinate sequence to re-apply the Z values"); } double x = seq.getX(i); double y = seq.getY(i); for (PointDistance pd : elevations) { double distance = pd.updateDistance(x, y, crs); // did we match an existing point? if (distance < PointDistance.EPS_METERS) { // copy over the z and bail out seq.setOrdinate(i, 2, pd.c.z); return; } } // ok, we need to apply the IDW interpolation for this point Collections.sort(elevations); double sum = 0; double weights = 0; final int usedPoints = Math.min(MAX_POINTS, elevations.size()); for (int j = 0; j < usedPoints; j++) { PointDistance pd = elevations.get(j); sum += pd.c.z / pd.squareDistance; weights += 1 / pd.squareDistance; } double z = sum / weights; // apply safe rounding to avoid numerical issues with the above calculation due // to the weights being, often, very small numbers BigDecimal bd = new BigDecimal(z); double rz = bd.setScale(scale, BigDecimal.ROUND_HALF_EVEN).doubleValue(); seq.setOrdinate(i, 2, rz); } } /** * Helper that keeps in the same container the point and its distance to a certain reference * point. Allows for sorting on said distance */ static final class PointDistance implements Comparable<PointDistance> { static final double EPS_METERS = 1e-6; static final double EPS_DEGREES = 1e-9; Coordinate c; double squareDistance; public PointDistance(Coordinate c) { this.c = c; } public boolean isSame(double x, double y, CoordinateReferenceSystem crs) { double tolerance = crs instanceof GeographicCRS ? EPS_DEGREES : EPS_METERS; return Math.abs(c.x - x) < tolerance && Math.abs(c.x - y) < tolerance; } public double updateDistance(double x, double y, CoordinateReferenceSystem crs) { if(crs instanceof DefaultGeographicCRS) { double d = ((DefaultGeographicCRS) crs).distance(new double[] {c.x, c.y}, new double[] {x, y}).doubleValue(); this.squareDistance = d * d; } else { double dx = c.x - x; double dy = c.y - y; this.squareDistance = dx * dx + dy * dy; } return this.squareDistance; } @Override public int compareTo(PointDistance o) { return (int) Math.signum(this.squareDistance - o.squareDistance); } @Override public String toString() { return "[" + c.x + " " + c.y + " " + c.z + "] - " + squareDistance; } } private class LinearElevationInterpolator implements GeometryComponentFilter { private final ArrayList<LineString> originalLines; public LinearElevationInterpolator(Geometry original, CoordinateReferenceSystem crs) { originalLines = new ArrayList<LineString>(); original.apply(new GeometryComponentFilter() { @Override public void filter(Geometry geom) { if(geom instanceof LineString) { originalLines.add((LineString) geom); } } }); } @Override public void filter(Geometry geom) { if(geom instanceof LineString) { LineString ls = ((LineString) geom); // look for the original line containing this one LineString original = getOriginator(ls); if(original == null) { LOGGER.log(java.util.logging.Level.WARNING, "Could not find the original line from which the output line " + geom + " originated"); return; } try { applyElevations(ls, original, false); } catch(ClippingException e) { // fine, let's try with the more expensive way then applyElevations(ls, original, true); } } } private void applyElevations(LineString ls, LineString original, boolean tolerant) { // do we have any elevation to bring on the new line? if(!hasElevations(original.getCoordinateSequence())) { return; } // let's do a synched scan of the two lines CoordinateSequence cs = ls.getCoordinateSequence(); CoordinateSequence csOrig = original.getCoordinateSequence(); Coordinate c1 = cs.getCoordinate(0); Coordinate c2 = cs.getCoordinate(1); int localIdx = 0; Coordinate o1 = csOrig.getCoordinate(0); Coordinate o2 = csOrig.getCoordinate(1); int origIdx = 0; RobustLineIntersector intersector = new RobustLineIntersector(); int matched = 0; boolean flipped = false; // search the fist segment of the origin that contains the first segment of the local for(;;) { intersector.computeIntersection(c1, c2, o1, o2); int intersectionNum = intersector.getIntersectionNum(); // doing these checks for any non collinear point is expensive, do it only if there is at least one intersection // or if we're in tolerant mode if(intersectionNum == LineIntersector.POINT_INTERSECTION || (tolerant && intersectionNum != LineIntersector.COLLINEAR)) { // this one might be due to a numerical issue, where the two lines to do intersect // exactly, but almost. Let's compute the distance and see LineSegment segment = new LineSegment(o1, o2); double d1 = segment.distance(c1); double d2 = segment.distance(c2); if(d1 <= PointDistance.EPS_METERS && d2 <= PointDistance.EPS_METERS) { intersectionNum = LineIntersector.COLLINEAR; } } if(intersectionNum == LineIntersector.COLLINEAR) { matched++; applyZValues(cs, localIdx, csOrig, origIdx); localIdx++; if(localIdx == cs.size() - 1) { // we reached the end, apply the Z also on the second ordinate // of the segment and exit applyZValues(cs, localIdx, csOrig, origIdx); break; } else { c1 = c2; c2 = cs.getCoordinate(localIdx + 1); } } else { origIdx++; if(origIdx >= csOrig.size() - 1) { if(!flipped) { // it may be that the two lines have different orientations, we'll flip ls and // start back ls = (LineString) ls.reverse(); cs = ls.getCoordinateSequence(); flipped = true; c1 = cs.getCoordinate(0); c2 = cs.getCoordinate(1); localIdx = 0; o1 = csOrig.getCoordinate(0); o2 = csOrig.getCoordinate(1); origIdx = 0; } else { throw new ClippingException("Could not find collinear segments between " + ls.toText() + "\n and \n" + original.toText() + "\n after matching " + matched + " points"); } } else { o1 = o2; o2 = csOrig.getCoordinate(origIdx + 1); } } } } private void applyZValues(CoordinateSequence cs, int idx, CoordinateSequence csOrig, int origIdx) { double lx1 = cs.getOrdinate(idx, 0); double ly1 = cs.getOrdinate(idx, 1); double lz1; double ox1 = csOrig.getOrdinate(origIdx, 0); double oy1 = csOrig.getOrdinate(origIdx, 1); double oz1 = csOrig.getOrdinate(origIdx, 2); double ox2 = csOrig.getOrdinate(origIdx + 1, 0); double oy2 = csOrig.getOrdinate(origIdx + 1, 1); double oz2 = csOrig.getOrdinate(origIdx + 1, 2); if(lx1 == ox1 && ly1 == oy1) { lz1 = oz1; } else { double d1 = distance(ox1, oy1, lx1, ly1); double d = distance(ox1, oy1, ox2, oy2); lz1 = oz1 + (oz2 - oz1) * (d1 / d); } cs.setOrdinate(idx, 2, lz1); } private double distance(double x1, double y1, double x2, double y2) { double dx = x1 - x2; double dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } /** * TODO: should we use a spatial index? Would be warranted only if * the input has a very large amount of sub-lines * @param ls * @return */ private LineString getOriginator(LineString ls) { LineString original = null; for (LineString ol : originalLines) { if(ls.equals(ol) || ls.overlaps(ol) || ol.contains(ls)) { original = ol; break; } } if(original == null) { // sigh, there might be a small difference in the coordinate values, // go for a more expensive, but tolerant search for (LineString ol : originalLines) { if(ol.buffer(PointDistance.EPS_METERS).contains(ls)) { original = ol; break; } } } return original; } } } static class ClippingException extends RuntimeException { /** * */ private static final long serialVersionUID = -1373822214375482149L; public ClippingException() { super(); } public ClippingException(String message, Throwable cause) { super(message, cause); } public ClippingException(String message) { super(message); } public ClippingException(Throwable cause) { super(cause); } } }