Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SEDONA-483] [SEDONA-491] Implement ST_IsPolygonCCW and ST_ForcePolygonCCW #1287

Merged
merged 7 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading
Loading