Skip to content

Commit

Permalink
[SEDONA-483] [SEDONA-491] Implement ST_IsPolygonCCW and ST_ForcePolyg…
Browse files Browse the repository at this point in the history
…onCCW (#1287)

* init commit

* add: ST_IsPolygonCCW and ST_ForcePolygonCCW

* docs: fix type

* fix: snowflake test 1

* fix: snowflake test 2

* fix: snowflake test 3

* fix: snowflake test 4
  • Loading branch information
furqaankhan committed Mar 25, 2024
1 parent 3efa8ed commit d347e74
Show file tree
Hide file tree
Showing 20 changed files with 451 additions and 1 deletion.
87 changes: 87 additions & 0 deletions common/src/main/java/org/apache/sedona/common/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.sedona.common.utils.H3Utils;
import org.apache.sedona.common.utils.S2Utils;
import org.locationtech.jts.algorithm.MinimumBoundingCircle;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.algorithm.hull.ConcaveHull;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.impl.CoordinateArraySequence;
Expand Down Expand Up @@ -816,6 +817,92 @@ public static double lineLocatePoint(Geometry geom, Geometry point)
return indexedLine.indexOf(point.getCoordinate()) / length;
}

/**
* Forces a Polygon/MultiPolygon to use counter-clockwise orientation for the exterior ring and a clockwise for the interior ring(s).
* @param geom
* @return a counter-clockwise orientated (Multi)Polygon
*/
public static Geometry forcePolygonCCW(Geometry geom) {
if (isPolygonCCW(geom)) {
return geom;
}

if (geom instanceof Polygon) {
return transformCCW((Polygon) geom);

} else if (geom instanceof MultiPolygon) {
List<Polygon> polygons = new ArrayList<>();
for (int i = 0; i < geom.getNumGeometries(); i++) {
Polygon polygon = (Polygon) geom.getGeometryN(i);
polygons.add((Polygon) transformCCW(polygon));
}

return new GeometryFactory().createMultiPolygon(polygons.toArray(new Polygon[0]));

}
// Non-polygonal geometries are returned unchanged
return geom;
}

private static Geometry transformCCW(Polygon polygon) {
LinearRing exteriorRing = polygon.getExteriorRing();
LinearRing exteriorRingEnforced = transformCCW(exteriorRing, true);

List<LinearRing> interiorRings = new ArrayList<>();
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
interiorRings.add(transformCCW(polygon.getInteriorRingN(i), false));
}

return new GeometryFactory(polygon.getPrecisionModel(), polygon.getSRID())
.createPolygon(exteriorRingEnforced, interiorRings.toArray(new LinearRing[0]));
}

private static LinearRing transformCCW(LinearRing ring, boolean isExteriorRing) {
boolean isRingCounterClockwise = Orientation.isCCW(ring.getCoordinateSequence());

LinearRing enforcedRing;
if (isExteriorRing) {
enforcedRing = isRingCounterClockwise ? (LinearRing) ring.copy() : ring.reverse();
} else {
enforcedRing = isRingCounterClockwise ? ring.reverse() : (LinearRing) ring.copy();
}
return enforcedRing;
}

/**
* This function accepts Polygon and MultiPolygon, if any other type is provided then it will return false.
* If the exterior ring is counter-clockwise and the interior ring(s) are clockwise then returns true, otherwise false.
* @param geom Polygon or MultiPolygon
* @return
*/
public static boolean isPolygonCCW(Geometry geom) {
if (geom instanceof MultiPolygon) {
MultiPolygon multiPolygon = (MultiPolygon) geom;

boolean arePolygonsCCW = checkIfPolygonCCW(((Polygon) multiPolygon.getGeometryN(0)));
for (int i = 1; i < multiPolygon.getNumGeometries(); i++) {
arePolygonsCCW = arePolygonsCCW && checkIfPolygonCCW((Polygon) multiPolygon.getGeometryN(i));
}
return arePolygonsCCW;
} else if (geom instanceof Polygon) {
return checkIfPolygonCCW((Polygon) geom);
}
// False for remaining geometry types
return false;
}

private static boolean checkIfPolygonCCW(Polygon geom) {
LinearRing exteriorRing = geom.getExteriorRing();
boolean isExteriorRingCCW = Orientation.isCCW(exteriorRing.getCoordinateSequence());

boolean isInteriorRingCCW = !Orientation.isCCW(geom.getInteriorRingN(0).getCoordinateSequence());
for (int i = 1; i < geom.getNumInteriorRing(); i++) {
isInteriorRingCCW = isInteriorRingCCW && !Orientation.isCCW(geom.getInteriorRingN(i).getCoordinateSequence());
}

return isExteriorRingCCW && isInteriorRingCCW;
}

