Skip to content

Commit

Permalink
Implement path_to_shapely and shapely_to_path
Browse files Browse the repository at this point in the history
  • Loading branch information
rcomer committed Oct 20, 2024
1 parent 5ba97d1 commit b53ab7f
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 291 deletions.
14 changes: 13 additions & 1 deletion docs/source/reference/matplotlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Artist extensions
SlippyImageArtist

Patch
~~~~~~~~~~~~~~~~~~~~~
~~~~~

.. automodule:: cartopy.mpl.patch

Expand All @@ -74,3 +74,15 @@ Patch
geos_to_path
path_segments
path_to_geos

Path
~~~~

.. automodule:: cartopy.mpl.path

.. autosummary::
:toctree: generated/

path_segments
path_to_shapely
shapely_to_path
18 changes: 18 additions & 0 deletions docs/source/whatsnew/v0.25.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,21 @@ Version 0.25 (Date TBD)
The new minimum supported versions of dependencies that have been updated are:

* Shapely 2.0


Features
--------

* Ruth Comer introduced `~cartopy.mpl.path.shapely_to_path` and
`~cartopy.mpl.path.path_to_shapely` which map a single Shapely geometry or
collection to a single Matplotlib path and *vice versa*. (:pull:`2455`)


Deprecations and Removals
-------------------------

