#  Copyright (c) 2018,2019 MetPy Developers.
#  Distributed under the terms of the BSD 3-Clause License.
#  SPDX-License-Identifier: BSD-3-Clause
"""Declarative plotting tools."""
from datetime import datetime
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
from traitlets import (Any, Bool, Float, HasTraits, Instance, Int, List, observe, Tuple,
                       Unicode, Union)
from . import cartopy_utils, ctables
from ..cbook import is_string_like
from ..package_tools import Exporter
from ..units import units
exporter = Exporter(globals())
_projections = {'lcc': ccrs.LambertConformal(central_latitude=40, central_longitude=-100,
                                             standard_parallels=[30, 60]),
                'ps': ccrs.NorthPolarStereo(central_longitude=-100),
                'mer': ccrs.Mercator()}
_areas = {
    '105': (-129.3, -22.37, 17.52, 53.78),
    'local': (-92., -64., 28.5, 48.5),
    'wvaac': (120.86, -15.07, -53.6, 89.74),
    'tropsfc': (-100., -55., 8., 33.),
    'epacsfc': (-155., -75., -20., 33.),
    'ofagx': (-100., -80., 20., 35.),
    'ahsf': (-105., -30., -5., 35.),
    'ehsf': (-145., -75., -5., 35.),
    'shsf': (-125., -75., -20., 5.),
    'tropful': (-160., 0., -20., 50.),
    'tropatl': (-115., 10., 0., 40.),
    'subtrop': (-90., -20., 20., 60.),
    'troppac': (-165., -80., -25., 45.),
    'gulf': (-105., -70., 10., 40.),
    'carib': (-100., -50., 0., 40.),
    'sthepac': (-170., -70., -60., 0.),
    'opcahsf': (-102., -20., 0., 45.),
    'opcphsf': (175., -70., -28., 45.),
    'wwe': (-106., -50., 18., 54.),
    'world': (-24., -24., -90., 90.),
    'nwwrd1': (-180., 180., -90., 90.),
    'nwwrd2': (0., 0., -90., 90.),
    'afna': (-135.02, -23.04, 10.43, 40.31),
    'awna': (-141.03, -18.58, 7.84, 35.62),
    'medr': (-178., -25., -15., 5.),
    'pacsfc': (129., -95., -5., 18.),
    'saudi': (4.6, 92.5, -13.2, 60.3),
    'natlmed': (-30., 70., 0., 65.),
    'ncna': (-135.5, -19.25, 8., 37.7),
    'ncna2': (-133.5, -20.5, 10., 42.),
    'hpcsfc': (-124., -26., 15., 53.),
    'atlhur': (-96., -6., 4., 3.),
    'nam': (-134., 3., -4., 39.),
    'sam': (-120., -20., -60., 20.),
    'samps': (-148., -36., -28., 12.),
    'eur': (-16., 80., 24., 52.),
    'afnh': (-155.19, 18.76, -6.8, -3.58),
    'awnh': (-158.94, 15.35, -11.55, -8.98),
    'wwwus': (-127.7, -59., 19.8, 56.6),
    'ccfp': (-130., -65., 22., 52.),
    'llvl': (-119.6, -59.5, 19.9, 44.5),
    'llvl2': (-125., -32.5, 5., 46.),
    'llvl_e': (-89., -59.5, 23.5, 44.5),
    'llvl_c': (-102.4, -81.25, 23.8, 51.6),
    'llvl_w': (-119.8, -106.5, 19.75, 52.8),
    'ak_artc': (163.7, -65.3, 17.5, 52.6),
    'fxpswna': (-80.5, 135., -1., 79.),
    'fxpsnna': (-80.5, 54., -1., 25.5),
    'fxpsna': (-72.6, 31.4, -3.6, 31.),
    'natl_ps': (-80.5, 54., -1., 25.5),
    'fxpsena': (-45., 54., 11., 25.5),
    'fxpsnp': (155.5, -106.5, 22.5, 47.),
    'npac_ps': (155.5, -106.5, 22.5, 47.),
    'fxpsus': (-120., -59., 20., 44.5),
    'fxmrwrd': (58., 58., -70., 70.),
    'fxmrwr2': (-131., -131., -70., 70.),
    'nwmrwrd': (70., 70., -70., 70.),
    'wrld_mr': (58., 58., -70., 70.),
    'fxmr110': (-180., -110., -20., 50.5),
    'fxmr180': (110., -180., -20., 50.5),
    'fxmrswp': (97.5, -147.5, -36., 45.5),
    'fxmrus': (-162.5, -37.5, -28., 51.2),
    'fxmrea': (-40., 20., -20., 54.2),
    'fxmrjp': (100., -160., 0., 45.),
    'icao_a': (-137.4, -12.6, -54., 67.),
    'icao_b': (-52.5, -16., -62.5, 77.5),
    'icao_b1': (-125., 40., -45.5, 62.7),
    'icao_c': (-35., 70., -45., 75.),
    'icao_d': (-15., 132., -27., 63.),
    'icao_e': (25., 180., -54., 40.),
    'icao_f': (100., -110., -52.7, 50.),
    'icao_g': (34.8, 157.2, -0.8, 13.7),
    'icao_h': (-79.1, 56.7, 1.6, 25.2),
    'icao_i': (166.24, -60.62, -6.74, 33.32),
    'icao_j': (106.8, -101.1, -27.6, 0.8),
    'icao_k': (3.3, 129.1, -11.1, 6.7),
    'icao_m': (100., -110., -10., 70.),
    'icao_eu': (-21.6, 68.4, 21.4, 58.7),
    'icao_me': (17., 70., 10., 44.),
    'icao_as': (53., 108., 00., 36.),
    'icao_na': (-54.1, 60.3, 17.2, 50.7),
    'nhem': (-135., 45., -15., -15.),
    'nhem_ps': (-135., 45., -15., -15.),
    'nhem180': (135., -45., -15., -15.),
    'nhem155': (160., -20., -15., -15.),
    'nhem165': (150., -30., -15., -15.),
    'nh45_ps': (-90., 90., -15., -15.),
    'nhem0': (-45., 135., -15., -15.),
    'shem_ps': (88., -92., 30., 30.),
    'hfo_gu': (160., -130., -30., 40.),
    'natl': (-110., 20.1, 15., 70.),
    'watl': (-84., -38., 25., 46.),
    'tatl': (-90., -15., -10., 35.),
    'npac': (102., -110., -12., 60.),
    'spac': (102., -70., -60., 20.),
    'tpac': (-165., -75., -10., 40.),
    'epac': (-134., -110., 12., 75.),
    'wpac': (130., -120., 0., 63.),
    'mpac': (128., -108., 15., 71.95),
    'opcsfp': (128.89, -105.3, 3.37, 16.77),
    'opcsfa': (-55.5, 75., -8.5, 52.6),
    'opchur': (-99., -15., 1., 50.05),
    'us': (-119., -56., 19., 47.),
    'spcus': (-116.4, -63.9, 22.1, 47.2),
    'afus': (-119.04, -63.44, 23.1, 44.63),
    'ncus': (-124.2, -40.98, 17.89, 47.39),
    'nwus': (-118., -55.5, 17., 46.5),
    'awips': (-127., -59., 20., 50.),
    'bwus': (-124.6, -46.7, 13.1, 43.1),
    'usa': (-118., -62., 22.8, 45.),
    'usnps': (-118., -62., 18., 51.),
    'uslcc': (-118., -62., 20., 51.),
    'uswn': (-129., -45., 17., 53.),
    'ussf': (-123.5, -44.5, 13., 32.1),
    'ussp': (-126., -49., 13., 54.),
    'whlf': (-123.8, -85.9, 22.9, 50.2),
    'chlf': (-111., -79., 27.5, 50.5),
    'centus': (-105.4, -77., 24.7, 47.6),
    'ehlf': (-96.2, -62.7, 22., 49.),
    'mehlf': (-89.9, -66.6, 23.8, 49.1),
    'bosfa': (-87.5, -63.5, 34.5, 50.5),
    'miafa': (-88., -72., 23., 39.),
    'chifa': (-108., -75., 34., 50.),
    'dfwfa': (-106.5, -80.5, 22., 40.),
    'slcfa': (-126., -98., 29.5, 50.5),
    'sfofa': (-129., -111., 30., 50.),
    'g8us': (-116., -58., 19., 56.),
    'wsig': (155., -115., 18., 58.),
    'esig': (-80., -30., 25., 51.),
    'eg8': (-79., -13., 24., 52.),
    'west': (-125., -90., 25., 55.),
    'cent': (-107.4, -75.3, 24.3, 49.7),
    'east': (-100.55, -65.42, 24.57, 47.2),
    'nwse': (-126., -102., 38.25, 50.25),
    'swse': (-126., -100., 28.25, 40.25),
    'ncse': (-108., -84., 38.25, 50.25),
    'scse': (-108.9, -84., 24., 40.25),
    'nese': (-89., -64., 37.25, 47.25),
    'sese': (-90., -66., 28.25, 40.25),
    'afwh': (170.7, 15.4, -48.6, 69.4),
    'afeh': (-9.3, -164.6, -48.6, 69.4),
    'afpc': (80.7, -74.6, -48.6, 69.4),
    'ak': (-179., -116.4, 49., 69.),
    'ak2': (-180., -106., 42., 73.),
    'nwak': (-180., -110., 50., 60.),
    'al': (-95., -79., 27., 38.),
    'ar': (-100.75, -84.75, 29.5, 40.5),
    'ca': (-127.75, -111.75, 31.5, 42.5),
    'co': (-114., -98., 33.5, 44.5),
    'ct': (-81.25, -65.25, 36., 47.),
    'dc': (-85., -69., 33.35, 44.35),
    'de': (-83.75, -67.75, 33.25, 44.25),
    'fl': (-90., -74., 23., 34.),
    'ga': (-92., -76., 27.5, 38.5),
    'hi': (-161.5, -152.5, 17., 23.),
    'nwxhi': (-166., -148., 14., 26.),
    'ia': (-102., -86., 36.5, 47.5),
    'id': (-123., -107., 39.25, 50.25),
    'il': (-97.75, -81.75, 34.5, 45.5),
    'in': (-94.5, -78.5, 34.5, 45.5),
    'ks': (-106.5, -90.5, 33.25, 44.25),
    'ky': (-93., -77., 31.75, 42.75),
    'la': (-100.75, -84.75, 25.75, 36.75),
    'ma': (-80.25, -64.25, 36.75, 47.75),
    'md': (-85.25, -69.25, 33.75, 44.75),
    'me': (-77.75, -61.75, 39.5, 50.5),
    'mi': (-93., -77., 37.75, 48.75),
    'mn': (-102., -86., 40.5, 51.5),
    'mo': (-101., -85., 33., 44.),
    'ms': (-98., -82., 27., 38.),
    'mt': (-117., -101., 41.5, 52.5),
    'nc': (-87.25, -71.25, 30., 41.),
    'nd': (-107.5, -91.5, 42.25, 53.25),
    'ne': (-107.5, -91.5, 36.25, 47.25),
    'nh': (-79.5, -63.5, 38.25, 49.25),
    'nj': (-82.5, -66.5, 34.75, 45.75),
    'nm': (-114.25, -98.25, 29., 40.),
    'nv': (-125., -109., 34., 45.),
    'ny': (-84., -68., 37.25, 48.25),
    'oh': (-91., -75., 34.5, 45.5),
    'ok': (-105.25, -89.25, 30.25, 41.25),
    'or': (-128., -112., 38.75, 49.75),
    'pa': (-86., -70., 35.5, 46.5),
    'ri': (-79.75, -63.75, 36., 47.),
    'sc': (-89., -73., 28.5, 39.5),
    'sd': (-107.5, -91.5, 39., 50.),
    'tn': (-95., -79., 30., 41.),
    'tx': (-107., -91., 25.4, 36.5),
    'ut': (-119., -103., 34., 45.),
    'va': (-86.5, -70.5, 32.25, 43.25),
    'vt': (-80.75, -64.75, 38.25, 49.25),
    'wi': (-98., -82., 38.5, 49.5),
    'wv': (-89., -73., 33., 44.),
    'wy': (-116., -100., 37.75, 48.75),
    'az': (-119., -103., 29., 40.),
    'wa': (-128., -112., 41.75, 52.75),
    'abrfc': (-108., -88., 30., 42.),
    'ab10': (-106.53, -90.28, 31.69, 40.01),
    'cbrfc': (-117., -103., 28., 46.),
    'cb10': (-115.69, -104.41, 29.47, 44.71),
    'lmrfc': (-100., -77., 26., 40.),
    'lm10': (-97.17, -80.07, 28.09, 38.02),
    'marfc': (-83.5, -70., 35.5, 44.),
    'ma10': (-81.27, -72.73, 36.68, 43.1),
    'mbrfc': (-116., -86., 33., 53.),
    'mb10': (-112.8, -89.33, 35.49, 50.72),
    'ncrfc': (-108., -76., 34., 53.),
    'nc10': (-104.75, -80.05, 35.88, 50.6),
    'nerfc': (-84., -61., 39., 49.),
    'ne10': (-80.11, -64.02, 40.95, 47.62),
    'nwrfc': (-128., -105., 35., 55.),
    'nw10': (-125.85, -109.99, 38.41, 54.46),
    'ohrfc': (-92., -75., 34., 44.),
    'oh10': (-90.05, -77.32, 35.2, 42.9),
    'serfc': (-94., -70., 22., 40.),
    'se10': (-90.6, -73.94, 24.12, 37.91),
    'wgrfc': (-112., -88., 21., 42.),
    'wg10': (-108.82, -92.38, 23.99, 39.18),
    'nwcn': (-133.5, -10.5, 32., 56.),
    'cn': (-120.4, -14., 37.9, 58.6),
    'ab': (-119.6, -108.2, 48.6, 60.4),
    'bc': (-134.5, -109., 47.2, 60.7),
    'mb': (-102.4, -86.1, 48.3, 60.2),
    'nb': (-75.7, -57.6, 42.7, 49.6),
    'nf': (-68., -47., 45., 62.),
    'ns': (-67., -59., 43., 47.5),
    'nt': (-131.8, -33.3, 57.3, 67.8),
    'on': (-94.5, -68.2, 41.9, 55.),
    'pe': (-64.6, -61.7, 45.8, 47.1),
    'qb': (-80., -49.2, 44.1, 60.9),
    'sa': (-111.2, -97.8, 48.5, 60.3),
    'yt': (-142., -117., 59., 70.5),
    'ag': (-80., -53., -56., -20.),
    'ah': (60., 77., 27., 40.),
    'afrca': (-25., 59.4, -36., 41.),
    'ai': (-14.3, -14.1, -8., -7.8),
    'alba': (18., 23., 39., 43.),
    'alge': (-9., 12., 15., 38.),
    'an': (10., 25., -20., -5.),
    'antl': (-70., -58., 11., 19.),
    'antg': (-86., -65., 17., 25.),
    'atg': (-62., -61.6, 16.9, 17.75),
    'au': (101., 148., -45., -6.5),
    'azor': (-27.6, -23., 36., 41.),
    'ba': (-80.5, -72.5, 22.5, 28.5),
    'be': (-64.9, -64.5, 32.2, 32.6),
    'bel': (2.5, 6.5, 49.4, 51.6),
    'bf': (113., 116., 4., 5.5),
    'bfa': (-6., 3., 9., 15.1),
    'bh': (-89.3, -88.1, 15.7, 18.5),
    'bi': (29., 30.9, -4.6, -2.2),
    'bj': (0., 5., 6., 12.6),
    'bn': (50., 51., 25.5, 27.1),
    'bo': (-72., -50., -24., -8.),
    'bots': (19., 29.6, -27., -17.),
    'br': (-62.5, -56.5, 12.45, 13.85),
    'bt': (71.25, 72.6, -7.5, -5.),
    'bu': (22., 30., 40., 45.),
    'bv': (3., 4., -55., -54.),
    'bw': (87., 93., 20.8, 27.),
    'by': (19., 33., 51., 60.),
    'bz': (-75., -30., -35., 5.),
    'cais': (-172., -171., -3., -2.),
    'nwcar': (-120., -50., -15., 35.),
    'cari': (-103., -53., 3., 36.),
    'cb': (13., 25., 7., 24.),
    'ce': (14., 29., 2., 11.5),
    'cg': (10., 20., -6., 5.),
    'ch': (-80., -66., -56., -15.),
    'ci': (85., 145., 14., 48.5),
    'cm': (7.5, 17.1, 1., 14.),
    'colm': (-81., -65., -5., 14.),
    'cr': (-19., -13., 27., 30.),
    'cs': (-86.5, -81.5, 8.2, 11.6),
    'cu': (-85., -74., 19., 24.),
    'cv': (-26., -22., 14., 18.),
    'cy': (32., 35., 34., 36.),
    'cz': (8.9, 22.9, 47.4, 52.4),
    'dj': (41.5, 44.1, 10.5, 13.1),
    'dl': (4.8, 16.8, 47., 55.),
    'dn': (8., 11., 54., 58.6),
    'do': (-61.6, -61.2, 15.2, 15.8),
    'dr': (-72.2, -68., 17.5, 20.2),
    'eg': (24., 37., 21., 33.),
    'eq': (-85., -74., -7., 3.),
    'er': (50., 57., 22., 26.6),
    'es': (-90.3, -87.5, 13., 14.6),
    'et': (33., 49., 2., 19.),
    'fa': (-8., -6., 61., 63.),
    'fg': (-55., -49., 1., 7.),
    'fi': (20.9, 35.1, 59., 70.6),
    'fj': (176., -179., 16., 19.),
    'fk': (-61.3, -57.5, -53., -51.),
    'fn': (0., 17., 11., 24.),
    'fr': (-5., 11., 41., 51.5),
    'gb': (-17.1, -13.5, 13., 14.6),
    'gc': (-82.8, -77.6, 17.9, 21.1),
    'gh': (-4.5, 1.5, 4., 12.),
    'gi': (-8., -4., 35., 38.),
    'gl': (-56.7, 14., 58.3, 79.7),
    'glp': (-64.2, -59.8, 14.8, 19.2),
    'gm': (144.5, 145.1, 13., 14.),
    'gn': (2., 16., 3.5, 15.5),
    'go': (8., 14.5, -4.6, 3.),
    'gr': (20., 27.6, 34., 42.),
    'gu': (-95.6, -85., 10.5, 21.1),
    'gw': (-17.5, -13.5, 10.8, 12.8),
    'gy': (-62., -55., 0., 10.),
    'ha': (-75., -71., 18., 20.),
    'he': (-6.1, -5.5, -16.3, -15.5),
    'hk': (113.5, 114.7, 22., 23.),
    'ho': (-90., -83., 13., 16.6),
    'hu': (16., 23., 45.5, 49.1),
    'ic': (43., 45., -13.2, -11.),
    'icel': (-24.1, -11.5, 63., 67.5),
    'ie': (-11.1, -4.5, 50., 55.6),
    'inda': (67., 92., 4.2, 36.),
    'indo': (95., 141., -8., 6.),
    'iq': (38., 50., 29., 38.),
    'ir': (44., 65., 25., 40.),
    'is': (34., 37., 29., 34.),
    'iv': (-9., -2., 4., 11.),
    'iw': (34.8, 35.6, 31.2, 32.6),
    'iy': (6.6, 20.6, 35.6, 47.2),
    'jd': (34., 39.6, 29., 33.6),
    'jm': (-80., -76., 16., 19.),
    'jp': (123., 155., 24., 47.),
    'ka': (131., 155., 1., 9.6),
    'kash': (74., 78., 32., 35.),
    'kb': (172., 177., -3., 3.2),
    'khm': (102., 108., 10., 15.),
    'ki': (105.2, 106.2, -11., -10.),
    'kn': (32.5, 42.1, -6., 6.),
    'kna': (-62.9, -62.4, 17., 17.5),
    'ko': (124., 131.5, 33., 43.5),
    'ku': (-168., -155., -24.1, -6.1),
    'kw': (46.5, 48.5, 28.5, 30.5),
    'laos': (100., 108., 13.5, 23.1),
    'lb': (34.5, 37.1, 33., 35.),
    'lc': (60.9, 61.3, 13.25, 14.45),
    'li': (-12., -7., 4., 9.),
    'ln': (-162.1, -154.9, -4.2, 6.),
    'ls': (27., 29.6, -30.6, -28.),
    'lt': (9.3, 9.9, 47., 47.6),
    'lux': (5.6, 6.6, 49.35, 50.25),
    'ly': (8., 26., 19., 35.),
    'maar': (-63.9, -62.3, 17., 18.6),
    'made': (-17.3, -16.5, 32.6, 33.),
    'mala': (100., 119.6, 1., 8.),
    'mali': (-12.5, 6., 8.5, 25.5),
    'maur': (57.2, 57.8, -20.7, -19.9),
    'maut': (-17.1, -4.5, 14.5, 28.1),
    'mc': (-13., -1., 25., 36.),
    'mg': (43., 50.6, -25.6, -12.),
    'mh': (160., 172., 4.5, 12.1),
    'ml': (14.3, 14.7, 35.8, 36.),
    'mmr': (92., 102., 7.5, 28.5),
    'mong': (87.5, 123.1, 38.5, 52.6),
    'mr': (-61.2, -60.8, 14.3, 15.1),
    'mu': (113., 114., 22., 23.),
    'mv': (70.1, 76.1, -6., 10.),
    'mw': (32.5, 36.1, -17., -9.),
    'mx': (-119., -83., 13., 34.),
    'my': (142.5, 148.5, 9., 25.),
    'mz': (29., 41., -26.5, -9.5),
    'nama': (11., 25., -29.5, -16.5),
    'ncal': (158., 172., -23., -18.),
    'ng': (130., 152., -11., 0.),
    'ni': (2., 14.6, 3., 14.),
    'nk': (-88., -83., 10.5, 15.1),
    'nl': (3.5, 7.5, 50.5, 54.1),
    'no': (3., 35., 57., 71.5),
    'np': (80., 89., 25., 31.),
    'nw': (166.4, 167.4, -1., 0.),
    'nz': (165., 179., -48., -33.),
    'om': (52., 60., 16., 25.6),
    'os': (9., 18., 46., 50.),
    'pf': (-154., -134., -28., -8.),
    'ph': (116., 127., 4., 21.),
    'pi': (-177.5, -167.5, -9., 1.),
    'pk': (60., 78., 23., 37.),
    'pl': (14., 25., 48.5, 55.),
    'pm': (-83., -77., 7., 10.),
    'po': (-10., -4., 36.5, 42.5),
    'pr': (-82., -68., -20., 5.),
    'pt': (-130.6, -129.6, -25.56, -24.56),
    'pu': (-67.5, -65.5, 17.5, 18.5),
    'py': (-65., -54., -32., -17.),
    'qg': (7., 12., -2., 3.),
    'qt': (50., 52., 24., 27.),
    'ra': (60., -165., 25., 55.),
    're': (55., 56., -21.5, -20.5),
    'riro': (-18., -12., 17.5, 27.5),
    'ro': (19., 31., 42.5, 48.5),
    'rw': (29., 31., -3., -1.),
    'saud': (34.5, 56.1, 15., 32.6),
    'sb': (79., 83., 5., 10.),
    'seyc': (55., 56., -5., -4.),
    'sg': (-18., -10., 12., 17.),
    'si': (39.5, 52.1, -4.5, 13.5),
    'sk': (109.5, 119.3, 1., 7.),
    'sl': (-13.6, -10.2, 6.9, 10.1),
    'sm': (-59., -53., 1., 6.),
    'sn': (10., 25., 55., 69.6),
    'so': (156., 167., -12., -6.),
    'sp': (-10., 6., 35., 44.),
    'sr': (103., 105., 1., 2.),
    'su': (21.5, 38.5, 3.5, 23.5),
    'sv': (30.5, 33.1, -27.5, -25.3),
    'sw': (5.9, 10.5, 45.8, 48.),
    'sy': (35., 42.6, 32., 37.6),
    'tanz': (29., 40.6, -13., 0.),
    'td': (-62.1, -60.5, 10., 11.6),
    'tg': (-0.5, 2.5, 5., 12.),
    'th': (97., 106., 5., 21.),
    'ti': (-71.6, -70.6, 21., 22.),
    'tk': (-173., -171., -11.5, -7.5),
    'to': (-178.5, -170.5, -22., -15.),
    'tp': (6., 7.6, 0., 2.),
    'ts': (7., 13., 30., 38.),
    'tu': (25., 48., 34.1, 42.1),
    'tv': (176., 180., -11., -5.),
    'tw': (120., 122., 21.9, 25.3),
    'ug': (29., 35., -3.5, 5.5),
    'uk': (-11., 5., 49., 60.),
    'ur': (24., 41., 44., 55.),
    'uy': (-60., -52., -35.5, -29.5),
    'vanu': (167., 170., -21., -13.),
    'vi': (-65.5, -64., 16.6, 19.6),
    'vk': (13.8, 25.8, 46.75, 50.75),
    'vn': (-75., -60., -2., 14.),
    'vs': (102., 110., 8., 24.),
    'wk': (166.1, 167.1, 18.8, 19.8),
    'ye': (42.5, 54.1, 12.5, 19.1),
    'yg': (13.5, 24.6, 40., 47.),
    'za': (16., 34., -36., -22.),
    'zb': (21., 35., -20., -7.),
    'zm': (170.5, 173.5, -15., -13.),
    'zr': (12., 31.6, -14., 6.),
    'zw': (25., 34., -22.9, -15.5)
}
class Panel(HasTraits):
    """Draw one or more plots."""
