Skip to content

Commit

Permalink
Add CoveragePolygonValidator section performance optimization (#1053)
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts committed May 21, 2024
1 parent 20374b9 commit 88303d0
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2022 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.coverage;

import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator;
import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Polygon;

class CoveragePolygon {

private Polygon polygon;
private Envelope polyEnv;
IndexedPointInAreaLocator locator;

public CoveragePolygon(Polygon poly) {
this.polygon = poly;
polyEnv = polygon.getEnvelopeInternal();
}

public boolean intersects(Envelope env) {
//-- test intersection explicitly to avoid expensive null check
//return polyEnv.intersects(env);
return ! (env.getMinX() > polyEnv.getMaxX()
|| env.getMaxX() < polyEnv.getMinX()
|| env.getMinY() > polyEnv.getMaxY()
|| env.getMaxY() < polyEnv.getMinY());
}

public boolean contains(Coordinate p) {
//-- test intersection explicitly to avoid expensive null check
if (! intersects(p))
return false;
PointOnGeometryLocator pia = getLocator();
return Location.INTERIOR == pia.locate(p);
}

private boolean intersects(Coordinate p) {
return ! (p.x > polyEnv.getMaxX() ||
p.x < polyEnv.getMinX() ||
p.y > polyEnv.getMaxY() ||
p.y < polyEnv.getMinY());
}

private PointOnGeometryLocator getLocator() {
if (locator == null) {
locator = new IndexedPointInAreaLocator(polygon);
}
return locator;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
*
*/
public class CoveragePolygonValidator {

/**
* Validates that a polygon is coverage-valid against the
* surrounding polygons in a polygonal coverage.
Expand Down Expand Up @@ -122,8 +122,7 @@ public static Geometry validate(Geometry targetPolygon, Geometry[] adjPolygons,
private double gapWidth = 0.0;
private GeometryFactory geomFactory;
private Geometry[] adjGeoms;
private List<Polygon> adjPolygons;
private IndexedPointInAreaLocator[] adjPolygonLocators;
private List<CoveragePolygon> adjCovPolygons;

/**
* Create a new validator.
Expand Down Expand Up @@ -158,8 +157,8 @@ public void setGapWidth(double gapWidth) {
* @return a linear geometry containing the segments causing invalidity (if any)
*/
public Geometry validate() {
adjPolygons = extractPolygons(adjGeoms);
adjPolygonLocators = new IndexedPointInAreaLocator[adjPolygons.size()];
List<Polygon> adjPolygons = extractPolygons(adjGeoms);
adjCovPolygons = toCoveragePolygons(adjPolygons);

List<CoverageRing> targetRings = CoverageRing.createRings(targetGeom);
List<CoverageRing> adjRings = CoverageRing.createRings(adjPolygons);
Expand All @@ -177,6 +176,14 @@ public Geometry validate() {
return createInvalidLines(targetRings);
}

private List<CoveragePolygon> toCoveragePolygons(List<Polygon> polygons) {
List<CoveragePolygon> covPolys = new ArrayList<CoveragePolygon>();
for (Polygon poly : polygons) {
covPolys.add(new CoveragePolygon(poly));
}
return covPolys;
}

private void checkTargetRings(List<CoverageRing> targetRings, List<CoverageRing> adjRings, Envelope targetEnv) {
markMatchedSegments(targetRings, adjRings, targetEnv);

Expand All @@ -197,7 +204,8 @@ private void checkTargetRings(List<CoverageRing> targetRings, List<CoverageRing>
* Do further checks to see if any of them are are invalid.
*/
markInvalidInteractingSegments(targetRings, adjRings, gapWidth);
markInvalidInteriorSegments(targetRings, adjPolygons);
markInvalidInteriorSegments(targetRings, adjCovPolygons);
//OLDmarkInvalidInteriorSegments(targetRings, adjPolygons);
}

private static List<Polygon> extractPolygons(Geometry[] geoms) {
Expand Down Expand Up @@ -368,77 +376,79 @@ private void markInvalidInteractingSegments(List<CoverageRing> targetRings, List
segSetMutInt.process(adjRings, detector);
}

/**
* Stride is chosen experimentally to provide good performance
*/
private static final int RING_SECTION_STRIDE = 1000;

/**
* Marks invalid target segments which are fully interior
* to an adjacent polygon.
*
* @param targetRings the rings with segments to test
* @param adjPolygons the adjacent polygons
* @param adjCovPolygons the adjacent polygons
*/
private void markInvalidInteriorSegments(List<CoverageRing> targetRings, List<Polygon> adjPolygons) {
private void markInvalidInteriorSegments(List<CoverageRing> targetRings, List<CoveragePolygon> adjCovPolygons) {
for (CoverageRing ring : targetRings) {
for (int i = 0; i < ring.size() - 1; i++) {
//-- skip check for segments with known state.
if (ring.isKnown(i))
continue;
int stride = RING_SECTION_STRIDE;
for (int i = 0; i < ring.size() - 1; i += stride) {
int iEnd = i + stride;
if (iEnd >= ring.size())
iEnd = ring.size() - 1;

/**
* Check if vertex is in interior of an adjacent polygon.
* If so, the segments on either side are in the interior.
* Mark them invalid, unless they are already matched.
*/
Coordinate p = ring.getCoordinate(i);
if (isInteriorVertex(p, adjPolygons)) {
ring.markInvalid(i);
//-- previous segment may be interior (but may also be matched)
int iPrev = i == 0 ? ring.size() - 2 : i-1;
if (! ring.isKnown(iPrev))
ring.markInvalid(iPrev);
}
markInvalidInteriorSection(ring, i, iEnd, adjCovPolygons);
}
}
}

/**
* Tests if a coordinate is in the interior of some adjacent polygon.
* Uses the cached Point-In-Polygon indexed locators, for performance.
* Marks invalid target segments in a section which are interior
* to an adjacent polygon.
* Processing a section at a time dramatically improves efficiency.
* Due to the coherent organization of polygon rings,
* sections usually have a high spatial locality.
* This means that sections typically intersect only a few or often no adjacent polygons.
* The section envelope can be computed and tested against adjacent polygon envelopes quickly.
* The section can be skipped entirely if it does not interact with any polygons.
*
* @param p the coordinate to test
* @param adjPolygons the list of polygons
* @return true if the point is in the interior
* @param ring
* @param iStart
* @param iEnd
* @param adjPolygons
*/
private boolean isInteriorVertex(Coordinate p, List<Polygon> adjPolygons) {
/**
* There should not be too many adjacent polygons,
* and hopefully not too many segments with unknown status
* so a linear scan should not be too inefficient
*/
//TODO: try a spatial index?
for (int i = 0; i < adjPolygons.size(); i++) {
Polygon adjPoly = adjPolygons.get(i);

if (polygonContainsPoint(i, adjPoly, p))
return true;
private void markInvalidInteriorSection(CoverageRing ring, int iStart, int iEnd, List<CoveragePolygon> adjPolygons) {
Envelope sectionEnv = ring.getEnvelope(iStart, iEnd);
//TODO: is it worth indexing polygons?
for (CoveragePolygon adjPoly : adjPolygons) {
if (adjPoly.intersects(sectionEnv)) {
//-- test vertices in section
for (int i = iStart; i < iEnd; i++) {
markInvalidInteriorSegment(ring, i, adjPoly);
}
}
}
return false;
}

private boolean polygonContainsPoint(int index, Polygon poly, Coordinate pt) {
if (! poly.getEnvelopeInternal().intersects(pt))
return false;
PointOnGeometryLocator pia = getLocator(index, poly);
return Location.INTERIOR == pia.locate(pt);
}

private PointOnGeometryLocator getLocator(int index, Polygon poly) {
IndexedPointInAreaLocator loc = adjPolygonLocators[index];
if (loc == null) {
loc = new IndexedPointInAreaLocator(poly);
adjPolygonLocators[index] = loc;
private void markInvalidInteriorSegment(CoverageRing ring, int i, CoveragePolygon adjPoly) {
//-- skip check for segments with known state.
if (ring.isKnown(i))
return;

/**
* Check if vertex is in interior of an adjacent polygon.
* If so, the segments on either side are in the interior.
* Mark them invalid, unless they are already matched.
*/
Coordinate p = ring.getCoordinate(i);
if (adjPoly.contains(p)) {
ring.markInvalid(i);
//-- previous segment may be interior (but may also be matched)
int iPrev = i == 0 ? ring.size() - 2 : i-1;
if (! ring.isKnown(iPrev))
ring.markInvalid(iPrev);
}
return loc;
}

private Geometry createInvalidLines(List<CoverageRing> rings) {
List<LineString> lines = new ArrayList<LineString>();
for (CoverageRing ring : rings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateArrays;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
Expand Down Expand Up @@ -92,6 +93,14 @@ private CoverageRing(Coordinate[] pts, boolean isInteriorOnRight) {
isMatched = new boolean[size() - 1];
}

public Envelope getEnvelope(int start, int end) {
Envelope env = new Envelope();
for (int i = start; i < end; i++) {
env.expandToInclude(getCoordinate(i));
}
return env;
}

/**
* Reports if the ring has canonical orientation,
* with the polygon interior on the right (shell is CW).
Expand Down

0 comments on commit 88303d0

Please sign in to comment.