Source code for cartopy.mpl.feature_artist

# 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.

"""
This module defines the :class:`FeatureArtist` class, for drawing
:class:`Feature` instances through an extension of the Matplotlib Artist interfaces.

"""

import warnings
import weakref

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

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


class _GeomKey:
    """
    Provide id() based equality and hashing for geometries.

    Instances of this class must be treated as immutable for the caching
    to operate correctly.

    A workaround for Shapely polygons no longer being hashable as of 1.5.13.

    """

    def __init__(self, geom):
        self._id = id(geom)

    def __eq__(self, other):
        return self._id == other._id

    def __hash__(self):
        return hash(self._id)


def _freeze(obj):
    """
    Recursively freeze the given object so that it might be suitable for
    use as a hashable.

    """
    if isinstance(obj, dict):
        obj = frozenset(((k, _freeze(v)) for k, v in obj.items()))
    elif isinstance(obj, list):
        obj = tuple(_freeze(item) for item in obj)
    elif isinstance(obj, np.ndarray):
        obj = tuple(obj)
    return obj


[docs] class FeatureArtist(matplotlib.collections.Collection): """ A subclass of :class:`~matplotlib.collections.Collection` capable of drawing a :class:`cartopy.feature.Feature`. """ _geom_key_to_geometry_cache = weakref.WeakValueDictionary() """ A mapping from _GeomKey to geometry to assist with the caching of transformed Matplotlib paths. """ _geom_key_to_path_cache = weakref.WeakKeyDictionary() """ A nested mapping from geometry (converted to a _GeomKey) and target projection to the resulting transformed Matplotlib paths:: {geom: {target_projection: list_of_paths}} This provides a significant boost when producing multiple maps of the same projection. """ def __init__(self, feature, **kwargs): """ Parameters ---------- feature An instance of :class:`cartopy.feature.Feature` to draw. styler A callable that given a geometry, returns matplotlib styling parameters. Other Parameters ---------------- **kwargs Keyword arguments to be used when drawing the feature. These will override those shared with the feature. """ super().__init__() self._styler = kwargs.pop('styler', None) self._kwargs = dict(kwargs) if 'color' in self._kwargs: # We want the user to be able to override both face and edge # colours if the original feature already supplied it. color = self._kwargs.pop('color') self._kwargs['facecolor'] = self._kwargs['edgecolor'] = color # Paths are worked out at draw, but add_collection fails if paths is # left to the default of None. self.set_paths([]) # Set default zorder so that features are drawn under # lines e.g. contours but over images and filled patches. # Note that the zorder of Patch, PatchCollection and PathCollection # are all 1 by default. Assuming default zorder, drawing takes place in # the following order: collections, patches, FeatureArtist, lines, # text. self.set_zorder(1.5) # Update drawing styles from the feature and **kwargs. self.set(**feature.kwargs) self.set(**self._kwargs) self._feature = feature
[docs] def set_facecolor(self, c): """ Set the facecolor(s) of the `.FeatureArtist`. If set to 'never' then subsequent calls will have no effect. Otherwise works the same as `matplotlib.collections.Collection.set_facecolor`. """ if isinstance(c, str) and c == 'never': self._never_fc = True super().set_facecolor('none') elif (getattr(self, '_never_fc', False) and (not isinstance(c, str) or c != 'none')): warnings.warn('facecolor will have no effect as it has been ' 'defined as "never".') else: super().set_facecolor(c)
if not _MPL_38: # set_paths does not yet exist on Collection. def set_paths(self, paths): self._paths = paths
[docs] @matplotlib.artist.allow_rasterization def draw(self, renderer): """ Draw the geometries of the feature that intersect with the extent of the :class:`cartopy.mpl.geoaxes.GeoAxes` instance to which this object has been added. """ if not self.get_visible(): return ax = self.axes feature_crs = self._feature.crs # Get geometries that we need to draw. extent = None try: extent = ax.get_extent(feature_crs) except ValueError: warnings.warn('Unable to determine extent. Defaulting to global.') if isinstance(self._feature, cfeature.ShapelyFeature): # User passed a specific list of geometries. If they also passed # `array` or a list of facecolors then we should keep the colours # consistent after pan/zoom. Do this by creating a Path for every # geometry regardless of whether they are currently in view. geoms = self._feature.geometries() else: # For efficiency on local maps with high resolution features (e.g # from Natural Earth), only create paths for geometries that are # in view. geoms = self._feature.intersecting_geometries(extent) stylised_paths = {} # Make an empty placeholder style dictionary for when styler is not # used. Freeze it so that we can use it as a dict key. We will need # to unfreeze all style dicts with dict(frozen) before passing to mpl. no_style = _freeze({}) # Project (if necessary) and convert geometries to matplotlib paths. key = ax.projection for geom in geoms: # As Shapely geometries cannot be relied upon to be # hashable, we have to use a WeakValueDictionary to manage # their weak references. The key can then be a simple, # "disposable", hashable geom-key object that just uses the # id() of a geometry to determine equality and hash value. # The only persistent, strong reference to the geom-key is # in the WeakValueDictionary, so when the geometry is # garbage collected so is the geom-key. # The geom-key is also used to access the WeakKeyDictionary # cache of transformed geometries. So when the geom-key is # garbage collected so are the transformed geometries. geom_key = _GeomKey(geom) FeatureArtist._geom_key_to_geometry_cache.setdefault( geom_key, geom) mapping = FeatureArtist._geom_key_to_path_cache.setdefault( geom_key, {}) geom_path = mapping.get(key) if geom_path is None: if ax.projection != feature_crs: projected_geom = ax.projection.project_geometry( geom, feature_crs) else: projected_geom = geom geom_path = cpath.shapely_to_path(projected_geom) mapping[key] = geom_path if self._styler is None: stylised_paths.setdefault(no_style, []).append(geom_path) else: style = _freeze(self._styler(geom)) stylised_paths.setdefault(style, []).append(geom_path) self.set_clip_path(ax.patch) # Draw each style individually. Note that there will only be multiple # styles if styler was used. for style, paths in stylised_paths.items(): style = dict(style) # Temporarily replace properties. orig_style = {k: getattr(self, f"get_{k}")() for k in style} self.set(paths=paths, **style) super().draw(renderer) self.set(paths=[], **orig_style)