# -*- coding: utf-8 -*-
"""
BeamBlock.py - Class for Beam Blockage calculations

Author::
Nick Guy - OU CIMMS/Univ of Miami

This program was ported from code written in IDL used at Colorado State University.
It is believed that the original code at CSU was written by Steven Nesbitt.  
Timothy Lang also contributed to the development of this program.

This program is not particularly fast as it reads in large DEM files.
"""
# Import required libraries
import struct

import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from matplotlib.path import Path
from glob import glob
import os
import subprocess
import netCDF4 as nc4
import time
from pkg_resources import Requirement, resource_filename

import pyradarmet as rmet
from .geometry import r_effective, ray_height
from .geometry import half_power_radius, range_correct, beam_block_frac
from scipy.io.idl import readsav
import scipy.ndimage as scim
from mpl_toolkits.basemap import Basemap

###################################

VERSION = '0.0.1'

##################################
# CONSTANTS #
#############
# Radius of earth (m)
RE = 6371000.

# Plotting setup
TITLEDICT = {'fontsize': 10}
PAN3_AX1 = [.08, .5, .43, .43]
PAN3_AX2 = [.58, .5, .43, .43]
PAN3_AX3 = [.10, .08, .80, .35]

# Set up the data directory for DEM files
#DEM_DATADIR = os.sep.join([os.path.dirname(__file__), 'data'])
#print DEM_DATADIR

class BeamBlock(object):
    """
    A class instance to calculalate beam blockage of weather radar
    """

###################################################

    def __init__(self, dem_filename=None, verbose=False,
                 dem_hdr_file=None, upper_left_x=None, upper_left_y=None,
                 radar_lon=None, radar_lat=None, radar_alt=None,
                 radar_antenna_height=None,
                 elev_angle=None, beamwidth=None, refract_grad=None,
                 range_dist=None, azimuthal_angle=None,
                 num_rng_pts=None, num_az_pts=None,
                 lon_box_size=6., lat_box_size=6.):
        """
        If initialized with a filename (incl. path), will call
        read_bin_netcdf() to populate the class instance.
        
        If not, it simply instances the class but does not populate
        its attributes.
        
        Parameters::
        ----------
        dem_filename : str
            Full path and filename of Digital Elevation Model file to use.
            If not supplied, a file will be selected, along with the 
            appropriate dem_hdr_file, based on radar_lon and radar_lat
        dem_hdr_file : str
            Full path and filename of Digital Elevation Model header file to use.
            This file will find upper_left_x and upper_left_y. 
            If not set, upper_left_x and upper_left_y must be set by user.
        upper_left_x : float
            Coordinate of upper left X position of file [decimal degrees]
            Only required if dem_hdr_file not supplied
        upper_left_y : float
            Coordinate of upper left Y position of file [decimal degrees]
        verbose: boolean
            Set to True for text output. Useful for debugging.
        
        radar_lon : float
            Radar longitude [decimal degrees]
        radar_lat : float
            Radar latitude [decimal degrees]
        radar_alt : float
            Radar altitude [meters]
        radar_antenna_height : float
            Radar antenna height [meters]
        elev_angle : float
            Elevation angle for analysis [degrees]
        beamwidth : float
            Radar beam width [degrees]
        refract_grad : float
            Vertical gradient of refraction [1/km]
        range_dist : float
            Radial distance of radar beam [km]
        azimuthal angle : float
            Azimuthal angle [decimal degrees] to display beam propagation along
        num_rng_pts : int
            Number of points along the range direction (i.e. # gates)
        num_az_pts : int
            Number of points along the azimuthal direction (i.e. # angles)
        lon_box_size : float
            The size of the longitudinal box to subset, radar in middle
        lat_box_size : float
            The size of the latitudinal box to subset, radar in middle
        """
        self.dem_file = dem_filename
        self.dem_file2 = None
        self.dem_hdr_file = dem_hdr_file
        self.upper_left_x = upper_left_x
        self.upper_left_y = upper_left_y
        self.lon_box_size = lon_box_size
        self.lat_box_size = lat_box_size
        self.ralt = radar_alt
        
        # Set the directory where DEM data can be found
        try:
            os.environ["GTOPO_DATA"]
            self.demdir = os.path.join(os.environ["GTOPO_DATA"], 'data')
        except KeyError:
            print "No GTOPO_DATA environmental variable found, "\
            "assuming data embedded in PyRadarMet package"
            self.demdir = os.sep.join([os.path.dirname(__file__), 'data'])
        
        # Set radar latitude and longitude
        if (radar_lon is None) or (radar_lat is None):
            print "Radar longitude, latitude, and altitude need to be defined!"
            exit()
        else:
            self.rlon, self.rlat= radar_lon, radar_lat
        
        # Set the height of the radar antenna above ground
        if radar_antenna_height is None:
            self.rheight = 0.
        else:
            self.rheight = radar_antenna_height
            
        # Set elevation angle (degrees) [e.g. 0.8, 1.3, 1.8, 2.5]
        if elev_angle is None:
            self.E = 0.8
        else:
            self.E = elev_angle

        # Set radar beam width (degrees)
        if beamwidth is None:
            self.BW = 0.91
        else:
            self.BW = beamwidth

        # Set the vertical refractivity gradient (1/km) [Standard Atm = -40]
        if refract_grad is None:
            self.dNdH = -40.
        else:
            self.dNdH = refract_grad

        # Set the radial distance of radar [range] in km
        if range_dist is None:
            self.range = 150.
        else:
            self.range = range_dist

        # Azimuthal direction to plot beam propagation
        if azimuthal_angle is None:
            self.Az_plot = 0
        else:
            self.Az_plot = azimuthal_angle

        # Set the number of points along the radial
        if num_rng_pts is None:
            self.nrng = 601
        else:
            self.nrng = num_rng_pts
        
        # Set the number of azimuthal points
        if num_az_pts is None:
            self.naz = 361
        else:
            self.naz = num_az_pts
        
        # Check the DEM file
        self._check_dem_file()