[docs]@exporter.export
class PanelContainer(HasTraits):
    """Hold multiple panels of plots."""
    size = Union([Tuple(Int(), Int()), Instance(type(None))], default_value=None)
    panels = List(Instance(Panel))
    @property
    def panel(self):
        """Provide simple access for a single panel."""
        return self.panels[0]
    @panel.setter
    def panel(self, val):
        self.panels = [val]
    @observe('panels')
    def _panels_changed(self, change):
        for panel in change.new:
            panel.parent = self
            panel.observe(self.refresh, names=('_need_redraw'))
    @property
    def figure(self):
        """Provide access to the underlying figure object."""
        if not hasattr(self, '_fig'):
            self._fig = plt.figure(figsize=self.size)
        return self._fig
[docs]    def refresh(self, _):
        """Refresh the rendering of all panels."""
        # First make sure everything is properly constructed
        self.draw()
        # Trigger the graphics refresh
        self.figure.canvas.draw()
        # Flush out interactive events--only ok on Agg for newer matplotlib
        try:
            self.figure.canvas.flush_events()
        except NotImplementedError:
            pass 
[docs]    def draw(self):
        """Draw the collection of panels."""
        for panel in self.panels:
            with panel.hold_trait_notifications():
                panel.draw() 
[docs]    def save(self, *args, **kwargs):
        """Save the constructed graphic as an image file."""
        self.draw()
        self.figure.savefig(*args, **kwargs) 
