Repository: geopandas/geopandas
Pull request #3299 (2024-05-19 18:45:17)

TST: test only relevant values in xyzservices explore tests
Compat with https://github.com/python-visualization/folium/pull/1952. There's no need to test `maxNativeZoom` on our side, keeping only the values that are relevant for our tests. That should make dev CI green again.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0da3ab12..2aff602e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -98,6 +98,7 @@ Bug fixes:
   geometry column (#2933).
 - Fix bug in `GeoDataFrame` constructor where if `geometry` is given a named
   `GeoSeries` the name was not used as the active geometry column name (#3237).
+- Fix bug in `GeoSeries` constructor when passing a Series and specifying a `crs` to not change the original input data (#2492).
 - Fix regression preventing reading from file paths containing hashes in `read_file`
   with the fiona engine (#3280). An analgous fix for pyogrio is included in
   pyogrio 0.8.1.
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index f09971eb..57a35cd1 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -101,11 +101,9 @@ Constructive methods and attributes
 
    GeoSeries.boundary
    GeoSeries.buffer
-   GeoSeries.build_area
    GeoSeries.centroid
    GeoSeries.concave_hull
    GeoSeries.convex_hull
-   GeoSeries.delaunay_triangles
    GeoSeries.envelope
    GeoSeries.extract_unique_points
    GeoSeries.force_2d
@@ -115,7 +113,6 @@ Constructive methods and attributes
    GeoSeries.minimum_clearance
    GeoSeries.minimum_rotated_rectangle
    GeoSeries.normalize
-   GeoSeries.polygonize
    GeoSeries.remove_repeated_points
    GeoSeries.reverse
    GeoSeries.sample_points
@@ -124,7 +121,6 @@ Constructive methods and attributes
    GeoSeries.simplify
    GeoSeries.snap
    GeoSeries.transform
-   GeoSeries.voronoi_polygons
 
 Affine transformations
 ----------------------
@@ -155,9 +151,14 @@ Aggregating and exploding
 .. autosummary::
    :toctree: api/
 
-   GeoSeries.union_all
-   GeoSeries.intersection_all
+   GeoSeries.build_area
+   GeoSeries.delaunay_triangles
    GeoSeries.explode
+   GeoSeries.intersection_all
+   GeoSeries.polygonize
+   GeoSeries.union_all
+   GeoSeries.voronoi_polygons
+
 
 Serialization / IO / conversion
 -------------------------------
diff --git a/geopandas/base.py b/geopandas/base.py
index 0eef84f6..42688dea 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -1376,6 +1376,7 @@ GeometryCollection
     def offset_curve(self, distance, quad_segs=8, join_style="round", mitre_limit=5.0):
         """Returns a ``LineString`` or ``MultiLineString`` geometry at a
         distance from the object on its right or its left side.
+
         Parameters
         ----------
         distance : float | array-like
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index e650503d..22aec170 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -154,8 +154,12 @@ class GeoSeries(GeoPandasBase, Series):
     """
 
     def __init__(self, data=None, index=None, crs: Optional[Any] = None, **kwargs):
-        if hasattr(data, "crs") and crs:
-            if not data.crs:
+        if (
+            hasattr(data, "crs")
+            or (isinstance(data, pd.Series) and hasattr(data.array, "crs"))
+        ) and crs:
+            data_crs = data.crs if hasattr(data, "crs") else data.array.crs
+            if not data_crs:
                 # make a copy to avoid setting CRS to passed GeometryArray
                 data = data.copy()
             else:
diff --git a/geopandas/io/arrow.py b/geopandas/io/arrow.py
index 96647227..dcecf85e 100644
--- a/geopandas/io/arrow.py
+++ b/geopandas/io/arrow.py
@@ -79,6 +79,37 @@ def _remove_id_from_member_of_ensembles(json_dict):
                 member.pop("id", None)
 
 
+# type ids 0 to 7
+_geometry_type_names = [
+    "Point",
+    "LineString",
+    "LineString",
+    "Polygon",
+    "MultiPoint",
+    "MultiLineString",
+    "MultiPolygon",
+    "GeometryCollection",
+]
+_geometry_type_names += [geom_type + " Z" for geom_type in _geometry_type_names]
+
+
+def _get_geometry_types(series):
+    """
+    Get unique geometry types from a GeoSeries.
+    """
+    arr_geometry_types = shapely.get_type_id(series.array._data)
+    # ensure to include "... Z" for 3D geometries
+    has_z = shapely.has_z(series.array._data)
+    arr_geometry_types[has_z] += 8
+
+    geometry_types = Series(arr_geometry_types).unique().tolist()
+    # drop missing values (shapely.get_type_id returns -1 for those)
+    if -1 in geometry_types:
+        geometry_types.remove(-1)
+
+    return sorted([_geometry_type_names[idx] for idx in geometry_types])
+
+
 def _create_metadata(df, schema_version=None):
     """Create and encode geo metadata dict.
 
@@ -106,13 +137,7 @@ def _create_metadata(df, schema_version=None):
     for col in df.columns[df.dtypes == "geometry"]:
         series = df[col]
 
-        # ensure to include "... Z" for 3D geometries
-        geometry_types = {
-            f"{geom} Z" if has_z else geom
-            for geom, has_z in zip(series.geom_type, series.has_z)
-        }
-        geometry_types = sorted(Series(list(geometry_types)).dropna())
-
+        geometry_types = _get_geometry_types(series)
         if schema_version[0] == "0":
             geometry_types_name = "geometry_type"
             if len(geometry_types) == 1:


Test patch:
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 747fc408..9cfad24b 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -141,6 +141,12 @@ def test_create_metadata_with_z_geometries():
         for geom_type in metadata["columns"]["geometry"]["geometry_types"]
     )
 
+    metadata = _create_metadata(df.iloc[5:7])
+    assert metadata["columns"]["geometry"]["geometry_types"] == [
+        "MultiPolygon",
+        "Polygon Z",
+    ]
+
 
 def test_crs_metadata_datum_ensemble():
     pyproj = pytest.importorskip("pyproj")
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 6b1ba8cd..2a872249 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -743,7 +743,7 @@ class TestExplore:
             'attribution":"\\u0026copy;\\u003cahref=\\"https://www.openstreetmap.org'
             in out_str
         )
-        assert '"maxNativeZoom":20,"maxZoom":20,"minZoom":0' in out_str
+        assert '"maxZoom":20,"minZoom":0' in out_str
 
     @pytest.mark.skipif(not HAS_PYPROJ, reason="requires pyproj")
     def test_xyzservices_query_name(self):
@@ -760,7 +760,7 @@ class TestExplore:
             'attribution":"\\u0026copy;\\u003cahref=\\"https://www.openstreetmap.org'
             in out_str
         )
-        assert '"maxNativeZoom":20,"maxZoom":20,"minZoom":0' in out_str
+        assert '"maxZoom":20,"minZoom":0' in out_str
 
     @pytest.mark.skipif(not HAS_PYPROJ, reason="requires pyproj")
     def test_xyzservices_providers_min_zoom_override(self):
@@ -771,7 +771,7 @@ class TestExplore:
         )
         out_str = self._fetch_map_string(m)
 
-        assert '"maxNativeZoom":20,"maxZoom":20,"minZoom":3' in out_str
+        assert '"maxZoom":20,"minZoom":3' in out_str
 
     @pytest.mark.skipif(not HAS_PYPROJ, reason="requires pyproj")
     def test_xyzservices_providers_max_zoom_override(self):
@@ -782,7 +782,7 @@ class TestExplore:
         )
         out_str = self._fetch_map_string(m)
 
-        assert '"maxNativeZoom":12,"maxZoom":12,"minZoom":0' in out_str
+        assert '"maxZoom":12,"minZoom":0' in out_str
 
     @pytest.mark.skipif(not HAS_PYPROJ, reason="requires pyproj")
     def test_xyzservices_providers_both_zooms_override(self):
@@ -795,7 +795,7 @@ class TestExplore:
         )
         out_str = self._fetch_map_string(m)
 
-        assert '"maxNativeZoom":12,"maxZoom":12,"minZoom":3' in out_str
+        assert '"maxZoom":12,"minZoom":3' in out_str
 
     def test_linearrings(self):
         rings = self.nybb.explode(index_parts=True).exterior
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index 68ae7e09..6f809c73 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -658,6 +658,18 @@ class TestConstructor:
         assert s.name == g.name
         assert s.index is g.index
 
+    @pytest.mark.skipif(not HAS_PYPROJ, reason="pyproj not available")
+    def test_from_series_no_set_crs_on_construction(self):
+        # https://github.com/geopandas/geopandas/issues/2492
+        # also when passing Series[geometry], ensure we don't change crs of
+        # original data
+        gs = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
+        s = pd.Series(gs)
+        result = GeoSeries(s, crs=4326)
+        assert s.values.crs is None
+        assert gs.crs is None
+        assert result.crs == "EPSG:4326"
+
     def test_copy(self):
         # default is to copy with CoW / pandas 3+
         arr = np.array([Point(x, x) for x in range(3)], dtype=object)