diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Orientation.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Orientation.java
index 409f98439b..db98ee1259 100644
--- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Orientation.java
+++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Orientation.java
@@ -13,6 +13,7 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
/**
* Functions to compute the orientation of basic geometric structures
@@ -107,104 +108,45 @@ public static int index(Coordinate p1, Coordinate p2, Coordinate q)
* oriented counter-clockwise.
*
* - The list of points is assumed to have the first and last points equal.
- *
- This will handle coordinate lists which contain repeated points.
+ *
- This handles coordinate lists which contain repeated points.
+ *
- This handles rings which contain collapsed segments
+ * (in particular, along the top of the ring).
*
- * This algorithm is only guaranteed to work with valid rings. If the
- * ring is invalid (e.g. self-crosses or touches), the computed result may not
- * be correct.
+ * This algorithm is guaranteed to work with valid rings.
+ * It also works with "mildly invalid" rings
+ * which contain collapsed (coincident) flat segments along the top of the ring.
+ * If the ring is "more" invalid (e.g. self-crosses or touches),
+ * the computed result may not be correct.
*
- * @param ring
- * an array of Coordinates forming a ring
+ * @param ring an array of Coordinates forming a ring (with first and last point identical)
* @return true if the ring is oriented counter-clockwise.
- * @throws IllegalArgumentException
- * if there are too few points to determine orientation (< 4)
+ * @throws IllegalArgumentException if there are too few points to determine orientation (< 4)
*/
public static boolean isCCW(Coordinate[] ring)
{
- // # of points without closing endpoint
- int nPts = ring.length - 1;
- // sanity check
- if (nPts < 3)
- throw new IllegalArgumentException(
- "Ring has fewer than 4 points, so orientation cannot be determined");
-
- // find highest point
- Coordinate hiPt = ring[0];
- int hiIndex = 0;
- for (int i = 1; i <= nPts; i++) {
- Coordinate p = ring[i];
- if (p.y > hiPt.y) {
- hiPt = p;
- hiIndex = i;
- }
- }
-
- // find distinct point before highest point
- int iPrev = hiIndex;
- do {
- iPrev = iPrev - 1;
- if (iPrev < 0)
- iPrev = nPts;
- } while (ring[iPrev].equals2D(hiPt) && iPrev != hiIndex);
-
- // find distinct point after highest point
- int iNext = hiIndex;
- do {
- iNext = (iNext + 1) % nPts;
- } while (ring[iNext].equals2D(hiPt) && iNext != hiIndex);
-
- Coordinate prev = ring[iPrev];
- Coordinate next = ring[iNext];
-
- /*
- * This check catches cases where the ring contains an A-B-A configuration
- * of points. This can happen if the ring does not contain 3 distinct points
- * (including the case where the input array has fewer than 4 elements), or
- * it contains coincident line segments.
- */
- if (prev.equals2D(hiPt) || next.equals2D(hiPt) || prev.equals2D(next))
- return false;
-
- int disc = Orientation.index(prev, hiPt, next);
-
- /*
- * If disc is exactly 0, lines are collinear. There are two possible cases:
- * (1) the lines lie along the x axis in opposite directions (2) the lines
- * lie on top of one another
- *
- * (1) is handled by checking if next is left of prev ==> CCW (2) will never
- * happen if the ring is valid, so don't check for it (Might want to assert
- * this)
- */
- boolean isCCW;
- if (disc == 0) {
- // poly is CCW if prev x is right of next x
- isCCW = (prev.x > next.x);
- }
- else {
- // if area is positive, points are ordered CCW
- isCCW = (disc > 0);
- }
- return isCCW;
+ // wrap with an XY CoordinateSequence
+ return isCCW(new CoordinateArraySequence(ring, 2, 0));
}
/**
- * Computes whether a ring defined by an {@link CoordinateSequence} is
+ * Computes whether a ring defined by a {@link CoordinateSequence} is
* oriented counter-clockwise.
*
* - The list of points is assumed to have the first and last points equal.
- *
- This will handle coordinate lists which contain repeated points.
+ *
- This handles coordinate lists which contain repeated points.
+ *
- This handles rings which contain collapsed segments
+ * (in particular, along the top of the ring).
*
- * This algorithm is only guaranteed to work with valid rings. If the
- * ring is invalid (e.g. self-crosses or touches), the computed result may not
- * be correct.
- *
- * @param ring
- * a CoordinateSequence forming a ring
+ * This algorithm is guaranteed to work with valid rings.
+ * It also works with "mildly invalid" rings
+ * which contain collapsed (coincident) flat segments along the top of the ring.
+ * If the ring is "more" invalid (e.g. self-crosses or touches),
+ * the computed result may not be correct.
+ *
+ * @param ring a CoordinateSequence forming a ring (with first and last point identical)
* @return true if the ring is oriented counter-clockwise.
- * @throws IllegalArgumentException
- * if there are too few points to determine orientation (< 4)
- */
+ * @throws IllegalArgumentException if there are too few points to determine orientation (< 4)
+ */
public static boolean isCCW(CoordinateSequence ring)
{
// # of points without closing endpoint
@@ -212,67 +154,84 @@ public static boolean isCCW(CoordinateSequence ring)
// sanity check
if (nPts < 3)
throw new IllegalArgumentException(
- "Ring has fewer than 4 points, so orientation cannot be determined");
-
- // find highest point
- Coordinate hiPt = ring.getCoordinate(0);
- int hiIndex = 0;
+ "Ring has fewer than 4 points, so orientation cannot be determined");
+
+ /**
+ * Find first highest point after a lower point, if one exists
+ * (e.g. a rising segment)
+ * If one does not exist, hiIndex will remain 0
+ * and the ring must be flat.
+ * Note this relies on the convention that
+ * rings have the same start and end point.
+ */
+ Coordinate upHiPt = ring.getCoordinate(0);
+ double prevY = upHiPt.y;
+ Coordinate upLowPt = null;
+ int iUpHi = 0;
for (int i = 1; i <= nPts; i++) {
- Coordinate p = ring.getCoordinate(i);
- if (p.y > hiPt.y) {
- hiPt = p;
- hiIndex = i;
+ double py = ring.getOrdinate(i, Coordinate.Y);
+ /**
+ * If segment is upwards and endpoint is higher, record it
+ */
+ if (py > prevY && py >= upHiPt.y) {
+ upHiPt = ring.getCoordinate(i);
+ iUpHi = i;
+ upLowPt = ring.getCoordinate(i-1);
}
+ prevY = py;
}
-
- // find distinct point before highest point
- Coordinate prev;
- int iPrev = hiIndex;
- do {
- iPrev = iPrev - 1;
- if (iPrev < 0)
- iPrev = nPts;
- prev = ring.getCoordinate(iPrev);
- } while (prev.equals2D(hiPt) && iPrev != hiIndex);
-
- // find distinct point after highest point
- Coordinate next;
- int iNext = hiIndex;
- do {
- iNext = (iNext + 1) % nPts;
- next = ring.getCoordinate(iNext);
- } while (next.equals2D(hiPt) && iNext != hiIndex);
-
- /*
- * This check catches cases where the ring contains an A-B-A configuration
- * of points. This can happen if the ring does not contain 3 distinct points
- * (including the case where the input array has fewer than 4 elements), or
- * it contains coincident line segments.
+ /**
+ * Check if ring is flat and return default value if so
*/
- if (prev.equals2D(hiPt) || next.equals2D(hiPt) || prev.equals2D(next))
- return false;
-
- int disc = Orientation.index(prev, hiPt, next);
+ if (iUpHi == 0) return false;
+
+ /**
+ * Find the next lower point after the high point
+ * (e.g. a falling segment).
+ * This must exist since ring is not flat.
+ */
+ int iDownLow = iUpHi;
+ do {
+ iDownLow = (iDownLow + 1) % nPts;
+ } while (iDownLow != iUpHi && ring.getOrdinate(iDownLow, Coordinate.Y) == upHiPt.y );
- /*
- * If disc is exactly 0, lines are collinear. There are two possible cases:
- * (1) the lines lie along the x axis in opposite directions (2) the lines
- * lie on top of one another
- *
- * (1) is handled by checking if next is left of prev ==> CCW (2) will never
- * happen if the ring is valid, so don't check for it (Might want to assert
- * this)
+ Coordinate downLowPt = ring.getCoordinate(iDownLow);
+ int iDownHi = iDownLow > 0 ? iDownLow - 1 : nPts - 1;
+ Coordinate downHiPt = ring.getCoordinate(iDownHi);
+
+ /**
+ * Two cases can occur:
+ * 1) the hiPt and the downPrevPt are the same.
+ * This is the general position case of a "pointed cap".
+ * The ring orientation is determined by the orientation of the cap
+ * 2) The hiPt and the downPrevPt are different.
+ * In this case the top of the cap is flat.
+ * The ring orientation is given by the direction of the flat segment
*/
- boolean isCCW;
- if (disc == 0) {
- // poly is CCW if prev x is right of next x
- isCCW = (prev.x > next.x);
+ if (upHiPt.equals2D(downHiPt)) {
+ /**
+ * Check for the case where the cap has configuration A-B-A.
+ * This can happen if the ring does not contain 3 distinct points
+ * (including the case where the input array has fewer than 4 elements), or
+ * it contains coincident line segments.
+ */
+ if (upLowPt.equals2D(upHiPt) || downLowPt.equals2D(upHiPt) || upLowPt.equals2D(downLowPt))
+ return false;
+
+ /**
+ * It can happen that the top segments are coincident.
+ * This is an invalid ring, which cannot be computed correctly.
+ * In this case the orientation is 0, and the result is false.
+ */
+ int index = index(upLowPt, upHiPt, downLowPt);
+ return index == COUNTERCLOCKWISE;
}
else {
- // if area is positive, points are ordered CCW
- isCCW = (disc > 0);
+ /**
+ * Flat cap - direction of flat top determines orientation
+ */
+ double delX = downHiPt.x - upHiPt.x;
+ return delX < 0;
}
- return isCCW;
}
-
}
diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/IsCCWTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/IsCCWTest.java
index 5deaf5d4bf..0744e52e34 100644
--- a/modules/core/src/test/java/org/locationtech/jts/algorithm/IsCCWTest.java
+++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/IsCCWTest.java
@@ -20,14 +20,13 @@
import junit.framework.TestCase;
import junit.textui.TestRunner;
+import test.jts.GeometryTestCase;
/**
* Tests CGAlgorithms.isCCW
* @version 1.7
*/
-public class IsCCWTest extends TestCase {
-
- private WKTReader reader = new WKTReader();
+public class IsCCWTest extends GeometryTestCase {
public static void main(String args[]) {
TestRunner.run(IsCCWTest.class);
@@ -35,35 +34,96 @@ public static void main(String args[]) {
public IsCCWTest(String name) { super(name); }
- public void testCCW() throws Exception
- {
- Coordinate[] pts = getCoordinates("POLYGON ((60 180, 140 240, 140 240, 140 240, 200 180, 120 120, 60 180))");
- assertEquals(false, Orientation.isCCW(pts));
- CoordinateSequence seq = getCoordinateSequence("POLYGON ((60 180, 140 240, 140 240, 140 240, 200 180, 120 120, 60 180))");
- assertEquals(false, Orientation.isCCW(seq));
-
- Coordinate[] pts2 = getCoordinates("POLYGON ((60 180, 140 120, 100 180, 140 240, 60 180))");
- assertEquals(true, Orientation.isCCW(pts2));
- CoordinateSequence seq2 = getCoordinateSequence("POLYGON ((60 180, 140 120, 100 180, 140 240, 60 180))");
- assertEquals(true, Orientation.isCCW(seq2));
-
- // same pts list with duplicate top point - check that isCCW still works
- Coordinate[] pts2x = getCoordinates( "POLYGON ((60 180, 140 120, 100 180, 140 240, 140 240, 60 180))");
- assertEquals(true, Orientation.isCCW(pts2x) );
- CoordinateSequence seq2x = getCoordinateSequence("POLYGON ((60 180, 140 120, 100 180, 140 240, 140 240, 60 180))");
- assertEquals(true, Orientation.isCCW(seq2x) );
+ public void testTooFewPoints() {
+ Coordinate[] pts = new Coordinate[] {
+ new Coordinate(0, 0),
+ new Coordinate(1, 1),
+ new Coordinate(2, 2)
+ };
+ boolean hasError = false;
+ try {
+ boolean isCCW = Orientation.isCCW(pts);
+ }
+ catch (IllegalArgumentException ex) {
+ hasError = true;
+ }
+ assertTrue(hasError);
+ }
+
+ public void testCCW() {
+ checkOrientationCCW(true, "POLYGON ((60 180, 140 120, 100 180, 140 240, 60 180))");
+ }
+
+ public void testRingCW() {
+ checkOrientationCCW(false, "POLYGON ((60 180, 140 240, 100 180, 140 120, 60 180))");
+ }
+
+ public void testCCWSmall() {
+ checkOrientationCCW(true, "POLYGON ((1 1, 9 1, 5 9, 1 1))");
+ }
+
+ public void testDuplicateTopPoint() {
+ checkOrientationCCW(true, "POLYGON ((60 180, 140 120, 100 180, 140 240, 140 240, 60 180))");
+ }
+
+ public void testFlatTopSegment() {
+ checkOrientationCCW(false, "POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200))");
+ }
+
+ public void testFlatMultipleTopSegment() {
+ checkOrientationCCW(false, "POLYGON ((100 200, 127 200, 151 200, 173 200, 200 200, 100 100, 100 200))");
+ }
+
+ public void testDegenerateRingHorizontal() {
+ checkOrientationCCW(false, "POLYGON ((100 200, 100 200, 200 200, 100 200))");
+ }
+
+ public void testDegenerateRingAngled() {
+ checkOrientationCCW(false, "POLYGON ((100 100, 100 100, 200 200, 100 100))");
+ }
+
+ public void testDegenerateRingVertical() {
+ checkOrientationCCW(false, "POLYGON ((200 100, 200 100, 200 200, 200 100))");
+ }
+
+ /**
+ * This case is an invalid ring, so answer is a default value
+ */
+ public void testTopAngledSegmentCollapse() {
+ checkOrientationCCW(false, "POLYGON ((10 20, 61 20, 20 30, 50 60, 10 20))");
+ }
+
+ public void testABATopFlatSegmentCollapse() {
+ checkOrientationCCW(true, "POLYGON ((71 0, 40 40, 70 40, 40 40, 20 0, 71 0))");
+ }
+
+ public void testABATopFlatSegmentCollapseMiddleStart() {
+ checkOrientationCCW(true, "POLYGON ((90 90, 50 90, 10 10, 90 10, 50 90, 90 90))");
+ }
+
+ public void testMultipleTopFlatSegmentCollapseSinglePoint() {
+ checkOrientationCCW(true, "POLYGON ((100 100, 200 100, 150 200, 170 200, 200 200, 100 200, 150 200, 100 100))");
+ }
+
+ public void testMultipleTopFlatSegmentCollapseFlatTop() {
+ checkOrientationCCW(true, "POLYGON ((10 10, 90 10, 70 70, 90 70, 10 70, 30 70, 50 70, 10 10))");
+ }
+
+ private void checkOrientationCCW(boolean expectedCCW, String wkt) {
+ Coordinate[] pts2x = getCoordinates(wkt);
+ assertEquals("Coordinate array isCCW: ", expectedCCW, Orientation.isCCW(pts2x) );
+ CoordinateSequence seq2x = getCoordinateSequence(wkt);
+ assertEquals("CoordinateSequence isCCW: ", expectedCCW, Orientation.isCCW(seq2x) );
}
private Coordinate[] getCoordinates(String wkt)
- throws ParseException
{
- Geometry geom = reader.read(wkt);
+ Geometry geom = read(wkt);
return geom.getCoordinates();
}
private CoordinateSequence getCoordinateSequence(String wkt)
- throws ParseException
{
- Geometry geom = reader.read(wkt);
+ Geometry geom = read(wkt);
if (geom.getGeometryType() != "Polygon")
throw new IllegalArgumentException("wkt");
Polygon poly = (Polygon)geom;