#        self._grab_dem_file()
        
        # Read the DEM file
        self.read_dem()
        
        # Process inputs to calculate beam properties
        self.calc_beam_blockage()
            
    ##############

    def help(self):

        _method_header_printout('help')
        print 'To define a new MosaicTile(), use instance = MosaicTile().'
        print 'then read in a file to populate attributes.'
        print 'Available read methods:'
        print '    read_mosaic_netcdf(<FILE>):'
        print '    read_mosaic_binary(<FILE>):'
        print '    Read v1 MRMS mosaics (7/30/2013 & earlier)'
        print '    Also v2 MRMS mosaics (7/30/2013 & after)'
        print 'Can also use instance = MosaicTile(filepath+name).'
        print 'Set binary=True as keyword above if reading an MRMS binary file.'
        print 'Other available methods:'
        print 'diag(), get_comp(), plot_vert(), plot_horiz(), three_panel_plot()'
        print 'subsection(), write_mosaic_binary(), output_composite()'
        _method_footer_printout()
        
##########################
# Read data file methods #
##########################

    def read_dem(self):
        '''Read a DEM file if passed or search for correct file
        '''
        if self.dem_hdr_file is None:
            if (self.upper_left_x is None) or (self.upper_left_y is None):
                print "Must prived upper_left_x and upper_left_y if \
                       no DEM header file is supplied"
        self.read_gt30_dem()

    def read_gt30_dem(self):
        '''
        Read in a binary GTOPO30 DEM file
        The following code was adapted from an example at:
        http://stevendkay.wordpress.com/tag/dem/
        '''
        if isinstance(self.dem_hdr_file, str) != False:
            # wrap header in dict, because the different elements aren't necessarily in the same row
            hdr = dict(np.loadtxt(self.dem_hdr_file, dtype='S20'))
            byteorder = hdr['BYTEORDER']
            pixeltype = hdr['PIXELTYPE']
            nrows = int(hdr['NROWS'])
            ncols = int(hdr['NCOLS'])
            missing = int(hdr['NODATA'])
            upper_left_x = float(hdr['ULXMAP'])
            upper_left_y = float(hdr['ULYMAP'])
            xdim = float(hdr['XDIM'])
            ydim = float(hdr['YDIM'])
        else:
            # From the output_parameters.hdr file or product documentation
            byteorder = 'M'
            pixeltype = 'UNSIGNEDINT'
            upper_left_x = self.upper_left_x
            upper_left_y = self.upper_left_y
            nrows = 6000
            ncols = 4800
            missing = -9999.
            xdim = 0.00833333333333
            ydim = 0.00833333333333
            
        # Extract GTOPO30 BIL file
        fi = open(self.dem_file, "rb")
        databin = fi.read()
        fi.close()

        # check for pixeltype, this has to be reviewed
        if pixeltype == 'SIGNEDINT':
            format = 'h'
        if pixeltype == 'UNSIGNEDINT':
            format = 'H'
        # Unpack binary data into a flat tuple z
        # check for byteorder
        if byteorder == 'M':
            s = ">%d%s" % (nrows * ncols, format,)
        else:
            s = "<%d%s" % (nrows * ncols, format,)

        z = struct.unpack(s, databin)
        
        topo = np.zeros((nrows, ncols))
        for r in range(0, nrows):
            for c in range(0, ncols):
                elevation = z[((ncols) * r) + c]
                if (elevation == 65535 or elevation < 0 or elevation > 20000):
                    # may not be needed depending on format, and the "magic number"
                    # value used for 'void' or missing data
                    elevation = 0.#np.nan
                topo[r][c] = float(elevation)

        lat = upper_left_y - ydim * np.arange(nrows)
        lon = upper_left_x + xdim * np.arange(ncols)
        
        # Find indices for a subset of DEM data
        londel = self.lon_box_size / 2.
        latdel = self.lat_box_size / 2.
        lonInd = np.where((lon >= (self.rlon - londel)) & \
                          (lon <= (self.rlon + londel)))
        latInd = np.where((lat >= (self.rlat - latdel)) & \
                          (lat <= (self.rlat + latdel)))

        lonSub = lon[np.min(lonInd):np.max(lonInd)]
        latSub = lat[np.min(latInd): np.max(latInd)]
        self.lon, self.lat = np.meshgrid(lonSub, latSub)
        self.topo = topo[np.min(latInd):np.max(latInd), np.min(lonInd): np.max(lonInd)]
                            
        self.topo = np.ma.masked_outside(self.topo, 0, 20000)
        self.topo = np.ma.masked_invalid(self.topo)
        
        # Find the elevation of the radar
        if self.ralt is None:
            radar_lon_index = self._get_array_index(self.rlon, self.lon[0, :])
            radar_lat_index = self._get_array_index(self.rlat, self.lat[:, 0])
        
            radar_alt_map = scim.map_coordinates(self.topo,
                                np.vstack((radar_lat_index, radar_lon_index)),
                                prefilter=False)
            self.ralt = self.rheight + radar_alt_map

