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-504] Add ST_ShiftLongitude #1255

Merged
merged 12 commits into from
Feb 28, 2024
26 changes: 26 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 @@ -239,6 +239,32 @@ public static Geometry normalize(Geometry geometry) {
return geometry;
}

public static Geometry shiftLongitude(Geometry geometry) {
geometry.apply(new CoordinateSequenceFilter() {
@Override
public void filter(CoordinateSequence seq, int i) {
double newX = seq.getX(i);
if (newX < 0) {
newX += 360;
} else if (newX > 180) {
newX -= 360;
}
seq.setOrdinate(i, CoordinateSequence.X, newX);
}

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

@Override
public boolean isGeometryChanged() {
return true;
}
});
return geometry;
}

public static Double x(Geometry geometry) {
if (geometry instanceof Point) {
return geometry.getCoordinate().x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.geotools.referencing.operation.projection.ProjectionException;
import org.junit.Test;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.opengis.referencing.FactoryException;
Expand All @@ -32,6 +33,8 @@
import java.util.*;
import java.util.stream.Collectors;

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

public class FunctionsTest extends TestBase {
Expand Down Expand Up @@ -1240,6 +1243,39 @@ public void testBestSRID() {
assertEquals("Expected World Mercator projection for wide range geometry", expectedEPSG, actualEPSG);
}

@Test
public void testShiftLongitude() throws ParseException {
Geometry point = geomFromWKT("POINT(-175 10)", 4230);
Geometry linestring1 = geomFromEWKT("LINESTRING(179 10, -179 10)");
Geometry linestring2 = geomFromEWKT("LINESTRING(179 10, 181 10)");
Geometry polygon = geomFromEWKT("POLYGON((179 10, -179 10, -179 20, 179 20, 179 10))");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for polygons with holes.

Geometry multiPoint = geomFromEWKT("MULTIPOINT((179 10), (-179 10))");
Geometry multiLineString = geomFromEWKT("MULTILINESTRING((179 10, -179 10), (179 20, 181 20))");
Geometry multiPolygon = geomFromEWKT("MULTIPOLYGON(((179 10, -179 10, -179 20, 179 20, 179 10)), ((-185 10, -185 20, -175 20, -175 10, -185 10)))");
Geometry geomCollection = geomFromEWKT("GEOMETRYCOLLECTION(POINT(190 10), LINESTRING(179 10, -179 10))");
Geometry polygonRing = geomFromEWKT("POLYGON((-170 -20, 170 -20, 170 20, -170 20, -170 -20), (-175 -10, 175 -10, 175 10, -175 10, -175 -10))");

Geometry expected1 = geomFromWKT("POINT (185 10)", 4230);
Geometry expected2 = geomFromEWKT("LINESTRING (179 10, 181 10)");
Geometry expected3 = geomFromEWKT("LINESTRING (179 10, -179 10)");
Geometry expected4 = geomFromEWKT("POLYGON ((179 10, 181 10, 181 20, 179 20, 179 10))");
Geometry expected5 = geomFromEWKT("MULTIPOINT ((179 10), (181 10))");
Geometry expected6 = geomFromEWKT("MULTILINESTRING ((179 10, 181 10), (179 20, -179 20))");
Geometry expected7 = geomFromEWKT("MULTIPOLYGON (((179 10, 181 10, 181 20, 179 20, 179 10)), ((175 10, 175 20, 185 20, 185 10, 175 10)))");
Geometry expected8 = geomFromEWKT("GEOMETRYCOLLECTION (POINT (-170 10), LINESTRING (179 10, 181 10))");
Geometry expected9 = geomFromEWKT("POLYGON ((190 -20, 170 -20, 170 20, 190 20, 190 -20), (185 -10, 175 -10, 175 10, 185 10, 185 -10))");

assertEquals(expected1, Functions.shiftLongitude(point));
assertEquals(expected2, Functions.shiftLongitude(linestring1));
assertEquals(expected3, Functions.shiftLongitude(linestring2));
assertEquals(expected4, Functions.shiftLongitude(polygon));
assertEquals(expected5, Functions.shiftLongitude(multiPoint));
assertEquals(expected6, Functions.shiftLongitude(multiLineString));
assertEquals(expected7, Functions.shiftLongitude(multiPolygon));
assertEquals(expected8, Functions.shiftLongitude(geomCollection));
assertEquals(expected9, Functions.shiftLongitude(polygonRing));
}

@Test
public void nRingsUnsupported() {
LineString lineString = GEOMETRY_FACTORY.createLineString(coordArray3d(0, 1, 1, 1, 2, 1, 1, 2, 2));
Expand Down
21 changes: 21 additions & 0 deletions docs/api/flink/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -2369,6 +2369,27 @@ Output:
SRID=3021;POLYGON ((1 1, 8 1, 8 8, 1 8, 1 1))
```

## ST_ShiftLongitude

Introduction: Modifies longitude coordinates in geometries, shifting values between -180..0 degrees to 180..360 degrees and vice versa. This is useful for normalizing data across the International Date Line and standardizing coordinate ranges for visualization and spheroidal calculations.

!!!note
This function is only applicable to geometries that use lon/lat coordinate systems.

Format: `ST_ShiftLongitude (geom: geometry)`

Since: `v1.6.0`

SQL example:
```SQL
SELECT ST_ShiftLongitude(ST_GeomFromText('LINESTRING(177 10, 179 10, -179 10, -177 10)'))
```

Output:
```sql
LINESTRING(177 10, 179 10, 181 10, 183 10)
```

## ST_SRID

Introduction: Return the spatial reference system identifier (SRID) of the geometry.
Expand Down
21 changes: 21 additions & 0 deletions docs/api/snowflake/vector-data/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,27 @@ SELECT ST_SetSRID(polygondf.countyshape, 3021)
FROM polygondf
```

## ST_ShiftLongitude

Introduction: Modifies longitude coordinates in geometries, shifting values between -180..0 degrees to 180..360 degrees and vice versa. This is useful for normalizing data across the International Date Line and standardizing coordinate ranges for visualization and spheroidal calculations.

!!!note
This function is only applicable to geometries that use lon/lat coordinate systems.

Format: `ST_ShiftLongitude (geom: geometry)`

Since: `v1.6.0`

SQL example:
```SQL
SELECT ST_ShiftLongitude(ST_GeomFromText('LINESTRING(177 10, 179 10, -179 10, -177 10)'))
```

Output:
```sql
LINESTRING(177 10, 179 10, 181 10, 183 10)
```

## ST_SimplifyPreserveTopology

Introduction: Simplifies a geometry and ensures that the result is a valid geometry having the same dimension and number of components as the input,
Expand Down
21 changes: 21 additions & 0 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -2379,6 +2379,27 @@ Output:
SRID=3021;POLYGON ((1 1, 8 1, 8 8, 1 8, 1 1))
```

## ST_ShiftLongitude

Introduction: Modifies longitude coordinates in geometries, shifting values between -180..0 degrees to 180..360 degrees and vice versa. This is useful for normalizing data across the International Date Line and standardizing coordinate ranges for visualization and spheroidal calculations.

!!!note
This function is only applicable to geometries that use lon/lat coordinate systems.

Format: `ST_ShiftLongitude (geom: geometry)`

Since: `v1.6.0`

SQL example:
```SQL
SELECT ST_ShiftLongitude(ST_GeomFromText('LINESTRING(177 10, 179 10, -179 10, -177 10)'))
```

Output:
```sql
LINESTRING(177 10, 179 10, 181 10, 183 10)
```

## ST_SimplifyPreserveTopology

Introduction: Simplifies a geometry and ensures that the result is a valid geometry having the same dimension and number of components as the input,
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 @@ -127,6 +127,7 @@ public static UserDefinedFunction[] getFuncs() {
new Functions.ST_MinimumBoundingRadius(),
new Functions.ST_Multi(),
new Functions.ST_StartPoint(),
new Functions.ST_ShiftLongitude(),
new Functions.ST_SimplifyPreserveTopology(),
new Functions.ST_Split(),
new Functions.ST_Subdivide(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ public int eval(@DataTypeHint(value = "RAW", bridgedTo = Geometry.class) Object
}
}

public static class ST_ShiftLongitude extends ScalarFunction {
@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class)
public Geometry eval(@DataTypeHint(value = "RAW", bridgedTo = Geometry.class) Object o) {
Geometry geom = (Geometry) o;
return org.apache.sedona.common.Functions.shiftLongitude(geom);
}
}

public static class ST_ClosestPoint 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 g1,
Expand Down
15 changes: 15 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 @@ -115,6 +115,21 @@ public void testBestSRID() {
assertEquals(3395, result);
}

@Test
public void testShiftLogitude() {
String actual = (String) first(tableEnv.sqlQuery("SELECT ST_AsText(ST_ShiftLongitude(ST_GeomFromWKT('POLYGON((179 10, -179 10, -179 20, 179 20, 179 10))')))")).getField(0);
String expected = "POLYGON ((179 10, 181 10, 181 20, 179 20, 179 10))";
assertEquals(expected, actual);

actual = (String) first(tableEnv.sqlQuery("SELECT ST_AsText(ST_ShiftLongitude(ST_GeomFromWKT('MULTIPOLYGON(((179 10, -179 10, -179 20, 179 20, 179 10)), ((-185 10, -185 20, -175 20, -175 10, -185 10)))')))")).getField(0);
expected = "MULTIPOLYGON (((179 10, 181 10, 181 20, 179 20, 179 10)), ((175 10, 175 20, 185 20, 185 10, 175 10)))";
assertEquals(expected, actual);

actual = (String) first(tableEnv.sqlQuery("SELECT ST_AsText(ST_ShiftLongitude(ST_GeomFromWKT('LINESTRING(179 10, 181 10)')))")).getField(0);
expected = "LINESTRING (179 10, -179 10)";
assertEquals(expected, actual);
}

@Test
public void testClosestPoint() {
Table table = tableEnv.sqlQuery("SELECT ST_GeomFromWKT('POINT (160 40)') AS g1, ST_GeomFromWKT('POINT (10 10)') as g2");
Expand Down
10 changes: 10 additions & 0 deletions python/sedona/sql/st_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ def ST_BestSRID(geometry: ColumnOrName) -> Column:
"""
return _call_st_function("ST_BestSRID", geometry)

@validate_argument_types
def ST_ShiftLongitude(geometry: ColumnOrName) -> Column:
"""Shifts longitudes between -180..0 degrees to 180..360 degrees and vice versa.

:param geometry: Geometry column.
:type geometry: ColumnOrName
:return: Shifted geometry
:rtype: Column
"""
return _call_st_function("ST_ShiftLongitude", geometry)

@validate_argument_types
def ST_Boundary(geometry: ColumnOrName) -> Column:
Expand Down
2 changes: 2 additions & 0 deletions python/tests/sql/test_dataframe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
(stf.ST_S2CellIDs, ("point", 30), "point_geom", "", [1153451514845492609]),
(stf.ST_SetPoint, ("line", 1, lambda: f.expr("ST_Point(1.0, 1.0)")), "linestring_geom", "", "LINESTRING (0 0, 1 1, 2 0, 3 0, 4 0, 5 0)"),
(stf.ST_SetSRID, ("point", 3021), "point_geom", "ST_SRID(geom)", 3021),
(stf.ST_ShiftLongitude, ("geom",), "triangle_geom", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"),
(stf.ST_SimplifyPreserveTopology, ("geom", 0.2), "0.9_poly", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"),
(stf.ST_Split, ("line", "points"), "multipoint_splitting_line", "", "MULTILINESTRING ((0 0, 0.5 0.5), (0.5 0.5, 1 1), (1 1, 1.5 1.5, 2 2))"),
(stf.ST_SRID, ("point",), "point_geom", "", 0),
Expand Down Expand Up @@ -303,6 +304,7 @@
(stf.ST_SetSRID, (None, 3021)),
(stf.ST_SetSRID, ("", None)),
(stf.ST_SetSRID, ("", 3021.0)),
(stf.ST_ShiftLongitude, (None,)),
(stf.ST_SimplifyPreserveTopology, (None, 0.2)),
(stf.ST_SimplifyPreserveTopology, ("", None)),
(stf.ST_SRID, (None,)),
Expand Down
13 changes: 13 additions & 0 deletions python/tests/sql/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ def test_st_bestsrid(self):
actual = function_df.take(1)[0][0]
assert actual == 3395

def test_st_shiftlongitude(self):
function_df = self.spark.sql("select ST_ShiftLongitude(ST_GeomFromWKT('POLYGON((179 10, -179 10, -179 20, 179 20, 179 10))'))")
actual = function_df.take(1)[0][0].wkt
assert actual == "POLYGON ((179 10, 181 10, 181 20, 179 20, 179 10))"

function_df = self.spark.sql("select ST_ShiftLongitude(ST_GeomFromWKT('POINT(-179 10)'))")
actual = function_df.take(1)[0][0].wkt
assert actual == "POINT (181 10)"

function_df = self.spark.sql("select ST_ShiftLongitude(ST_GeomFromWKT('LINESTRING(179 10, 181 10)'))")
actual = function_df.take(1)[0][0].wkt
assert actual == "LINESTRING (179 10, -179 10)"

def test_st_envelope(self):
polygon_from_wkt = self.spark.read.format("csv"). \
option("delimiter", "\t"). \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,12 @@
(SuiteContainer.empty()
.with_function_name("ST_BestSRID")
.with_arguments(["ST_GeomFromText('POINT (-177 60)')"])
.with_expected_result(32601))
.with_expected_result(32601)),
(SuiteContainer.empty()
.with_function_name("ST_ShiftLongitude")
.with_arguments(["ST_GeomFromText('POINT (-177 60)')"])
.with_expected_result("POINT (183 60)")
.with_transform("ST_AsText"))
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ public void test_ST_BestSRID() {
);
}

@Test
public void test_ST_ShiftLongitude() {
registerUDF("ST_ShiftLongitude", byte[].class);
verifySqlSingleRes(
"select sedona.ST_AsText(sedona.ST_ShiftLongitude(sedona.ST_GeomFromText('LINESTRING (179.95 10, -179.95 10)')))",
"LINESTRING (179.95 10, 180.05 10)"
);
}

@Test
public void test_ST_BuildArea() {
registerUDF("ST_BuildArea", byte[].class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ public void test_ST_BestSRID() {
);
}

@Test
public void test_ST_ShiftLongitude() {
registerUDFV2("ST_ShiftLongitude", String.class);
verifySqlSingleRes(
"select sedona.ST_ShiftLongitude(ST_GeometryFromWKT('POINT (-179 60)'))",
"{\"type\":\"Point\",\"coordinates\":[181.0,60.0]}"
);
}

@Test
public void test_ST_Boundary() {
registerUDFV2("ST_Boundary", String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ public static int ST_BestSRID(byte[] geometry) {
);
}

@UDFAnnotations.ParamMeta(argNames = {"geometry"})
public static byte[] ST_ShiftLongitude(byte[] geometry) {
return GeometrySerde.serialize(
Functions.shiftLongitude(
GeometrySerde.deserialize(geometry)
)
);
}

@UDFAnnotations.ParamMeta(argNames = {"geometry"})
public static byte[] ST_BuildArea(byte[] geometry) {
return GeometrySerde.serialize(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ public static int ST_BestSRID(String geometry) {
);
}

@UDFAnnotations.ParamMeta(argNames = {"geometry"}, argTypes = {"Geometry"})
public static String ST_ShiftLongitude(String geometry) {
return GeometrySerde.serGeoJson(
Functions.shiftLongitude(
GeometrySerde.deserGeoJson(geometry)
)
);
}

@UDFAnnotations.ParamMeta(argNames = {"geometry", "radius"}, argTypes = {"Geometry", "double"}, returnTypes = "Geometry")
public static String ST_Buffer(String geometry, double radius) {
return GeometrySerde.serGeoJson(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ object Catalog {
function[ST_NDims](),
function[ST_Buffer](),
function[ST_BestSRID](),
function[ST_ShiftLongitude](),
function[ST_Envelope](),
function[ST_Length](),
function[ST_Area](),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ case class ST_BestSRID(inputExpressions: Seq[Expression])
}
}

case class ST_ShiftLongitude(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.shiftLongitude _) {

protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
}
}

/**
* Return the bounding rectangle for a Geometry
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ object st_functions extends DataFrameAPI {
def ST_BestSRID(geometry: Column): Column = wrapExpression[ST_BestSRID](geometry)
def ST_BestSRID(geometry: String): Column = wrapExpression[ST_BestSRID](geometry)

def ST_ShiftLongitude(geometry: Column): Column = wrapExpression[ST_ShiftLongitude](geometry)
def ST_ShiftLongitude(geometry: String): Column = wrapExpression[ST_ShiftLongitude](geometry)

def ST_BuildArea(geometry: Column): Column = wrapExpression[ST_BuildArea](geometry)
def ST_BuildArea(geometry: String): Column = wrapExpression[ST_BuildArea](geometry)

Expand Down
Loading
Loading