Skip to content

Commit

Permalink
Fix LargestEmptyCircle to handle polygonal obstacles (#988)
Browse files Browse the repository at this point in the history
  • Loading branch information
dr-jts committed Jun 28, 2023
1 parent b4d5b49 commit d92f783
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 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.algorithm.construct;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.operation.distance.IndexedFacetDistance;

/**
* Computes the distance between a point and a geometry
* (which may be a collection containing any type of geometry).
* Also computes the pair of points containing the input
* point and the nearest point on the geometry.
*
* @author mdavis
*
*/
class IndexedDistanceToPoint {

private Geometry targetGeometry;
private IndexedFacetDistance facetDistance;
private IndexedPointInPolygonsLocater ptLocater;

public IndexedDistanceToPoint(Geometry geom) {
this.targetGeometry = geom;
}

private void init() {
if (facetDistance != null)
return;
facetDistance = new IndexedFacetDistance(targetGeometry);
ptLocater = new IndexedPointInPolygonsLocater(targetGeometry);
}

/**
* Computes the distance from a point to the geometry.
*
* @param pt the input point
* @return the distance to the geometry
*/
public double distance(Point pt) {
init();
//-- distance is 0 if point is inside a target polygon
if (isInArea(pt)) {
return 0;
}
return facetDistance.distance(pt);
}

private boolean isInArea(Point pt) {
return Location.EXTERIOR != ptLocater.locate(pt.getCoordinate());
}

/**
* Gets the nearest locations between the geometry and a point.
* The first location lies on the geometry,
* and the second location is the provided point.
*
* @param pt the point to compute the nearest location for
* @return a pair of locations
*/
public Coordinate[] nearestPoints(Point pt) {
init();
if (isInArea(pt)) {
Coordinate p = pt.getCoordinate();
return new Coordinate[] { p.copy(), p.copy() };
}
return facetDistance.nearestPoints(pt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2023 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.algorithm.construct;

import java.util.List;

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.Geometry;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.util.PolygonalExtracter;
import org.locationtech.jts.index.strtree.STRtree;

/**
* Determines the location of a point in the polygonal elements of a geometry.
* Uses spatial indexing to provide efficient performance.
*
* @author mdavis
*
*/
class IndexedPointInPolygonsLocater implements PointOnGeometryLocator {

private Geometry geom;
private List<Geometry> polys;
private STRtree index;

public IndexedPointInPolygonsLocater(Geometry geom) {
this.geom = geom;
}

private void init() {
if (polys != null)
return;
polys = PolygonalExtracter.getPolygonals(geom);
index = new STRtree();
for (int i = 0; i < polys.size(); i++) {
Geometry poly = polys.get(i);
index.insert(poly.getEnvelopeInternal(), new IndexedPointInAreaLocator(poly));
}
}

@Override
public int locate(Coordinate p) {
init();

List results = index.query(new Envelope(p));
for (int i = 0; i < results.size(); i++) {
IndexedPointInAreaLocator ptLocater = (IndexedPointInAreaLocator) results.get(i);
int loc = ptLocater.locate(p);
if (loc != Location.EXTERIOR)
return loc;
}
return Location.EXTERIOR;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
/**
* Constructs the Largest Empty Circle for a set
* of obstacle geometries, up to a given accuracy distance tolerance.
* The obstacles are point and line geometries.
* (Polygonal obstacles may be supplied, but only their boundaries are used.)
* The obstacles may be any combination of point, linear and polygonal geometries.
* <p>
* The Largest Empty Circle (LEC) is the largest circle
* whose interior does not intersect with any obstacle
Expand All @@ -45,18 +44,13 @@
* If it is not specified the convex hull of the obstacles is used as the boundary.
* <p>
* To compute an LEC which lies <i>wholly</i> within
* a polygonal boundary, include the boundary polygon as an obstacle as well.
* a polygonal boundary, include the boundary of the polygon(s) as an obstacle.
* <p>
* The implementation uses a successive-approximation technique
* over a grid of square cells covering the obstacles and boundary.
* The grid is refined using a branch-and-bound algorithm.
* Point containment and distance are computed in a performant
* way by using spatial indexes.
* <p>
* <h3>Future Enhancements</h3>
* <ul>
* <li>Support polygons as obstacles
* </ul>
*
* @author Martin Davis
*
Expand Down Expand Up @@ -131,8 +125,8 @@ public static LineString getRadiusLine(Geometry obstacles, Geometry boundary, do
private double tolerance;

private GeometryFactory factory;
private IndexedPointInAreaLocator ptLocater;
private IndexedFacetDistance obstacleDistance;
private IndexedDistanceToPoint obstacleDistance;
private IndexedPointInAreaLocator boundaryPtLocater;
private IndexedFacetDistance boundaryDistance;
private Envelope gridEnv;
private Cell farthestCell;
Expand Down Expand Up @@ -168,7 +162,7 @@ public LargestEmptyCircle(Geometry obstacles, Geometry boundary, double toleranc
this.boundary = boundary;
this.factory = obstacles.getFactory();
this.tolerance = tolerance;
obstacleDistance = new IndexedFacetDistance( obstacles );
obstacleDistance = new IndexedDistanceToPoint( obstacles );
}

/**
Expand Down Expand Up @@ -220,7 +214,7 @@ public LineString getRadiusLine() {
* @return the signed distance to the constraints (negative indicates outside the boundary)
*/
private double distanceToConstraints(Point p) {
boolean isOutide = Location.EXTERIOR == ptLocater.locate(p.getCoordinate());
boolean isOutide = Location.EXTERIOR == boundaryPtLocater.locate(p.getCoordinate());
if (isOutide) {
double boundaryDist = boundaryDistance.distance(p);
return -boundaryDist;
Expand All @@ -244,7 +238,7 @@ private void initBoundary() {
gridEnv = bounds.getEnvelopeInternal();
// if bounds does not enclose an area cannot create a ptLocater
if (bounds.getDimension() >= 2) {
ptLocater = new IndexedPointInAreaLocator( bounds );
boundaryPtLocater = new IndexedPointInAreaLocator( bounds );
boundaryDistance = new IndexedFacetDistance( bounds );
}
}
Expand All @@ -255,8 +249,8 @@ private void compute() {
// check if already computed
if (centerCell != null) return;

// if ptLocater is not present then result is degenerate (represented as zero-radius circle)
if (ptLocater == null) {
// if boundaryPtLocater is not present then result is degenerate (represented as zero-radius circle)
if (boundaryPtLocater == null) {
Coordinate pt = obstacles.getCoordinate();
centerPt = pt.copy();
centerPoint = factory.createPoint(pt);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 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.geom.util;

import java.util.ArrayList;
import java.util.List;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;

/**
* Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry}.
*/
public class PolygonalExtracter
{
/**
* Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry}
* and adds them to the provided list.
*
* @param geom the geometry from which to extract
* @param list the list to add the extracted elements to
*/
public static List<Geometry> getPolygonals(Geometry geom, List<Geometry> list)
{
if (geom instanceof Polygon || geom instanceof MultiPolygon) {
list.add(geom);
}
else if (geom instanceof GeometryCollection) {
for (int i = 0; i < geom.getNumGeometries(); i++) {
getPolygonals(geom.getGeometryN(i), list);
}
}
// skip non-Polygonal elemental geometries
return list;
}

/**
* Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry}
* and returns them in a list.
*
* @param geom the geometry from which to extract
*/
public static List<Geometry> getPolygonals(Geometry geom)
{
return getPolygonals(geom, new ArrayList<Geometry>());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,16 @@ public void testBoundaryMultiSquares() {
}

public void testBoundaryAsObstacle() {
checkCircle("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POINT (4 3), POINT (7 6))",
checkCircle("GEOMETRYCOLLECTION (LINESTRING (1 9, 9 9, 9 1, 1 1, 1 9), POINT (4 3), POINT (7 6))",
"POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))",
0.01, 4, 6, 3 );
}

public void testObstacleEmptyElement() {
checkCircle("GEOMETRYCOLLECTION (LINESTRING EMPTY, POINT (4 3), POINT (7 6), POINT (4 6))",
0.01, 5.5, 4.5, 2.12 );
}

//========================================================

/**
Expand Down

0 comments on commit d92f783

Please sign in to comment.