All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.h2gis.functions.spatial.clean.MakeValidOp Maven / Gradle / Ivy

The newest version!
/*
 * This program extends Java Topology Suite (JTS) capability and is made
 * available to any Software already using Java Topology Suite.
 *
 * Copyright (C) Michaël Michaud (2017)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as
 * published by the Free Software Foundation, either version 2.1 of the
 * License, or any later version.
 *
 * This program 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
 * Lesser GNU General Public License for more details.
 *
 * You should have received a copy of the Lesser GNU General Public
 * License along with this program.
 * If not, see .
 */
package org.h2gis.functions.spatial.clean;

import com.vividsolutions.jts.algorithm.RayCrossingCounter;
import com.vividsolutions.jts.algorithm.RobustLineIntersector;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geom.impl.PackedCoordinateSequenceFactory;
import com.vividsolutions.jts.geom.util.PolygonExtracter;
import com.vividsolutions.jts.noding.IntersectionAdder;
import com.vividsolutions.jts.noding.MCIndexNoder;
import com.vividsolutions.jts.noding.NodedSegmentString;
import com.vividsolutions.jts.operation.linemerge.LineMerger;
import com.vividsolutions.jts.operation.polygonize.Polygonizer;
import com.vividsolutions.jts.operation.union.UnaryUnionOp;

import java.util.*;

import static com.vividsolutions.jts.geom.impl.PackedCoordinateSequenceFactory.*;
import com.vividsolutions.jts.geom.util.LineStringExtracter;
import com.vividsolutions.jts.geom.util.PointExtracter;
import java.sql.SQLException;

