/* Spatial Operations & Editing Tools for uDig * * Axios Engineering under a funding contract with: * Diputación Foral de Gipuzkoa, Ordenación Territorial * * http://b5m.gipuzkoa.net * http://www.axios.es * * (C) 2006, Diputación Foral de Gipuzkoa, Ordenación Territorial (DFG-OT). * DFG-OT agrees to license under Lesser General Public License (LGPL). * * 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 es.axios.lib.geometry.util; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.geotools.feature.FeatureCollection; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import com.vividsolutions.jts.algorithm.CentralEndpointIntersector; import com.vividsolutions.jts.algorithm.HCoordinate; import com.vividsolutions.jts.algorithm.NotRepresentableException; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; 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.operation.linemerge.LineMerger; import com.vividsolutions.jts.operation.polygonize.Polygonizer; import es.axios.lib.geometry.merge.MergeStrategy; import es.axios.udig.ui.commons.internal.i18n.Messages; /** * Geometry utility methods * <p> * Collection of method which gets feature or feature collection to applay * geometry operations * </p> * * @author Mauricio Pazos (www.axios.es) * @author Aritz Davila (www.axios.es) * @since 1.1.0 */ public class GeometryUtil { /** * unused */ private GeometryUtil() { // util class } /** * Returns a geometry which is the union of all the non null default * geometries from the features in <code>featureCollection</code> * * @param featureCollection * @param expectedGeometryClass * * @return Geometry Union */ public static Geometry geometryUnion(final FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection) { Geometry resultGeom = null; Iterator<SimpleFeature> iterator = null; try { SimpleFeature currFeature; iterator = featureCollection.iterator(); while (iterator.hasNext()) { currFeature = iterator.next(); Geometry featureGeom = (Geometry) currFeature.getDefaultGeometry(); assert featureGeom != null : "the feature must have almost one geometry"; //$NON-NLS-1$ featureGeom.normalize(); if (resultGeom == null) { resultGeom = featureGeom; } else { resultGeom = MergeStrategy.mergeOp(resultGeom, featureGeom); } } } finally { // ask feature collection to close potentially still open iterators if (iterator != null) { featureCollection.close(iterator); } } return resultGeom; } /** * Extracts the geometries and makes a geometry array. * <p> * Note the resulting array size might be lower than the featureCollection * size, as it will not contain null geometries. * </p> * * @param featureCollection * @return Geometry[] geometries present in features */ public static Geometry[] extractGeometries(final FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection) { Iterator<SimpleFeature> iter = featureCollection.iterator(); try { SimpleFeature feature; ArrayList<Geometry> geometries = new ArrayList<Geometry>(featureCollection.size()); int finalSize = 0; while (iter.hasNext()) { feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); if (geometry == null) { continue; } else if (geometry instanceof GeometryCollection) { GeometryCollection geomSet = (GeometryCollection) geometry; final int size = geomSet.getNumGeometries(); for (int j = 0; j < size; j++) { geometries.add(geomSet.getGeometryN(j)); } finalSize += size; } else { geometries.add(geometry); finalSize++; } } return geometries.toArray(new Geometry[finalSize]); } finally { featureCollection.close(iter); } } /** * Adapts a Geometry <code>geom</code> to another type of geometry given the * desired geometry class. * <p> * Currently implemented adaptations: * <ul> * <li>Point -> MultiPoint. Wraps the Point on a single part MultiPoint. * <li>Polygon -> MultiPolygon. Wraps the Polygon on a single part * MultiPolygon. * <li>LineString -> MultiLineString. Wraps the LineString on a single part * MultiLineString. * <li>MultiLineString -> LineString. Succeeds if merging the parts result * in a single LineString, fails otherwise. * <li>MultiPolygon -> Polygon. Succeeds if merging the parts result in a * single Polygon, fails otherwise. * <li>* -> GeometryCollection * </ul> * * If the given type of geometry is the same as the desired one, returns * that type of geometry. * </p> * TODO: add more adaptations on an as needed basis * * @param inputGeom * @param adaptTo * @return a new Geometry adapted * @throws IllegalArgumentException * if <code>geom</code> cannot be adapted as <code>adapTo</code> */ public static Geometry adapt(final Geometry inputGeom, final Class<? extends Geometry> adaptTo) { assert inputGeom != null : "inputGeom can't be null"; //$NON-NLS-1$ assert adaptTo != null : "adaptTo can't be null"; //$NON-NLS-1$ if (Geometry.class.equals(adaptTo)) { return inputGeom; } final Class<?> geomClass = inputGeom.getClass(); if (geomClass.equals(adaptTo)) { return inputGeom; } final GeometryFactory gf = inputGeom.getFactory(); if (MultiPoint.class.equals(adaptTo) && Point.class.equals(geomClass)) { return gf.createMultiPoint(new Point[] { (Point) inputGeom }); } if (Polygon.class.equals(adaptTo)) { if (adaptTo.equals(geomClass)) { return inputGeom; } Polygonizer polygonnizer = new Polygonizer(); polygonnizer.add(inputGeom); Collection<? extends Polygon> polys = polygonnizer.getPolygons(); Polygon[] polygons = new ArrayList<Polygon>(polys).toArray(new Polygon[polys.size()]); if (polygons.length == 1) { return polygons[0]; } } if (MultiPolygon.class.equals(adaptTo)) { if (adaptTo.equals(geomClass)) { return inputGeom; } if (Polygon.class.equals(geomClass)) { return gf.createMultiPolygon(new Polygon[] { (Polygon) inputGeom }); } /* * Polygonizer polygonnizer = new Polygonizer(); * polygonnizer.add(inputGeom); Collection polys = * polygonnizer.getPolygons(); Polygon[] polygons = new * ArrayList<Polygon>(polys).toArray(new Polygon[polys.size()]); if * (MultiPolygon.class.equals(adaptTo)) { return * gf.createMultiPolygon(polygons); } if (polygons.length == 1) { * return polygons[0]; } */ } if (GeometryCollection.class.equals(adaptTo)) { return gf.createGeometryCollection(new Geometry[] { inputGeom }); } if (MultiLineString.class.equals(adaptTo) || LineString.class.equals(adaptTo)) { LineMerger merger = new LineMerger(); merger.add(inputGeom); Collection<? extends LineString> mergedLineStrings = merger.getMergedLineStrings(); ArrayList<LineString> lineList = new ArrayList<LineString>(mergedLineStrings); LineString[] lineStrings = lineList.toArray(new LineString[mergedLineStrings.size()]); assert lineStrings.length >= 1; if (MultiLineString.class.equals(adaptTo)) { MultiLineString line = gf.createMultiLineString(lineStrings); return line; } else { // adapts to LineString class if (lineStrings.length == 1) { Geometry mergedResult = (Geometry) lineStrings[0]; return mergedResult; } else { // it has more than one geometry final String msg = MessageFormat.format(Messages.GeometryUtil_DonotKnowHowAdapt, geomClass.getSimpleName(), adaptTo.getSimpleName()); throw new IllegalArgumentException(msg); } } } if (Polygon.class.equals(adaptTo) && (MultiPolygon.class.equals(geomClass))) { // adapts multipolygon to polygon assert inputGeom.getNumGeometries() == 1 : "the collection must have 1 element to adapt to Polygon"; //$NON-NLS-1$ return inputGeom.getGeometryN(1); } else if (LineString.class.equals(adaptTo) && (MultiLineString.class.equals(geomClass))) { // adapts MultiLinestring to Linestring assert inputGeom.getNumGeometries() == 1 : "the collection must have 1 element to adapt to Polygon"; //$NON-NLS-1$ return inputGeom.getGeometryN(1); } // Adapt a geometry collection that contains points, to Point or // MultiPoint. if (GeometryCollection.class.equals(geomClass) && (MultiPoint.class.equals(adaptTo) || Point.class.equals(adaptTo))) { List<Geometry> geomList = new ArrayList<Geometry>(); for (int i = 0; i < inputGeom.getNumGeometries(); i++) { Geometry geom = inputGeom.getGeometryN(i); Class<?> clazz = geom.getClass(); if (MultiPoint.class.equals(clazz) || Point.class.equals(clazz)) { geomList.add(geom); } } return gf.buildGeometry(geomList); } // Adapt a geometry collection that contains lines, to LineString or // MultiLineString. if (GeometryCollection.class.equals(geomClass) && (MultiLineString.class.equals(adaptTo) || LineString.class.equals(adaptTo))) { List<Geometry> geomList = new ArrayList<Geometry>(); for (int i = 0; i < inputGeom.getNumGeometries(); i++) { Geometry geom = inputGeom.getGeometryN(i); Class<?> clazz = geom.getClass(); if (MultiLineString.class.equals(clazz) || LineString.class.equals(clazz)) { geomList.add(geom); } } return gf.buildGeometry(geomList); } // Adapt a geometry collection that contains polygons, to Polyong or // MultiPolygon. if (GeometryCollection.class.equals(geomClass) && (MultiPolygon.class.equals(adaptTo) || Polygon.class.equals(adaptTo))) { List<Geometry> geomList = new ArrayList<Geometry>(); for (int i = 0; i < inputGeom.getNumGeometries(); i++) { Geometry geom = inputGeom.getGeometryN(i); Class<?> clazz = geom.getClass(); if (MultiPolygon.class.equals(clazz) || Polygon.class.equals(clazz)) { geomList.add(geom); } } return gf.buildGeometry(geomList); } final String msg = MessageFormat.format(Messages.GeometryUtil_DonotKnowHowAdapt, geomClass.getSimpleName(), adaptTo.getSimpleName()); throw new IllegalArgumentException(msg); } /** * Creates a geometry collection using the simple geometries contained in * the gem List. * * @param simpleGeometries * homogeneous list of zero or more geometries of the class: * Points, LineStrings or Polygons * @param expectedClass * expected result class * * @return a new instance of GeometryCollection with the simple geometries */ public static GeometryCollection adaptToGeomCollection( ArrayList<Geometry> simpleGeometries, Class<? extends GeometryCollection> expectedClass) { if (simpleGeometries.size() == 0) { return GeometryUtil.getNullGeometryCollection(); } final GeometryFactory geomFactory = simpleGeometries.get(0).getFactory(); GeometryCollection geomResult = null; if (MultiPolygon.class.equals(expectedClass)) { geomResult = geomFactory.createMultiPolygon(simpleGeometries.toArray(new Polygon[simpleGeometries.size()])); } else if (MultiLineString.class.equals(expectedClass)) { geomResult = geomFactory.createMultiLineString(simpleGeometries.toArray(new LineString[simpleGeometries .size()])); } else if (MultiPoint.class.equals(expectedClass)) { geomResult = geomFactory.createMultiPoint(simpleGeometries.toArray(new Point[simpleGeometries.size()])); } else { assert false : "illegal argument: the expectedClass parameter must be a subclass of GeometryCollection"; //$NON-NLS-1$ } return geomResult; } /** * * @return an empty geometry collection */ private static GeometryCollection getNullGeometryCollection() { GeometryFactory gf = new GeometryFactory(); return gf.createGeometryCollection(new Geometry[] {}); } /** * creates the source geometry array to the class required * * @param sourceGeomArray * one or more geometries * @param requiredGeom * * @return new geometry of the required class */ public static Geometry adapt(final ArrayList<Geometry> sourceGeomArray, final Class<? extends Geometry> requiredGeom) { assert sourceGeomArray.size() >= 1 : "one or more geometries are required in the source geometry collection"; //$NON-NLS-1$ final GeometryFactory geomFactory = sourceGeomArray.get(0).getFactory(); Geometry result; if (GeometryCollection.class.equals(requiredGeom.getSuperclass())) { Class<? extends GeometryCollection> geomCollectionClass = (Class<? extends GeometryCollection>) requiredGeom; result = adaptToGeomCollection(sourceGeomArray, geomCollectionClass); } else {// simple geometry is required result = geomFactory.buildGeometry(sourceGeomArray); } return result; } /** * @param geomClass * @return the geometry class's dimension */ public static int getDimension(final Class<?> geomClass) { if ((Point.class.equals(geomClass)) || (MultiPoint.class.equals(geomClass))) { return 0; } else if ((LineString.class.equals(geomClass)) || (MultiLineString.class.equals(geomClass))) { return 1; } else if ((Polygon.class.equals(geomClass)) || (MultiPolygon.class.equals(geomClass))) { return 2; } else { final String msg = MessageFormat.format(Messages.GeometryUtil_CannotGetDimension, geomClass.getName()); throw new IllegalArgumentException(msg); } } /** * @param simpleGeometry * Point, LineString or Polygon class * @return a compatible Geometry collection for the simple geometry */ public static Class<? extends GeometryCollection> getCompatibleCollection(final Class<? extends Geometry> simpleGeometry) { if (Point.class.equals(simpleGeometry)) { return MultiPoint.class; } else if (LineString.class.equals(simpleGeometry)) { return MultiLineString.class; } else if (Polygon.class.equals(simpleGeometry)) { return MultiPolygon.class; } else { throw new IllegalArgumentException(Messages.GeometryUtil_ExpectedSimpleGeometry); } } /** * <p> * * <pre> * Converts the feature LineString into Polygon. If the LineString geometry * can't be converted, (i.e, the line has 2 coordinates, so its impossible * to create a linearRing) throws {@link IllegalArgumentException}. * * The line must has at least 3 coordinates. * * - Get the line geometry. * - For each geometry, create a LinerRing. * - Combine all the LinearRing into one Geometry. * * Return the combined geometry. * * </pre> * * </p> * * @param feature * @return */ public static Geometry convertLineStringIntoPolygonGeometry(SimpleFeature feature) { GeometryFactory factory = new GeometryFactory(); Geometry featureGeom = (Geometry) feature.getDefaultGeometry(); List<Geometry> ringsList = new ArrayList<Geometry>(); for (int i = 0; i < featureGeom.getNumGeometries(); i++) { Geometry line = featureGeom.getGeometryN(i); Coordinate[] lineCoor = line.getCoordinates(); if (lineCoor.length < 3) { // not a valid geometry for convert to polygon, so continue. continue; } // for creating a linearRing, the geometry must form a closed // geometry. // close the geometry. Coordinate[] closedLine = closeGeometry(lineCoor); // try to create a linearRing. LinearRing ring; try { ring = factory.createLinearRing(closedLine); // Create the polygon geometry and add to the list. Geometry polygonGeometry = factory.createPolygon(ring, null); ringsList.add(polygonGeometry); } catch (Exception e) { // Couldn't create a ring from this geometry,so continue. continue; } } Geometry combined = factory.buildGeometry(ringsList); // combined.union(); return combined; } /** * This coordinates belong to a LineString geometry, it's coordinates aren't * closed, close the coordinate. The first coordinate must be equal to the * last coordinate. * * @param lineCoor * @return */ public static Coordinate[] closeGeometry(Coordinate[] lineCoor) { if (lineCoor[0].equals(lineCoor[lineCoor.length - 1])) { // it's closed so return it. return lineCoor; } Coordinate[] closedCoor = new Coordinate[lineCoor.length + 1]; int i; for (i = 0; i < lineCoor.length; i++) { closedCoor[i] = lineCoor[i]; } // closed the geometry by setting the last coordinate equal to the first // coordinate closedCoor[i] = lineCoor[0]; assert closedCoor[0].equals(closedCoor[closedCoor.length - 1]); return closedCoor; } private static Coordinate[][] inputLines = new Coordinate[2][2]; /** * This method computes the actual value of the intersection point. To * obtain the maximum precision from the intersection calculation, the * coordinates are normalized by subtracting the minimum ordinate values (in * absolute value). This has the effect of removing common significant * digits from the calculation to maintain more bits of precision. * * @param precisionModel */ public static Coordinate intersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { inputLines[0][0] = p1; inputLines[0][1] = p2; inputLines[1][0] = q1; inputLines[1][1] = q2; Coordinate intPt = intersectionWithNormalization(p1, p2, q1, q2); return intPt; } private static Coordinate intersectionWithNormalization(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { Coordinate n1 = new Coordinate(p1); Coordinate n2 = new Coordinate(p2); Coordinate n3 = new Coordinate(q1); Coordinate n4 = new Coordinate(q2); Coordinate normPt = new Coordinate(); normalizeToEnvCentre(n1, n2, n3, n4, normPt); Coordinate intPt = safeHCoordinateIntersection(n1, n2, n3, n4); intPt.x += normPt.x; intPt.y += normPt.y; return intPt; } /** * Normalize the supplied coordinates to so that the midpoint of their * intersection envelope lies at the origin. * * @param n00 * @param n01 * @param n10 * @param n11 * @param normPt */ private static void normalizeToEnvCentre( Coordinate n00, Coordinate n01, Coordinate n10, Coordinate n11, Coordinate normPt) { double minX0 = n00.x < n01.x ? n00.x : n01.x; double minY0 = n00.y < n01.y ? n00.y : n01.y; double maxX0 = n00.x > n01.x ? n00.x : n01.x; double maxY0 = n00.y > n01.y ? n00.y : n01.y; double minX1 = n10.x < n11.x ? n10.x : n11.x; double minY1 = n10.y < n11.y ? n10.y : n11.y; double maxX1 = n10.x > n11.x ? n10.x : n11.x; double maxY1 = n10.y > n11.y ? n10.y : n11.y; double intMinX = minX0 > minX1 ? minX0 : minX1; double intMaxX = maxX0 < maxX1 ? maxX0 : maxX1; double intMinY = minY0 > minY1 ? minY0 : minY1; double intMaxY = maxY0 < maxY1 ? maxY0 : maxY1; double intMidX = (intMinX + intMaxX) / 2.0; double intMidY = (intMinY + intMaxY) / 2.0; normPt.x = intMidX; normPt.y = intMidY; /* * // equilavalent code using more modular but slower method Envelope * env0 = new Envelope(n00, n01); Envelope env1 = new Envelope(n10, * n11); Envelope intEnv = env0.intersection(env1); Coordinate intMidPt * = intEnv.centre(); * * normPt.x = intMidPt.x; normPt.y = intMidPt.y; */ n00.x -= normPt.x; n00.y -= normPt.y; n01.x -= normPt.x; n01.y -= normPt.y; n10.x -= normPt.x; n10.y -= normPt.y; n11.x -= normPt.x; n11.y -= normPt.y; } /** * Computes a segment intersection using homogeneous coordinates. Round-off * error can cause the raw computation to fail, (usually due to the segments * being approximately parallel). If this happens, a reasonable * approximation is computed instead. * * @param p1 * a segment endpoint * @param p2 * a segment endpoint * @param q1 * a segment endpoint * @param q2 * a segment endpoint * @return the computed intersection point */ private static Coordinate safeHCoordinateIntersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { Coordinate intPt = null; try { intPt = HCoordinate.intersection(p1, p2, q1, q2); } catch (NotRepresentableException e) { // compute an approximate result intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); } return intPt; } /** * Rotate the given lineString with the provided angle. * * @param input * The lineString to rotate. * @param angle * The angle in radiant. * @return The rotated lineString. */ public static LineString rotation(LineString input, double angle) { Coordinate center = input.getCentroid().getCoordinate(); return rotation(input, angle, center); } /** * Rotate the given lineString with the provided angle and a center point as * a reference point required for rotate. * * @param input * The input lineString to rotate. * @param angle * The angle in radiant. * @param center * The center point as reference to rotate. * @return The rotated lineString. */ public static LineString rotation(LineString input, double angle, Coordinate center) { GeometryFactory gf = input.getFactory(); Coordinate[] lineCoords = input.getCoordinates(); List<Coordinate> rotationCoords = new LinkedList<Coordinate>(); for (int i = 0; i < lineCoords.length; i++) { Coordinate actualCoord = lineCoords[i]; rotationCoords.add(rotate(actualCoord, center, angle)); } Coordinate[] newCoords = rotationCoords.toArray(new Coordinate[rotationCoords.size()]); return gf.createLineString(newCoords); } /** * Rotate the given polygon. * * @param input * The polygon to rotate. * @param angle * Angle in radiant. * @return The rotated polygon. */ public static Polygon rotation(Polygon input, double angle) { Coordinate center = input.getCentroid().getCoordinate(); return rotation(input, angle, center); } /** * Rotates the polygon. * * @param input * The polygon to rotate * @param angle * Angle in radiant. * @param pivot * x,y coordinate reference needed to rotate. * @return a new polygon */ public static Polygon rotation(Polygon input, double angle, Coordinate pivot) { GeometryFactory gf = input.getFactory(); Coordinate[] shellCoords = input.getExteriorRing().getCoordinates(); List<Coordinate> rotationShell = new LinkedList<Coordinate>(); for (int i = 0; i < shellCoords.length; i++) { Coordinate actualCoord = shellCoords[i]; rotationShell.add(rotate(actualCoord, pivot, angle)); } List<LinearRing> holes = new LinkedList<LinearRing>(); for (int j = 0; j < input.getNumInteriorRing(); j++) { List<Coordinate> rotationHole = new LinkedList<Coordinate>(); Coordinate[] holeCoords = input.getInteriorRingN(j).getCoordinates(); for (int i = 0; i < holeCoords.length; i++) { Coordinate actualCoord = holeCoords[i]; rotationHole.add(rotate(actualCoord, pivot, angle)); } Coordinate[] rotatedHole = rotationHole.toArray(new Coordinate[rotationHole.size()]); holes.add(gf.createLinearRing(rotatedHole)); } Coordinate[] newCoords = rotationShell.toArray(new Coordinate[rotationShell.size()]); LinearRing shell = gf.createLinearRing(newCoords); LinearRing[] rings = holes.toArray(new LinearRing[holes.size()]); return gf.createPolygon(shell, rings); } /** * Rotate the given coordinate. * * @param actualCoord * Coordinate to rotate. * @param center * Reference coordinate needed to rotate. * @param angle * Angle in radiant. * @return The rotated coordinate. */ private static Coordinate rotate(Coordinate actualCoord, Coordinate center, double angle) { Coordinate rotateCoord = new Coordinate(); rotateCoord.x = (Math.cos(angle) * (actualCoord.x - center.x)) - (Math.sin(angle) * (actualCoord.y - center.y)) + center.x; rotateCoord.y = (Math.sin(angle) * (actualCoord.x - center.x)) + (Math.cos(angle) * (actualCoord.y - center.y)) + center.y; assert rotateCoord != null; return rotateCoord; } }