###############################################################################
# Copyright (c) 2010-2024 Trimble Inc.
# $Id: NoPiDI_Plot.py,v 1.42 2024/06/18 21:35:49 acartmel Exp $
###############################################################################
#
# NoPiDI_Plot.py
#
# This file contains all the functions uses to generate all the plots of the
# NoPi results output
# The plots are saved to disk as PNG files
#
###############################################################################

from pylab import *
from numpy import *
try:
  nanmean([1,2])
except:
  from scipy.stats.stats import nanmean
import gc
from NoPiDI_Utils import add_utils_dir_to_path
add_utils_dir_to_path()

from NoPiUT_Common_Meas import *
from NoPiUT_Common_Display import *
from NoPiDI_Utils import find

import multiprocessing as mp

######################################################################
# A wrapper class to optionally run each requested savefig() in a new
# process (for speed).
######################################################################
class PlotHelper():
  class PlotInfo():
    def __init__(self,proc,fig):
      self.proc = proc
      self.fig = fig

  def __init__(self, allow_async, n_processes=mp.cpu_count()):
    self.allow_async = allow_async
    self.plots = []
    self.n_processes = n_processes

  def async_plotter(self, fig, filename):
    fig.savefig(filename, format='png')

  def save_and_close(self, fig, filename):
    if not self.allow_async:
      # Parallel processing is disabled, so do the plot right now.
      savefig( filename, format = 'png' )
      # Matplotlib does not clear out memory when a figure is closed. One must
      # explicitly invoke clf() to clear memory. close() is required when one uses
      # show() to display a figure
      clf()
      close( fig )
      return

    # Kick off a process for each savefig() command (up to n_processes)
    p = mp.Process(target=self.async_plotter,
                   args=(fig, filename))
    p.start()
    self.plots.append(self.PlotInfo(p,fig))
    while True:
      n_proc = 0
      for curr_plt in list(self.plots): # copy so it is safe to modify
        if curr_plt.proc.is_alive():
          n_proc += 1
        else:
          close(curr_plt.fig)
          curr_plt.proc.join()
          self.plots.remove( curr_plt )
      if n_proc <= self.n_processes:
        break
      # If we get here we've reached the maximum # of parallel plot
      # commands.
      time.sleep(0.1)

  def join(self):
    for p in self.plots:
      p.proc.join()

# Disable interactive mode
ioff()

# Time epochs in NoPi output files are stated in ms
TIME_AXIS_SCALING = 1000.0

# Allow parallel plotting for speed?
def set_parallel_plotting(allow):
  global plot_helper
  if allow:
    print("Allowing parallel plotting")
  plot_helper = PlotHelper(allow_async=allow)

# You'll run much faster by setting this False (but lose per-sv plots)
def set_per_sv_plotting(do_per_sv):
  global do_per_sv_plots
  do_per_sv_plots = do_per_sv
  if not do_per_sv_plots:
    print("Disabling per-SV plots")

def set_sv_plot_slips(do_sv_slips):
  global do_sv_plots_slips
  do_sv_plots_slips = do_sv_slips
  if do_sv_plots_slips:
    print("Show slips on SV plots")

def set_cno_avg_plotting(do_cno_avg):
  global do_cno_avg_plot
  do_cno_avg_plot = do_cno_avg
  if not do_cno_avg_plot:
    print("Suppress C/No average overlay")

def set_y_axis_autoscaling(y_axis_autoscale):
  global do_y_axis_autoscale
  do_y_axis_autoscale = y_axis_autoscale
  if not do_y_axis_autoscale:
    print("Overide the y-axis auto-scaling")

###############################################################################
# Simple function to create a new figure
# Different figure sizes are supported for different plots
###############################################################################

def create_figure( tall_fig ) :
  if ( tall_fig ) :
    tmp_fig = figure( figsize=[8.5, 8.0] )
  else :
    tmp_fig = figure( figsize=[8.5, 6.0] )

  grid( True )

  return( tmp_fig )


###############################################################################
# Simple function to save and close a given figure
# Generates and applies the titles and axis labels
# Saves the figure as a PNG and then closes it
###############################################################################

