Repository: geopandas/geopandas

Issue:
The x, y, z coordinate attributes error with empty points
Hi! Working with GeoPandas `0.10.2` and the method [here ](https://geopandas.readthedocs.io/en/stable/docs/reference/api/geopandas.GeoSeries.z.html#geopandas.GeoSeries.z) fails:

```python
>>> my_geodataframe.geometry.x
...
IndexError: index out of range
```
 
See the full traceback attached: [traceback.log](https://github.com/geopandas/geopandas/files/7503163/traceback.log).

Ideally, however, this should work: with `my_geodataframe.geometry` I get a `GeoSeries`, so the `.[xyx]` attribute should return the longitude/latitude/elevation.

_Originally posted by @baggiponte in https://github.com/geopandas/geopandas/issues/246#issuecomment-963926763_

Discussion:

Patch:
diff --git a/geopandas/array.py b/geopandas/array.py
index cbfd5a6d..553fb69c 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -867,7 +867,14 @@ class GeometryArray(ExtensionArray):
     def x(self):
         """Return the x location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_x(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_x(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_x(self.data)
         else:
             message = "x attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -876,7 +883,14 @@ class GeometryArray(ExtensionArray):
     def y(self):
         """Return the y location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_y(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_y(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_y(self.data)
         else:
             message = "y attribute access only provided for Point geometries"
             raise ValueError(message)
@@ -885,7 +899,14 @@ class GeometryArray(ExtensionArray):
     def z(self):
         """Return the z location of point geometries in a GeoSeries"""
         if (self.geom_type[~self.isna()] == "Point").all():
-            return vectorized.get_z(self.data)
+            empty = self.is_empty
+            if empty.any():
+                nonempty = ~empty
+                coords = np.full_like(nonempty, dtype=float, fill_value=np.nan)
+                coords[nonempty] = vectorized.get_z(self.data[nonempty])
+                return coords
+            else:
+                return vectorized.get_z(self.data)
         else:
             message = "z attribute access only provided for Point geometries"
             raise ValueError(message)
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 5468718c..9be068b2 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -665,21 +665,6 @@ class GeoSeries(GeoPandasBase, Series):
         GeoSeries.notna : inverse of isna
         GeoSeries.is_empty : detect empty geometries
         """
-        if self.is_empty.any():
-            warnings.warn(
-                "GeoSeries.isna() previously returned True for both missing (None) "
-                "and empty geometries. Now, it only returns True for missing values. "
-                "Since the calling GeoSeries contains empty geometries, the result "
-                "has changed compared to previous versions of GeoPandas.\n"
-                "Given a GeoSeries 's', you can use 's.is_empty | s.isna()' to get "
-                "back the old behaviour.\n\n"
-                "To further ignore this warning, you can do: \n"
-                "import warnings; warnings.filterwarnings('ignore', 'GeoSeries.isna', "
-                "UserWarning)",
-                UserWarning,
-                stacklevel=2,
-            )
-
         return super().isna()
 
     def isnull(self):
diff --git a/geopandas/tools/__init__.py b/geopandas/tools/__init__.py
index 82e6e9e5..495d5045 100644
--- a/geopandas/tools/__init__.py
+++ b/geopandas/tools/__init__.py
@@ -1,4 +1,3 @@
-from .crs import explicit_crs_from_epsg
 from .geocoding import geocode, reverse_geocode
 from .overlay import overlay
 from .sjoin import sjoin, sjoin_nearest
@@ -7,7 +6,6 @@ from .clip import clip
 
 __all__ = [
     "collect",
-    "explicit_crs_from_epsg",
     "geocode",
     "overlay",
     "reverse_geocode",
diff --git a/geopandas/tools/crs.py b/geopandas/tools/crs.py
deleted file mode 100644
index fadc68a5..00000000
--- a/geopandas/tools/crs.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import warnings
-
-from pyproj import CRS
-
-
-def explicit_crs_from_epsg(crs=None, epsg=None):
-    """
-    Gets full/explicit CRS from EPSG code provided.
-
-    Parameters
-    ----------
-    crs : dict or string, default None
-        An existing crs dict or Proj string with the 'init' key specifying an EPSG code
-    epsg : string or int, default None
-       The EPSG code to lookup
-    """
-    warnings.warn(
-        "explicit_crs_from_epsg is deprecated. "
-        "You can set the epsg on the GeoDataFrame (gdf) using gdf.crs=epsg",
-        FutureWarning,
-        stacklevel=2,
-    )
-    if crs is not None:
-        return CRS.from_user_input(crs)
-    elif epsg is not None:
-        return CRS.from_epsg(epsg)
-    raise ValueError("Must pass either crs or epsg.")
-
-
-def epsg_from_crs(crs):
-    """
-    Returns an epsg code from a crs dict or Proj string.
-
-    Parameters
-    ----------
-    crs : dict or string, default None
-        A crs dict or Proj string
-
-    """
-    warnings.warn(
-        "epsg_from_crs is deprecated. "
-        "You can get the epsg code from GeoDataFrame (gdf) "
-        "using gdf.crs.to_epsg()",
-        FutureWarning,
-        stacklevel=2,
-    )
-    crs = CRS.from_user_input(crs)
-    if "init=epsg" in crs.to_string().lower():
-        epsg_code = crs.to_epsg(0)
-    else:
-        epsg_code = crs.to_epsg()
-    return epsg_code
-
-
-def get_epsg_file_contents():
-    warnings.warn("get_epsg_file_contents is deprecated.", FutureWarning, stacklevel=2)
-    return ""


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 941b70fd..05475a96 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -7,6 +7,7 @@ from pandas import DataFrame, Index, MultiIndex, Series
 from shapely.geometry import LinearRing, LineString, MultiPoint, Point, Polygon
 from shapely.geometry.collection import GeometryCollection
 from shapely.ops import unary_union
+from shapely import wkt
 
 from geopandas import GeoDataFrame, GeoSeries
 from geopandas.base import GeoPandasBase
@@ -72,6 +73,10 @@ class TestGeomMethods:
         self.landmarks = GeoSeries([self.esb, self.sol], crs="epsg:4326")
         self.pt2d = Point(-73.9847, 40.7484)
         self.landmarks_mixed = GeoSeries([self.esb, self.sol, self.pt2d], crs=4326)
+        self.pt_empty = wkt.loads("POINT EMPTY")
+        self.landmarks_mixed_empty = GeoSeries(
+            [self.esb, self.sol, self.pt2d, self.pt_empty], crs=4326
+        )
         self.l1 = LineString([(0, 0), (0, 1), (1, 1)])
         self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)])
         self.g5 = GeoSeries([self.l1, self.l2])
@@ -623,6 +628,15 @@ class TestGeomMethods:
         expected_z = [30.3244, 31.2344, np.nan]
         assert_array_dtype_equal(expected_z, self.landmarks_mixed.geometry.z)
 
+    def test_xyz_points_empty(self):
+        expected_x = [-73.9847, -74.0446, -73.9847, np.nan]
+        expected_y = [40.7484, 40.6893, 40.7484, np.nan]
+        expected_z = [30.3244, 31.2344, np.nan, np.nan]
+
+        assert_array_dtype_equal(expected_x, self.landmarks_mixed_empty.geometry.x)
+        assert_array_dtype_equal(expected_y, self.landmarks_mixed_empty.geometry.y)
+        assert_array_dtype_equal(expected_z, self.landmarks_mixed_empty.geometry.z)
+
     def test_xyz_polygons(self):
         # accessing x attribute in polygon geoseries should raise an error
         with pytest.raises(ValueError):
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index c9d27406..6d785d3b 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -379,15 +379,6 @@ class TestSeries:
         assert_geoseries_equal(expected, GeoSeries.from_xy(x, y, z))
 
 
-def test_missing_values_empty_warning():
-    s = GeoSeries([Point(1, 1), None, np.nan, BaseGeometry(), Polygon()])
-    with pytest.warns(UserWarning):
-        s.isna()
-
-    with pytest.warns(UserWarning):
-        s.notna()
-
-
 @pytest.mark.filterwarnings("ignore::UserWarning")
 def test_missing_values():
     s = GeoSeries([Point(1, 1), None, np.nan, BaseGeometry(), Polygon()])
diff --git a/geopandas/tools/tests/test_tools.py b/geopandas/tools/tests/test_tools.py
index 7117a2a2..603aad0d 100644
--- a/geopandas/tools/tests/test_tools.py
+++ b/geopandas/tools/tests/test_tools.py
@@ -1,21 +1,11 @@
-from packaging.version import Version
-
 from shapely.geometry import LineString, MultiPoint, Point
-import pyproj
-from pyproj import CRS
 
 from geopandas import GeoSeries
 from geopandas.tools import collect
-from geopandas.tools.crs import epsg_from_crs, explicit_crs_from_epsg
 
 import pytest
 
 
-# pyproj 2.3.1 fixed a segfault for the case working in an environment with
-# 'init' dicts (https://github.com/pyproj4/pyproj/issues/415)
-PYPROJ_LT_231 = Version(pyproj.__version__) < Version("2.3.1")
-
-
 class TestTools:
     def setup_method(self):
         self.p1 = Point(0, 0)
@@ -59,27 +49,3 @@ class TestTools:
     def test_collect_mixed_multi(self):
         with pytest.raises(ValueError):
             collect([self.mpc, self.mp1])
-
-    @pytest.mark.skipif(PYPROJ_LT_231, reason="segfault")
-    def test_epsg_from_crs(self):
-        with pytest.warns(FutureWarning):
-            assert epsg_from_crs({"init": "epsg:4326"}) == 4326
-            assert epsg_from_crs({"init": "EPSG:4326"}) == 4326
-            assert epsg_from_crs("+init=epsg:4326") == 4326
-
-    @pytest.mark.skipif(PYPROJ_LT_231, reason="segfault")
-    def test_explicit_crs_from_epsg(self):
-        with pytest.warns(FutureWarning):
-            assert explicit_crs_from_epsg(epsg=4326) == CRS.from_epsg(4326)
-            assert explicit_crs_from_epsg(epsg="4326") == CRS.from_epsg(4326)
-            assert explicit_crs_from_epsg(crs={"init": "epsg:4326"}) == CRS.from_dict(
-                {"init": "epsg:4326"}
-            )
-            assert explicit_crs_from_epsg(crs="+init=epsg:4326") == CRS.from_proj4(
-                "+init=epsg:4326"
-            )
-
-    @pytest.mark.filterwarnings("ignore:explicit_crs_from_epsg:FutureWarning")
-    def test_explicit_crs_from_epsg__missing_input(self):
-        with pytest.raises(ValueError):
-            explicit_crs_from_epsg()