/*
* The JTS Topology Suite is a collection of Java classes that
* implement the fundamental operations required to validate a given
* geo-spatial data set to a known topological specification.
*
* Copyright (C) 2001 Vivid Solutions
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.revolsys.geometry.operation.union;
import java.util.ArrayList;
import java.util.List;
import com.revolsys.collection.list.Lists;
import com.revolsys.geometry.index.strtree.STRtree;
import com.revolsys.geometry.model.BoundingBox;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.Polygon;
import com.revolsys.geometry.model.Polygonal;
import com.revolsys.geometry.model.TopologyException;
import com.revolsys.util.Property;
/**
* Provides an efficient method of unioning a collection of
* {@link Polygonal} geometrys.
* The geometries are indexed using a spatial index,
* and unioned recursively in index order.
* For geometries with a high degree of overlap,
* this has the effect of reducing the number of vertices
* early in the process, which increases speed
* and robustness.
* <p>
* This algorithm is faster and more robust than
* the simple iterated approach of
* repeatedly unioning each polygon to a result geometry.
* <p>
* The <tt>buffer(0)</tt> trick is sometimes faster, but can be less robust and
* can sometimes take a long time to complete.
* This is particularly the case where there is a high degree of overlap
* between the polygons. In this case, <tt>buffer(0)</tt> is forced to compute
* with <i>all</i> line segments from the outset,
* whereas cascading can eliminate many segments
* at each stage of processing.
* The best situation for using <tt>buffer(0)</tt> is the trivial case
* where there is <i>no</i> overlap between the input geometries.
* However, this case is likely rare in practice.
*
* @author Martin Davis
*
*/
public class CascadedPolygonUnion {
/**
* The effectiveness of the index is somewhat sensitive
* to the node capacity.
* Testing indicates that a smaller capacity is better.
* For an STRtree, 4 is probably a good number (since
* this produces 2x2 "squares").
*/
private static final int STRTREE_NODE_CAPACITY = 4;
/**
* Gets the element at a given list index, or
* null if the index is out of range.
*
* @param list
* @param index
* @return the geometry at the given index
* or null if the index is out of range
*/
private static Polygonal getPolygon(final List<Polygonal> list, final int index) {
if (index < 0 || index >= list.size()) {
return null;
} else {
return list.get(index);
}
}
/**
* Computes a {@link Polygonal} containing only {@link Polygonal} components.
* Extracts the {@link Polygon}s from the input
* and returns them as an appropriate {@link Polygonal} geometry.
* <p>
* If the input is already <tt>Polygonal</tt>, it is returned unchanged.
* <p>
* A particular use case is to filter out non-polygonal components
* returned from an overlay operation.
*
* @param geometry the geometry to filter
* @return a Polygonal geometry
*/
private static Polygonal restrictToPolygons(final Geometry geometry) {
if (geometry instanceof Polygonal) {
return (Polygonal)geometry;
} else {
final List<Polygon> polygons = geometry.getGeometries(Polygon.class);
if (polygons.size() == 1) {
return polygons.get(0);
}
final GeometryFactory geometryFactory = geometry.getGeometryFactory();
return geometryFactory.polygonal(polygons);
}
}
/**
* Computes the union of
* a collection of {@link Polygonal} {@link Polygonal}s.
*
* @param polygons a collection of {@link Polygonal} {@link Polygonal}s
*/
public static Polygonal union(final Iterable<? extends Polygonal> polygons) {
final CascadedPolygonUnion op = new CascadedPolygonUnion(polygons);
return op.union();
}
private GeometryFactory geometryFactory;
private List<Polygon> polygons = new ArrayList<>();
/**
* Creates a new instance to union
* the given collection of {@link Polygonal}s.
*
* @param polygons a collection of {@link Polygonal} {@link Polygonal}s
*/
public CascadedPolygonUnion(final Iterable<? extends Polygonal> polygons) {
if (polygons != null) {
for (final Polygonal polygonal : polygons) {
for (final Polygon polygon : polygonal.polygons()) {
this.polygons.add(polygon);
}
}
}
}
// ========================================================
/*
* The following methods are for experimentation only
*/
/**
* Unions a list of geometries
* by treating the list as a flattened binary tree,
* and performing a cascaded union on the tree.
*/
private Polygonal binaryUnion(final List<Polygonal> polygons) {
return binaryUnion(polygons, 0, polygons.size());
}
/**
* Unions a section of a list using a recursive binary union on each half
* of the section.
*
* @param polygons the list of geometries containing the section to union
* @param start the start index of the section
* @param end the index after the end of the section
* @return the union of the list section
*/
private Polygonal binaryUnion(final List<Polygonal> polygons, final int start, final int end) {
if (end - start <= 1) {
final Polygonal polygon1 = getPolygon(polygons, start);
return unionSafe(polygon1, null);
} else if (end - start == 2) {
final Polygonal polygon1 = getPolygon(polygons, start);
final Polygonal polygon2 = getPolygon(polygons, start + 1);
return unionSafe(polygon1, polygon2);
} else {
// recurse on both halves of the list
final int mid = (end + start) / 2;
final Polygonal polygon1 = binaryUnion(polygons, start, mid);
final Polygonal polygon2 = binaryUnion(polygons, mid, end);
return unionSafe(polygon1, polygon2);
}
}
// =======================================
private Polygonal extractByEnvelope(final BoundingBox envelope, final Polygonal polygonal,
final List<Polygonal> disjointGeoms) {
final List<Polygon> intersectingGeoms = new ArrayList<>();
for (final Polygon polygon : polygonal.polygons()) {
final BoundingBox boundingBox = polygon.getBoundingBox();
if (boundingBox.intersects(envelope)) {
intersectingGeoms.add(polygon);
} else {
disjointGeoms.add(polygon);
}
}
if (intersectingGeoms.isEmpty()) {
return null;
} else {
return this.geometryFactory.geometry(intersectingGeoms);
}
}
/**
* Reduces a tree of geometries to a list of geometries
* by recursively unioning the subtrees in the list.
*
* @param geomTree a tree-structured list of geometries
* @return a list of Geometrys
*/
private List<Polygonal> reduceToGeometries(final List<?> items) {
final List<Polygonal> geoms = new ArrayList<>();
for (final Object item : items) {
Polygonal polygon = null;
if (item instanceof List) {
final List<?> childItems = (List<?>)item;
polygon = unionTree(childItems);
} else if (item instanceof Polygonal) {
polygon = (Polygonal)item;
}
geoms.add(polygon);
}
return geoms;
}
/**
* Computes the union of the input geometries.
* <p>
* This method discards the input geometries as they are processed.
* In many input cases this reduces the memory retained
* as the operation proceeds.
* Optimal memory usage is achieved
* by disposing of the original input collection
* before calling this method.
*
* @return the union of the input geometries
* or null if no input geometries were provided
* @throws IllegalStateException if this method is called more than once
*/
public Polygonal union() {
if (this.polygons == null) {
throw new IllegalStateException("union() method cannot be called twice");
} else if (this.polygons.isEmpty()) {
return null;
} else {
this.geometryFactory = this.polygons.get(0).getGeometryFactory();
/**
* A spatial index to organize the collection
* into groups of close geometries.
* This makes unioning more efficient, since vertices are more likely
* to be eliminated on each round.
*/
final STRtree<Polygon> index = new STRtree<>(STRTREE_NODE_CAPACITY);
for (final Polygon polygon : this.polygons) {
final BoundingBox boundingBox = polygon.getBoundingBox();
index.insertItem(boundingBox, polygon);
}
this.polygons = null;
final List<?> itemTree = index.itemsTree();
final Polygonal unionAll = unionTree(itemTree);
return unionAll;
}
}
/**
* Encapsulates the actual unioning of two polygonal geometries.
*
* @param polygonal1
* @param polygonal2
* @return
*/
private Polygonal unionActual(final Polygonal polygonal1, final Polygonal polygonal2) {
if (Property.isEmpty(polygonal1)) {
return polygonal2;
} else if (Property.isEmpty(polygonal2)) {
return polygonal1;
} else {
try {
final Geometry union = polygonal1.union(polygonal2);
return restrictToPolygons(union);
} catch (final TopologyException e) {
final List<Polygon> polygons = new ArrayList<>();
Lists.addAll(polygons, polygonal1.polygons());
Lists.addAll(polygons, polygonal2.polygons());
return GeometryFactory.newGeometry(polygons);
}
}
}
private Polygonal unionOptimized(final Polygonal polygonal1, final Polygonal polygonal2) {
final BoundingBox boundingBox1 = polygonal1.getBoundingBox();
final BoundingBox boundingBox2 = polygonal2.getBoundingBox();
// *
if (!boundingBox1.intersects(boundingBox2)) {
final Polygonal polygonal = this.geometryFactory.geometry(polygonal1, polygonal2);
return polygonal;
} else if (polygonal1.getGeometryCount() <= 1 && polygonal2.getGeometryCount() <= 1) {
return unionActual(polygonal1, polygonal2);
} else {
final BoundingBox boundingBoxIntersection = boundingBox1.intersection(boundingBox2);
return unionUsingEnvelopeIntersection(polygonal1, polygonal2, boundingBoxIntersection);
}
}
/**
* Computes the union of two geometries,
* either or both of which may be null.
*
* @param polygonal1 a Geometry
* @param polygonal2 a Geometry
* @return the union of the input(s)
* or null if both inputs are null
*/
private Polygonal unionSafe(final Polygonal polygonal1, final Polygonal polygonal2) {
if (polygonal1 == null && polygonal2 == null) {
return null;
} else if (polygonal1 == null) {
return polygonal2;
} else if (polygonal2 == null) {
return polygonal1;
} else {
return unionOptimized(polygonal1, polygonal2);
}
}
/**
* Recursively unions all subtrees in the list into single geometries.
* The result is a list of Geometrys only
*/
private Polygonal unionTree(final List<?> items) {
final List<Polygonal> geoms = reduceToGeometries(items);
final Polygonal union = binaryUnion(geoms);
return union;
}
/**
* Unions two polygonal geometries, restricting computation
* to the envelope intersection where possible.
* The case of MultiPolygons is optimized to union only
* the polygons which lie in the intersection of the two geometry's envelopes.
* Polygons outside this region can simply be combined with the union result,
* which is potentially much faster.
* This case is likely to occur often during cascaded union, and may also
* occur in real world data (such as unioning data for parcels on different street blocks).
*
* @param polygonal1 a polygonal geometry
* @param polygonal2 a polygonal geometry
* @param common the intersection of the envelopes of the inputs
* @return the union of the inputs
*/
private Polygonal unionUsingEnvelopeIntersection(final Polygonal polygonal1,
final Polygonal polygonal2, final BoundingBox common) {
final List<Polygonal> disjointPolygons = new ArrayList<>();
final Polygonal newPolygonal1 = extractByEnvelope(common, polygonal1, disjointPolygons);
final Polygonal newPolygonal2 = extractByEnvelope(common, polygonal2, disjointPolygons);
final Polygonal union = unionActual(newPolygonal1, newPolygonal2);
disjointPolygons.add(union);
final Polygonal overallUnion = this.geometryFactory.geometry(disjointPolygons);
return overallUnion;
}
}