###################################################
# Calculation methods #
###################################################

    def calc_beam_blockage(self):
        '''Calculate the properties of the beam for calculation'''
        # Initialize arrays
        self.inX = np.empty(self.nrng)
        self.inY = np.empty(self.nrng)

        self.rng_lon = np.empty([self.naz, self.nrng])
        self.rng_lat = np.empty([self.naz, self.nrng])
        self.terr = np.empty([self.naz, self.nrng])
        
        # Points along each ray to calculate beam propagation
        # e.g. From 0 to 150 km, with 0.25 km bin spacing
        self.rng = np.linspace(0, self.range*1000., self.nrng)
        
        # Calculate the effective radius
        self.Reff = r_effective()

        # Calculate the height of center of beam
        self.h = ray_height(self.rng, self.E, 
                                          self.ralt, reff=self.Reff)

        # Calculate the beam radius
        self.a = half_power_radius(self.rng, self.BW)

        # Calculate rng_gnd (actual distance along ground)
        self.rng_gnd = range_correct(self.rng, self.h, self.E)

        # Set up arrays for calculations
        self.phis = np.linspace(0, 360, self.naz)
        
        # Initialize the beam blockage arrays with 0
        self.PBB = np.zeros((self.naz, self.nrng))
        self.CBB = np.zeros((self.naz, self.nrng))

        for jj, az in enumerate(self.phis):
            # Calculate the Lons/Lats along the rng_gnd
            self.rng_lat[jj, :] = np.degrees(np.arcsin(np.sin(np.radians(self.rlat)) * \
                                            np.cos(self.rng_gnd / RE) +\
                                            np.cos(np.radians(self.rlat)) * \
                                            np.sin(self.rng_gnd / RE) *\
                                            np.cos(np.radians(az))\
                                            )\
                                            )
            self.rng_lon[jj, :] = self.rlon + \
                     np.degrees(np.arctan2(np.sin(np.radians(az)) * \
                                np.sin(self.rng_gnd / RE) *\
                                np.cos(np.radians(self.rlat)), \
                                np.cos(self.rng_gnd / RE) - \
                                np.sin(np.radians(self.rlat)) * \
                                np.sin(np.radians(self.rng_lat[jj, :]))\
                                )\
                                )
                                
            # Find the indices for interpolation
            for ii, junk in enumerate(self.inX):
                self.inX[ii] = self._get_array_index(self.rng_lon[jj, ii], self.lon[0, :])
                self.inY[ii] = self._get_array_index(self.rng_lat[jj, ii], self.lat[:, 0])

            
            # Interpolate terrain heights to the radar grid
            self.terr[jj, :] = scim.map_coordinates(self.topo, 
                                                    np.vstack((self.inY, self.inX)),
                                                    prefilter=False)

            # Calculate PBB along range
            self.PBB[jj, :] = beam_block_frac(self.terr[jj, :], 
                                                            self.h, self.a)

        self.PBB = np.ma.masked_invalid(self.PBB)

        for ii in range(self.nrng):
            if ii == 0:
                self.CBB[:, ii] = self.PBB[:, ii]
            else:
                self.CBB[:,ii] = np.fmax([self.PBB[:, ii]], [self.CBB[:, ii-1]])
                
        self.terr = np.ma.masked_less(self.terr, -1000.)