def save_and_close_figure( fig,
                           diffs_summ,
                           stats,
                           combo,
                           meas_type,
                           plot_type
                         ) :
  figure( fig.number )

  ylabel( get_meas_title( meas_type )
        + ' '
        + get_meas_units( meas_type, diffs_summ.combo[combo].resolve_sdiff )
        )

  if ( plot_type == 'TIME_ACQ_PLOT' ) :
    if ( stats.sv_id == 0 ) :
      sv_id = 'all'
    else :
      sv_id = str( int( stats.sv_id ) )

    # Mean and std deviation for dataset comprising of both mod200 ms and non
    # mod200ms observations
    mean   = stats.data_mean
    sigma  = stats.data_std
    epochs = len(stats.residual) + len(stats.res_mod200)

    title( ' Time Series Acq Analysis SV: '
         + sv_id
         + str( "\n" )
         + r'$\mu=' + str( "%.3f" % mean ) + '$ '
         + r'$\sigma=' + str( "%.3f" % sigma ) + '$ '
         + '$' + get_meas_units( meas_type, diffs_summ.combo[combo].resolve_sdiff ) + '$ '
         + '$epochs=' + str( epochs ) + '$'
         )
  elif ( plot_type == 'BASE_CNO' or plot_type == 'ROVR_CNO' ) :
    title( 'CNo variation with Elevation' )
    ylabel( 'CNo [dB-Hz]' )
  elif ( plot_type == 'REF_SVS_PLOT' ) :
    title( 'D.D. Reference SVs' )
    ylabel( 'Reference SV ID ['
          + convert_sat_type( diffs_summ.combo[combo].ref_sat_type )
          + ']'
          )
  else :
    resolve_sdiff = diffs_summ.combo[ combo ].resolve_sdiff
    title( gen_plot_title( stats, meas_type, resolve_sdiff ) )


  # Work out suitable limits for each plot type
  [ xmin, xmax, ymin, ymax ] = axis( )

  if ( plot_type == 'TIME_PLOT' or plot_type == 'REF_SVS_PLOT' ) :
    xlabel( 'GPS ToW [s]' )

    [ xmin, xmax ] = [ diffs_summ.stime_ms/TIME_AXIS_SCALING,
                       diffs_summ.etime_ms/TIME_AXIS_SCALING
                     ]

    if ( plot_type == 'REF_SVS_PLOT' ) :
      if ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_GPS' ) :
        ymin =   1
        ymax =  32
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_SBAS' ) :
        ymin = 120
        ymax = 158
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_GLN' ) :
        ymin =   1
        ymax =  24
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_GAL' ) :
        ymin =   1
        ymax =  36
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_BDS' ) :
        ymin =   1
        ymax =  63
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_QZSS' ) :
        ymin = 193
        ymax = 202
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_IRNSS' ) :
        ymin =   1
        ymax =  14
      elif ( diffs_summ.combo[combo].ref_sat_type == 'SAT_TYPE_XONA' ) :
        ymin =   1
        ymax = 254

      axis( [ xmin, xmax, ymin - 1, ymax + 1 ] )

  elif ( plot_type == 'ELEV_PLOT'
      or plot_type == 'BASE_CNO'
       ) :
    xlabel( 'Elevation at Base [Deg]' )
    [ xmin, xmax ] = [ -2, 92 ]

  elif ( plot_type == 'ROVR_CNO' ) :
    xlabel( 'Elevation at Rover [Deg]' )
    [ xmin, xmax ] = [ -2, 92 ]

  elif ( plot_type == 'TIME_ACQ_PLOT' ) :
    xlabel( ' Epochs Since Acquisition @ '
          + str( true_divide( diffs_summ.drate_ms, TIME_AXIS_SCALING ) )
          + 's Data Rate '
          )
    [ xmin, xmax ] = [ 0, cNoPiConst.ACQ_ANALYSIS_DUR/diffs_summ.drate_ms + 1 ]

  if ( plot_type != 'REF_SVS_PLOT' ) :
    # Inflate ymin and ymax by 15% to avoid matplotlib collapsing the plotting
    # axis to fit tight.
    if ( ymin < 0 ) :
      ymin *= 1.15
    else :
      ymin *= 0.85
    if ( ymax < 0 ) :
      ymax *= 0.85
    else :
      ymax *= 1.15

  if ( plot_type == 'TIME_PLOT' ) and not do_y_axis_autoscale:
    if meas_type == cNoPiConst.MEAS_TYPE_DD_CARR :
      ymin = min([ymin,-500])
      ymax = max([ymax,+500])
      #ymin = -500
      #ymax = +500
    if meas_type == cNoPiConst.MEAS_TYPE_DD_CODE :
      ymin = min([ymin,-10])
      ymax = max([ymax,+10])
      #ymin = -10
      #ymax = +10
    if meas_type == cNoPiConst.MEAS_TYPE_SD_CNO :
      ymin = min([ymin,-5])
      ymax = max([ymax,+25])
      #ymin = -5
      #ymax = +25

  axis( [ xmin, xmax, ymin, ymax ] )

  pname = gen_plot_name( stats.sv_id, combo, meas_type, plot_type )

  plot_helper.save_and_close( fig, pname )

  # Invoke garbage collector to clear out memory
  gc.collect()


