# (C) British Crown Copyright 2010 - 2013, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris.  If not, see <http://www.gnu.org/licenses/>.


# import iris tests first so that some things can be initialised before
# importing anything else
import iris.tests as tests

from functools import wraps
import types
import warnings

import matplotlib.pyplot as plt
import numpy as np

import iris
import iris.coords as coords
import iris.plot as iplt
import iris.quickplot as qplt
import iris.symbols
import iris.tests.stock
import iris.tests.test_mapping as test_mapping


def simple_cube():
    cube = iris.tests.stock.realistic_4d()
    cube = cube[:, 0, 0, :]
    cube.coord('time').guess_bounds()
    return cube


class TestSimple(tests.GraphicsTest):
    def test_points(self):
        cube = simple_cube()
        qplt.contourf(cube)
        self.check_graphic()

    def test_bounds(self):
        cube = simple_cube()
        qplt.pcolor(cube)
        self.check_graphic()


class TestMissingCoord(tests.GraphicsTest):
    def _check(self, cube):
        qplt.contourf(cube)
        self.check_graphic()

        qplt.pcolor(cube)
        self.check_graphic()

    def test_no_u(self):
        cube = simple_cube()
        cube.remove_coord('grid_longitude')
        self._check(cube)

    def test_no_v(self):
        cube = simple_cube()
        cube.remove_coord('time')
        self._check(cube)

    def test_none(self):
        cube = simple_cube()
        cube.remove_coord('grid_longitude')
        cube.remove_coord('time')
        self._check(cube)


@iris.tests.skip_data
class TestMissingCS(tests.GraphicsTest):
    @iris.tests.skip_data
    def test_missing_cs(self):
        cube = tests.stock.simple_pp()
        cube.coord("latitude").coord_system = None
        cube.coord("longitude").coord_system = None
        qplt.contourf(cube)
        qplt.plt.gca().coastlines()
        self.check_graphic()


class TestHybridHeight(tests.GraphicsTest):
    def setUp(self):
        self.cube = iris.tests.stock.realistic_4d()[0, :15, 0, :]

    def _check(self, plt_method, test_altitude=True):
        plt_method(self.cube)
        self.check_graphic()

        plt_method(self.cube, coords=['level_height', 'grid_longitude'])
        self.check_graphic()

        plt_method(self.cube, coords=['grid_longitude', 'level_height'])
        self.check_graphic()

        if test_altitude:
            plt_method(self.cube, coords=['grid_longitude', 'altitude'])
            self.check_graphic()

            plt_method(self.cube, coords=['altitude', 'grid_longitude'])
            self.check_graphic()

    def test_points(self):
        self._check(qplt.contourf)

    def test_bounds(self):
        self._check(qplt.pcolor, test_altitude=False)

    def test_orography(self):
        qplt.contourf(self.cube)
        iplt.orography_at_points(self.cube)
        iplt.points(self.cube)
        self.check_graphic()

        coords = ['altitude', 'grid_longitude']
        qplt.contourf(self.cube, coords=coords)
        iplt.orography_at_points(self.cube, coords=coords)
        iplt.points(self.cube, coords=coords)
        self.check_graphic()

        # TODO: Test bounds once they are supported.
        with self.assertRaises(NotImplementedError):
            qplt.pcolor(self.cube)
            iplt.orography_at_bounds(self.cube)
            iplt.outline(self.cube)
            self.check_graphic()


class Test1dPlotMultiArgs(tests.GraphicsTest):
    # tests for iris.plot using multi-argument calling convention

    def setUp(self):
        self.cube1d = _load_4d_testcube()[0, :, 0, 0]
        self.draw_method = iplt.plot

    def test_cube(self):
        # just plot a cube against its dim coord
        self.draw_method(self.cube1d)   # altitude vs temp
        self.check_graphic()

    def test_coord(self):
        # plot the altitude coordinate
        self.draw_method(self.cube1d.coord('altitude'))
        self.check_graphic()

    def test_coord_cube(self):
        # plot temperature against sigma
        self.draw_method(self.cube1d.coord('sigma'), self.cube1d)
        self.check_graphic()

    def test_cube_coord(self):
        # plot a vertical profile of temperature
        self.draw_method(self.cube1d, self.cube1d.coord('altitude'))
        self.check_graphic()

    def test_coord_coord(self):
        # plot two coordinates that are not mappable
        self.draw_method(self.cube1d.coord('sigma'),
                         self.cube1d.coord('altitude'))
        self.check_graphic()

    def test_coord_coord_map(self):
        # plot lat-lon aux coordinates of a trajectory, which draws a map
        lon = iris.coords.AuxCoord([0, 5, 10, 15, 20, 25, 30, 35, 40, 45],
                                   standard_name='longitude',
                                   units='degrees_north')
        lat = iris.coords.AuxCoord([45, 55, 50, 60, 55, 65, 60, 70, 65, 75],
                                   standard_name='latitude',
                                   units='degrees_north')
        self.draw_method(lon, lat)
        plt.gca().coastlines()
        self.check_graphic()

    def test_cube_cube(self):
        # plot two phenomena against eachother, in this case just dummy data
        cube1 = self.cube1d.copy()
        cube2 = self.cube1d.copy()
        cube1.rename('some phenomenon')
        cube2.rename('some other phenomenon')
        cube1.units = iris.unit.Unit('no_unit')
        cube2.units = iris.unit.Unit('no_unit')
        cube1.data[:] = np.linspace(0, 1, 7)
        cube2.data[:] = np.exp(cube1.data)
        self.draw_method(cube1, cube2)
        self.check_graphic()

    def test_incompatible_objects(self):
        # incompatible objects (not the same length) should raise an error
        with self.assertRaises(ValueError):
            self.draw_method(self.cube1d.coord('time'), (self.cube1d))

    def test_multimidmensional(self):
        # multidimensional cubes are not allowed
        cube = _load_4d_testcube()[0, :, :, 0]
        with self.assertRaises(ValueError):
            self.draw_method(cube)

    def test_not_cube_or_coord(self):
        # inputs must be cubes or coordinates, otherwise an error should be
        # raised
        xdim = np.arange(self.cube1d.shape[0])
        with self.assertRaises(TypeError):
            self.draw_method(xdim, self.cube1d)

    def test_coords_deprecated(self):
        # ensure a warning is raised if the old coords keyword argument is
        # used, and make sure the plot produced is consistent with the old
        # interface
        msg = 'Missing deprecation warning for coords keyword.'
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter('always')
            self.draw_method(self.cube1d, coords=['sigma'])
            self.assertEqual(len(w), 1, msg)
        self.check_graphic()

    def test_coords_deprecation_too_many(self):
        # in deprecation mode, too many coords is an error
        with self.assertRaises(ValueError):
            self.draw_method(self.cube1d, coords=['sigma', 'sigma'])

    def test_coords_deprecation_invalid_span(self):
        # in deprecation mode, a coordinate that doesn't span data is an error
        with self.assertRaises(ValueError):
            self.draw_method(self.cube1d, coords=['time'])


class Test1dQuickplotPlotMultiArgs(Test1dPlotMultiArgs):
    # tests for iris.plot using multi-argument calling convention

    def setUp(self):
        self.cube1d = _load_4d_testcube()[0, :, 0, 0]
        self.draw_method = qplt.plot


@tests.skip_data
class Test1dScatter(tests.GraphicsTest):

    def setUp(self):
        self.cube = iris.load_cube(
            tests.get_data_path(('NAME', 'NAMEIII_trajectory.txt')),
            'Temperature')
        self.draw_method = iplt.scatter

    def test_coord_coord(self):
        x = self.cube.coord('longitude')
        y = self.cube.coord('height')
        c = self.cube.data
        self.draw_method(x, y, c=c, edgecolor='none')
        self.check_graphic()

    def test_coord_coord_map(self):
        x = self.cube.coord('longitude')
        y = self.cube.coord('latitude')
        c = self.cube.data
        self.draw_method(x, y, c=c, edgecolor='none')
        plt.gca().coastlines()
        self.check_graphic()

    def test_coord_cube(self):
        x = self.cube.coord('latitude')
        y = self.cube
        c = self.cube.coord('Travel Time').points
        self.draw_method(x, y, c=c, edgecolor='none')
        self.check_graphic()

    def test_cube_coord(self):
        x = self.cube
        y = self.cube.coord('height')
        c = self.cube.coord('Travel Time').points
        self.draw_method(x, y, c=c, edgecolor='none')
        self.check_graphic()

    def test_cube_cube(self):
        x = iris.load_cube(
            tests.get_data_path(('NAME', 'NAMEIII_trajectory.txt')),
            'Rel Humidity')
        y = self.cube
        c = self.cube.coord('Travel Time').points
        self.draw_method(x, y, c=c, edgecolor='none')
        self.check_graphic()

    def test_incompatible_objects(self):
        # cubes/coordinates of different sizes cannot be plotted
        x = self.cube
        y = self.cube.coord('height')[:-1]
        with self.assertRaises(ValueError):
            self.draw_method(x, y)

    def test_multidimensional(self):
        # multidimensional cubes/coordinates are not allowed
        x = _load_4d_testcube()[0, :, :, 0]
        y = x.coord('model_level_number')
        with self.assertRaises(ValueError):
            self.draw_method(x, y)

    def test_not_cube_or_coord(self):
        # inputs must be cubes or coordinates
        x = np.arange(self.cube.shape[0])
        y = self.cube
        with self.assertRaises(TypeError):
            self.draw_method(x, y)


@tests.skip_data
class Test1dQuickplotScatter(Test1dScatter):

    def setUp(self):
        self.cube = iris.load_cube(
            tests.get_data_path(('NAME', 'NAMEIII_trajectory.txt')),
            'Temperature')
        self.draw_method = qplt.scatter


@iris.tests.skip_data
class TestAttributePositive(tests.GraphicsTest):
    def test_1d_positive_up(self):
        path = tests.get_data_path(('NetCDF', 'ORCA2', 'votemper.nc'))
        cube = iris.load_cube(path)
        qplt.plot(cube.coord('depth'), cube[0, :, 60, 80])
        self.check_graphic()

    def test_1d_positive_down(self):
        path = tests.get_data_path(('NetCDF', 'ORCA2', 'votemper.nc'))
        cube = iris.load_cube(path)
        qplt.plot(cube[0, :, 60, 80], cube.coord('depth'))
        self.check_graphic()

    def test_2d_positive_up(self):
        path = tests.get_data_path(('NetCDF', 'testing',
                                    'small_theta_colpex.nc'))
        cube = iris.load_cube(path)[0, :, 42, :]
        qplt.pcolormesh(cube)
        self.check_graphic()

    def test_2d_positive_down(self):
        path = tests.get_data_path(('NetCDF', 'ORCA2', 'votemper.nc'))
        cube = iris.load_cube(path)[0, :, 42, :]
        qplt.pcolormesh(cube)
        self.check_graphic()


# Caches _load_4d_testcube so subsequent calls are faster
def cache(fn, cache={}):
    def inner(*args, **kwargs):
        key = fn.__name__
        if key not in cache:
            cache[key] = fn(*args, **kwargs)
        return cache[key]
    return inner


@cache
def _load_4d_testcube():
    # Load example 4d data (TZYX).
    test_cube = iris.tests.stock.realistic_4d()
    # Replace forecast_period coord with a multi-valued version.
    time_coord = test_cube.coord('time')
    n_times = len(time_coord.points)
    forecast_dims = test_cube.coord_dims(time_coord)
    test_cube.remove_coord('forecast_period')
    # Make up values (including bounds), to roughly match older testdata.
    point_values = np.linspace((1 + 1.0 / 6), 2.0, n_times)
    point_uppers = point_values + (point_values[1] - point_values[0])
    bound_values = np.column_stack([point_values, point_uppers])
    # NOTE: this must be a DimCoord
    #  - an equivalent AuxCoord produces different plots.
    new_forecast_coord = iris.coords.DimCoord(
        points=point_values,
        bounds=bound_values,
        standard_name='forecast_period',
        units=iris.unit.Unit('hours')
    )
    test_cube.add_aux_coord(new_forecast_coord, forecast_dims)
    # Heavily reduce dimensions for faster testing.
    # NOTE: this makes ZYX non-contiguous.  Doesn't seem to matter for now.
    test_cube = test_cube[:, ::10, ::10, ::10]
    return test_cube


@cache
def _load_wind_no_bounds():
    # Load the COLPEX data => TZYX
    path = tests.get_data_path(('PP', 'COLPEX', 'small_eastward_wind.pp'))
    wind = iris.load_cube(path, 'eastward_wind')

    # Remove bounds from all coords that have them.
    wind.coord('grid_latitude').bounds = None
    wind.coord('grid_longitude').bounds = None
    wind.coord('level_height').bounds = None
    wind.coord('sigma').bounds = None

    return wind[:, :, :50, :50]


def _time_series(src_cube):
    # Until we have plotting support for multiple axes on the same dimension,
    # remove the time coordinate and its axis.
    cube = src_cube.copy()
    cube.remove_coord('time')
    return cube


def _date_series(src_cube):
    # Until we have plotting support for multiple axes on the same dimension,
    # remove the forecast_period coordinate and its axis.
    cube = src_cube.copy()
    cube.remove_coord('forecast_period')
    return cube


class SliceMixin(object):
    """Mixin class providing tests for each 2-dimensional permutation of axes.

    Requires self.draw_method to be the relevant plotting function,
    and self.results to be a dictionary containing the desired test results."""

    def test_yx(self):
        cube = self.wind[0, 0, :, :]
        self.draw_method(cube)
        self.check_graphic()

    def test_zx(self):
        cube = self.wind[0, :, 0, :]
        self.draw_method(cube)
        self.check_graphic()

    def test_tx(self):
        cube = _time_series(self.wind[:, 0, 0, :])
        self.draw_method(cube)
        self.check_graphic()

    def test_zy(self):
        cube = self.wind[0, :, :, 0]
        self.draw_method(cube)
        self.check_graphic()

    def test_ty(self):
        cube = _time_series(self.wind[:, 0, :, 0])
        self.draw_method(cube)
        self.check_graphic()

    def test_tz(self):
        cube = _time_series(self.wind[:, :, 0, 0])
        self.draw_method(cube)
        self.check_graphic()


@iris.tests.skip_data
class TestContour(tests.GraphicsTest, SliceMixin):
    """Test the iris.plot.contour routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = iplt.contour


@iris.tests.skip_data
class TestContourf(tests.GraphicsTest, SliceMixin):
    """Test the iris.plot.contourf routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = iplt.contourf


@iris.tests.skip_data
class TestPcolor(tests.GraphicsTest, SliceMixin):
    """Test the iris.plot.pcolor routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = iplt.pcolor


@iris.tests.skip_data
class TestPcolormesh(tests.GraphicsTest, SliceMixin):
    """Test the iris.plot.pcolormesh routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = iplt.pcolormesh


def check_warnings(method):
    """
    Decorator that adds a catch_warnings and filter to assert
    the method being decorated issues a UserWarning.

    """
    @wraps(method)
    def decorated_method(self, *args, **kwargs):
        # Force reset of iris.coords warnings registry to avoid suppression of
        # repeated warnings. warnings.resetwarnings() does not do this.
        if hasattr(coords, '__warningregistry__'):
            coords.__warningregistry__.clear()

        # Check that method raises warning.
        with warnings.catch_warnings():
            warnings.simplefilter("error")
            with self.assertRaises(UserWarning):
                return method(self, *args, **kwargs)
    return decorated_method


def ignore_warnings(method):
    """
    Decorator that adds a catch_warnings and filter to suppress
    any warnings issues by the method being decorated.

    """
    @wraps(method)
    def decorated_method(self, *args, **kwargs):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            return method(self, *args, **kwargs)
    return decorated_method


class CheckForWarningsMetaclass(type):
    """
    Metaclass that adds a further test for each base class test
    that checks that each test raises a UserWarning. Each base
    class test is then overriden to ignore warnings in order to
    check the underlying functionality.

    """
    def __new__(cls, name, bases, local):
        def add_decorated_methods(attr_dict, target_dict, decorator):
            for key, value in attr_dict.items():
                if (isinstance(value, types.FunctionType) and
                        key.startswith('test')):
                    new_key = '_'.join((key, decorator.__name__))
                    if new_key not in target_dict:
                        wrapped = decorator(value)
                        wrapped.__name__ = new_key
                        target_dict[new_key] = wrapped
                    else:
                        raise RuntimeError('A attribute called {!r} '
                                           'already exists.'.format(new_key))

        def override_with_decorated_methods(attr_dict, target_dict,
                                            decorator):
            for key, value in attr_dict.items():
                if (isinstance(value, types.FunctionType) and
                        key.startswith('test')):
                    target_dict[key] = decorator(value)

        # Add decorated versions of base methods
        # to check for warnings.
        for base in bases:
            add_decorated_methods(base.__dict__, local, check_warnings)

        # Override base methods to ignore warnings.
        for base in bases:
            override_with_decorated_methods(base.__dict__, local,
                                            ignore_warnings)

        return type.__new__(cls, name, bases, local)


@iris.tests.skip_data
class TestPcolorNoBounds(tests.GraphicsTest, SliceMixin):
    """
    Test the iris.plot.pcolor routine on a cube with coordinates
    that have no bounds.

    """
    __metaclass__ = CheckForWarningsMetaclass

    def setUp(self):
        self.wind = _load_wind_no_bounds()
        self.draw_method = iplt.pcolor


@iris.tests.skip_data
class TestPcolormeshNoBounds(tests.GraphicsTest, SliceMixin):
    """
    Test the iris.plot.pcolormesh routine on a cube with coordinates
    that have no bounds.

    """
    __metaclass__ = CheckForWarningsMetaclass

    def setUp(self):
        self.wind = _load_wind_no_bounds()
        self.draw_method = iplt.pcolormesh


class Slice1dMixin(object):
    """Mixin class providing tests for each 1-dimensional permutation of axes.

    Requires self.draw_method to be the relevant plotting function,
    and self.results to be a dictionary containing the desired test results."""

    def test_x(self):
        cube = self.wind[0, 0, 0, :]
        self.draw_method(cube)
        self.check_graphic()

    def test_y(self):
        cube = self.wind[0, 0, :, 0]
        self.draw_method(cube)
        self.check_graphic()

    def test_z(self):
        cube = self.wind[0, :, 0, 0]
        self.draw_method(cube)
        self.check_graphic()

    def test_t(self):
        cube = _time_series(self.wind[:, 0, 0, 0])
        self.draw_method(cube)
        self.check_graphic()

    def test_t_dates(self):
        cube = _date_series(self.wind[:, 0, 0, 0])
        self.draw_method(cube)
        plt.gcf().autofmt_xdate()
        plt.xlabel('Phenomenon time')

        self.check_graphic()


