Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions lib/cartopy/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,59 @@ def y_limits(self):
return self._y_limits


class LambertZoneII(Projection):
"""
Lambert zone II (extended) projection (https://epsg.io/27572).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some more text about this being used for France or general information about this projection so someone reading this would know more at first glance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added slightly more information in the docstring.


"""
def __init__(self):
proj4_params = [('proj', 'lcc'),
('lat_1', 46.8),
('lat_0', 46.8),
('lon_0', 0.0),
('k_0', 0.99987742),
('x_0', 600000.0),
('y_0', 2200000.0),
('pm', 'paris'),
('units', 'm'),
('no_defs', None)]

globe = Globe(semimajor_axis='6378249.2',
semiminor_axis='6356515.0',
towgs84='-168,-60,320,0,0,0,0')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do these values come from? (I didn't see anything about this in the EPSG website) Is this projection only valid with this Globe and do we need to allow users to override this default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ellipsoid corresponds to EPSG:7011. These values (and the conversion to WGS84) can be found in a report [in French] produced by the French National Institute of Geographic and Forest Information in sections 3.1.1 and 4.3.1, respectively. As EPSG:27572 clearly defines the ellipsoid to use, I don't think we need to allow users to override them.


super().__init__(proj4_params, globe=globe)

x0, x1, y0, y1 = [0, 1.2e6, 1.6e6, 2.7e6]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about these bounds values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the purpose of this projection is to cover hexagonal France and Corsica, these values include those two objects. They do not come from an official standard though. How else could they be chosen? I would gladly welcome suggestions on this and amend accordingly. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a quick look at the EPSG version in your original post, and realized that the boundary there is being set by the area_of_use, which should be a good quantity we can reference for our boundary. The interesting thing here is that I think the ellipsoid/globe being used may not correspond to the area of use coordinates being reported.

Here is we are calling self.as_geodetic()

points = self.transform_points(self.as_geodetic(), lons, lats)

but here it says that it is values reported using the WGS84 ellipse.
# Geographic area of the entire dataset referenced to WGS 84

So, I think this may be a bug in the epsg code path and we should be doing something like:
points = self.transform_points(PlateCarree().as_geodetic(), lons, lats)

Thoughts on how that looks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing with PlateCarree does fix the initial problem mentioned in the opening message of this PR. However, the resulting bounds are not wide enough to include the southmost part of hexagonal France and Corsica.

import matplotlib.pyplot as plt
import cartopy

ax = plt.subplot(projection=cartopy.crs.epsg(27572))
ax.set_extent(cartopy.crs.epsg(27572).bounds, crs=cartopy.crs.epsg(27572))
ax.add_feature(cartopy.feature.BORDERS)
ax.add_feature(cartopy.feature.COASTLINE)
ax.scatter(564055, 2571176, transform=cartopy.crs.epsg(27572))
plt.show()

myplot

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 My proposal would be to do something like this then to update the bounds manually after initialization from the EPSG crs.

class LambertZoneII(Projection):
    def __init__(self):
        crs = pyproj.CRS.from_epsg(27572)
        super().__init__(crs.to_wkt())
        # Projected bounds from https://epsg.io/27572
        self.bounds = [-5242.32, 1212512.16, 1589155.51, 2706796.21]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've done just that. Works just fine. :-)
I have updated the test image too, as the extent is now slightly different from the one I had arbitrarily set before.
Thanks.


self.bounds = (x0, x1, y0, y1)

self._boundary = sgeom.LineString(
[(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]
)

self._x_limits = [x0, x1]
self._y_limits = [y0, y1]

self._threshold = min(x1 - x0, y1 - y0) / 100.

@property
def boundary(self):
return self._boundary

@property
def x_limits(self):
return self._x_limits

@property
def y_limits(self):
return self._y_limits

@property
def threshold(self):
return self._threshold


class LambertAzimuthalEqualArea(Projection):
"""
A Lambert Azimuthal Equal-Area projection.
Expand Down
26 changes: 26 additions & 0 deletions lib/cartopy/tests/crs/test_lambert_conformal.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,29 @@ def test_single_npole(self):
assert_array_almost_equal(n_pole_crs.y_limits,
expected_y,
decimal=0)


class TestLambertZoneII:
def setup_class(self):
self.point_a = (1.4868268900254693, 48.13277955695077)
self.point_b = (-2.3188020040300126, 48.68412929316207)
self.src_crs = ccrs.PlateCarree()
self.nan = float('nan')

def test_default(self):
proj = ccrs.LambertZoneII()
res = proj.transform_point(*self.point_a, src_crs=self.src_crs)
np.testing.assert_array_almost_equal(res,
(536690.18620, 2348515.62248),
decimal=5)
res = proj.transform_point(*self.point_b, src_crs=self.src_crs)
np.testing.assert_array_almost_equal(res,
(257199.57387, 2419655.71471),
decimal=5)

def test_nan(self):
proj = ccrs.LambertZoneII()
res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs)
assert np.all(np.isnan(res))
res = proj.transform_point(float('nan'), 0.0, src_crs=self.src_crs)
assert np.all(np.isnan(res))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a super helpful map after all with the limited boundary :) But it is small enough and shows the boundaries so perhaps still useful? Up to you whether you think it is worthwhile to keep this or not, I could be convinced either way on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately we don't see the red and blue lines as on the other test maps. But I guess this is still another useful additional unit test to have. And it does allow to see its boundaries. All in all, I would be in favour to keep it. :)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/cartopy/tests/mpl/test_mpl_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def test_simple_global():
pytest.param((ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')),
id='InterruptedGoodeHomolosine'),
ccrs.LambertCylindrical,
ccrs.LambertZoneII,
pytest.param((ccrs.Mercator, dict(min_latitude=-85, max_latitude=85)),
id='Mercator'),
ccrs.Miller,
Expand Down