public static Geometry difference(Geometry leftGeometry, Geometry rightGeometry) {
boolean isIntersects = leftGeometry.intersects(rightGeometry);
if (!isIntersects) {
Expand Down
47 changes: 47 additions & 0 deletions common/src/test/java/org/apache/sedona/common/FunctionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,53 @@ public void geometricMedian() throws Exception {
assertEquals(0, expected.compareTo(actual, COORDINATE_SEQUENCE_COMPARATOR));
}

@Test
public void testForcePolygonCCW() throws ParseException {
Geometry polyCW = Constructors.geomFromWKT("POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))", 0);
String actual = Functions.asWKT(Functions.forcePolygonCCW(polyCW));
String expected = "POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))";
assertEquals(expected, actual);

// both exterior ring and interior ring are counter-clockwise
polyCW = Constructors.geomFromWKT("POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))", 0);
actual = Functions.asWKT(Functions.forcePolygonCCW(polyCW));
expected = "POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))";
assertEquals(expected, actual);

Geometry mPoly = Constructors.geomFromWKT("MULTIPOLYGON (((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20)), ((40 40, 45 30, 20 45, 40 40)))", 0);
actual = Functions.asWKT(Functions.forcePolygonCCW(mPoly));
expected = "MULTIPOLYGON (((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)), ((40 40, 20 45, 45 30, 40 40)))";
assertEquals(expected, actual);

mPoly = Constructors.geomFromWKT("MULTIPOLYGON (((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 8 2, 8 8, 2 8, 2 2), (4 4, 6 4, 6 6, 4 6, 4 4), (6 6, 8 6, 8 8, 6 8, 6 6), (3 3, 4 3, 4 4, 3 4, 3 3), (5 5, 6 5, 6 6, 5 6, 5 5), (7 7, 8 7, 8 8, 7 8, 7 7)))", 0);
actual = Functions.asWKT(Functions.forcePolygonCCW(mPoly));
expected = "MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2), (4 4, 4 6, 6 6, 6 4, 4 4), (6 6, 6 8, 8 8, 8 6, 6 6), (3 3, 3 4, 4 4, 4 3, 3 3), (5 5, 5 6, 6 6, 6 5, 5 5), (7 7, 7 8, 8 8, 8 7, 7 7)))";
assertEquals(expected, actual);

Geometry nonPoly = Constructors.geomFromWKT("POINT (45 20)", 0);
actual = Functions.asWKT(Functions.forcePolygonCCW(nonPoly));
expected = Functions.asWKT(nonPoly);
assertEquals(expected, actual);
}

@Test
public void testIsPolygonCCW() throws ParseException {
Geometry polyCCW = Constructors.geomFromWKT("POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))", 0);
assertTrue(Functions.isPolygonCCW(polyCCW));

Geometry polyCW = Constructors.geomFromWKT("POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))", 0);
assertFalse(Functions.isPolygonCCW(polyCW));

Geometry mPoly = Constructors.geomFromWKT("MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2), (4 4, 4 6, 6 6, 6 4, 4 4), (6 6, 6 8, 8 8, 8 6, 6 6), (3 3, 3 4, 4 4, 4 3, 3 3), (5 5, 5 6, 6 6, 6 5, 5 5), (7 7, 7 8, 8 8, 8 7, 7 7)))", 0);
assertTrue(Functions.isPolygonCCW(mPoly));

Geometry point = Constructors.geomFromWKT("POINT (45 20)", 0);
assertFalse(Functions.isPolygonCCW(point));

Geometry lineClosed = Constructors.geomFromWKT("LINESTRING (30 20, 20 25, 20 15, 30 20)", 0);
assertFalse(Functions.isPolygonCCW(lineClosed));
}

@Test
public void geometricMedianTolerance() throws Exception {
MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPointFromCoords(
Expand Down
40 changes: 40 additions & 0 deletions docs/api/flink/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,26 @@ Output:
LINESTRING EMPTY
```

## ST_ForcePolygonCCW

Introduction: For (Multi)Polygon geometries, this function sets the exterior ring orientation to counter-clockwise and interior rings to clockwise orientation. Non-polygonal geometries are returned unchanged.

Format: `ST_ForcePolygonCCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_AsText(ST_ForcePolygonCCW(ST_GeomFromText('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))')))
```

Output:

```
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_FrechetDistance

Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries,
Expand Down Expand Up @@ -1630,6 +1650,26 @@ Output:
false
```

## ST_IsPolygonCCW

Introduction: Returns true if all polygonal components in the input geometry have their exterior rings oriented counter-clockwise and interior rings oriented clockwise.

Format: `ST_IsPolygonCCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_IsPolygonCCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))'))
```

Output:

```
true
```

## ST_IsRing

Introduction: RETURN true if LINESTRING is ST_IsClosed and ST_IsSimple.
Expand Down
36 changes: 36 additions & 0 deletions docs/api/snowflake/vector-data/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,24 @@ Input: `LINESTRING EMPTY`

Output: `LINESTRING EMPTY`

## ST_ForcePolygonCCW

Introduction: For (Multi)Polygon geometries, this function sets the exterior ring orientation to counter-clockwise and interior rings to clockwise orientation. Non-polygonal geometries are returned unchanged.

Format: `ST_ForcePolygonCCW(geom: Geometry)`

SQL Example:

```sql
SELECT ST_AsText(ST_ForcePolygonCCW(ST_GeomFromText('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))')))
```

Output:

```
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_FrechetDistance

Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries,
Expand Down Expand Up @@ -1168,6 +1186,24 @@ SELECT ST_IsEmpty(polygondf.countyshape)
FROM polygondf
```

## ST_IsPolygonCCW

Introduction: Returns true if all polygonal components in the input geometry have their exterior rings oriented counter-clockwise and interior rings oriented clockwise.

Format: `ST_IsPolygonCCW(geom: Geometry)`

SQL Example:

```sql
SELECT ST_IsPolygonCCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))'))
```

Output:

```
true
```

## ST_IsRing

Introduction: RETURN true if LINESTRING is ST_IsClosed and ST_IsSimple.
Expand Down
40 changes: 40 additions & 0 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,26 @@ Output:
LINESTRING EMPTY
```

