###############################################################################
# Copyright (c) 2012 Trimble Navigation Ltd
# $Id: NoPiSD_Plot.py,v 1.1 2012/02/04 01:08:47 shankar Exp $
###############################################################################
#
# NoPiSD_Plot.py
#
# This file contains functions required to generate long term trends. The
# generated plots are stored as PNG files under method specific directories
###############################################################################

import sys
import string

from NoPiSD_Utils import add_utils_dir_to_path, convert_mm_to_mcycles
add_utils_dir_to_path()

from NoPiUT_Common_Display import *
from NoPiUT_Common_Meas import *

from numpy import zeros
import matplotlib as mpl
from pylab import *
import datetime as dt
ioff()

global metric_list, num_metric
metric_list = get_metrics_list()
IDX_INVALID = -1
LIM_INVALID = 0xffff

###############################################################################
# Top level function to load y-axis fixed limit files for all three physical
# baselines
###############################################################################

def load_axis_limits( concat_summ ) :

  global zero_baseline_min, zero_baseline_max
  global  short_baseline_min, short_baseline_max
  global antenna_switch_min, antenna_switch_max

  fname = 'axis_limits_zero_baseline.cfg'
  [ zero_baseline_min, zero_baseline_max ] = read_limits_file( concat_summ, fname )
  fname = 'axis_limits_short_baseline.cfg'
  [ short_baseline_min, short_baseline_max ] = read_limits_file( concat_summ, fname )
  fname = 'axis_limits_antenna_switch.cfg'
  [ antenna_switch_min, antenna_switch_max ] = read_limits_file( concat_summ, fname )

###############################################################################
# Parses each line read from a fixed limits file. Populates a 3-D list indexed
# on combo number, measurement type and metric. Combo number is assigned based
# on combo names specified in the concatenation summary file
###############################################################################

def read_limits_file( concat_summ, fname ) :

  num_metrics  = len( get_metrics_list() )
  num_combos   = concat_summ.num_combos
  num_mtypes   = 3

  # Statically allocated two 3-D lists to store min and max limits for each
  # combo/meas_type/metric
  min_axis_limit  = zeros( ( num_combos, num_mtypes, num_metrics ) )
  max_axis_limit  = zeros( ( num_combos, num_mtypes, num_metrics ) )

  limit_file_cols = { 'Meas Type'     : 0,
                      'Metric'        : 1,
                      'Ref Sat Type'  : 2,
                      'Ref Sub Type'  : 3,
                      'Test Sat Type' : 4,
                      'Test Sub Type' : 5,
                      'Min'           : 6,
                      'Max'           : 7 }

  if os.access( fname, os.R_OK ) :
    fin = open( fname, 'rt' )
    limits = fin.readlines()
    fin.close()
    
    # Read and parse each valid line read from a fixed limits file
    for line in limits :
      tmp_line = shlex.split( line, True )
      if ( len( tmp_line ) > 0 ) :
        # Limit files are .csv format files. If a file needs to be updated,
        # please rename the .cfg file to a .csv before opening it in Excel.
        split_line = string.split( line, ',' )

        # Identify meas_type and metric
        meas_type = int( split_line[ limit_file_cols[ 'Meas Type' ] ] )
        metric = int( split_line[ limit_file_cols[ 'Metric' ] ] )
        # Reconstruct combo name for line read from limits file
        tmp_combo_name = '%s %s %s %s' %      \
                         ( split_line[ limit_file_cols[ 'Ref Sat Type' ] ],    \
                           split_line[ limit_file_cols[ 'Ref Sub Type' ] ],    \
                           split_line[ limit_file_cols[ 'Test Sat Type' ] ],   \
                           split_line[ limit_file_cols[ 'Test Sub Type' ] ] )
        # Loop through all combos and determine if the line read specified
        # default/exception limits. Populate appropriate axis limit array
        # elements
        for idx in range( concat_summ.num_combos ) :
          combo_idx = IDX_INVALID
          # If combo name is for default limits, assign it to all combos
          if ( ( 'SAT_TYPE_ALL' in tmp_combo_name ) or 
               ( 'SAT_TYPE_GLN' in tmp_combo_name and 
                 'SAT_TYPE_GLN' in concat_summ.combo[ idx ].combo_name  ) or 
               ( tmp_combo_name in concat_summ.combo[ idx ].combo_name ) 
             ) :
            combo_idx = idx
          # Populate suitable axis limit arrays with either default or exception
          # values
          if ( combo_idx != IDX_INVALID ) :
            ymin = float( split_line[ limit_file_cols[ 'Min' ] ] )
            ymax = float( split_line[ limit_file_cols[ 'Max' ] ] )  
            min_axis_limit[ combo_idx ][ meas_type ] [metric ] = ymin
            max_axis_limit[ combo_idx ][ meas_type ][ metric ] = ymax

  # Flag a warning if limits file cannot be found. The returned 3-D lists will
  # be populated with zeros
  else :
    print 'Cannot locate limits file: %s' % fname

  # Return populated limit lists.
  return( min_axis_limit, max_axis_limit )
          
