Skip to content

Commit

Permalink
[SEDONA-484] [SEDONA-492] Implement ST_IsPolygonCW and ST_ForcePolygo…
Browse files Browse the repository at this point in the history
…nCW (#1286)

* feat: add ST_IsPolygonCW and ST_ForcePolygonCW

* fix: snowflake tests and add spark dataframe api tests

* fix: snowflake tests

* fix: snowflake tests

* fix: snowflake tests

* fix: bug and add tests for MultiPolygon and Non-polygonal geometries

* fix: snowflake tests

* fix: snowflake tests

* docs: add docs for all 3 apis

* fix: snowflake test 1

* fix: snowflake test 2

* fix: lint issues
  • Loading branch information
furqaankhan committed Mar 26, 2024
1 parent d347e74 commit 7a49ef7
Show file tree
Hide file tree
Showing 20 changed files with 448 additions and 0 deletions.
86 changes: 86 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 @@ -810,6 +810,92 @@ public static Geometry lineInterpolatePoint(Geometry geom, double fraction) {
return GEOMETRY_FACTORY.createPoint(interPoint);
}

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

if (geom instanceof Polygon) {
return transformCW((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) transformCW(polygon));
}

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

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

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

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

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

private static LinearRing transformCW(LinearRing ring, boolean isExteriorRing) {
boolean isRingClockwise = !Orientation.isCCW(ring.getCoordinateSequence());

LinearRing enforcedRing;
if (isExteriorRing) {
enforcedRing = isRingClockwise ? (LinearRing) ring.copy() : ring.reverse();
} else {
enforcedRing = isRingClockwise ? 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 clockwise and the interior ring(s) are counter-clockwise then returns true, otherwise false.
* @param geom Polygon or MultiPolygon
* @return
*/
public static boolean isPolygonCW(Geometry geom) {
if (geom instanceof MultiPolygon) {
MultiPolygon multiPolygon = (MultiPolygon) geom;

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

private static boolean checkIfPolygonCW(Polygon geom) {
LinearRing exteriorRing = geom.getExteriorRing();
boolean isExteriorRingCW = !Orientation.isCCW(exteriorRing.getCoordinateSequence());

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

return isExteriorRingCW && isInteriorRingCW;
}

public static double lineLocatePoint(Geometry geom, Geometry point)
{
double length = geom.getLength();
Expand Down
45 changes: 45 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 @@ -690,6 +690,34 @@ public void geometricMedian() throws Exception {
}

@Test
public void testForcePolygonCW() 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);
String actual = Functions.asWKT(Functions.forcePolygonCW(polyCCW));
String expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))";
assertEquals(expected, actual);

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

Geometry mPoly = Constructors.geomFromWKT("MULTIPOLYGON (((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 25, 20 15, 30 20)), ((40 40, 20 45, 45 30, 40 40)))", 0);
actual = Functions.asWKT(Functions.forcePolygonCW(mPoly));
expected = "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)))";
assertEquals(expected, actual);

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);
actual = Functions.asWKT(Functions.forcePolygonCW(mPoly));
expected = "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)))";
assertEquals(expected, actual);

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

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));
Expand Down Expand Up @@ -719,6 +747,23 @@ public void testForcePolygonCCW() throws ParseException {
}

@Test
public void testIsPolygonCW() 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);
assertFalse(Functions.isPolygonCW(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);
assertTrue(Functions.isPolygonCW(polyCW));

Geometry 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);
assertTrue(Functions.isPolygonCW(mPoly));

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

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

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));
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 @@ -1241,6 +1241,26 @@ Output:
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_ForcePolygonCW

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

Format: `ST_ForcePolygonCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

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

Output:

```
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -1670,6 +1690,26 @@ Output:
true
```

## ST_IsPolygonCW

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_IsPolygonCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_IsPolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -960,6 +960,24 @@ Output:
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_ForcePolygonCW

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

Format: `ST_ForcePolygonCW(geom: Geometry)`

SQL Example:

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

Output:

```
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -1204,6 +1222,24 @@ Output:
true
```

## ST_IsPolygonCW

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_IsPolygonCW(geom: Geometry)`

SQL Example:

```sql
SELECT ST_IsPolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -1248,6 +1248,26 @@ Output:
POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20))
```

## ST_ForcePolygonCW

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

Format: `ST_ForcePolygonCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

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

Output:

```
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -1678,6 +1698,26 @@ Output:
true
```

## ST_IsPolygonCW

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_IsPolygonCW(geom: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_IsPolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 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 @@ -110,6 +110,7 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_SetSRID(),
new Functions.ST_SRID(),
new Functions.ST_IsClosed(),
new Functions.ST_IsPolygonCW(),
new Functions.ST_IsRing(),
new Functions.ST_IsSimple(),
new Functions.ST_IsValid(),
Expand Down Expand Up @@ -139,6 +140,7 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_GeometricMedian(),
new Functions.ST_NumPoints(),
new Functions.ST_Force3D(),
new Functions.ST_ForcePolygonCW(),
new Functions.ST_NRings(),
new Functions.ST_IsPolygonCCW(),
new Functions.ST_ForcePolygonCCW(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,14 @@ public boolean eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jt
}
}

public static class ST_IsPolygonCW 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.isPolygonCW(geom);
}
}

public static class ST_IsRing extends ScalarFunction {
@DataTypeHint("Boolean")
public boolean eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) {
Expand Down Expand Up @@ -999,6 +1007,14 @@ public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.j
}
}

public static class ST_ForcePolygonCW 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.forcePolygonCW(geometry);
}
}

public static class ST_NRings extends ScalarFunction {
@DataTypeHint(value = "Integer")
public int eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) throws Exception {
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 @@ -1187,6 +1187,25 @@ public void testForce3DDefaultValue() {
assertEquals(expectedDims, actual);
}

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

@Test
public void testIsPolygonCW() {
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_IsPolygonCW.class.getSimpleName(), $("polyCCW")))).getField(0);
assertFalse(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_IsPolygonCW.class.getSimpleName(), $("polyCW")))).getField(0);
assertTrue(actual);
}

@Test
public void testNRings() {
Integer expected = 1;
Expand Down
Loading

0 comments on commit 7a49ef7

Please sign in to comment.