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 8d4fb1bbba [GH-2060] Geopandas.GeoSeries: Implement crosses, overlaps,
touches, within, covers, covered_by, contains, distance (#2061)
8d4fb1bbba is described below
commit 8d4fb1bbbafde8db01c9d845ba4d4261636c4681
Author: Peter Nguyen <[email protected]>
AuthorDate: Fri Jul 18 11:13:14 2025 -0700
[GH-2060] Geopandas.GeoSeries: Implement crosses, overlaps, touches,
within, covers, covered_by, contains, distance (#2061)
* Implement functions
* Skip covers and covered_by tests for old versions
* Skip covered by for shapely < 2
* Update python/tests/geopandas/test_match_geopandas_series.py
Co-authored-by: Copilot <[email protected]>
---------
Co-authored-by: Feng Zhang <[email protected]>
Co-authored-by: Copilot <[email protected]>
---
python/sedona/geopandas/geoseries.py | 1021 ++++++++++++++++++--
python/tests/geopandas/test_geopandas_base.py | 6 +
python/tests/geopandas/test_geoseries.py | 269 +++++-
.../tests/geopandas/test_match_geopandas_series.py | 172 +++-
4 files changed, 1409 insertions(+), 59 deletions(-)
diff --git a/python/sedona/geopandas/geoseries.py
b/python/sedona/geopandas/geoseries.py
index 1b0ebe66a3..9a42285a23 100644
--- a/python/sedona/geopandas/geoseries.py
+++ b/python/sedona/geopandas/geoseries.py
@@ -1052,6 +1052,8 @@ class GeoSeries(GeoFrame, pspd.Series):
index,
align=False,
rename="get_geometry",
+ returns_geom=True,
+ default_val=None,
)
@property
@@ -1395,22 +1397,709 @@ class GeoSeries(GeoFrame, pspd.Series):
geom = ps_series.iloc[0]
return geom
+ def crosses(self, other, align=None) -> pspd.Series:
+ """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that cross `other`.
+
+ An object is said to cross `other` if its `interior` intersects the
+ `interior` of the other but does not contain it, and the dimension of
+ the intersection is less than the dimension of the one or the other.
+
+ Note: Unlike Geopandas, Sedona's implementation always return NULL
when GeometryCollection is involved.
+
+ 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 if is
+ crossed.
+ 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.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... Point(0, 1),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... LineString([(1, 0), (1, 3)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... Point(1, 1),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 1 LINESTRING (0 0, 2 2)
+ 2 LINESTRING (2 0, 0 2)
+ 3 POINT (0 1)
+ dtype: geometry
+ >>> s2
+ 1 LINESTRING (1 0, 1 3)
+ 2 LINESTRING (2 0, 0 2)
+ 3 POINT (1 1)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries crosses a single
+ geometry:
+
+ >>> line = LineString([(-1, 1), (3, 1)])
+ >>> s.crosses(line)
+ 0 True
+ 1 True
+ 2 True
+ 3 False
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s.crosses(s2, align=True)
+ 0 False
+ 1 True
+ 2 False
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s.crosses(s2, align=False)
+ 0 True
+ 1 True
+ 2 False
+ 3 False
+ dtype: bool
+
+ Notice that a line does not cross a point that it contains.
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``crosses`` *any* element of the other one.
+
+ See also
+ --------
+ GeoSeries.disjoint
+ GeoSeries.intersects
+
+ """
+ # Sedona does not support GeometryCollection (errors), so we return
NULL for now to avoid error
+ select = """
+ CASE
+ WHEN GeometryType(`L`) == 'GEOMETRYCOLLECTION' OR
GeometryType(`R`) == 'GEOMETRYCOLLECTION' THEN NULL
+ ELSE ST_Crosses(`L`, `R`)
+ END
+ """
+
+ result = self._row_wise_operation(
+ select, other, align, rename="crosses", default_val="FALSE"
+ )
+ return to_bool(result)
+
+ def disjoint(self, other, align=None):
+ # Implementation of the abstract method
+ raise NotImplementedError("This method is not implemented yet.")
+
def intersects(
self, other: Union["GeoSeries", BaseGeometry], align: Union[bool,
None] = None
) -> pspd.Series:
"""Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
each aligned geometry that intersects `other`.
- An object is said to intersect `other` if its `boundary` and `interior`
- intersects in any way with those of the other.
+ An object is said to intersect `other` if its `boundary` and `interior`
+ intersects in any way 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 if is
+ intersected.
+ 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.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... Point(0, 1),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... LineString([(1, 0), (1, 3)]),
+ ... LineString([(2, 0), (0, 2)]),
+ ... Point(1, 1),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 1 LINESTRING (0 0, 2 2)
+ 2 LINESTRING (2 0, 0 2)
+ 3 POINT (0 1)
+ dtype: geometry
+
+ >>> s2
+ 1 LINESTRING (1 0, 1 3)
+ 2 LINESTRING (2 0, 0 2)
+ 3 POINT (1 1)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries crosses a single
+ geometry:
+
+ >>> line = LineString([(-1, 1), (3, 1)])
+ >>> s.intersects(line)
+ 0 True
+ 1 True
+ 2 True
+ 3 True
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s.intersects(s2, align=True)
+ 0 False
+ 1 True
+ 2 True
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s.intersects(s2, align=False)
+ 0 True
+ 1 True
+ 2 True
+ 3 True
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``crosses`` *any* element of the other one.
+
+ See also
+ --------
+ GeoSeries.disjoint
+ GeoSeries.crosses
+ GeoSeries.touches
+ GeoSeries.intersection
+ """
+
+ result = self._row_wise_operation(
+ "ST_Intersects(`L`, `R`)",
+ other,
+ align,
+ rename="intersects",
+ default_val="FALSE",
+ )
+ return to_bool(result)
+
+ def overlaps(self, other, align=None) -> pspd.Series:
+ """Returns True for all aligned geometries that overlap other, else
False.
+
+ In the original Geopandas, Geometries overlap if they have more than
one but not all
+ points in common, have the same dimension, and the intersection of the
+ interiors of the geometries has the same dimension as the geometries
+ themselves.
+
+ However, in Sedona, we return True in the case where the geometries
points match.
+
+ Note: Sedona's behavior may also differ from Geopandas for
GeometryCollections.
+
+ 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 if
+ overlaps.
+ 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.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, MultiPoint, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... MultiPoint([(0, 0), (0, 1)]),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 0), (0, 2)]),
+ ... LineString([(0, 1), (1, 1)]),
+ ... LineString([(1, 1), (3, 3)]),
+ ... Point(0, 1),
+ ... ],
+ ... )
+
+ We can check if each geometry of GeoSeries overlaps a single
+ geometry:
+
+ >>> polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
+ >>> s.overlaps(polygon)
+ 0 True
+ 1 True
+ 2 False
+ 3 False
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We align both GeoSeries
+ based on index values and compare elements with the same index.
+
+ >>> s.overlaps(s2)
+ 0 False
+ 1 True
+ 2 False
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s.overlaps(s2, align=False)
+ 0 True
+ 1 False
+ 2 True
+ 3 False
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``overlaps`` *any* element of the other one.
+
+ See also
+ --------
+ GeoSeries.crosses
+ GeoSeries.intersects
+
+ """
+ # Note: We cannot efficiently match geopandas behavior because
Sedona's ST_Overlaps returns True for equal geometries
+ # ST_Overlaps(`L`, `R`) AND ST_Equals(`L`, `R`) does not work because
ST_Equals errors on invalid geometries
+
+ result = self._row_wise_operation(
+ "ST_Overlaps(`L`, `R`)",
+ other,
+ align,
+ rename="overlaps",
+ default_val="FALSE",
+ )
+ return to_bool(result)
+
+ def touches(self, other, align=None) -> pspd.Series:
+ """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that touches `other`.
+
+ An object is said to touch `other` if it has at least one point in
+ common with `other` and its interior does not intersect with any part
+ of the other. Overlapping features therefore do not touch.
+
+ Note: Sedona's behavior may also differ from Geopandas for
GeometryCollections.
+
+ 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 if is
+ touched.
+ 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 shapely.geometry import Polygon, LineString, MultiPoint, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... MultiPoint([(0, 0), (0, 1)]),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (-2, 0), (0, -2)]),
+ ... LineString([(0, 1), (1, 1)]),
+ ... LineString([(1, 1), (3, 0)]),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 1 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 2 LINESTRING (0 0, 2 2)
+ 3 MULTIPOINT ((0 0), (0 1))
+ dtype: geometry
+
+ >>> s2
+ 1 POLYGON ((0 0, -2 0, 0 -2, 0 0))
+ 2 LINESTRING (0 1, 1 1)
+ 3 LINESTRING (1 1, 3 0)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries touches a single
+ geometry:
+
+ >>> line = LineString([(0, 0), (-1, -2)])
+ >>> s.touches(line)
+ 0 True
+ 1 True
+ 2 True
+ 3 True
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s.touches(s2, align=True)
+ 0 False
+ 1 True
+ 2 True
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s.touches(s2, align=False)
+ 0 True
+ 1 False
+ 2 True
+ 3 False
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``touches`` *any* element of the other one.
+
+ See also
+ --------
+ GeoSeries.overlaps
+ GeoSeries.intersects
+
+ """
+
+ result = self._row_wise_operation(
+ "ST_Touches(`L`, `R`)",
+ other,
+ align,
+ rename="touches",
+ default_val="FALSE",
+ )
+ return to_bool(result)
+
+ def within(self, other, align=None) -> pspd.Series:
+ """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that is within `other`.
+
+ An object is said to be within `other` if at least one of its points
is located
+ in the `interior` and no points are located in the `exterior` of the
other.
+ If either object is empty, this operation returns ``False``.
+
+ This is the inverse of `contains` in the sense that the
+ expression ``a.within(b) == b.contains(a)`` always evaluates to
+ ``True``.
+
+ Note: Sedona's behavior may also differ from Geopandas for
GeometryCollections and for geometries that are equal.
+
+ 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 if each
+ geometry is within.
+ 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 shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (1, 2), (0, 2)]),
+ ... LineString([(0, 0), (0, 2)]),
+ ... Point(0, 1),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 1), (0, 1)]),
+ ... LineString([(0, 0), (0, 2)]),
+ ... LineString([(0, 0), (0, 1)]),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 1 POLYGON ((0 0, 1 2, 0 2, 0 0))
+ 2 LINESTRING (0 0, 0 2)
+ 3 POINT (0 1)
+ dtype: geometry
+
+ >>> s2
+ 1 POLYGON ((0 0, 1 1, 0 1, 0 0))
+ 2 LINESTRING (0 0, 0 2)
+ 3 LINESTRING (0 0, 0 1)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries is within a single
+ geometry:
+
+ >>> polygon = Polygon([(0, 0), (2, 2), (0, 2)])
+ >>> s.within(polygon)
+ 0 True
+ 1 True
+ 2 False
+ 3 False
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s2.within(s)
+ 0 False
+ 1 False
+ 2 True
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s2.within(s, align=False)
+ 1 True
+ 2 False
+ 3 True
+ 4 True
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries is ``within`` any element of the other one.
+
+ See also
+ --------
+ GeoSeries.contains
+ """
+ result = self._row_wise_operation(
+ "ST_Within(`L`, `R`)",
+ other,
+ align,
+ rename="within",
+ default_val="FALSE",
+ )
+ return to_bool(result)
+
+ def covers(self, other, align=None) -> pspd.Series:
+ """
+ Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that is entirely covering `other`.
+
+ An object A is said to cover another object B if no points of B lie
+ in the exterior of A.
+ If either object is empty, this operation returns ``False``.
+
+ Note: Sedona's implementation instead returns False for identical
geometries.
+ Sedona's behavior may also differ from Geopandas for
GeometryCollections.
+
+ The operation works on a 1-to-1 row-wise manner.
+
+ See
+
https://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html
+ for reference.
+
+ Parameters
+ ----------
+ other : Geoseries or geometric object
+ The Geoseries (elementwise) or geometric object to check is being
covered.
+ 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 shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... Point(0, 0),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ ... Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ ... LineString([(1, 1), (1.5, 1.5)]),
+ ... Point(0, 0),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))
+ 1 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 2 LINESTRING (0 0, 2 2)
+ 3 POINT (0 0)
+ dtype: geometry
+
+ >>> s2
+ 1 POLYGON ((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, ...
+ 2 POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))
+ 3 LINESTRING (1 1, 1.5 1.5)
+ 4 POINT (0 0)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries covers a single
+ geometry:
+
+ >>> poly = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
+ >>> s.covers(poly)
+ 0 True
+ 1 False
+ 2 False
+ 3 False
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s.covers(s2, align=True)
+ 0 False
+ 1 False
+ 2 False
+ 3 False
+ 4 False
+ dtype: bool
+
+ >>> s.covers(s2, align=False)
+ 0 True
+ 1 False
+ 2 True
+ 3 True
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``covers`` any element of the other one.
+
+ See also
+ --------
+ GeoSeries.covered_by
+ GeoSeries.overlaps
+ """
+ result = self._row_wise_operation(
+ "ST_Covers(`L`, `R`)",
+ other,
+ align,
+ rename="covers",
+ default_val="FALSE",
+ )
+ return to_bool(result)
+
+ def covered_by(self, other, align=None) -> pspd.Series:
+ """
+ Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that is entirely covered by `other`.
+
+ An object A is said to cover another object B if no points of B lie
+ in the exterior of A.
+
+ Note: Sedona's implementation instead returns False for identical
geometries.
+ Sedona's behavior may differ from Geopandas for GeometryCollections.
The operation works on a 1-to-1 row-wise manner.
+ See
+
https://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html
+ for reference.
+
Parameters
----------
- other : GeoSeries or geometric object
- The GeoSeries (elementwise) or geometric object to test if is
- intersected.
+ other : Geoseries or geometric object
+ The Geoseries (elementwise) or geometric object to check is being
covered.
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.
@@ -1421,45 +2110,45 @@ class GeoSeries(GeoFrame, pspd.Series):
Examples
--------
- >>> from sedona.geopandas import GeoSeries
>>> from shapely.geometry import Polygon, LineString, Point
>>> s = GeoSeries(
... [
- ... Polygon([(0, 0), (2, 2), (0, 2)]),
- ... LineString([(0, 0), (2, 2)]),
- ... LineString([(2, 0), (0, 2)]),
- ... Point(0, 1),
+ ... Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ ... Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ ... LineString([(1, 1), (1.5, 1.5)]),
+ ... Point(0, 0),
... ],
... )
>>> s2 = GeoSeries(
... [
- ... LineString([(1, 0), (1, 3)]),
- ... LineString([(2, 0), (0, 2)]),
- ... Point(1, 1),
- ... Point(0, 1),
+ ... Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... LineString([(0, 0), (2, 2)]),
+ ... Point(0, 0),
... ],
... index=range(1, 5),
... )
>>> s
- 0 POLYGON ((0 0, 2 2, 0 2, 0 0))
- 1 LINESTRING (0 0, 2 2)
- 2 LINESTRING (2 0, 0 2)
- 3 POINT (0 1)
+ 0 POLYGON ((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, ...
+ 1 POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))
+ 2 LINESTRING (1 1, 1.5 1.5)
+ 3 POINT (0 0)
dtype: geometry
+ >>>
>>> s2
- 1 LINESTRING (1 0, 1 3)
- 2 LINESTRING (2 0, 0 2)
- 3 POINT (1 1)
- 4 POINT (0 1)
+ 1 POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))
+ 2 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 3 LINESTRING (0 0, 2 2)
+ 4 POINT (0 0)
dtype: geometry
- We can check if each geometry of GeoSeries crosses a single
+ We can check if each geometry of GeoSeries is covered by a single
geometry:
- >>> line = LineString([(-1, 1), (3, 1)])
- >>> s.intersects(line)
+ >>> poly = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
+ >>> s.covered_by(poly)
0 True
1 True
2 True
@@ -1472,49 +2161,131 @@ class GeoSeries(GeoFrame, pspd.Series):
``align=True`` or ignore index and compare elements based on their
matching
order using ``align=False``:
- >>> s.intersects(s2, align=True)
+ >>> s.covered_by(s2, align=True)
0 False
1 True
2 True
- 3 False
+ 3 True
4 False
dtype: bool
- >>> s.intersects(s2, align=False)
- 0 True
- 1 True
- 2 True
- 3 True
+ >>> s.covered_by(s2, align=False)
+ 0 True
+ 1 False
+ 2 True
+ 3 True
dtype: bool
Notes
-----
This method works in a row-wise manner. It does not check if an element
- of one GeoSeries ``crosses`` *any* element of the other one.
+ of one GeoSeries is ``covered_by`` any element of the other one.
See also
--------
- GeoSeries.disjoint
- GeoSeries.crosses
- GeoSeries.touches
- GeoSeries.intersection
+ GeoSeries.covers
+ GeoSeries.overlaps
"""
+ result = self._row_wise_operation(
+ "ST_CoveredBy(`L`, `R`)",
+ other,
+ align,
+ rename="covered_by",
+ default_val="FALSE",
+ )
+ return to_bool(result)
- select = "ST_Intersects(`L`, `R`)"
+ def distance(self, other, align=None) -> pspd.Series:
+ """Returns a ``Series`` containing the distance to aligned `other`.
- # ps.Series.fillna() call in to_bool, doesn't work for the output for
- # intersects here for some reason. So we manually handle the nulls
here.
- select = f"""
- CASE
- WHEN `L` IS NULL OR `R` IS NULL THEN FALSE
- ELSE {select}
- END
+ 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.
+
+ Returns
+ -------
+ Series (float)
+
+ Examples
+ --------
+ >>> from sedona.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 0), (1, 1)]),
+ ... Polygon([(0, 0), (-1, 0), (-1, 1)]),
+ ... LineString([(1, 1), (0, 0)]),
+ ... Point(0, 0),
+ ... ],
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ ... Point(3, 1),
+ ... LineString([(1, 0), (2, 0)]),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 1 0, 1 1, 0 0))
+ 1 POLYGON ((0 0, -1 0, -1 1, 0 0))
+ 2 LINESTRING (1 1, 0 0)
+ 3 POINT (0 0)
+ dtype: geometry
+
+ >>> s2
+ 1 POLYGON ((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, ...
+ 2 POINT (3 1)
+ 3 LINESTRING (1 0, 2 0)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check the distance of each geometry of GeoSeries to a single
+ geometry:
+
+ >>> point = Point(-1, 0)
+ >>> s.distance(point)
+ 0 1.0
+ 1 0.0
+ 2 1.0
+ 3 1.0
+ dtype: float64
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and use elements with the same index using
+ ``align=True`` or ignore index and use elements based on their matching
+ order using ``align=False``:
+
+ >>> s.distance(s2, align=True)
+ 0 NaN
+ 1 0.707107
+ 2 2.000000
+ 3 1.000000
+ 4 NaN
+ dtype: float64
+
+ >>> s.distance(s2, align=False)
+ 0 0.000000
+ 1 3.162278
+ 2 0.707107
+ 3 1.000000
+ dtype: float64
"""
result = self._row_wise_operation(
- select, other, align, rename="intersects", returns_geom=False
+ "ST_Distance(`L`, `R`)", other, align, rename="distance",
default_val="NULL"
)
- return to_bool(result)
+ return result
def intersection(
self, other: Union["GeoSeries", BaseGeometry], align: Union[bool,
None] = None
@@ -1619,7 +2390,12 @@ class GeoSeries(GeoFrame, pspd.Series):
GeoSeries.union
"""
return self._row_wise_operation(
- "ST_Intersection(`L`, `R`)", other, align, rename="intersection"
+ "ST_Intersection(`L`, `R`)",
+ other,
+ align,
+ rename="intersection",
+ returns_geom=True,
+ default_val="NULL",
)
def _row_wise_operation(
@@ -1628,11 +2404,15 @@ class GeoSeries(GeoFrame, pspd.Series):
other: Any,
align: Union[bool, None],
rename: str,
- returns_geom: bool = True,
+ returns_geom: bool = False,
+ default_val: Union[str, None] = None,
):
"""
Helper function to perform a row-wise operation on two GeoSeries.
The self column and other column are aliased to `L` and `R`,
respectively.
+
+ default_val : str or None (default "FALSE")
+ The value to use if either L or R is null. If None, nulls are not
handled.
"""
from pyspark.sql.functions import col
@@ -1666,6 +2446,17 @@ class GeoSeries(GeoFrame, pspd.Series):
col(index_col),
)
joined_df = df.join(other_df, on=index_col, how="outer")
+
+ if default_val is not None:
+ # ps.Series.fillna() doesn't always work for the output for some
reason
+ # so we manually handle the nulls here.
+ select = f"""
+ CASE
+ WHEN `L` IS NULL OR `R` IS NULL THEN {default_val}
+ ELSE {select}
+ END
+ """
+
return self._query_geometry_column(
select,
cols=["L", "R"],
@@ -1678,9 +2469,124 @@ class GeoSeries(GeoFrame, pspd.Series):
# Implementation of the abstract method
raise NotImplementedError("This method is not implemented yet.")
- def contains(self, other, align=None):
- # Implementation of the abstract method
- raise NotImplementedError("This method is not implemented yet.")
+ def contains(self, other, align=None) -> pspd.Series:
+ """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
+ each aligned geometry that contains `other`.
+
+ An object is said to contain `other` if at least one point of `other`
lies in
+ the interior and no points of `other` lie in the exterior of the
object.
+ (Therefore, any given polygon does not contain its own boundary -
there is not
+ any point that lies in the interior.)
+ If either object is empty, this operation returns ``False``.
+
+ This is the inverse of `within` in the sense that the expression
+ ``a.contains(b) == b.within(a)`` always evaluates to ``True``.
+
+ Note: Sedona's implementation instead returns False for identical
geometries.
+
+ 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 if it
+ is contained.
+ 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.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 1), (0, 1)]),
+ ... LineString([(0, 0), (0, 2)]),
+ ... LineString([(0, 0), (0, 1)]),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(0, 4),
+ ... )
+ >>> s2 = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (2, 2), (0, 2)]),
+ ... Polygon([(0, 0), (1, 2), (0, 2)]),
+ ... LineString([(0, 0), (0, 2)]),
+ ... Point(0, 1),
+ ... ],
+ ... index=range(1, 5),
+ ... )
+
+ >>> s
+ 0 POLYGON ((0 0, 1 1, 0 1, 0 0))
+ 1 LINESTRING (0 0, 0 2)
+ 2 LINESTRING (0 0, 0 1)
+ 3 POINT (0 1)
+ dtype: geometry
+
+ >>> s2
+ 1 POLYGON ((0 0, 2 2, 0 2, 0 0))
+ 2 POLYGON ((0 0, 1 2, 0 2, 0 0))
+ 3 LINESTRING (0 0, 0 2)
+ 4 POINT (0 1)
+ dtype: geometry
+
+ We can check if each geometry of GeoSeries contains a single
+ geometry:
+
+ >>> point = Point(0, 1)
+ >>> s.contains(point)
+ 0 False
+ 1 True
+ 2 False
+ 3 True
+ dtype: bool
+
+ We can also check two GeoSeries against each other, row by row.
+ The GeoSeries above have different indices. We can either align both
GeoSeries
+ based on index values and compare elements with the same index using
+ ``align=True`` or ignore index and compare elements based on their
matching
+ order using ``align=False``:
+
+ >>> s2.contains(s, align=True)
+ 0 False
+ 1 False
+ 2 False
+ 3 True
+ 4 False
+ dtype: bool
+
+ >>> s2.contains(s, align=False)
+ 1 True
+ 2 False
+ 3 True
+ 4 True
+ dtype: bool
+
+ Notes
+ -----
+ This method works in a row-wise manner. It does not check if an element
+ of one GeoSeries ``contains`` any element of the other one.
+
+ See also
+ --------
+ GeoSeries.contains_properly
+ GeoSeries.within
+ """
+ result = self._row_wise_operation(
+ "ST_Contains(`L`, `R`)",
+ other,
+ align,
+ rename="contains",
+ default_val="FALSE",
+ )
+ return to_bool(result)
def contains_properly(self, other, align=None):
# Implementation of the abstract method
@@ -2398,16 +3304,21 @@ class GeoSeries(GeoFrame, pspd.Series):
result = self._query_geometry_column(select, col, "")
elif isinstance(value, (GeoSeries, GeometryArray, gpd.GeoSeries)):
- if isinstance(value, (gpd.GeoSeries, GeometryArray)):
+ if not isinstance(value, GeoSeries):
value = GeoSeries(value)
- # Replace all None's with empty geometries
+ # Replace all None's with empty geometries (this is a recursive
call)
value = value.fillna(None)
# Coalesce: If the value in L is null, use the corresponding value
in R for that row
select = f"COALESCE(`L`, `R`)"
result = self._row_wise_operation(
- select, value, align=None, rename="fillna"
+ select,
+ value,
+ align=None,
+ rename="fillna",
+ returns_geom=True,
+ default_val=None,
)
else:
raise ValueError(f"Invalid value type: {type(value)}")
diff --git a/python/tests/geopandas/test_geopandas_base.py
b/python/tests/geopandas/test_geopandas_base.py
index c67af0dd5f..a772b273a6 100644
--- a/python/tests/geopandas/test_geopandas_base.py
+++ b/python/tests/geopandas/test_geopandas_base.py
@@ -23,6 +23,7 @@ import pandas as pd
import pyspark.pandas as ps
from pandas.testing import assert_series_equal
from contextlib import contextmanager
+from shapely.geometry import GeometryCollection
from shapely.geometry.base import BaseGeometry
@@ -98,6 +99,11 @@ class TestGeopandasBase(TestBase):
finally:
ps.reset_option("compute.ops_on_diff_frames")
+ def contains_any_geom_collection(self, geoms1, geoms2) -> bool:
+ return any(isinstance(g, GeometryCollection) for g in geoms1) or any(
+ isinstance(g, GeometryCollection) for g in geoms2
+ )
+
@classmethod
def check_geom_equals(cls, actual: BaseGeometry, expected: BaseGeometry):
assert isinstance(actual, BaseGeometry)
diff --git a/python/tests/geopandas/test_geoseries.py
b/python/tests/geopandas/test_geoseries.py
index 625d1b9ffa..48d1e11c11 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -793,6 +793,41 @@ class TestGeoSeries(TestGeopandasBase):
expected = GeometryCollection()
self.check_geom_equals(result, expected)
+ def test_crosses(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ LineString([(2, 0), (0, 2)]),
+ Point(0, 1),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ LineString([(1, 0), (1, 3)]),
+ LineString([(2, 0), (0, 2)]),
+ Point(1, 1),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+
+ line = LineString([(-1, 1), (3, 1)])
+ result = s.crosses(line)
+ expected = pd.Series([True, True, True, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.crosses(s2, align=True)
+ expected = pd.Series([False, True, False, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.crosses(s2, align=False)
+ expected = pd.Series([True, True, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_disjoint(self):
+ pass
+
def test_intersects(self):
s = sgpd.GeoSeries(
[
@@ -837,6 +872,208 @@ class TestGeoSeries(TestGeopandasBase):
result = s.intersects(s2, align=False)
expected = pd.Series([True, True, True, True])
+ def test_overlaps(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ MultiPoint([(0, 0), (0, 1)]),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 0), (0, 2)]),
+ LineString([(0, 1), (1, 1)]),
+ LineString([(1, 1), (3, 3)]),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+
+ polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
+ result = s.overlaps(polygon)
+ expected = pd.Series([True, True, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.overlaps(s2, align=True)
+ expected = pd.Series([False, True, False, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.overlaps(s2, align=False)
+ expected = pd.Series([True, False, True, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_touches(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ MultiPoint([(0, 0), (0, 1)]),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0, 0), (-2, 0), (0, -2)]),
+ LineString([(0, 1), (1, 1)]),
+ LineString([(1, 1), (3, 0)]),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+ line = LineString([(0, 0), (-1, -2)])
+ result = s.touches(line)
+ expected = pd.Series([True, True, True, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.touches(s2, align=True)
+ expected = pd.Series([False, True, True, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.touches(s2, align=False)
+ expected = pd.Series([True, False, True, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_within(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (1, 2), (0, 2)]),
+ LineString([(0, 0), (0, 2)]),
+ Point(0, 1),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0, 0), (1, 1), (0, 1)]),
+ LineString([(0, 0), (0, 2)]),
+ LineString([(0, 0), (0, 1)]),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+
+ polygon = Polygon([(0, 0), (2, 2), (0, 2)])
+ result = s.within(polygon)
+ expected = pd.Series([True, True, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s2.within(s, align=True)
+ expected = pd.Series([False, False, True, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s2.within(s, align=False)
+ expected = pd.Series([True, False, True, True], index=range(1, 5))
+ assert_series_equal(result.to_pandas(), expected)
+
+ # Ensure we return False if either geometries are empty
+ s = GeoSeries([Point(), Point(), Polygon(), Point(0, 1)])
+ result = s.within(s2, align=False)
+ expected = pd.Series([False, False, False, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_covers(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ Point(0, 0),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ LineString([(1, 1), (1.5, 1.5)]),
+ Point(0, 0),
+ ],
+ index=range(1, 5),
+ )
+
+ poly = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
+ result = s.covers(poly)
+ expected = pd.Series([True, False, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.covers(s2, align=True)
+ expected = pd.Series([False, False, False, False, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.covers(s2, align=False)
+ expected = pd.Series([True, False, True, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ # Ensure we return False if either geometries are empty
+ s = GeoSeries([Point(), Point(), Polygon(), Point(0, 0)])
+ result = s.covers(s2, align=False)
+ expected = pd.Series([False, False, False, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_covered_by(self):
+ s = GeoSeries(
+ [
+ Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ LineString([(1, 1), (1.5, 1.5)]),
+ Point(0, 0),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ LineString([(0, 0), (2, 2)]),
+ Point(0, 0),
+ ],
+ index=range(1, 5),
+ )
+
+ poly = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
+ result = s.covered_by(poly)
+ expected = pd.Series([True, True, True, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.covered_by(s2, align=True)
+ expected = pd.Series([False, True, True, True, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.covered_by(s2, align=False)
+ expected = pd.Series([True, False, True, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ def test_distance(self):
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (1, 0), (1, 1)]),
+ Polygon([(0, 0), (-1, 0), (-1, 1)]),
+ LineString([(1, 1), (0, 0)]),
+ Point(0, 0),
+ ],
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0.5, 0.5), (1.5, 0.5), (1.5, 1.5), (0.5, 1.5)]),
+ Point(3, 1),
+ LineString([(1, 0), (2, 0)]),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+ point = Point(-1, 0)
+ result = s.distance(point)
+ expected = pd.Series([1.0, 0.0, 1.0, 1.0])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.distance(s2, align=True)
+ expected = pd.Series([np.nan, 0.707107, 2.000000, 1.000000, np.nan])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s.distance(s2, align=False)
+ expected = pd.Series([0.000000, 3.162278, 0.707107, 1.000000])
+ assert_series_equal(result.to_pandas(), expected)
+
def test_intersection(self):
import pyspark.pandas as ps
@@ -946,7 +1183,37 @@ class TestGeoSeries(TestGeopandasBase):
pass
def test_contains(self):
- pass
+ s = GeoSeries(
+ [
+ Polygon([(0, 0), (1, 1), (0, 1)]),
+ LineString([(0, 0), (0, 2)]),
+ LineString([(0, 0), (0, 1)]),
+ Point(0, 1),
+ ],
+ index=range(0, 4),
+ )
+ s2 = GeoSeries(
+ [
+ Polygon([(0, 0), (2, 2), (0, 2)]),
+ Polygon([(0, 0), (1, 2), (0, 2)]),
+ LineString([(0, 0), (0, 2)]),
+ Point(0, 1),
+ ],
+ index=range(1, 5),
+ )
+
+ point = Point(0, 1)
+ result = s.contains(point)
+ expected = pd.Series([False, True, False, True])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s2.contains(s, align=True)
+ expected = pd.Series([False, False, False, True, False])
+ assert_series_equal(result.to_pandas(), expected)
+
+ result = s2.contains(s, align=False)
+ expected = pd.Series([True, False, True, True], index=range(1, 5))
+ assert_series_equal(result.to_pandas(), expected)
def test_contains_properly(self):
pass
diff --git a/python/tests/geopandas/test_match_geopandas_series.py
b/python/tests/geopandas/test_match_geopandas_series.py
index 16bf86ad33..e71ec4ce9a 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -657,11 +657,36 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
gpd_result = gpd.GeoSeries([]).union_all()
self.check_geom_equals(sgpd_result, gpd_result)
+ def test_crosses(self):
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if self.contains_any_geom_collection(geom, geom2):
+ continue
+
+ # We explicitly specify align=True to quite warnings in
geopandas, despite it being the default
+ gpd_result = gpd.GeoSeries(geom).crosses(
+ gpd.GeoSeries(geom2), align=True
+ )
+ sgpd_result = GeoSeries(geom).crosses(GeoSeries(geom2),
align=True)
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).crosses(GeoSeries(geom2),
align=False)
+ gpd_result = gpd.GeoSeries(geom).crosses(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_disjoint(self):
+ pass
+
def test_intersects(self):
for _, geom in self.geoms:
for _, geom2 in self.geoms:
- sgpd_result = GeoSeries(geom).intersects(GeoSeries(geom2))
- gpd_result =
gpd.GeoSeries(geom).intersects(gpd.GeoSeries(geom2))
+ sgpd_result = GeoSeries(geom).intersects(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).intersects(
+ gpd.GeoSeries(geom2), align=True
+ )
self.check_pd_series_equal(sgpd_result, gpd_result)
if len(geom) == len(geom2):
@@ -721,8 +746,149 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
def test_intersection_all(self):
pass
+ def test_overlaps(self):
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ # Sedona's results differ from geopandas for these cases
+ if geom == geom2 or self.contains_any_geom_collection(geom,
geom2):
+ continue
+
+ sgpd_result = GeoSeries(geom).overlaps(GeoSeries(geom2,
align=True))
+ gpd_result = gpd.GeoSeries(geom).overlaps(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).overlaps(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).overlaps(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_touches(self):
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if self.contains_any_geom_collection(geom, geom2):
+ continue
+ sgpd_result = GeoSeries(geom).touches(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).touches(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).touches(GeoSeries(geom2),
align=False)
+ gpd_result = gpd.GeoSeries(geom).touches(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_within(self):
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if geom == geom2 or self.contains_any_geom_collection(geom,
geom2):
+ continue
+
+ sgpd_result = GeoSeries(geom).within(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).within(
+ gpd.GeoSeries(geom2), align=True
+ )
+
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).within(GeoSeries(geom2),
align=False)
+ gpd_result = gpd.GeoSeries(geom).within(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_covers(self):
+ if parse_version(gpd.__version__) < parse_version("0.8.0"):
+ pytest.skip("geopandas < 0.8.0 does not support covered_by")
+
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if geom == geom2 or self.contains_any_geom_collection(geom,
geom2):
+ continue
+
+ sgpd_result = GeoSeries(geom).covers(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).covers(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).covers(GeoSeries(geom2),
align=False)
+ gpd_result = gpd.GeoSeries(geom).covers(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_covered_by(self):
+ if parse_version(shapely.__version__) < parse_version("2.0.0"):
+ pytest.skip("shapely < 2.0.0 does not support covered_by")
+
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if geom == geom2 or self.contains_any_geom_collection(geom,
geom2):
+ continue
+
+ sgpd_result = GeoSeries(geom).covered_by(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).covered_by(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).covered_by(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).covered_by(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ def test_distance(self):
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ sgpd_result = GeoSeries(geom).distance(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).distance(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).distance(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).distance(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
def test_contains(self):
- pass
+ for _, geom in self.geoms:
+ for _, geom2 in self.geoms:
+ if geom == geom2 or self.contains_any_geom_collection(geom,
geom2):
+ continue
+ sgpd_result = GeoSeries(geom).contains(GeoSeries(geom2),
align=True)
+ gpd_result = gpd.GeoSeries(geom).contains(
+ gpd.GeoSeries(geom2), align=True
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
+
+ if len(geom) == len(geom2):
+ sgpd_result = GeoSeries(geom).contains(
+ GeoSeries(geom2), align=False
+ )
+ gpd_result = gpd.GeoSeries(geom).contains(
+ gpd.GeoSeries(geom2), align=False
+ )
+ self.check_pd_series_equal(sgpd_result, gpd_result)
def test_contains_properly(self):
pass