[docs]    def show(self):
        """Show the constructed graphic on the screen."""
        self.draw()
        self.figure.show()  
[docs]@exporter.export
class MapPanel(Panel):
    """Draw one or more plots on a map."""
    parent = Instance(PanelContainer)
    layout = Tuple(Int(), Int(), Int(), default_value=(1, 1, 1))
    plots = List(Any())
    _need_redraw = Bool(default_value=True)
    area = Union([Unicode(), Tuple(Float(), Float(), Float(), Float())], allow_none=True,
                 default_value=None)
    projection = Union([Unicode(), Instance(ccrs.Projection)], default_value='data')
    layers = List(Union([Unicode(), Instance(cfeature.Feature)]), default_value=['coastline'])
    title = Unicode()
    @observe('plots')
    def _plots_changed(self, change):
        """Handle when our collection of plots changes."""
        for plot in change.new:
            plot.parent = self
            plot.observe(self.refresh, names=('_need_redraw'))
        self._need_redraw = True
    @observe('parent')
    def _parent_changed(self, _):
        """Handle when the parent is changed."""
        self.ax = None
    @property
    def _proj_obj(self):
        """Return the projection as a Cartopy object.
        Handles looking up a string for the projection, or if the projection
        is set to ``'data'`` looks at the data for the projection.
        """
        if is_string_like(self.projection):
            if self.projection == 'data':
                return self.plots[0].griddata.metpy.cartopy_crs
            else:
                return _projections[self.projection]
        else:
            return self.projection
    @property
    def _layer_features(self):
        """Iterate over all map features and return as Cartopy objects.
        Handle converting names of maps to auto-scaling map features.
        """
        for item in self.layers:
            if is_string_like(item):
                item = item.upper()
                try:
                    scaler = cfeature.AdaptiveScaler('110m', (('50m', 50), ('10m', 15)))
                    feat = getattr(cfeature, item).with_scale(scaler)
                except AttributeError:
                    scaler = cfeature.AdaptiveScaler('20m', (('5m', 5), ('500k', 1)))
                    feat = getattr(cartopy_utils, item).with_scale(scaler)
            else:
                feat = item
            yield feat
    @observe('area')
    def _set_need_redraw(self, _):
        """Watch traits and set the need redraw flag as necessary."""
        self._need_redraw = True
    @property
    def ax(self):
        """Get the :class:`matplotlib.axes.Axes` to draw on.
        Creates a new instance if necessary.
        """
        # If we haven't actually made an instance yet, make one with the right size and
        # map projection.
        if getattr(self, '_ax', None) is None:
            self._ax = self.parent.figure.add_subplot(*self.layout, projection=self._proj_obj)
        return self._ax
    @ax.setter
    def ax(self, val):
        """Set the :class:`matplotlib.axes.Axes` to draw on.
        Clears existing state as necessary.
        """
        if getattr(self, '_ax', None) is not None:
            self._ax.cla()
        self._ax = val
