Repository: martinfleis/momepy (python)

Issue:
Tesselation fails at Generating input point array... with ValueError: need at least one array to concatenate
As the title says, Tesselation fails at the `Generating input point array....` step with `ValueError: need at least one array to concatenate`

My code is simply:
```
import momepy as mm
tess = mm.Tessellation(edgeDF5, 'communityID', limit=wardsPolygon, shrink=0)
```
Where `wardsPolygon` is a `<class 'shapely.geometry.multipolygon.MultiPolygon'>` of Tokyo's 23 wards and
`'communityID'` is a column of unique IDs for each polygon in my geopadaframe called `edgeDF5`, which contains Polygons of neighborhoods in the 'geometry' column (and no other columns).
I don't want space between adjacent polygons; I want a partition (every point is in some polygon), so I set the shrink to 0.

Here's the output:
```
Traceback (most recent call last):

  File "...3938314878.py", line 1, in <module>
    tess = mm.Tessellation(edgeDF5, 'communityID', limit=wardsPolygon, shrink=0)

  File "...\lib\site-packages\momepy\elements.py", line 252, in __init__
    self.tessellation = self._morphological_tessellation(

  File "...\lib\site-packages\momepy\elements.py", line 276, in _morphological_tessellation
    points, ids = self._dense_point_array(

  File "...\lib\site-packages\momepy\elements.py", line 334, in _dense_point_array
    points = np.vstack(points)

  File "<__array_function__ internals>", line 5, in vstack

  File "...\lib\site-packages\numpy\core\shape_base.py", line 282, in vstack
    return _nx.concatenate(arrs, 0)

  File "<__array_function__ internals>", line 5, in concatenate

ValueError: need at least one array to concatenate
```
The result of checking the input is: 
```
print(mm.CheckTessellationInput(edgeDF5, shrink=0))
Collapsed features  : 0
Split features      : 0
Overlapping features: 0
```
So, why isn't this working and what do I need to do to make it work?

Pull request description:
ENH: catch geographic CRS in Tessellation
Closes #297

Patch:
diff --git a/momepy/elements.py b/momepy/elements.py
index 6740a10..d09f324 100644
--- a/momepy/elements.py
+++ b/momepy/elements.py
@@ -5,6 +5,7 @@
 # generating derived elements (street edge, block)
 import geopandas as gpd
 import libpysal
+import warnings
 import numpy as np
 import pygeos
 import pandas as pd
@@ -200,6 +201,13 @@ class Tessellation:
         self.segment = segment
         self.enclosure_id = enclosure_id
 
+        if gdf.crs and gdf.crs.is_geographic:
+            raise ValueError(
+                "Geometry is in a geographic CRS. "
+                "Use 'GeoDataFrame.to_crs()' to re-project geometries to a "
+                "projected CRS before using Tessellation.\n",
+            )
+
         if limit is not None and enclosures is not None:
             raise ValueError(
                 "Both `limit` and `enclosures` cannot be passed together. "
@@ -367,7 +375,6 @@ class Tessellation:
         ids_original = list(orig_gdf[unique_id])
         ids_generated = list(tesselation[unique_id])
         if len(ids_original) != len(ids_generated):
-            import warnings
 
             self.collapsed = set(ids_original).difference(ids_generated)
             warnings.warn(
@@ -381,8 +388,6 @@ class Tessellation:
             unique_id
         ]
         if len(self.multipolygons) > 0:
-            import warnings
-
             warnings.warn(
                 "Tessellation contains MultiPolygon elements. Initial objects should "
                 f"be edited. unique_id of affected elements: {list(self.multipolygons)}"
@@ -459,8 +464,6 @@ class Tessellation:
             except ImportError:
                 use_dask = False
 
-                import warnings
-
                 warnings.warn(
                     "dask.dataframe could not be imported. Setting `use_dask=False`."
                 )
@@ -745,8 +748,6 @@ def get_network_id(left, right, network_id, min_size=100, verbose=True):
     series = pd.Series(result, index=left.index)
 
     if series.isnull().any():
-        import warnings
-
         warnings.warn(
             "Some objects were not attached to the network. "
             "Set larger min_size. {} affected elements".format(sum(series.isnull()))


Test patch:
diff --git a/tests/test_elements.py b/tests/test_elements.py
index a3d0d04..6fc06a8 100644
--- a/tests/test_elements.py
+++ b/tests/test_elements.py
@@ -33,6 +33,7 @@ class TestElements:
         ).tessellation
         assert len(bands) == len(self.df_streets)
 
+    def test_enclosed_tess(self):
         #  test_enclosed_tessellation
         enc1 = mm.Tessellation(
             self.df_buildings, "uID", enclosures=self.enclosures
@@ -51,11 +52,13 @@ class TestElements:
 
         assert_geodataframe_equal(enc1, enc1_loop)
 
-        with pytest.raises(ValueError):
+    def test_limit_enclosures_combo_error(self):
+        with pytest.raises(ValueError, match="Both `limit` and `enclosures` cannot"):
             mm.Tessellation(
                 self.df_buildings, "uID", limit=self.limit, enclosures=self.enclosures
             )
 
+    def test_custom_enclosure_id(self):
         # non-standard enclosure ids
         encl = self.enclosures.copy()
         ids = list(range(len(encl) * 2))
@@ -66,7 +69,7 @@ class TestElements:
         assert len(enc) == 155
         assert isinstance(enc, gpd.GeoDataFrame)
 
-        # erroneous geometry
+    def test_erroroneous_geom(self):
         df = self.df_buildings
         b = df.total_bounds
         x = np.mean([b[0], b[2]])
@@ -80,6 +83,10 @@ class TestElements:
         assert tess.collapsed == {145}
         assert len(tess.multipolygons) == 3
 
+    def test_crs_error(self):
+        with pytest.raises(ValueError, match="Geometry is in a geographic CRS"):
+            mm.Tessellation(self.df_buildings.to_crs(4326), "uID", self.limit)
+
     def test_Blocks(self):
         blocks = mm.Blocks(
             self.df_tessellation, self.df_streets, self.df_buildings, "bID", "uID"