You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2024/05/28 16:04:50 UTC
(sedona) branch master updated: [SEDONA-565] Add ST_ForceRHR (#1435)
This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new 104c14595 [SEDONA-565] Add ST_ForceRHR (#1435)
104c14595 is described below
commit 104c1459579cf1c15458f2a4bd369a4e302d7f8f
Author: Jia Yu <ji...@apache.org>
AuthorDate: Tue May 28 09:04:45 2024 -0700
[SEDONA-565] Add ST_ForceRHR (#1435)
* [111] Add ST_ForceRHR (#140)
* Update versions
---------
Co-authored-by: Furqaan Khan <46...@users.noreply.github.com>
---
docs/api/flink/Function.md | 20 ++++++++++++++++++++
docs/api/snowflake/vector-data/Function.md | 18 ++++++++++++++++++
docs/api/sql/Function.md | 20 ++++++++++++++++++++
.../main/java/org/apache/sedona/flink/Catalog.java | 1 +
.../apache/sedona/flink/expressions/Functions.java | 8 ++++++++
.../java/org/apache/sedona/flink/FunctionTest.java | 8 ++++++++
python/sedona/sql/st_functions.py | 9 +++++++++
python/tests/sql/test_dataframe_api.py | 2 ++
python/tests/sql/test_function.py | 6 ++++++
.../sedona/snowflake/snowsql/TestFunctions.java | 9 +++++++++
.../sedona/snowflake/snowsql/TestFunctionsV2.java | 9 +++++++++
.../org/apache/sedona/snowflake/snowsql/UDFs.java | 9 +++++++++
.../org/apache/sedona/snowflake/snowsql/UDFsV2.java | 9 +++++++++
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 1 +
.../spark/sql/sedona_sql/expressions/Functions.scala | 7 +++++++
.../sql/sedona_sql/expressions/st_functions.scala | 3 +++
.../apache/sedona/sql/dataFrameAPITestScala.scala | 7 +++++++
.../org/apache/sedona/sql/functionTestScala.scala | 7 +++++++
18 files changed, 153 insertions(+)
diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md
index ca1c5f6b8..a6535e6fb 100644
--- a/docs/api/flink/Function.md
+++ b/docs/api/flink/Function.md
@@ -1274,6 +1274,26 @@ Output:
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))
```
+## 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).
+
+Format: `ST_ForceRHR(geom: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_AsText(ST_ForceRHR(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,
diff --git a/docs/api/snowflake/vector-data/Function.md b/docs/api/snowflake/vector-data/Function.md
index eedcc50a2..b93f5f58a 100644
--- a/docs/api/snowflake/vector-data/Function.md
+++ b/docs/api/snowflake/vector-data/Function.md
@@ -1005,6 +1005,24 @@ Output:
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))
```
+## 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).
+
+Format: `ST_ForceRHR(geom: Geometry)`
+
+SQL Example:
+
+```sql
+SELECT ST_AsText(ST_ForceRHR(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,
diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md
index 0fe320261..397feebff 100644
--- a/docs/api/sql/Function.md
+++ b/docs/api/sql/Function.md
@@ -1279,6 +1279,26 @@ Output:
POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))
```
+## 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).
+
+Format: `ST_ForceRHR(geom: Geometry)`
+
+Since: `v1.6.1`
+
+SQL Example:
+
+```sql
+SELECT ST_AsText(ST_ForceRHR(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,
diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
index 24395238f..0847ab2a3 100644
--- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java
+++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java
@@ -144,6 +144,7 @@ public class Catalog {
new Functions.ST_NumPoints(),
new Functions.ST_Force3D(),
new Functions.ST_ForcePolygonCW(),
+ new Functions.ST_ForceRHR(),
new Functions.ST_NRings(),
new Functions.ST_IsPolygonCCW(),
new Functions.ST_ForcePolygonCCW(),
diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
index fe11db120..80239d32e 100644
--- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
+++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java
@@ -1030,6 +1030,14 @@ public class Functions {
}
}
+ public static class ST_ForceRHR 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 {
diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
index 7649e0040..95a1bee71 100644
--- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
+++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java
@@ -1223,6 +1223,14 @@ public class FunctionTest extends TestBase{
assertEquals(expectedDims, actual);
}
+ @Test
+ public void testForceRHR() {
+ Table polyTable = tableEnv.sqlQuery("SELECT ST_ForceRHR(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 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");
diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py
index c22b85490..e2f84b4ed 100644
--- a/python/sedona/sql/st_functions.py
+++ b/python/sedona/sql/st_functions.py
@@ -1469,6 +1469,15 @@ def ST_ForcePolygonCW(geometry: ColumnOrName) -> Column:
"""
return _call_st_function("ST_ForcePolygonCW", geometry)
+@validate_argument_types
+def ST_ForceRHR(geometry: ColumnOrName) -> Column:
+ """
+ Returns
+ :param geometry: Geometry column to change orientation
+ :return: Clockwise oriented geometry
+ """
+ return _call_st_function("ST_ForceRHR", geometry)
+
@validate_argument_types
def ST_NRings(geometry: ColumnOrName) -> Column:
"""
diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py
index 67ba78c52..2fec6006f 100644
--- a/python/tests/sql/test_dataframe_api.py
+++ b/python/tests/sql/test_dataframe_api.py
@@ -118,6 +118,7 @@ test_configurations = [
(stf.ST_Force3D, ("point", 1.0), "point_geom", "", "POINT Z (0 1 1)"),
(stf.ST_ForcePolygonCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
(stf.ST_ForcePolygonCCW, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 0, 3 3, 0 0), (1 1, 2 2, 2 1, 1 1))"),
+ (stf.ST_ForceRHR, ("geom",), "geom_with_hole", "", "POLYGON ((0 0, 3 3, 3 0, 0 0), (1 1, 2 1, 2 2, 1 1))"),
(stf.ST_FrechetDistance, ("point", "line",), "point_and_line", "", 5.0990195135927845),
(stf.ST_GeometricMedian, ("multipoint",), "multipoint_geom", "", "POINT (22.500002656424286 21.250001168173426)"),
(stf.ST_GeometryN, ("geom", 0), "multipoint", "", "POINT (0 0)"),
@@ -281,6 +282,7 @@ wrong_type_configurations = [
(stf.ST_Force_2D, (None,)),
(stf.ST_ForcePolygonCW, (None,)),
(stf.ST_ForcePolygonCCW, (None,)),
+ (stf.ST_ForceRHR, (None,)),
(stf.ST_GeometryN, (None, 0)),
(stf.ST_GeometryN, ("", None)),
(stf.ST_GeometryN, ("", 0.0)),
diff --git a/python/tests/sql/test_function.py b/python/tests/sql/test_function.py
index 791a3073b..6ac59b29e 100644
--- a/python/tests/sql/test_function.py
+++ b/python/tests/sql/test_function.py
@@ -1395,6 +1395,12 @@ class TestPredicateJoin(TestBase):
expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))"
assert expected == actual
+ def test_forceRHR(self):
+ actualDf = self.spark.sql("SELECT ST_ForceRHR(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")
+ actual = actualDf.selectExpr("ST_AsText(polyCW)").take(1)[0][0]
+ expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))"
+ assert expected == actual
+
def test_nRings(self):
expected = 1
actualDf = self.spark.sql("SELECT ST_GeomFromText('POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))') AS geom")
diff --git a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
index 8c26cc404..805527286 100644
--- a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
+++ b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctions.java
@@ -1046,6 +1046,15 @@ public class TestFunctions extends TestBase {
);
}
+ @Test
+ public void test_ST_ForceRHR() {
+ registerUDF("ST_ForceRHR", byte[].class);
+ verifySqlSingleRes(
+ "SELECT sedona.ST_AsText(sedona.ST_ForceRHR(sedona.ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')))",
+ "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))"
+ );
+ }
+
@Test
public void test_ST_LengthSpheroid() {
registerUDF("ST_LengthSpheroid", byte[].class);
diff --git a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
index 289d40ee3..ef3b385c7 100644
--- a/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
+++ b/snowflake-tester/src/test/java/org/apache/sedona/snowflake/snowsql/TestFunctionsV2.java
@@ -1005,6 +1005,15 @@ public class TestFunctionsV2
);
}
+ @Test
+ public void test_ST_ForceRHR() {
+ registerUDFV2("ST_ForceRHR", String.class);
+ verifySqlSingleRes(
+ "SELECT ST_AsText(sedona.ST_ForceRHR(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))')))",
+ "POLYGON((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20))"
+ );
+ }
+
@Test
public void test_ST_LengthSpheroid() {
registerUDFV2("ST_LengthSpheroid", String.class);
diff --git a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
index 116766ed3..efd8fa786 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFs.java
@@ -1360,6 +1360,15 @@ public class UDFs {
);
}
+ @UDFAnnotations.ParamMeta(argNames = {"geom"})
+ public static byte[] ST_ForceRHR(byte[] geom) {
+ return GeometrySerde.serialize(
+ Functions.forcePolygonCW(
+ GeometrySerde.deserialize(geom)
+ )
+ );
+ }
+
@UDFAnnotations.ParamMeta(argNames = {"geom"})
public static double ST_LengthSpheroid(byte[] geom) {
return Spheroid.length(
diff --git a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
index da0e56027..2374b2652 100644
--- a/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
+++ b/snowflake/src/main/java/org/apache/sedona/snowflake/snowsql/UDFsV2.java
@@ -1198,6 +1198,15 @@ public class UDFsV2
);
}
+ @UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"}, returnTypes = "Geometry")
+ public static String ST_ForceRHR(String geom) {
+ return GeometrySerde.serGeoJson(
+ Functions.forcePolygonCW(
+ GeometrySerde.deserGeoJson(geom)
+ )
+ );
+ }
+
@UDFAnnotations.ParamMeta(argNames = {"geom"}, argTypes = {"Geometry"})
public static double ST_LengthSpheroid(String geom) {
return Spheroid.length(
diff --git a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index bab9ffe1b..48a795256 100644
--- a/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -152,6 +152,7 @@ object Catalog {
function[ST_AsEWKT](),
function[ST_Force_2D](),
function[ST_ForcePolygonCW](),
+ function[ST_ForceRHR](),
function[ST_ZMax](),
function[ST_ZMin](),
function[ST_YMax](),
diff --git a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
index f734c99de..7a12ec791 100644
--- a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
+++ b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala
@@ -1131,6 +1131,13 @@ case class ST_ForcePolygonCW(inputExpressions: Seq[Expression])
}
}
+case class ST_ForceRHR(inputExpressions: Seq[Expression])
+ extends InferredExpression(Functions.forcePolygonCW _) {
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
+ copy(inputExpressions = newChildren)
+ }
+}
+
case class ST_NRings(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.nRings _) {
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
diff --git a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
index 4ec1885e8..30eb19a56 100644
--- a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
+++ b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala
@@ -400,6 +400,9 @@ object st_functions extends DataFrameAPI {
def ST_ForcePolygonCW(geometry: Column): Column = wrapExpression[ST_ForcePolygonCW](geometry)
def ST_ForcePolygonCW(geometry: String): Column = wrapExpression[ST_ForcePolygonCW](geometry)
+ def ST_ForceRHR(geometry: Column): Column = wrapExpression[ST_ForceRHR](geometry)
+ def ST_ForceRHR(geometry: String): Column = wrapExpression[ST_ForceRHR](geometry)
+
def ST_NRings(geometry: Column): Column = wrapExpression[ST_NRings](geometry)
def ST_NRings(geometry: String): Column = wrapExpression[ST_NRings](geometry)
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
index da4584abf..579d23b6c 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala
@@ -1212,6 +1212,13 @@ class dataFrameAPITestScala extends TestBaseScala {
assertEquals(expectedGeomDefaultValue, wktWriter.write(actualGeomDefaultValue))
}
+ it("Passed ST_ForceRHR") {
+ val baseDf = sparkSession.sql("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 poly")
+ val actual = baseDf.select(ST_AsText(ST_ForceRHR("poly"))).take(1)(0).get(0).asInstanceOf[String]
+ val 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)
+ }
+
it("Passed ST_ForcePolygonCW") {
val baseDf = sparkSession.sql("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 poly")
val actual = baseDf.select(ST_AsText(ST_ForcePolygonCW("poly"))).take(1)(0).get(0).asInstanceOf[String]
diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
index 807513951..c87507a62 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala
@@ -948,6 +948,13 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample
assert(expected.equals(actual))
}
+ it("Should pass ST_ForceRHR") {
+ val baseDf = sparkSession.sql("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 poly")
+ val actual = baseDf.selectExpr("ST_AsText(ST_ForceRHR(poly))").first().getString(0)
+ val expected = "POLYGON ((20 35, 45 20, 30 5, 10 10, 10 30, 20 35), (30 20, 20 25, 20 15, 30 20))"
+ assert(expected.equals(actual))
+ }
+
it("Should pass ST_IsPolygonCW") {
var actual = sparkSession.sql("SELECT ST_IsPolygonCW(ST_GeomFromWKT('POLYGON ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35),(30 20, 20 15, 20 25, 30 20))'))").first().getBoolean(0)
assert(actual == false)