[docs]    def refresh(self, changed):
        """Refresh the drawing if necessary."""
        self._need_redraw = changed.new 
[docs]    def draw(self):
        """Draw the panel."""
        # Only need to run if we've actually changed.
        if self._need_redraw:
            # Draw all of the plots.
            for p in self.plots:
                with p.hold_trait_notifications():
                    p.draw()
            # Add all of the maps
            for feat in self._layer_features:
                self.ax.add_feature(feat)
            # Set the extent as appropriate based on the area. One special case for 'global'
            if self.area == 'global':
                self.ax.set_global()
            elif self.area is not None:
                # Try to look up if we have a string
                if is_string_like(self.area):
                    area = _areas[self.area]
                # Otherwise, assume we have a tuple to use as the extent
                else:
                    area = self.area
                self.ax.set_extent(area, ccrs.PlateCarree())
            # Use the set title or generate one.
            title = self.title or ', '.join(plot.name for plot in self.plots)
            self.ax.set_title(title)
            self._need_redraw = False  
[docs]@exporter.export
class Plot2D(HasTraits):
    """Represent plots of 2D data."""
    parent = Instance(Panel)
    _need_redraw = Bool(default_value=True)
    field = Unicode()
    level = Union([Int(allow_none=True, default_value=None), Instance(units.Quantity)])
    time = Instance(datetime, allow_none=True)
    colormap = Unicode(allow_none=True, default_value=None)
    image_range = Union([Tuple(Int(allow_none=True), Int(allow_none=True)),
                         Instance(plt.Normalize)], default_value=(None, None))
    @property
    def _cmap_obj(self):
        """Return the colormap object.
        Handle convert the name of the colormap to an object from matplotlib or metpy.
        """
        try:
            return ctables.registry.get_colortable(self.colormap)
        except KeyError:
            return plt.get_cmap(self.colormap)
    @property
    def _norm_obj(self):
        """Return the normalization object.
        Converts the tuple image range to a matplotlib normalization instance.
        """
        return plt.Normalize(*self.image_range)
