-
Notifications
You must be signed in to change notification settings - Fork 366
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
Introduce path_to_shapely
and shapely_to_path
#2455
Conversation
lib/cartopy/mpl/patch.py
Outdated
if geom.is_empty: | ||
pass |
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.
These 2 lines were introduced at #560, but the test added then has since been updated to expect a list of points. So I don't think this loop is relevant any more.
lib/cartopy/mpl/patch.py
Outdated
# Figure out what type of object to return | ||
if not polygons: | ||
if not linestrings: | ||
if not points: | ||
# No geometries. Return an empty point | ||
return sgeom.Point() | ||
elif len(points) > 1: | ||
return sgeom.MultiPoint(points) | ||
else: | ||
return points[0] | ||
elif not points: | ||
if len(linestrings) > 1: | ||
return sgeom.MultiLineString(linestrings) | ||
else: | ||
return linestrings[0] | ||
else: | ||
if not linestrings and not points: | ||
if len(polygons) > 1: | ||
return sgeom.MultiPolygon(polygons) | ||
else: | ||
return polygons[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.
I feel like I'm missing an obvious slicker way to do this...
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.
There is a lot of logic encapsulated here, so I think you might only be able to get it slightly simpler.
Maybe you could put these cases into a pattern matching syntax if we are only supporting 3.10+ for the next release... That might not be any easier/cleaner, so just throwing that out there as a possible idea. I think this looks OK currently.
@@ -122,7 +122,8 @@ def test_contourf_transform_path_counting(): | |||
gc.collect() | |||
initial_cache_size = len(cgeoaxes._PATH_TRANSFORM_CACHE) | |||
|
|||
with mock.patch('cartopy.mpl.patch.path_to_geos') as path_to_geos_counter: | |||
with mock.patch('cartopy.mpl.patch.path_to_shapely', | |||
wraps=cartopy.mpl.patch.path_to_shapely) as path_to_shapely_counter: |
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 was failing because project_geometry
received a MagicMock
that it didn't know what to do with. I'm confused why that didn't also happen previously with path_to_geos
.
I also tested this with my user code that generates many contour plots. I got identical results with my branch and with |
Broke the minimum versions tests 😢 |
Most of the tests were fixed by bumping minimum Shapely to v2.0. I think that’s reasonable to do since v2 will be 2 years old in December. However, I think the remaining test failure might need a workaround for lack of matplotlib/matplotlib#25252. It’s too soon to drop support for MPL 3.7. |
lib/cartopy/mpl/patch.py
Outdated
:class:`shapely.geometry.collection.GeometryCollection`. | ||
|
||
""" | ||
# Convert path into numpy array of vertices (and associated codes) |
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.
Are we able to leverage any of the recent contour logic / path handling you were updating in Matplotlib?
There are a bunch of path handling routines for stitching the previous individual paths into a compound path with the recent refactor that might give some inspiration here as well.
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.
ContourSet's allsegs
and allkinds
use the private Path._iter_connected_components
. We could raise a feature request to have that made public. However, I made a quick test swapping out our path splitting logic here for _iter_connected_components
and got some spectacular image test failures. I didn't dig too deep, but it seems some of the _iter_connected_components
paths have an extra STOP
on the end which our own splitting logic doesn't produce.
Or have I misunderstood what you meant?
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.
That is exactly what I was wondering, thanks for looking into it :) I'm satisfied to keep it here for now and we can always move to something shared later.
I wonder if the extra STOPs result from an extra LINETO in the simplified path creation in MPL, noticed here and in the linked issues:
matplotlib/matplotlib#28478 (comment)
But that is a digression, this works great for now 👍
lib/cartopy/mpl/patch.py
Outdated
# Figure out what type of object to return | ||
if not polygons: | ||
if not linestrings: | ||
if not points: | ||
# No geometries. Return an empty point | ||
return sgeom.Point() | ||
elif len(points) > 1: | ||
return sgeom.MultiPoint(points) | ||
else: | ||
return points[0] | ||
elif not points: | ||
if len(linestrings) > 1: | ||
return sgeom.MultiLineString(linestrings) | ||
else: | ||
return linestrings[0] | ||
else: | ||
if not linestrings and not points: | ||
if len(polygons) > 1: | ||
return sgeom.MultiPolygon(polygons) | ||
else: | ||
return polygons[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.
There is a lot of logic encapsulated here, so I think you might only be able to get it slightly simpler.
Maybe you could put these cases into a pattern matching syntax if we are only supporting 3.10+ for the next release... That might not be any easier/cleaner, so just throwing that out there as a possible idea. I think this looks OK currently.
lib/cartopy/mpl/patch.py
Outdated
if _MPL_38 or path.vertices.size > 0: | ||
paths.append(path) | ||
return Path.make_compound_path(*paths) | ||
elif hasattr(shape, '_as_mpl_path'): |
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.
What shapes/objects have _as_mpl_path
that we haven't handled above? This seems a bit odd to have the function shapely_to_path
but then accept other things besides shapely geometries.
lib/cartopy/tests/mpl/test_patch.py
Outdated
import shapely.geometry as sgeom | ||
|
||
import cartopy.mpl.patch as cpatch | ||
|
||
|
||
class Test_path_to_geos: | ||
def test_empty_polygon(self): | ||
@pytest.mark.parametrize('legacy', [False, True]) |
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.
Maybe be a bit more explicit in the variable name about what is legacy here.
@pytest.mark.parametrize('legacy', [False, True]) | |
@pytest.mark.parametrize('use_legacy_path_to_geos', [False, True]) |
docs/source/whatsnew/v0.25.rst
Outdated
deprecated. Use `~cartopy.mpl.path.path_to_shapely` and | ||
`~cartopy.mpl.path.shapely_to_path` instead. | ||
|
||
* `cartopy.mpl.patch.path_segments` has moved to `cartopy.mpl.path.path_segments`. |
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'm wondering if we actually want to privatise path_segments
? Since it's only two lines it's not hard for downstream to vendor, and it would mean we have more freedom to do what we want with it in future.
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.
Yeah, I think it would be good to do this and it would allow us to use MPL's version in the future easier as a drop-in replacement.
@@ -0,0 +1,92 @@ | |||
# Copyright Crown and Cartopy Contributors |
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 used git mv
to change this from test_patch.py
so I'm not clear why it's showing as a deletion and new file 😕
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've had this happen before as well and I think it is due to gits # of lines changed detection limits for what it decides is a move vs change, but I'm not positive and also curious.
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 moved the rename to its own commit, so it's now easier to see what I actually changed. It also restored the blame that had gone missing, so I propose to keep that separate commit at merge.
lib/cartopy/mpl/patch.py
Outdated
:class:`shapely.geometry.collection.GeometryCollection`. | ||
|
||
""" | ||
# Convert path into numpy array of vertices (and associated codes) |
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.
ContourSet's allsegs
and allkinds
use the private Path._iter_connected_components
. We could raise a feature request to have that made public. However, I made a quick test swapping out our path splitting logic here for _iter_connected_components
and got some spectacular image test failures. I didn't dig too deep, but it seems some of the _iter_connected_components
paths have an extra STOP
on the end which our own splitting logic doesn't produce.
Or have I misunderstood what you meant?
Rationale
Closes #2447. Introduce
shapely_to_path
which maps a single Shapely geometry or collection to a single Matplotlib path, andpath_to_shapely
, which does the opposite. To take full advantage of this in our transforms code, also modifyproject_geometry
so that it returns aGeometryCollection
from either aGeometryCollection
or aLinearRing
. PreviouslyGeometryCollection
was not supported andLinearRing
was mapped to a tuple.Note the first commit just duplicates the existing
path_to_geos
andgeos_to_path
and all the material changes are in the second commit. I rearranged the logic inpath_to_shapely
because it seemed simpler to separate the geometry types into different lists up front.I also went ahead and started a whatsnew for the next release. I think that was suggested a couple of releases ago and it does seem simpler to me to add the entries as we go.
Implications
The new behaviour of
project_geometry
from aLinearRing
could be a breaking change but, since the docstring ofproject_geometry
states that it returns a Shapely geometry, I claim this one as a bug fix.