/**
 * Operator to make a geometry valid.
 * 
* Making a geometry valid will remove duplicate points although duplicate points * do not make a geometry invalid. * * @author Michaël Michaud */ public class MakeValidOp { private static final Coordinate[] EMPTY_COORD_ARRAY = new Coordinate[0]; private static final LinearRing[] EMPTY_RING_ARRAY = new LinearRing[0]; // If preserveGeomDim is true, the geometry dimension returned by MakeValidOp // must be the same as the inputGeometryType (degenerate components of lower // dimension are removed). // If preserveGeomDim is false MakeValidOp will preserve as much coordinates // as possible and may return a geometry of lower dimension or a // GeometryCollection if input geometry or geometry components have not the // required number of points. private boolean preserveGeomDim = true; // If preserveCoordDim is true, MakeValidOp preserves third and fourth ordinates. // If preserveCoordDim is false, third dimension is preserved but not fourth one. private boolean preserveCoordDim = true; // If preserveDuplicateCoord is true, MakeValidOp will preserve duplicate // coordinates as much as possible. Generally, duplicate coordinates can be // preserved for linear geometries but not for areal geometries (overlay // operations used to repair polygons remove duplicate points). // If preserveDuplicateCoord is false, all duplicated coordinates are removed. private boolean preserveDuplicateCoord = true; public MakeValidOp() { } public MakeValidOp setPreserveGeomDim(boolean preserveGeomDim) { this.preserveGeomDim = preserveGeomDim; return this; } public MakeValidOp setPreserveCoordDim(boolean preserveCoordDim) { this.preserveCoordDim = preserveCoordDim; return this; } public MakeValidOp setPreserveDuplicateCoord(boolean preserveDuplicateCoord) { this.preserveDuplicateCoord = preserveDuplicateCoord; return this; } /** * Decompose a geometry recursively into simple components. * * @param geometry input geometry * @param list a list of simple components (Point, LineString or Polygon) */ private static void decompose(Geometry geometry, Collection list) { for (int i = 0; i < geometry.getNumGeometries(); i++) { Geometry component = geometry.getGeometryN(i); if (component instanceof GeometryCollection) { decompose(component, list); } else { list.add(component); } } } /** * Repair an invalid geometry. *
* If preserveGeomDim is true, makeValid will remove degenerated geometries * from the result, i.e geometries which dimension is lower than the input * geometry dimension (except for mixed GeometryCollection). *
* A multi-geometry will always produce a multi-geometry (eventually empty * or made of a single component). A simple geometry may produce a * multi-geometry (ex. polygon with self-intersection will generally produce * a multi-polygon). In this case, it is up to the client to explode * multi-geometries if he needs to. *
* If preserveGeomDim is off, it is up to the client to filter degenerate * geometries. *
* WARNING : for geometries of dimension 1 (linear), duplicate coordinates * are preserved as much as possible. For geometries of dimension 2 (areal), * duplicate coordinates are generally removed due to the use of overlay * operations. * * @param geometry input geometry * @return a valid Geometry */ public Geometry makeValid(Geometry geometry) { // Input geometry is recursively exploded into a list of simple components List list = new ArrayList<>(geometry.getNumGeometries()); decompose(geometry, list); // Each single component is made valid Collection list2 = new ArrayList<>(); for (Geometry component : list) { if (component instanceof Point) { Point p = makePointValid((Point) component); if (!p.isEmpty()) { list2.add(p); } } else if (component instanceof LineString) { Geometry geom = makeLineStringValid((LineString) component); for (int i = 0; i < geom.getNumGeometries(); i++) { if (!geom.getGeometryN(i).isEmpty()) { list2.add(geom.getGeometryN(i)); } } } else if (component instanceof Polygon) { Geometry geom = makePolygonValid((Polygon) component); for (int i = 0; i < geom.getNumGeometries(); i++) { if (!geom.getGeometryN(i).isEmpty()) { list2.add(geom.getGeometryN(i)); } } } else { assert false : "Should never reach here"; } } list.clear(); for (Geometry g : list2) { // If preserveGeomDim is true and original input geometry is not a GeometryCollection // components with a lower dimension than input geometry are removed if (preserveGeomDim && !geometry.getClass().getSimpleName().equals("GeometryCollection")) { removeLowerDimension(g, list, geometry.getDimension()); } else { decompose(g, list); } } list2 = list; // In a MultiPolygon, polygons cannot touch or overlap each other // (adjacent polygons are not merged in the context of a mixed GeometryCollection) if (list2.size() > 1) { boolean multiPolygon = true; for (Geometry geom : list2) { if (geom.getDimension() < 2) { multiPolygon = false; } } if (multiPolygon) { list2 = unionAdjacentPolygons(list2); } } if (list2.isEmpty()) { GeometryFactory factory = geometry.getFactory(); if (geometry instanceof Point) { return factory.createPoint((Coordinate) null); } else if (geometry instanceof LinearRing) { return factory.createLinearRing(EMPTY_COORD_ARRAY); } else if (geometry instanceof LineString) { return factory.createLineString(EMPTY_COORD_ARRAY); } else if (geometry instanceof Polygon) { return factory.createPolygon(factory.createLinearRing(EMPTY_COORD_ARRAY), EMPTY_RING_ARRAY); } else if (geometry instanceof MultiPoint) { return factory.createMultiPoint(new Point[0]); } else if (geometry instanceof MultiLineString) { return factory.createMultiLineString(new LineString[0]); } else if (geometry instanceof MultiPolygon) { return factory.createMultiPolygon(new Polygon[0]); } else { return factory.createGeometryCollection(new Geometry[0]); } } else { CoordinateSequenceFactory csFactory = geometry.getFactory().getCoordinateSequenceFactory(); // Preserve 4th coordinate dimension as much as possible if preserveCoordDim is true if (preserveCoordDim && csFactory instanceof PackedCoordinateSequenceFactory && ((PackedCoordinateSequenceFactory) csFactory).getDimension() == 4) { Map map = new HashMap<>(); gatherDim4(geometry, map); list2 = restoreDim4(list2, map); } Geometry result = geometry.getFactory().buildGeometry(list2); // If input geometry was a GeometryCollection and result is a simple geometry // create a multi-geometry made of a single component if (geometry instanceof GeometryCollection && !(result instanceof GeometryCollection)) { if (geometry instanceof MultiPoint && result instanceof Point) { result = geometry.getFactory().createMultiPoint(new Point[]{(Point) result}); } else if (geometry instanceof MultiLineString && result instanceof LineString) { result = geometry.getFactory().createMultiLineString(new LineString[]{(LineString) result}); } else if (geometry instanceof MultiPolygon && result instanceof Polygon) { result = geometry.getFactory().createMultiPolygon(new Polygon[]{(Polygon) result}); } } return result; } } // Reursively remove geometries with a dimension less than dimension parameter private void removeLowerDimension(Geometry geometry, List result, int dimension) { for (int i = 0; i < geometry.getNumGeometries(); i++) { Geometry g = geometry.getGeometryN(i); if (g instanceof GeometryCollection) { removeLowerDimension(g, result, dimension); } else if (g.getDimension() >= dimension) { result.add(g); } } } // Union adjacent polygons to make an invalid MultiPolygon valid private Collection unionAdjacentPolygons(Collection list) { UnaryUnionOp op = new UnaryUnionOp(list); Geometry result = op.union(); if (result.getNumGeometries() < list.size()) { list.clear(); for (int i = 0; i < result.getNumGeometries(); i++) { list.add(result.getGeometryN(i)); } } return list; } // If X or Y is null, return an empty Point private Point makePointValid(Point point) { CoordinateSequence sequence = point.getCoordinateSequence(); GeometryFactory factory = point.getFactory(); CoordinateSequenceFactory csFactory = factory.getCoordinateSequenceFactory(); if (sequence.size() == 0) { return point; } else if (Double.isNaN(sequence.getOrdinate(0, 0)) || Double.isNaN(sequence.getOrdinate(0, 1))) { return factory.createPoint(csFactory.create(0, sequence.getDimension())); } else if (sequence.size() == 1) { return point; } else { throw new RuntimeException("JTS cannot create a point from a CoordinateSequence containing several points"); } } /** * Returns a coordinateSequence free of Coordinates with X or Y NaN value, * and if desired, free of duplicated coordinates. makeSequenceValid keeps * the original dimension of input sequence. * * @param sequence input sequence of coordinates * @param preserveDuplicateCoord if duplicate coordinates must be preserved * @param close if the sequence must be closed * @return a new CoordinateSequence with valid XY values */ private static CoordinateSequence makeSequenceValid(CoordinateSequence sequence, boolean preserveDuplicateCoord, boolean close) { int dim = sequence.getDimension(); // we add 1 to the sequence size for the case where we have to close the linear ring double[] array = new double[(sequence.size() + 1) * sequence.getDimension()]; boolean modified = false; int count = 0; // Iterate through coordinates, skip points with x=NaN, y=NaN or duplicate for (int i = 0; i < sequence.size(); i++) { if (Double.isNaN(sequence.getOrdinate(i, 0)) || Double.isNaN(sequence.getOrdinate(i, 1))) { modified = true; continue; } if (!preserveDuplicateCoord && count > 0 && sequence.getCoordinate(i).equals(sequence.getCoordinate(i - 1))) { modified = true; continue; } for (int j = 0; j < dim; j++) { array[count * dim + j] = sequence.getOrdinate(i, j); if (j == dim - 1) { count++; } } } // Close the sequence if it is not closed and there is already 3 distinct coordinates if (close && count > 2 && (array[0] != array[(count - 1) * dim] || array[1] != array[(count - 1) * dim + 1])) { System.arraycopy(array, 0, array, count * dim, dim); modified = true; count++; } // Close z, m dimension if needed if (close && count > 3 && dim > 2) { for (int d = 2; d < dim; d++) { if (array[(count - 1) * dim + d] != array[d]) { modified = true; } array[(count - 1) * dim + d] = array[d]; } } if (modified) { double[] shrinkedArray = new double[count * dim]; System.arraycopy(array, 0, shrinkedArray, 0, count * dim); return PackedCoordinateSequenceFactory.DOUBLE_FACTORY.create(shrinkedArray, dim); } else { return sequence; } } /** * Returns *
    *
  • an empty LineString if input CoordinateSequence has no valid * point
  • *
  • a Point if input CoordinateSequence has a single valid Point
  • *
* makeLineStringValid keeps the original dimension of input sequence. * * @param lineString the LineString to make valid * @return a valid LineString or a Point if lineString length equals 0 */ private Geometry makeLineStringValid(LineString lineString) { CoordinateSequence sequence = lineString.getCoordinateSequence(); CoordinateSequence sequenceWithoutDuplicates = makeSequenceValid(sequence, false, false); GeometryFactory factory = lineString.getFactory(); if (sequenceWithoutDuplicates.size() == 0) { // no valid point -> empty LineString return factory.createLineString(factory.getCoordinateSequenceFactory().create(0, sequence.getDimension())); } else if (sequenceWithoutDuplicates.size() == 1) { // a single valid point -> returns a Point if (preserveGeomDim) { return factory.createLineString(factory.getCoordinateSequenceFactory().create(0, sequence.getDimension())); } else { return factory.createPoint(sequenceWithoutDuplicates); } } else if (preserveDuplicateCoord) { return factory.createLineString(makeSequenceValid(sequence, true, false)); } else { return factory.createLineString(sequenceWithoutDuplicates); } } /** * Making a Polygon valid may creates *
    *
  • an Empty Polygon if input has no valid coordinate
  • *
  • a Point if input has only one valid coordinate
  • *
  • a LineString if input has only a valid segment
  • *
  • a Polygon in most cases
  • *
  • a MultiPolygon if input has a self-intersection
  • *
  • a GeometryCollection if input has degenerate parts (ex. degenerate * holes)
  • *
* * @param polygon the Polygon to make valid * @return a valid Geometry which may be of any type if the source geometry * is not valid. */ private Geometry makePolygonValid(Polygon polygon) { //This first step analyze linear components and create degenerate geometries //of dimension 0 or 1 if they do not form valid LinearRings //If degenerate geometries are found, it may produce a GeometryCollection with //heterogeneous dimension Geometry geom = makePolygonComponentsValid(polygon); List list = new ArrayList<>(); for (int i = 0; i < geom.getNumGeometries(); i++) { Geometry component = geom.getGeometryN(i); if (component instanceof Polygon) { Geometry nodedPolygon = nodePolygon((Polygon) component); for (int j = 0; j < nodedPolygon.getNumGeometries(); j++) { list.add(nodedPolygon.getGeometryN(j)); } } else { list.add(component); } } return polygon.getFactory().buildGeometry(list); } /** * The method makes sure that outer and inner rings form valid LinearRings. *

* If outerRing is not a valid LinearRing, every linear component is * considered as a degenerated geometry of lower dimension (0 or 1) *

*

* If outerRing is a valid LinearRing but some innerRings are not, invalid * innerRings are transformed into LineString (or Point) and the returned * geometry may be a GeometryCollection of heterogeneous dimension. *

* * @param polygon simple Polygon to make valid * @return a Geometry which may not be a Polygon if the source Polygon is * invalid */ private Geometry makePolygonComponentsValid(Polygon polygon) { GeometryFactory factory = polygon.getFactory(); CoordinateSequence outerRingSeq = makeSequenceValid(polygon.getExteriorRing().getCoordinateSequence(), false, true); // The validated sequence of the outerRing does not form a valid LinearRing // -> build valid 0-dim or 1-dim geometry from all the rings if (outerRingSeq.size() == 0 || outerRingSeq.size() < 4) { List list = new ArrayList<>(); if (outerRingSeq.size() > 0) { list.add(makeLineStringValid(polygon.getExteriorRing())); } for (int i = 0; i < polygon.getNumInteriorRing(); i++) { Geometry g = makeLineStringValid(polygon.getInteriorRingN(i)); if (!g.isEmpty()) { list.add(g); } } if (list.isEmpty()) { return factory.createPolygon(outerRingSeq); } else { return factory.buildGeometry(list); } } // OuterRing forms a valid ring. // Inner rings may be degenerated else { List innerRings = new ArrayList<>(); List degeneratedRings = new ArrayList<>(); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { CoordinateSequence seq = makeSequenceValid(polygon.getInteriorRingN(i).getCoordinateSequence(), false, true); if (seq.size() > 3) { innerRings.add(factory.createLinearRing(seq)); } else if (seq.size() > 1) { degeneratedRings.add(factory.createLineString(seq)); } else if (seq.size() == 1) { degeneratedRings.add(factory.createPoint(seq)); } // seq.size == 0 } Polygon poly = factory.createPolygon(factory.createLinearRing(outerRingSeq), innerRings.toArray(new LinearRing[innerRings.size()])); if (degeneratedRings.isEmpty()) { return poly; } else { degeneratedRings.add(0, poly); return factory.buildGeometry(degeneratedRings); } } } /** * Computes a valid Geometry from a Polygon which may not be valid * (auto-intersecting ring or overlapping holes). *
    *
  • creates a Geometry from the noded exterior boundary
  • *
  • remove Geometries computed from noded interior boundaries
  • *
*/ private Geometry nodePolygon(Polygon polygon) { LinearRing exteriorRing = (LinearRing) polygon.getExteriorRing(); Geometry geom = getArealGeometryFromLinearRing(exteriorRing); // geom can be a GeometryCollection // extract polygonal areas because symDifference cannot process GeometryCollections List polys = new ArrayList<>(); List lines = new ArrayList<>(); List points = new ArrayList<>(); geom.apply(new PolygonExtracter(polys)); geom.apply(new LineStringExtracter(lines)); geom.apply(new PointExtracter(points)); geom = geom.getFactory().buildGeometry(polys); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { LinearRing interiorRing = (LinearRing) polygon.getInteriorRingN(i); // extract polygonal areas because symDifference cannot process GeometryCollections polys.clear(); getArealGeometryFromLinearRing(interiorRing).apply(new PolygonExtracter(polys)); // TODO avoid the use of difference operator geom = geom.symDifference(geom.getFactory().buildGeometry(polys)); } List result = new ArrayList<>(); result.add(geom); result.addAll(lines); result.addAll(points); return geom.getFactory().buildGeometry(result); } /** * Node a LinearRing and return a MultiPolygon containing *
    *
  • a single Polygon if the LinearRing is simple
  • *
  • several Polygons if the LinearRing auto-intersects
  • *
* This is used to repair auto-intersecting Polygons */ private Geometry getArealGeometryFromLinearRing(LinearRing ring) { if (ring.isSimple()) { return ring.getFactory().createMultiPolygon(new Polygon[]{ ring.getFactory().createPolygon(ring, EMPTY_RING_ARRAY) }); } else { // Node input LinearRing and extract unique segments Set lines = nodeLineString(ring.getCoordinates(), ring.getFactory()); lines = getSegments(lines); // Polygonize the line network Polygonizer polygonizer = new Polygonizer(); polygonizer.add(lines); // Computes intersections to determine the status of each polygon Collection geoms = new ArrayList(); for (Object object : polygonizer.getPolygons()) { Polygon polygon = (Polygon) object; Coordinate p = polygon.getInteriorPoint().getCoordinate(); int location = RayCrossingCounter.locatePointInRing(p, ring.getCoordinateSequence()); if (location == Location.INTERIOR) { geoms.add(polygon); } } Geometry unionPoly = UnaryUnionOp.union(geoms); Geometry unionLines = UnaryUnionOp.union(lines).difference(unionPoly.getBoundary()); geoms.clear(); decompose(unionPoly, geoms); decompose(unionLines, geoms); return ring.getFactory().buildGeometry(geoms); } } /** * Return a set of segments from a linestring * * @param lines * @return */ private Set getSegments(Collection lines) { Set set = new HashSet<>(); for (LineString line : lines) { Coordinate[] cc = line.getCoordinates(); for (int i = 1; i < cc.length; i++) { if (!cc[i - 1].equals(cc[i])) { LineString segment = line.getFactory().createLineString( new Coordinate[]{new Coordinate(cc[i - 1]), new Coordinate(cc[i])}); set.add(segment); } } } return set; } // Use ring to restore M values on geoms private Collection restoreDim4(Collection geoms, Map map) { GeometryFactory factory = new GeometryFactory( new PackedCoordinateSequenceFactory(PackedCoordinateSequenceFactory.DOUBLE, 4)); Collection result = new ArrayList<>(); for (Geometry geom : geoms) { if (geom instanceof Point) { result.add(factory.createPoint(restoreDim4( ((Point) geom).getCoordinateSequence(), map))); } else if (geom instanceof LineString) { result.add(factory.createLineString(restoreDim4( ((LineString) geom).getCoordinateSequence(), map))); } else if (geom instanceof Polygon) { LinearRing outer = factory.createLinearRing(restoreDim4( ((Polygon) geom).getExteriorRing().getCoordinateSequence(), map)); LinearRing[] inner = new LinearRing[((Polygon) geom).getNumInteriorRing()]; for (int i = 0; i < ((Polygon) geom).getNumInteriorRing(); i++) { inner[i] = factory.createLinearRing(restoreDim4( ((Polygon) geom).getInteriorRingN(i).getCoordinateSequence(), map)); } result.add(factory.createPolygon(outer, inner)); } else { for (int i = 0; i < geom.getNumGeometries(); i++) { result.addAll(restoreDim4(Collections.singleton(geom.getGeometryN(i)), map)); } } } return result; } private void gatherDim4(Geometry geometry, Map map) { if (geometry instanceof Point) { gatherDim4(((Point) geometry).getCoordinateSequence(), map); } else if (geometry instanceof LineString) { gatherDim4(((LineString) geometry).getCoordinateSequence(), map); } else if (geometry instanceof Polygon) { Polygon polygon = (Polygon) geometry; gatherDim4(polygon.getExteriorRing().getCoordinateSequence(), map); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { gatherDim4(polygon.getInteriorRingN(i).getCoordinateSequence(), map); } } else { for (int i = 0; i < geometry.getNumGeometries(); i++) { gatherDim4(geometry.getGeometryN(i), map); } } } private void gatherDim4(CoordinateSequence cs, Map map) { if (cs.getDimension() == 4) { for (int i = 0; i < cs.size(); i++) { map.put(cs.getCoordinate(i), cs.getOrdinate(i, 3)); } } } // Use map to restore M values on the coordinate array private CoordinateSequence restoreDim4(CoordinateSequence cs, Map map) { CoordinateSequence seq = new PackedCoordinateSequenceFactory(DOUBLE, 4).create(cs.size(), 4); for (int i = 0; i < cs.size(); i++) { seq.setOrdinate(i, 0, cs.getOrdinate(i, 0)); seq.setOrdinate(i, 1, cs.getOrdinate(i, 1)); seq.setOrdinate(i, 2, cs.getOrdinate(i, 2)); Double d = map.get(cs.getCoordinate(i)); seq.setOrdinate(i, 3, d == null ? Double.NaN : d); } return seq; } /** * Nodes a LineString and returns a List of Noded LineString's. Used to * repare auto-intersecting LineString and Polygons. This method cannot * process CoordinateSequence. The noding process is limited to 3d * geometries.
* Preserves duplicate coordinates. * * @param coords coordinate array to be noded * @param gf geometryFactory to use * @return a list of noded LineStrings */ private Set nodeLineString(Coordinate[] coords, GeometryFactory gf) { MCIndexNoder noder = new MCIndexNoder(); noder.setSegmentIntersector(new IntersectionAdder(new RobustLineIntersector())); List list = new ArrayList<>(); list.add(new NodedSegmentString(coords, null)); noder.computeNodes(list); List lineStringList = new ArrayList<>(); for (Object segmentString : noder.getNodedSubstrings()) { lineStringList.add(gf.createLineString( ((NodedSegmentString) segmentString).getCoordinates() )); } // WARNING : merger loose original linestrings // It is useful for LinearRings but should not be used for (Multi)LineStrings LineMerger merger = new LineMerger(); merger.add(lineStringList); lineStringList = (List) merger.getMergedLineStrings(); // Remove duplicate linestrings preserving main orientation Set lineStringSet = new HashSet<>(); for (LineString line : lineStringList) { // TODO as equals makes a topological comparison, comparison with line.reverse maybe useless if (!lineStringSet.contains(line) && !lineStringSet.contains(line.reverse())) { lineStringSet.add(line); } } return lineStringSet; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy