Skip to content

Commit 7a93715

Browse files
authored
[GH-2047] Geopandas.GeoSeries: Implement to_crs (#2048)
1 parent c18ddda commit 7a93715

3 files changed

Lines changed: 145 additions & 25 deletions

File tree

python/sedona/geopandas/geoseries.py

Lines changed: 122 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ def set_crs(
282282
283283
Examples
284284
--------
285+
>>> from sedona.geopandas import GeoSeries
285286
>>> from shapely.geometry import Point
286-
>>> s = geopandas.GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
287+
>>> s = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
287288
>>> s
288289
0 POINT (1 1)
289290
1 POINT (2 2)
@@ -479,10 +480,10 @@ def _to_geopandas(self) -> gpd.GeoSeries:
479480
pd_series = self._to_internal_pandas()
480481
try:
481482
return gpd.GeoSeries(
482-
pd_series.map(lambda wkb: shapely.wkb.loads(bytes(wkb)))
483+
pd_series.map(lambda wkb: shapely.wkb.loads(bytes(wkb))), crs=self.crs
483484
)
484-
except Exception as e:
485-
return gpd.GeoSeries(pd_series)
485+
except TypeError:
486+
return gpd.GeoSeries(pd_series, crs=self.crs)
486487

487488
def to_spark_pandas(self) -> pspd.Series:
488489
return pspd.Series(self._psdf._to_internal_pandas())
@@ -512,7 +513,6 @@ def copy(self, deep=False):
512513
513514
Examples:
514515
>>> from shapely.geometry import Point
515-
>>> import geopandas as gpd
516516
>>> from sedona.geopandas import GeoSeries
517517
518518
>>> gs = GeoSeries([Point(1, 1), Point(2, 2)])
@@ -542,7 +542,6 @@ def area(self) -> pspd.Series:
542542
Examples
543543
--------
544544
>>> from shapely.geometry import Polygon
545-
>>> import geopandas as gpd
546545
>>> from sedona.geopandas import GeoSeries
547546
548547
>>> gs = GeoSeries([Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])])
@@ -615,7 +614,6 @@ def length(self) -> pspd.Series:
615614
Examples
616615
--------
617616
>>> from shapely.geometry import Polygon
618-
>>> import geopandas as gpd
619617
>>> from sedona.geopandas import GeoSeries
620618
621619
>>> gs = GeoSeries([Point(0, 0), LineString([(0, 0), (1, 1)]), Polygon([(0, 0), (1, 0), (1, 1)]), GeometryCollection([Point(0, 0), LineString([(0, 0), (1, 1)]), Polygon([(0, 0), (1, 0), (1, 1)])])])
@@ -649,8 +647,9 @@ def is_valid(self) -> pspd.Series:
649647
An example with one invalid polygon (a bowtie geometry crossing itself)
650648
and one missing geometry:
651649
650+
>>> from sedona.geopandas import GeoSeries
652651
>>> from shapely.geometry import Polygon
653-
>>> s = geopandas.GeoSeries(
652+
>>> s = GeoSeries(
654653
... [
655654
... Polygon([(0, 0), (1, 1), (0, 1)]),
656655
... Polygon([(0,0), (1, 1), (1, 0), (0, 1)]), # bowtie geometry
@@ -697,16 +696,15 @@ def is_empty(self) -> pspd.Series:
697696
An example of a GeoDataFrame with one empty point, one point and one missing
698697
value:
699698
699+
>>> from sedona.geopandas import GeoSeries
700700
>>> from shapely.geometry import Point
701-
>>> d = {'geometry': [Point(), Point(2, 1), None]}
702-
>>> gdf = geopandas.GeoDataFrame(d, crs="EPSG:4326")
703-
>>> gdf
704-
geometry
701+
>>> geoseries = GeoSeries([Point(), Point(2, 1), None], crs="EPSG:4326")
702+
>>> geoseries
705703
0 POINT EMPTY
706704
1 POINT (2 1)
707705
2 None
708706
709-
>>> gdf.is_empty
707+
>>> geoseries.is_empty
710708
0 True
711709
1 False
712710
2 False
@@ -743,8 +741,9 @@ def is_simple(self) -> pspd.Series:
743741
744742
Examples
745743
--------
744+
>>> from sedona.geopandas import GeoSeries
746745
>>> from shapely.geometry import LineString
747-
>>> s = geopandas.GeoSeries(
746+
>>> s = GeoSeries(
748747
... [
749748
... LineString([(0, 0), (1, 1), (1, -1), (0, 1)]),
750749
... LineString([(0, 0), (1, 1), (1, -1)]),
@@ -793,8 +792,9 @@ def has_z(self) -> pspd.Series:
793792
794793
Examples
795794
--------
795+
>>> from sedona.geopandas import GeoSeries
796796
>>> from shapely.geometry import Point
797-
>>> s = geopandas.GeoSeries(
797+
>>> s = GeoSeries(
798798
... [
799799
... Point(0, 1),
800800
... Point(0, 1, 2),
@@ -967,16 +967,17 @@ def intersects(
967967
968968
Examples
969969
--------
970+
>>> from sedona.geopandas import GeoSeries
970971
>>> from shapely.geometry import Polygon, LineString, Point
971-
>>> s = geopandas.GeoSeries(
972+
>>> s = GeoSeries(
972973
... [
973974
... Polygon([(0, 0), (2, 2), (0, 2)]),
974975
... LineString([(0, 0), (2, 2)]),
975976
... LineString([(2, 0), (0, 2)]),
976977
... Point(0, 1),
977978
... ],
978979
... )
979-
>>> s2 = geopandas.GeoSeries(
980+
>>> s2 = GeoSeries(
980981
... [
981982
... LineString([(1, 0), (1, 3)]),
982983
... LineString([(2, 0), (0, 2)]),
@@ -1051,8 +1052,9 @@ def intersection(
10511052
10521053
Examples
10531054
--------
1055+
>>> from sedona.geopandas import GeoSeries
10541056
>>> from shapely.geometry import Polygon, LineString, Point
1055-
>>> s = geopandas.GeoSeries(
1057+
>>> s = GeoSeries(
10561058
... [
10571059
... Polygon([(0, 0), (2, 2), (0, 2)]),
10581060
... Polygon([(0, 0), (2, 2), (0, 2)]),
@@ -1061,7 +1063,7 @@ def intersection(
10611063
... Point(0, 1),
10621064
... ],
10631065
... )
1064-
>>> s2 = geopandas.GeoSeries(
1066+
>>> s2 = GeoSeries(
10651067
... [
10661068
... Polygon([(0, 0), (1, 1), (0, 1)]),
10671069
... LineString([(1, 0), (1, 3)]),
@@ -1277,8 +1279,9 @@ def x(self) -> pspd.Series:
12771279
Examples
12781280
--------
12791281
1282+
>>> from sedona.geopandas import GeoSeries
12801283
>>> from shapely.geometry import Point
1281-
>>> s = geopandas.GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
1284+
>>> s = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
12821285
>>> s.x
12831286
0 1.0
12841287
1 2.0
@@ -1305,8 +1308,9 @@ def y(self) -> pspd.Series:
13051308
Examples
13061309
--------
13071310
1311+
>>> from sedona.geopandas import GeoSeries
13081312
>>> from shapely.geometry import Point
1309-
>>> s = geopandas.GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
1313+
>>> s = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
13101314
>>> s.y
13111315
0 1.0
13121316
1 2.0
@@ -1334,8 +1338,9 @@ def z(self) -> pspd.Series:
13341338
Examples
13351339
--------
13361340
1341+
>>> from sedona.geopandas import GeoSeries
13371342
>>> from shapely.geometry import Point
1338-
>>> s = geopandas.GeoSeries([Point(1, 1, 1), Point(2, 2, 2), Point(3, 3, 3)])
1343+
>>> s = GeoSeries([Point(1, 1, 1), Point(2, 2, 2), Point(3, 3, 3)])
13391344
>>> s.z
13401345
0 1.0
13411346
1 2.0
@@ -1681,7 +1686,101 @@ def explode(self, ignore_index=False, index_parts=False) -> "GeoSeries":
16811686
def to_crs(
16821687
self, crs: Union[Any, None] = None, epsg: Union[int, None] = None
16831688
) -> "GeoSeries":
1684-
raise NotImplementedError("GeoSeries.to_crs() is not implemented yet.")
1689+
"""Returns a ``GeoSeries`` with all geometries transformed to a new
1690+
coordinate reference system.
1691+
1692+
Transform all geometries in a GeoSeries to a different coordinate
1693+
reference system. The ``crs`` attribute on the current GeoSeries must
1694+
be set. Either ``crs`` or ``epsg`` may be specified for output.
1695+
1696+
This method will transform all points in all objects. It has no notion
1697+
of projecting entire geometries. All segments joining points are
1698+
assumed to be lines in the current projection, not geodesics. Objects
1699+
crossing the dateline (or other projection boundary) will have
1700+
undesirable behavior.
1701+
1702+
Parameters
1703+
----------
1704+
crs : pyproj.CRS, optional if `epsg` is specified
1705+
The value can be anything accepted
1706+
by :meth:`pyproj.CRS.from_user_input() <pyproj.crs.CRS.from_user_input>`,
1707+
such as an authority string (eg "EPSG:4326") or a WKT string.
1708+
epsg : int, optional if `crs` is specified
1709+
EPSG code specifying output projection.
1710+
1711+
Returns
1712+
-------
1713+
GeoSeries
1714+
1715+
Examples
1716+
--------
1717+
>>> from shapely.geometry import Point
1718+
>>> from sedona.geopandas import GeoSeries
1719+
>>> geoseries = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)], crs=4326)
1720+
>>> geoseries.crs
1721+
<Geographic 2D CRS: EPSG:4326>
1722+
Name: WGS 84
1723+
Axis Info [ellipsoidal]:
1724+
- Lat[north]: Geodetic latitude (degree)
1725+
- Lon[east]: Geodetic longitude (degree)
1726+
Area of Use:
1727+
- name: World
1728+
- bounds: (-180.0, -90.0, 180.0, 90.0)
1729+
Datum: World Geodetic System 1984
1730+
- Ellipsoid: WGS 84
1731+
- Prime Meridian: Greenwich
1732+
1733+
>>> geoseries = geoseries.to_crs(3857)
1734+
>>> print(geoseries)
1735+
0 POINT (111319.491 111325.143)
1736+
1 POINT (222638.982 222684.209)
1737+
2 POINT (333958.472 334111.171)
1738+
dtype: geometry
1739+
>>> geoseries.crs
1740+
<Projected CRS: EPSG:3857>
1741+
Name: WGS 84 / Pseudo-Mercator
1742+
Axis Info [cartesian]:
1743+
- X[east]: Easting (metre)
1744+
- Y[north]: Northing (metre)
1745+
Area of Use:
1746+
- name: World - 85°S to 85°N
1747+
- bounds: (-180.0, -85.06, 180.0, 85.06)
1748+
Coordinate Operation:
1749+
- name: Popular Visualisation Pseudo-Mercator
1750+
- method: Popular Visualisation Pseudo Mercator
1751+
Datum: World Geodetic System 1984
1752+
- Ellipsoid: WGS 84
1753+
- Prime Meridian: Greenwich
1754+
1755+
"""
1756+
1757+
from pyproj import CRS
1758+
1759+
old_crs = self.crs
1760+
if old_crs is None:
1761+
raise ValueError(
1762+
"Cannot transform naive geometries. "
1763+
"Please set a crs on the object first."
1764+
)
1765+
assert isinstance(old_crs, CRS)
1766+
1767+
if crs is not None:
1768+
crs = CRS.from_user_input(crs)
1769+
elif epsg is not None:
1770+
crs = CRS.from_epsg(epsg)
1771+
else:
1772+
raise ValueError("Must pass either crs or epsg.")
1773+
1774+
# skip if the input CRS and output CRS are the exact same
1775+
if old_crs.is_exact_same(crs):
1776+
return self
1777+
1778+
col = self.get_first_geometry_column()
1779+
return self._query_geometry_column(
1780+
f"ST_Transform(`{col}`, 'EPSG:{old_crs.to_epsg()}', 'EPSG:{crs.to_epsg()}')",
1781+
col,
1782+
"",
1783+
)
16851784

16861785
def estimate_utm_crs(self, datum_name: str = "WGS 84"):
16871786
raise NotImplementedError(

python/tests/geopandas/test_geoseries.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,21 @@ def test_explode(self):
189189
pass
190190

191191
def test_to_crs(self):
192-
pass
192+
from pyproj import CRS
193+
194+
geoseries = sgpd.GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)], crs=4326)
195+
assert isinstance(geoseries.crs, CRS) and geoseries.crs.to_epsg() == 4326
196+
result = geoseries.to_crs(3857)
197+
assert isinstance(result.crs, CRS) and result.crs.to_epsg() == 3857
198+
expected = gpd.GeoSeries(
199+
[
200+
Point(111319.49079327356, 111325.14286638486),
201+
Point(222638.98158654712, 222684.20850554455),
202+
Point(333958.4723798207, 334111.1714019597),
203+
],
204+
crs=3857,
205+
)
206+
self.check_sgpd_equals_gpd(result, expected)
193207

194208
def test_estimate_utm_crs(self):
195209
pass

python/tests/geopandas/test_match_geopandas_series.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,14 @@ def test_explode(self):
303303
pass
304304

305305
def test_to_crs(self):
306-
pass
306+
for _, geom in self.geoms:
307+
sgpd_result = GeoSeries(geom, crs=4326)
308+
gpd_result = gpd.GeoSeries(geom, crs=4326)
309+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
310+
311+
sgpd_result = sgpd_result.to_crs(epsg=3857)
312+
gpd_result = gpd_result.to_crs(epsg=3857)
313+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
307314

308315
def test_estimate_utm_crs(self):
309316
pass

0 commit comments

Comments
 (0)