Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CoveragePolygonValidator section performance optimization #1053

Merged
merged 2 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading