From 27154d303a7821136b952a5cc1dc07b72fe364f5 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 3 Nov 2014 17:01:44 +0000 Subject: [PATCH] New C++ contour code with corner_mask --- CHANGELOG | 16 +- doc/api/api_changes/2015-02-19-IMT.rst | 25 + doc/users/whats_new/plotting.rst | 16 + .../pylab_examples/contour_corner_mask.py | 35 + lib/matplotlib/contour.py | 95 +- lib/matplotlib/rcsetup.py | 9 + .../contour_corner_mask_False.png | Bin 0 -> 43451 bytes .../test_contour/contour_corner_mask_True.png | Bin 0 -> 55241 bytes .../test_patheffects/collection.svg | 11044 ++++++++++++++-- lib/matplotlib/tests/test_coding_standards.py | 1 + lib/matplotlib/tests/test_contour.py | 18 + lib/matplotlib/tri/_tri_wrapper.cpp | 7 +- lib/matplotlib/tri/tricontour.py | 7 - matplotlibrc.template | 3 +- setup.py | 1 + setupext.py | 17 +- src/_contour.cpp | 1784 +++ src/_contour.h | 530 + src/_contour_wrapper.cpp | 196 + 19 files changed, 12589 insertions(+), 1215 deletions(-) create mode 100644 doc/api/api_changes/2015-02-19-IMT.rst create mode 100644 examples/pylab_examples/contour_corner_mask.py create mode 100644 lib/matplotlib/tests/baseline_images/test_contour/contour_corner_mask_False.png create mode 100644 lib/matplotlib/tests/baseline_images/test_contour/contour_corner_mask_True.png create mode 100644 src/_contour.cpp create mode 100644 src/_contour.h create mode 100644 src/_contour_wrapper.cpp diff --git a/CHANGELOG b/CHANGELOG index f9400c7d0e88..38b1fe4e97aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,14 +1,18 @@ +2015-02-19 Rewrite of C++ code that calculates contours to add support for + corner masking. This is controlled by the 'corner_mask' keyword + in plotting commands 'contour' and 'contourf'. - IMT + 2015-01-23 Text bounding boxes are now computed with advance width rather than - ink area. This may result in slightly different placement of text. + ink area. This may result in slightly different placement of text. 2014-10-27 Allowed selection of the backend using the `MPLBACKEND` environment variable. Added documentation on backend selection methods. 2014-09-27 Overhauled `colors.LightSource`. Added `LightSource.hillshade` to - allow the independent generation of illumination maps. Added new - types of blending for creating more visually appealing shaded relief - plots (e.g. `blend_mode="overlay"`, etc, in addition to the legacy - "hsv" mode). + allow the independent generation of illumination maps. Added new + types of blending for creating more visually appealing shaded relief + plots (e.g. `blend_mode="overlay"`, etc, in addition to the legacy + "hsv" mode). 2014-06-10 Added Colorbar.remove() @@ -64,7 +68,7 @@ of a generator at line 263. 2014-04-02 Added `clipon=False` to patch creation of wedges and shadows - in `pie`. + in `pie`. 2014-02-25 In backend_qt4agg changed from using update -> repaint under windows. See comment in source near `self._priv_update` for diff --git a/doc/api/api_changes/2015-02-19-IMT.rst b/doc/api/api_changes/2015-02-19-IMT.rst new file mode 100644 index 000000000000..86a6033eb620 --- /dev/null +++ b/doc/api/api_changes/2015-02-19-IMT.rst @@ -0,0 +1,25 @@ +Changed behaviour of contour plots +`````````````````````````````````` + +The default behaviour of :func:`~matplotlib.pyplot.contour` and +:func:`~matplotlib.pyplot.contourf` when using a masked array is now determined +by the new keyword argument `corner_mask`, or if this is not specified then +the new rcParam `contour.corner_mask` instead. The new default behaviour is +equivalent to using `corner_mask=True`; the previous behaviour can be obtained +using `corner_mask=False` or by changing the rcParam. The example +http://matplotlib.org/examples/pylab_examples/contour_corner_mask.py +demonstrates the difference. Use of the old contouring algorithm, which is +obtained with `corner_mask='legacy'`, is now deprecated. + +Contour labels may now appear in different places than in earlier versions of +matplotlib. + +In addition, the keyword argument `nchunk` now applies to +:func:`~matplotlib.pyplot.contour` as well as +:func:`~matplotlib.pyplot.contourf`, and it subdivides the domain into +subdomains of exactly `nchunk` by `nchunk` quads, whereas previously it was +only roughly `nchunk` by `nchunk` quads. + +The C/C++ object that performs contour calculations used to be stored in the +public attribute QuadContourSet.Cntr, but is now stored in a private attribute +and should not be accessed by end users. diff --git a/doc/users/whats_new/plotting.rst b/doc/users/whats_new/plotting.rst index 18df08d905cc..1139b5e78e78 100644 --- a/doc/users/whats_new/plotting.rst +++ b/doc/users/whats_new/plotting.rst @@ -3,3 +3,19 @@ Added center and frame kwargs to pie These control where the center of the pie graph are and if the Axes frame is shown. + +Contour plot corner masking +``````````````````````````` + +Ian Thomas rewrote the C++ code that calculates contours to add support for +corner masking. This is controlled by a new keyword argument +``corner_mask`` in the functions :func:`~matplotlib.pyplot.contour` and +:func:`~matplotlib.pyplot.contourf`. The previous behaviour, which is now +obtained using ``corner_mask=False``, was for a single masked point to +completely mask out all four quads touching that point. The new behaviour, +obtained using ``corner_mask=True``, only masks the corners of those +quads touching the point; any triangular corners comprising three unmasked +points are contoured as usual. If the ``corner_mask`` keyword argument is not +specified, the default value is taken from rcParams. + +.. plot:: mpl_examples/pylab_examples/contour_corner_mask.py diff --git a/examples/pylab_examples/contour_corner_mask.py b/examples/pylab_examples/contour_corner_mask.py new file mode 100644 index 000000000000..646d6e575362 --- /dev/null +++ b/examples/pylab_examples/contour_corner_mask.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +""" +Illustrate the difference between corner_mask=False and corner_mask=True +for masked contour plots. +""" +import matplotlib.pyplot as plt +import numpy as np + +# Data to plot. +x, y = np.meshgrid(np.arange(7), np.arange(10)) +z = np.sin(0.5*x)*np.cos(0.52*y) + +# Mask various z values. +mask = np.zeros_like(z, dtype=np.bool) +mask[2, 3:5] = True +mask[3:5, 4] = True +mask[7, 2] = True +mask[5, 0] = True +mask[0, 6] = True +z = np.ma.array(z, mask=mask) + +corner_masks = [False, True] +for i, corner_mask in enumerate(corner_masks): + plt.subplot(1, 2, i+1) + cs = plt.contourf(x, y, z, corner_mask=corner_mask) + plt.contour(cs, colors='k') + plt.title('corner_mask = {}'.format(corner_mask)) + + # Plot grid. + plt.grid(c='k', ls='-', alpha=0.3) + + # Indicate masked points with red circles. + plt.plot(np.ma.array(x, mask=~mask), y, 'ro') + +plt.show() diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index acf0b086aff6..c4b3d16de2fd 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -13,6 +13,7 @@ import numpy as np from numpy import ma import matplotlib._cntr as _cntr +import matplotlib._contour as _contour import matplotlib.path as mpath import matplotlib.ticker as ticker import matplotlib.cm as cm @@ -989,9 +990,10 @@ def get_transform(self): def __getstate__(self): state = self.__dict__.copy() - # the C object Cntr cannot currently be pickled. This isn't a big issue - # as it is not actually used once the contour has been calculated - state['Cntr'] = None + # the C object _contour_generator cannot currently be pickled. This + # isn't a big issue as it is not actually used once the contour has + # been calculated. + state['_contour_generator'] = None return state def legend_elements(self, variable_name='x', str_format=str): @@ -1433,18 +1435,34 @@ def _process_args(self, *args, **kwargs): Process args and kwargs. """ if isinstance(args[0], QuadContourSet): - C = args[0].Cntr if self.levels is None: self.levels = args[0].levels self.zmin = args[0].zmin self.zmax = args[0].zmax + self._corner_mask = args[0]._corner_mask + if self._corner_mask == 'legacy': + contour_generator = args[0].Cntr + else: + contour_generator = args[0]._contour_generator else: x, y, z = self._contour_args(args, kwargs) _mask = ma.getmask(z) - if _mask is ma.nomask: + if _mask is ma.nomask or not _mask.any(): _mask = None - C = _cntr.Cntr(x, y, z.filled(), _mask) + + self._corner_mask = kwargs.get('corner_mask', None) + if self._corner_mask is None: + self._corner_mask = mpl.rcParams['contour.corner_mask'] + + if self._corner_mask == 'legacy': + cbook.warn_deprecated('1.5', + name="corner_mask='legacy'", + alternative='corner_mask=False or True') + contour_generator = _cntr.Cntr(x, y, z.filled(), _mask) + else: + contour_generator = _contour.QuadContourGenerator( + x, y, z.filled(), _mask, self._corner_mask, self.nchunk) t = self.get_transform() @@ -1465,7 +1483,10 @@ def _process_args(self, *args, **kwargs): self.ax.update_datalim([(x0, y0), (x1, y1)]) self.ax.autoscale_view(tight=True) - self.Cntr = C + if self._corner_mask == 'legacy': + self.Cntr = contour_generator + else: + self._contour_generator = contour_generator def _get_allsegs_and_allkinds(self): """ @@ -1476,20 +1497,28 @@ def _get_allsegs_and_allkinds(self): lowers, uppers = self._get_lowers_and_uppers() allkinds = [] for level, level_upper in zip(lowers, uppers): - nlist = self.Cntr.trace(level, level_upper, - nchunk=self.nchunk) - nseg = len(nlist) // 2 - segs = nlist[:nseg] - kinds = nlist[nseg:] - allsegs.append(segs) + if self._corner_mask == 'legacy': + nlist = self.Cntr.trace(level, level_upper, + nchunk=self.nchunk) + nseg = len(nlist) // 2 + vertices = nlist[:nseg] + kinds = nlist[nseg:] + else: + vertices, kinds = \ + self._contour_generator.create_filled_contour( + level, level_upper) + allsegs.append(vertices) allkinds.append(kinds) else: allkinds = None for level in self.levels: - nlist = self.Cntr.trace(level) - nseg = len(nlist) // 2 - segs = nlist[:nseg] - allsegs.append(segs) + if self._corner_mask == 'legacy': + nlist = self.Cntr.trace(level) + nseg = len(nlist) // 2 + vertices = nlist[:nseg] + else: + vertices = self._contour_generator.create_contour(level) + allsegs.append(vertices) return allsegs, allkinds def _contour_args(self, args, kwargs): @@ -1672,6 +1701,20 @@ def _initialize_x_y(self, z): Optional keyword arguments: + *corner_mask*: [ *True* | *False* | 'legacy' ] + Enable/disable corner masking, which only has an effect if *Z* is + a masked array. If *False*, any quad touching a masked point is + masked out. If *True*, only the triangular corners of quads + nearest those points are always masked out, other triangular + corners comprising three unmasked points are contoured as usual. + If 'legacy', the old contouring algorithm is used, which is + equivalent to *False* and is deprecated, only remaining whilst the + new algorithm is tested fully. + + If not specified, the default is taken from + rcParams['contour.corner_mask'], which is True unless it has + been modified. + *colors*: [ *None* | string | (mpl_colors) ] If *None*, the colormap specified by cmap will be used. @@ -1750,6 +1793,15 @@ def _initialize_x_y(self, z): filled contours, the default is *True*. For line contours, it is taken from rcParams['lines.antialiased']. + *nchunk*: [ 0 | integer ] + If 0, no subdivision of the domain. Specify a positive integer to + divide the domain into subdomains of *nchunk* by *nchunk* quads. + Chunking reduces the maximum length of polygons generated by the + contouring algorithm which reduces the rendering workload passed + on to the backend and also requires slightly less RAM. It can + however introduce rendering artifacts at chunk boundaries depending + on the backend, the *antialiased* flag and value of *alpha*. + contour-only keyword arguments: *linewidths*: [ *None* | number | tuple of numbers ] @@ -1774,13 +1826,6 @@ def _initialize_x_y(self, z): contourf-only keyword arguments: - *nchunk*: [ 0 | integer ] - If 0, no subdivision of the domain. Specify a positive integer to - divide the domain into subdomains of roughly *nchunk* by *nchunk* - points. This may never actually be advantageous, so this option may - be removed. Chunking introduces artifacts at the chunk boundaries - unless *antialiased* is *False*. - *hatches*: A list of cross hatch patterns to use on the filled areas. If None, no hatching will be added to the contour. @@ -1802,4 +1847,6 @@ def _initialize_x_y(self, z): .. plot:: mpl_examples/pylab_examples/contour_demo.py .. plot:: mpl_examples/pylab_examples/contourf_demo.py + + .. plot:: mpl_examples/pylab_examples/contour_corner_mask.py """ diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 60ab3e1a3fbb..51289832c6b1 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -389,6 +389,13 @@ def validate_negative_linestyle_legacy(s): return (0, dashes) # (offset, (solid, blank)) +def validate_corner_mask(s): + if s == 'legacy': + return s + else: + return validate_bool(s) + + def validate_tkpythoninspect(s): # Introduced 2010/07/05 warnings.warn("tk.pythoninspect is obsolete, and has no effect") @@ -589,8 +596,10 @@ def __call__(self, s): 'image.origin': ['upper', six.text_type], # lookup table 'image.resample': [False, validate_bool], + # contour props 'contour.negative_linestyle': ['dashed', validate_negative_linestyle_legacy], + 'contour.corner_mask': [True, validate_corner_mask], # axes props 'axes.axisbelow': [False, validate_bool], diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_corner_mask_False.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_corner_mask_False.png new file mode 100644 index 0000000000000000000000000000000000000000..93780be92fc11300268fd2150de1a828f79eb5ff GIT binary patch literal 43451 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 z
  • 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/collection.svg b/lib/matplotlib/tests/baseline_images/test_patheffects/collection.svg index fd0b3372977f..6a5caa1b7622 100644 --- a/lib/matplotlib/tests/baseline_images/test_patheffects/collection.svg +++ b/lib/matplotlib/tests/baseline_images/test_patheffects/collection.svg @@ -31,6 +31,28 @@ z + + + + + + - - - - + - - + - +L518.4 97.2787" style="fill:none;stroke:#000080;stroke-width:5;"/> - - + + + + + + + + + + + + - - - - - - - + - - + + - - - + - +L518.4 69.5254" style="fill:none;stroke:#0028ff;stroke-width:5;"/> - - - - +M72 288.401 +L74.9217 291.055 +L74.996 291.127 +L77.9919 294.029 +L78.5294 294.545 +L80.9879 297.117 +L81.8794 298.036 +L83.9839 300.447 +L84.9441 301.527 +L85.6214 302.393" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M83.8277 348.875 +L82.5279 350.4 +L80.9879 352.02 +L79.2341 353.891 +L77.9919 355.103 +L75.677 357.382 +L74.996 358.005 +L72 360.761" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M72 71.2387 +L74.996 73.9946 +L75.677 74.6182 +L77.9919 76.8973 +L79.2341 78.1091 +L80.9879 79.9798 +L82.5279 81.6 +L83.9839 83.3086 +L85.531 85.0909 +L86.0693 85.8001" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M83.3613 132.266 +L81.8794 133.964 +L80.9879 134.883 +L78.5294 137.455 +L77.9919 137.971 +L74.996 140.873 +L74.9217 140.945 +L72 143.599" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - +L74.996 291.127 +L77.9919 294.029 +L78.5294 294.545 +L80.9879 297.117 +L81.8794 298.036 +L83.9839 300.447 +L84.9441 301.527 +L85.6214 302.393" style="fill:none;stroke:#00d4ff;stroke-width:5;"/> + - +L75.677 74.6182 +L77.9919 76.8973 +L79.2341 78.1091 +L80.9879 79.9798 +L82.5279 81.6 +L83.9839 83.3086 +L85.531 85.0909 +L86.0693 85.8001" style="fill:none;stroke:#00d4ff;stroke-width:5;"/> + - - - - - - - + - + +M72 270.287 +L74.996 272.608 +L76.2821 273.6 +L77.9919 274.932 +L80.7899 277.091 +L80.9879 277.247 +L83.9839 279.574 +L85.3055 280.582 +L86.9799 281.899 +L89.809 284.073 +L89.9758 284.206 +L90.162 284.352" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - + +L125.928 312.043 +L128.923 314.476 +L130.482 315.491 +L131.919 316.859 +L134.901 318.982 +L134.915 319.002 +L137.911 321.665 +L139.82 322.473 +L140.584 325.964 +L137.911 327.35 +L135.98 329.455 +L134.915 330.006 +L131.919 332.364 +L131.368 332.945 +L128.923 334.645 +L126.858 336.436 +L125.928 337.043 +L122.932 339.378 +L122.322 339.927 +L119.936 341.667 +L117.814 343.418 +L116.94 344.029 +L113.944 346.355 +L113.293 346.909 +L110.948 348.655 +L108.783 350.4 +L107.952 351.001 +L104.956 353.324 +L104.267 353.891 +L101.96 355.63 +L99.7571 357.382 +L98.9638 357.969 +L95.9678 360.289 +L95.2441 360.873 +L92.9718 362.602 +L90.7334 364.364 +L89.9758 364.934 +L86.9799 367.254 +L86.2216 367.855 +L83.9839 369.57 +L81.7108 371.345 +L80.9879 371.898 +L77.9919 374.217 +L77.1995 374.836 +L74.996 376.538 +L72.6888 378.327 +L72 378.861" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - + - + - + +M72 53.1393 +L72.6888 53.6727 +L74.996 55.4622 +L77.1995 57.1636 +L77.9919 57.7828 +L80.9879 60.1025 +L81.7108 60.6545 +L83.9839 62.4298 +L86.2216 64.1455 +L86.9799 64.7465 +L89.9758 67.0663 +L90.1714 67.2135" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - + +L126.858 95.5636 +L128.923 97.3554 +L131.368 99.0545 +L131.919 99.6355 +L134.915 101.994 +L135.98 102.545 +L137.911 104.65 +L140.584 106.036 +L139.82 109.527 +L137.911 110.335 +L134.915 112.998 +L134.901 113.018 +L131.919 115.141 +L130.482 116.509 +L128.923 117.524 +L125.928 119.957 +L125.883 120 +L122.932 122.192 +L121.421 123.491 +L119.936 124.534 +L116.94 126.916 +L116.865 126.982 +L113.944 129.184 +L112.384 130.473 +L110.948 131.517 +L107.952 133.875 +L107.847 133.964 +L104.956 136.161 +L103.355 137.455 +L101.96 138.49 +L98.9638 140.834 +L98.828 140.945 +L95.9678 143.133 +L94.3297 144.436 +L92.9718 145.459 +L89.9758 147.794 +L89.809 147.927 +L86.9799 150.101 +L85.3055 151.418 +L83.9839 152.426 +L80.9879 154.753 +L80.7899 154.909 +L77.9919 157.068 +L76.2821 158.4 +L74.996 159.392 +L72 161.713" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> + + + + + + - - +M72 270.287 +L74.996 272.608 +L76.2821 273.6 +L77.9919 274.932 +L80.7899 277.091 +L80.9879 277.247 +L83.9839 279.574 +L85.3055 280.582 +L86.9799 281.899 +L89.809 284.073 +L89.9758 284.206 +L90.162 284.352" style="fill:none;stroke:#7dff7a;stroke-width:5;"/> +L125.928 312.043 +L128.923 314.476 +L130.482 315.491 +L131.919 316.859 +L134.901 318.982 +L134.915 319.002 +L137.911 321.665 +L139.82 322.473 +L140.584 325.964 +L137.911 327.35 +L135.98 329.455 +L134.915 330.006 +L131.919 332.364 +L131.368 332.945 +L128.923 334.645 +L126.858 336.436 +L125.928 337.043 +L122.932 339.378 +L122.322 339.927 +L119.936 341.667 +L117.814 343.418 +L116.94 344.029 +L113.944 346.355 +L113.293 346.909 +L110.948 348.655 +L108.783 350.4 +L107.952 351.001 +L104.956 353.324 +L104.267 353.891 +L101.96 355.63 +L99.7571 357.382 +L98.9638 357.969 +L95.9678 360.289 +L95.2441 360.873 +L92.9718 362.602 +L90.7334 364.364 +L89.9758 364.934 +L86.9799 367.254 +L86.2216 367.855 +L83.9839 369.57 +L81.7108 371.345 +L80.9879 371.898 +L77.9919 374.217 +L77.1995 374.836 +L74.996 376.538 +L72.6888 378.327 +L72 378.861" style="fill:none;stroke:#7dff7a;stroke-width:5;"/> +M72 53.1393 +L72.6888 53.6727 +L74.996 55.4622 +L77.1995 57.1636 +L77.9919 57.7828 +L80.9879 60.1025 +L81.7108 60.6545 +L83.9839 62.4298 +L86.2216 64.1455 +L86.9799 64.7465 +L89.9758 67.0663 +L90.1714 67.2135" style="fill:none;stroke:#7dff7a;stroke-width:5;"/> +L126.858 95.5636 +L128.923 97.3554 +L131.368 99.0545 +L131.919 99.6355 +L134.915 101.994 +L135.98 102.545 +L137.911 104.65 +L140.584 106.036 +L139.82 109.527 +L137.911 110.335 +L134.915 112.998 +L134.901 113.018 +L131.919 115.141 +L130.482 116.509 +L128.923 117.524 +L125.928 119.957 +L125.883 120 +L122.932 122.192 +L121.421 123.491 +L119.936 124.534 +L116.94 126.916 +L116.865 126.982 +L113.944 129.184 +L112.384 130.473 +L110.948 131.517 +L107.952 133.875 +L107.847 133.964 +L104.956 136.161 +L103.355 137.455 +L101.96 138.49 +L98.9638 140.834 +L98.828 140.945 +L95.9678 143.133 +L94.3297 144.436 +L92.9718 145.459 +L89.9758 147.794 +L89.809 147.927 +L86.9799 150.101 +L85.3055 151.418 +L83.9839 152.426 +L80.9879 154.753 +L80.7899 154.909 +L77.9919 157.068 +L76.2821 158.4 +L74.996 159.392 +L72 161.713" style="fill:none;stroke:#7dff7a;stroke-width:5;"/> + + +M202.501 388.8 +L200.827 387.499 +L197.959 385.309 +L197.831 385.213 +L194.835 383.019 +L193.15 381.818 +L191.839 380.892 +L188.843 378.844 +L188.056 378.327 +L185.847 376.872 +L182.851 374.987 +L182.6 374.836 +L182.004 374.476" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M139.835 360.836 +L139.184 360.873 +L137.911 360.94 +L134.915 361.272 +L131.919 361.776 +L128.923 362.45 +L125.928 363.291 +L122.932 364.295 +L122.756 364.364 +L119.936 365.41 +L116.94 366.669 +L114.402 367.855 +L113.944 368.062 +L110.948 369.546 +L107.952 371.153 +L107.618 371.345 +L104.956 372.844 +L101.96 374.636 +L101.642 374.836 +L98.9638 376.507 +L96.1764 378.327 +L95.9678 378.463 +L92.9718 380.497 +L91.0903 381.818 +L89.9758 382.608 +L86.9799 384.792 +L86.2864 385.309 +L83.9839 387.06 +L81.7387 388.8" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +L377.589 377.709 +L376.666 378.327 +L374.593 379.714 +L371.597 381.793 +L371.561 381.818 +L368.601 383.954 +L366.77 385.309 +L365.605 386.188 +L362.609 388.494 +L362.218 388.8" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M72 252.179 +L72.5207 252.655 +L74.996 254.792 +L76.5702 256.145 +L77.9919 257.315 +L80.8385 259.636 +L80.9879 259.754 +L83.9839 262.088 +L85.3436 263.127 +L86.9799 264.352 +L89.9758 266.543 +L90.0822 266.618 +L92.9718 268.649 +L95.1204 270.109 +L95.9678 270.684 +L98.9638 272.639 +L100.506 273.6 +L101.96 274.514 +L104.956 276.301 +L106.365 277.091 +L107.952 277.999 +L110.948 279.598 +L112.944 280.582 +L113.944 281.09 +L116.94 282.479 +L119.936 283.728 +L120.872 284.073 +L122.932 284.864 +L125.928 285.859 +L128.923 286.691 +L131.919 287.359 +L133.151 287.564 +L134.915 287.874 +L137.911 288.221 +L140.907 288.386 +L143.903 288.369 +L146.899 288.169 +L149.895 287.788 +L151.091 287.564 +L152.891 287.244 +L155.887 286.545 +L158.883 285.682 +L161.879 284.657 +L163.362 284.073 +L164.875 283.501 +L167.871 282.225 +L170.867 280.81 +L171.31 280.582 +L173.863 279.303 +L176.859 277.682 +L177.879 277.091 +L179.112 276.391" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - - - - - - - + - + - + - + - - - - - + +L514.143 225.524" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> + + + + - - - + + + + + + +L114.512 385.309 +L113.944 385.564 +L110.948 387.036 +L107.952 388.631 +L107.656 388.8" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M457.049 388.8 +L455.485 387.932 +L452.489 386.389 +L450.207 385.309 +L449.493 384.978 +L446.497 383.716 +L444.528 382.976" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +L395.565 385.042 +L394.993 385.309 +L392.569 386.461 +L389.573 388.01 +L388.154 388.8" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> +M107.656 43.2 +L107.952 43.3689 +L110.948 44.9639 +L113.944 46.4362 +L114.512 46.6909 +L116.94 47.7587 +L118.992 48.5702" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> + + + + + + + + + +L84.3698 242.182 +L87.8025 245.673 +L91.6635 249.164 +L95.9769 252.655 +L101.96 256.874 +L106.431 259.636 +L110.948 262.112 +L113.944 263.582 +L116.94 264.904 +L121.436 266.618 +L122.932 267.137 +L125.928 268.034 +L128.923 268.785 +L131.919 269.387 +L134.915 269.837 +L137.911 270.133 +L140.907 270.274 +L143.903 270.259 +L146.899 270.089 +L149.895 269.763 +L152.891 269.284 +L155.887 268.653 +L158.883 267.874 +L162.812 266.618 +L164.875 265.877 +L167.871 264.663 +L171.252 263.127 +L176.859 260.19 +L179.855 258.411 +L183.384 256.145 +L188.843 252.198 +L192.58 249.164 +L196.437 245.673 +L199.868 242.182 +L202.887 238.691 +L205.498 235.2 +L207.698 231.709 +L209.815 227.346 +L210.824 224.727 +L211.729 221.236 +L212.184 217.745 +L212.184 214.255 +L211.729 210.764 +L210.824 207.273 +L209.477 203.782 +L207.698 200.291 +L205.498 196.8 +L202.394 192.736 +L202.394 192.736" style="fill:none;stroke:#000000;stroke-linejoin:miter;stroke-width:12;"/> - + - - - - - + + + + + - - - + - - - + - - - - - +L457.049 43.2" style="fill:none;stroke:#ff4700;stroke-width:5;"/> +L84.3698 242.182 +L87.8025 245.673 +L91.6635 249.164 +L95.9769 252.655 +L101.96 256.874 +L106.431 259.636 +L110.948 262.112 +L113.944 263.582 +L116.94 264.904 +L121.436 266.618 +L122.932 267.137 +L125.928 268.034 +L128.923 268.785 +L131.919 269.387 +L134.915 269.837 +L137.911 270.133 +L140.907 270.274 +L143.903 270.259 +L146.899 270.089 +L149.895 269.763 +L152.891 269.284 +L155.887 268.653 +L158.883 267.874 +L162.812 266.618 +L164.875 265.877 +L167.871 264.663 +L171.252 263.127 +L176.859 260.19 +L179.855 258.411 +L183.384 256.145 +L188.843 252.198 +L192.58 249.164 +L196.437 245.673 +L199.868 242.182 +L202.887 238.691 +L205.498 235.2 +L207.698 231.709 +L209.815 227.346 +L210.824 224.727 +L211.729 221.236 +L212.184 217.745 +L212.184 214.255 +L211.729 210.764 +L210.824 207.273 +L209.477 203.782 +L207.698 200.291 +L205.498 196.8 +L202.394 192.736 +L202.394 192.736" style="fill:none;stroke:#ff4700;stroke-width:5;"/> - - - - - + - + - + - + - + - + +<<<<<<< HEAD +======= + + +>>>>>>> New C++ contour code with corner_mask +<<<<<<< HEAD +======= + + +>>>>>>> New C++ contour code with corner_mask +<<<<<<< HEAD +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> New C++ contour code with corner_mask +<<<<<<< HEAD +======= + + +>>>>>>> New C++ contour code with corner_mask ptr->create_contour(level))); + CALL_CPP("create_contour", (result = self->ptr->create_contour(level))); return result; } @@ -287,12 +287,13 @@ const char* PyTriContourGenerator_create_filled_contour__doc__ = static PyObject* PyTriContourGenerator_create_filled_contour(PyTriContourGenerator* self, PyObject* args, PyObject* kwds) { double lower_level, upper_level; - if (!PyArg_ParseTuple(args, "dd:create_contour", &lower_level, &upper_level)) { + if (!PyArg_ParseTuple(args, "dd:create_filled_contour", + &lower_level, &upper_level)) { return NULL; } PyObject* result; - CALL_CPP("get_edges", + CALL_CPP("create_filled_contour", (result = self->ptr->create_filled_contour(lower_level, upper_level))); return result; diff --git a/lib/matplotlib/tri/tricontour.py b/lib/matplotlib/tri/tricontour.py index 497d8ac400a3..960848b66976 100644 --- a/lib/matplotlib/tri/tricontour.py +++ b/lib/matplotlib/tri/tricontour.py @@ -258,13 +258,6 @@ def _contour_args(self, args, kwargs): *antialiased*: [ *True* | *False* ] enable antialiasing - *nchunk*: [ 0 | integer ] - If 0, no subdivision of the domain. Specify a positive integer to - divide the domain into subdomains of roughly *nchunk* by *nchunk* - points. This may never actually be advantageous, so this option may - be removed. Chunking introduces artifacts at the chunk boundaries - unless *antialiased* is *False*. - Note: tricontourf fills intervals that are closed at the top; that is, for boundaries *z1* and *z2*, the filled region is:: diff --git a/matplotlibrc.template b/matplotlibrc.template index 7195de0b940a..4405b359bc1c 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -360,7 +360,8 @@ backend : %(backend)s #image.resample : False ### CONTOUR PLOTS -#contour.negative_linestyle : dashed # dashed | solid +#contour.negative_linestyle : dashed # dashed | solid +#contour.corner_mask : True # True | False | legacy ### Agg rendering ### Warning: experimental, 2008/10/10 diff --git a/setup.py b/setup.py index dba6f7b7a9bf..409a90f60530 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ setupext.Image(), setupext.TTConv(), setupext.Path(), + setupext.ContourLegacy(), setupext.Contour(), setupext.Delaunay(), setupext.QhullWrap(), diff --git a/setupext.py b/setupext.py index 780a21f0ee3a..984f2f7db1e6 100755 --- a/setupext.py +++ b/setupext.py @@ -1060,8 +1060,8 @@ def get_extension(self): return ext -class Contour(SetupPackage): - name = "contour" +class ContourLegacy(SetupPackage): + name = "contour_legacy" def get_extension(self): sources = [ @@ -1072,6 +1072,19 @@ def get_extension(self): return ext +class Contour(SetupPackage): + name = "contour" + + def get_extension(self): + sources = [ + "src/_contour.cpp", + "src/_contour_wrapper.cpp", + ] + ext = make_extension('matplotlib._contour', sources) + Numpy().add_flags(ext) + return ext + + class Delaunay(SetupPackage): name = "delaunay" diff --git a/src/_contour.cpp b/src/_contour.cpp new file mode 100644 index 000000000000..0856a043afd7 --- /dev/null +++ b/src/_contour.cpp @@ -0,0 +1,1784 @@ +// This file contains liberal use of asserts to assist code development and +// debugging. Standard matplotlib builds disable asserts so they cause no +// performance reduction. To enable the asserts, you need to undefine the +// NDEBUG macro, which is achieved by adding the following +// undef_macros=['NDEBUG'] +// to the appropriate make_extension call in setupext.py, and then rebuilding. +#define NO_IMPORT_ARRAY + +#include "src/mplutils.h" +#include "src/_contour.h" +#include + + +// 'kind' codes. +#define MOVETO 1 +#define LINETO 2 +#define CLOSEPOLY 79 + +// Point indices from current quad index. +#define POINT_SW (quad) +#define POINT_SE (quad+1) +#define POINT_NW (quad+_nx) +#define POINT_NE (quad+_nx+1) + +// CacheItem masks, only accessed directly to set. To read, use accessors +// detailed below. 1 and 2 refer to level indices (lower and upper). +#define MASK_Z_LEVEL 0x0003 // Combines the following two. +#define MASK_Z_LEVEL_1 0x0001 // z > lower_level. +#define MASK_Z_LEVEL_2 0x0002 // z > upper_level. +#define MASK_VISITED_1 0x0004 // Algorithm has visited this quad. +#define MASK_VISITED_2 0x0008 +#define MASK_SADDLE_1 0x0010 // quad is a saddle quad. +#define MASK_SADDLE_2 0x0020 +#define MASK_SADDLE_LEFT_1 0x0040 // Contours turn left at saddle quad. +#define MASK_SADDLE_LEFT_2 0x0080 +#define MASK_SADDLE_START_SW_1 0x0100 // Next visit starts on S or W edge. +#define MASK_SADDLE_START_SW_2 0x0200 +#define MASK_BOUNDARY_S 0x0400 // S edge of quad is a boundary. +#define MASK_BOUNDARY_W 0x0800 // W edge of quad is a boundary. +// EXISTS_QUAD bit is always used, but the 4 EXISTS_CORNER are only used if +// _corner_mask is true. Only one of EXISTS_QUAD or EXISTS_??_CORNER is ever +// set per quad, hence not using unique bits for each; care is needed when +// testing for these flags as they overlap. +#define MASK_EXISTS_QUAD 0x1000 // All of quad exists (is not masked). +#define MASK_EXISTS_SW_CORNER 0x2000 // SW corner exists, NE corner is masked. +#define MASK_EXISTS_SE_CORNER 0x3000 +#define MASK_EXISTS_NW_CORNER 0x4000 +#define MASK_EXISTS_NE_CORNER 0x5000 +#define MASK_EXISTS 0x7000 // Combines all 5 EXISTS masks. + +// The following are only needed for filled contours. +#define MASK_VISITED_S 0x10000 // Algorithm has visited S boundary. +#define MASK_VISITED_W 0x20000 // Algorithm has visited W boundary. +#define MASK_VISITED_CORNER 0x40000 // Algorithm has visited corner edge. + + +// Accessors for various CacheItem masks. li is shorthand for level_index. +#define Z_LEVEL(quad) (_cache[quad] & MASK_Z_LEVEL) +#define Z_NE Z_LEVEL(POINT_NE) +#define Z_NW Z_LEVEL(POINT_NW) +#define Z_SE Z_LEVEL(POINT_SE) +#define Z_SW Z_LEVEL(POINT_SW) +#define VISITED(quad,li) (_cache[quad] & (li==1 ? MASK_VISITED_1 : MASK_VISITED_2)) +#define VISITED_S(quad) (_cache[quad] & MASK_VISITED_S) +#define VISITED_W(quad) (_cache[quad] & MASK_VISITED_W) +#define VISITED_CORNER(quad) (_cache[quad] & MASK_VISITED_CORNER) +#define SADDLE(quad,li) (_cache[quad] & (li==1 ? MASK_SADDLE_1 : MASK_SADDLE_2)) +#define SADDLE_LEFT(quad,li) (_cache[quad] & (li==1 ? MASK_SADDLE_LEFT_1 : MASK_SADDLE_LEFT_2)) +#define SADDLE_START_SW(quad,li) (_cache[quad] & (li==1 ? MASK_SADDLE_START_SW_1 : MASK_SADDLE_START_SW_2)) +#define BOUNDARY_S(quad) (_cache[quad] & MASK_BOUNDARY_S) +#define BOUNDARY_W(quad) (_cache[quad] & MASK_BOUNDARY_W) +#define BOUNDARY_N(quad) BOUNDARY_S(quad+_nx) +#define BOUNDARY_E(quad) BOUNDARY_W(quad+1) +#define EXISTS_QUAD(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_QUAD) +#define EXISTS_NONE(quad) ((_cache[quad] & MASK_EXISTS) == 0) +// The following are only used if _corner_mask is true. +#define EXISTS_SW_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_SW_CORNER) +#define EXISTS_SE_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_SE_CORNER) +#define EXISTS_NW_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_NW_CORNER) +#define EXISTS_NE_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_NE_CORNER) +#define EXISTS_ANY_CORNER(quad) (!EXISTS_NONE(quad) && !EXISTS_QUAD(quad)) +#define EXISTS_W_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SW_CORNER(quad) || EXISTS_NW_CORNER(quad)) +#define EXISTS_E_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SE_CORNER(quad) || EXISTS_NE_CORNER(quad)) +#define EXISTS_S_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SW_CORNER(quad) || EXISTS_SE_CORNER(quad)) +#define EXISTS_N_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_NW_CORNER(quad) || EXISTS_NE_CORNER(quad)) +// Note that EXISTS_NE_CORNER(quad) is equivalent to BOUNDARY_SW(quad), etc. + + + +QuadEdge::QuadEdge() + : quad(-1), edge(Edge_None) +{} + +QuadEdge::QuadEdge(long quad_, Edge edge_) + : quad(quad_), edge(edge_) +{} + +bool QuadEdge::operator<(const QuadEdge& other) const +{ + if (quad != other.quad) + return quad < other.quad; + else + return edge < other.edge; +} + +bool QuadEdge::operator==(const QuadEdge& other) const +{ + return quad == other.quad && edge == other.edge; +} + +bool QuadEdge::operator!=(const QuadEdge& other) const +{ + return !operator==(other); +} + +std::ostream& operator<<(std::ostream& os, const QuadEdge& quad_edge) +{ + return os << quad_edge.quad << ' ' << quad_edge.edge; +} + + + +XY::XY() +{} + +XY::XY(const double& x_, const double& y_) + : x(x_), y(y_) +{} + +bool XY::operator==(const XY& other) const +{ + return x == other.x && y == other.y; +} + +bool XY::operator!=(const XY& other) const +{ + return x != other.x || y != other.y; +} + +XY XY::operator*(const double& multiplier) const +{ + return XY(x*multiplier, y*multiplier); +} + +const XY& XY::operator+=(const XY& other) +{ + x += other.x; + y += other.y; + return *this; +} + +const XY& XY::operator-=(const XY& other) +{ + x -= other.x; + y -= other.y; + return *this; +} + +XY XY::operator+(const XY& other) const +{ + return XY(x + other.x, y + other.y); +} + +XY XY::operator-(const XY& other) const +{ + return XY(x - other.x, y - other.y); +} + +std::ostream& operator<<(std::ostream& os, const XY& xy) +{ + return os << '(' << xy.x << ' ' << xy.y << ')'; +} + + + +ContourLine::ContourLine(bool is_hole) + : std::vector(), + _is_hole(is_hole), + _parent(0) +{} + +void ContourLine::add_child(ContourLine* child) +{ + assert(!_is_hole && "Cannot add_child to a hole"); + assert(child != 0 && "Null child ContourLine"); + _children.push_back(child); +} + +void ContourLine::clear_parent() +{ + assert(is_hole() && "Cannot clear parent of non-hole"); + assert(_parent != 0 && "Null parent ContourLine"); + _parent = 0; +} + +const ContourLine::Children& ContourLine::get_children() const +{ + assert(!_is_hole && "Cannot get_children of a hole"); + return _children; +} + +const ContourLine* ContourLine::get_parent() const +{ + assert(_is_hole && "Cannot get_parent of a non-hole"); + return _parent; +} + +ContourLine* ContourLine::get_parent() +{ + assert(_is_hole && "Cannot get_parent of a non-hole"); + return _parent; +} + +bool ContourLine::is_hole() const +{ + return _is_hole; +} + +void ContourLine::push_back(const XY& point) +{ + if (empty() || point != back()) + std::vector::push_back(point); +} + +void ContourLine::set_parent(ContourLine* parent) +{ + assert(_is_hole && "Cannot set parent of a non-hole"); + assert(parent != 0 && "Null parent ContourLine"); + _parent = parent; +} + +void ContourLine::write() const +{ + std::cout << "ContourLine " << this << " of " << size() << " points:"; + for (const_iterator it = begin(); it != end(); ++it) + std::cout << ' ' << *it; + if (is_hole()) + std::cout << " hole, parent=" << get_parent(); + else { + std::cout << " not hole"; + if (!_children.empty()) { + std::cout << ", children="; + for (Children::const_iterator it = _children.begin(); + it != _children.end(); ++it) + std::cout << *it << ' '; + } + } + std::cout << std::endl; +} + + + +Contour::Contour() +{} + +Contour::~Contour() +{ + delete_contour_lines(); +} + +void Contour::delete_contour_lines() +{ + for (iterator line_it = begin(); line_it != end(); ++line_it) { + delete *line_it; + *line_it = 0; + } + std::vector::clear(); +} + +void Contour::write() const +{ + std::cout << "Contour of " << size() << " lines." << std::endl; + for (const_iterator it = begin(); it != end(); ++it) + (*it)->write(); +} + + + +ParentCache::ParentCache(long nx, long x_chunk_points, long y_chunk_points) + : _nx(nx), + _x_chunk_points(x_chunk_points), + _y_chunk_points(y_chunk_points), + _lines(0), // Initialised when first needed. + _istart(0), + _jstart(0) +{ + assert(_x_chunk_points > 0 && _y_chunk_points > 0 && + "Chunk sizes must be positive"); +} + +ContourLine* ParentCache::get_parent(long quad) +{ + long index = quad_to_index(quad); + ContourLine* parent = _lines[index]; + while (parent == 0) { + index -= _x_chunk_points; + assert(index >= 0 && "Failed to find parent in chunk ParentCache"); + parent = _lines[index]; + } + assert(parent != 0 && "Failed to find parent in chunk ParentCache"); + return parent; +} + +long ParentCache::quad_to_index(long quad) const +{ + long i = quad % _nx; + long j = quad / _nx; + long index = (i-_istart) + (j-_jstart)*_x_chunk_points; + + assert(i >= _istart && i < _istart + _x_chunk_points && + "i-index outside chunk"); + assert(j >= _jstart && j < _jstart + _y_chunk_points && + "j-index outside chunk"); + assert(index >= 0 && index < static_cast(_lines.size()) && + "ParentCache index outside chunk"); + + return index; +} + +void ParentCache::set_chunk_starts(long istart, long jstart) +{ + assert(istart >= 0 && jstart >= 0 && + "Chunk start indices cannot be negative"); + _istart = istart; + _jstart = jstart; + if (_lines.empty()) + _lines.resize(_x_chunk_points*_y_chunk_points, 0); + else + std::fill(_lines.begin(), _lines.end(), (ContourLine*)0); +} + +void ParentCache::set_parent(long quad, ContourLine& contour_line) +{ + assert(!_lines.empty() && + "Accessing ParentCache before it has been initialised"); + long index = quad_to_index(quad); + if (_lines[index] == 0) + _lines[index] = (contour_line.is_hole() ? contour_line.get_parent() + : &contour_line); +} + + + +QuadContourGenerator::QuadContourGenerator(const CoordinateArray& x, + const CoordinateArray& y, + const CoordinateArray& z, + const MaskArray& mask, + bool corner_mask, + long chunk_size) + : _x(x), + _y(y), + _z(z), + _nx(static_cast(_x.dim(1))), + _ny(static_cast(_x.dim(0))), + _n(_nx*_ny), + _corner_mask(corner_mask), + _chunk_size(chunk_size > 0 ? std::min(chunk_size, std::max(_nx, _ny)-1) + : std::max(_nx, _ny)-1), + _nxchunk(calc_chunk_count(_nx)), + _nychunk(calc_chunk_count(_ny)), + _chunk_count(_nxchunk*_nychunk), + _cache(new CacheItem[_n]), + _parent_cache(_nx, + chunk_size > 0 ? chunk_size+1 : _nx, + chunk_size > 0 ? chunk_size+1 : _ny) +{ + assert(!_x.empty() && !_y.empty() && !_z.empty() && "Empty array"); + assert(_y.dim(0) == _x.dim(0) && _y.dim(1) == _x.dim(1) && + "Different-sized y and x arrays"); + assert(_z.dim(0) == _x.dim(0) && _z.dim(1) == _x.dim(1) && + "Different-sized z and x arrays"); + assert((mask.empty() || + (mask.dim(0) == _x.dim(0) && mask.dim(1) == _x.dim(1))) && + "Different-sized mask and x arrays"); + + init_cache_grid(mask); +} + +QuadContourGenerator::~QuadContourGenerator() +{ + delete [] _cache; +} + +void QuadContourGenerator::append_contour_line_to_vertices( + ContourLine& contour_line, + PyObject* vertices_list) const +{ + assert(vertices_list != 0 && "Null python vertices_list"); + + // Convert ContourLine to python equivalent, and clear it. + npy_intp dims[2] = {static_cast(contour_line.size()), 2}; + numpy::array_view line(dims); + npy_intp i = 0; + for (ContourLine::const_iterator point = contour_line.begin(); + point != contour_line.end(); ++point, ++i) { + line(i, 0) = point->x; + line(i, 1) = point->y; + } + if (PyList_Append(vertices_list, line.pyobj())) { + Py_XDECREF(vertices_list); + throw "Unable to add contour line to vertices_list"; + } + + contour_line.clear(); +} + +void QuadContourGenerator::append_contour_to_vertices_and_codes( + Contour& contour, + PyObject* vertices_list, + PyObject* codes_list) const +{ + assert(vertices_list != 0 && "Null python vertices_list"); + assert(codes_list != 0 && "Null python codes_list"); + + // Convert Contour to python equivalent, and clear it. + for (Contour::iterator line_it = contour.begin(); line_it != contour.end(); + ++line_it) { + ContourLine& line = **line_it; + if (line.is_hole()) { + // If hole has already been converted to python its parent will be + // set to 0 and it can be deleted. + if (line.get_parent() != 0) { + delete *line_it; + *line_it = 0; + } + } + else { + // Non-holes are converted to python together with their child + // holes so that they are rendered correctly. + ContourLine::const_iterator point; + ContourLine::Children::const_iterator children_it; + + const ContourLine::Children& children = line.get_children(); + npy_intp npoints = static_cast(line.size() + 1); + for (children_it = children.begin(); children_it != children.end(); + ++children_it) + npoints += static_cast((*children_it)->size() + 1); + + npy_intp vertices_dims[2] = {npoints, 2}; + numpy::array_view vertices(vertices_dims); + double* vertices_ptr = vertices.data(); + + npy_intp codes_dims[1] = {npoints}; + numpy::array_view codes(codes_dims); + unsigned char* codes_ptr = codes.data(); + + for (point = line.begin(); point != line.end(); ++point) { + *vertices_ptr++ = point->x; + *vertices_ptr++ = point->y; + *codes_ptr++ = (point == line.begin() ? MOVETO : LINETO); + } + point = line.begin(); + *vertices_ptr++ = point->x; + *vertices_ptr++ = point->y; + *codes_ptr++ = CLOSEPOLY; + + for (children_it = children.begin(); children_it != children.end(); + ++children_it) { + ContourLine& child = **children_it; + for (point = child.begin(); point != child.end(); ++point) { + *vertices_ptr++ = point->x; + *vertices_ptr++ = point->y; + *codes_ptr++ = (point == child.begin() ? MOVETO : LINETO); + } + point = child.begin(); + *vertices_ptr++ = point->x; + *vertices_ptr++ = point->y; + *codes_ptr++ = CLOSEPOLY; + + child.clear_parent(); // To indicate it can be deleted. + } + + if (PyList_Append(vertices_list, vertices.pyobj()) || + PyList_Append(codes_list, codes.pyobj())) { + Py_XDECREF(vertices_list); + Py_XDECREF(codes_list); + contour.delete_contour_lines(); + throw "Unable to add contour line to vertices and codes lists"; + } + + delete *line_it; + *line_it = 0; + } + } + + // Delete remaining contour lines. + contour.delete_contour_lines(); +} + +long QuadContourGenerator::calc_chunk_count(long point_count) const +{ + assert(point_count > 0 && "point count must be positive"); + assert(_chunk_size > 0 && "Chunk size must be positive"); + + if (_chunk_size > 0) { + long count = (point_count-1) / _chunk_size; + if (count*_chunk_size < point_count-1) + ++count; + + assert(count >= 1 && "Invalid chunk count"); + return count; + } + else + return 1; +} + +PyObject* QuadContourGenerator::create_contour(const double& level) +{ + init_cache_levels(level, level); + + PyObject* vertices_list = PyList_New(0); + if (vertices_list == 0) + throw "Failed to create Python list"; + + // Lines that start and end on boundaries. + long ichunk, jchunk, istart, iend, jstart, jend; + for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { + get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); + + for (long j = jstart; j < jend; ++j) { + long quad_end = iend + j*_nx; + for (long quad = istart + j*_nx; quad < quad_end; ++quad) { + if (EXISTS_NONE(quad) || VISITED(quad,1)) continue; + + if (BOUNDARY_S(quad) && Z_SW >= 1 && Z_SE < 1 && + start_line(vertices_list, quad, Edge_S, level)) continue; + + if (BOUNDARY_W(quad) && Z_NW >= 1 && Z_SW < 1 && + start_line(vertices_list, quad, Edge_W, level)) continue; + + if (BOUNDARY_N(quad) && Z_NE >= 1 && Z_NW < 1 && + start_line(vertices_list, quad, Edge_N, level)) continue; + + if (BOUNDARY_E(quad) && Z_SE >= 1 && Z_NE < 1 && + start_line(vertices_list, quad, Edge_E, level)) continue; + + if (_corner_mask) { + // Equates to NE boundary. + if (EXISTS_SW_CORNER(quad) && Z_SE >= 1 && Z_NW < 1 && + start_line(vertices_list, quad, Edge_NE, level)) continue; + + // Equates to NW boundary. + if (EXISTS_SE_CORNER(quad) && Z_NE >= 1 && Z_SW < 1 && + start_line(vertices_list, quad, Edge_NW, level)) continue; + + // Equates to SE boundary. + if (EXISTS_NW_CORNER(quad) && Z_SW >= 1 && Z_NE < 1 && + start_line(vertices_list, quad, Edge_SE, level)) continue; + + // Equates to SW boundary. + if (EXISTS_NE_CORNER(quad) && Z_NW >= 1 && Z_SE < 1 && + start_line(vertices_list, quad, Edge_SW, level)) continue; + } + } + } + } + + // Internal loops. + ContourLine contour_line(false); // Reused for each contour line. + for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { + get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); + + for (long j = jstart; j < jend; ++j) { + long quad_end = iend + j*_nx; + for (long quad = istart + j*_nx; quad < quad_end; ++quad) { + if (EXISTS_NONE(quad) || VISITED(quad,1)) + continue; + + Edge start_edge = get_start_edge(quad, 1); + if (start_edge == Edge_None) + continue; + + QuadEdge quad_edge(quad, start_edge); + QuadEdge start_quad_edge(quad_edge); + + // To obtain output identical to that produced by legacy code, + // sometimes need to ignore the first point and add it on the + // end instead. + bool ignore_first = (start_edge == Edge_N); + follow_interior(contour_line, quad_edge, 1, level, + !ignore_first, &start_quad_edge, 1, false); + if (ignore_first && !contour_line.empty()) + contour_line.push_back(contour_line.front()); + append_contour_line_to_vertices(contour_line, vertices_list); + + // Repeat if saddle point but not visited. + if (SADDLE(quad,1) && !VISITED(quad,1)) + --quad; + } + } + } + + return vertices_list; +} + +PyObject* QuadContourGenerator::create_filled_contour(const double& lower_level, + const double& upper_level) +{ + init_cache_levels(lower_level, upper_level); + + Contour contour; + + PyObject* vertices = PyList_New(0); + if (vertices == 0) + throw "Failed to create Python list"; + + PyObject* codes = PyList_New(0); + if (codes == 0) { + Py_XDECREF(vertices); + throw "Failed to create Python list"; + } + + long ichunk, jchunk, istart, iend, jstart, jend; + for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { + get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); + _parent_cache.set_chunk_starts(istart, jstart); + + for (long j = jstart; j < jend; ++j) { + long quad_end = iend + j*_nx; + for (long quad = istart + j*_nx; quad < quad_end; ++quad) { + if (!EXISTS_NONE(quad)) + single_quad_filled(contour, quad, lower_level, upper_level); + } + } + + // Clear VISITED_W and VISITED_S flags that are reused by later chunks. + if (jchunk < _nychunk-1) { + long quad_end = iend + jend*_nx; + for (long quad = istart + jend*_nx; quad < quad_end; ++quad) + _cache[quad] &= ~MASK_VISITED_S; + } + + if (ichunk < _nxchunk-1) { + long quad_end = iend + jend*_nx; + for (long quad = iend + jstart*_nx; quad < quad_end; quad += _nx) + _cache[quad] &= ~MASK_VISITED_W; + } + + // Create python objects to return for this chunk. + append_contour_to_vertices_and_codes(contour, vertices, codes); + } + + PyObject* tuple = PyTuple_New(2); + if (tuple == 0) { + Py_XDECREF(vertices); + Py_XDECREF(codes); + throw "Failed to create Python tuple"; + } + + // No error checking here as filling in a brand new pre-allocated tuple. + PyTuple_SET_ITEM(tuple, 0, vertices); + PyTuple_SET_ITEM(tuple, 1, codes); + + return tuple; +} + +XY QuadContourGenerator::edge_interp(const QuadEdge& quad_edge, + const double& level) +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + return interp(get_edge_point_index(quad_edge, true), + get_edge_point_index(quad_edge, false), + level); +} + +unsigned int QuadContourGenerator::follow_boundary( + ContourLine& contour_line, + QuadEdge& quad_edge, + const double& lower_level, + const double& upper_level, + unsigned int level_index, + const QuadEdge& start_quad_edge) +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + assert(is_edge_a_boundary(quad_edge) && "Not a boundary edge"); + assert((level_index == 1 || level_index == 2) && + "level index must be 1 or 2"); + assert(start_quad_edge.quad >= 0 && start_quad_edge.quad < _n && + "Start quad index out of bounds"); + assert(start_quad_edge.edge != Edge_None && "Invalid start edge"); + + // Only called for filled contours, so always updates _parent_cache. + unsigned int end_level = 0; + bool first_edge = true; + bool stop = false; + long& quad = quad_edge.quad; + + while (true) { + // Levels of start and end points of quad_edge. + unsigned int start_level = + (first_edge ? Z_LEVEL(get_edge_point_index(quad_edge, true)) + : end_level); + long end_point = get_edge_point_index(quad_edge, false); + end_level = Z_LEVEL(end_point); + + if (level_index == 1) { + if (start_level <= level_index && end_level == 2) { + // Increasing z, switching levels from 1 to 2. + level_index = 2; + stop = true; + } + else if (start_level >= 1 && end_level == 0) { + // Decreasing z, keeping same level. + stop = true; + } + } + else { // level_index == 2 + if (start_level <= level_index && end_level == 2) { + // Increasing z, keeping same level. + stop = true; + } + else if (start_level >= 1 && end_level == 0) { + // Decreasing z, switching levels from 2 to 1. + level_index = 1; + stop = true; + } + } + + if (!first_edge && !stop && quad_edge == start_quad_edge) + // Return if reached start point of contour line. Do this before + // checking/setting VISITED flags as will already have been + // visited. + break; + + switch (quad_edge.edge) { + case Edge_E: + assert(!VISITED_W(quad+1) && "Already visited"); + _cache[quad+1] |= MASK_VISITED_W; + break; + case Edge_N: + assert(!VISITED_S(quad+_nx) && "Already visited"); + _cache[quad+_nx] |= MASK_VISITED_S; + break; + case Edge_W: + assert(!VISITED_W(quad) && "Already visited"); + _cache[quad] |= MASK_VISITED_W; + break; + case Edge_S: + assert(!VISITED_S(quad) && "Already visited"); + _cache[quad] |= MASK_VISITED_S; + break; + case Edge_NE: + case Edge_NW: + case Edge_SW: + case Edge_SE: + assert(!VISITED_CORNER(quad) && "Already visited"); + _cache[quad] |= MASK_VISITED_CORNER; + break; + default: + assert(0 && "Invalid Edge"); + break; + } + + if (stop) { + // Exiting boundary to enter interior. + contour_line.push_back(edge_interp(quad_edge, + level_index == 1 ? lower_level + : upper_level)); + break; + } + + move_to_next_boundary_edge(quad_edge); + + // Just moved to new quad edge, so label parent of start of quad edge. + switch (quad_edge.edge) { + case Edge_W: + case Edge_SW: + case Edge_S: + case Edge_SE: + if (!EXISTS_SE_CORNER(quad)) + _parent_cache.set_parent(quad, contour_line); + break; + case Edge_E: + case Edge_NE: + case Edge_N: + case Edge_NW: + if (!EXISTS_SW_CORNER(quad)) + _parent_cache.set_parent(quad + 1, contour_line); + break; + default: + assert(0 && "Invalid edge"); + break; + } + + // Add point to contour. + contour_line.push_back(get_point_xy(end_point)); + + if (first_edge) + first_edge = false; + } + + return level_index; +} + +void QuadContourGenerator::follow_interior(ContourLine& contour_line, + QuadEdge& quad_edge, + unsigned int level_index, + const double& level, + bool want_initial_point, + const QuadEdge* start_quad_edge, + unsigned int start_level_index, + bool set_parents) +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds."); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + assert((level_index == 1 || level_index == 2) && + "level index must be 1 or 2"); + assert((start_quad_edge == 0 || + (start_quad_edge->quad >= 0 && start_quad_edge->quad < _n)) && + "Start quad index out of bounds."); + assert((start_quad_edge == 0 || start_quad_edge->edge != Edge_None) && + "Invalid start edge"); + assert((start_level_index == 1 || start_level_index == 2) && + "start level index must be 1 or 2"); + + long& quad = quad_edge.quad; + Edge& edge = quad_edge.edge; + + if (want_initial_point) + contour_line.push_back(edge_interp(quad_edge, level)); + + CacheItem visited_mask = (level_index == 1 ? MASK_VISITED_1 : MASK_VISITED_2); + CacheItem saddle_mask = (level_index == 1 ? MASK_SADDLE_1 : MASK_SADDLE_2); + Dir dir = Dir_Straight; + + while (true) { + assert(!EXISTS_NONE(quad) && "Quad does not exist"); + assert(!(_cache[quad] & visited_mask) && "Quad already visited"); + + // Determine direction to move to next quad. If the quad is already + // labelled as a saddle quad then the direction is easily read from + // the cache. Otherwise the direction is determined differently + // depending on whether the quad is a corner quad or not. + + if (_cache[quad] & saddle_mask) { + // Already identified as a saddle quad, so direction is easy. + dir = (SADDLE_LEFT(quad,level_index) ? Dir_Left : Dir_Right); + _cache[quad] |= visited_mask; + } + else if (EXISTS_ANY_CORNER(quad)) { + // Need z-level of point opposite the entry edge, as that + // determines whether contour turns left or right. + long point_opposite = -1; + switch (edge) { + case Edge_E: + point_opposite = (EXISTS_SE_CORNER(quad) ? POINT_SW + : POINT_NW); + break; + case Edge_N: + point_opposite = (EXISTS_NW_CORNER(quad) ? POINT_SW + : POINT_SE); + break; + case Edge_W: + point_opposite = (EXISTS_SW_CORNER(quad) ? POINT_SE + : POINT_NE); + break; + case Edge_S: + point_opposite = (EXISTS_SW_CORNER(quad) ? POINT_NW + : POINT_NE); + break; + case Edge_NE: point_opposite = POINT_SW; break; + case Edge_NW: point_opposite = POINT_SE; break; + case Edge_SW: point_opposite = POINT_NE; break; + case Edge_SE: point_opposite = POINT_NW; break; + default: assert(0 && "Invalid edge"); break; + } + assert(point_opposite != -1 && "Failed to find opposite point"); + + // Lower-level polygons (level_index == 1) always have higher + // values to the left of the contour. Upper-level contours + // (level_index == 2) are reversed, which is what the fancy XOR + // does below. + if ((Z_LEVEL(point_opposite) >= level_index) ^ (level_index == 2)) + dir = Dir_Right; + else + dir = Dir_Left; + _cache[quad] |= visited_mask; + } + else { + // Calculate configuration of this quad. + long point_left = -1, point_right = -1; + switch (edge) { + case Edge_E: point_left = POINT_SW; point_right = POINT_NW; break; + case Edge_N: point_left = POINT_SE; point_right = POINT_SW; break; + case Edge_W: point_left = POINT_NE; point_right = POINT_SE; break; + case Edge_S: point_left = POINT_NW; point_right = POINT_NE; break; + default: assert(0 && "Invalid edge"); break; + } + + unsigned int config = (Z_LEVEL(point_left) >= level_index) << 1 | + (Z_LEVEL(point_right) >= level_index); + + // Upper level (level_index == 2) polygons are reversed compared to + // lower level ones, i.e. higher values on the right rather than + // the left. + if (level_index == 2) + config = 3 - config; + + // Calculate turn direction to move to next quad along contour line. + if (config == 1) { + // New saddle quad, set up cache bits for it. + double zmid = 0.25*(get_point_z(POINT_SW) + + get_point_z(POINT_SE) + + get_point_z(POINT_NW) + + get_point_z(POINT_NE)); + _cache[quad] |= (level_index == 1 ? MASK_SADDLE_1 : MASK_SADDLE_2); + if ((zmid > level) ^ (level_index == 2)) { + dir = Dir_Right; + } + else { + dir = Dir_Left; + _cache[quad] |= (level_index == 1 ? MASK_SADDLE_LEFT_1 + : MASK_SADDLE_LEFT_2); + } + if (edge == Edge_N || edge == Edge_E) { + // Next visit to this quad must start on S or W. + _cache[quad] |= (level_index == 1 ? MASK_SADDLE_START_SW_1 + : MASK_SADDLE_START_SW_2); + } + } + else { + // Normal (non-saddle) quad. + dir = (config == 0 ? Dir_Left + : (config == 3 ? Dir_Right : Dir_Straight)); + _cache[quad] |= visited_mask; + } + } + + // Use dir to determine exit edge. + edge = get_exit_edge(quad_edge, dir); + + if (set_parents) { + if (edge == Edge_E) + _parent_cache.set_parent(quad+1, contour_line); + else if (edge == Edge_W) + _parent_cache.set_parent(quad, contour_line); + } + + // Add new point to contour line. + contour_line.push_back(edge_interp(quad_edge, level)); + + // Stop if reached boundary. + if (is_edge_a_boundary(quad_edge)) + break; + + move_to_next_quad(quad_edge); + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + + // Return if reached start point of contour line. + if (start_quad_edge != 0 && + quad_edge == *start_quad_edge && + level_index == start_level_index) + break; + } +} + +void QuadContourGenerator::get_chunk_limits(long ijchunk, + long& ichunk, + long& jchunk, + long& istart, + long& iend, + long& jstart, + long& jend) +{ + assert(ijchunk >= 0 && ijchunk < _chunk_count && "ijchunk out of bounds"); + ichunk = ijchunk % _nxchunk; + jchunk = ijchunk / _nxchunk; + istart = ichunk*_chunk_size; + iend = (ichunk == _nxchunk-1 ? _nx : (ichunk+1)*_chunk_size); + jstart = jchunk*_chunk_size; + jend = (jchunk == _nychunk-1 ? _ny : (jchunk+1)*_chunk_size); +} + +Edge QuadContourGenerator::get_corner_start_edge(long quad, + unsigned int level_index) const +{ + assert(quad >= 0 && quad < _n && "Quad index out of bounds"); + assert((level_index == 1 || level_index == 2) && + "level index must be 1 or 2"); + assert(EXISTS_ANY_CORNER(quad) && "Quad is not a corner"); + + // Diagram for NE corner. Rotate for other corners. + // + // edge12 + // point1 +---------+ point2 + // \ | + // \ | edge23 + // edge31 \ | + // \ | + // + point3 + // + long point1, point2, point3; + Edge edge12, edge23, edge31; + switch (_cache[quad] & MASK_EXISTS) { + case MASK_EXISTS_SW_CORNER: + point1 = POINT_SE; point2 = POINT_SW; point3 = POINT_NW; + edge12 = Edge_S; edge23 = Edge_W; edge31 = Edge_NE; + break; + case MASK_EXISTS_SE_CORNER: + point1 = POINT_NE; point2 = POINT_SE; point3 = POINT_SW; + edge12 = Edge_E; edge23 = Edge_S; edge31 = Edge_NW; + break; + case MASK_EXISTS_NW_CORNER: + point1 = POINT_SW; point2 = POINT_NW; point3 = POINT_NE; + edge12 = Edge_W; edge23 = Edge_N; edge31 = Edge_SE; + break; + case MASK_EXISTS_NE_CORNER: + point1 = POINT_NW; point2 = POINT_NE; point3 = POINT_SE; + edge12 = Edge_N; edge23 = Edge_E; edge31 = Edge_SW; + break; + default: + assert(0 && "Invalid EXISTS for quad"); + return Edge_None; + } + + unsigned int config = (Z_LEVEL(point1) >= level_index) << 2 | + (Z_LEVEL(point2) >= level_index) << 1 | + (Z_LEVEL(point3) >= level_index); + + // Upper level (level_index == 2) polygons are reversed compared to lower + // level ones, i.e. higher values on the right rather than the left. + if (level_index == 2) + config = 7 - config; + + switch (config) { + case 0: return Edge_None; + case 1: return edge23; + case 2: return edge12; + case 3: return edge12; + case 4: return edge31; + case 5: return edge23; + case 6: return edge31; + case 7: return Edge_None; + default: assert(0 && "Invalid config"); return Edge_None; + } +} + +long QuadContourGenerator::get_edge_point_index(const QuadEdge& quad_edge, + bool start) const +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + + // Edges are ordered anticlockwise around their quad, as indicated by + // directions of arrows in diagrams below. + // Full quad NW corner (others similar) + // + // POINT_NW Edge_N POINT_NE POINT_NW Edge_N POINT_NE + // +----<-----+ +----<-----+ + // | | | / + // | | | quad / + // Edge_W V quad ^ Edge_E Edge_W V ^ + // | | | / Edge_SE + // | | | / + // +---->-----+ + + // POINT_SW Edge_S POINT_SE POINT_SW + // + const long& quad = quad_edge.quad; + switch (quad_edge.edge) { + case Edge_E: return (start ? POINT_SE : POINT_NE); + case Edge_N: return (start ? POINT_NE : POINT_NW); + case Edge_W: return (start ? POINT_NW : POINT_SW); + case Edge_S: return (start ? POINT_SW : POINT_SE); + case Edge_NE: return (start ? POINT_SE : POINT_NW); + case Edge_NW: return (start ? POINT_NE : POINT_SW); + case Edge_SW: return (start ? POINT_NW : POINT_SE); + case Edge_SE: return (start ? POINT_SW : POINT_NE); + default: assert(0 && "Invalid edge"); return 0; + } +} + +Edge QuadContourGenerator::get_exit_edge(const QuadEdge& quad_edge, + Dir dir) const +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + + const long& quad = quad_edge.quad; + const Edge& edge = quad_edge.edge; + if (EXISTS_ANY_CORNER(quad)) { + // Corner directions are always left or right. A corner is a triangle, + // entered via one edge so the other two edges are the left and right + // ones. + switch (edge) { + case Edge_E: + return (EXISTS_SE_CORNER(quad) + ? (dir == Dir_Left ? Edge_S : Edge_NW) + : (dir == Dir_Right ? Edge_N : Edge_SW)); + case Edge_N: + return (EXISTS_NW_CORNER(quad) + ? (dir == Dir_Right ? Edge_W : Edge_SE) + : (dir == Dir_Left ? Edge_E : Edge_SW)); + case Edge_W: + return (EXISTS_SW_CORNER(quad) + ? (dir == Dir_Right ? Edge_S : Edge_NE) + : (dir == Dir_Left ? Edge_N : Edge_SE)); + case Edge_S: + return (EXISTS_SW_CORNER(quad) + ? (dir == Dir_Left ? Edge_W : Edge_NE) + : (dir == Dir_Right ? Edge_E : Edge_NW)); + case Edge_NE: return (dir == Dir_Left ? Edge_S : Edge_W); + case Edge_NW: return (dir == Dir_Left ? Edge_E : Edge_S); + case Edge_SW: return (dir == Dir_Left ? Edge_N : Edge_E); + case Edge_SE: return (dir == Dir_Left ? Edge_W : Edge_N); + default: assert(0 && "Invalid edge"); return Edge_None; + } + } + else { + // A full quad has four edges, entered via one edge so that other three + // edges correspond to left, straight and right directions. + switch (edge) { + case Edge_E: + return (dir == Dir_Left ? Edge_S : + (dir == Dir_Right ? Edge_N : Edge_W)); + case Edge_N: + return (dir == Dir_Left ? Edge_E : + (dir == Dir_Right ? Edge_W : Edge_S)); + case Edge_W: + return (dir == Dir_Left ? Edge_N : + (dir == Dir_Right ? Edge_S : Edge_E)); + case Edge_S: + return (dir == Dir_Left ? Edge_W : + (dir == Dir_Right ? Edge_E : Edge_N)); + default: assert(0 && "Invalid edge"); return Edge_None; + } + } +} + +XY QuadContourGenerator::get_point_xy(long point) const +{ + assert(point >= 0 && point < _n && "Point index out of bounds."); + return XY(_x.data()[static_cast(point)], + _y.data()[static_cast(point)]); +} + +const double& QuadContourGenerator::get_point_z(long point) const +{ + assert(point >= 0 && point < _n && "Point index out of bounds."); + return _z.data()[static_cast(point)]; +} + +Edge QuadContourGenerator::get_quad_start_edge(long quad, + unsigned int level_index) const +{ + assert(quad >= 0 && quad < _n && "Quad index out of bounds"); + assert((level_index == 1 || level_index == 2) && + "level index must be 1 or 2"); + assert(EXISTS_QUAD(quad) && "Quad does not exist"); + + unsigned int config = (Z_NW >= level_index) << 3 | + (Z_NE >= level_index) << 2 | + (Z_SW >= level_index) << 1 | + (Z_SE >= level_index); + + // Upper level (level_index == 2) polygons are reversed compared to lower + // level ones, i.e. higher values on the right rather than the left. + if (level_index == 2) + config = 15 - config; + + switch (config) { + case 0: return Edge_None; + case 1: return Edge_E; + case 2: return Edge_S; + case 3: return Edge_E; + case 4: return Edge_N; + case 5: return Edge_N; + case 6: + // If already identified as a saddle quad then the start edge is + // read from the cache. Otherwise return either valid start edge + // and the subsequent call to follow_interior() will correctly set + // up saddle bits in cache. + if (!SADDLE(quad,level_index) || SADDLE_START_SW(quad,level_index)) + return Edge_S; + else + return Edge_N; + case 7: return Edge_N; + case 8: return Edge_W; + case 9: + // See comment for 6 above. + if (!SADDLE(quad,level_index) || SADDLE_START_SW(quad,level_index)) + return Edge_W; + else + return Edge_E; + case 10: return Edge_S; + case 11: return Edge_E; + case 12: return Edge_W; + case 13: return Edge_W; + case 14: return Edge_S; + case 15: return Edge_None; + default: assert(0 && "Invalid config"); return Edge_None; + } +} + +Edge QuadContourGenerator::get_start_edge(long quad, + unsigned int level_index) const +{ + if (EXISTS_ANY_CORNER(quad)) + return get_corner_start_edge(quad, level_index); + else + return get_quad_start_edge(quad, level_index); +} + +void QuadContourGenerator::init_cache_grid(const MaskArray& mask) +{ + long i, j, quad; + + if (mask.empty()) { + // No mask, easy to calculate quad existance and boundaries together. + quad = 0; + for (j = 0; j < _ny; ++j) { + for (i = 0; i < _nx; ++i, ++quad) { + _cache[quad] = 0; + + if (i < _nx-1 && j < _ny-1) + _cache[quad] |= MASK_EXISTS_QUAD; + + if ((i % _chunk_size == 0 || i == _nx-1) && j < _ny-1) + _cache[quad] |= MASK_BOUNDARY_W; + + if ((j % _chunk_size == 0 || j == _ny-1) && i < _nx-1) + _cache[quad] |= MASK_BOUNDARY_S; + } + } + } + else { + // Casting avoids problem when sizeof(bool) != sizeof(npy_bool). + const npy_bool* mask_ptr = + reinterpret_cast(mask.data()); + + // Have mask so use two stages. + // Stage 1, determine if quads/corners exist. + quad = 0; + for (j = 0; j < _ny; ++j) { + for (i = 0; i < _nx; ++i, ++quad) { + _cache[quad] = 0; + + if (i < _nx-1 && j < _ny-1) { + unsigned int config = mask_ptr[POINT_NW] << 3 | + mask_ptr[POINT_NE] << 2 | + mask_ptr[POINT_SW] << 1 | + mask_ptr[POINT_SE]; + + if (_corner_mask) { + switch (config) { + case 0: _cache[quad] = MASK_EXISTS_QUAD; break; + case 1: _cache[quad] = MASK_EXISTS_NW_CORNER; break; + case 2: _cache[quad] = MASK_EXISTS_NE_CORNER; break; + case 4: _cache[quad] = MASK_EXISTS_SW_CORNER; break; + case 8: _cache[quad] = MASK_EXISTS_SE_CORNER; break; + default: + // Do nothing, quad is masked out. + break; + } + } + else if (config == 0) + _cache[quad] = MASK_EXISTS_QUAD; + } + } + } + + // Stage 2, calculate W and S boundaries. For each quad use boundary + // data already calculated for quads to W and S, so must iterate + // through quads in correct order (increasing i and j indices). + // Cannot use boundary data for quads to E and N as have not yet + // calculated it. + quad = 0; + for (j = 0; j < _ny; ++j) { + for (i = 0; i < _nx; ++i, ++quad) { + if (_corner_mask) { + bool W_exists_none = (i == 0 || EXISTS_NONE(quad-1)); + bool S_exists_none = (j == 0 || EXISTS_NONE(quad-_nx)); + bool W_exists_E_edge = (i > 0 && EXISTS_E_EDGE(quad-1)); + bool S_exists_N_edge = (j > 0 && EXISTS_N_EDGE(quad-_nx)); + + if ((EXISTS_W_EDGE(quad) && W_exists_none) || + (EXISTS_NONE(quad) && W_exists_E_edge) || + (i % _chunk_size == 0 && EXISTS_W_EDGE(quad) && + W_exists_E_edge)) + _cache[quad] |= MASK_BOUNDARY_W; + + if ((EXISTS_S_EDGE(quad) && S_exists_none) || + (EXISTS_NONE(quad) && S_exists_N_edge) || + (j % _chunk_size == 0 && EXISTS_S_EDGE(quad) && + S_exists_N_edge)) + _cache[quad] |= MASK_BOUNDARY_S; + } + else { + bool W_exists_quad = (i > 0 && EXISTS_QUAD(quad-1)); + bool S_exists_quad = (j > 0 && EXISTS_QUAD(quad-_nx)); + + if ((EXISTS_QUAD(quad) != W_exists_quad) || + (i % _chunk_size == 0 && EXISTS_QUAD(quad) && + W_exists_quad)) + _cache[quad] |= MASK_BOUNDARY_W; + + if ((EXISTS_QUAD(quad) != S_exists_quad) || + (j % _chunk_size == 0 && EXISTS_QUAD(quad) && + S_exists_quad)) + _cache[quad] |= MASK_BOUNDARY_S; + } + } + } + } +} + +void QuadContourGenerator::init_cache_levels(const double& lower_level, + const double& upper_level) +{ + assert(upper_level >= lower_level && + "upper and lower levels are wrong way round"); + + bool two_levels = (lower_level != upper_level); + CacheItem keep_mask = + (_corner_mask ? MASK_EXISTS | MASK_BOUNDARY_S | MASK_BOUNDARY_W + : MASK_EXISTS_QUAD | MASK_BOUNDARY_S | MASK_BOUNDARY_W); + + if (two_levels) { + const double* z_ptr = _z.data(); + for (long quad = 0; quad < _n; ++quad, ++z_ptr) { + _cache[quad] &= keep_mask; + if (*z_ptr > upper_level) + _cache[quad] |= MASK_Z_LEVEL_2; + else if (*z_ptr > lower_level) + _cache[quad] |= MASK_Z_LEVEL_1; + } + } + else { + const double* z_ptr = _z.data(); + for (long quad = 0; quad < _n; ++quad, ++z_ptr) { + _cache[quad] &= keep_mask; + if (*z_ptr > lower_level) + _cache[quad] |= MASK_Z_LEVEL_1; + } + } +} + +XY QuadContourGenerator::interp( + long point1, long point2, const double& level) const +{ + assert(point1 >= 0 && point1 < _n && "Point index 1 out of bounds."); + assert(point2 >= 0 && point2 < _n && "Point index 2 out of bounds."); + assert(point1 != point2 && "Identical points"); + double fraction = (get_point_z(point2) - level) / + (get_point_z(point2) - get_point_z(point1)); + return get_point_xy(point1)*fraction + get_point_xy(point2)*(1.0 - fraction); +} + +bool QuadContourGenerator::is_edge_a_boundary(const QuadEdge& quad_edge) const +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + + switch (quad_edge.edge) { + case Edge_E: return BOUNDARY_E(quad_edge.quad); + case Edge_N: return BOUNDARY_N(quad_edge.quad); + case Edge_W: return BOUNDARY_W(quad_edge.quad); + case Edge_S: return BOUNDARY_S(quad_edge.quad); + case Edge_NE: return EXISTS_SW_CORNER(quad_edge.quad); + case Edge_NW: return EXISTS_SE_CORNER(quad_edge.quad); + case Edge_SW: return EXISTS_NE_CORNER(quad_edge.quad); + case Edge_SE: return EXISTS_NW_CORNER(quad_edge.quad); + default: assert(0 && "Invalid edge"); return true; + } +} + +void QuadContourGenerator::move_to_next_boundary_edge(QuadEdge& quad_edge) const +{ + assert(is_edge_a_boundary(quad_edge) && "QuadEdge is not a boundary"); + + long& quad = quad_edge.quad; + Edge& edge = quad_edge.edge; + + quad = get_edge_point_index(quad_edge, false); + + // quad is now such that POINT_SW is the end point of the quad_edge passed + // to this function. + + // To find the next boundary edge, first attempt to turn left 135 degrees + // and if that edge is a boundary then move to it. If not, attempt to turn + // left 90 degrees, then left 45 degrees, then straight on, etc, until can + // move. + // First determine which edge to attempt first. + int index = 0; + switch (edge) { + case Edge_E: index = 0; break; + case Edge_SE: index = 1; break; + case Edge_S: index = 2; break; + case Edge_SW: index = 3; break; + case Edge_W: index = 4; break; + case Edge_NW: index = 5; break; + case Edge_N: index = 6; break; + case Edge_NE: index = 7; break; + default: assert(0 && "Invalid edge"); break; + } + + // If _corner_mask not set, only need to consider odd index in loop below. + if (!_corner_mask) + ++index; + + // Try each edge in turn until a boundary is found. + int start_index = index; + do + { + switch (index) { + case 0: + if (EXISTS_SE_CORNER(quad-_nx-1)) { // Equivalent to BOUNDARY_NW + quad -= _nx+1; + edge = Edge_NW; + return; + } + break; + case 1: + if (BOUNDARY_N(quad-_nx-1)) { + quad -= _nx+1; + edge = Edge_N; + return; + } + break; + case 2: + if (EXISTS_SW_CORNER(quad-1)) { // Equivalent to BOUNDARY_NE + quad -= 1; + edge = Edge_NE; + return; + } + break; + case 3: + if (BOUNDARY_E(quad-1)) { + quad -= 1; + edge = Edge_E; + return; + } + break; + case 4: + if (EXISTS_NW_CORNER(quad)) { // Equivalent to BOUNDARY_SE + edge = Edge_SE; + return; + } + break; + case 5: + if (BOUNDARY_S(quad)) { + edge = Edge_S; + return; + } + break; + case 6: + if (EXISTS_NE_CORNER(quad-_nx)) { // Equivalent to BOUNDARY_SW + quad -= _nx; + edge = Edge_SW; + return; + } + break; + case 7: + if (BOUNDARY_W(quad-_nx)) { + quad -= _nx; + edge = Edge_W; + return; + } + break; + default: assert(0 && "Invalid index"); break; + } + + if (_corner_mask) + index = (index + 1) % 8; + else + index = (index + 2) % 8; + } while (index != start_index); + + assert(0 && "Failed to find next boundary edge"); +} + +void QuadContourGenerator::move_to_next_quad(QuadEdge& quad_edge) const +{ + assert(quad_edge.quad >= 0 && quad_edge.quad < _n && + "Quad index out of bounds"); + assert(quad_edge.edge != Edge_None && "Invalid edge"); + + // Move from quad_edge.quad to the neighbouring quad in the direction + // specified by quad_edge.edge. + switch (quad_edge.edge) { + case Edge_E: quad_edge.quad += 1; quad_edge.edge = Edge_W; break; + case Edge_N: quad_edge.quad += _nx; quad_edge.edge = Edge_S; break; + case Edge_W: quad_edge.quad -= 1; quad_edge.edge = Edge_E; break; + case Edge_S: quad_edge.quad -= _nx; quad_edge.edge = Edge_N; break; + default: assert(0 && "Invalid edge"); break; + } +} + +void QuadContourGenerator::single_quad_filled(Contour& contour, + long quad, + const double& lower_level, + const double& upper_level) +{ + assert(quad >= 0 && quad < _n && "Quad index out of bounds"); + + // Order of checking is important here as can have different ContourLines + // from both lower and upper levels in the same quad. First check the S + // edge, then move up the quad to the N edge checking as required. + + // Possible starts from S boundary. + if (BOUNDARY_S(quad) && EXISTS_S_EDGE(quad)) { + + // Lower-level start from S boundary into interior. + if (!VISITED_S(quad) && Z_SW >= 1 && Z_SE == 0) + contour.push_back(start_filled(quad, Edge_S, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from S boundary into interior. + if (!VISITED_S(quad) && Z_SW < 2 && Z_SE == 2) + contour.push_back(start_filled(quad, Edge_S, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start following S boundary from W to E. + if (!VISITED_S(quad) && Z_SW <= 1 && Z_SE == 1) + contour.push_back(start_filled(quad, Edge_S, 1, NotHole, Boundary, + lower_level, upper_level)); + + // Upper-level start following S boundary from W to E. + if (!VISITED_S(quad) && Z_SW == 2 && Z_SE == 1) + contour.push_back(start_filled(quad, Edge_S, 2, NotHole, Boundary, + lower_level, upper_level)); + } + + // Possible starts from W boundary. + if (BOUNDARY_W(quad) && EXISTS_W_EDGE(quad)) { + + // Lower-level start from W boundary into interior. + if (!VISITED_W(quad) && Z_NW >= 1 && Z_SW == 0) + contour.push_back(start_filled(quad, Edge_W, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from W boundary into interior. + if (!VISITED_W(quad) && Z_NW < 2 && Z_SW == 2) + contour.push_back(start_filled(quad, Edge_W, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start following W boundary from N to S. + if (!VISITED_W(quad) && Z_NW <= 1 && Z_SW == 1) + contour.push_back(start_filled(quad, Edge_W, 1, NotHole, Boundary, + lower_level, upper_level)); + + // Upper-level start following W boundary from N to S. + if (!VISITED_W(quad) && Z_NW == 2 && Z_SW == 1) + contour.push_back(start_filled(quad, Edge_W, 2, NotHole, Boundary, + lower_level, upper_level)); + } + + // Possible starts from NE boundary. + if (EXISTS_SW_CORNER(quad)) { // i.e. BOUNDARY_NE + + // Lower-level start following NE boundary from SE to NW, hole. + if (!VISITED_CORNER(quad) && Z_NW == 1 && Z_SE == 1) + contour.push_back(start_filled(quad, Edge_NE, 1, Hole, Boundary, + lower_level, upper_level)); + } + // Possible starts from SE boundary. + else if (EXISTS_NW_CORNER(quad)) { // i.e. BOUNDARY_SE + + // Lower-level start from N to SE. + if (!VISITED(quad,1) && Z_NW == 0 && Z_SW == 0 && Z_NE >= 1) + contour.push_back(start_filled(quad, Edge_N, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from SE to N, hole. + if (!VISITED(quad,2) && Z_NW < 2 && Z_SW < 2 && Z_NE == 2) + contour.push_back(start_filled(quad, Edge_SE, 2, Hole, Interior, + lower_level, upper_level)); + + // Upper-level start from N to SE. + if (!VISITED(quad,2) && Z_NW == 2 && Z_SW == 2 && Z_NE < 2) + contour.push_back(start_filled(quad, Edge_N, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start from SE to N, hole. + if (!VISITED(quad,1) && Z_NW >= 1 && Z_SW >= 1 && Z_NE == 0) + contour.push_back(start_filled(quad, Edge_SE, 1, Hole, Interior, + lower_level, upper_level)); + } + // Possible starts from NW boundary. + else if (EXISTS_SE_CORNER(quad)) { // i.e. BOUNDARY_NW + + // Lower-level start from NW to E. + if (!VISITED(quad,1) && Z_SW == 0 && Z_SE == 0 && Z_NE >= 1) + contour.push_back(start_filled(quad, Edge_NW, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from E to NW, hole. + if (!VISITED(quad,2) && Z_SW < 2 && Z_SE < 2 && Z_NE == 2) + contour.push_back(start_filled(quad, Edge_E, 2, Hole, Interior, + lower_level, upper_level)); + + // Upper-level start from NW to E. + if (!VISITED(quad,2) && Z_SW == 2 && Z_SE == 2 && Z_NE < 2) + contour.push_back(start_filled(quad, Edge_NW, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start from E to NW, hole. + if (!VISITED(quad,1) && Z_SW >= 1 && Z_SE >= 1 && Z_NE == 0) + contour.push_back(start_filled(quad, Edge_E, 1, Hole, Interior, + lower_level, upper_level)); + } + // Possible starts from SW boundary. + else if (EXISTS_NE_CORNER(quad)) { // i.e. BOUNDARY_SW + + // Lower-level start from SW boundary into interior. + if (!VISITED_CORNER(quad) && Z_NW >= 1 && Z_SE == 0) + contour.push_back(start_filled(quad, Edge_SW, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from SW boundary into interior. + if (!VISITED_CORNER(quad) && Z_NW < 2 && Z_SE == 2) + contour.push_back(start_filled(quad, Edge_SW, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start following SW boundary from NW to SE. + if (!VISITED_CORNER(quad) && Z_NW <= 1 && Z_SE == 1) + contour.push_back(start_filled(quad, Edge_SW, 1, NotHole, Boundary, + lower_level, upper_level)); + + // Upper-level start following SW boundary from NW to SE. + if (!VISITED_CORNER(quad) && Z_NW == 2 && Z_SE == 1) + contour.push_back(start_filled(quad, Edge_SW, 2, NotHole, Boundary, + lower_level, upper_level)); + } + + // A full (unmasked) quad can only have a start on the NE corner, i.e. from + // N to E (lower level) or E to N (upper level). Any other start will have + // already been created in a call to this function for a prior quad so we + // don't need to test for it again here. + // + // The situation is complicated by the possibility that the quad is a + // saddle quad, in which case a contour line starting on the N could leave + // by either the W or the E. We only need to consider those leaving E. + // + // A NE corner can also have a N to E or E to N start. + if (EXISTS_QUAD(quad) || EXISTS_NE_CORNER(quad)) { + + // Lower-level start from N to E. + if (!VISITED(quad,1) && Z_NW == 0 && Z_SE == 0 && Z_NE >= 1 && + (!SADDLE(quad,1) || SADDLE_LEFT(quad,1))) + contour.push_back(start_filled(quad, Edge_N, 1, NotHole, Interior, + lower_level, upper_level)); + + // Upper-level start from E to N, hole. + if (!VISITED(quad,2) && Z_NW < 2 && Z_SE < 2 && Z_NE == 2 && + (!SADDLE(quad,2) || !SADDLE_LEFT(quad,2))) + contour.push_back(start_filled(quad, Edge_E, 2, Hole, Interior, + lower_level, upper_level)); + + // Upper-level start from N to E. + if (!VISITED(quad,2) && Z_NW == 2 && Z_SE == 2 && Z_NE < 2 && + (!SADDLE(quad,2) || SADDLE_LEFT(quad,2))) + contour.push_back(start_filled(quad, Edge_N, 2, NotHole, Interior, + lower_level, upper_level)); + + // Lower-level start from E to N, hole. + if (!VISITED(quad,1) && Z_NW >= 1 && Z_SE >= 1 && Z_NE == 0 && + (!SADDLE(quad,1) || !SADDLE_LEFT(quad,1))) + contour.push_back(start_filled(quad, Edge_E, 1, Hole, Interior, + lower_level, upper_level)); + + // All possible contours passing through the interior of this quad + // should have already been created, so assert this. + assert((VISITED(quad,1) || get_start_edge(quad, 1) == Edge_None) && + "Found start of contour that should have already been created"); + assert((VISITED(quad,2) || get_start_edge(quad, 2) == Edge_None) && + "Found start of contour that should have already been created"); + } + + // Lower-level start following N boundary from E to W, hole. + // This is required for an internal masked region which is a hole in a + // surrounding contour line. + if (BOUNDARY_N(quad) && EXISTS_N_EDGE(quad) && + !VISITED_S(quad+_nx) && Z_NW == 1 && Z_NE == 1) + contour.push_back(start_filled(quad, Edge_N, 1, Hole, Boundary, + lower_level, upper_level)); +} + +ContourLine* QuadContourGenerator::start_filled( + long quad, + Edge edge, + unsigned int start_level_index, + HoleOrNot hole_or_not, + BoundaryOrInterior boundary_or_interior, + const double& lower_level, + const double& upper_level) +{ + assert(quad >= 0 && quad < _n && "Quad index out of bounds"); + assert(edge != Edge_None && "Invalid edge"); + assert((start_level_index == 1 || start_level_index == 2) && + "start level index must be 1 or 2"); + + ContourLine* contour_line = new ContourLine(hole_or_not == Hole); + if (hole_or_not == Hole) { + // Find and set parent ContourLine. + ContourLine* parent = _parent_cache.get_parent(quad + 1); + assert(parent != 0 && "Failed to find parent ContourLine"); + contour_line->set_parent(parent); + parent->add_child(contour_line); + } + + QuadEdge quad_edge(quad, edge); + const QuadEdge start_quad_edge(quad_edge); + unsigned int level_index = start_level_index; + + // If starts on interior, can only finish on interior. + // If starts on boundary, can only finish on boundary. + + while (true) { + if (boundary_or_interior == Interior) { + double level = (level_index == 1 ? lower_level : upper_level); + follow_interior(*contour_line, quad_edge, level_index, level, + false, &start_quad_edge, start_level_index, true); + } + else { + level_index = follow_boundary( + *contour_line, quad_edge, lower_level, + upper_level, level_index, start_quad_edge); + } + + if (quad_edge == start_quad_edge && (boundary_or_interior == Boundary || + level_index == start_level_index)) + break; + + if (boundary_or_interior == Boundary) + boundary_or_interior = Interior; + else + boundary_or_interior = Boundary; + } + + return contour_line; +} + +bool QuadContourGenerator::start_line( + PyObject* vertices_list, long quad, Edge edge, const double& level) +{ + assert(vertices_list != 0 && "Null python vertices list"); + assert(is_edge_a_boundary(QuadEdge(quad, edge)) && + "QuadEdge is not a boundary"); + + QuadEdge quad_edge(quad, edge); + ContourLine contour_line(false); + follow_interior(contour_line, quad_edge, 1, level, true, 0, 1, false); + append_contour_line_to_vertices(contour_line, vertices_list); + return VISITED(quad,1); +} + +void QuadContourGenerator::write_cache(bool grid_only) const +{ + std::cout << "-----------------------------------------------" << std::endl; + for (long quad = 0; quad < _n; ++quad) + write_cache_quad(quad, grid_only); + std::cout << "-----------------------------------------------" << std::endl; +} + +void QuadContourGenerator::write_cache_quad(long quad, bool grid_only) const +{ + long j = quad / _nx; + long i = quad - j*_nx; + std::cout << quad << ": i=" << i << " j=" << j + << " EXISTS=" << EXISTS_QUAD(quad); + if (_corner_mask) + std::cout << " CORNER=" << EXISTS_SW_CORNER(quad) << EXISTS_SE_CORNER(quad) + << EXISTS_NW_CORNER(quad) << EXISTS_NE_CORNER(quad); + std::cout << " BNDY=" << (BOUNDARY_S(quad)>0) << (BOUNDARY_W(quad)>0); + if (!grid_only) { + std::cout << " Z=" << Z_LEVEL(quad) + << " SAD=" << (SADDLE(quad,1)>0) << (SADDLE(quad,2)>0) + << " LEFT=" << (SADDLE_LEFT(quad,1)>0) << (SADDLE_LEFT(quad,2)>0) + << " NW=" << (SADDLE_START_SW(quad,1)>0) << (SADDLE_START_SW(quad,2)>0) + << " VIS=" << (VISITED(quad,1)>0) << (VISITED(quad,2)>0) + << (VISITED_S(quad)>0) << (VISITED_W(quad)>0) + << (VISITED_CORNER(quad)>0); + } + std::cout << std::endl; +} diff --git a/src/_contour.h b/src/_contour.h new file mode 100644 index 000000000000..e01c3bc732b9 --- /dev/null +++ b/src/_contour.h @@ -0,0 +1,530 @@ +/* + * QuadContourGenerator + * -------------------- + * A QuadContourGenerator generates contours for scalar fields defined on + * quadrilateral grids. A single QuadContourGenerator object can create both + * line contours (at single levels) and filled contours (between pairs of + * levels) for the same field. + * + * A field to be contoured has nx, ny points in the x- and y-directions + * respectively. The quad grid is defined by x and y arrays of shape(ny, nx), + * and the field itself is the z array also of shape(ny, nx). There is an + * optional boolean mask; if it exists then it also has shape(ny, nx). The + * mask applies to grid points rather than quads. + * + * How quads are masked based on the point mask is determined by the boolean + * 'corner_mask' flag. If false then any quad that has one or more of its four + * corner points masked is itself masked. If true the behaviour is the same + * except that any quad which has exactly one of its four corner points masked + * has only the triangular corner (half of the quad) adjacent to that point + * masked; the opposite triangular corner has three unmasked points and is not + * masked. + * + * By default the entire domain of nx*ny points is contoured together which can + * result in some very long polygons. The alternative is to break up the + * domain into subdomains or 'chunks' of smaller size, each of which is + * independently contoured. The size of these chunks is controlled by the + * 'nchunk' (or 'chunk_size') parameter. Chunking not only results in shorter + * polygons but also requires slightly less RAM. It can result in rendering + * artifacts though, depending on backend, antialiased flag and alpha value. + * + * Notation + * -------- + * i and j are array indices in the x- and y-directions respectively. Although + * a single element of an array z can be accessed using z[j][i] or z(j,i), it + * is often convenient to use the single quad index z[quad], where + * quad = i + j*nx + * and hence + * i = quad % nx + * j = quad / nx + * + * Rather than referring to x- and y-directions, compass directions are used + * instead such that W, E, S, N refer to the -x, +x, -y, +y directions + * respectively. To move one quad to the E you would therefore add 1 to the + * quad index, to move one quad to the N you would add nx to the quad index. + * + * Cache + * ----- + * Lots of information that is reused during contouring is stored as single + * bits in a mesh-sized cache, indexed by quad. Each quad's cache entry stores + * information about the quad itself such as if it is masked, and about the + * point at the SW corner of the quad, and about the W and S edges. Hence + * information about each point and each edge is only stored once in the cache. + * + * Cache information is divided into two types: that which is constant over the + * lifetime of the QuadContourGenerator, and that which changes for each + * contouring operation. The former is all grid-specific information such + * as quad and corner masks, and which edges are boundaries, either between + * masked and non-masked regions or between adjacent chunks. The latter + * includes whether points lie above or below the current contour levels, plus + * some flags to indicate how the contouring is progressing. + * + * Line Contours + * ------------- + * A line contour connects points with the same z-value. Each point of such a + * contour occurs on an edge of the grid, at a point linearly interpolated to + * the contour z-level from the z-values at the end points of the edge. The + * direction of a line contour is such that higher values are to the left of + * the contour, so any edge that the contour passes through will have a left- + * hand end point with z > contour level and a right-hand end point with + * z <= contour level. + * + * Line contours are of two types. Firstly there are open line strips that + * start on a boundary, traverse the interior of the domain and end on a + * boundary. Secondly there are closed line loops that occur completely within + * the interior of the domain and do not touch a boundary. + * + * The QuadContourGenerator makes two sweeps through the grid to generate line + * contours for a particular level. In the first sweep it looks only for start + * points that occur on boundaries, and when it finds one it follows the + * contour through the interior until it finishes on another boundary edge. + * Each quad that is visited by the algorithm has a 'visited' flag set in the + * cache to indicate that the quad does not need to be visited again. In the + * second sweep all non-visited quads are checked to see if they contain part + * of an interior closed loop, and again each time one is found it is followed + * through the domain interior until it returns back to its start quad and is + * therefore completed. + * + * The situation is complicated by saddle quads that have two opposite corners + * with z >= contour level and the other two corners with z < contour level. + * These therefore contain two segments of a line contour, and the visited + * flags take account of this by only being set on the second visit. On the + * first visit a number of saddle flags are set in the cache to indicate which + * one of the two segments has been completed so far. + * + * Filled Contours + * --------------- + * Filled contours are produced between two contour levels and are always + * closed polygons. They can occur completely within the interior of the + * domain without touching a boundary, following either the lower or upper + * contour levels. Those on the lower level are exactly like interior line + * contours with higher values on the left. Those on the upper level are + * reversed such that higher values are on the right. + * + * Filled contours can also involve a boundary in which case they consist of + * one or more sections along a boundary and one or more sections through the + * interior. Interior sections can be on either level, and again those on the + * upper level have higher values on the right. Boundary sections can remain + * on either contour level or switch between the two. + * + * Once the start of a filled contour is found, the algorithm is similar to + * that for line contours in that it follows the contour to its end, which + * because filled contours are always closed polygons will be by returning + * back to the start. However, because two levels must be considered, each + * level has its own set of saddle and visited flags and indeed some extra + * visited flags for boundary edges. + * + * The major complication for filled contours is that some polygons can be + * holes (with points ordered clockwise) within other polygons (with points + * ordered anticlockwise). When it comes to rendering filled contours each + * non-hole polygon must be rendered along with its zero or more contained + * holes or the rendering will not be correct. The filled contour finding + * algorithm could progress pretty much as the line contour algorithm does, + * taking each polygon as it is found, but then at the end there would have to + * be an extra step to identify the parent non-hole polygon for each hole. + * This is not a particularly onerous task but it does not scale well and can + * easily dominate the execution time of the contour finding for even modest + * problems. It is much better to identity each hole's parent non-hole during + * the sweep algorithm. + * + * This requirement dictates the order that filled contours are identified. As + * the algorithm sweeps up through the grid, every time a polygon passes + * through a quad a ParentCache object is updated with the new possible parent. + * When a new hole polygon is started, the ParentCache is used to find the + * first possible parent in the same quad or to the S of it. Great care is + * needed each time a new quad is checked to see if a new polygon should be + * started, as a single quad can have multiple polygon starts, e.g. a quad + * could be a saddle quad for both lower and upper contour levels, meaning it + * has four contour line segments passing through it which could all be from + * different polygons. The S-most polygon must be started first, then the next + * S-most and so on until the N-most polygon is started in that quad. + */ +#ifndef _CONTOUR_H +#define _CONTOUR_H + +#include "src/numpy_cpp.h" +#include +#include +#include +#include + + +// Edge of a quad including diagonal edges of masked quads if _corner_mask true. +typedef enum +{ + // Listing values here so easier to check for debug purposes. + Edge_None = -1, + Edge_E = 0, + Edge_N = 1, + Edge_W = 2, + Edge_S = 3, + // The following are only used if _corner_mask is true. + Edge_NE = 4, + Edge_NW = 5, + Edge_SW = 6, + Edge_SE = 7 +} Edge; + +// Combination of a quad and an edge of that quad. +// An invalid quad edge has quad of -1. +struct QuadEdge +{ + QuadEdge(); + QuadEdge(long quad_, Edge edge_); + bool operator<(const QuadEdge& other) const; + bool operator==(const QuadEdge& other) const; + bool operator!=(const QuadEdge& other) const; + friend std::ostream& operator<<(std::ostream& os, + const QuadEdge& quad_edge); + + long quad; + Edge edge; +}; + +// 2D point with x,y coordinates. +struct XY +{ + XY(); + XY(const double& x_, const double& y_); + bool operator==(const XY& other) const; + bool operator!=(const XY& other) const; + XY operator*(const double& multiplier) const; + const XY& operator+=(const XY& other); + const XY& operator-=(const XY& other); + XY operator+(const XY& other) const; + XY operator-(const XY& other) const; + friend std::ostream& operator<<(std::ostream& os, const XY& xy); + + double x, y; +}; + +// A single line of a contour, which may be a closed line loop or an open line +// strip. Identical adjacent points are avoided using push_back(). +// A ContourLine is either a hole (points ordered clockwise) or it is not +// (points ordered anticlockwise). Each hole has a parent ContourLine that is +// not a hole; each non-hole contains zero or more child holes. A non-hole and +// its child holes must be rendered together to obtain the correct results. +class ContourLine : public std::vector +{ +public: + typedef std::list Children; + + ContourLine(bool is_hole); + void add_child(ContourLine* child); + void clear_parent(); + const Children& get_children() const; + const ContourLine* get_parent() const; + ContourLine* get_parent(); + bool is_hole() const; + void push_back(const XY& point); + void set_parent(ContourLine* parent); + void write() const; + +private: + bool _is_hole; + ContourLine* _parent; // Only set if is_hole, not owned. + Children _children; // Only set if !is_hole, not owned. +}; + + +// A Contour is a collection of zero or more ContourLines. +class Contour : public std::vector +{ +public: + Contour(); + virtual ~Contour(); + void delete_contour_lines(); + void write() const; +}; + + +// Single chunk of ContourLine parents, indexed by quad. As a chunk's filled +// contours are created, the ParentCache is updated each time a ContourLine +// passes through each quad. When a new ContourLine is created, if it is a +// hole its parent ContourLine is read from the ParentCache by looking at the +// start quad, then each quad to the S in turn until a non-zero ContourLine is +// found. +class ParentCache +{ +public: + ParentCache(long nx, long x_chunk_points, long y_chunk_points); + ContourLine* get_parent(long quad); + void set_chunk_starts(long istart, long jstart); + void set_parent(long quad, ContourLine& contour_line); + +private: + long quad_to_index(long quad) const; + + long _nx; + long _x_chunk_points, _y_chunk_points; // Number of points not quads. + std::vector _lines; // Not owned. + long _istart, _jstart; +}; + + +// See overview of algorithm at top of file. +class QuadContourGenerator +{ +public: + typedef numpy::array_view CoordinateArray; + typedef numpy::array_view MaskArray; + + // Constructor with optional mask. + // x, y, z: double arrays of shape (ny,nx). + // mask: boolean array, ether empty (if no mask), or of shape (ny,nx). + // corner_mask: flag for different masking behaviour. + // chunk_size: 0 for no chunking, or +ve integer for size of chunks that + // the domain is subdivided into. + QuadContourGenerator(const CoordinateArray& x, + const CoordinateArray& y, + const CoordinateArray& z, + const MaskArray& mask, + bool corner_mask, + long chunk_size); + + // Destructor. + ~QuadContourGenerator(); + + // Create and return polygons for a line (i.e. non-filled) contour at the + // specified level. + PyObject* create_contour(const double& level); + + // Create and return polygons for a filled contour between the two + // specified levels. + PyObject* create_filled_contour(const double& lower_level, + const double& upper_level); + +private: + // Typedef for following either a boundary of the domain or the interior; + // clearer than using a boolean. + typedef enum + { + Boundary, + Interior + } BoundaryOrInterior; + + // Typedef for direction of movement from one quad to the next. + typedef enum + { + Dir_Right = -1, + Dir_Straight = 0, + Dir_Left = +1 + } Dir; + + // Typedef for a polygon being a hole or not; clearer than using a boolean. + typedef enum + { + NotHole, + Hole + } HoleOrNot; + + // Append a C++ ContourLine to the end of a python list. Used for line + // contours where each ContourLine is converted to a separate numpy array + // of (x,y) points. + // Clears the ContourLine too. + void append_contour_line_to_vertices(ContourLine& contour_line, + PyObject* vertices_list) const; + + // Append a C++ Contour to the end of two python lists. Used for filled + // contours where each non-hole ContourLine and its child holes are + // represented by a numpy array of (x,y) points and a second numpy array of + // 'kinds' or 'codes' that indicates where the points array is split into + // individual polygons. + // Clears the Contour too, freeing each ContourLine as soon as possible + // for minimum RAM usage. + void append_contour_to_vertices_and_codes(Contour& contour, + PyObject* vertices_list, + PyObject* codes_list) const; + + // Return number of chunks that fit in the specified point_count. + long calc_chunk_count(long point_count) const; + + // Return the point on the specified QuadEdge that intersects the specified + // level. + XY edge_interp(const QuadEdge& quad_edge, const double& level); + + // Follow a contour along a boundary, appending points to the ContourLine + // as it progresses. Only called for filled contours. Stops when the + // contour leaves the boundary to move into the interior of the domain, or + // when the start_quad_edge is reached in which case the ContourLine is a + // completed closed loop. Always adds the end point of each boundary edge + // to the ContourLine, regardless of whether moving to another boundary + // edge or leaving the boundary into the interior. Never adds the start + // point of the first boundary edge to the ContourLine. + // contour_line: ContourLine to append points to. + // quad_edge: on entry the QuadEdge to start from, on exit the QuadEdge + // that is stopped on. + // lower_level: lower contour z-value. + // upper_level: upper contour z-value. + // level_index: level index started on (1 = lower, 2 = upper level). + // start_quad_edge: QuadEdge that the ContourLine started from, which is + // used to check if the ContourLine is finished. + // Returns the end level_index. + unsigned int follow_boundary(ContourLine& contour_line, + QuadEdge& quad_edge, + const double& lower_level, + const double& upper_level, + unsigned int level_index, + const QuadEdge& start_quad_edge); + + // Follow a contour across the interior of the domain, appending points to + // the ContourLine as it progresses. Called for both line and filled + // contours. Stops when the contour reaches a boundary or, if the + // start_quad_edge is specified, when quad_edge == start_quad_edge and + // level_index == start_level_index. Always adds the end point of each + // quad traversed to the ContourLine; only adds the start point of the + // first quad if want_initial_point flag is true. + // contour_line: ContourLine to append points to. + // quad_edge: on entry the QuadEdge to start from, on exit the QuadEdge + // that is stopped on. + // level_index: level index started on (1 = lower, 2 = upper level). + // level: contour z-value. + // want_initial_point: whether want to append the initial point to the + // ContourLine or not. + // start_quad_edge: the QuadEdge that the ContourLine started from to + // check if the ContourLine is finished, or 0 if no check should occur. + // start_level_index: the level_index that the ContourLine started from. + // set_parents: whether should set ParentCache as it progresses or not. + // This is true for filled contours, false for line contours. + void follow_interior(ContourLine& contour_line, + QuadEdge& quad_edge, + unsigned int level_index, + const double& level, + bool want_initial_point, + const QuadEdge* start_quad_edge, + unsigned int start_level_index, + bool set_parents); + + // Return the index limits of a particular chunk. + void get_chunk_limits(long ijchunk, + long& ichunk, + long& jchunk, + long& istart, + long& iend, + long& jstart, + long& jend); + + // Check if a contour starts within the specified corner quad on the + // specified level_index, and if so return the start edge. Otherwise + // return Edge_None. + Edge get_corner_start_edge(long quad, unsigned int level_index) const; + + // Return index of point at start or end of specified QuadEdge, assuming + // anticlockwise ordering around non-masked quads. + long get_edge_point_index(const QuadEdge& quad_edge, bool start) const; + + // Return the edge to exit a quad from, given the specified entry quad_edge + // and direction to move in. + Edge get_exit_edge(const QuadEdge& quad_edge, Dir dir) const; + + // Return the (x,y) coordinates of the specified point index. + XY get_point_xy(long point) const; + + // Return the z-value of the specified point index. + const double& get_point_z(long point) const; + + // Check if a contour starts within the specified non-corner quad on the + // specified level_index, and if so return the start edge. Otherwise + // return Edge_None. + Edge get_quad_start_edge(long quad, unsigned int level_index) const; + + // Check if a contour starts within the specified quad, whether it is a + // corner or a full quad, and if so return the start edge. Otherwise + // return Edge_None. + Edge get_start_edge(long quad, unsigned int level_index) const; + + // Initialise the cache to contain grid information that is constant + // across the lifetime of this object, i.e. does not vary between calls to + // create_contour() and create_filled_contour(). + void init_cache_grid(const MaskArray& mask); + + // Initialise the cache with information that is specific to contouring the + // specified two levels. The levels are the same for contour lines, + // different for filled contours. + void init_cache_levels(const double& lower_level, + const double& upper_level); + + // Return the (x,y) point at which the level intersects the line connecting + // the two specified point indices. + XY interp(long point1, long point2, const double& level) const; + + // Return true if the specified QuadEdge is a boundary, i.e. is either an + // edge between a masked and non-masked quad/corner or is a chunk boundary. + bool is_edge_a_boundary(const QuadEdge& quad_edge) const; + + // Follow a boundary from one QuadEdge to the next in an anticlockwise + // manner around the non-masked region. + void move_to_next_boundary_edge(QuadEdge& quad_edge) const; + + // Move from the quad specified by quad_edge.quad to the neighbouring quad + // by crossing the edge specified by quad_edge.edge. + void move_to_next_quad(QuadEdge& quad_edge) const; + + // Check for filled contours starting within the specified quad and + // complete any that are found, appending them to the specified Contour. + void single_quad_filled(Contour& contour, + long quad, + const double& lower_level, + const double& upper_level); + + // Start and complete a filled contour line. + // quad: index of quad to start ContourLine in. + // edge: edge of quad to start ContourLine from. + // start_level_index: the level_index that the ContourLine starts from. + // hole_or_not: whether the ContourLine is a hole or not. + // boundary_or_interior: whether the ContourLine starts on a boundary or + // the interior. + // lower_level: lower contour z-value. + // upper_level: upper contour z-value. + // Returns newly created ContourLine. + ContourLine* start_filled(long quad, + Edge edge, + unsigned int start_level_index, + HoleOrNot hole_or_not, + BoundaryOrInterior boundary_or_interior, + const double& lower_level, + const double& upper_level); + + // Start and complete a line contour that both starts and end on a + // boundary, traversing the interior of the domain. + // vertices_list: Python list that the ContourLine should be appended to. + // quad: index of quad to start ContourLine in. + // edge: boundary edge to start ContourLine from. + // level: contour z-value. + // Returns true if the start quad does not need to be visited again, i.e. + // VISITED(quad,1). + bool start_line(PyObject* vertices_list, + long quad, + Edge edge, + const double& level); + + // Debug function that writes the cache status to stdout. + void write_cache(bool grid_only = false) const; + + // Debug function that writes that cache status for a single quad to + // stdout. + void write_cache_quad(long quad, bool grid_only) const; + + + + // Note that mask is not stored as once it has been used to initialise the + // cache it is no longer needed. + CoordinateArray _x, _y, _z; + long _nx, _ny; // Number of points in each direction. + long _n; // Total number of points (and hence quads). + + bool _corner_mask; + long _chunk_size; // Number of quads per chunk (not points). + // Always > 0, unlike python nchunk which is 0 + // for no chunking. + + long _nxchunk, _nychunk; // Number of chunks in each direction. + long _chunk_count; // Total number of chunks. + + typedef uint32_t CacheItem; + CacheItem* _cache; + + ParentCache _parent_cache; // On W quad sides. +}; + +#endif // _CONTOUR_H diff --git a/src/_contour_wrapper.cpp b/src/_contour_wrapper.cpp new file mode 100644 index 000000000000..775e34f05938 --- /dev/null +++ b/src/_contour_wrapper.cpp @@ -0,0 +1,196 @@ +#include "src/_contour.h" +#include "src/mplutils.h" +#include "src/py_exceptions.h" + +/* QuadContourGenerator */ + +typedef struct +{ + PyObject_HEAD; + QuadContourGenerator* ptr; +} PyQuadContourGenerator; + +static PyTypeObject PyQuadContourGeneratorType; + +static PyObject* PyQuadContourGenerator_new(PyTypeObject* type, PyObject* args, PyObject* kwds) +{ + PyQuadContourGenerator* self; + self = (PyQuadContourGenerator*)type->tp_alloc(type, 0); + self->ptr = NULL; + return (PyObject*)self; +} + +const char* PyQuadContourGenerator_init__doc__ = + "QuadContourGenerator(x, y, z, mask, corner_mask, chunk_size)\n" + "\n" + "Create a new C++ QuadContourGenerator object\n"; + +static int PyQuadContourGenerator_init(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) +{ + QuadContourGenerator::CoordinateArray x, y, z; + QuadContourGenerator::MaskArray mask; + bool corner_mask; + long chunk_size; + + if (!PyArg_ParseTuple(args, "O&O&O&O&il", + &x.converter_contiguous, &x, + &y.converter_contiguous, &y, + &z.converter_contiguous, &z, + &mask.converter_contiguous, &mask, + &corner_mask, + &chunk_size)) { + return -1; + } + + if (x.empty() || y.empty() || z.empty() || + y.dim(0) != x.dim(0) || z.dim(0) != x.dim(0) || + y.dim(1) != x.dim(1) || z.dim(1) != x.dim(1)) { + PyErr_SetString(PyExc_ValueError, + "x, y and z must all be 2D arrays with the same dimensions"); + } + + // Mask array is optional, if set must be same size as other arrays. + if (!mask.empty() && (mask.dim(0) != x.dim(0) || mask.dim(1) != x.dim(1))) { + PyErr_SetString(PyExc_ValueError, + "If mask is set it must be a 2D array with the same dimensions as x."); + } + + CALL_CPP_INIT("QuadContourGenerator", + (self->ptr = new QuadContourGenerator( + x, y, z, mask, corner_mask, chunk_size))); + return 0; +} + +static void PyQuadContourGenerator_dealloc(PyQuadContourGenerator* self) +{ + delete self->ptr; + Py_TYPE(self)->tp_free((PyObject *)self); +} + +const char* PyQuadContourGenerator_create_contour__doc__ = + "create_contour(level)\n" + "\n" + "Create and return a non-filled contour."; + +static PyObject* PyQuadContourGenerator_create_contour(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) +{ + double level; + if (!PyArg_ParseTuple(args, "d:create_contour", &level)) { + return NULL; + } + + PyObject* result; + CALL_CPP("create_contour", (result = self->ptr->create_contour(level))); + return result; +} + +const char* PyQuadContourGenerator_create_filled_contour__doc__ = + "create_filled_contour(lower_level, upper_level)\n" + "\n" + "Create and return a filled contour"; + +static PyObject* PyQuadContourGenerator_create_filled_contour(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) +{ + double lower_level, upper_level; + if (!PyArg_ParseTuple(args, "dd:create_filled_contour", + &lower_level, &upper_level)) { + return NULL; + } + + PyObject* result; + CALL_CPP("create_filled_contour", + (result = self->ptr->create_filled_contour(lower_level, + upper_level))); + return result; +} + +static PyTypeObject* PyQuadContourGenerator_init_type(PyObject* m, PyTypeObject* type) +{ + static PyMethodDef methods[] = { + {"create_contour", (PyCFunction)PyQuadContourGenerator_create_contour, METH_VARARGS, PyQuadContourGenerator_create_contour__doc__}, + {"create_filled_contour", (PyCFunction)PyQuadContourGenerator_create_filled_contour, METH_VARARGS, PyQuadContourGenerator_create_filled_contour__doc__}, + {NULL} + }; + + memset(type, 0, sizeof(PyTypeObject)); + type->tp_name = "matplotlib.QuadContourGenerator"; + type->tp_doc = PyQuadContourGenerator_init__doc__; + type->tp_basicsize = sizeof(PyQuadContourGenerator); + type->tp_dealloc = (destructor)PyQuadContourGenerator_dealloc; + type->tp_flags = Py_TPFLAGS_DEFAULT; + type->tp_methods = methods; + type->tp_new = PyQuadContourGenerator_new; + type->tp_init = (initproc)PyQuadContourGenerator_init; + + if (PyType_Ready(type) < 0) { + return NULL; + } + + if (PyModule_AddObject(m, "QuadContourGenerator", (PyObject*)type)) { + return NULL; + } + + return type; +} + + +/* Module */ + +extern "C" { + +struct module_state +{ +/* The Sun compiler can't handle empty structs */ +#if defined(__SUNPRO_C) || defined(_MSC_VER) + int _dummy; +#endif +}; + +#if PY3K +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_contour", + NULL, + sizeof(struct module_state), + NULL, + NULL, + NULL, + NULL, + NULL +}; + +#define INITERROR return NULL + +PyMODINIT_FUNC PyInit__contour(void) + +#else +#define INITERROR return + +PyMODINIT_FUNC init_contour(void) +#endif + +{ + PyObject *m; + +#if PY3K + m = PyModule_Create(&moduledef); +#else + m = Py_InitModule3("_contour", NULL, NULL); +#endif + + if (m == NULL) { + INITERROR; + } + + if (!PyQuadContourGenerator_init_type(m, &PyQuadContourGeneratorType)) { + INITERROR; + } + + import_array(); + +#if PY3K + return m; +#endif +} + +} // extern "C"