###############################################################################
# Generate the plot for a single satellite
# Includes the results on the common plots for all satellites too
###############################################################################

def plot_diff_data( din,
                    diffs_summ,
                    stats,
                    idx_ref,
                    idx_vld,
                    combo,
                    meas_type,
                    fig_all,
                    fig_all_el,
                    plot_raw_cno_elev = False,
                    plot_ref_svs = False,
                    fig_base_cno_el = '',
                    fig_rovr_cno_el = '',
                    fig_ref_svs = ''
                  ) :

  data_col = get_data_column( diffs_summ, meas_type )
  elev_data_col = get_elev_data_column( 'Base' )
  fcol = get_flags_column( diffs_summ )

  if ( do_per_sv_plots ) :
    fig_sv = create_figure( True )
    fig_sv_el = create_figure( True )

  sv_colour = get_colour( int(stats.sv_id) )

  # Plot epochs when this was the double difference reference SV
  if ( len( idx_ref ) > 2 ) :
    # Insert NaNs into the data for mising epochs to force a 'pen-up' in
    # the plots
    tind  = find( diff( din[idx_ref, 0] ) > diffs_summ.drate_ms )
    taxis = insert( din[idx_ref, 0], tind + 1, din[idx_ref[tind], 0]+1 )
    daxis = insert( din[idx_ref, data_col], tind + 1, nan )

    if ( plot_ref_svs ) :
      figure( fig_ref_svs.number )
      plot( taxis / TIME_AXIS_SCALING, daxis + int(stats.sv_id), 'b.-' )

    if ( do_per_sv_plots ) :
      figure( fig_sv.number )
      plot( taxis / TIME_AXIS_SCALING, daxis, 'r' )
      if do_sv_plots_slips and meas_type == cNoPiConst.MEAS_TYPE_DD_CARR:
        i = find( din[idx_ref, fcol].astype(int) & cNoPiConst.DIFFS_FLG_CYC_SLIP )
        if len(i) > 0:
          plot( taxis[i] / TIME_AXIS_SCALING, daxis[i], 'vk', markeredgecolor='r' )

      figure( fig_sv_el.number )
      elaxis = din[idx_ref, elev_data_col]
      daxis_el = din[idx_ref, data_col]
      plot( elaxis, daxis_el, 'r.', linestyle='' )

  # Plot valid double/single difference measurement data
  if ( len( idx_vld ) > 2 ) :
    # Insert NaNs into the data for mising epochs to force a 'pen-up' in
    # the plots
    tind  = find( diff( din[idx_vld, 0] ) > diffs_summ.drate_ms )
    taxis = insert( din[idx_vld, 0], tind + 1, din[idx_vld[tind], 0] )
    daxis = insert( din[idx_vld, data_col], tind + 1, nan )
    elaxis   = din[idx_vld, elev_data_col]
    daxis_el = din[idx_vld, data_col]

    # Find the points surrounded by NaNs to plot as dots
    dots = []
    if len(tind):
      aind = tind.copy()
      adj = 1
      for x in range(len(aind)):
        aind[x] = aind[x] + adj
        adj += 1

      tmp1 = diff(aind)
      tmp2 = find(tmp1 == 2)
      dots = aind[tmp2+1]-1
      # Also find if the first point has NaN after it
      if aind[0] == 1:
        dots = insert(dots, 0, 0)
      # Or if the last point has NaN before it
      if aind[-1] == len(daxis)-2:
        dots = append(dots, len(daxis)-1)
      #print(dots)

    if ( do_per_sv_plots ) :
      figure( fig_sv.number )
      plot( taxis /TIME_AXIS_SCALING, daxis, 'b' )
      for x in dots:
        plot( taxis[x]/TIME_AXIS_SCALING, daxis[x], 'b', marker='.' )

      if do_sv_plots_slips and meas_type == cNoPiConst.MEAS_TYPE_DD_CARR:
        i = find( din[idx_vld, fcol].astype(int) & cNoPiConst.DIFFS_FLG_CYC_SLIP )
        if len(i) > 0:
          plot( taxis[i] / TIME_AXIS_SCALING, daxis[i], 'vk', markeredgecolor='r' )

      figure( fig_sv_el.number )
      plot( elaxis, daxis_el, 'b.', linestyle='' )

    figure( fig_all.number )
    plot( taxis/TIME_AXIS_SCALING, daxis, color=sv_colour )
    for x in dots:
      plot( taxis[x]/TIME_AXIS_SCALING, daxis[x], marker='.', color=sv_colour )
    if do_sv_plots_slips and meas_type == cNoPiConst.MEAS_TYPE_DD_CARR:
      i = find( din[idx_vld, fcol].astype(int) & cNoPiConst.DIFFS_FLG_CYC_SLIP )
      if len(i) > 0:
        plot( taxis[i] / TIME_AXIS_SCALING, daxis[i], 'vk', markeredgecolor='r', zorder=3 )

    figure( fig_all_el.number )
    plot( elaxis, daxis_el, linestyle='', marker='.', color=sv_colour )

  # Generate raw CNo versus Elevation plots for each combo. Executed only if
  # NoPi output mtb files contain both base & rover azimuth/elevation/CNo data
  if ( plot_raw_cno_elev ) :
    data_scale = get_data_scale( meas_type )
    # Plot raw base CNo versus elevation
    figure( fig_base_cno_el.number )
    base_el_col = get_elev_data_column( 'Base' )
    base_cno_col = get_cno_data_column( 'Base' )
    plot( din[ :, base_el_col ],
          din[ :, base_cno_col ] * data_scale,
          linestyle='',
          marker='x',
          color=sv_colour
        )

    # Plot raw rover CNo versus elevation
    figure( fig_rovr_cno_el.number )
    rovr_el_col = get_elev_data_column( 'Rover' )
    rovr_cno_col = get_cno_data_column( 'Rover' )
    plot( din[ :, rovr_el_col ],
          din[ :, rovr_cno_col ] * data_scale,
          linestyle='',
          marker='x',
          color=sv_colour
        )

  if ( do_per_sv_plots ) :
    save_and_close_figure( fig_sv,
                           diffs_summ,
                           stats,
                           combo,
                           meas_type,
                           'TIME_PLOT'
                         )
    save_and_close_figure( fig_sv_el,
                           diffs_summ,
                           stats,
                           combo,
                           meas_type,
                           'ELEV_PLOT'
                         )