## ST_ForcePolygonCCW

Introduction: For (Multi)Polygon geometries, this function sets the exterior ring orientation to counter-clockwise and interior rings to clockwise orientation. Non-polygonal geometries are returned unchanged.

Format: `ST_ForcePolygonCCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_AsText(ST_ForcePolygonCCW(ST_GeomFromText('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))')))
```

Output:

```
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_FrechetDistance

Introduction: Computes and returns discrete [Frechet Distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) between the given two geometries,
Expand Down Expand Up @@ -1638,6 +1658,26 @@ Output:
false
```

## ST_IsPolygonCCW

Introduction: Returns true if all polygonal components in the input geometry have their exterior rings oriented counter-clockwise and interior rings oriented clockwise.

Format: `ST_IsPolygonCCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_IsPolygonCCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))'))
```

Output:

```
true
```

## ST_IsRing

Introduction: RETURN true if LINESTRING is ST_IsClosed and ST_IsSimple.
Expand Down
2 changes: 2 additions & 0 deletions flink/src/main/java/org/apache/sedona/flink/Catalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_NumPoints(),
new Functions.ST_Force3D(),
new Functions.ST_NRings(),
new Functions.ST_IsPolygonCCW(),
new Functions.ST_ForcePolygonCCW(),
new Functions.ST_Translate(),
new Functions.ST_VoronoiPolygons(),
new Functions.ST_FrechetDistance(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,22 @@ public int eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.ge
}
}

public static class ST_ForcePolygonCCW extends ScalarFunction {
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) {
Geometry geometry = (Geometry) o;
return org.apache.sedona.common.Functions.forcePolygonCCW(geometry);
}
}

public static class ST_IsPolygonCCW extends ScalarFunction {
@DataTypeHint("Boolean")
public boolean eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.isPolygonCCW(geom);
}
}

public static class ST_Translate extends ScalarFunction {
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o,
Expand Down
19 changes: 19 additions & 0 deletions flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,25 @@ public void testNRings() {
assertEquals(expected, actual);
}

@Test
public void testForcePolygonCCW() {
Table polyTable = tableEnv.sqlQuery("SELECT ST_ForcePolygonCCW(ST_GeomFromWKT('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))')) AS polyCW");
String actual = (String) first(polyTable.select(call(Functions.ST_AsText.class.getSimpleName(), $("polyCW")))).getField(0);
String expected = "POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))";
assertEquals(expected, actual);
}

@Test
public void testIsPolygonCCW() {
Table polyTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))') AS polyCCW");
boolean actual = (boolean) first(polyTable.select(call(Functions.ST_IsPolygonCCW.class.getSimpleName(), $("polyCCW")))).getField(0);
assertTrue(actual);

polyTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))') AS polyCW");
actual = (boolean) first(polyTable.select(call(Functions.ST_IsPolygonCCW.class.getSimpleName(), $("polyCW")))).getField(0);
assertFalse(actual);
}

@Test
public void testTranslate() {
Table polyTable = tableEnv.sqlQuery("SELECT ST_Translate(ST_GeomFromWKT('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))'), 2, 5)" + "AS " + polygonColNames[0]);
Expand Down

0 comments on commit d347e74

Please sign in to comment.