@iris.tests.skip_data
class TestPlot(tests.GraphicsTest, Slice1dMixin):
    """Test the iris.plot.plot routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = iplt.plot


@iris.tests.skip_data
class TestQuickplotPlot(tests.GraphicsTest, Slice1dMixin):
    """Test the iris.quickplot.plot routine."""
    def setUp(self):
        self.wind = _load_4d_testcube()
        self.draw_method = qplt.plot


_load_cube_once_cache = {}


def load_cube_once(filename, constraint):
    """Same syntax as load_cube, but will only load a file once,

    then cache the answer in a dictionary.

    """
    global _load_cube_once_cache
    key = (filename, str(constraint))
    cube = _load_cube_once_cache.get(key, None)

    if cube is None:
        cube = iris.load_cube(filename, constraint)
        _load_cube_once_cache[key] = cube

    return cube


class LambdaStr(object):
    """Provides a callable function which has a sensible __repr__."""
    def __init__(self, repr, lambda_fn):
        self.repr = repr
        self.lambda_fn = lambda_fn

    def __call__(self, *args, **kwargs):
        return self.lambda_fn(*args, **kwargs)

    def __repr__(self):
        return self.repr


@iris.tests.skip_data
class TestPlotCoordinatesGiven(tests.GraphicsTest):
    def setUp(self):
        filename = tests.get_data_path(('PP', 'COLPEX',
                                        'theta_and_orog_subset.pp'))
        self.cube = load_cube_once(filename, 'air_potential_temperature')

        self.draw_module = iris.plot
        self.contourf = LambdaStr('iris.plot.contourf',
                                  lambda cube, *args, **kwargs:
                                  iris.plot.contourf(cube, *args, **kwargs))
        self.contour = LambdaStr('iris.plot.contour',
                                 lambda cube, *args, **kwargs:
                                 iris.plot.contour(cube, *args, **kwargs))
        self.points = LambdaStr('iris.plot.points',
                                lambda cube, *args, **kwargs:
                                iris.plot.points(cube, c=cube.data,
                                                 *args, **kwargs))
        self.plot = LambdaStr('iris.plot.plot',
                              lambda cube, *args, **kwargs:
                              iris.plot.plot(cube, *args, **kwargs))

        self.results = {'yx': ([self.contourf, ['grid_latitude',
                                                'grid_longitude']],
                               [self.contourf, ['grid_longitude',
                                                'grid_latitude']],
                               [self.contour, ['grid_latitude',
                                               'grid_longitude']],
                               [self.contour, ['grid_longitude',
                                               'grid_latitude']],
                               [self.points, ['grid_latitude',
                                              'grid_longitude']],
                               [self.points, ['grid_longitude',
                                              'grid_latitude']],),
                        'zx': ([self.contourf, ['model_level_number',
                                                'grid_longitude']],
                               [self.contourf, ['grid_longitude',
                                                'model_level_number']],
                               [self.contour, ['model_level_number',
                                               'grid_longitude']],
                               [self.contour, ['grid_longitude',
                                               'model_level_number']],
                               [self.points, ['model_level_number',
                                              'grid_longitude']],
                               [self.points, ['grid_longitude',
                                              'model_level_number']],),
                        'tx': ([self.contourf, ['time', 'grid_longitude']],
                               [self.contourf, ['grid_longitude', 'time']],
                               [self.contour, ['time', 'grid_longitude']],
                               [self.contour, ['grid_longitude', 'time']],
                               [self.points, ['time', 'grid_longitude']],
                               [self.points, ['grid_longitude', 'time']],),
                        'x': ([self.plot, ['grid_longitude']],),
                        'y': ([self.plot, ['grid_latitude']],)
                        }

    def draw(self, draw_method, *args, **kwargs):
        draw_fn = getattr(self.draw_module, draw_method)
        draw_fn(*args, **kwargs)
        self.check_graphic()

    def run_tests(self, cube, results):
        for draw_method, coords in results:
            draw_method(cube, coords=coords)
            try:
                self.check_graphic()
            except AssertionError, err:
                self.fail('Draw method %r failed with coords: %r. '
                          'Assertion message: %s' % (draw_method, coords, err))

    def run_tests_1d(self, cube, results):
        # there is a different calling convention for 1d plots
        for draw_method, coords in results:
            draw_method(cube.coord(coords[0]), cube)
            try:
                self.check_graphic()
            except AssertionError as err:
                msg = 'Draw method {!r} failed with coords: {!r}. ' \
                      'Assertion message: {!s}'
                self.fail(msg.format(draw_method, coords, err))

    def test_yx(self):
        test_cube = self.cube[0, 0, :, :]
        self.run_tests(test_cube, self.results['yx'])

    def test_zx(self):
        test_cube = self.cube[0, :15, 0, :]
        self.run_tests(test_cube, self.results['zx'])

    def test_tx(self):
        test_cube = self.cube[:, 0, 0, :]
        self.run_tests(test_cube, self.results['tx'])

    def test_x(self):
        test_cube = self.cube[0, 0, 0, :]
        self.run_tests_1d(test_cube, self.results['x'])

    def test_y(self):
        test_cube = self.cube[0, 0, :, 0]
        self.run_tests_1d(test_cube, self.results['y'])

    def test_badcoords(self):
        cube = self.cube[0, 0, :, :]
        draw_fn = getattr(self.draw_module, 'contourf')
        self.assertRaises(ValueError, draw_fn, cube,
                          coords=['grid_longitude', 'grid_longitude'])
        self.assertRaises(ValueError, draw_fn, cube,
                          coords=['grid_longitude', 'grid_longitude',
                                  'grid_latitude'])
        self.assertRaises(iris.exceptions.CoordinateNotFoundError, draw_fn,
                          cube, coords=['grid_longitude', 'wibble'])
        self.assertRaises(ValueError, draw_fn, cube, coords=[])
        self.assertRaises(ValueError, draw_fn, cube,
                          coords=[cube.coord('grid_longitude'),
                                  cube.coord('grid_longitude')])
        self.assertRaises(ValueError, draw_fn, cube,
                          coords=[cube.coord('grid_longitude'),
                                  cube.coord('grid_longitude'),
                                  cube.coord('grid_longitude')])

    def test_non_cube_coordinate(self):
        cube = self.cube[0, :, :, 0]
        pts = -100 + np.arange(cube.shape[1]) * 13
        x = coords.DimCoord(pts, standard_name='model_level_number',
                            attributes={'positive': 'up'})
        self.draw('contourf', cube, coords=['grid_latitude', x])


@iris.tests.skip_data
class TestPlotDimAndAuxCoordsKwarg(tests.GraphicsTest):
    def setUp(self):
        filename = tests.get_data_path(('NetCDF', 'rotated', 'xy',
                                        'rotPole_landAreaFraction.nc'))
        self.cube = iris.load_cube(filename)

    def test_default(self):
        iplt.contourf(self.cube)
        plt.gca().coastlines()
        self.check_graphic()

    def test_coords(self):
        # Pass in dimension coords.
        rlat = self.cube.coord('grid_latitude')
        rlon = self.cube.coord('grid_longitude')
        iplt.contourf(self.cube, coords=[rlon, rlat])
        plt.gca().coastlines()
        self.check_graphic()
        # Pass in auxiliary coords.
        lat = self.cube.coord('latitude')
        lon = self.cube.coord('longitude')
        iplt.contourf(self.cube, coords=[lon, lat])
        plt.gca().coastlines()
        self.check_graphic()

    def test_coord_names(self):
        # Pass in names of dimension coords.
        iplt.contourf(self.cube, coords=['grid_longitude', 'grid_latitude'])
        plt.gca().coastlines()
        self.check_graphic()
        # Pass in names of auxiliary coords.
        iplt.contourf(self.cube, coords=['longitude', 'latitude'])
        plt.gca().coastlines()
        self.check_graphic()

    def test_yx_order(self):
        # Do not attempt to draw coastlines as it is not a map.
        iplt.contourf(self.cube, coords=['grid_latitude', 'grid_longitude'])
        self.check_graphic()
        iplt.contourf(self.cube, coords=['latitude', 'longitude'])
        self.check_graphic()


class TestSymbols(tests.GraphicsTest):
    def test_cloud_cover(self):
        iplt.symbols(range(10), [0] * 10, [iris.symbols.CLOUD_COVER[i]
                                           for i in range(10)], 0.375)
        self.check_graphic()


class TestPlottingExceptions(tests.IrisTest):
    def setUp(self):
        self.bounded_cube = tests.stock.lat_lon_cube()
        self.bounded_cube.coord("latitude").guess_bounds()
        self.bounded_cube.coord("longitude").guess_bounds()

    def test_boundmode_multidim(self):
        # Test exception translation.
        # We can't get contiguous bounded grids from multi-d coords.
        cube = self.bounded_cube
        cube.remove_coord("latitude")
        cube.add_aux_coord(coords.AuxCoord(points=cube.data,
                                           standard_name='latitude',
                                           units='degrees'), [0, 1])
        with self.assertRaises(ValueError):
            iplt.pcolormesh(cube, coords=['longitude', 'latitude'])

    def test_boundmode_4bounds(self):
        # Test exception translation.
        # We can only get contiguous bounded grids with 2 bounds per point.
        cube = self.bounded_cube
        lat = coords.AuxCoord.from_coord(cube.coord("latitude"))
        lat.bounds = np.array([lat.points, lat.points + 1,
                               lat.points + 2, lat.points + 3]).transpose()
        cube.remove_coord("latitude")
        cube.add_aux_coord(lat, 0)
        with self.assertRaises(ValueError):
            iplt.pcolormesh(cube, coords=['longitude', 'latitude'])

    def test_different_coord_systems(self):
        cube = self.bounded_cube
        lat = cube.coord('latitude')
        lon = cube.coord('longitude')
        lat.coord_system = iris.coord_systems.GeogCS(7000000)
        lon.coord_system = iris.coord_systems.GeogCS(7000001)
        with self.assertRaises(ValueError):
            iplt.pcolormesh(cube, coords=['longitude', 'latitude'])


@iris.tests.skip_data
class TestPlotOtherCoordSystems(tests.GraphicsTest):
    def test_plot_tmerc(self):
        filename = tests.get_data_path(('NetCDF', 'transverse_mercator',
                                        'tmean_1910_1910.nc'))
        self.cube = iris.load_cube(filename)
        iplt.pcolormesh(self.cube[0])
        plt.gca().coastlines()
        self.check_graphic()


if __name__ == "__main__":
    tests.main()
