Skip to content

Commit

Permalink
fix treatment of spine paths with explicit path-codes (#2362)
Browse files Browse the repository at this point in the history
* fix usage of multi-paths with GeoSpines
In case the GeoSpine path represents a multi-path, don't attempt to close
it since this would convert the multi-path into a single closed path
resulting in unwanted connector-lines between the individual polygons.

* Use `path.to_polygons()` to ensure paths are properly closed.
`path.should_simplify` is temporarily set to `False` to avoid
unwanted path-simplification applied in `to_polygons`.
  • Loading branch information
raphaelquast authored Jun 23, 2024
1 parent f11eb85 commit 738bba5
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 6 deletions.
15 changes: 9 additions & 6 deletions lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
# CARTOPY_USER_BACKGROUNDS environment variable.
_USER_BG_IMGS = {}


# XXX call this InterCRSTransform
class InterProjectionTransform(mtransforms.Transform):
"""
Expand Down Expand Up @@ -229,13 +228,15 @@ def set_transform(self, transform):
super().set_transform(self._trans_wrap)

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

def _adjust_location(self):
if self.stale:
self.set_path(self._original_path.clip_to_bbox(self.axes.viewLim))
self.set_path(
cpatch._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.
self._trans_wrap.invalidate()
Expand All @@ -253,14 +254,16 @@ def __init__(self, axes, **kwargs):
super().__init__(axes, 'geo', self._original_path, **kwargs)

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

def _adjust_location(self):
if self.stale:
self._path = self._original_path.clip_to_bbox(self.axes.viewLim)
self._path = mpath.Path(self._path.vertices, closed=True)
self._path = cpatch._ensure_path_closed(
self._original_path.clip_to_bbox(self.axes.viewLim)
)

def get_window_extent(self, renderer=None):
# make sure the location is updated so that transforms etc are
Expand Down
31 changes: 31 additions & 0 deletions lib/cartopy/mpl/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@
import shapely.geometry as sgeom


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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions lib/cartopy/tests/mpl/test_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright Crown and Cartopy Contributors
#
# This file is part of Cartopy and is released under the BSD 3-clause license.
# See LICENSE in the root of the repository for full licensing details.

from matplotlib.path import Path
import matplotlib.pyplot as plt
import numpy as np
import pytest

import cartopy.crs as ccrs


circle_verts = np.array([
(21.33625905034713, 41.90051020408163),
(20.260167503134653, 36.31721708143521),
(17.186058185078757, 31.533809612693403),
(12.554340705353228, 28.235578526280886),
(7.02857405747471, 26.895042042418392),
(1.4004024147599825, 27.704250959518802),
(-3.5238590865749853, 30.547274662874656),
(-7.038740387110195, 35.0168098237746),
(-8.701144846177232, 41.42387784534415),
(-7.802741418346137, 47.03850217758886),
(-4.224119444187849, 52.606946662776494),
(0.5099122186645388, 55.75656240544797),
(6.075429329647158, 56.92110282927732),
(11.675092899771812, 55.933731058974054),
(16.50667197715324, 52.93590205611499),
(19.87797456957319, 48.357097196578444),
(21.33625905034713, 41.90051020408163)
])

circle_codes = [
Path.MOVETO,
*[Path.LINETO]*(len(circle_verts)),
Path.CLOSEPOLY
]

rectangle_verts = np.array([
(55.676020408163225, 36.16071428571428),
(130.29336734693877, 36.16071428571428),
(130.29336734693877, -4.017857142857167),
(55.676020408163225, -4.017857142857167),
(55.676020408163225, 36.16071428571428)
])

rectangle_codes = [
Path.MOVETO,
*[Path.LINETO]*(len(rectangle_verts)),
Path.CLOSEPOLY
]


@pytest.mark.natural_earth
@pytest.mark.mpl_image_compare(filename='multi_path_boundary.png')
def test_multi_path_boundary():
offsets = np.array([[30, 30], [70, 30], [110, 20]])

closed = [True, False, False]

vertices, codes = [], []
# add closed and open circles
for offset, close in zip(offsets, closed):
c = circle_verts + offset
if close:
vertices.extend([c[0], *c, c[-1]])
codes.extend([Path.MOVETO, *[Path.LINETO]*len(c), Path.CLOSEPOLY])
else:
vertices.extend([c[0], *c])
codes.extend([Path.MOVETO, *[Path.LINETO]*len(c)])

# add rectangle
vertices.extend(
[rectangle_verts[0], *rectangle_verts, rectangle_verts[-1]]
)
codes.extend(
[Path.MOVETO, *[Path.LINETO]*len(rectangle_verts), Path.CLOSEPOLY]
)

bnds = [*map(min, zip(*vertices)), *map(max, zip(*vertices))]

f, ax = plt.subplots(subplot_kw=dict(projection=ccrs.PlateCarree()))
ax.set_extent((bnds[0], bnds[2], bnds[1], bnds[3]))
ax.coastlines()
ax.set_boundary(Path(vertices, codes))

return f

0 comments on commit 738bba5

Please sign in to comment.