* `~cartopy.mpl.patch.path_to_geos` and `~cartopy.mpl.patch.geos_to_path` are
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`.
14 changes: 11 additions & 3 deletions lib/cartopy/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ class Projection(CRS, metaclass=ABCMeta):
'MultiPoint': '_project_multipoint',
'MultiLineString': '_project_multiline',
'MultiPolygon': '_project_multipolygon',
'GeometryCollection': '_project_geometry_collection'
}
# Whether or not this projection can handle wrapped coordinates
_wrappable = False
Expand Down Expand Up @@ -835,7 +836,8 @@ def _project_line_string(self, geometry, src_crs):
def _project_linear_ring(self, linear_ring, src_crs):
"""
Project the given LinearRing from the src_crs into this CRS and
returns a list of LinearRings and a single MultiLineString.
returns a GeometryCollection containing zero or more LinearRings and
a single MultiLineString.
"""
debug = False
Expand Down Expand Up @@ -915,7 +917,7 @@ def _project_linear_ring(self, linear_ring, src_crs):
if rings:
multi_line_string = sgeom.MultiLineString(line_strings)

return rings, multi_line_string
return sgeom.GeometryCollection([*rings, multi_line_string])

def _project_multipoint(self, geometry, src_crs):
geoms = []
Expand All @@ -939,6 +941,11 @@ def _project_multipolygon(self, geometry, src_crs):
geoms.extend(r.geoms)
return sgeom.MultiPolygon(geoms)

def _project_geometry_collection(self, geometry, src_crs):
return sgeom.GeometryCollection(
[self.project_geometry(geom, src_crs) for geom in geometry.geoms])


def _project_polygon(self, polygon, src_crs):
"""
Return the projected polygon(s) derived from the given polygon.
Expand All @@ -957,7 +964,8 @@ def _project_polygon(self, polygon, src_crs):
rings = []
multi_lines = []
for src_ring in [polygon.exterior] + list(polygon.interiors):
p_rings, p_mline = self._project_linear_ring(src_ring, src_crs)
geom_collection = self._project_linear_ring(src_ring, src_crs)
*p_rings, p_mline = geom_collection.geoms
if p_rings:
rings.extend(p_rings)
if len(p_mline.geoms) > 0:
Expand Down
9 changes: 2 additions & 7 deletions lib/cartopy/mpl/feature_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

import matplotlib.artist
import matplotlib.collections
import matplotlib.path as mpath
import numpy as np

import cartopy.feature as cfeature
from cartopy.mpl import _MPL_38
import cartopy.mpl.patch as cpatch
import cartopy.mpl.path as cpath


class _GeomKey:
Expand Down Expand Up @@ -217,11 +216,7 @@ def draw(self, renderer):
else:
projected_geom = geom

geom_paths = cpatch.geos_to_path(projected_geom)

# The transform may have split the geometry into two paths, we only want
# one compound path.
geom_path = mpath.Path.make_compound_path(*geom_paths)
geom_path = cpath.shapely_to_path(projected_geom)
mapping[key] = geom_path

if self._styler is None:
Expand Down
35 changes: 10 additions & 25 deletions lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import cartopy.mpl.contour
import cartopy.mpl.feature_artist as feature_artist
import cartopy.mpl.geocollection
import cartopy.mpl.patch as cpatch
import cartopy.mpl.path as cpath
from cartopy.mpl.slippy_image_artist import SlippyImageArtist


Expand Down Expand Up @@ -170,26 +170,11 @@ def transform_path_non_affine(self, src_path):
if src_path.vertices.shape == (1, 2):
return mpath.Path(self.transform(src_path.vertices))

transformed_geoms = []
geoms = cpatch.path_to_geos(src_path)
geom = cpath.path_to_shapely(src_path)
transformed_geom = self.target_projection.project_geometry(
geom, self.source_projection)

for geom in geoms:
proj_geom = self.target_projection.project_geometry(
geom, self.source_projection)
transformed_geoms.append(proj_geom)

if not transformed_geoms:
result = mpath.Path(np.empty([0, 2]))
else:
paths = cpatch.geos_to_path(transformed_geoms)
if not paths:
return mpath.Path(np.empty([0, 2]))
points, codes = list(zip(*[cpatch.path_segments(path,
curves=False,
simplify=False)
for path in paths]))
result = mpath.Path(np.concatenate(points, 0),
np.concatenate(codes))
result = cpath.shapely_to_path(transformed_geom)

# store the result in the cache for future performance boosts
key = (self.source_projection, self.target_projection)
Expand Down Expand Up @@ -228,14 +213,14 @@ def set_transform(self, transform):
super().set_transform(self._trans_wrap)

def set_boundary(self, path, transform):
self._original_path = cpatch._ensure_path_closed(path)
self._original_path = cpath._ensure_path_closed(path)
self.set_transform(transform)
self.stale = True

def _adjust_location(self):
if self.stale:
self.set_path(
cpatch._ensure_path_closed(
cpath._ensure_path_closed(
self._original_path.clip_to_bbox(self.axes.viewLim)))
# Some places in matplotlib's transform stack cache the actual
# path so we trigger an update by invalidating the transform.
Expand All @@ -255,13 +240,13 @@ def __init__(self, axes, **kwargs):

def set_boundary(self, path, transform):
# Make sure path is closed (required by "Path.clip_to_bbox")
self._original_path = cpatch._ensure_path_closed(path)
self._original_path = cpath._ensure_path_closed(path)
self.set_transform(transform)
self.stale = True

def _adjust_location(self):
if self.stale:
self._path = cpatch._ensure_path_closed(
self._path = cpath._ensure_path_closed(
self._original_path.clip_to_bbox(self.axes.viewLim)
)

Expand Down Expand Up @@ -1535,7 +1520,7 @@ def _boundary(self):
The :data:`.patch` and :data:`.spines['geo']` are updated to match.
"""
path, = cpatch.geos_to_path(self.projection.boundary)
path = cpath.shapely_to_path(self.projection.boundary)

# Get the outline path in terms of self.transData
proj_to_data = self.projection._as_mpl_transform(self) - self.transData
Expand Down
60 changes: 25 additions & 35 deletions lib/cartopy/mpl/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,30 @@
"""

import warnings

from matplotlib.path import Path
import numpy as np
import shapely.geometry as sgeom

import cartopy.mpl.path as cpath

def _ensure_path_closed(path):
"""
Method to ensure that a path contains only closed sub-paths.
Parameters
----------
path
A :class:`matplotlib.path.Path` instance.
Returns
-------
path
A :class:`matplotlib.path.Path` instance with only closed polygons.
"""
# Split path into potential sub-paths and close all polygons
# (explicitly disable path simplification applied in to_polygons)
should_simplify = path.should_simplify
try:
path.should_simplify = False
polygons = path.to_polygons()
finally:
path.should_simplify = should_simplify

codes, vertices = [], []
for poly in polygons:
vertices.extend([poly[0], *poly])
codes.extend([Path.MOVETO, *[Path.LINETO]*(len(poly) - 1), Path.CLOSEPOLY])

return Path(vertices, codes)

def geos_to_path(shape):
"""
Create a list of :class:`matplotlib.path.Path` objects that describe
a shape.
.. deprecated:: 0.25
Use `cartopy.mpl.path.shapely_to_path` instead.
Parameters
----------
shape
A list, tuple or single instance of any of the following
types: :class:`shapely.geometry.point.Point`,
:class:`shapely.geometry.linestring.LineString`,
:class:`shapely.geometry.linestring.LinearRing`,
:class:`shapely.geometry.polygon.LinearRing`,
:class:`shapely.geometry.polygon.Polygon`,
:class:`shapely.geometry.multipoint.MultiPoint`,
:class:`shapely.geometry.multipolygon.MultiPolygon`,
Expand All @@ -73,6 +49,9 @@ def geos_to_path(shape):
A list of :class:`matplotlib.path.Path` objects.
"""
warnings.warn("geos_to_path is deprecated and will be removed in a future release."
" Use cartopy.mpl.path.shapely_to_path instead.",
DeprecationWarning, stacklevel=2)
if isinstance(shape, (list, tuple)):
paths = []
for shp in shape:
Expand Down Expand Up @@ -115,6 +94,9 @@ def path_segments(path, **kwargs):
Create an array of vertices and a corresponding array of codes from a
:class:`matplotlib.path.Path`.
.. deprecated:: 0.25
Use the identical `cartopy.mpl.path.path_segments` instead.
Parameters
----------
path
Expand All @@ -123,7 +105,7 @@ def path_segments(path, **kwargs):
Other Parameters
----------------
kwargs
See :func:`matplotlib.path.iter_segments` for details of the keyword
See `matplotlib.path.Path.iter_segments` for details of the keyword
arguments.
Returns
Expand All @@ -135,15 +117,20 @@ def path_segments(path, **kwargs):
codes and their meanings.
"""
pth = path.cleaned(**kwargs)
return pth.vertices[:-1, :], pth.codes[:-1]
warnings.warn(
"path_segements has moved. Please import from cartopy.mpl.path instead.",
DeprecationWarning, stacklevel=2)
return cpath.path_segments(path, **kwargs)


def path_to_geos(path, force_ccw=False):
"""
Create a list of Shapely geometric objects from a
:class:`matplotlib.path.Path`.
.. deprecated:: 0.25
Use `cartopy.mpl.path.path_to_shapely` instead.
Parameters
----------
path
Expand All @@ -163,8 +150,11 @@ def path_to_geos(path, force_ccw=False):
:class:`shapely.geometry.multilinestring.MultiLineString`.
"""
warnings.warn("path_to_geos is deprecated and will be removed in a future release."
" Use cartopy.mpl.path.path_to_shapely instead.",
DeprecationWarning, stacklevel=2)
# Convert path into numpy array of vertices (and associated codes)
path_verts, path_codes = path_segments(path, curves=False)
path_verts, path_codes = cpath.path_segments(path, curves=False)

# Split into subarrays such that each subarray consists of connected
# line segments based on the start of each one being marked by a
Expand Down
Loading

0 comments on commit b53ab7f

Please sign in to comment.