###############################################################################
# Update time series S.D. CNo versus plots with avg. S.D. CNo. Average values
# computed using a 120 second sliding window commencing from the processing
# start time specified in the diffs summary file
###############################################################################

def plot_time_avg_sdiff_cno( diffs_summ, fig ) :

  if(do_cno_avg_plot == False):
    return

  AVG_WINDOW_DUR = 120.0

  # Work out 120 second sliding window intervals
  processing_dur = (diffs_summ.etime_ms-diffs_summ.stime_ms)/TIME_AXIS_SCALING
  avg_cno_timetags = arange( diffs_summ.stime_ms/TIME_AXIS_SCALING,
                             diffs_summ.etime_ms/TIME_AXIS_SCALING + 1,
                             processing_dur/AVG_WINDOW_DUR
                           )

  avg_sd_cno = ones( len( avg_cno_timetags ) - 1 )*nan

  figure( fig.number )
  sv_count = len( gca().get_lines() )

  # Retrieve data for each SV from the CNo versus Elevation plot. Work out the
  # average CNo for each observed elevation.
  for sv in range( sv_count ) :
    line_obj = gca().get_lines()[sv]
    x_data = line_obj.get_xdata()
    y_data = line_obj.get_ydata()

    # Compute average S.D. CNo for each 120 second sliding window
    for timetag_idx in range( len( avg_cno_timetags ) - 1 ) :
      # Find all observations within the current 120 second window
      data_idx = find( (x_data[:] >= avg_cno_timetags[ timetag_idx ])\
                       & (x_data[:] <= avg_cno_timetags[ timetag_idx + 1 ]) )
      # Compute avg. SD CNo over all previously previously processed satellites
      if ( len( data_idx ) > 0 ) :
        cur_sv_sd_cno = mean( y_data[ data_idx ] )
        if isnan(avg_sd_cno[timetag_idx]):
          # If avg_sd_cno==nan then nanmean() will generate a warning because
          # it only has 1 valid value.  Avoid that warning...
          avg_sd_cno[timetag_idx]=cur_sv_sd_cno
        else:
          tmp_data = [ cur_sv_sd_cno, avg_sd_cno[timetag_idx] ]
          avg_sd_cno[timetag_idx]=nanmean( tmp_data )

  # Plot avg. CNo in the CNo versus elevation plot. The avg. value is
  # shown at the middle of averaging window duration
  plot( [ time - AVG_WINDOW_DUR/2 for time in avg_cno_timetags[1:] ],
        avg_sd_cno,
        'rx',
        markersize=9.0,
        markeredgewidth=1.5
      )

  del avg_cno_timetags, avg_sd_cno


