Source code for contours.core

# -* coding: utf-8 -*-
"""Common enums, functions, and classes for the `contours` package."""

# Python 2 support
# pylint: disable=redefined-builtin,unused-wildcard-import,wildcard-import
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from builtins import *

import numbers
import itertools
from enum import IntEnum
import numpy as np

# Attempt to import shapely and enable speedups.
try:
    import shapely.speedups
    if shapely.speedups.available:
        shapely.speedups.enable()
finally:
    try:
        from shapely.geometry import LineString, LinearRing, Polygon
    except ImportError:
        pass


[docs]class MPLPATHCODE(IntEnum): """Matplotlib path codes. These are included in this library for the purpose of writting your own formatter. The description of each one is copied verbatim from the documentation for :class:`matplotlib.path.Path`. """ STOP = 0 """1 vertex (ignored) A marker fo the end of the entire path (currently not required and ignored). """ MOVETO = 1 """1 vertex Pick up the pen and move to the given vertex. """ LINETO = 2 """1 vertex Draw a line from the current position to the given vertex. """ CURVE3 = 3 """1 control point, 1 endpoint Draw a quadratic Bezier curve from he current position, with the given control point, to the given end point. """ CURVE4 = 4 """2 control points, 1 endpoint Draw a cubic Bezier curve from the current position, with the given control points, to the given and point. """ CLOSEPOLY = 79 """1 vertex (ignored) Draw a line segment to the start point of the current polyline. """
def null_formatter(level, vertices, codes=None): """Null formatter that passes through the raw vertices and codes.""" return level, vertices, codes
[docs]def numpy_formatter(_, vertices, codes=None): """`NumPy`_ style contour formatter. Contours are returned as a list of Nx2 arrays containing the x and y vertices of the contour line. For filled contours the direction of vertices matters: * CCW (ACW): The vertices give the exterior of a contour polygon. * CW: The vertices give a hole of a contour polygon. This hole will always be inside the exterior of the last contour exterior. .. note:: This is the fastest format. .. _NumPy: http://www.numpy.org """ if codes is None: return vertices numpy_vertices = [] for vertices_, codes_ in zip(vertices, codes): starts = np.nonzero(codes_ == MPLPATHCODE.MOVETO)[0] stops = np.nonzero(codes_ == MPLPATHCODE.CLOSEPOLY)[0] for start, stop in zip(starts, stops): numpy_vertices.append(vertices_[start:stop+1, :]) return numpy_vertices
[docs]def matlab_formatter(level, vertices, codes=None): """`MATLAB`_ style contour formatter. Contours are returned as a single Nx2, `MATLAB`_ style, contour array. There are two types of rows in this format: * Header: The first element of a header row is the level of the contour (the lower level for filled contours) and the second element is the number of vertices (to follow) belonging to this contour line. * Vertex: x,y coordinate pairs of the vertex. A header row is always followed by the coresponding number of vertices. Another header row may follow if there are more contour lines. For filled contours the direction of vertices matters: * CCW (ACW): The vertices give the exterior of a contour polygon. * CW: The vertices give a hole of a contour polygon. This hole will always be inside the exterior of the last contour exterior. For further explanation of this format see the `Mathworks documentation <https://www.mathworks.com/help/matlab/ref/contour-properties.html#prop_ContourMatrix>`_ noting that the MATLAB format used in the `contours` package is the transpose of that used by `MATLAB`_ (since `MATLAB`_ is column-major and `NumPy`_ is row-major by default). .. _NumPy: http://www.numpy.org .. _MATLAB: https://www.mathworks.com/products/matlab.html """ vertices = numpy_formatter(level, vertices, codes) if codes is not None: level = level[0] headers = np.vstack(( [v.shape[0] for v in vertices], [level]*len(vertices))).T vertices = np.vstack( list(it.__next__() for it in itertools.cycle((iter(headers), iter(vertices))))) return vertices
[docs]def shapely_formatter(_, vertices, codes=None): """`Shapely`_ style contour formatter. Contours are returned as a list of :class:`shapely.geometry.LineString`, :class:`shapely.geometry.LinearRing`, and :class:`shapely.geometry.Point` geometry elements. Filled contours return a list of :class:`shapely.geometry.Polygon` elements instead. .. note:: If possible, `Shapely speedups`_ will be enabled. .. _Shapely: http://toblerity.org/shapely/manual.html .. _Shapely speedups: http://toblerity.org/shapely/manual.html#performance See Also -------- `descartes <https://bitbucket.org/sgillies/descartes/>`_ : Use `Shapely`_ or GeoJSON-like geometric objects as matplotlib paths and patches. """ elements = [] if codes is None: for vertices_ in vertices: if np.all(vertices_[0, :] == vertices_[-1, :]): # Contour is single point. if len(vertices) < 3: elements.append(Point(vertices_[0, :])) # Contour is closed. else: elements.append(LinearRing(vertices_)) # Contour is open. else: elements.append(LineString(vertices_)) else: for vertices_, codes_ in zip(vertices, codes): starts = np.nonzero(codes_ == MPLPATHCODE.MOVETO)[0] stops = np.nonzero(codes_ == MPLPATHCODE.CLOSEPOLY)[0] try: rings = [LinearRing(vertices_[start:stop+1, :]) for start, stop in zip(starts, stops)] elements.append(Polygon(rings[0], rings[1:])) except ValueError as err: # Verify error is from degenerate (single point) polygon. if np.any(stop - start - 1 == 0): # Polygon is single point, remove the polygon. if stops[0] < starts[0]+2: pass # Polygon has single point hole, remove the hole. else: rings = [ LinearRing(vertices_[start:stop+1, :]) for start, stop in zip(starts, stops) if stop >= start+2] elements.append(Polygon(rings[0], rings[1:])) else: raise(err) return elements
[docs]class ContourMixin(object): """Mixin to provide the public contour methods. Parameters ---------- formatter : callable A conversion function to convert from the internal Matplotlib contour format to an external format. See :ref:`formatters` for more information. Attributes ---------- formatter : :func:`callable` A conversion function to convert from the internal Matplotlib contour format to an external format. See :ref:`formatters` for more information. """ __slots__ = ('formatter',) def __init__(self, formatter=numpy_formatter, *args, **kwargs): """Initialize a :class:`ContourMixin`, see class docstring.""" # pylint: disable=unused-argument self.formatter = formatter
[docs] def contour(self, level): """Get contour lines at the given level. Parameters ---------- level : numbers.Number The data level to calculate the contour lines for. Returns ------- : The result of the :attr:`formatter` called on the contour at the given `level`. """ if not isinstance(level, numbers.Number): raise TypeError( ("'_level' must be of type 'numbers.Number' but is " "'{:s}'").format(type(level))) vertices = self._contour_generator.create_contour(level) return self.formatter(level, vertices)
[docs] def filled_contour(self, min=None, max=None): """Get contour polygons between the given levels. Parameters ---------- min : numbers.Number or None The minimum data level of the contour polygon. If :obj:`None`, ``numpy.finfo(numpy.float64).min`` will be used. max : numbers.Number or None The maximum data level of the contour polygon. If :obj:`None`, ``numpy.finfo(numpy.float64).max`` will be used. Returns ------- : The result of the :attr:`formatter` called on the filled contour between `min` and `max`. """ # pylint: disable=redefined-builtin,redefined-outer-name # Get the contour vertices. if min is None: min = np.finfo(np.float64).min if max is None: max = np.finfo(np.float64).max vertices, codes = ( self._contour_generator.create_filled_contour(min, max)) return self.formatter((min, max), vertices, codes)