import matplotlib.path
import matplotlib.ticker as mticker
import cartopy
import cartopy.crs as ccrs
import numpy
import shapely.geometry
from geometric_features.plot import subdivide_geom
[docs]def add_inset(fig, fc, latlonbuffer=45., polarbuffer=5., width=1.0,
              height=1.0, lowerleft=None, xbuffer=None, ybuffer=None,
              maxlength=1.):
    """
    Plots an inset map showing the location of a transect or polygon.  Shapes
    are plotted on a polar grid if they are entirely poleward of +/-50 deg.
    latitude and with a lat/lon grid if not.
    Parameters
    ----------
    fig : ``matplotlib.figure.Figure``
        A matplotlib figure to add the inset to
    fc : ``geometric_features.FeatureCollection``
        A collection of regions, transects and/or points to plot in the inset
    latlonbuffer : float, optional
        The number of degrees lat/lon to use as a buffer around the shape(s)
        to plot if a lat/lon plot is used.
    polarbuffer : float, optional
        The number of degrees latitude to use as a buffer equatorward of the
        shape(s) in polar plots
    width, height : float, optional
        width and height in inches of the inset
    lowerleft : pair of floats, optional
        the location of the lower left corner of the axis in inches, default
        puts the inset in the upper right corner of ``fig``.
    xbuffer, ybuffer : float, optional
        right and top buffers from the top-right corner (in inches) if
        lowerleft is ``None``.
    maxlength : float or ``None``, optional
        Any segments longer than maxlength will be subdivided in the plot to
        ensure curvature.  If ``None``, no subdivision is performed.
    Returns
    -------
    inset : ``matplotlib.axes.Axes``
        The new inset axis
    """
    # Authors
    # -------
    # Xylar Asay-Davis
    minLon, minLat, maxLon, maxLat = _get_bounds(fc)
    figsize = fig.get_size_inches()
    width /= figsize[0]
    height /= figsize[1]
    if lowerleft is None:
        if xbuffer is None:
            xbuffer = 0.1*width
        else:
            xbuffer /= figsize[0]
        if ybuffer is None:
            ybuffer = xbuffer*figsize[0]/figsize[1]
        else:
            ybuffer /= figsize[1]
        lowerleft = [1.0 - width - xbuffer, 1.0 - height - ybuffer]
    else:
        lowerleft = [lowerleft[0]/figsize[0], lowerleft[1]/figsize[1]]
    bounds = [lowerleft[0], lowerleft[1], width, height]
    if maxLat <= -50:
        # an Antarctic-focused map makes the most sense
        inset = fig.add_axes(bounds,
                             projection=ccrs.SouthPolarStereo())
        extent = [-180., 180., -90., max(-65., maxLat+polarbuffer)]
        _set_circular_boundary(inset)
        xlocator = mticker.FixedLocator(numpy.linspace(-180., 180., 9))
        ylocator = mticker.FixedLocator(numpy.linspace(-90., -50., 9))
    elif minLat >= 50:
        # an Arctic-focused map makes the most sense
        inset = fig.add_axes(bounds,
                             projection=ccrs.NorthPolarStereo())
        extent = [-180, 180, min(65., minLat-polarbuffer), 90]
        _set_circular_boundary(inset)
        xlocator = mticker.FixedLocator(numpy.linspace(-180., 180., 9))
        ylocator = mticker.FixedLocator(numpy.linspace(50., 90., 9))
    else:
        inset = fig.add_axes(bounds,
                             projection=ccrs.PlateCarree())
        extent = [max(-180., minLon-latlonbuffer),
                  min(180., maxLon+latlonbuffer),
                  max(-90., minLat-latlonbuffer),
                  min(90., maxLat+latlonbuffer)]
        xlocator = None
        ylocator = None
    # kind of like "top" justified -- graphics are toward the "north" end of
    # the subplot
    inset.set_anchor('N')
    inset.set_extent(extent,  ccrs.PlateCarree())
    inset.add_feature(cartopy.feature.LAND, zorder=1)
    inset.add_feature(cartopy.feature.OCEAN, zorder=0)
    gl = inset.gridlines(crs=ccrs.PlateCarree(), draw_labels=False,
                         linewidth=0.5, color='gray', alpha=0.5,
                         linestyle='--')
    if xlocator is not None:
        gl.xlocator = xlocator
    if ylocator is not None:
        gl.ylocator = ylocator
    for feature in fc.features:
        geomtype = feature['geometry']['type']
        shape = shapely.geometry.shape(feature['geometry'])
        if maxlength is not None:
            shape = subdivide_geom(shape, shape.geom_type, maxlength)
        if geomtype in ['Polygon', 'MultiPolygon']:
            inset.add_geometries((shape,), crs=ccrs.PlateCarree(),
                                 edgecolor='blue', facecolor='blue', alpha=0.4,
                                 linewidth=1.)
        elif geomtype in ['Point', 'MultiPoint']:
            point_x, point_y = shape.xy
            inset.scatter(point_x, point_y, s=9, color='k',
                          transform=ccrs.PlateCarree(), edgecolors='face')
        else:
            inset.add_geometries((shape,), crs=ccrs.PlateCarree(),
                                 edgecolor='k', facecolor='none', alpha=1.,
                                 linewidth=1.)
            # put a red point at the beginning and a blue point at the end
            # of the transect to help show the orientation
            begin = shape.coords[0]
            end = shape.coords[-1]
            inset.plot(begin[0], begin[1], color='r', marker='o',
                       markersize=3., transform=ccrs.PlateCarree())
            inset.plot(end[0], end[1], color='g', marker='o',
                       markersize=3., transform=ccrs.PlateCarree())
    return inset 
def _set_circular_boundary(ax):
    """Set the boundary of the given axis to be circular (for a polar plot)"""
    # Compute a circle in axes coordinates, which we can use as a boundary
    # for the map. We can pan/zoom as much as we like - the boundary will be
    # permanently circular.
    theta = numpy.linspace(0, 2*numpy.pi, 100)
    center = numpy.array([0.5, 0.5])
    radius = 0.5
    verts = numpy.vstack([numpy.sin(theta), numpy.cos(theta)]).T
    circle = matplotlib.path.Path(verts * radius + center)
    ax.set_boundary(circle, transform=ax.transAxes)
def _get_bounds(fc):
    """Compute the lon/lat bounding box for all transects and regions"""
    bounds = shapely.geometry.GeometryCollection()
    for feature in fc.features:
        shape = shapely.geometry.shape(feature['geometry'])
        shape_bounds = shapely.geometry.box(*shape.bounds)
        bounds = shapely.geometry.box(*bounds.union(shape_bounds).bounds)
    return bounds.bounds