Skip to content
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
60 changes: 56 additions & 4 deletions common/src/main/java/org/apache/sedona/common/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,12 @@ public static boolean isRing(Geometry geometry) {
&& geometry.isSimple();
}

public static boolean isLineStringCCW(Geometry geometry) {
return geometry instanceof LineString
&& geometry.getNumPoints() >= 4
&& Orientation.isCCW(((LineString) geometry).getCoordinateSequence());
}

public static boolean isSimple(Geometry geometry) {
return new IsSimpleOp(geometry).isSimple();
}
Expand Down Expand Up @@ -1544,12 +1550,21 @@ public static double perimeter(Geometry geometry) {

/**
* Forces a Polygon/MultiPolygon to use clockwise orientation for the exterior ring and a
* counter-clockwise for the interior ring(s).
* counter-clockwise for the interior ring(s). Polygonal members inside GeometryCollections are
* transformed recursively.
*
* @param geom
* @return a clockwise orientated (Multi)Polygon
* @return the geometry with polygonal components oriented clockwise
*/
public static Geometry forcePolygonCW(Geometry geom) {
if (geom == null || geom.isEmpty()) {
return geom;
}

if (isPlainGeometryCollection(geom)) {
return forcePolygonOrientationInCollection((GeometryCollection) geom, true);
}

if (isPolygonCW(geom)) {
return geom;
}
Expand Down Expand Up @@ -1658,12 +1673,21 @@ public static Geometry locateAlong(Geometry linear, double measure) {

/**
* Forces a Polygon/MultiPolygon to use counter-clockwise orientation for the exterior ring and a
* clockwise for the interior ring(s).
* clockwise for the interior ring(s). Polygonal members inside GeometryCollections are
* transformed recursively.
*
* @param geom
* @return a counter-clockwise orientated (Multi)Polygon
* @return the geometry with polygonal components oriented counter-clockwise
*/
public static Geometry forcePolygonCCW(Geometry geom) {
if (geom == null || geom.isEmpty()) {
return geom;
}

if (isPlainGeometryCollection(geom)) {
return forcePolygonOrientationInCollection((GeometryCollection) geom, false);
}

if (isPolygonCCW(geom)) {
return geom;
}
Expand All @@ -1684,6 +1708,34 @@ public static Geometry forcePolygonCCW(Geometry geom) {
return geom;
}

private static boolean isPlainGeometryCollection(Geometry geometry) {
return geometry instanceof GeometryCollection
&& !(geometry instanceof MultiPoint)
&& !(geometry instanceof MultiLineString)
&& !(geometry instanceof MultiPolygon);
}

private static Geometry forcePolygonOrientationInCollection(
GeometryCollection collection, boolean clockwise) {
Geometry[] orientedGeometries = new Geometry[collection.getNumGeometries()];
boolean changed = false;
for (int i = 0; i < collection.getNumGeometries(); i++) {
Geometry geometry = collection.getGeometryN(i);
Geometry orientedGeometry = clockwise ? forcePolygonCW(geometry) : forcePolygonCCW(geometry);
orientedGeometries[i] = orientedGeometry;
changed |= orientedGeometry != geometry;
}

if (!changed) {
return collection;
}
GeometryCollection orientedCollection =
collection.getFactory().createGeometryCollection(orientedGeometries);
orientedCollection.setSRID(collection.getSRID());
orientedCollection.setUserData(collection.getUserData());
return orientedCollection;
}

private static Geometry transformCCW(Polygon polygon) {
LinearRing exteriorRing = polygon.getExteriorRing();
LinearRing exteriorRingEnforced = transformCCW(exteriorRing, true);
Expand Down
133 changes: 133 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 @@ -1772,6 +1772,139 @@ public void testForcePolygonCCW() throws ParseException {
assertEquals(expected, actual);
}

@Test
public void testForcePolygonCWRecursesIntoNestedGeometryCollections() throws ParseException {
Geometry source =
Constructors.geomFromWKT(
"GEOMETRYCOLLECTION ("
+ "POINT (9 9), "
+ "GEOMETRYCOLLECTION ("
+ "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)), "
+ "LINESTRING (3 3, 4 4), "
+ "GEOMETRYCOLLECTION EMPTY, "
+ "MULTIPOLYGON (((10 0, 12 0, 12 2, 10 2, 10 0)))), "
+ "POLYGON ((20 0, 22 0, 22 2, 20 2, 20 0)))",
4326);

Geometry result = Functions.forcePolygonCW(source);

assertNestedCollectionOrientation(source, result, true);
}

@Test
public void testForcePolygonCCWRecursesIntoNestedGeometryCollections() throws ParseException {
Geometry source =
Constructors.geomFromWKT(
"GEOMETRYCOLLECTION ("
+ "POINT (9 9), "
+ "GEOMETRYCOLLECTION ("
+ "POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0)), "
+ "LINESTRING (3 3, 4 4), "
+ "GEOMETRYCOLLECTION EMPTY, "
+ "MULTIPOLYGON (((10 0, 10 2, 12 2, 12 0, 10 0)))), "
+ "POLYGON ((20 0, 20 2, 22 2, 22 0, 20 0)))",
4326);

Geometry result = Functions.forcePolygonCCW(source);

assertNestedCollectionOrientation(source, result, false);
}

private void assertNestedCollectionOrientation(
Geometry source, Geometry result, boolean clockwise) {
assertTrue(result instanceof GeometryCollection);
assertEquals(source.getSRID(), result.getSRID());
assertEquals(source.getNumGeometries(), result.getNumGeometries());

assertEquals(source.getGeometryN(0), result.getGeometryN(0));
assertTrue(result.getGeometryN(1) instanceof GeometryCollection);
assertTrue(result.getGeometryN(2) instanceof Polygon);

GeometryCollection sourceNested = (GeometryCollection) source.getGeometryN(1);
GeometryCollection resultNested = (GeometryCollection) result.getGeometryN(1);
assertEquals(sourceNested.getSRID(), resultNested.getSRID());
assertEquals(sourceNested.getNumGeometries(), resultNested.getNumGeometries());

assertTrue(resultNested.getGeometryN(0) instanceof Polygon);
assertEquals(sourceNested.getGeometryN(1), resultNested.getGeometryN(1));
assertEquals(sourceNested.getGeometryN(2), resultNested.getGeometryN(2));
assertTrue(resultNested.getGeometryN(2).isEmpty());
assertTrue(resultNested.getGeometryN(3) instanceof MultiPolygon);

Geometry nestedPolygon = resultNested.getGeometryN(0);
Geometry nestedMultiPolygon = resultNested.getGeometryN(3);
Geometry topLevelPolygon = result.getGeometryN(2);
assertEquals(source.getSRID(), nestedPolygon.getSRID());
assertEquals(source.getSRID(), nestedMultiPolygon.getSRID());
assertEquals(source.getSRID(), topLevelPolygon.getSRID());

if (clockwise) {
assertTrue(Functions.isPolygonCW(nestedPolygon));
assertTrue(Functions.isPolygonCW(nestedMultiPolygon));
assertTrue(Functions.isPolygonCW(topLevelPolygon));
assertTrue(Functions.isPolygonCCW(sourceNested.getGeometryN(0)));
assertTrue(Functions.isPolygonCCW(sourceNested.getGeometryN(3)));
assertTrue(Functions.isPolygonCCW(source.getGeometryN(2)));
} else {
assertTrue(Functions.isPolygonCCW(nestedPolygon));
assertTrue(Functions.isPolygonCCW(nestedMultiPolygon));
assertTrue(Functions.isPolygonCCW(topLevelPolygon));
assertTrue(Functions.isPolygonCW(sourceNested.getGeometryN(0)));
assertTrue(Functions.isPolygonCW(sourceNested.getGeometryN(3)));
assertTrue(Functions.isPolygonCW(source.getGeometryN(2)));
}
}

@Test
public void testForcePolygonOrientationPreservesCollectionMetadata() throws ParseException {
Geometry source =
Constructors.geomFromWKT(
"GEOMETRYCOLLECTION ("
+ "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)), "
+ "POLYGON ((10 0, 10 2, 12 2, 12 0, 10 0)))",
0);
source.setSRID(4326);
source.setUserData("metadata");

Geometry clockwise = Functions.forcePolygonCW(source);
Geometry counterClockwise = Functions.forcePolygonCCW(source);

assertEquals(0, source.getFactory().getSRID());
assertEquals(4326, clockwise.getSRID());
assertEquals(4326, counterClockwise.getSRID());
assertEquals("metadata", clockwise.getUserData());
assertEquals("metadata", counterClockwise.getUserData());
}

@Test
public void testForcePolygonOrientationPreservesNull() {
assertNull(Functions.forcePolygonCW(null));
assertNull(Functions.forcePolygonCCW(null));
}

@Test
public void testIsLineStringCCW() throws ParseException {
Geometry closedCCW = Constructors.geomFromWKT("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)", 0);
Geometry closedCW = Constructors.geomFromWKT("LINESTRING (0 0, 0 1, 1 1, 1 0, 0 0)", 0);
Geometry openCCW = Constructors.geomFromWKT("LINESTRING (0 0, 1 0, 1 1, 0 1)", 0);
Geometry asymmetricOpen = Constructors.geomFromWKT("LINESTRING (0 1, 0 -1, -1 -2, 3 -2)", 0);
Geometry asymmetricClosed =
Constructors.geomFromWKT("LINESTRING (0 1, 0 -1, -1 -2, 3 -2, 0 1)", 0);

assertTrue(Functions.isLineStringCCW(closedCCW));
assertFalse(Functions.isLineStringCCW(closedCW));
assertTrue(Functions.isLineStringCCW(openCCW));
assertFalse(Functions.isLineStringCCW(asymmetricOpen));
assertTrue(Functions.isLineStringCCW(asymmetricClosed));
assertFalse(
Functions.isLineStringCCW(Constructors.geomFromWKT("LINESTRING (0 0, 1 0, 0 1)", 0)));
assertFalse(
Functions.isLineStringCCW(Constructors.geomFromWKT("LINESTRING (0 0, 1 0, 0 0)", 0)));
assertFalse(Functions.isLineStringCCW(Constructors.geomFromWKT("LINESTRING EMPTY", 0)));
assertFalse(Functions.isLineStringCCW(Constructors.geomFromWKT("POINT (0 0)", 0)));
assertFalse(Functions.isLineStringCCW(null));
}

@Test
public void testIsPolygonCW() throws ParseException {
Geometry polyCCW =
Expand Down
44 changes: 44 additions & 0 deletions docs/api/flink/Geometry-Accessors/ST_IsLineStringCCW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# ST_IsLineStringCCW

Introduction: Returns true if the input LineString's coordinate sequence has counter-clockwise ring orientation.

![ST_IsLineStringCCW](../../../image/ST_IsLineStringCCW/ST_IsLineStringCCW.svg "ST_IsLineStringCCW")

The input LineString does not need to be closed. Its existing coordinate sequence is evaluated without adding a closing coordinate. The function returns false for non-LineString geometries and LineStrings with fewer than four points.

Format: `ST_IsLineStringCCW(geom: Geometry)`

Return type: `Boolean`

Since: `v1.9.1`

SQL Example:

```sql
SELECT ST_IsLineStringCCW(ST_GeomFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)'))
```

Output:

```
true
```
2 changes: 1 addition & 1 deletion docs/api/flink/Geometry-Editors/ST_ForcePolygonCCW.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# 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.
Introduction: Sets Polygon and MultiPolygon exterior rings counter-clockwise and interior rings clockwise. Polygonal members of GeometryCollections are oriented recursively, while non-polygonal members are preserved unchanged.

![ST_ForcePolygonCCW](../../../image/ST_ForcePolygonCCW/ST_ForcePolygonCCW.svg "ST_ForcePolygonCCW")

Expand Down
2 changes: 1 addition & 1 deletion docs/api/flink/Geometry-Editors/ST_ForcePolygonCW.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# 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.
Introduction: Sets Polygon and MultiPolygon exterior rings clockwise and interior rings counter-clockwise. Polygonal members of GeometryCollections are oriented recursively, while non-polygonal members are preserved unchanged.

![ST_ForcePolygonCW](../../../image/ST_ForcePolygonCW/ST_ForcePolygonCW.svg "ST_ForcePolygonCW")

Expand Down
2 changes: 1 addition & 1 deletion docs/api/flink/Geometry-Editors/ST_ForceRHR.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# ST_ForceRHR

Introduction: Sets the orientation of polygon vertex orderings to follow the Right-Hand-Rule convention. The exterior ring will have a clockwise winding order, while any interior rings are oriented counter-clockwise. This ensures the area bounded by the polygon falls on the right-hand side relative to the ring directions. The function is an alias for [ST_ForcePolygonCW](ST_ForcePolygonCW.md).
Introduction: Sets polygon vertex orderings to follow the Right-Hand-Rule convention: exterior rings are clockwise and interior rings are counter-clockwise. Polygonal members of GeometryCollections are oriented recursively, while non-polygonal members are preserved unchanged. The function is an alias for [ST_ForcePolygonCW](ST_ForcePolygonCW.md).

![ST_ForceRHR](../../../image/ST_ForceRHR/ST_ForceRHR.svg "ST_ForceRHR")

Expand Down
9 changes: 5 additions & 4 deletions docs/api/flink/Geometry-Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ These functions extract information and properties from geometry objects.
| [ST_IsClosed](Geometry-Accessors/ST_IsClosed.md) | Boolean | RETURNS true if the LINESTRING start and end point are the same. | v1.3.0 |
| [ST_IsCollection](Geometry-Accessors/ST_IsCollection.md) | Boolean | Returns `TRUE` if the geometry type of the input is a geometry collection type. Collection types are the following: | v1.5.0 |
| [ST_IsEmpty](Geometry-Accessors/ST_IsEmpty.md) | Boolean | Test if a geometry is empty geometry | v1.2.1 |
| [ST_IsLineStringCCW](Geometry-Accessors/ST_IsLineStringCCW.md) | Boolean | Returns true if the input LineString's coordinate sequence has counter-clockwise ring orientation. | v1.9.1 |
| [ST_IsPolygonCCW](Geometry-Accessors/ST_IsPolygonCCW.md) | Boolean | Returns true if all polygonal components in the input geometry have their exterior rings oriented counter-clockwise and interior rings oriented clockwise. | v1.6.0 |
| [ST_IsPolygonCW](Geometry-Accessors/ST_IsPolygonCW.md) | Boolean | Returns true if all polygonal components in the input geometry have their exterior rings oriented counter-clockwise and interior rings oriented clockwise. | v1.6.0 |
| [ST_IsPolygonCW](Geometry-Accessors/ST_IsPolygonCW.md) | Boolean | Returns true if all polygonal components in the input geometry have their exterior rings oriented clockwise and interior rings oriented counter-clockwise. | v1.6.0 |
| [ST_IsRing](Geometry-Accessors/ST_IsRing.md) | Boolean | RETURN true if LINESTRING is ST_IsClosed and ST_IsSimple. | v1.3.0 |
| [ST_IsSimple](Geometry-Accessors/ST_IsSimple.md) | Boolean | Test if geometry's only self-intersections are at boundary points. | v1.3.0 |
| [ST_M](Geometry-Accessors/ST_M.md) | Double | Returns M Coordinate of given Point, null otherwise. | v1.6.1 |
Expand Down Expand Up @@ -118,9 +119,9 @@ These functions create modified geometries by changing type, structure, or verti
| [ST_Force4D](Geometry-Editors/ST_Force4D.md) | Geometry | Converts the input geometry to 4D XYZM representation. Retains original Z and M values if present. Assigning 0.0 defaults if `mValue` and `zValue` aren't specified. The output contains X, Y, Z, and... | v1.6.1 |
| [ST_Force_2D](Geometry-Editors/ST_Force_2D.md) | Geometry | Forces the geometries into a "2-dimensional mode" so that all output representations will only have the X and Y coordinates. This function is an alias of [ST_Force2D](Geometry-Editors/ST_Force2D.md). | v1.2.1 |
| [ST_ForceCollection](Geometry-Editors/ST_ForceCollection.md) | Geometry | This function converts the input geometry into a GeometryCollection, regardless of the original geometry type. If the input is a multipart geometry, such as a MultiPolygon or MultiLineString, it wi... | v1.6.1 |
| [ST_ForcePolygonCCW](Geometry-Editors/ST_ForcePolygonCCW.md) | Geometry | 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. | v1.6.0 |
| [ST_ForcePolygonCW](Geometry-Editors/ST_ForcePolygonCW.md) | Geometry | 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. | v1.6.0 |
| [ST_ForceRHR](Geometry-Editors/ST_ForceRHR.md) | Geometry | Sets the orientation of polygon vertex orderings to follow the Right-Hand-Rule convention. The exterior ring will have a clockwise winding order, while any interior rings are oriented counter-clock... | v1.6.1 |
| [ST_ForcePolygonCCW](Geometry-Editors/ST_ForcePolygonCCW.md) | Geometry | Sets polygon exterior rings counter-clockwise and interior rings clockwise, including polygonal members nested in GeometryCollections. Non-polygonal members are preserved unchanged. | v1.6.0 |
| [ST_ForcePolygonCW](Geometry-Editors/ST_ForcePolygonCW.md) | Geometry | Sets polygon exterior rings clockwise and interior rings counter-clockwise, including polygonal members nested in GeometryCollections. Non-polygonal members are preserved unchanged. | v1.6.0 |
| [ST_ForceRHR](Geometry-Editors/ST_ForceRHR.md) | Geometry | Applies right-hand-rule ring orientation recursively to polygonal members, including those nested in GeometryCollections. Non-polygonal members are preserved unchanged. | v1.6.1 |
| [ST_LineFromMultiPoint](Geometry-Editors/ST_LineFromMultiPoint.md) | Geometry | Creates a LineString from a MultiPoint geometry. | v1.3.0 |
| [ST_LineMerge](Geometry-Editors/ST_LineMerge.md) | Geometry | Returns a LineString or MultiLineString formed by sewing together the constituent line work of a MULTILINESTRING. | v1.5.0 |
| [ST_LineSegments](Geometry-Editors/ST_LineSegments.md) | `Array<Geometry>` | This function transforms a LineString containing multiple coordinates into an array of LineStrings, each with precisely two coordinates. The `lenient` argument, true by default, prevents an excepti... | v1.7.1 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# ST_IsLineStringCCW

Introduction: Returns true if the input LineString's coordinate sequence has counter-clockwise ring orientation.

![ST_IsLineStringCCW](../../../../image/ST_IsLineStringCCW/ST_IsLineStringCCW.svg "ST_IsLineStringCCW")

The input LineString does not need to be closed. Its existing coordinate sequence is evaluated without adding a closing coordinate. The function returns false for non-LineString geometries and LineStrings with fewer than four points.

Format: `ST_IsLineStringCCW(geom: Geometry)`

Return type: `Boolean`

SQL Example:

```sql
SELECT ST_IsLineStringCCW(ST_GeomFromWKT('LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)'))
```

Output:

```
true
```
Loading
Loading