###############################################################################
# Create and return a new figure to plot statistical trends over time
###############################################################################

def create_figure( fig_num ) :

  new_figure = figure( fig_num, figsize = [ 8.5, 7.0 ] )
  hold ( 'on' )
  
  return( new_figure ) 


###############################################################################
# Top level function called from NoPiSC_Main to plot trends. Plots
# trends for each metric in the default list of metrics
###############################################################################

def gen_trend_plots( din,
                     concat_summ,  
                     metric, 
                     fig_num, 
                     **kwargs ) :

  fig = create_figure( fig_num )
  ax  = fig.add_subplot(111)

  color = 'b'
  label = ''
  metric_col = get_concat_file_metric_column( metric )

  # Update values for color and label if they were specified in the dictionary
  if 'color' in kwargs :
    color = kwargs[ 'color' ]
  if 'label' in kwargs :
    label = kwargs[ 'label' ]

  date_str = concat_summ.start_date
  start_date = dt.date( year = int( date_str[ 0:4 ] ), 
                        month = int( date_str[ 4:6 ] ),
                        day = int( date_str[ 6: ] ) 
                      )
  xaxis_date = []
  for idx in range( len( din[ :,0 ] ) ) :
    tmp_date = start_date + dt.timedelta( din[ idx, 0 ] )
    xaxis_date.append( date2num( tmp_date ) )

  if ( len( label ) > 0 ) :
    ax.plot_date( xaxis_date, din[:, metric_col], label = label, 
                  color = color, linestyle='-', marker='None' )    
  else :
    ax.plot_date( xaxis_date, din[:, metric_col], 
                  color = color, linestyle='-', marker='None' )

###############################################################################
# Concatenated statistics files have an extra column compared to a single day
# statistics file.
#
# The first column in a concatenated file indicates the day number. The first
# day of the concatenation period is day number 1. get_metric_column() in
# NoPi_Utils/NoPiUT_Common_Meas.py does not take into account an additional
# column in a concatenated file. This wrapper function increments column number
# returned by get_metric_column() by 1 to match with the column format of a
# concatenated file.
###############################################################################

def get_concat_file_metric_column( metric ) :
  data_column = get_metrics_column( metric )
  data_column += 1

  return( data_column )


###############################################################################
# Top level function to save to disk trend plots comprising of trends
# for each receiver pair in a particular baseline.
###############################################################################

def save_and_close_fig( concat_summ, 
                        trend_dir_name,
                        method,
                        fig_num,
                        **kwargs ) :

  if ( method < 4 ) :
    kwargs.update( { 'show_legend' : True } )
  else :
    kwargs.update( { 'show_legend' : False } )

  figure( fig_num )

  # Format and save auto scale plot
  auto_scale = True
  format_fig( concat_summ,                        
              auto_scale,
              method,
              **kwargs )
  fig_name = gen_fig_name( concat_summ, trend_dir_name, 
                           auto_scale, method, **kwargs )
  savefig( fig_name, format='png' )

  # Format and save fixed scale plot
  auto_scale = False
  format_fig( concat_summ,                        
              auto_scale,
              method,
              **kwargs )
  fig_name = gen_fig_name( concat_summ, trend_dir_name, 
                           auto_scale, method, **kwargs )
  savefig( fig_name, format='png' )

  # Close figure
  close( fig_num )

###############################################################################
# Function formats each long term trends plot. Include plot title,
# axis labels, legend and update axis limits
###############################################################################