###############
# Get methods #
###############

    def _get_array_index(self, value, array):
        '''Calculate the exact index position within latitude array'''
        # Find the spacing
        dp = np.absolute(array[1] - array[0])
        # Calculate the relative position
        pos = np.absolute(value - array[0]) / dp
        return pos

####################
# DEM file methods #
####################
    
    def _check_dem_file(self):
        '''Check if dem file is passed and set if not'''
        ###Add ability to stitch 2 files together self.rlon - londel
        # Set the radar min and max longitude to plot, arbitrary chosen
        minlat, maxlat = self.rlat - 4, self.rlat + 4
        minlon, maxlon = self.rlon - 4, self.rlon + 4

        # get radar bounding box corner coords
        radar_corners = np.array([[minlon, minlat], [minlon, maxlat],
                            [maxlon, maxlat], [maxlon, minlat]])

        # file list which will take the found hdr files
        flist = []

        # iterate over dem files, besides antarcps
        for dem_hdr_file in sorted(glob(os.path.join(self.demdir, '[e,w]*.hdr'))):
            hdr = dict(np.loadtxt(dem_hdr_file, dtype='S20'))
            nrows = int(hdr['NROWS'])
            ncols = int(hdr['NCOLS'])
            missing = int(hdr['NODATA'])
            upper_left_x = float(hdr['ULXMAP'])
            upper_left_y = float(hdr['ULYMAP'])
            xdim = float(hdr['XDIM'])
            ydim = float(hdr['YDIM'])

            lower_right_x = upper_left_x + ncols * xdim
            lower_right_y = upper_left_y - nrows * ydim

            # construct dem bounding box
            dem_bbox = [[upper_left_x, lower_right_y], [upper_left_x, upper_left_y],
                        [lower_right_y, upper_left_y], [lower_right_x, lower_right_y]]

            # make matplotlib Path from dem bounding box
            dem_bbox_path = Path(dem_bbox)

            # check if radar corner points are inside
            inside = dem_bbox_path.contains_points(radar_corners)

            # if inside put file into list
            if np.any(inside):
                dem_filename, ext = os.path.splitext(dem_hdr_file)
                flist.append(dem_filename + '.dem')
                if np.all(inside):
                    break

        # construct gdalbuildvirt command
        command = ['gdalbuildvrt', '-overwrite', '-te', str(minlon), str(minlat),
                   str(maxlon), str(maxlat), os.path.join(self.demdir, 'interim.vrt')]

        for f in flist:
            command.append(f)

        # call gdalbuildvirt subprocess
        subprocess.call(command)
        vrt_file = os.path.join(self.demdir, 'interim.vrt')
        dem_file = os.path.join(self.demdir, 'interim.dem')
        # call gdalwarp subprocess
        command = ["gdalwarp", '-of', 'Ehdr', '-overwrite', vrt_file, dem_file]
        subprocess.call(command)

        # set filenames
        self.dem_file = dem_file
        self.dem_hdr_file = os.path.join(self.demdir, 'interim.hdr')

        if self.dem_file is None:
            if (self.rlon >= -180.) and (self.rlon < 180.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'antarcps'
            elif (self.rlon >= 20.) and (self.rlon < 60.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'e020n40'
            elif (self.rlon >= 20.) and (self.rlon < 60.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'e020n90'
            elif (self.rlon >= 20.) and (self.rlon < 60.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'e020s10'
                
            elif (self.rlon >= 60.) and (self.rlon < 100.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'e060n40'
            elif (self.rlon >= 60.) and (self.rlon < 100.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'e060n90'
            elif (self.rlon >= 60.) and (self.rlon < 100.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'e060s10'
            elif (self.rlon >= 60.) and (self.rlon < 120.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'e060s60'
                
            elif (self.rlon >= 100.) and (self.rlon < 140.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'e100n40'
            elif (self.rlon >= 100.) and (self.rlon < 140.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'e100n90'
            elif (self.rlon >= 100.) and (self.rlon < 140.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'e100s10'
            elif (self.rlon >= 120.) and (self.rlon < 180.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'e120s60'
                
            elif (self.rlon >= 140.) and (self.rlon < 180.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'e140n40'
            elif (self.rlon >= 140.) and (self.rlon < 180.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'e140n90'
            elif (self.rlon >= 140.) and (self.rlon < 180.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'e140s10'
                
            elif (self.rlon >= 0.) and (self.rlon < 60.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'w000s60'
                
            elif (self.rlon >= -20.) and (self.rlon < 20.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'w020n40'
            elif (self.rlon >= -20.) and (self.rlon < 20.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'w020n90'
            elif (self.rlon >= -20.) and (self.rlon < 20.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'w020s10'
                
            elif (self.rlon >= -60.) and (self.rlon < -20.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'w060n40'
            elif (self.rlon >= -60.) and (self.rlon < -20.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'w060n90'
            elif (self.rlon >= -60.) and (self.rlon < -20.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'w060s10'
            elif (self.rlon >= -60.) and (self.rlon < 0.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'w060s60'
                
            elif (self.rlon >= -100.) and (self.rlon < -60.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'w100n40'
            elif (self.rlon >= -100.) and (self.rlon < -60.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'w100n90'
            elif (self.rlon >= -100.) and (self.rlon < -60.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'w100s10'
            elif (self.rlon >= -120.) and (self.rlon < -60.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'w120s60'
                
            elif (self.rlon >= -140.) and (self.rlon < -100.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'w140n40'
            elif (self.rlon >= -140.) and (self.rlon < -100.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'w140n90'
            elif (self.rlon >= -140.) and (self.rlon < -100.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'w140s10'
                
            elif (self.rlon >= -180.) and (self.rlon < -140.) and \
               (self.rlat >= -10.) and (self.rlat < 40.):
                dem_filename = 'w180n40'
            elif (self.rlon >= -180.) and (self.rlon < -140.) and \
               (self.rlat >= 40.) and (self.rlat < 90.):
                dem_filename = 'w180n90'
            elif (self.rlon >= -180.) and (self.rlon < -140.) and \
               (self.rlat >= -60.) and (self.rlat < -10.):
                dem_filename = 'w180s10'
            elif (self.rlon >= -180.) and (self.rlon < -120.) and \
               (self.rlat >= -90.) and (self.rlat < -60.):
                dem_filename = 'w180s60'
        
            self.dem_file = os.path.join(self.demdir, (dem_filename + ".dem"))
            self.dem_hdr_file = os.path.join(self.demdir, (dem_filename + ".hdr"))
            
            # Need an alternative method here to find points close to boundary    
            dem_filename2 = None         
            if dem_filename2 is not None:
                print "MAY NEED A SECOND DEM FILE"
    
################
# Plot methods #
################

    def plot_3panel(self, beam_blockage='complete',
                    lon_plot_range=2., lat_plot_range=2.,
                    terrain_cmap=None, beam_blockage_cmap=None,
                    elev_min=None, elev_max=None,
                    range_rings=None,
                    profile_ymin=None, profile_ymax=None,
                    ht_shade_min=None, ht_shade_max=None,
                    lat_spacing=None, lon_spacing=None,
                    saveFig=False):
        """
        Create a 3-panel plot characterization of beam blockage
        
        Parameters::
        ----------
        beam_blockage : str
            'complete' or 'partial' chooses which field to plot
            Defaults to complete.
        lon_plot_range : float
            Horizontal coverage plots will show +/- lon_plot_range
        lat_plot_range : float
            Horizontal coverage plots will show +/- lat_plot_range
        terrain_cmap : str
            Matplotlib colormap to use for elevation map
        beam_blockage_cmap : str
            Matplotlib colormap to use for beam blockage map
        elev_min : float
            Minumum elevation to display on beam propagation and terrain height [meters]
        elev_max : float
            Maximum elevation to display on beam propagation and terrain height [meters]
        range_rings : float
            A list with location of range rings - e.g. [50., 100.]
        ht_shade_min : float
            Minimum elevation to use in topography colorbar shading [meters]
        ht_shade_max : float
            Maximum elevation to use in topography colorbar shading [meters]
        lat_spacing : float
            Spacing to use for latitudinal lines on maps [degrees]
        lon_spacing : float
            Spacing to use for longitudinal lines on maps [degrees]
        saveFig : boolean
            True to save figure as output file, False to show
        """
        
        # Set the radar min and max longitude to plot
        self.minlat, self.maxlat = self.rlat - lat_plot_range, self.rlat + lat_plot_range
        self.minlon, self.maxlon = self.rlon - lon_plot_range, self.rlon + lon_plot_range
        if terrain_cmap is None:
            terrain_cmap = 'BrBG_r'
        if beam_blockage_cmap is None:
            beam_blockage_cmap = 'PuRd'
        self.terr_cmap, self.bb_cmap = terrain_cmap, beam_blockage_cmap
        
        fig = plt.figure(figsize=(10,8))
        ax1 = fig.add_axes(PAN3_AX1)
        ax2 = fig.add_axes(PAN3_AX2)
        ax3 = fig.add_axes(PAN3_AX3)
        
        self.draw_terrain_height_map(fig, ax1, vmin=ht_shade_min, vmax=ht_shade_max,
                                     lat_spacing=lat_spacing, lon_spacing=lon_spacing)
        if beam_blockage == 'partial':
            self.draw_bb_map(fig, ax2, BB=self.PBB, range_rings=range_rings,
                            lat_spacing=lat_spacing, lon_spacing=lon_spacing)
        elif beam_blockage == 'complete':
            self.draw_bb_map(fig, ax2, range_rings=range_rings,
                            lat_spacing=lat_spacing, lon_spacing=lon_spacing)
        self.draw_beam_terrain_profile(fig, ax3, ymin=elev_min, ymax=elev_max)
        
        if saveFig:
            plt.savefig('BBmap.png', format='png')
        else:
            plt.show()
    
    def draw_terrain_height_map(self, fig, ax, vmin=None, vmax=None,
                                lat_spacing=None, lon_spacing=None):
        '''Draw the terrain heights'''
        if vmin is None:
            topomin = 0.05
        else:
            topomin = vmin/1000.
        if vmax is None:
            topomax = 3.
        else:
            topomax = vmax/1000.
            
        if lat_spacing is None:
            lat_spacing = 1.
        if lon_spacing is None:
            lon_spacing = 1.
            
        bm1 = Basemap(projection='cea', resolution='l', area_thresh = 10000.,
                    llcrnrlon=self.minlon, urcrnrlon=self.maxlon,
                    llcrnrlat=self.minlat, urcrnrlat=self.maxlat,
                    ax=ax)
        ax.set_title('Terrain within %02d km of Radar (km)'%(self.range), fontdict=TITLEDICT)
        bm1.drawmeridians(np.arange(self.minlon, self.maxlon, lon_spacing), labels=[1,0,0,1])           
        bm1.drawparallels(np.arange(self.minlat, self.maxlat, lat_spacing), labels=[1,0,0,1])             
        bm1.drawcountries()
        bm1.drawcoastlines()
        bm1.drawrivers()
        
        xbm1, ybm1 = bm1(self.lon, self.lat)
        Htmap = bm1.pcolormesh(xbm1, ybm1, self.topo/1000., vmin=topomin, vmax=topomax,
                               cmap=self.terr_cmap)
        bm1.plot(self.rlon, self.rlat, 'rD', latlon=True)
        #plot_range_ring(50., bm=bm1, color='w')
        fig.colorbar(Htmap, ax=ax)
        
    def draw_bb_map(self, fig, ax, BB=None, range_rings=None,
                                lat_spacing=None, lon_spacing=None):
        '''Draw the Beam Blockage'''
        if BB is None:
            BB = self.CBB
            
        if lat_spacing is None:
            lat_spacing = 1.
        if lon_spacing is None:
            lon_spacing = 1.
            
        bm2 = Basemap(projection='cea', resolution='l', area_thresh = 10000.,
                    llcrnrlon=self.minlon, urcrnrlon=self.maxlon,
                    llcrnrlat=self.minlat, urcrnrlat=self.maxlat,
                    ax=ax)
        ax.set_title('Beam-blockage fraction', fontdict=TITLEDICT)
        bm2.drawmeridians(np.arange(self.minlon, self.maxlon, lon_spacing), labels=[1,0,0,1])           
        bm2.drawparallels(np.arange(self.minlat, self.maxlat, lat_spacing), labels=[1,0,0,1])             
        bm2.drawcountries()
        bm2.drawcoastlines()
        bm2.drawrivers()

        xbm2, ybm2 = bm2(self.rng_lon, self.rng_lat)
        BBmap = bm2.pcolormesh(xbm2, ybm2, BB, vmin=0., vmax=1., cmap=self.bb_cmap)
        if range_rings is not None:
            for nn in range(len(range_rings)):
                self.plot_range_ring(range_rings[nn], bm=bm2)
        fig.colorbar(BBmap, ax=ax)
         
    def draw_beam_terrain_profile(self, fig, ax, ymin=None, ymax=None):
        '''Draw the Beam height along with terrain and PBB'''
        if ymin is None:
            ymin = 0.
        if ymax is None:
            ymax = 5000.
        
        bc, = ax.plot(self.rng_gnd / 1000., self.h / 1000., '-b', 
                     linewidth=3, label='Beam Center')
        b3db, = ax.plot(self.rng_gnd / 1000., (self.h + self.a) / 1000., ':b', 
                       linewidth=1.5, label='3 dB Beam width')
        ax.plot(self.rng_gnd / 1000., (self.h - self.a) / 1000., ':b')
        tf = ax.fill_between(self.rng_gnd / 1000., 0., 
                        self.terr[self.Az_plot, :] / 1000., 
                        color='0.75')
        ax.set_xlim(0., self.range)
        ax.set_ylim(ymin / 1000., ymax / 1000.)
        ax.set_title(r'dN/dh = %d km$^{-1}$; Elevation angle = %g degrees at %d azimuth'% \
                      (self.dNdH, self.E, self.Az_plot), fontdict=TITLEDICT)
        ax.set_xlabel('Range (km)')
        ax.set_ylabel('Height (km)')

        axb = ax.twinx()
        bbf, = axb.plot(self.rng_gnd / 1000., self.CBB[self.Az_plot, :], '-k', 
                       label='BBF')
        axb.set_ylabel('Beam-blockage fraction')
        axb.set_ylim(0., 1.)
        axb.set_xlim(0., self.range)
        
        ax.legend((bc, b3db, bbf), ('Beam Center', '3 dB Beam width', 'BBF'),
                                   loc='upper left', fontsize=10)

    def plot_range_ring(self, range_ring_location_km, bm=None,
                        color='k', ls='-'):
        """
        Plot a single range ring.
        Parameters::
        ----------
        range_ring_location_km : float
            Location of range ring in km.
        npts: int
            Number of points in the ring, higher for better resolution.
        ax : Axis
            Axis to plot on. None will use the current axis.
        """
        npts = 100
        bm.tissot(self.rlon, self.rlat, 
                  np.degrees(range_ring_location_km * 1000. / RE), npts,
                  fill=False, color='black', linestyle='dashed')
    
################
# Save methods #
################

    def save_map_data(self, filename=None):
        """
        Save beam blockage and mapped data to NetCDF file
        
        Parameters::
        ----------
        filename : str
            Output name of NetCDF file
        """
        if filename is None:
            filename = 'BBmap_data'
        else:
            filename = filename
            
        # Open a NetCDF file to write to
        nc_fid = nc4.Dataset(filename + '.nc', 'w', format='NETCDF4')
        nc_fid.description = "PyRadarMet BeamBlockage calculations"
        
        # Define dimensions
        xid = nc_fid.createDimension('range', self.nrng)
        yid = nc_fid.createDimension('azimuth', self.naz)
        
        # Set global attributes
        nc_fid.topo_source = 'USGS GTOPO30 DEM files'
        nc_fid.history = 'Created ' + time.ctime(time.time()) + ' using python netCDF4 module'
        
        # Create Output variables
        azid = nc_fid.createVariable('azimuth', np.float32, ('azimuth',))
        azid.units = 'degrees'
        azid.long_name = 'Azimuth'
        azid[:] = self.phis[:]
        
        rngid = nc_fid.createVariable('range', np.float32, ('range',))
        rngid.units = 'meters'
        rngid.long_name = 'Range'
        rngid[:] = self.rng_gnd[:]
        
        topoid = nc_fid.createVariable('topography', np.float32, ('azimuth','range'))
        topoid.units = 'meters'
        topoid.long_name = 'Terrain Height'
        topoid[:] = self.terr[:]
        
        lonid = nc_fid.createVariable('longitude', np.float32, ('azimuth', 'range'))
        lonid.units = 'degrees east'
        lonid.long_name = 'Longitude'
        lonid[:] = self.rng_lon[:]
        
        latid = nc_fid.createVariable('latitude', np.float32, ('azimuth', 'range'))
        latid.units = 'degrees north'
        latid.long_name = 'Latitude'
        latid[:] = self.rng_lat[:]
        
        pbbid = nc_fid.createVariable('pbb', np.float32, ('azimuth','range'))
        pbbid.units = 'unitless'
        pbbid.long_name = 'Partial Beam Blockage Fraction'
        pbbid[:] = self.PBB[:]
        
        cbbid = nc_fid.createVariable('cbb', np.float32, ('azimuth','range'))
        cbbid.units = 'unitless'
        cbbid.long_name = 'Cumulative Beam Blockage Fraction'
        cbbid[:] = self.CBB[:]
        
        print "Saving " + filename + '.nc'
        # Close the NetCDF file
        nc_fid.close()