###############################################################################
# Update Base/Rover CNo versus elevation plots with avg. CNo versus elevation
# values
# Average CNo is calculated over CNo for all SVs in 1 degree elevation
# increments
###############################################################################

def plot_avg_cno_elev( fig ) :

  MAX_ELEV = 91 # Avg CNo is range [0:90] degrees elevation
  elev_lim = range( MAX_ELEV )
  avg_cno  = ones( MAX_ELEV )*nan

  figure( fig.number )
  sv_count = len( gca().get_lines() )

  # Retrieve data for each SV from the CNo versus Elevation plot. Work out the
  # average CNo for each observed elevation.
  for sv in range( sv_count ) :
    line_obj = gca().get_lines()[sv]
    x_data = line_obj.get_xdata()
    y_data = line_obj.get_ydata()
    for elev in elev_lim :
      data_idx = find( x_data[:] == elev )
      if ( len( data_idx ) > 0 ) :
        cur_sv_avg_cno = mean( y_data[ data_idx ] )
        avg_cno[ elev ] = nanmean( [ cur_sv_avg_cno, avg_cno[ elev ] ] )

  # Plot avg. CNo in the CNo versus elevation plot
  plot( elev_lim, avg_cno, 'rx', markersize=9.0, markeredgewidth=1.5 )

  del elev_lim, avg_cno


###############################################################################
# Generate the plot for a single satellite
# Includes the results on the common plots for all satellites too
###############################################################################