[docs]    def clear(self):
        """Clear the plot.
        Resets all internal state and sets need for redraw.
        """
        if getattr(self, 'handle', None) is not None:
            self.clear_handle()
            self._need_redraw = True 
[docs]    def clear_handle(self):
        """Clear the handle to the plot instance."""
        self.handle.remove()
        self.handle = None 
    @observe('parent')
    def _parent_changed(self, _):
        """Handle setting the parent object for the plot."""
        self.clear()
    @observe('field', 'level', 'time')
    def _update_data(self, _=None):
        """Handle updating the internal cache of data.
        Responds to changes in various subsetting parameters.
        """
        self._griddata = None
        self.clear()
    # Can't be a Traitlet because notifications don't work with arrays for traits
    # notification never happens
    @property
    def data(self):
        """Access the current data subset."""
        return self._data
    @data.setter
    def data(self, val):
        self._data = val
        self._update_data()
    @property
    def griddata(self):
        """Return the internal cached data."""
        if getattr(self, '_griddata', None) is None:
            if self.field:
                data = self.data.metpy.parse_cf(self.field)
            elif not hasattr(self.data.metpy, 'x'):
                # Handles the case where we have a dataset but no specified field
                raise ValueError('field attribute has not been set.')
            else:
                data = self.data
            subset = {'method': 'nearest'}
            if self.level is not None:
                subset[data.metpy.vertical.name] = self.level
            if self.time is not None:
                subset[data.metpy.time.name] = self.time
            self._griddata = data.metpy.sel(**subset).squeeze()
        return self._griddata
    @property
    def plotdata(self):
        """Return the data for plotting.
        The data array, x coordinates, and y coordinates.
        """
        x = self.griddata.metpy.x
        y = self.griddata.metpy.y
        if 'degree' in x.units:
            import numpy as np
            x, y, _ = self.griddata.metpy.cartopy_crs.transform_points(ccrs.PlateCarree(),
                                                                       *np.meshgrid(x, y)).T
            x = x[:, 0] % 360
            y = y[0, :]
        return x, y, self.griddata
    @property
    def name(self):
        """Generate a name for the plot."""
        ret = self.field
        if self.level is not None:
            ret += '@{:d}'.format(self.level)
        return ret
