Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a special projection for EPSG:27572 #2427

Merged
merged 9 commits into from
Oct 7, 2024
54 changes: 54 additions & 0 deletions lib/cartopy/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,60 @@ def y_limits(self):
return self._y_limits


class L2E(Projection):
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this inherit from LambertConformal if it is that type of projection? I think the naming should contain Lambert at least, here is a quick suggestion.

Suggested change
class L2E(Projection):
class LambertZoneII(LambertConformal):

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 not inherited from LambertConformal because it does not make it possible to set some PROJ4 parameters, such as the scaling factor k_0.

"""
Lambert zone II projection (also known as "Lambert 2 étendue" in French)
(https://epsg.io/27572).

"""
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 TestL2E:
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.L2E()
res = proj.transform_point(*self.point_a, src_crs=self.src_crs)
np.testing.assert_array_almost_equal(res,
(536745.89626, 2348522.77899),
decimal=5)
res = proj.transform_point(*self.point_b, src_crs=self.src_crs)
np.testing.assert_array_almost_equal(res,
(257266.90019, 2419663.00145),
decimal=5)

def test_nan(self):
proj = ccrs.L2E()
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))
Loading