def plot_acq_data( diffs_summ,
                   acq_stats,
                   fig_acq_all,
                   combo,
                   meas_type
                 ) :

  fig_acq_sv = create_figure( True )

  if ( ( acq_stats.acq_epochs > 0 ) ) :
    plot( acq_stats.res_epoch,
          acq_stats.residual,
          linestyle='',
          marker='x',
          color='m'
        )
    plot( acq_stats.mod200_epoch,
          acq_stats.res_mod200,
          linestyle='',
          color='r',
          marker='x'
        )

    figure( fig_acq_all.number )
    sv_colour = get_colour( int(acq_stats.sv_id) )

    plot( acq_stats.res_epoch,
          acq_stats.residual,
          linestyle='',
          marker='x',
          color=sv_colour
        )
    plot( acq_stats.mod200_epoch,
          acq_stats.res_mod200,
          linestyle='',
          marker='x',
          color=sv_colour
        )
  else :
    figure( fig_acq_sv.number )
    plot( range( 0, int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          zeros( int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          'r'
        )

  save_and_close_figure( fig_acq_sv,
                         diffs_summ,
                         acq_stats,
                         combo,
                         meas_type,
                         'TIME_ACQ_PLOT'
                       )


###############################################################################
# Generate the distribution plots for a single single combo
###############################################################################

def dist_diff_data( din,
                    diffs_summ,
                    stats,
                    idx_vld,
                    combo,
                    meas_type ) :

  fig_hist = create_figure( False )

  if ( len( idx_vld ) > 2 ) :
    data_col = get_data_column( diffs_summ, meas_type )

    hist( din[ idx_vld, data_col ], 40, align='mid', density=False )

    resolve_sdiff = diffs_summ.combo[ combo ].resolve_sdiff
    title( gen_plot_title( stats, meas_type, resolve_sdiff ) )

    xlabel( get_meas_title( meas_type )
          + ' '
          + get_meas_units( meas_type,diffs_summ.combo[combo].resolve_sdiff )
          )

    ylabel('Distribution [count]')

    pname = gen_plot_name( stats.sv_id, combo, meas_type, 'DIST' )
    plot_helper.save_and_close( fig_hist, pname )


###############################################################################
# Generate the distribution plots for a single single combo
###############################################################################

def dist_acq_data( diffs_summ,
                   acq_stats,
                   combo,
                   meas_type ) :

  fig_hist = create_figure( False )
  fig_hist_mod200 = create_figure( False )

  figure( fig_hist.number )

  # Need 2 or more points to plot a histogram
  if ( ( acq_stats.acq_epochs > 0 ) and ( len( acq_stats.residual ) > 1 ) ) :
    hist( acq_stats.residual, 40, align='mid', density=False )
  else :
    plot( range( 0, int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          zeros( int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          'r'
        )

  title( gen_acq_hist_title( diffs_summ,
                             acq_stats,
                             combo,
                             meas_type,
                             'DIST_RES'
                           )
       )

  xlabel( get_meas_title( meas_type )
        + ' '
        + get_meas_units( meas_type, diffs_summ.combo[combo].resolve_sdiff )
        )

  ylabel('Distribution [count]')

  pname = gen_plot_name( acq_stats.sv_id, combo, meas_type, 'DIST_RES' )
  plot_helper.save_and_close( fig_hist, pname )

  figure( fig_hist_mod200.number )

  # Need 2 or more points to plot a histogram
  if ( ( acq_stats.acq_epochs > 0 ) and ( len(acq_stats.res_mod200) > 1 ) ):
    hist( acq_stats.res_mod200, 40, align='mid', density=False )
  else :
    plot( range( 0, int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          zeros( int(divide(cNoPiConst.ACQ_ANALYSIS_DUR, diffs_summ.drate_ms))+1 ),
          'r'
        )

  title( gen_acq_hist_title( diffs_summ,
                             acq_stats,
                             combo,
                             meas_type,
                             'DIST_RES_MOD200'
                           )
       )

  resolve_sdiff = diffs_summ.combo[ combo ].resolve_sdiff

  xlabel( get_meas_title( meas_type )
        + ' '
        + get_meas_units( meas_type, resolve_sdiff )
        )

  ylabel( 'Distribution [count]' )

  pname = gen_plot_name( acq_stats.sv_id, combo, meas_type, 'DIST_RES_MOD200' )
  plot_helper.save_and_close( fig_hist_mod200, pname )


###############################################################################
# Generate the title of the plot
# Includes the statistics information on the data
###############################################################################

def gen_plot_title( stats,
                    meas_type,
                    resolve_sdiff
                  ) :

  label = get_meas_title( meas_type )

  if ( stats.sv_id != 0 ) :
    label += ' SV:' + str( int( stats.sv_id ) )

  label += ( str( "\n" )
           + '$min=' + str( "%.3f" % stats.vld_min) + '$ '
           + '$max=' + str( "%.3f" % stats.vld_max) + '$ '
           + r'$\mu=' + str( "%.3f" % stats.vld_mean ) + '$ '
           + r'$\sigma=' + str( "%.3f" % stats.vld_std ) + '$ '
           + '$mav=' + str( "%.3f" % stats.vld_mav) + '$ '
           + '$' + get_meas_units( meas_type, resolve_sdiff ) + '$ '
           + '$epochs=' + str( stats.vld_epochs ) + '$'
           )

  return( label )


###############################################################################
# Generate the file name used to save the plot
###############################################################################

def gen_plot_name( sat_id,
                   combo,
                   meas_type,
                   plot_type
                 ) :

  if ( sat_id == 0 ) :
    sv_id = 'all'
  else :
    sv_id = str( int( sat_id ) )

  tmp_pname = ( get_meas_name( meas_type )
              + '_'
              + sv_id
              + '_'
              + str( int( combo ) )
              + '.png'
              )

  if ( plot_type == 'TIME_PLOT' ) :
    pname = 'time_' + tmp_pname
  elif ( plot_type == 'ELEV_PLOT' ) :
    pname = 'elev_' + tmp_pname
  elif ( plot_type == 'DIST' ) :
    pname = 'dist_' + tmp_pname
  elif ( plot_type == 'DIST_RES' ) :
    pname = 'dist_res_' + tmp_pname
  elif ( plot_type == 'DIST_RES_MOD200' ) :
    pname = 'dist_res_mod200_' + tmp_pname
  elif ( plot_type == 'TIME_ACQ_PLOT' ) :
    pname = 'time_acq_' + tmp_pname
  elif ( plot_type == 'BASE_CNO' ) :
    pname = 'base_cno_' + str( int( combo ) ) + '.png'
  elif ( plot_type == 'ROVR_CNO' ) :
    pname = 'rovr_cno_' + str( int( combo ) ) + '.png'
  elif ( plot_type == 'REF_SVS_PLOT' ) :
    pname = 'ref_svs_all_' + str( int( combo ) ) + '.png'

  # Include the plots directory name
  pname = 'plots/' + pname

  return( pname )


###############################################################################
# Generate title for Acquisition Analysis histogram
# Includes the statistics information on the data
###############################################################################

def gen_acq_hist_title( diffs_summ,
                        stats,
                        combo,
                        meas_type,
                        plot_type ) :

  label = get_meas_title( meas_type )

  if ( stats.sv_id != 0 ) :
    label = label + ' SV: ' + str( int( stats.sv_id ) )
  else :
    label = label + ' SV: All'

  tmp_str = ( str('\n')
            + str( divide( cNoPiConst.ACQ_ANALYSIS_DUR, 1000 ) )
            + 's Acq Analysis : '
            )

  resolve_sdiff = diffs_summ.combo[combo].resolve_sdiff

  if ( plot_type == 'DIST_RES_MOD200' ):
    label += ( tmp_str
             + 'Mod 200 ms epochs'
             + str( '\n' )
             + r'$\mu=' + str( "%.3f" % stats.mean_mod200 ) + '$ '
             + r'$\sigma=' + str( "%.3f" % stats.std_mod200 ) + '$ '
             + '$' + get_meas_units( meas_type, resolve_sdiff ) + '$ '
             + '$epochs=' + str( len(stats.res_mod200) ) + '$'
             )
  else :
    label += ( tmp_str
             + 'non-Mod 200 ms epochs'
             + str( '\n' )
             + r'$\mu=' + str( "%.3f" % stats.mean_res ) + '$ '
             + r'$\sigma=' + str( "%.3f" % stats.std_res ) + '$ '
             + '$' + get_meas_units( meas_type, resolve_sdiff ) + '$ '
             + '$epochs=' + str( len(stats.residual) ) + '$'
             )

  return( label )
