OverlayArea.java
/*
* Copyright (c) 2020 Martin Davis
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.operation.overlayarea;
import java.util.List;
import org.locationtech.jts.algorithm.Area;
import org.locationtech.jts.algorithm.LineIntersector;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.algorithm.RobustLineIntersector;
import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator;
import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator;
import org.locationtech.jts.algorithm.locate.SimplePointInAreaLocator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFilter;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.index.ItemVisitor;
import org.locationtech.jts.index.kdtree.KdNode;
import org.locationtech.jts.index.kdtree.KdTree;
import org.locationtech.jts.index.strtree.STRtree;
import org.locationtech.jts.math.MathUtil;
/**
* Computes the area of the overlay of two polygons without forming
* the actual topology of the overlay.
* Since the topology is not needed, the computation is
* is insensitive to the fine details of the overlay topology,
* and hence is fully robust.
* It also allows for a simpler implementation with more aggressive
* performance optimization.
* <p>
* The algorithm uses mathematics derived from the work of William R. Franklin.
* The area of a polygon can be computed as a sum of the partial areas
* computed for each {@link EdgeVector} of the polygon.
* This allows the area of the intersection of two polygons to be computed
* by summing the partial areas for the edge vectors of the intersection resultant.
* To determine the edge vectors all that is required
* is to compute the vertices of the intersection resultant,
* along with the direction (not the length) of the edges they belong to.
* The resultant vertices are the vertices where the edges of the inputs intersect,
* along with the vertices of each input which lie in the interior of the other input.
* The direction of the edge vectors is the same as the parent edges from which they derive.
* Determining the vertices of intersection is simpler and more robust
* than determining the values of the actual edge line segments in the overlay result.
*
* @author Martin Davis
*
*/
public class OverlayArea {
public static double intersectionArea(Geometry geom0, Geometry geom1) {
if (! interacts(geom0, geom1))
return 0;
OverlayArea area = new OverlayArea(geom0);
return area.intersectionArea(geom1);
}
private static boolean interacts(Geometry geom0, Geometry geom1) {
return geom0.getEnvelopeInternal().intersects(geom1.getEnvelopeInternal());
}
private static LineIntersector li = new RobustLineIntersector();
private Geometry geom0;
private Envelope geomEnv0;
private IndexedPointInAreaLocator locator0;
private STRtree indexSegs;
private KdTree vertexIndex;
public OverlayArea(Geometry geom) {
this.geom0 = geom;
//TODO: handle holes and multipolygons
if (! (geom0 instanceof Polygon
&& ((Polygon) geom0).getNumInteriorRing() == 0))
throw new IllegalArgumentException("Currently only Polygons with no holes supported");
geomEnv0 = geom.getEnvelopeInternal();
locator0 = new IndexedPointInAreaLocator(geom);
indexSegs = buildSegmentIndex(geom);
vertexIndex = buildVertexIndex(geom);
}
private boolean interacts(Geometry geom) {
return geomEnv0.intersects(geom.getEnvelopeInternal());
}
public double intersectionArea(Geometry geom) {
//-- intersection area is 0 if geom does not interact with geom0
if (! interacts(geom)) return 0;
PolygonAreaFilter filter = new PolygonAreaFilter();
geom.apply(filter);
return filter.area;
}
private class PolygonAreaFilter implements GeometryFilter {
double area = 0;
@Override
public void filter(Geometry geom) {
if (geom instanceof Polygon) {
area += intersectionAreaPolygon((Polygon) geom);
}
}
}
private double intersectionAreaPolygon(Polygon geom) {
//-- optimization - intersection area is 0 if geom does not interact with geom0
if (! interacts(geom)) return 0;
double area = 0;
area += intersectionArea(geom.getExteriorRing());
for (int i = 0; i < geom.getNumInteriorRing(); i++) {
LinearRing hole = geom.getInteriorRingN(i);
// skip holes which do not interact
if (interacts(hole)) {
area -= intersectionArea(hole);
}
}
return area;
}
private double intersectionArea(LinearRing geom) {
double areaInt = areaForIntersections(geom);
/**
* If area for segment intersections is zero then no segments intersect.
* This means that either the geometries are disjoint,
* OR one is inside the other.
* This allows computing the area efficiently
* using a simple inside/outside test
*/
if (areaInt == 0.0) {
return areaContainedOrDisjoint(geom);
}
/**
* The geometries intersect, so add areas for interior vertices
*/
double areaVert1 = areaForInteriorVertices(geom);
IndexedPointInAreaLocator locator1 = new IndexedPointInAreaLocator(geom);
double areaVert0 = areaForInteriorVerticesIndexed(geom0, vertexIndex, geom.getEnvelopeInternal(), locator1);
return (areaInt + areaVert1 + areaVert0) / 2;
}
/**
* Computes the area for the situation where the geometries are known to either
* be disjoint, or have one contained in the other.
*
* @param geom the other geometry to intersect
* @return the area of the contained geometry, or 0.0 if disjoint
*/
private double areaContainedOrDisjoint(LinearRing geom) {
double area0 = areaForContainedGeom(geom, geom0.getEnvelopeInternal(), locator0);
// if area is non-zero then geom is contained in geom0
if (area0 != 0.0) return area0;
// only checking one point, so non-indexed is faster
SimplePointInAreaLocator locator = new SimplePointInAreaLocator(geom);
double area1 = areaForContainedGeom(geom0, geom.getEnvelopeInternal(), locator);
// geom0 is either disjoint or contained - either way we are done
return area1;
}
/**
* Tests and computes the area of a geometry contained in the other,
* or 0.0 if the geometry is disjoint.
*
* @param geom
* @param env
* @param locator
* @return the area of the contained geometry, or 0 if it is disjoint
*/
private double areaForContainedGeom(Geometry geom, Envelope env, PointOnGeometryLocator locator) {
Coordinate pt = geom.getCoordinate();
// fast check for disjoint
if (! env.covers(pt)) return 0.0;
// full check for contained
if (Location.INTERIOR != locator.locate(pt)) return 0.0;
return area(geom);
}
private static double area(Geometry geom) {
if (geom instanceof LinearRing) {
return Area.ofRing( ((LinearRing) geom).getCoordinateSequence() );
}
return geom.getArea();
}
private double areaForIntersections(LinearRing geom) {
double area = 0.0;
CoordinateSequence seq = geom.getCoordinateSequence();
boolean isCCW = Orientation.isCCW(seq);
// Compute rays for all intersections
for (int j = 0; j < seq.size() - 1; j++) {
Coordinate b0 = seq.getCoordinate(j);
Coordinate b1 = seq.getCoordinate(j+1);
if (isCCW) {
// flip segment orientation
Coordinate temp = b0; b0 = b1; b1 = temp;
}
Envelope env = new Envelope(b0, b1);
IntersectionVisitor intVisitor = new IntersectionVisitor(b0, b1);
indexSegs.query(env, intVisitor);
area += intVisitor.getArea();
}
return area;
}
class IntersectionVisitor implements ItemVisitor {
double area = 0.0;
private Coordinate b0;
private Coordinate b1;
IntersectionVisitor(Coordinate b0, Coordinate b1) {
this.b0 = b0;
this.b1 = b1;
}
double getArea() {
return area;
}
public void visitItem(Object item) {
LineSegment seg = (LineSegment) item;
area += areaForIntersection(seg.p0, seg.p1, b0, b1);
}
}
private static double areaForIntersection(Coordinate a0, Coordinate a1, Coordinate b0, Coordinate b1 ) {
// TODO: can the intersection computation be optimized?
li.computeIntersection(a0, a1, b0, b1);
if (! li.hasIntersection()) return 0.0;
/**
* An intersection creates two edge vectors which contribute to the area.
*
* With both rings oriented CW (effectively)
* There are two situations for segment intersection:
*
* 1) A entering B, B exiting A => rays are IP->A1:R, IP->B0:L
* 2) A exiting B, B entering A => rays are IP->A0:L, IP->B1:R
* (where IP is the intersection point,
* and :L/R indicates result polygon interior is to the Left or Right).
*
* For accuracy the full edge is used to provide the direction vector.
*/
Coordinate intPt = li.getIntersection(0);
boolean isAenteringB = Orientation.COUNTERCLOCKWISE == Orientation.index(a0, a1, b1);
if ( isAenteringB ) {
return EdgeVector.area2Term(intPt, a0, a1, true)
+ EdgeVector.area2Term(intPt, b1, b0, false);
}
else {
return EdgeVector.area2Term(intPt, a1, a0, false)
+ EdgeVector.area2Term(intPt, b0, b1, true);
}
}
private double areaForInteriorVertices(LinearRing ring) {
/**
* Compute rays originating at vertices inside the intersection result
* (i.e. A vertices inside B, and B vertices inside A)
*/
double area = 0.0;
CoordinateSequence seq = ring.getCoordinateSequence();
boolean isCW = ! Orientation.isCCW(seq);
for (int i = 0; i < seq.size()-1; i++) {
Coordinate v = seq.getCoordinate(i);
// quick bounda check
if (! geomEnv0.contains(v)) continue;
// is this vertex in interior of intersection result?
if (Location.INTERIOR == locator0.locate(v)) {
Coordinate vPrev = i == 0 ? seq.getCoordinate(seq.size()-2) : seq.getCoordinate(i-1);
Coordinate vNext = seq.getCoordinate(i+1);
area += EdgeVector.area2Term(v, vPrev, ! isCW)
+ EdgeVector.area2Term(v, vNext, isCW);
}
}
return area;
}
private double areaForInteriorVerticesIndexed(Geometry geom, KdTree vertexIndex, Envelope env, IndexedPointInAreaLocator locator) {
/**
* Compute rays originating at vertices inside the intersection result
* (i.e. A vertices inside B, and B vertices inside A)
*/
double area = 0.0;
CoordinateSequence seq = getVertices(geom);
boolean isCW = ! Orientation.isCCW(seq);
List<KdNode> verts = vertexIndex.query(env);
for (KdNode kdNode : verts) {
int i = (Integer) kdNode.getData();
Coordinate v = seq.getCoordinate(i);
// is this vertex in interior of intersection result?
if (Location.INTERIOR == locator.locate(v)) {
Coordinate vPrev = i == 0 ? seq.getCoordinate(seq.size()-2) : seq.getCoordinate(i-1);
Coordinate vNext = seq.getCoordinate(i+1);
area += EdgeVector.area2Term(v, vPrev, ! isCW)
+ EdgeVector.area2Term(v, vNext, isCW);
}
}
return area;
}
private static CoordinateSequence getVertices(Geometry geom) {
Polygon poly = (Polygon) geom;
CoordinateSequence seq = poly.getExteriorRing().getCoordinateSequence();
return seq;
}
private static STRtree buildSegmentIndex(Geometry geom) {
Coordinate[] coords = geom.getCoordinates();
boolean isCCW = Orientation.isCCW(coords);
STRtree index = new STRtree();
for (int i = 0; i < coords.length - 1; i++) {
Coordinate a0 = coords[i];
Coordinate a1 = coords[i+1];
LineSegment seg = new LineSegment(a0, a1);
if (isCCW) {
seg = new LineSegment(a1, a0);
}
Envelope env = new Envelope(a0, a1);
index.insert(env, seg);
}
return index;
}
private static KdTree buildVertexIndex(Geometry geom) {
Coordinate[] coords = geom.getCoordinates();
KdTree index = new KdTree();
//-- don't insert duplicate last vertex
int[] ints = MathUtil.shuffle(coords.length - 1);
//Arrays.sort(ints);
for (int i : ints) {
index.insert(coords[i], i);
}
//System.out.println("Depth = " + index.depth() + " size = " + index.size());
return index;
}
}