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 883811b696 [GH-2731] Implement GeoSeries: frechet_distance,
hausdorff_distance, geom_equals, interpolate, project (#2732)
883811b696 is described below
commit 883811b6966feeb7396b0e969017ed85d3063743
Author: Jia Yu <[email protected]>
AuthorDate: Sun Mar 15 21:27:34 2026 -0700
[GH-2731] Implement GeoSeries: frechet_distance, hausdorff_distance,
geom_equals, interpolate, project (#2732)
---
.gitignore | 1 +
.../java/org/apache/sedona/common/Functions.java | 4 +-
.../java/org/apache/sedona/common/Predicates.java | 3 +
.../org/apache/sedona/common/FunctionsTest.java | 34 +++
.../org/apache/sedona/common/PredicatesTest.java | 17 ++
python/sedona/spark/geopandas/base.py | 257 +++++++++++++++++++++
python/sedona/spark/geopandas/geoseries.py | 85 +++++++
python/tests/geopandas/test_geoseries.py | 171 ++++++++++++++
.../tests/geopandas/test_match_geopandas_series.py | 77 ++++++
.../org/apache/sedona/snowflake/snowsql/UDFs.java | 2 +-
.../apache/sedona/snowflake/snowsql/UDFsV2.java | 2 +-
.../org/apache/sedona/sql/functionTestScala.scala | 24 ++
12 files changed, 674 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index cd609e393b..82c2646a07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@
/.scala-build
/.settings/
/.venv/
+/.venv-test/
/.vscode/
/bin/
/conf/
diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java
b/common/src/main/java/org/apache/sedona/common/Functions.java
index 3760c33115..0c7f487b65 100644
--- a/common/src/main/java/org/apache/sedona/common/Functions.java
+++ b/common/src/main/java/org/apache/sedona/common/Functions.java
@@ -1340,6 +1340,7 @@ public class Functions {
}
public static Geometry lineInterpolatePoint(Geometry geom, double fraction) {
+ if (geom.isEmpty()) return geom.getFactory().createPoint();
double length = geom.getLength();
LengthIndexedLine indexedLine = new LengthIndexedLine(geom);
Coordinate interPoint = indexedLine.extractPoint(length * fraction);
@@ -1485,7 +1486,8 @@ public class Functions {
return ConstrainedDelaunayTriangulator.triangulate(geom);
}
- public static double lineLocatePoint(Geometry geom, Geometry point) {
+ public static Double lineLocatePoint(Geometry geom, Geometry point) {
+ if (geom.isEmpty() || point.isEmpty()) return null;
double length = geom.getLength();
LengthIndexedLine indexedLine = new LengthIndexedLine(geom);
return indexedLine.indexOf(point.getCoordinate()) / length;
diff --git a/common/src/main/java/org/apache/sedona/common/Predicates.java
b/common/src/main/java/org/apache/sedona/common/Predicates.java
index 21fd11cff9..9a030748e5 100644
--- a/common/src/main/java/org/apache/sedona/common/Predicates.java
+++ b/common/src/main/java/org/apache/sedona/common/Predicates.java
@@ -56,6 +56,9 @@ public class Predicates {
}
public static boolean equals(Geometry leftGeometry, Geometry rightGeometry) {
+ if (leftGeometry.isEmpty() && rightGeometry.isEmpty()) {
+ return true;
+ }
return leftGeometry.equalsTopo(rightGeometry);
}
diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
index 4e9e6cd0a2..882a18de82 100644
--- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java
@@ -4407,6 +4407,40 @@ public class FunctionsTest extends TestBase {
assertEquals(expectedResult3, actual3, FP_TOLERANCE);
}
+ @Test
+ public void lineLocatePointEmpty() {
+ LineString emptyLine = GEOMETRY_FACTORY.createLineString();
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1));
+ assertNull(Functions.lineLocatePoint(emptyLine, point));
+ }
+
+ @Test
+ public void lineLocatePointEmptyPoint() {
+ LineString line =
+ GEOMETRY_FACTORY.createLineString(
+ new Coordinate[] {new Coordinate(0, 0), new Coordinate(1, 1)});
+ Geometry emptyPoint = GEOMETRY_FACTORY.createPoint();
+ assertNull(Functions.lineLocatePoint(line, emptyPoint));
+ }
+
+ @Test
+ public void lineLocatePointZeroLength() {
+ LineString zeroLen =
+ GEOMETRY_FACTORY.createLineString(
+ new Coordinate[] {new Coordinate(1, 1), new Coordinate(1, 1)});
+ Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(2, 2));
+ Double result = Functions.lineLocatePoint(zeroLen, point);
+ assertTrue(Double.isNaN(result));
+ }
+
+ @Test
+ public void lineInterpolatePointEmpty() {
+ LineString emptyLine = GEOMETRY_FACTORY.createLineString();
+ Geometry actual = Functions.lineInterpolatePoint(emptyLine, 0.5);
+ assertTrue(actual.isEmpty());
+ assertEquals("Point", actual.getGeometryType());
+ }
+
@Test
public void locateAlong() throws ParseException {
Geometry geom =
diff --git a/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
b/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
index 7e8616c976..8d48ae1402 100644
--- a/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
+++ b/common/src/test/java/org/apache/sedona/common/PredicatesTest.java
@@ -102,6 +102,23 @@ public class PredicatesTest extends TestBase {
assertFalse(Predicates.equals(gc1, gc3));
}
+ @Test
+ public void testEqualsEmptyGeometries() throws ParseException {
+ // Empty geometries of different types should be considered equal
+ Geometry pointEmpty = geomFromEWKT("POINT EMPTY");
+ Geometry multipolygonEmpty = geomFromEWKT("MULTIPOLYGON EMPTY");
+ assertTrue(Predicates.equals(pointEmpty, multipolygonEmpty));
+
+ // Empty geometries of the same type should be equal
+ Geometry linestringEmpty = geomFromEWKT("LINESTRING EMPTY");
+ Geometry linestringEmpty2 = geomFromEWKT("LINESTRING EMPTY");
+ assertTrue(Predicates.equals(linestringEmpty, linestringEmpty2));
+
+ // An empty geometry and a non-empty geometry should not be equal
+ Geometry point = geomFromEWKT("POINT(0 0)");
+ assertFalse(Predicates.equals(pointEmpty, point));
+ }
+
@Test
public void testRelateBoolean() throws ParseException {
Geometry geom1 = geomFromEWKT("POINT(1 2)");
diff --git a/python/sedona/spark/geopandas/base.py
b/python/sedona/spark/geopandas/base.py
index ac6827e097..858bd6e72f 100644
--- a/python/sedona/spark/geopandas/base.py
+++ b/python/sedona/spark/geopandas/base.py
@@ -2363,6 +2363,263 @@ class GeoFrame(metaclass=ABCMeta):
"""
return _delegate_to_geometry_column("distance", self, other, align)
+ def frechet_distance(self, other, align=None, densify=None):
+ """Returns a ``Series`` containing the discrete Fréchet distance to
aligned `other`.
+
+ The Fréchet distance is a measure of similarity: it is the greatest
distance
+ between any point in A and the closest point in B. The discrete
distance is an
+ approximation of this metric: only vertices are considered. The
parameter
+ ``densify`` makes this approximation less coarse by splitting the line
segments
+ between vertices before computing the distance.
+
+ The operation works on a 1-to-1 row-wise manner.
+
+ Parameters
+ ----------
+ other : GeoSeries or geometric object
+ The GeoSeries (elementwise) or geometric object to find the
+ distance to.
+ align : bool | None (default None)
+ If True, automatically aligns GeoSeries based on their indices.
None defaults to True.
+ If False, the order of elements is preserved.
+ densify : float, optional
+ The densify parameter is not supported by Sedona.
+ Passing a value will raise a ``NotImplementedError``.
+
+ Returns
+ -------
+ Series (float)
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> from shapely.geometry import LineString
+ >>> s1 = GeoSeries(
+ ... [
+ ... LineString([(0, 0), (1, 0), (2, 0)]),
+ ... LineString([(0, 0), (1, 1)]),
+ ... ]
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... LineString([(0, 1), (1, 2), (2, 1)]),
+ ... LineString([(1, 0), (2, 1)]),
+ ... ]
+ ... )
+
+ >>> s1.frechet_distance(s2)
+ 0 2.0
+ 1 1.0
+ dtype: float64
+
+ See also
+ --------
+ GeoSeries.hausdorff_distance
+ """
+ return _delegate_to_geometry_column(
+ "frechet_distance", self, other, align, densify
+ )
+
+ def hausdorff_distance(self, other, align=None, densify=None):
+ """Returns a ``Series`` containing the Hausdorff distance to aligned
`other`.
+
+ The Hausdorff distance is the largest distance consisting of any point
in `self`
+ with the nearest point in `other`.
+
+ The operation works on a 1-to-1 row-wise manner.
+
+ Parameters
+ ----------
+ other : GeoSeries or geometric object
+ The GeoSeries (elementwise) or geometric object to find the
+ distance to.
+ align : bool | None (default None)
+ If True, automatically aligns GeoSeries based on their indices.
None defaults to True.
+ If False, the order of elements is preserved.
+ densify : float, optional
+ The fraction by which to densify each segment. Each segment will be
+ split into a number of equal-length subsegments whose fraction of
+ the segment length is closest to the given fraction.
+
+ Returns
+ -------
+ Series (float)
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> from shapely.geometry import LineString
+ >>> s1 = GeoSeries(
+ ... [
+ ... LineString([(0, 0), (1, 0), (2, 0)]),
+ ... LineString([(0, 0), (1, 1)]),
+ ... ]
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... LineString([(0, 1), (1, 2), (2, 1)]),
+ ... LineString([(1, 0), (2, 1)]),
+ ... ]
+ ... )
+
+ >>> s1.hausdorff_distance(s2)
+ 0 2.0
+ 1 1.0
+ dtype: float64
+
+ See also
+ --------
+ GeoSeries.frechet_distance
+ """
+ return _delegate_to_geometry_column(
+ "hausdorff_distance", self, other, align, densify
+ )
+
+ def geom_equals(self, other, align=None):
+ """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry equal to `other`.
+
+ An object is said to be equal to `other` if its set-theoretic
+ boundary, interior, and exterior coincides with those of the other.
+
+ The operation works on a 1-to-1 row-wise manner.
+
+ Parameters
+ ----------
+ other : GeoSeries or geometric object
+ The GeoSeries (elementwise) or geometric object to test for
+ equality.
+ align : bool | None (default None)
+ If True, automatically aligns GeoSeries based on their indices.
None defaults to True.
+ If False, the order of elements is preserved.
+
+ Returns
+ -------
+ Series (bool)
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> from shapely.geometry import Point
+ >>> s1 = GeoSeries(
+ ... [
+ ... Point(0, 0),
+ ... Point(1, 1),
+ ... Point(2, 2),
+ ... ]
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Point(0, 0),
+ ... Point(2, 2),
+ ... Point(2, 2),
+ ... ]
+ ... )
+
+ >>> s1.geom_equals(s2)
+ 0 True
+ 1 False
+ 2 True
+ dtype: bool
+
+ See also
+ --------
+ GeoSeries.geom_equals_exact
+ """
+ return _delegate_to_geometry_column("geom_equals", self, other, align)
+
+ def interpolate(self, distance, normalized=False):
+ """Return a point at the specified distance along each geometry.
+
+ Parameters
+ ----------
+ distance : float or Series of floats
+ Distance(s) along the geometries at which a point should be
+ returned. If np.array or pd.Series are used then it must have
+ same length as the GeoSeries.
+ normalized : bool (default False)
+ If True, ``distance`` will be interpreted as a fraction
+ of the geometric object's length.
+
+ Returns
+ -------
+ GeoSeries
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> from shapely.geometry import LineString
+ >>> s = GeoSeries(
+ ... [
+ ... LineString([(0, 0), (2, 0), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... ],
+ ... )
+
+ >>> s.interpolate(1)
+ 0 POINT (1 0)
+ 1 POINT (0.70711 0.70711)
+ 2 POINT (1.29289 0.70711)
+ dtype: geometry
+
+ See also
+ --------
+ GeoSeries.project
+ """
+ return _delegate_to_geometry_column("interpolate", self, distance,
normalized)
+
+ def project(self, other, normalized=False, align=None):
+ """Return the distance along each geometry nearest to `other`.
+
+ The operation works on a 1-to-1 row-wise manner.
+
+ The project method is the inverse of interpolate.
+
+ Parameters
+ ----------
+ other : GeoSeries or geometric object
+ The *other* geometry to compute the projected point from.
+ normalized : bool (default False)
+ If True, return the distance normalized to the length of the
object.
+ align : bool | None (default None)
+ If True, automatically aligns GeoSeries based on their indices.
None defaults to True.
+ If False, the order of elements is preserved.
+
+ Returns
+ -------
+ Series (float)
+
+ Examples
+ --------
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> from shapely.geometry import LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... LineString([(0, 0), (2, 0), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... ],
+ ... )
+
+ >>> s.project(Point(1, 0))
+ 0 1.000000
+ 1 0.707107
+ 2 0.707107
+ dtype: float64
+
+ >>> s.project(Point(1, 0), normalized=True)
+ 0 0.207107
+ 1 0.250000
+ 2 0.250000
+ dtype: float64
+
+ See also
+ --------
+ GeoSeries.interpolate
+ """
+ return _delegate_to_geometry_column("project", self, other,
normalized, align)
+
def intersection(self, other, align=None):
"""Returns a ``GeoSeries`` of the intersection of points in each
aligned geometry with `other`.
diff --git a/python/sedona/spark/geopandas/geoseries.py
b/python/sedona/spark/geopandas/geoseries.py
index 96631f3679..f3d96dba30 100644
--- a/python/sedona/spark/geopandas/geoseries.py
+++ b/python/sedona/spark/geopandas/geoseries.py
@@ -1330,6 +1330,91 @@ class GeoSeries(GeoFrame, pspd.Series):
)
return result
+ def frechet_distance(self, other, align=None, densify=None) -> pspd.Series:
+ if densify is not None:
+ raise NotImplementedError(
+ "Sedona does not support the densify parameter for
frechet_distance."
+ )
+
+ other_series, extended = self._make_series_of_val(other)
+ align = False if extended else align
+
+ spark_expr = stf.ST_FrechetDistance(F.col("L"), F.col("R"))
+ result = self._row_wise_operation(
+ spark_expr,
+ other_series,
+ align,
+ default_val=None,
+ )
+ return result
+
+ def hausdorff_distance(self, other, align=None, densify=None) ->
pspd.Series:
+ other_series, extended = self._make_series_of_val(other)
+ align = False if extended else align
+
+ if densify is not None:
+ spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"),
densify)
+ else:
+ spark_expr = stf.ST_HausdorffDistance(F.col("L"), F.col("R"))
+ result = self._row_wise_operation(
+ spark_expr,
+ other_series,
+ align,
+ default_val=None,
+ )
+ return result
+
+ def geom_equals(self, other, align=None) -> pspd.Series:
+ other_series, extended = self._make_series_of_val(other)
+ align = False if extended else align
+
+ spark_expr = stp.ST_Equals(F.col("L"), F.col("R"))
+ result = self._row_wise_operation(
+ spark_expr,
+ other_series,
+ align,
+ returns_geom=False,
+ default_val=False,
+ )
+ return _to_bool(result)
+
+ def interpolate(self, distance, normalized=False) -> "GeoSeries":
+ other_series, extended = self._make_series_of_val(distance)
+ align = not extended
+
+ if normalized:
+ spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), F.col("R"))
+ else:
+ length = stf.ST_Length(F.col("L"))
+ fraction = F.when(length == 0, F.lit(0.0)).otherwise(F.col("R") /
length)
+ spark_expr = stf.ST_LineInterpolatePoint(F.col("L"), fraction)
+ return self._row_wise_operation(
+ spark_expr,
+ other_series,
+ align=align,
+ returns_geom=True,
+ )
+
+ def project(self, other, normalized=False, align=None) -> pspd.Series:
+ other_series, extended = self._make_series_of_val(other)
+ align = False if extended else align
+
+ if normalized:
+ spark_expr = stf.ST_LineLocatePoint(F.col("L"), F.col("R"))
+ else:
+ locate = stf.ST_LineLocatePoint(F.col("L"), F.col("R"))
+ length = stf.ST_Length(F.col("L"))
+ spark_expr = F.when(locate.isNull(), F.lit(None)).otherwise(
+ F.when(length == 0, F.lit(0.0)).otherwise(locate * length)
+ )
+ result = self._row_wise_operation(
+ spark_expr,
+ other_series,
+ align,
+ default_val=None,
+ )
+ return result
+
def intersection(
self, other: Union["GeoSeries", BaseGeometry], align: Union[bool,
None] = None
) -> "GeoSeries":
diff --git a/python/tests/geopandas/test_geoseries.py
b/python/tests/geopandas/test_geoseries.py
index 9bc572a151..e929e3e43d 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -2635,6 +2635,177 @@ e": "Feature", "properties": {}, "geometry": {"type":
"Point", "coordinates": [3
expected = pd.Series(["FF2F11212", "212101212"])
self.check_pd_series_equal(result, expected)
+ def test_frechet_distance(self):
+ s1 = GeoSeries(
+ [
+ LineString([(0, 0), (1, 0), (2, 0)]),
+ LineString([(0, 0), (1, 1)]),
+ ]
+ )
+ s2 = GeoSeries(
+ [
+ LineString([(0, 1), (1, 2), (2, 1)]),
+ LineString([(1, 0), (2, 1)]),
+ ]
+ )
+
+ result = s1.frechet_distance(s2, align=False)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(result, expected)
+
+ # Test with single geometry
+ line = LineString([(0, 1), (1, 2), (2, 1)])
+ result = s1.frechet_distance(line)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(result, expected)
+
+ # Test that GeoDataFrame works too
+ df_result = s1.to_geoframe().frechet_distance(s2, align=False)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(df_result, expected)
+
+ # Test that densify raises NotImplementedError
+ with pytest.raises(NotImplementedError):
+ s1.frechet_distance(s2, densify=0.5)
+
+ def test_hausdorff_distance(self):
+ s1 = GeoSeries(
+ [
+ LineString([(0, 0), (1, 0), (2, 0)]),
+ LineString([(0, 0), (1, 1)]),
+ ]
+ )
+ s2 = GeoSeries(
+ [
+ LineString([(0, 1), (1, 2), (2, 1)]),
+ LineString([(1, 0), (2, 1)]),
+ ]
+ )
+
+ result = s1.hausdorff_distance(s2, align=False)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(result, expected)
+
+ # Test with single geometry
+ line = LineString([(0, 1), (1, 2), (2, 1)])
+ result = s1.hausdorff_distance(line)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(result, expected)
+
+ # Test that GeoDataFrame works too
+ df_result = s1.to_geoframe().hausdorff_distance(s2, align=False)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(df_result, expected)
+
+ # Test with densify parameter
+ result = s1.hausdorff_distance(s2, densify=0.5, align=False)
+ expected = pd.Series([2.0, 1.0])
+ self.check_pd_series_equal(result, expected)
+
+ def test_geom_equals(self):
+ s1 = GeoSeries(
+ [
+ Point(0, 0),
+ Point(1, 1),
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+ ]
+ )
+ s2 = GeoSeries(
+ [
+ Point(0, 0),
+ Point(2, 2),
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+ ]
+ )
+
+ result = s1.geom_equals(s2, align=False)
+ expected = pd.Series([True, False, True])
+ self.check_pd_series_equal(result, expected)
+
+ # Test with single geometry
+ result = s1.geom_equals(Point(0, 0))
+ expected = pd.Series([True, False, False])
+ self.check_pd_series_equal(result, expected)
+
+ # Test that GeoDataFrame works too
+ df_result = s1.to_geoframe().geom_equals(s2, align=False)
+ expected = pd.Series([True, False, True])
+ self.check_pd_series_equal(df_result, expected)
+
+ def test_interpolate(self):
+ s = GeoSeries(
+ [
+ LineString([(0, 0), (2, 0), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ LineString([(2, 0), (0, 2)]),
+ ]
+ )
+
+ # Test with absolute distance
+ result = s.interpolate(1)
+ expected = gpd.GeoSeries(
+ [
+ Point(1, 0),
+ Point(0.7071067811865476, 0.7071067811865476),
+ Point(1.2928932188134524, 0.7071067811865476),
+ ]
+ )
+ self.check_sgpd_equals_gpd(result, expected)
+
+ # Test with normalized distance
+ result = s.interpolate(0.5, normalized=True)
+ expected = gpd.GeoSeries(s.to_geopandas().interpolate(0.5,
normalized=True))
+ self.check_sgpd_equals_gpd(result, expected)
+
+ # Test that GeoDataFrame works too
+ df_result = s.to_geoframe().interpolate(1)
+ expected = gpd.GeoSeries(
+ [
+ Point(1, 0),
+ Point(0.7071067811865476, 0.7071067811865476),
+ Point(1.2928932188134524, 0.7071067811865476),
+ ]
+ )
+ self.check_sgpd_equals_gpd(df_result, expected)
+
+ def test_project(self):
+ s = GeoSeries(
+ [
+ LineString([(0, 0), (2, 0), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ LineString([(2, 0), (0, 2)]),
+ ]
+ )
+
+ # Test with a single point
+ result = s.project(Point(1, 0))
+ expected = pd.Series([1.0, 0.7071067811865476, 0.7071067811865476])
+ self.check_pd_series_equal(result, expected)
+
+ # Test with normalized=True
+ result = s.project(Point(1, 0), normalized=True)
+ expected = pd.Series(s.to_geopandas().project(Point(1, 0),
normalized=True))
+ self.check_pd_series_equal(result, expected)
+
+ # Test with two GeoSeries
+ s2 = GeoSeries(
+ [
+ Point(1, 0),
+ Point(1, 0),
+ Point(2, 1),
+ ]
+ )
+ result = s.project(s2, align=False)
+ expected = pd.Series(
+ s.to_geopandas().project(gpd.GeoSeries(s2.to_geopandas()),
align=False)
+ )
+ self.check_pd_series_equal(result, expected)
+
+ # Test that GeoDataFrame works too
+ df_result = s.to_geoframe().project(Point(1, 0))
+ expected = pd.Series([1.0, 0.7071067811865476, 0.7071067811865476])
+ self.check_pd_series_equal(df_result, expected)
+
def test_set_crs(self):
geo_series = sgpd.GeoSeries([Point(0, 0), Point(1, 1)],
name="geometry")
assert geo_series.crs == None
diff --git a/python/tests/geopandas/test_match_geopandas_series.py
b/python/tests/geopandas/test_match_geopandas_series.py
index 6dd5af9dff..4c1af9eab5 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -1285,6 +1285,83 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
)
self.check_pd_series_equal(sgpd_result, gpd_result)
+ def test_frechet_distance(self):
+ line_pairs = [
+ (self.linestrings, self.linestrings),
+ (self.linearrings, self.linearrings),
+ (self.linestrings, self.linearrings),
+ ]
+ for geom, geom2 in line_pairs:
+ sgpd_result = GeoSeries(geom).frechet_distance(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).frechet_distance(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).frechet_distance(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).frechet_distance(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_hausdorff_distance(self):
+ for geom, geom2 in self.pairs:
+ sgpd_result = GeoSeries(geom).hausdorff_distance(
+ GeoSeries(geom2), align=True
+ )
+ gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).hausdorff_distance(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).hausdorff_distance(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_geom_equals(self):
+ for geom, geom2 in self.pairs:
+ sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).geom_equals(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).geom_equals(GeoSeries(geom2),
align=False)
+ gpd_result = gpd.GeoSeries(geom).geom_equals(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_interpolate(self):
+ for geom in [self.linestrings, self.linearrings]:
+ sgpd_result = GeoSeries(geom).interpolate(1.0)
+ gpd_result = gpd.GeoSeries(geom).interpolate(1.0)
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+ sgpd_result = GeoSeries(geom).interpolate(0.5, normalized=True)
+ gpd_result = gpd.GeoSeries(geom).interpolate(0.5, normalized=True)
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+ def test_project(self):
+ for geom in [self.linestrings, self.linearrings]:
+ point = Point(1, 1)
+ sgpd_result = GeoSeries(geom).project(point)
+ gpd_result = gpd.GeoSeries(geom).project(point)
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ sgpd_result = GeoSeries(geom).project(point, normalized=True)
+ gpd_result = gpd.GeoSeries(geom).project(point, normalized=True)
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
def test_set_crs(self):
for geom in self.geoms:
if isinstance(geom[0], Polygon) and geom[0] == Polygon():
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 f9f56101a9..dafdc8fc97 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
@@ -638,7 +638,7 @@ public class UDFs {
}
@UDFAnnotations.ParamMeta(argNames = {"geom", "point"})
- public static double ST_LineLocatePoint(byte[] geom, byte[] point) {
+ public static Double ST_LineLocatePoint(byte[] geom, byte[] point) {
return Functions.lineLocatePoint(
GeometrySerde.deserialize(geom), GeometrySerde.deserialize(point));
}
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 4861fce4ce..80fbb34d07 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
@@ -812,7 +812,7 @@ public class UDFsV2 {
@UDFAnnotations.ParamMeta(
argNames = {"geom", "point"},
argTypes = {"Geometry", "Geometry"})
- public static double ST_LineLocatePoint(String geom, String point) {
+ public static Double ST_LineLocatePoint(String geom, String point) {
return Functions.lineLocatePoint(
GeometrySerde.deserGeoJson(geom), GeometrySerde.deserGeoJson(point));
}
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 4c83046cec..377e598b48 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
@@ -2497,6 +2497,26 @@ class functionTestScala
assert(result(3).get(0).asInstanceOf[Double] == 1.0)
}
+ it("Should return null for ST_LineLocatePoint with empty geometry") {
+ val df = sparkSession.sql(
+ "SELECT ST_LineLocatePoint(ST_GeomFromWKT('LINESTRING EMPTY'),
ST_GeomFromWKT('POINT(1 1)')) AS loc")
+ assert(df.take(1)(0).isNullAt(0))
+ }
+
+ it("Should return null for ST_LineLocatePoint with empty point") {
+ val df = sparkSession.sql(
+ "SELECT ST_LineLocatePoint(ST_GeomFromWKT('LINESTRING(0 0, 1 1)'),
ST_GeomFromWKT('POINT EMPTY')) AS loc")
+ assert(df.take(1)(0).isNullAt(0))
+ }
+
+ it("Should return POINT EMPTY for ST_LineInterpolatePoint with empty
geometry") {
+ val df = sparkSession.sql(
+ "SELECT ST_LineInterpolatePoint(ST_GeomFromWKT('LINESTRING EMPTY'), 0.5)
AS pt")
+ val geom = df.take(1)(0).get(0).asInstanceOf[Geometry]
+ assert(geom.isEmpty)
+ assert(geom.getGeometryType == "Point")
+ }
+
it("Should pass ST_Multi") {
val df = sparkSession.sql("select ST_Astext(ST_Multi(ST_Point(1.0,1.0)))")
val result = df.collect()
@@ -3284,6 +3304,10 @@ class functionTestScala
val expected = expectedResult
assertEquals(expected, actual, 1e-9)
}
+ // Empty geometries should return null
+ val dfEmpty = sparkSession.sql(
+ "SELECT ST_FrechetDistance(ST_GeomFromWKT('LINESTRING (0 0, 1 0)'),
ST_GeomFromWKT('POINT EMPTY'))")
+ assert(dfEmpty.take(1)(0).isNullAt(0))
}
it("should pass ST_Affine") {