def format_fig( concat_summ,
                auto_scale,
                method,
                **kwargs ) :

  title( gen_plot_title( concat_summ, method, **kwargs ) )

  metric = kwargs[ 'metric' ]
  mtype = kwargs[ 'mtype' ]

  #xlabel( 'Days since: %s' % concat_summ.start_date )

  # Work out y-axis label based on meas type and metric
  if ( metric.upper( ) == 'THREE_SIGMA' ) :
    ylabel( '% Epochs > Normative 3-$\sigma$' )
  elif( metric.upper( ) == 'ROB_THREE_SIGMA' ) :
    ylabel( '% Epochs > Robust 3-$\sigma$' )
  elif( metric.upper( ) == 'SKEWNESS' ) :
    ylabel( 'Skewness' )
  else :
    # D.D. carrier phase scaled to mcycles when using S.D. ambiguity
    # resolution. Hence there is no need to pass a value for the resolve_sdiff
    # argument in get_meas_units(). The argument defaults to 0 in the function
    # definition
    ylabel( get_meas_units( mtype ) )

  # Display month and year on xaxis. Major ticks are set to the first day of
  # each month. Minor ticks on the plot corresponds to a week.
  fig = gcf()
  ax  = gca()
  monthsLoc = mpl.dates.MonthLocator()
  weeksLoc = mpl.dates.WeekdayLocator()
  ax.xaxis.set_major_locator(monthsLoc)
  ax.xaxis.set_minor_locator(weeksLoc)
  monthsFmt = mpl.dates.DateFormatter('%b,%y')
  ax.xaxis.set_major_formatter(monthsFmt)
  fig.autofmt_xdate( bottom=0.15 )
  ax.xaxis.grid( True, 'major' )
  ax.yaxis.grid( True )

  # Work out suitable plot axis limits based on fixed or auto-scaled plots
  [ xmin, xmax, ymin, ymax ] = axis( )

  if ( auto_scale ) :
    axis( [ xmin, xmax, ymin-abs( ymin )*0.20, ymax+abs( ymax )*0.20 ] )
  else :
    fxd_ymin, fxd_ymax = get_yaxis_limits(concat_summ, method, **kwargs )

    # Loop through all lines in a plot and plot xs for days which exceed the
    # fixed limit thresholds. Set the marker colour to match each solid line's
    # line colour. This is to help identify which combo/receiver pair exceeded
    # the fixed thresholds
    line_count = len( gca().get_lines() )
    for idx in range( line_count ) :
      # Retrieve each dataset on a plot as a matplotlib.lines.Line2D object. The
      # object contains the x-axis/y-axis data and plot color
      line_obj = gca().get_lines()[ idx ]
      line_color = line_obj.get_color()
      x_data = line_obj.get_xdata()
      y_data = line_obj.get_ydata()
      index_min = find( y_data < fxd_ymin )
      index_max = find( y_data > fxd_ymax )
      # Plot x on days when the data exceeds the fixed limit thresholds
      ax.plot_date( x_data[ index_min ], 
                    ones( len( index_min ) ) * 0.95 * fxd_ymin, 
                    color=line_color, linestyle='None', 
                   marker='x', markersize=7.0, markeredgewidth=1.2 )
      ax.plot_date( x_data[ index_max ], 
                    ones( len( index_max ) ) * 0.95 * fxd_ymax,
                    color=line_color, linestyle='None', 
                    marker='x', markersize=7.0, markeredgewidth=1.2 )
      axis( [ xmin, xmax, fxd_ymin, fxd_ymax ] )

  # Show legend only for methods: 1,2 and 3
  if ( kwargs[ 'show_legend' ] ) :
    if ( auto_scale ) :
      legend( loc='best', ncol=4, mode='expand' )
    else :      
      fig.subplots_adjust(bottom=0.15)
      legend( bbox_to_anchor=( 0.0, -0.20, 1.0, 0.102 ), loc='best', ncol=4, 
              mode='expand', borderaxespad=0.15)

###############################################################################
# Return plot y-axis limits
###############################################################################

def get_yaxis_limits( concat_summ,
                      method,
                      **kwargs ) :

  [ ymin, ymax ] = [ LIM_INVALID, -LIM_INVALID ]

  meas_type = kwargs[ 'mtype' ]
  metric    = metric_list.index( kwargs[ 'metric' ] )
  baseline  = ''
  if 'baseline' in kwargs :
    baseline  = kwargs[ 'baseline' ]

  # Identify appropriate global limit lists based on baseline name
  if ( 'Short' in baseline ) :
    local_min = short_baseline_min
    local_max = short_baseline_max
  elif ( 'Antenna' in baseline or 
         'Relock' in baseline  ) :
    local_min = antenna_switch_min
    local_max = antenna_switch_max
  elif ( 'Zero' in baseline ) :
    local_min = zero_baseline_min
    local_max = zero_baseline_max

  # Methods 1 & 2 are easy. Each combo/meas_type/metric should be an unique
  # limit specified in the corresponding physical baseline axis_limit file
  if ( method in [1, 2] ) :
    combo_idx = kwargs[ 'combo_idx' ]
    ymin = local_min[ combo_idx ][ meas_type ][ metric ]
    ymax = local_max[ combo_idx ][ meas_type ][ metric ]
  # Methods 3 & 4 consolidate all combos in a single plot based on a single
  # receiver pair or over all receiver pairs belonging a specific physical
  # baseline. Must loop over all combos to work out valid axis limits
  elif ( method in [3, 4] ) :
    ymin, ymax = get_ylim_all_combos( concat_summ, local_min, 
                                      local_max, **kwargs )
  # Method 5 combines all combos and all receiver pairs on a single plot. Find
  # "global" min and max values over all combos and physical baseline specific
  # limits
  elif ( method == 5 ) :
    local_min = zero_baseline_min
    local_max = zero_baseline_max
    tmp_ymin, tmp_ymax = get_ylim_all_combos( concat_summ, local_min,
                                              local_max, **kwargs )
    ymin = min( ymin, tmp_ymin )
    ymax = max( ymax, tmp_ymax )
    local_min = short_baseline_min
    local_max = short_baseline_max
    tmp_ymin, tmp_ymax = get_ylim_all_combos( concat_summ, local_min,
                                              local_max, **kwargs )
    ymin = min( ymin, tmp_ymin )
    ymax = max( ymax, tmp_ymax )
    local_min = antenna_switch_min
    local_max = antenna_switch_max
    tmp_ymin, tmp_ymax = get_ylim_all_combos( concat_summ, local_min,
                                              local_max, **kwargs )
    ymin = min( ymin, tmp_ymin )
    ymax = max( ymax, tmp_ymax )

  return( ymin, ymax )
    
