-
Notifications
You must be signed in to change notification settings - Fork 365
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
Conversation
this projection is relevant for mainland France, and is necessary because `cartopy.crs.epsg(27572)` does not work when used as a ``projection`` argument for geoaxes
There seems to be a precision problem between the conversion using >>> cartopy.crs.L2E().transform_point(1.4868268900254693, 48.13277955695077, src_crs=cartopy.crs.PlateCarree())
(np.float64(536690.1861966171), np.float64(2348515.622484158)) vs. >>> pyproj.Transformer.from_crs("EPSG:4326", "EPSG:27572").transform(48.13277955695077, 1.4868268900254693)
(536745.8962648578, 2348522.778985058) Yet: >>> cartopy.crs.L2E().to_proj4()
/home/user/miniforge3/envs/env/lib/python3.12/site-packages/pyproj/crs/crs.py:1293: UserWarning: You will likely lose important projection information when converting to a PROJ string from another format. See: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems
proj = self._crs.to_proj4(version=version)
'+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +ellps=clrk80ign +pm=paris +towgs84=-168,-60,320,0,0,0,0 +units=m +no_defs +type=crs' >>> pyproj.CRS.from_epsg(27572).to_proj4()
/home/user/miniforge3/envs/env/lib/python3.12/site-packages/pyproj/crs/crs.py:1293: UserWarning: You will likely lose important projection information when converting to a PROJ string from another format. See: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems
proj = self._crs.to_proj4(version=version)
'+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +ellps=clrk80ign +pm=paris +units=m +no_defs +type=crs' Here are the packages versions I am working with: $ mamba list
cartopy 0.23.0 py312h1d6d2e6_1 conda-forge
...
proj 9.4.1 h54d7996_1 conda-forge
...
pyproj 3.6.1 py312h01329cd_8 conda-forge
Is this expected? Should I adjust the expected values in the unit test accordingly? |
Hi there, Could anyone have a look at this PR please? Thank you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a great start! Similar comments as here to update the test images too:
#2442 (review)
Regarding your comment
There seems to be a precision problem between the conversion using pyproj directly and the method transform_point of Projection.
This could be due to using PlateCarree and not using a geodetic coordinate system?
pyproj.CRS.from_epsg(4326).to_proj4()
'+proj=longlat +datum=WGS84 +no_defs +type=crs'
cartopy.crs.PlateCarree().to_proj4()
'+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +to_meter=111319.490793274 +no_defs +type=crs'
lib/cartopy/crs.py
Outdated
@@ -1815,6 +1815,60 @@ def y_limits(self): | |||
return self._y_limits | |||
|
|||
|
|||
class L2E(Projection): |
There was a problem hiding this comment.
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.
class L2E(Projection): | |
class LambertZoneII(LambertConformal): |
There was a problem hiding this comment.
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.
Hi @greglucas, Thank you very much for reviewing my PR. I have renamed the class, updated the expected values for the unit test (given the PlateCarree vs. EPSG:4326 inherent differences), and added a test image for this new projection. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might help to add some comments/links for the values you've chosen here. It is unclear whether some of them are what you think looks good for this projection versus parameters that are dictated by some standard.
I think it might make sense to add a scale_factor
or k_0
keyword argument to the LambertConformal
class which would allow you to sublcass this and then set the explicit values through the super call (similar to what OSGB is doing). Although you also have a different boundary which is currently just the corner points and thus won't be "curved" like the general LambertConformal
class which has a boundary made from linspace (not sure if that is preferable or not?).
Feel free to disagree with any of these comments, most of them are just my ignorance of the projection itself.
lib/cartopy/crs.py
Outdated
@@ -1815,6 +1815,59 @@ def y_limits(self): | |||
return self._y_limits | |||
|
|||
|
|||
class LambertZoneII(Projection): | |||
""" | |||
Lambert zone II (extended) projection (https://epsg.io/27572). |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/cartopy/crs.py
Outdated
globe = Globe(semimajor_axis='6378249.2', | ||
semiminor_axis='6356515.0', | ||
towgs84='-168,-60,320,0,0,0,0') |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. :)
lib/cartopy/crs.py
Outdated
|
||
super().__init__(proj4_params, globe=globe) | ||
|
||
x0, x1, y0, y1 = [0, 1.2e6, 1.6e6, 2.7e6] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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()
Line 688 in 694efcf
points = self.transform_points(self.as_geodetic(), lons, lats) |
but here it says that it is values reported using the WGS84 ellipse.
Line 678 in 694efcf
# 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?
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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]
There was a problem hiding this comment.
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.
Ok, I will consider this and get back to you on this soon. Thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Thank you for going back and forth on the implementation with me and sorry for sending you down a few different paths. I think this makes for a nice example of how we can add other named projections following this pattern.
I'm going to leave this open for a day or two in case anyone else wants to chime in with anything.
Thank you very much for your help and advice @greglucas. :-) |
Rationale
The use of
cartopy.crs.epsg(27572)
for the Lambert Zone II projection does not always work properly when provided as a keyword argument for GeoAxes. Yet, this is still a relevant and widely used projection for mainland France, so it would be very helpful to have a working alternative incartopy
, like other national CRSs such ascartopy.crs.OSGB
for example.Implications
This change will make it possible for users to plot geospatial datasets in this projection.
Example
vs.
Thank you to @muniers for originally implementing the solution contained in this PR, and for allowing me to submit this PR.