Repository: SciTools/cartopy (python)

Issue:
Projection._project_multiline() returns empty list rather than empty sgeom.MultiLineString
### Description

When providing a MultiLineString to the [crs.Projection.project_geometry()](https://scitools.org.uk/cartopy/docs/latest/_modules/cartopy/crs.html#Projection) function, if an empty MultiLineString is provided or if the projection returns no geometries, an empty list is returned, rather than an empty MultiLineString. In all other _project_* functions, an empty geometry is returned if the geoms list is empty.

This is a problem for me as I am projecting global LineStrings/MultiLineStrings from Plate Carree to Mercator, and some of these objects may be north/south of the limits of the Mercator projection, and so return nothing when projected. This leads to crs.Projection.project_geometry() returning an empty list rather than an empty MultiLineString, which caused my downstream code to fail as it expected the returned object to have  a `geoms` attribute. I can bypass this issue with an `isinstance(geom, list)` condition, but the underlying issue should probably be fixed.

#### Code to reproduce

```
>>> import cartopy.crs as ccrs
>>> import shapely.geometry as sgeom
>>> ccrs.Mercator().project_geometry(sgeom.MultiLineString([sgeom.LineString([[0, 0], [1, 1]])]), ccrs.PlateCarree())
<MULTILINESTRING ((0 0, 111319.491 110579.965))>
>>> type(ccrs.Mercator().project_geometry(sgeom.MultiLineString([sgeom.LineString([[0, 0], [1, 1]])]), ccrs.PlateCarree()))
<class 'shapely.geometry.multilinestring.MultiLineString'>
>>> ccrs.Mercator().project_geometry(sgeom.MultiLineString(), ccrs.PlateCarree())
[]
>>> type(ccrs.Mercator().project_geometry(sgeom.MultiLineString(), ccrs.PlateCarree()))
<class 'list'>
>>> ccrs.Mercator().project_geometry(sgeom.MultiLineString([sgeom.LineString([[0, 88], [1, 88]])]), ccrs.PlateCarree()) 
[]
>>> type(ccrs.Mercator().project_geometry(sgeom.MultiLineString([sgeom.LineString([[0, 88], [1, 88]])]), ccrs.PlateCarree()))
<class 'list'>
```

Pull request:
FIX/MNT: Simplify project geometry handling
The MultiLineString return type was a plain list, but it should be an empty MultiLineString to be consistent with the other return types. Additionally, all geometry constructors take empty lists, so just use that rather than special-casing the returns.

Closes #2406 

Patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
index c2441516..bc354198 100644
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -918,10 +918,7 @@ class Projection(CRS, metaclass=ABCMeta):
         geoms = []
         for geom in geometry.geoms:
             geoms.append(self._project_point(geom, src_crs))
-        if geoms:
-            return sgeom.MultiPoint(geoms)
-        else:
-            return sgeom.MultiPoint()
+        return sgeom.MultiPoint(geoms)
 
     def _project_multiline(self, geometry, src_crs):
         geoms = []
@@ -929,10 +926,7 @@ class Projection(CRS, metaclass=ABCMeta):
             r = self._project_line_string(geom, src_crs)
             if r:
                 geoms.extend(r.geoms)
-        if geoms:
-            return sgeom.MultiLineString(geoms)
-        else:
-            return []
+        return sgeom.MultiLineString(geoms)
 
     def _project_multipolygon(self, geometry, src_crs):
         geoms = []
@@ -940,11 +934,7 @@ class Projection(CRS, metaclass=ABCMeta):
             r = self._project_polygon(geom, src_crs)
             if r:
                 geoms.extend(r.geoms)
-        if geoms:
-            result = sgeom.MultiPolygon(geoms)
-        else:
-            result = sgeom.MultiPolygon()
-        return result
+        return sgeom.MultiPolygon(geoms)
 
     def _project_polygon(self, polygon, src_crs):
         """


Test patch:
diff --git a/lib/cartopy/tests/test_line_string.py b/lib/cartopy/tests/test_line_string.py
index a0b42ff0..926dd5b5 100644
--- a/lib/cartopy/tests/test_line_string.py
+++ b/lib/cartopy/tests/test_line_string.py
@@ -8,6 +8,7 @@ import time
 
 import numpy as np
 import pytest
+import shapely
 import shapely.geometry as sgeom
 
 import cartopy.crs as ccrs
@@ -73,6 +74,15 @@ class TestLineString:
         tgt_proj.project_geometry(line_string, src_proj)
         assert time.time() < cutoff_time, 'Projection took too long'
 
+    @pytest.mark.skipif(shapely.__version__ < "2",
+                        reason="Shapely <2 has an incorrect geom_type ")
+    def test_multi_linestring_return_type(self):
+        # Check that the return type of project_geometry is a MultiLineString
+        # and not an empty list
+        multi_line_string = ccrs.Mercator().project_geometry(
+            sgeom.MultiLineString(), ccrs.PlateCarree())
+        assert isinstance(multi_line_string, sgeom.MultiLineString)
+
 
 class FakeProjection(ccrs.PlateCarree):
     def __init__(self, left_offset=0, right_offset=0):