Repository: SciTools/cartopy (python)

Issue:
Error initializing LambertConformal projection on sphere with 0.23
### Description
<!-- Please provide a general introduction to the issue/proposal. -->
Found this while trying to upgrade MetPy's CI to run with Cartopy 0.23.

<!--
If you are reporting a bug, attach the *entire* traceback from Python.

If you are proposing an enhancement/new feature, provide links to related articles, reference examples, etc.

If you are asking a question, please ask on StackOverflow and use the cartopy tag. All cartopy
questions on StackOverflow can be found at https://stackoverflow.com/questions/tagged/cartopy
-->

#### Code to reproduce

```python
import cartopy.crs as ccrs
ccrs.LambertConformal(globe=ccrs.Globe(ellipse='sphere'))
```

#### Traceback

```pytb
---------------------------------------------------------------------------
GEOSException                             Traceback (most recent call last)
Cell In[2], line 1
----> 1 ccrs.LambertConformal(globe=ccrs.Globe(ellipse='sphere'))

File [~/miniconda3/envs/py312/lib/python3.12/site-packages/cartopy/crs.py:1795](http://localhost:51607/lab/tree/~/miniconda3/envs/py312/lib/python3.12/site-packages/cartopy/crs.py#line=1794), in LambertConformal.__init__(self, central_longitude, central_latitude, false_easting, false_northing, standard_parallels, globe, cutoff)
   1790     lons[1:-1] = np.linspace(central_longitude - 180 + 0.001,
   1791                              central_longitude + 180 - 0.001, n)
   1793 points = self.transform_points(PlateCarree(globe=globe), lons, lats)
-> 1795 self._boundary = sgeom.LinearRing(points)
   1796 mins = np.min(points, axis=0)
   1797 maxs = np.max(points, axis=0)

File [~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/geometry/polygon.py:104](http://localhost:51607/lab/tree/~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/geometry/polygon.py#line=103), in LinearRing.__new__(self, coordinates)
     99 if len(coordinates) == 0:
    100     # empty geometry
    101     # TODO better constructor + should shapely.linearrings handle this?
    102     return shapely.from_wkt("LINEARRING EMPTY")
--> 104 geom = shapely.linearrings(coordinates)
    105 if not isinstance(geom, LinearRing):
    106     raise ValueError("Invalid values passed to LinearRing constructor")

File [~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/decorators.py:77](http://localhost:51607/lab/tree/~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/decorators.py#line=76), in multithreading_enabled.<locals>.wrapped(*args, **kwargs)
     75     for arr in array_args:
     76         arr.flags.writeable = False
---> 77     return func(*args, **kwargs)
     78 finally:
     79     for arr, old_flag in zip(array_args, old_flags):

File [~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/creation.py:171](http://localhost:51607/lab/tree/~/miniconda3/envs/py312/lib/python3.12/site-packages/shapely/creation.py#line=170), in linearrings(coords, y, z, indices, out, **kwargs)
    169 coords = _xyz_to_coords(coords, y, z)
    170 if indices is None:
--> 171     return lib.linearrings(coords, out=out, **kwargs)
    172 else:
    173     return simple_geometries_1d(coords, indices, GeometryType.LINEARRING, out=out)

GEOSException: IllegalArgumentException: Points of LinearRing do not form a closed linestring
```

<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system


### Cartopy version
0.23

Shapely 2.0.4
</details>


Pull request description:
Fix incorrect internal use of PlateCarree
<!--

Thanks for contributing to cartopy!
Please use this template as a guide to streamline the pull request you are about to make.

Remember: it is significantly easier to merge small pull requests. Consider whether this pull
request could be broken into smaller parts before submitting.

-->

## Rationale

This replaces a couple of internal uses of PlateCarree() for lon/lat coords with more proper geodetic CRS's on the same ellipse, which avoids some precision problems. In the case of LambertConformal, this was resulting in a traceback when creating a boundary on a spherical Earth datum.

This also fixes another case doing the same thing, seemingly without problem, but it seems better to avoid `PlateCarree()` here too.

Fixes #2377.
<!-- Please provide detail as to *why* you are making this change. -->


## Implications

<!-- If applicable, to the best of your knowledge, what are the implications of this change? -->


<!--
## Checklist

 * If this is a new feature, please provide an example of its use in the description. We may want to make a
   follow-on pull request to put the example in the gallery!

 * Ensure there is a suitable item in the cartopy test suite for the change you are proposing.

 * Note that you will automatically be asked to sign the
   [Contributor Licence Agreement](https://cla-assistant.io/SciTools/)
   (CLA), if you have not already done so.

-->


Patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
index e43a266a..a39703e8 100644
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -1790,7 +1790,7 @@ class LambertConformal(Projection):
             lons[1:-1] = np.linspace(central_longitude - 180 + 0.001,
                                      central_longitude + 180 - 0.001, n)
 
-        points = self.transform_points(PlateCarree(globe=globe), lons, lats)
+        points = self.transform_points(self.as_geodetic(), lons, lats)
 
         self._boundary = sgeom.LinearRing(points)
         mins = np.min(points, axis=0)
@@ -1866,7 +1866,7 @@ class LambertAzimuthalEqualArea(Projection):
         lon = central_longitude + 180
         sign = np.sign(central_latitude) or 1
         lat = -central_latitude + sign * 0.01
-        x, max_y = self.transform_point(lon, lat, PlateCarree(globe=globe))
+        x, max_y = self.transform_point(lon, lat, self.as_geodetic())
 
         coords = _ellipse_boundary(a * 1.9999, max_y - false_northing,
                                    false_easting, false_northing, 61)


Test patch:
diff --git a/lib/cartopy/tests/crs/test_lambert_conformal.py b/lib/cartopy/tests/crs/test_lambert_conformal.py
index 23bfea68..da754c4d 100644
--- a/lib/cartopy/tests/crs/test_lambert_conformal.py
+++ b/lib/cartopy/tests/crs/test_lambert_conformal.py
@@ -3,6 +3,7 @@
 # This file is part of Cartopy and is released under the BSD 3-clause license.
 # See LICENSE in the root of the repository for full licensing details.
 
+import numpy as np
 from numpy.testing import assert_array_almost_equal
 import pyproj
 import pytest
@@ -38,6 +39,17 @@ def test_default_with_cutoff():
                               (-49788019.81831982, 30793476.08487709))
 
 
+def test_sphere():
+    """Test LambertConformal with spherical globe. (#2377)"""
+    globe = ccrs.Globe(ellipse='sphere')
+
+    # This would error creating a boundary
+    crs = ccrs.LambertConformal(globe=globe)
+
+    assert np.all(np.isfinite(crs.x_limits))
+    assert np.all(np.isfinite(crs.y_limits))
+
+
 def test_specific_lambert():
     # This projection comes from EPSG Projection 3034 - ETRS89 / ETRS-LCC.
     crs = ccrs.LambertConformal(central_longitude=10,