###############################################################################
# Loop over all combos and return min and max limits 
###############################################################################

def get_ylim_all_combos( concat_summ, 
                         local_min, 
                         local_max, 
                         **kwargs ) :

  meas_type        = kwargs[ 'mtype' ]
  metric           = kwargs[ 'metric' ]
  metric_idx       = metric_list.index( metric )
  scale_inv_metric = [ 'SKEWNESS', 'THREE_SIGMA', 'ROB_THREE_SIGMA' ]

  [ ymin, ymax ] = [ LIM_INVALID, -LIM_INVALID ]

  # Min and Max values over all valid combos. Both combos will use the default
  # limits. This loop is required to handle exception limits.
  for idx in range( concat_summ.num_combos ) :
    tmp_min = local_min[ idx ][ meas_type ][ metric_idx ] 
    tmp_max = local_max[ idx ][ meas_type ][ metric_idx ] 
    ymin = min( ymin, tmp_min )
    ymax = max( ymax, tmp_max )    
  return( ymin, ymax )
    

###############################################################################
# Generate plot titles for long term trends 
###############################################################################

def gen_plot_title( concat_summ, method, **kwargs ) :
  
  mname = get_meas_title( kwargs['mtype'] )

  plot_title = string.join( [ mname,               
                              'Trends - Metric:',  
                              kwargs['metric'] ]
                          )

  if ( method in [ 1, 2 ] ) :
    combo_name = concat_summ.combo[ kwargs[ 'combo_idx' ] ].combo_name
    combo_name = get_combo_short_name( combo_name )
    combo_name = string.join( string.split( combo_name, '_' ) )
    plot_title = string.join( [ combo_name, plot_title ] )
  elif ( method in [ 3, 4 ] ) :
    baseline = string.split( kwargs[ 'baseline' ], '_' )
    baseline = string.join( baseline )
    plot_title = string.join( [ baseline, plot_title ] )


  return( plot_title )

###############################################################################
# Generate file name for long term trends plot. Plot saved as a 'PNG' image
###############################################################################

def gen_fig_name( concat_summ, trend_dir_name, auto_scale, method, **kwargs ) :
  
  mname = string.capitalize( get_meas_name( kwargs['mtype'] ) )

  fig_name = string.join( [ mname, 
                            kwargs['metric'],       
                          ],               
                          '_' )

  if ( method in [ 1, 2 ] ) :
    combo_name = concat_summ.combo[ kwargs[ 'combo_idx' ] ].combo_name
    combo_name = get_combo_short_name( combo_name )
    fig_name = string.join( [ combo_name,       
                              fig_name ],       
                            '_' )
  elif ( method in [ 3, 4 ] ) :
    fig_name = string.join( [ kwargs[ 'baseline' ],
                              fig_name ],          
                            '_' )

  if ( auto_scale ) :
    fig_name += '_Auto'

  fig_name = os.path.join( trend_dir_name, fig_name )
  fig_name += '.png' 
 
  return( fig_name )


###############################################################################
# Function returns a shorter combo name given a combo name as
# used in the diffs_summary file
###############################################################################

def get_combo_short_name( combo_name ) :

  [ ref_sat_type, ref_sub_type, 
    tst_sat_type, tst_sub_type ] = string.split( combo_name )

  combo_short_name = string.join( [ convert_sat_type( ref_sat_type ),     
                                    convert_sub_type( ref_sub_type ) ],   
                                    '_' )

  if ( ref_sat_type != tst_sat_type ) :
    combo_short_name = string.join( [ tmp_combo_name,                    
                                      convert_sat_type( tst_sat_type ),  
                                      convert_sub_type( tst_sub_type ) ],
                                      '_' )

  return( combo_short_name )
