/* * 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 java.util.logging.Logger; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.AttributeTypeBuilder; import org.geotools.feature.collection.DecoratingSimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geometry.jts.JTS; 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.referencing.CRS; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.densify.Densifier; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.GeometryFilter; 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 providing the intersection between two feature collections * * @author Gianni Barrotta - Sinergis * @author Andrea Di Nora - Sinergis * @author Pietro Arena - Sinergis * @author Luca Paolino - GeoSolutions */ @DescribeProcess(title = "intersection", description = "Returns the intersections between two feature " + "collections adding the attributes from both of them") public class IntersectionFeatureCollection implements GSProcess { private static final Logger logger = Logger .getLogger("org.geotools.process.feature.gs.IntersectionFeatureCollection"); public static enum IntersectionMode { INTERSECTION, FIRST, SECOND }; static final String ECKERT_IV_WKT = "PROJCS[\"World_Eckert_IV\",GEOGCS[\"GCS_WGS_1984\",DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137.0,298.257223563]],PRIMEM[\"Greenwich\",0.0],UNIT[\"Degree\",0.0174532925199433]],PROJECTION[\"Eckert_IV\"],PARAMETER[\"Central_Meridian\",0.0],UNIT[\"Meter\",1.0]]"; @DescribeResult(description = "feature collection containg the intersections between the two feature " + "collections and the attributes from both of them") public SimpleFeatureCollection execute( @DescribeParameter(name = "first feature collection", description = "First feature collection") SimpleFeatureCollection firstFeatures, @DescribeParameter(name = "second feature collection", description = "Second feature collection") SimpleFeatureCollection secondFeatures, @DescribeParameter(name = "first attributes to retain", collectionType = String.class, min = 0, description = "List of the first feature collection attributes to output") List<String> firstAttributes, @DescribeParameter(name = "second attributes to retain", collectionType = String.class, min = 0, description = "List of the second feature collection attributes to output") List<String> sndAttributes, @DescribeParameter(name = "intersectionMode", min = 0, description = "The operations to perform: set INTERSECTION if the geometry is the intersection, FIRST if the geometry is extracted by firstFeatures, SECOND if it is extracted by secondFeatures (DEFAULT=INTERSECTION)") IntersectionMode intersectionMode, @DescribeParameter(name = "percentagesEnabled", min = 0, description = "Set it true to get the intersection percentage parameters, false otherwise (DEFAULT=false)") Boolean percentagesEnabled, @DescribeParameter(name = "areasEnabled", min = 0, description = "Set it true to get the area attributes , false otherwise (DEFAULT=false)") Boolean areasEnabled) { // assign defaults logger.fine("INTERSECTION FEATURE COLLECTION WPS STARTED"); if (percentagesEnabled == null) { percentagesEnabled = false; } if (areasEnabled == null) { areasEnabled = false; } if (intersectionMode == null) { intersectionMode = IntersectionMode.INTERSECTION; } // basic geometry checks Class firstGeomType = firstFeatures.getSchema().getGeometryDescriptor().getType() .getBinding(); Class secondGeomType = secondFeatures.getSchema().getGeometryDescriptor().getType() .getBinding(); if ((percentagesEnabled || areasEnabled) && (!isGeometryTypeIn(firstGeomType, MultiPolygon.class, Polygon.class) || !isGeometryTypeIn( secondGeomType, MultiPolygon.class, Polygon.class))) { throw new IllegalArgumentException( "In case of opMode or areaMode are true, the features in the first and second collection must be polygonal"); } if (!isGeometryTypeIn(firstGeomType, MultiPolygon.class, Polygon.class, MultiLineString.class, LineString.class)) { throw new IllegalArgumentException( "First feature collection must be polygonal or linear"); } return new IntersectedFeatureCollection(firstFeatures, firstAttributes, secondFeatures, sndAttributes, intersectionMode, percentagesEnabled, areasEnabled); } /** * Checks if the specified class is same, or subclass, of any of the specified target classes * * @param test * @param targets * @return */ static boolean isGeometryTypeIn(Class test, Class... targets) { for (Class target : targets) { if (target.isAssignableFrom(test)) { return true; } } return false; } static Geometry densify(Geometry geom, CoordinateReferenceSystem crs, double maxAreaError) throws FactoryException, TransformException { // basic checks if (maxAreaError <= 0) { throw new IllegalArgumentException("maxAreaError must be greater than 0"); } if (!(geom instanceof Polygon) && !(geom instanceof MultiPolygon)) { throw new IllegalArgumentException("Geom must be poligonal"); } if (crs == null) { throw new IllegalArgumentException("CRS cannot be set to null"); } double previousArea = 0.0; CoordinateReferenceSystem targetCRS = CRS.parseWKT(ECKERT_IV_WKT); MathTransform firstTransform = CRS.findMathTransform(crs, targetCRS); GeometryFactory geomFactory = new GeometryFactory(); int ngeom = geom.getNumGeometries(); Geometry densifiedGeometry = geom; double areaError = 1.0d; int maxIterate = 0; do { double max = 0; maxIterate++; // check the maximum side length of the densifiedGeometry for (int j = 0; j < ngeom; j++) { Geometry geometry = densifiedGeometry.getGeometryN(j); Coordinate[] coordinates = geometry.getCoordinates(); int n = coordinates.length; for (int i = 0; i < (n - 1); i++) { Coordinate[] coords = new Coordinate[2]; coords[0] = coordinates[i]; coords[1] = coordinates[i + 1]; LineString lineString = geomFactory.createLineString(coords); if (lineString.getLength() > max) max = lineString.getLength(); } } // calculate the denified geometry densifiedGeometry = Densifier.densify(densifiedGeometry, max / 2); // reproject densifiedGeometry to Eckert IV Geometry targetGeometry = JTS.transform(densifiedGeometry, firstTransform); double nextArea = targetGeometry.getArea(); // evaluate the current error areaError = Math.abs(previousArea - nextArea) / nextArea; // logger3.info("AREA ERROR"+areaError); previousArea = nextArea; // check whether the current error is greater than the maximum allowed } while (areaError > maxAreaError && maxIterate < 10); return densifiedGeometry; } static double getIntersectionArea(Geometry first, CoordinateReferenceSystem firstCRS, Geometry second, CoordinateReferenceSystem secondCRS, boolean divideFirst) { // basic checks if (firstCRS == null || secondCRS == null) throw new IllegalArgumentException("CRS cannot be set to null"); if (!Polygon.class.isAssignableFrom(first.getClass()) && !MultiPolygon.class.isAssignableFrom(first.getClass())) throw new IllegalArgumentException("first geometry must be poligonal"); if (!Polygon.class.isAssignableFrom(second.getClass()) && !MultiPolygon.class.isAssignableFrom(second.getClass())) throw new IllegalArgumentException("second geometry must be poligonal"); try { Geometry firstTargetGeometry = reprojectAndDensify(first, firstCRS, null); Geometry secondTargetGeometry = reprojectAndDensify(second, firstCRS, null); double numeratorArea = (double) (firstTargetGeometry.intersection(secondTargetGeometry)) .getArea(); if (divideFirst) { double denom = firstTargetGeometry.getArea(); if (denom!=0) return numeratorArea / denom; return 0; } double denom = secondTargetGeometry.getArea(); if (denom!=0) return numeratorArea / denom; return 0; } catch (Exception e) { e.printStackTrace(); return -1; } } static Geometry reprojectAndDensify(Geometry first, CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) throws FactoryException, TransformException { if (targetCRS == null) { targetCRS = CRS.parseWKT(ECKERT_IV_WKT); } MathTransform firstTransform = CRS.findMathTransform(sourceCRS, targetCRS); Geometry geometry = JTS.transform(densify(first, sourceCRS, 0.01d), firstTransform); return geometry; } static AttributeDescriptor getIntersectionType(SimpleFeatureCollection first, SimpleFeatureCollection second) { Class firstGeomType = first.getSchema().getGeometryDescriptor().getType().getBinding(); Class secondGeomType = second.getSchema().getGeometryDescriptor().getType().getBinding(); // figure out the output geometry type Class binding; if (isGeometryTypeIn(secondGeomType, Point.class)) { binding = Point.class; } else if (isGeometryTypeIn(secondGeomType, MultiPoint.class)) { binding = MultiPoint.class; } else if (isGeometryTypeIn(secondGeomType, LineString.class, MultiLineString.class)) { binding = MultiLineString.class; } else if (isGeometryTypeIn(secondGeomType, Polygon.class, MultiPolygon.class)) { if (isGeometryTypeIn(firstGeomType, Polygon.class, MultiPolygon.class)) { binding = MultiPolygon.class; } else { binding = MultiLineString.class; } } else { // we can't be more precise than this binding = Geometry.class; } AttributeTypeBuilder builder = new AttributeTypeBuilder(); builder.setName("the_geom"); builder.setBinding(binding); builder.setCRS(first.features().next().getFeatureType().getCoordinateReferenceSystem()); AttributeDescriptor descriptor = builder.buildDescriptor("the_geom"); return descriptor; } /** * Delegate that will compute the intersections on the go */ static class IntersectedFeatureCollection extends DecoratingSimpleFeatureCollection { @Override public SimpleFeatureType getSchema() { return fb.getFeatureType(); } SimpleFeatureCollection features; List<String> firstAttributes = null; List<String> sndAttributes = null; IntersectionMode intersectionMode; boolean percentagesEnabled; boolean areasEnabled; // added SimpleFeatureBuilder fb; AttributeDescriptor geomType = null; public IntersectedFeatureCollection(SimpleFeatureCollection delegate, List<String> firstAttributes, SimpleFeatureCollection features, List<String> sndAttributes, IntersectionMode intersectionMode, boolean percentagesEnabled, boolean areasEnabled) { super(delegate); this.features = features; this.firstAttributes = firstAttributes; this.sndAttributes = sndAttributes; this.intersectionMode = intersectionMode; this.percentagesEnabled = percentagesEnabled; this.areasEnabled = areasEnabled; SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder(); SimpleFeatureType firstFeatureCollectionSchema = delegate.getSchema(); SimpleFeatureType secondFeatureCollectionSchema = features.getSchema(); if (intersectionMode == IntersectionMode.FIRST) { geomType = firstFeatureCollectionSchema.getGeometryDescriptor(); } if (intersectionMode == IntersectionMode.SECOND) { geomType = secondFeatureCollectionSchema.getGeometryDescriptor(); } if (intersectionMode == IntersectionMode.INTERSECTION) { geomType = getIntersectionType(delegate, features); } tb.add(geomType); // gather the attributes from the first feature collection and skip collectAttributes(firstFeatureCollectionSchema, firstAttributes, tb); // gather the attributes from the second feature collection collectAttributes(secondFeatureCollectionSchema, sndAttributes, tb); // add the dyamic attributes as needed if (percentagesEnabled) { tb.add("percentageA", Double.class); tb.add("percentageB", Double.class); } if (areasEnabled) { tb.add("areaA", Double.class); tb.add("areaB", Double.class); } tb.add("INTERSECTION_ID", Integer.class); tb.setDescription(firstFeatureCollectionSchema.getDescription()); tb.setCRS(firstFeatureCollectionSchema.getCoordinateReferenceSystem()); tb.setAbstract(firstFeatureCollectionSchema.isAbstract()); tb.setSuperType((SimpleFeatureType) firstFeatureCollectionSchema.getSuper()); tb.setName(firstFeatureCollectionSchema.getName()); this.fb = new SimpleFeatureBuilder(tb.buildFeatureType()); } private void collectAttributes(SimpleFeatureType schema, List<String> retainedAttributes, SimpleFeatureTypeBuilder tb) { for (AttributeDescriptor descriptor : schema.getAttributeDescriptors()) { // check whether descriptor has been selected in the attribute list boolean isInRetainList = true; if (retainedAttributes != null) { isInRetainList = retainedAttributes.contains(descriptor.getLocalName()); logger.fine("Checking "+descriptor.getLocalName()+" --> "+isInRetainList); } if (!isInRetainList || schema.getGeometryDescriptor() == descriptor) { continue; } // build the attribute to return AttributeTypeBuilder builder = new AttributeTypeBuilder(); builder.setName(schema.getName().getLocalPart() + "_" + descriptor.getName()); builder.setNillable(descriptor.isNillable()); builder.setBinding(descriptor.getType().getBinding()); builder.setMinOccurs(descriptor.getMinOccurs()); builder.setMaxOccurs(descriptor.getMaxOccurs()); builder.setDefaultValue(descriptor.getDefaultValue()); builder.setCRS(schema.getCoordinateReferenceSystem()); AttributeDescriptor intersectionDescriptor = builder.buildDescriptor(schema .getName().getLocalPart() + "_" + descriptor.getName(), descriptor.getType()); tb.add(intersectionDescriptor); tb.addBinding(descriptor.getType()); } } @Override public SimpleFeatureIterator features() { return new IntersectedFeatureIterator(delegate.features(), delegate, features, delegate.getSchema(), features.getSchema(), firstAttributes, sndAttributes, intersectionMode, percentagesEnabled, areasEnabled,fb); } @Override public Iterator<SimpleFeature> iterator() { return new WrappingIterator(features()); } @Override public void close(Iterator<SimpleFeature> close) { if (close instanceof WrappingIterator) { ((WrappingIterator) close).close(); } } } /** * Builds the intersections while streaming */ static class IntersectedFeatureIterator implements SimpleFeatureIterator { SimpleFeatureIterator delegate; SimpleFeatureCollection firstFeatures; SimpleFeatureCollection secondFeatures; SimpleFeatureCollection subFeatureCollection; SimpleFeatureBuilder fb; SimpleFeature next; SimpleFeature first; Integer iterationIndex = 0; boolean complete = true; boolean added = false; SimpleFeatureCollection intersectedGeometries; SimpleFeatureIterator iterator; String dataGeomName; List<String> retainAttributesFst = null; List<String> retainAttributesSnd = null; AttributeDescriptor geomType = null; boolean percentagesEnabled; boolean areasEnabled; IntersectionMode intersectionMode; int id=0; public IntersectedFeatureIterator(SimpleFeatureIterator delegate, SimpleFeatureCollection firstFeatures, SimpleFeatureCollection secondFeatures, SimpleFeatureType firstFeatureCollectionSchema, SimpleFeatureType secondFeatureCollectionSchema, List<String> retainAttributesFstPar, List<String> retainAttributesSndPar, IntersectionMode intersectionMode, boolean percentagesEnabled, boolean areasEnabled, SimpleFeatureBuilder sfb) { this.retainAttributesFst = retainAttributesFstPar; this.retainAttributesSnd = retainAttributesSndPar; this.delegate = delegate; this.firstFeatures = firstFeatures; this.secondFeatures = secondFeatures; this.percentagesEnabled = percentagesEnabled; this.areasEnabled = areasEnabled; this.intersectionMode = intersectionMode; logger.fine("Creating schema"); // create the geometry attribute descriptor for the result // SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder(); if (intersectionMode == IntersectionMode.FIRST) { geomType = firstFeatureCollectionSchema.getGeometryDescriptor(); } if (intersectionMode == IntersectionMode.SECOND) { geomType = secondFeatureCollectionSchema.getGeometryDescriptor(); } if (intersectionMode == IntersectionMode.INTERSECTION) { geomType = getIntersectionType(firstFeatures, secondFeatures); } this.fb = sfb; subFeatureCollection = this.secondFeatures; this.dataGeomName = this.firstFeatures.getSchema().getGeometryDescriptor() .getLocalName(); logger.fine("Schema created"); } public void close() { delegate.close(); } public boolean hasNext() { // logger.info("qui"); logger.finer("HAS NEXT"); while ((next == null && delegate.hasNext()) || (next == null && added)) { // logger.info("qui nel while"); if (complete) { first = delegate.next(); intersectedGeometries = null; } // logger.info("qui dopo check if (complete)"); //logger.finer("control HAS NEXT"); for (Object attribute : first.getAttributes()) { if (attribute instanceof Geometry && attribute.equals(first.getDefaultGeometry())) { Geometry currentGeom = (Geometry) attribute; if (intersectedGeometries == null && !added) { intersectedGeometries = filteredCollection(currentGeom, subFeatureCollection); iterator = intersectedGeometries.features(); } try { while (iterator.hasNext()) { added = false; SimpleFeature second = iterator.next(); if (currentGeom.getEnvelope().intersects( ((Geometry) second.getDefaultGeometry()))) { // compute geometry if (intersectionMode == IntersectionMode.INTERSECTION) { attribute = currentGeom.intersection((Geometry) second .getDefaultGeometry()); GeometryFilterImpl filter = new GeometryFilterImpl(geomType .getType().getBinding()); ((Geometry) attribute).apply(filter); attribute = filter.getGeometry(); } else if (intersectionMode == IntersectionMode.FIRST) { attribute = currentGeom; } else if (intersectionMode == IntersectionMode.SECOND) { attribute = (Geometry) second.getDefaultGeometry(); } if (((Geometry) attribute).getNumGeometries() > 0) { fb.add(attribute); fb.set("INTERSECTION_ID", id++); // add the non geometric attributes addAttributeValues(first, retainAttributesFst, fb); addAttributeValues(second, retainAttributesSnd, fb); // add the dynamic attributes if (percentagesEnabled) { addPercentages(currentGeom, second); } if (areasEnabled) { addAreas(currentGeom, second); } // build the feature next = fb.buildFeature(iterationIndex.toString()); // update iterator status if (iterator.hasNext()) { complete = false; added = true; iterationIndex++; return next != null; } iterationIndex++; } } complete = false; } complete = true; } finally { if (!added) { iterator.close(); } } } } } return next != null; } private void addAttributeValues(SimpleFeature feature, List<String> retained, SimpleFeatureBuilder fb) { Iterator<AttributeDescriptor> firstIterator = feature.getType().getAttributeDescriptors() .iterator(); while (firstIterator.hasNext()) { AttributeDescriptor ad = firstIterator.next(); Object firstAttribute = feature.getAttribute(ad.getLocalName()); if ((retained == null || retained.contains(ad.getLocalName())) && !(firstAttribute instanceof Geometry)) { fb.add(feature.getAttribute(ad.getLocalName())); } } } private void addAreas(Geometry currentGeom, SimpleFeature second) { CoordinateReferenceSystem firstCRS = firstFeatures.getSchema().getCoordinateReferenceSystem(); CoordinateReferenceSystem secondCRS = secondFeatures.getSchema().getCoordinateReferenceSystem(); try { double areaA = IntersectionFeatureCollection.reprojectAndDensify(currentGeom, firstCRS, null).getArea(); double areaB = IntersectionFeatureCollection.reprojectAndDensify( (Geometry) second.getDefaultGeometry(), secondCRS, null).getArea(); fb.set("areaA", areaA); fb.set("areaB", areaB); } catch (Exception e) { System.out.println("" + e); fb.set("areaA", -1); fb.set("areaB", -1); } } private void addPercentages(Geometry currentGeom, SimpleFeature second) { CoordinateReferenceSystem firstCRS = firstFeatures.getSchema().getCoordinateReferenceSystem(); CoordinateReferenceSystem secondCRS = secondFeatures.getSchema().getCoordinateReferenceSystem(); double percentageA = IntersectionFeatureCollection.getIntersectionArea(currentGeom, firstCRS, (Geometry) second.getDefaultGeometry(), secondCRS, true); double percentageB = IntersectionFeatureCollection.getIntersectionArea(currentGeom, firstCRS, (Geometry) second.getDefaultGeometry(), secondCRS, false); fb.set("percentageA", percentageA); fb.set("percentageB", percentageB); } public SimpleFeature next() throws NoSuchElementException { if (!hasNext()) { throw new NoSuchElementException("hasNext() returned false!"); } SimpleFeature result = next; next = null; return result; } private SimpleFeatureCollection filteredCollection(Geometry currentGeom, SimpleFeatureCollection subFeatureCollection) { FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null); Filter intersectFilter = ff.intersects(ff.property(dataGeomName), ff.literal(currentGeom)); SimpleFeatureCollection subFeatureCollectionIntersection = this.subFeatureCollection .subCollection(intersectFilter); if (subFeatureCollectionIntersection.size() == 0) { subFeatureCollectionIntersection = subFeatureCollection; } return subFeatureCollectionIntersection; } } static class GeometryFilterImpl implements GeometryFilter { GeometryFactory factory = new GeometryFactory(); ArrayList<Geometry> collection = new ArrayList<Geometry>(); Class binding = null; GeometryFilterImpl(Class binding) { this.binding = binding; } @Override public void filter(Geometry gmtr) { if (MultiPolygon.class.isAssignableFrom(binding)) { if (gmtr.getArea() != 0.0d && gmtr.getGeometryType().equals("Polygon")) { collection.add(gmtr); } } if (MultiLineString.class.isAssignableFrom(binding)) { if (gmtr.getLength() != 0.0d && gmtr.getGeometryType().equals("LineString")) { collection.add(gmtr); } } if (MultiPoint.class.isAssignableFrom(binding)) { if (gmtr.getNumGeometries() > 0 && gmtr.getGeometryType().equals("Point")) { collection.add(gmtr); } } if (Point.class.isAssignableFrom(binding)) { if (gmtr.getGeometryType().equals("Point")) { collection.add(gmtr); } } } public Geometry getGeometry() { int n = collection.size(); if (MultiPolygon.class.isAssignableFrom(binding)) { Polygon[] array = new Polygon[n]; for (int i = 0; i < n; i++) array[i] = (Polygon) collection.get(i); return factory.createMultiPolygon(array); } if (MultiLineString.class.isAssignableFrom(binding)) { LineString[] array = new LineString[n]; for (int i = 0; i < n; i++) array[i] = (LineString) collection.get(i); return factory.createMultiLineString(array); } if (MultiPoint.class.isAssignableFrom(binding)) { Point[] array = new Point[n]; for (int i = 0; i < n; i++) array[i] = (Point) collection.get(i); return factory.createMultiPoint(array); } return null; } } }