Repository: geopandas/geopandas

Issue:
BUG: to_parquet doesn't set correct geometry_type in GeoParquet metadata for 3D geometries
The geometry_type should be "POINT Z" and not just "POINT":

```
In [12]: df = geopandas.GeoDataFrame({'col': [1, 2, 3]}, geometry=geopandas.points_from_xy([1, 2, 3], [2, 3, 4], [1, 1, 1]))

In [13]: df
Out[13]: 
   col                           geometry
0    1  POINT Z (1.00000 2.00000 1.00000)
1    2  POINT Z (2.00000 3.00000 1.00000)
2    3  POINT Z (3.00000 4.00000 1.00000)

In [14]: df.to_parquet("test_z_dimension.parquet")

In [15]: import pyarrow.parquet as pq

In [16]: meta = pq.read_metadata("test_z_dimension.parquet")

In [19]: meta.metadata[b"geo"]
Out[19]: b'{"primary_column": "geometry", "columns": {"geometry": {"encoding": "WKB", "crs": null, "geometry_types": ["Point"], "bbox": [1.0, 2.0, 3.0, 4.0]}}, "version": "1.0.0-beta.1", "creator": {"library": "geopandas", "version": "0.12.1+62.ga24aad54"}}'
```

Discussion:
JohnMoutafis: Hadn't setup the pre commit hook so I had to make some linding connections 😓
JohnMoutafis: @martinfleis `Tests / ubuntu-latest, ci/envs/310-latest-conda-forge.yaml (pull_request)` seems to be stuck. Can we rerun it?
JohnMoutafis: @jorisvandenbossche it doesn't seem that the test fail is related to my changes
m-richards: > @jorisvandenbossche it doesn't seem that the last test fail is related to my changes

That looks right, nothing to be worried about in this PR (the failing environment is built against the nightly dev release of pandas, so the failures are due to upstream changes we'll need to incorporate)
JohnMoutafis: @jorisvandenbossche any news on that?
jorisvandenbossche: @JohnMoutafis apologies for the very slow follow-up. I rebased this again to fix the conflicts with latest main, cleaned up a few comments and unnecessary test code, but for the rest it is looking good! Thanks again for the contribution

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1c5409b..0da3ab12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,6 +101,7 @@ Bug fixes:
 - 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.
+- Fix `to_parquet` to write correct metadata in case of 3D geometries (#2824).
 
 Deprecations and compatibility notes:
 
diff --git a/geopandas/io/arrow.py b/geopandas/io/arrow.py
index ec90275a..a24c3200 100644
--- a/geopandas/io/arrow.py
+++ b/geopandas/io/arrow.py
@@ -104,7 +104,14 @@ def _create_metadata(df, schema_version=None):
     column_metadata = {}
     for col in df.columns[df.dtypes == "geometry"]:
         series = df[col]
-        geometry_types = sorted(Series(series.geom_type.unique()).dropna())
+
+        # 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())
+
         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 2f5b0d81..67f1b652 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -11,7 +11,7 @@ from pandas import DataFrame, read_parquet as pd_read_parquet
 from pandas.testing import assert_frame_equal
 import numpy as np
 import shapely
-from shapely.geometry import box, Point, MultiPolygon
+from shapely.geometry import box, Point, LineString, MultiPolygon, Polygon
 
 
 import geopandas
@@ -86,6 +86,61 @@ def test_create_metadata(naturalearth_lowres):
     assert metadata["creator"]["version"] == geopandas.__version__
 
 
+def test_create_metadata_with_z_geometries():
+    geometry_types = [
+        "Point",
+        "Point Z",
+        "LineString",
+        "LineString Z",
+        "Polygon",
+        "Polygon Z",
+        "MultiPolygon",
+        "MultiPolygon Z",
+    ]
+    df = geopandas.GeoDataFrame(
+        {
+            "geo_type": geometry_types,
+            "geometry": [
+                Point(1, 2),
+                Point(1, 2, 3),
+                LineString([(0, 0), (1, 1), (2, 2)]),
+                LineString([(0, 0, 1), (1, 1, 2), (2, 2, 3)]),
+                Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]),
+                Polygon([(0, 0, 0), (0, 1, 0.5), (1, 1, 1), (1, 0, 0.5)]),
+                MultiPolygon(
+                    [
+                        Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]),
+                        Polygon([(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5)]),
+                    ]
+                ),
+                MultiPolygon(
+                    [
+                        Polygon([(0, 0, 0), (0, 1, 0.5), (1, 1, 1), (1, 0, 0.5)]),
+                        Polygon(
+                            [
+                                (0.5, 0.5, 1),
+                                (0.5, 1.5, 1.5),
+                                (1.5, 1.5, 2),
+                                (1.5, 0.5, 1.5),
+                            ]
+                        ),
+                    ]
+                ),
+            ],
+        },
+    )
+    metadata = _create_metadata(df)
+    assert sorted(metadata["columns"]["geometry"]["geometry_types"]) == sorted(
+        geometry_types
+    )
+    # only 3D geometries
+    metadata = _create_metadata(df.iloc[1::2])
+    assert all(
+        geom_type.endswith(" Z")
+        for geom_type in metadata["columns"]["geometry"]["geometry_types"]
+    )
+
+
 def test_crs_metadata_datum_ensemble():
     pyproj = pytest.importorskip("pyproj")
     # compatibility for older PROJ versions using PROJJSON with datum ensembles