[docs]    def draw(self):
        """Draw the plot."""
        if self._need_redraw:
            if getattr(self, 'handle', None) is None:
                self._build()
            self._need_redraw = False  
[docs]@exporter.export
class ImagePlot(Plot2D):
    """Represent an image plot."""
    @observe('colormap', 'image_range')
    def _set_need_redraw(self, _):
        """Handle changes to attributes that just need a simple redraw."""
        if hasattr(self, 'handle'):
            self.handle.set_cmap(self._cmap_obj)
            self.handle.set_norm(self._norm_obj)
            self._need_redraw = True
    @property
    def plotdata(self):
        """Return the data for plotting.
        The data array, x coordinates, and y coordinates.
        """
        x = self.griddata.metpy.x
        y = self.griddata.metpy.y
        # At least currently imshow with cartopy does not like this
        if 'degree' in x.units:
            x = x.data
            x[x > 180] -= 360
        return x, y, self.griddata
    def _build(self):
        """Build the plot by calling any plotting methods as necessary."""
        x, y, imdata = self.plotdata
        # We use min/max for y and manually figure out origin to try to avoid upside down
        # images created by images where y[0] > y[-1]
        extents = (x[0], x[-1], y.min(), y.max())
        origin = 'upper' if y[0] > y[-1] else 'lower'
        self.handle = self.parent.ax.imshow(imdata, extent=extents, origin=origin,
                                            cmap=self._cmap_obj, norm=self._norm_obj,
                                            transform=imdata.metpy.cartopy_crs) 
[docs]@exporter.export
class ContourPlot(Plot2D):
    """Represent a contour plot."""
    linecolor = Unicode('black')
    linewidth = Int(2)
    contours = Union([List(Float()), Int()], default_value=25)
    @observe('contours', 'linecolor', 'linewidth')
    def _set_need_rebuild(self, _):
        """Handle changes to attributes that need to regenerate everything."""
        # Because matplotlib doesn't let you just change these properties, we need
        # to trigger a clear and re-call of contour()
        self.clear()
[docs]    def clear_handle(self):
        """Clear the handle to the plot instance."""
        for col in self.handle.collections:
            col.remove()
        self.handle = None 
    def _build(self):
        """Build the plot by calling any plotting methods as necessary."""
        x, y, imdata = self.plotdata
        self.handle = self.parent.ax.contour(x, y, imdata, self.contours,
                                             colors=self.linecolor, linewidths=self.linewidth,
                                             transform=imdata.metpy.cartopy_crs)