Skip to content

Commit 2eeb684

Browse files
authored
Add MaximumInscribedCircle fast exact calculation for simple shapes (#1123)
1 parent a5ff7ba commit 2eeb684

File tree

6 files changed

+319
-5
lines changed

6 files changed

+319
-5
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright (c) 2020 Martin Davis.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
7+
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
8+
* and the Eclipse Distribution License is available at
9+
*
10+
* http://www.eclipse.org/org/documents/edl-v10.php.
11+
*/
12+
package org.locationtech.jts.algorithm.construct;
13+
14+
import org.locationtech.jts.algorithm.Angle;
15+
import org.locationtech.jts.algorithm.Orientation;
16+
import org.locationtech.jts.geom.Coordinate;
17+
import org.locationtech.jts.geom.CoordinateArrays;
18+
import org.locationtech.jts.geom.CoordinateSequence;
19+
import org.locationtech.jts.geom.Geometry;
20+
import org.locationtech.jts.geom.LineSegment;
21+
import org.locationtech.jts.geom.LinearRing;
22+
import org.locationtech.jts.geom.Polygon;
23+
import org.locationtech.jts.geom.Triangle;
24+
25+
/**
26+
* Computes the Maximum Inscribed Circle for some kinds of convex polygons.
27+
* It determines the circle center point by computing Voronoi node points
28+
* and testing them for distance to generating edges.
29+
* This is more precise than iterated approximation,
30+
* and faster for small polygons (such as triangles and convex quadrilaterals).
31+
*
32+
* @author Martin Davis
33+
*
34+
*/
35+
class ExactMaxInscribedCircle {
36+
37+
/**
38+
* Tests whether a given geometry is supported by this class.
39+
* Currently only triangles and convex quadrilaterals are supported.
40+
*
41+
* @param geom an areal geometry
42+
* @return true if the geometry shape can be evaluated
43+
*/
44+
public static boolean isSupported(Geometry geom) {
45+
if (! isSimplePolygon(geom))
46+
return false;
47+
Polygon polygon = (Polygon) geom;
48+
if (isTriangle(polygon))
49+
return true;
50+
if (isQuadrilateral(polygon) && isConvex(polygon))
51+
return true;
52+
return false;
53+
}
54+
55+
private static boolean isSimplePolygon(Geometry geom) {
56+
return geom instanceof Polygon
57+
&& ((Polygon) geom).getNumInteriorRing() == 0;
58+
}
59+
60+
private static boolean isTriangle(Polygon polygon) {
61+
return polygon.getNumPoints() == 4;
62+
}
63+
64+
private static boolean isQuadrilateral(Polygon polygon) {
65+
return polygon.getNumPoints() == 5;
66+
}
67+
68+
public static Coordinate[] computeRadius(Polygon polygon) {
69+
Coordinate[] ring = polygon.getExteriorRing().getCoordinates();
70+
if (ring.length == 4)
71+
return computeTriangle(ring);
72+
else if (ring.length == 5)
73+
return computeConvexQuadrilateral(ring);
74+
throw new IllegalArgumentException("Input must be a triangle or convex quadrilateral");
75+
}
76+
77+
private static Coordinate[] computeTriangle(Coordinate[] ring) {
78+
Coordinate center = Triangle.inCentre(ring[0], ring[1], ring[2]);
79+
LineSegment seg = new LineSegment(ring[0], ring[1]);
80+
Coordinate radius = seg.project(center);
81+
return new Coordinate[] { center, radius };
82+
}
83+
84+
/**
85+
* The Voronoi nodes of a convex polygon occur at the intersection point
86+
* of two bisectors of each triplet of edges.
87+
* The Maximum Inscribed Circle center is the node
88+
* is the farthest distance from the generating edges.
89+
* For a quadrilateral there are 4 distinct edge triplets,
90+
* at each edge with its adjacent edges.
91+
*
92+
* @param ring the polygon ring
93+
* @return an array containing the incircle center and radius points
94+
*/
95+
private static Coordinate[] computeConvexQuadrilateral(Coordinate[] ring) {
96+
Coordinate[] ringCW = CoordinateArrays.orient(ring, true);
97+
98+
double diameter = CoordinateArrays.envelope(ringCW).getDiameter();
99+
100+
//-- compute corner bisectors
101+
LineSegment[] bisector = computeBisectors(ringCW, diameter);
102+
//-- compute nodes and find interior one farthest from sides
103+
double maxDist = -1;
104+
Coordinate center = null;
105+
Coordinate radius = null;
106+
for (int i = 0; i < 4; i++) {
107+
LineSegment b1 = bisector[i];
108+
int i2 = (i + 1) % 4;
109+
LineSegment b2 = bisector[i2];
110+
111+
Coordinate nodePt = b1.intersection(b2);
112+
113+
//-- only interior nodes are considered
114+
if (! isPointInConvexRing(ringCW, nodePt)) {
115+
continue;
116+
}
117+
118+
//-- check if node is further than current max center
119+
Coordinate r = nearestEdgePt(ringCW, nodePt);
120+
double dist = nodePt.distance(r);
121+
if (maxDist < 0 || dist > maxDist) {
122+
center = nodePt;
123+
radius = r;
124+
maxDist = dist;
125+
//System.out.println(WKTWriter.toLineString(center, radius));
126+
}
127+
}
128+
return new Coordinate[] { center, radius };
129+
}
130+
131+
private static LineSegment[] computeBisectors(Coordinate[] ptsCW, double diameter) {
132+
LineSegment[] bisector = new LineSegment[4];
133+
for (int i = 0; i < 4; i++) {
134+
bisector[i] = computeConvexBisector(ptsCW, i, diameter);
135+
}
136+
return bisector;
137+
}
138+
139+
private static Coordinate nearestEdgePt(Coordinate[] ring, Coordinate pt) {
140+
Coordinate nearestPt = null;
141+
double minDist = -1;
142+
for (int i = 0; i < ring.length - 1; i++) {
143+
LineSegment edge = new LineSegment(ring[i], ring[i + 1]);
144+
Coordinate r = edge.closestPoint(pt);
145+
double dist = pt.distance(r);
146+
if (minDist < 0 || dist < minDist) {
147+
minDist = dist;
148+
nearestPt = r;
149+
}
150+
}
151+
return nearestPt;
152+
}
153+
154+
private static LineSegment computeConvexBisector(Coordinate[] pts, int index, double len) {
155+
Coordinate basePt = pts[index];
156+
int iPrev = index == 0 ? pts.length - 2 : index - 1;
157+
int iNext = index >= pts.length ? 0 : index + 1;
158+
Coordinate pPrev = pts[iPrev];
159+
Coordinate pNext = pts[iNext];
160+
if (! isConvex(pPrev, basePt, pNext))
161+
throw new IllegalArgumentException("Input is not convex");
162+
double bisectAng = Angle.bisector(pPrev, basePt, pNext);
163+
Coordinate endPt = Angle.project(basePt, bisectAng, len);
164+
return new LineSegment(basePt.copy(), endPt);
165+
}
166+
167+
private static boolean isConvex(Polygon polygon) {
168+
LinearRing shell = polygon.getExteriorRing();
169+
return isConvex(shell.getCoordinateSequence());
170+
}
171+
172+
private static boolean isConvex(CoordinateSequence ring) {
173+
/**
174+
* A ring cannot be all concave, so if it has a consistent
175+
* orientation it must be convex.
176+
*/
177+
int n = ring.size();
178+
if (n < 4)
179+
return false;
180+
int ringOrient = 0;
181+
for (int i = 0; i < n - 1; i++) {
182+
int i1 = i + 1;
183+
int i2 = (i1 >= n - 1) ? 1 : i1 + 1;
184+
int orient = Orientation.index(ring.getCoordinate(i),
185+
ring.getCoordinate(i1), ring.getCoordinate(i2));
186+
if (orient == Orientation.COLLINEAR)
187+
continue;
188+
if (ringOrient == 0) {
189+
ringOrient = orient;
190+
}
191+
else if (orient != ringOrient) {
192+
return false;
193+
}
194+
}
195+
return true;
196+
}
197+
198+
private static boolean isConvex(Coordinate p0, Coordinate p1, Coordinate p2) {
199+
return Orientation.CLOCKWISE == Orientation.index(p0, p1, p2);
200+
}
201+
202+
private static boolean isPointInConvexRing(Coordinate[] ringCW, Coordinate p) {
203+
for (int i = 0; i < ringCW.length - 1; i++) {
204+
Coordinate p0 = ringCW[i];
205+
Coordinate p1 = ringCW[i + 1];
206+
int orient = Orientation.index(p0, p1, p);
207+
if (orient == Orientation.COUNTERCLOCKWISE)
208+
return false;
209+
}
210+
return true;
211+
}
212+
}

modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ public MaximumInscribedCircle(Geometry polygonal, double tolerance) {
144144
this.inputGeom = polygonal;
145145
this.factory = polygonal.getFactory();
146146
this.tolerance = tolerance;
147-
ptLocater = new IndexedPointInAreaLocator(polygonal);
148-
indexedDistance = new IndexedFacetDistance( polygonal.getBoundary() );
149147
}
150148

151149
/**
@@ -210,7 +208,39 @@ private double distanceToBoundary(double x, double y) {
210208

211209
private void compute() {
212210
// check if already computed
213-
if (centerCell != null) return;
211+
if (centerPt != null) return;
212+
213+
/**
214+
* Handle empty or flat geometries.
215+
*/
216+
if (inputGeom.getArea() == 0.0) {
217+
Coordinate c = inputGeom.getCoordinate().copy();
218+
createResult(c, c.copy());
219+
return;
220+
}
221+
222+
/**
223+
* Optimization for small simple convex polygons
224+
*/
225+
if (ExactMaxInscribedCircle.isSupported(inputGeom)) {
226+
Coordinate[] centreRadius = ExactMaxInscribedCircle.computeRadius((Polygon) inputGeom);
227+
createResult(centreRadius[0], centreRadius[1]);
228+
return;
229+
}
230+
231+
computeApproximation();
232+
}
233+
234+
private void createResult(Coordinate c, Coordinate r) {
235+
centerPt = c;
236+
radiusPt = r;
237+
centerPoint = factory.createPoint(centerPt);
238+
radiusPoint = factory.createPoint(radiusPt);
239+
}
240+
241+
private void computeApproximation() {
242+
ptLocater = new IndexedPointInAreaLocator(inputGeom);
243+
indexedDistance = new IndexedFacetDistance( inputGeom.getBoundary() );
214244

215245
// Priority queue of cells, ordered by maximum distance from boundary
216246
PriorityQueue<Cell> cellQueue = new PriorityQueue<>();

modules/core/src/main/java/org/locationtech/jts/geom/CoordinateArrays.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Collection;
1616
import java.util.Comparator;
1717

18+
import org.locationtech.jts.algorithm.Orientation;
1819
import org.locationtech.jts.math.MathUtil;
1920

2021

@@ -192,6 +193,24 @@ public static Coordinate ptNotInList(Coordinate[] testPts, Coordinate[] pts) {
192193
return null;
193194
}
194195

196+
/**
197+
* Orients an array of points to have a specified orientation.
198+
* The array is not copied if it already has the required orientation.
199+
* The points are not copied.
200+
*
201+
* @param pts the array to orient
202+
* @param orientCW true if the points should be oriented CVW
203+
* @return the oriented array
204+
*/
205+
public static Coordinate[] orient(Coordinate[] pts, boolean orientCW) {
206+
boolean isFlipped = orientCW == Orientation.isCCW(pts);
207+
if (isFlipped) {
208+
pts = pts.clone();
209+
CoordinateArrays.reverse(pts);
210+
}
211+
return pts;
212+
}
213+
195214
/**
196215
* Compares two {@link Coordinate} arrays
197216
* in the forward direction of their coordinates,

modules/core/src/main/java/org/locationtech/jts/geom/Triangle.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ private static double det(double m00, double m01, double m10, double m11)
261261
* the point which is equidistant from the sides of the triangle. It is also
262262
* the point at which the bisectors of the triangle's angles meet. It is the
263263
* centre of the triangle's <i>incircle</i>, which is the unique circle that
264-
* is tangent to each of the triangle's three sides.
264+
* is tangent to each of the triangle's three sides
265+
* (and hence the maximum inscribed circle).
265266
* <p>
266267
* The incentre always lies within the triangle.
267268
*

modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,41 @@ public static void main(String args[]) {
1515

1616
public MaximumInscribedCircleTest(String name) { super(name); }
1717

18+
public void testTriangleRight() {
19+
checkCircle("POLYGON ((1 1, 1 9, 9 1, 1 1))",
20+
0.001, 3.343, 3.343, 2.343 );
21+
}
22+
23+
public void testTriangleObtuse() {
24+
checkCircle("POLYGON ((1 1, 1 9, 2 2, 1 1))",
25+
0.001, 1.485, 2.173, 0.485 );
26+
}
27+
1828
public void testSquare() {
1929
checkCircle("POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200))",
2030
0.001, 150, 150, 50 );
2131
}
2232

33+
public void testThinQuad() {
34+
checkCircle("POLYGON ((1 2, 9 3, 9 1, 1 1, 1 2))",
35+
0.001, 8.0623, 1.9377, 0.93774 );
36+
}
37+
2338
public void testDiamond() {
2439
checkCircle("POLYGON ((150 250, 50 150, 150 50, 250 150, 150 250))",
2540
0.001, 150, 150, 70.71 );
2641
}
2742

43+
public void testChevron() {
44+
checkCircle("POLYGON ((1 1, 6 9, 3.7 2.5, 9 1, 1 1))",
45+
0.001, 2.82, 2.008, 1.008 );
46+
}
47+
48+
public void testChevronFat() {
49+
checkCircle("POLYGON ((1 1, 6 9, 5.9 5, 9 1, 1 1))",
50+
0.001, 4.7545, 3.0809, 2.081 );
51+
}
52+
2853
public void testCircle() {
2954
Geometry centre = read("POINT (100 100)");
3055
Geometry circle = centre.buffer(100, 20);
@@ -65,7 +90,7 @@ public void testCollapsedLineFlat() {
6590
}
6691

6792
/**
68-
* Invalid polygon collapsed to a point
93+
* Invalid triangle polygon collapsed to a point
6994
*/
7095
public void testCollapsedPoint() {
7196
checkCircle("POLYGON ((100 100, 100 100, 100 100, 100 100))",

modules/core/src/test/java/org/locationtech/jts/geom/CoordinateArraysTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
package org.locationtech.jts.geom;
1313

1414

15+
import org.locationtech.jts.algorithm.Orientation;
16+
1517
import junit.textui.TestRunner;
1618
import test.jts.GeometryTestCase;
1719

@@ -174,6 +176,31 @@ public void testEnforceConsistency(){
174176
assertTrue( array[1] != fixed[1] ); // processing needed to CoordinateXYZM
175177
}
176178

179+
public void testOrientCW() {
180+
checkOrient("POLYGON ((1 1, 9 9, 9 1, 1 1))");
181+
}
182+
183+
public void testOrientCCW() {
184+
checkOrient("POLYGON ((9 7, 5 9, 1 4, 5 4, 4 1, 8 1, 9 7))");
185+
}
186+
187+
private void checkOrient(String wkt) {
188+
Coordinate[] pts = read(wkt).getCoordinates();
189+
//-- orient CW
190+
Coordinate[] ptsCW = CoordinateArrays.orient(pts, true);
191+
assertEquals(false, Orientation.isCCW(ptsCW));
192+
Coordinate[] ptsCCW = CoordinateArrays.orient(pts, false);
193+
assertEquals(true, Orientation.isCCW(ptsCCW));
194+
//-- check that original is unchanged for same orientation
195+
boolean isCCW = Orientation.isCCW(pts);
196+
if (isCCW) {
197+
assertTrue(pts == ptsCCW);
198+
}
199+
else {
200+
assertTrue(pts == ptsCW);
201+
}
202+
}
203+
177204
private static void checkCoordinateAt(Coordinate[] seq1, int pos1,
178205
Coordinate[] seq2, int pos2) {
179206
Coordinate c1 = seq1[pos1], c2 = seq2[pos2];

0 commit comments

Comments
 (0)