Skip to content

Commit

Permalink
[SEDONA-508] Add ST_CrossesDateLine (#1259)
Browse files Browse the repository at this point in the history
* Implement ST_CrossesDateLine

* Add docs

* Refactor crossesDateLine(); Fix python test

* Port ST_CrossesDateLine from Predicates to Functions

* Fix typo
  • Loading branch information
prantogg committed Feb 29, 2024
1 parent 7e3fe3a commit 7d293df
Show file tree
Hide file tree
Showing 23 changed files with 313 additions and 3 deletions.
45 changes: 45 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 @@ -218,6 +218,51 @@ public static int bestSRID(Geometry geometry) {
return Spheroid.EPSG_WORLD_MERCATOR;
}

/**
* Checks if a geometry crosses the International Date Line.
*
* @param geometry The geometry to check.
* @return True if the geometry crosses the Date Line, false otherwise.
*/
public static boolean crossesDateLine(Geometry geometry) {
if (geometry == null || geometry.isEmpty()) {
return false;
}

CoordinateSequenceFilter filter = new CoordinateSequenceFilter() {
private Coordinate previous = null;
private boolean crossesDateLine = false;

@Override
public void filter(CoordinateSequence seq, int i) {
if (i == 0) {
previous = seq.getCoordinateCopy(i);
return;
}

Coordinate current = seq.getCoordinateCopy(i);
if (Math.abs(current.x - previous.x) > 180) {
crossesDateLine = true;
}

previous = current;
}

@Override
public boolean isDone() {
return crossesDateLine;
}

@Override
public boolean isGeometryChanged() {
return false;
}
};

geometry.apply(filter);
return filter.isDone();
}

public static Geometry envelope(Geometry geometry) {
return geometry.getEnvelope();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
*/
package org.apache.sedona.common;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.*;
import org.apache.sedona.common.sphere.Spheroid;

import java.util.concurrent.atomic.AtomicBoolean;

public class Predicates {
public static boolean contains(Geometry leftGeometry, Geometry rightGeometry) {
return leftGeometry.contains(rightGeometry);
Expand Down Expand Up @@ -62,4 +64,5 @@ public static boolean dWithin(Geometry leftGeometry, Geometry rightGeometry, dou
return leftGeometry.isWithinDistance(rightGeometry, distance);
}
}

}
32 changes: 32 additions & 0 deletions common/src/test/java/org/apache/sedona/common/PredicatesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.io.ParseException;

import static org.apache.sedona.common.Constructors.geomFromEWKT;
import static org.apache.sedona.common.Constructors.geomFromWKT;
import static org.apache.sedona.common.Functions.crossesDateLine;
import static org.junit.Assert.*;

public class PredicatesTest extends TestBase {
Expand Down Expand Up @@ -72,5 +76,33 @@ public void testDWithinSpheroid() {
assertTrue(actual);
}

@Test
public void testCrossesDateLine() throws ParseException {
Geometry geom1 = geomFromEWKT("LINESTRING(170 30, -170 30)");
Geometry geom2 = geomFromEWKT("LINESTRING(-120 30, -130 40)");
Geometry geom3 = geomFromEWKT("POLYGON((175 10, -175 10, -175 -10, 175 -10, 175 10))");
Geometry geom4 = geomFromEWKT("POLYGON((-120 10, -130 10, -130 -10, -120 -10, -120 10))");
Geometry geom5 = geomFromEWKT("POINT(180 30)");
Geometry geom6 = geomFromEWKT("POLYGON((160 20, 180 20, 180 -20, 160 -20, 160 20), (165 15, 175 15, 175 -15, 165 -15, 165 15))");
Geometry geom8 = geomFromEWKT("POLYGON((170 -10, -170 -10, -170 10, 170 10, 170 -10), (175 -5, -175 -5, -175 5, 175 5, 175 -5))");

// Multi-geometry test cases
Geometry multiGeom1 = geomFromEWKT("MULTILINESTRING((170 30, -170 30), (-120 30, -130 40))");
Geometry multiGeom2 = geomFromEWKT("MULTIPOLYGON(((175 10, -175 10, -175 -10, 175 -10, 175 10)), ((-120 10, -130 10, -130 -10, -120 -10, -120 10)))");
Geometry multiGeom3 = geomFromEWKT("MULTIPOINT((180 30), (170 -20))");
Geometry multiGeom4 = geomFromEWKT("MULTIPOLYGON(((160 20, 180 20, 180 -20, 160 -20, 160 20)), ((-120 10, -130 10, -130 -10, -120 -10, -120 10)))");

assertEquals(true, crossesDateLine(geom1));
assertEquals(false, crossesDateLine(geom2));
assertEquals(true, crossesDateLine(geom3));
assertEquals(false, crossesDateLine(geom4));
assertEquals(false, crossesDateLine(geom5));
assertEquals(false, crossesDateLine(geom6));
assertEquals(true, crossesDateLine(geom8));
assertEquals(true, crossesDateLine(multiGeom1));
assertEquals(true, crossesDateLine(multiGeom2));
assertEquals(false, crossesDateLine(multiGeom3));
assertEquals(false, crossesDateLine(multiGeom4));
}

}
29 changes: 29 additions & 0 deletions docs/api/flink/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,35 @@ Output:
2
```

## ST_CrossesDateLine

Introduction: This function determines if a given geometry crosses the International Date Line. It operates by checking if the difference in longitude between any pair of consecutive points in the geometry exceeds 180 degrees. If such a difference is found, it is assumed that the geometry crosses the Date Line. It returns true if the geometry crosses the Date Line, and false otherwise.

!!!note
The function assumes that the provided geometry is in lon/lat coordinate reference system where longitude values range from -180 to 180 degrees.

!!!note
For multi-geometries (e.g., MultiPolygon, MultiLineString), this function will return true if any one of the geometries within the multi-geometry crosses the International Date Line.

Format: `ST_CrossesDateLine(geometry: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_CrossesDateLine(ST_GeomFromWKT('LINESTRING(170 30, -170 30)'))
```

Output:

```sql
true
```

!!!Warning
For geometries that span more than 180 degrees in longitude without actually crossing the Date Line, this function may still return true, indicating a crossing.

## ST_Dimension

Introduction: Return the topological dimension of this Geometry object, which must be less than or equal to the coordinate dimension. OGC SPEC s2.1.1.1 - returns 0 for POINT, 1 for LINESTRING, 2 for POLYGON, and the largest dimension of the components of a GEOMETRYCOLLECTION. If the dimension is unknown (e.g. for an empty GEOMETRYCOLLECTION) 0 is returned.
Expand Down
29 changes: 29 additions & 0 deletions docs/api/snowflake/vector-data/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,35 @@ Output:
2
```

## ST_CrossesDateLine

Introduction: This function determines if a given geometry crosses the International Date Line. It operates by checking if the difference in longitude between any pair of consecutive points in the geometry exceeds 180 degrees. If such a difference is found, it is assumed that the geometry crosses the Date Line. It returns true if the geometry crosses the Date Line, and false otherwise.

!!!note
The function assumes that the provided geometry is in lon/lat coordinate reference system where longitude values range from -180 to 180 degrees.

!!!note
For multi-geometries (e.g., MultiPolygon, MultiLineString), this function will return true if any one of the geometries within the multi-geometry crosses the International Date Line.

Format: `ST_CrossesDateLine(geometry: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_CrossesDateLine(ST_GeomFromWKT('LINESTRING(170 30, -170 30)'))
```

Output:

```sql
true
```

!!!Warning
For geometries that span more than 180 degrees in longitude without actually crossing the Date Line, this function may still return true, indicating a crossing.

## ST_Degrees

Introduction: Convert an angle in radian to degrees.
Expand Down
29 changes: 29 additions & 0 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,35 @@ Output:
2
```

## ST_CrossesDateLine

Introduction: This function determines if a given geometry crosses the International Date Line. It operates by checking if the difference in longitude between any pair of consecutive points in the geometry exceeds 180 degrees. If such a difference is found, it is assumed that the geometry crosses the Date Line. It returns true if the geometry crosses the Date Line, and false otherwise.

!!!note
The function assumes that the provided geometry is in lon/lat coordinate reference system where longitude values range from -180 to 180 degrees.

!!!note
For multi-geometries (e.g., MultiPolygon, MultiLineString), this function will return true if any one of the geometries within the multi-geometry crosses the International Date Line.

Format: `ST_CrossesDateLine(geometry: Geometry)`

Since: `v1.6.0`

SQL Example:

```sql
SELECT ST_CrossesDateLine(ST_GeomFromWKT('LINESTRING(170 30, -170 30)'))
```

Output:

```sql
true
```

!!!Warning
For geometries that span more than 180 degrees in longitude without actually crossing the Date Line, this function may still return true, indicating a crossing.

## ST_Degrees

Introduction: Convert an angle in radian to degrees.
Expand Down
1 change: 1 addition & 0 deletions flink/src/main/java/org/apache/sedona/flink/Catalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_CollectionExtract(),
new Functions.ST_ConcaveHull(),
new Functions.ST_ConvexHull(),
new Functions.ST_CrossesDateLine(),
new Functions.ST_Envelope(),
new Functions.ST_Difference(),
new Functions.ST_Dimension(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,21 @@ public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.j
}
}

public static class ST_CrossesDateLine extends ScalarFunction
{
/**
* Constructor for relation checking without duplicate removal
*/
public ST_CrossesDateLine() {}

@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.crossesDateLine(geom);
}
}

public static class ST_Envelope 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
27 changes: 27 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 @@ -199,6 +199,33 @@ public void testConvexHull() {
assertEquals("POLYGON ((0 0, 1 2, 3 2, 5 0, 0 0))", result.toString());
}

@Test
public void testCrossesDateLine() {
// Test line crossing the Date Line
Table table1 = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(170 30, -170 30)') AS geom");
table1 = table1.select(call("ST_CrossesDateLine", $("geom")));
Boolean actual1 = (Boolean) first(table1).getField(0);
assertEquals(true, actual1);

// Test line not crossing the Date Line
Table table2 = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(-120 30, -130 40)') AS geom");
table2 = table2.select(call("ST_CrossesDateLine", $("geom")));
Boolean actual2 = (Boolean) first(table2).getField(0);
assertEquals(false, actual2);

// Test polygon crossing the Date Line
Table table3 = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON((175 10, -175 10, -175 -10, 175 -10, 175 10))') AS geom");
table3 = table3.select(call("ST_CrossesDateLine", $("geom")));
Boolean actual3 = (Boolean) first(table3).getField(0);
assertEquals(true, actual3);

// Test polygon not crossing the Date Line
Table table4 = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POLYGON((-120 10, -130 10, -130 -10, -120 -10, -120 10))') AS geom");
table4 = table4.select(call("ST_CrossesDateLine", $("geom")));
Boolean actual4 = (Boolean) first(table4).getField(0);
assertEquals(false, actual4);
}

@Test
public void testDifference() {
Table lineTable = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('LINESTRING(50 100, 50 200)') AS g1, ST_GeomFromWKT('LINESTRING(50 50, 50 150)') as g2");
Expand Down
12 changes: 12 additions & 0 deletions python/sedona/sql/st_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"ST_ClosestPoint",
"ST_ConcaveHull",
"ST_ConvexHull",
"ST_CrossesDateLine",
"ST_Difference",
"ST_Dimension",
"ST_Distance",
Expand Down Expand Up @@ -457,6 +458,17 @@ def ST_ConvexHull(geometry: ColumnOrName) -> Column:
"""
return _call_st_function("ST_ConvexHull", geometry)

@validate_argument_types
def ST_CrossesDateLine(a: ColumnOrName) -> Column:
"""Check whether geometry a crosses the International Date Line.
:param a: Geometry to check crossing with.
:type a: ColumnOrName
:return: True if geometry a cross the dateline.
:rtype: Column
"""
return _call_st_function("ST_CrossesDateLine", (a))

@validate_argument_types
def ST_Dimension(geometry: ColumnOrName):
"""Calculate the inherent dimension of a geometry column.
Expand Down
1 change: 0 additions & 1 deletion python/sedona/sql/st_predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def ST_Crosses(a: ColumnOrName, b: ColumnOrName) -> Column:
"""
return _call_predicate_function("ST_Crosses", (a, b))


@validate_argument_types
def ST_Disjoint(a: ColumnOrName, b: ColumnOrName) -> Column:
"""Check whether two geometries are disjoint.
Expand Down
4 changes: 4 additions & 0 deletions python/tests/sql/test_dataframe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
(stf.ST_ConcaveHull, ("geom", 1.0, True), "triangle_geom", "", "POLYGON ((1 1, 1 0, 0 0, 1 1))"),
(stf.ST_ConvexHull, ("geom",), "triangle_geom", "", "POLYGON ((0 0, 1 1, 1 0, 0 0))"),
(stf.ST_CoordDim, ("point",), "point_geom", "", 2),
(stf.ST_CrossesDateLine, ("line",), "line_crossing_dateline", "", True),
(stf.ST_Difference, ("a", "b"), "overlapping_polys", "", "POLYGON ((1 0, 0 0, 0 1, 1 1, 1 0))"),
(stf.ST_Dimension, ("geom",), "geometry_geom_collection", "", 1),
(stf.ST_Distance, ("a", "b"), "two_points", "", 3.0),
Expand Down Expand Up @@ -241,6 +242,7 @@
(stf.ST_CollectionExtract, (None,)),
(stf.ST_ConcaveHull, (None, 1.0)),
(stf.ST_ConvexHull, (None,)),
(stf.ST_CrossesDateLine, (None,)),
(stf.ST_Difference, (None, "b")),
(stf.ST_Difference, ("", None)),
(stf.ST_Distance, (None, "")),
Expand Down Expand Up @@ -445,6 +447,8 @@ def base_df(self, request):
return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('POINT (0 0)') AS origin, ST_GeomFromWKT('POINT (1 0)') as point")
elif request.param == "ny_seattle":
return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('POINT (-122.335167 47.608013)') AS seattle, ST_GeomFromWKT('POINT (-73.935242 40.730610)') as ny")
elif request.param == "line_crossing_dateline":
return TestDataFrameAPI.spark.sql("SELECT ST_GeomFromWKT('LINESTRING (179.95 30, -179.95 30)') AS line")
raise ValueError(f"Invalid base_df name passed: {request.param}")

def _id_test_configuration(val):
Expand Down
14 changes: 14 additions & 0 deletions python/tests/sql/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,20 @@ def test_st_centroid(self):
function_df = self.spark.sql("select ST_Centroid(polygondf.countyshape) from polygondf")
function_df.show()

def test_st_crossesdateline(self):
crosses_test_table = self.spark.sql(
"select ST_GeomFromWKT('POLYGON((175 10, -175 10, -175 -10, 175 -10, 175 10))') as geom")
crosses_test_table.createOrReplaceTempView("crossesTesttable")
crosses = self.spark.sql("select(ST_CrossesDateLine(geom)) from crossesTesttable")

not_crosses_test_table = self.spark.sql(
"select ST_GeomFromWKT('POLYGON((1 1, 4 1, 4 4, 1 4, 1 1))') as geom")
not_crosses_test_table.createOrReplaceTempView("notCrossesTesttable")
not_crosses = self.spark.sql("select(ST_CrossesDateLine(geom)) from notCrossesTesttable")

assert crosses.take(1)[0][0]
assert not not_crosses.take(1)[0][0]

def test_st_length(self):
polygon_wkt_df = self.spark.read.format("csv"). \
option("delimiter", "\t"). \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ public void test_ST_ConvexHull() {
"POLYGON ((10 8, 20 30, 100 190, 150 10, 10 8))"
);
}

@Test
public void test_ST_CrossesDateLine() {
registerUDF("ST_CrossesDateLine", byte[].class);
verifySqlSingleRes(
"SELECT SEDONA.ST_CrossesDateLine(sedona.ST_GeomFromWKT('POLYGON((175 10, -175 10, -175 -10, 175 -10, 175 10))'))",
true
);
verifySqlSingleRes(
"SELECT SEDONA.ST_CrossesDateLine(sedona.ST_GeomFromWKT('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'))",
false
);
}
@Test
public void test_ST_Difference() {
registerUDF("ST_Difference", byte[].class, byte[].class);
Expand Down
Loading

0 comments on commit 7d293df

Please sign in to comment.