//********************************************************************************
//* File       : EpParse.cpp                                                     *
//* Author     : Mahlon R. Smith                                                 *
//*              Copyright (c) 2022-2026 Mahlon R. Smith, The Software Samurai   *
//*                 GNU GPL copyright notice below                               *
//* Date       : 20-Dec-2025                                                     *
//* Version    : (see AppVersion string)                                         *
//*                                                                              *
//* Description: Support file for EarthPoints class.                             *
//*              Parse command-line arguments and parse contents of file         *
//*              containing test records.                                        *
//********************************************************************************

//*****************
//* Include Files *
//*****************
#include "EarthPoints.hpp"    //* Application-class definition

//***************
//* Definitions *
//***************
#if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0
//* Text corresponding to enum xCode.*
// Programmer's Note: Indexing this array requires a trick. See gclaParseRecord().
static const char * xcodeText[] = 
{ "xcGOOD", "xcOOR", "xcFNF", "xcRNF", "xcRSYN", "xcFCMD" } ;
#endif   // DEBUG_EPNTS && DEBUG_PARSE

//**************
//* Local data *
//**************
//* Text corresponding to enum valFmt.                 *
//* Used by DisplayConvData() AND by debugging methods.*
static const char * valFmtText[] = 
{ "vfUnk", "vfDec", "vfMin", "vfSec", "vfRad" } ;


//********************
//* Local prototypes *
//********************
static short gclaAttrScan ( gString& gsAttr ) ;


//*************************
//*  GetCommandLineArgs   *
//*************************
//********************************************************************************
//* Capture user's command-line arguments.                                       *
//* Valid Arguments: see DisplayHelp() method.                                   *
//*                                                                              *
//*                                                                              *
//* Input  : argCount : number of elements in 'argList' array                    *
//*          argList  : array of command-line arguments                          *
//*          argEnv   : array of environment variables                           *
//*                                                                              *
//* Returns: member of enum clArgs                                               *
//********************************************************************************
//* Programmer's Note: Even though it is not documented, we accept both          *
//* lowercase and uppercase option and sub-option characters where possible.     *
//* This allows us to add new, case-sensitive options at a later date.           *
//*                                                                              *
//* If user is foolish enough to specify coordinates using BOTH the              *
//* '--coa' or '--cob' options AND using the --file option, he/she/they will     *
//* likely be surprised at the results because the later one will overwrite      *
//* the earlier one.                                                             *
//********************************************************************************

clArgs EarthPoints::GetCommandLineArgs ( commArgs& ca )
{
   #if DEBUG_EPNTS != 0
   #if DEBUG_VERBOSE != 0
   this->gclaSysInfo () ;  //* Display system info *
   #endif   // DEBUG_VERBOSE

   #define DEBUG_GCLA (0)
   #if DEBUG_GCLA != 0
   gString gsdbg ;
   bool dbgReport = false ;
   #endif   // DEBUG_GCLA
   #endif   // DEBUG_EPNTS

   clArgs userOption = clQuickHelp ;   // return value (default == short help)


   //* If user provided command-line arguments, parse them. *
   //* Note that first argument is application name.        *
   if ( ca.argCount > 1 )
   {
      gString gs ;               // text formatting
      short j = ZERO ;           // for multiple arguments in same token
      char  argLetter ;          // short-form arguments
      bool  multiarg = false ;   // 'true' if concatenated arguments
      bool  syntax = false ;     // 'true' if syntax error(s) on command line

      for ( short i = 1 ; (i < ca.argCount) || (multiarg != false) ; i++ )
      {
         #if DEBUG_GCLA != 0
         if ( dbgReport )
            wcout << gsdbg.gstr() << endl ;
         gsdbg.compose( "%hd) '%s'", &i, ca.argList[i] ) ;
         dbgReport = true ;
         #endif   // DEBUG_GCLA

         // Programmer's Note: For this application, concatenated arguments 
         // are not practical, so 'multiarg' should not be invoked.
         if ( multiarg != false )   // if additional switches in same argument
            --i ;
         else
            j = ZERO ;

         //* If a command-line switch OR continuing previous switch argument *
         if ( ca.argList[i][j] == DASH || multiarg != false )
         {
            multiarg = false ;
            ++j ;

            if ( ca.argList[i][j] == DASH ) // (double dash)
            {  //* Long-form command switches *
               gs = ca.argList[i] ;

               //* Display application version number and copyright  *
               //* information, then exit.                           *
               //* Note: Full option: "--version", but only six(6)   *
               //*       characters are required.                    *
               if ( (gs.compare( L"--vers", true, 6 )) == ZERO )
               {  // Programmer's Note: Request for version number overrides
                  // all other options and error conditions. Among other things,
                  // this is because the documentation recommends that this 
                  // command be used to test the application build.
                  if ( (gs.find( L'a', false )) > ZERO )
                     userOption = clVersiona ;  // include AnsiCmd version
                  else
                     userOption = clVersion ;   // application version only
                  break ;     // ignore any remaining arguments
               }

               //* Invoke command-line help *
               else if ( (gs.compare( L"--help", true, 6 )) == ZERO )
               {  // Programmer's Note: Request for help overrides all options 
                  // except '--version'. Note however that parsing of remaining 
                  // arguments continues after receipt of a help request.
                  if ( (gs.find( L"less" )) > ZERO )
                     userOption = clHelpless ;
                  else
                     userOption = clHelp ;
                  continue ;   // finished with this option
               }

               //* Enable verbose output.                            *
               //* Note: Full option: "--verbose", but only six(6)   *
               //*       characters are required.                    *
               else if ( (gs.compare( L"--verb", true, 6 )) == ZERO )
               {
                  this->verboseOutput = true ;
                  continue ;   // finished with this option
               }

               //* Format the arc-distance report within an ACWin object.*
               //* Note: Full option: "--window", but only six(6)        *
               //*       characters are required.                        *
               //* Note: Arguments to the '--window' option are optional,*
               //* so if no '=' found, then assume no arguments.         *
               else if ( (gs.compare( L"--wind", true, 6 )) == ZERO )
               {
                  this->windowOutput = true ;
                  short offset, r, c ;
                  if ( (offset = gs.after( L'=' )) > ZERO )
                  {
                     gs.shiftChars( -offset ) ;
                     if ( (gs.gscanf( "%hd : %hd", &r, &c )) == 2 )
                     {
                        //* Positive or zero values only.           *
                        //* (we don't know terminal dimensions yet) *
                        if ( r < ZERO )   r = 1 ;
                        if ( c < ZERO )   c = 1 ;
                        this->winpos = { r, c } ;
                     }
                  }
                  continue ;   // finished with this option
               }

               //* Capture the option switch and all of its arguments.      *
               //* If option and arguments are not separated by a '=',      *
               //* append the next token which SHOULD contain all arguments *
               //* because user SHOULD HAVE enclosed all elements of the    *
               //* argument within single or double quotes.                 *
               //* Note, however, that users are simple creatures, so if the*
               //*       next token is not a complete GPS record (or other  *
               //*       option/argument sequence), chaos may ensue... i.e. *
               //*       parsing of the sequence will fail.                 *
               if ( ((gs.find( L'=' )) < ZERO) && 
                    ((i + 1) < ca.argCount) && (ca.argList[i][0] != '-') )
                     gs.append( "=%s", ca.argList[++i] ) ;

               #if DEBUG_GCLA != 0
               gsdbg.compose( "%hd) '%S'", &i, gs.gstr() ) ;
               wcout << gsdbg.gstr() << endl ;
               dbgReport = false ;
               #endif   // DEBUG_GCLA

               //* 'coa' or 'cob' coordinate pair. Parse the argument(s) *
               //* and store them in the associated data member.         *
               if ( ((gs.compare( L"--coa", true, 5 )) == ZERO) ||
                    ((gs.compare( L"--cob", true, 5 )) == ZERO) )
               {
                  argLetter = tolower( gs.ustr()[4] ) ;
                  if ( (this->gclaParseRecord ( gs, 
                        ((argLetter == 'a') ? &this->coord_a : &this->coord_b))) )
                  {
                     //* If neither Help nor Version was invoked, *
                     //* then ready to calculate results.         *
                     if ( userOption == clQuickHelp )
                        userOption = clArc ;
                  }
               }  // (--coa || --cob)

               //* Extract GPS coordinate records from a file *
               else if ( (gs.compare( L"--file", true, 6 )) == ZERO )
               {
                  //* Scan the specified file for the two(2) specified    *
                  //* GPS location records.                               *
                  //* 'coord_a' and 'coord_b' receive the decoded records.*
                  //* If error, 'exitCode' receives error type.           *
                  if ( (this->gclaScanRecordFile ( gs )) )
                  { /* Nothing to be done at this time. */ }

                  //* If neither help nor version were specified, *
                  //* signal caller to calculate distance.        *
                  if ( userOption == clQuickHelp )
                     userOption = clArc ;
               }  // ("--file")

               //* Select the formula to be used for calculating the *
               //* distance between the specified coordinates.       *
               //* Note: Full option: "--formula", but only six(6)   *
               //*       characters are required.                    *
               else if ( (gs.compare( L"--form", true, 6 )) == ZERO )
               {
                  if ( !(this->gclaSelectFormula ( gs )) )
                  { /* Nothing to be done at this time. */ }
               }

               //* Decode a single latitude or longitude value and *
               //* display it in all supported formats.            *
               //* Primarily used for debugging, but may be useful *
               //* for curious users.                              *
               //* Note: Full option: "--convert", but only six(6)   *
               //*       characters are required.                    *
               else if ( (gs.compare( L"--conv", true, 6 )) == ZERO )
               {
                  //* Parse the specified value.         *
                  //* If error, 'exitCode' indicates the *
                  //* type of error (xcOOR or xcRSYN).   *
                  if ( !(this->gclaScanValue ( gs, &this->coord_a.lat )) )
                  { /* Nothing to be done at this time. */ }

                  if ( userOption == clQuickHelp )
                     userOption = clConvert ;
               }

               else if ( (gs.compare( L"--color", true, 7 )) == ZERO )
               {
                  //* If syntax error, defaults will be returned.*
                  if ( !(this->gclaColorScheme ( gs, ca.tConfig )) ) // parse the command
                  { /* Nothing to be done at this time. */ }
                  this->fgndAttr = ca.tConfig.foreground ;  // remember foreground
                  this->bgndAttr = ca.tConfig.background ;  // remember background
               }

               else if ( (gs.compare( L"--term", true, 6 )) == ZERO )
               {
                  //* Important Note: This call is to a non-member method  *
                  //* within the AnsiCmd library. This is done to simplify *
                  //* application-level code for parsing command-line args.*
                  if ( !(ansicmdSetup ( gs, ca.tConfig )) )
                  { /* Nothing to be done at this time. */ }
               }

               //* Validate contents of specified file containing *
               //* GPS coordinate records.                        *
               else if ( (gs.compare( L"--dump", true, 6 )) == ZERO )
               {
                  if ( userOption == clQuickHelp )
                  {
                     //* Extract and store the target filespec *
                     short off ;
                     if ( ((off = gs.after( L'=' )) > ZERO) &&
                          (gs.gstr()[off] != L'\0') )
                     {
                        gs.shiftChars( -(off) ) ;
                        gs.copy( this->fspec, gsALLOCMIN ) ;
                     }
                     else     // valid filespec not specified
                        this->exitCode = xcFNF ;

                     //* If neither help nor version were specified, *
                     //* signal caller to calculate distance.        *
                     if ( userOption == clQuickHelp )
                        userOption = clDump ;
                  }
               }

               //* Test the commands of the AnsiCmd class.                     *
               //* These are the ANSI escape sequences && ASCII control codes. *
               //* AnsiCmd tests override application-level functionality      *
               //* (except 'help' and 'version' commands).                     *
               // Programmer's Note: Early versions of the application used "ansi" 
               // as the switch to invoke tests. The switch has been changed 
               // to "--test"; however, the original switch is still recognized.
               else if ( ((gs.compare( L"--test", true, 6 )) == ZERO) ||
                         ((gs.compare( L"--ansi", true, 6 )) == ZERO) ) // (deprecated)
               {
                  if ( (userOption != clHelp) && (userOption != clHelpless) )
                  {
                     ansicmdSetup ( gs, this->act ) ;
                     userOption = clAnsi ;
                  }
               }  // (--test)

               //* Invalid long-form option.      *
               //* Ignore all remaining arguments.*
               else
               {
                  if ( (userOption != clHelp) && (userOption != clHelpless) )
                  {
                     userOption = clQuickHelp ;
                     syntax = true ;
                  }
               }
               continue ;     // finished with this argument

            }  // (double dash)

            //**************************
            //** Short-form Arguments **
            //**************************
            argLetter = ca.argList[i][j] ;

            #if DEBUG_GCLA != 0
            gsdbg.compose( "%hd) '%s'", &i, &ca.argList[i][j] ) ;
            wcout << gsdbg.gstr() << endl ;
            dbgReport = false ;
            #endif   // DEBUG_GCLA

            //** Verbose Output **
            if ( (argLetter == 'v') || (argLetter == 'V') )
            {
               this->verboseOutput = true ;
            }

            //** Short-form Help **
            else if ( (argLetter == 'h') || (argLetter == 'H') )
            {
               if ( ca.argList[i][j+1] == 'l' )
                  userOption = clHelpless ;
               else
                  userOption = clHelp ;
            }

            //* Invalid short-form option.     *
            //* Ignore all remaining arguments.*
            else
            {
               if ( (userOption != clHelp) && (userOption != clHelpless) )
               {
                  userOption = clQuickHelp ;
                  syntax = true ;
               }
            }
         }

         //* An argument was found that neither begins with      *
         //* a dash ('-') nor is it associated with the previous *
         //* argument. We cannot continue parsing the arguments. *
         else
         {
            if ( (userOption != clHelp) && (userOption != clHelpless) )
            {
               userOption = clQuickHelp ;
               syntax = true ;
            }

            #if DEBUG_GCLA != 0
            gs = ca.argList[i] ;
            gsdbg.append( "  Error: '%S'", gs.gstr() ) ;
            #endif   // DEBUG_GCLA
         }
      }     // for(;;)

      //* If syntax error(s) and no request for Help or Version *
      //* return request for Quick Help.                        *
      if ( syntax && ((userOption != clHelpless) && (userOption != clHelp) &&
                      (userOption != clVersiona) && (userOption != clVersion)) )
         userOption = clQuickHelp ;

      #if DEBUG_GCLA != 0
      wcout << L"Press Enter..." ; wcout.flush() ; getchar() ; // wait for user response
      #endif   // DEBUG_GCLA
   }
   return userOption ;

}  //* End GetCommandLineArgs() *

//*************************
//*    gclaParseRecord    *
//*************************
//********************************************************************************
//* Parse a position coordinate pair into its component parts: label, latitude   *
//* and longitude.                                                               *
//*                                                                              *
//* Input  : gaRec   : (by reference) coordinate record as a text string         *
//*          cPtr    : pointer to an gpsCoord instance where parsed value        *
//*                    is to be stored                                           *
//*                                                                              *
//* Returns: 'true'  if conversion successful                                    *
//*          'false' if parsing error (bad syntax) or value out-of-range         *
//*                  The 'exitCode' member will be set to indicate error.        *
//********************************************************************************
//* A "record" takes the form:                                                   *
//* [--coa= | --cob=][LABEL:]LATITUDE:LONGITUDE                                  *
//*                                                                              *
//* [--coa= | --cob=] is present only if the record was specified on the         *
//*                   command line                                               *
//* [LABEL:]    is optional, and if specified is the human-readable name of      *
//*             the location specified by the latitude/longitude pair and is     *
//*             separated from the numerical data by a colon (':').              *
//* LATITUDE    latitude in one of the four accepted formats (see below)         *
//* :           colon character separates the latitude and longitude values.     *
//* LONGITUDE   longitude in one of the four accepted formats (see below)        *
//*                                                                              *
//*   * Whitespace between data fields is ignored.                               *
//*                                                                              *
//*                                                                              *
//* Latitude and longitude value formats:                                        *
//* -------------------------------------                                        *
//* Decimal Degrees: 48.3645                                                     *
//* Whole-number Degrees + Decimal Minutes: -48 36.4505                          *
//* Whole-number Degrees and Minutes + Decimal Seconds: 48 36 45.2864            *
//* Decimal Radians: 5.246801  (radians are expressed as positive values only)   *
//*                                                                              *
//* Latitude and Longitude range:                                                *
//* Degrees:       -180.0 <= value <= 180.0                                      *
//* Radians:          0.0 <= value <= 6.283185307                                *
//* Minutes:          0.0 <= value <= 60.0                                       *
//* Seconds:          0.0 <= value <= 60.0                                       *
//*   * The sign of the degrees value is the sign of the value. If minutes or    *
//*     seconds are specified separately, they will be silently recorded as      *
//*     ABSOLUTE values without generating a range error.                        *
//*                                                                              *
//*                                                                              *
//* Validating record syntax:                                                    *
//* -------------------------                                                    *
//* Syntax is validated by the presence (or absence) of the delimiters (':')     *
//* and by their positions within the record.                                    *
//*                                                                              *
//* colon1   colon2   compare  Explanation                                       *
//* ------   ------   -------  --------------------------                        *
//* -1      -1        c1==c2   bad syntax, cannot parse                          *
//* ==0     -1                 lat:lon delimiter missing, cannot parse           *
//* >0      -1                 no label present, c1 delimits lat:lon             *
//* >=0     >=0       c1==c2   this would indicate broken code                   *
//* ==0     >0        c2 > c1  null label, discard c1                            *
//* >0      >0        c2 > c1  scan for label first then latitude:longitude      *
//*                                                                              *
//********************************************************************************

bool EarthPoints::gclaParseRecord ( const gString& gsRec, gpsCoord *cPtr )
{
   gString gs, gslab, gslat, gslon ;   // text formatting
   short offset,                       // offsets into source string
         colon1, colon2 ;              // position of delimiters
   bool syntaxOK = false ;             // 'true' if valid record syntax

   cPtr->reset() ;               // initialize the target object

   //* Discard the command switch and the switch/argument delimiter, *
   //* if present. (Note: this is a case-insensitive search.)        *
   if ( ((gsRec.find( L"--coa=" )) == ZERO ) || ((gsRec.find( L"--cob=" )) == ZERO) )
   {
      offset = gsRec.after( L'=' ) ;
      gs = &gsRec.gstr()[offset] ;
   }
   //* Copy the source record unmodified *
   else
      gs = gsRec ;

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0
   wcout << L"gclaParseRecord(" << gs.gstr() << L")\n" ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE

   //* Validate the syntax as indicated by the position *
   //* of the delimiters. (see notes above)             *
   colon1 = gs.find( L':' ) ;
   if ( colon1 >= ZERO )
      colon2 = gs.find( L':', (colon1 + 1) ) ;

   //* If both delimeters are present, capture the label field.*
   if ( (colon1 >= ZERO) && (colon2 > colon1) )
   {
      gs.substr( gslab, ZERO, (colon1 + 1) ) ;  // capture the raw label string
      gslab.erase( L':' ) ;                     // remove the delimiter
      gslab.strip() ;                           // remove leading/trailing whitespace
      if ( (gslab.gschars()) > 1 )              // if non-empty, save the label
         gslab.copy( cPtr->label, LABEL_LEN ) ;
      gs.shiftChars( -(colon1 + 1) ) ;          // discard the processed data
      gs.strip() ;
      //* 'gs' now contains only the coordinate data *
      colon2 = gs.find( L':' ) ;                // index the lat:lon delimiter
      syntaxOK = true ;
   }
   //* Only one delimiter present. We must assume that label was not     *
   //* specified and that the delimiter separates latitude and longitude.*
   else if ( (colon1 > ZERO) && (colon2 < ZERO) )
   {
      colon2 = colon1 ;
      colon1 = -1 ;
      syntaxOK = true ;
   }
   else                    // syntax error colon(s) missing
      this->SetWarningCode ( xcRSYN ) ;

   //* If lat:lon delimiter present, capture the latitude and longitude.*
   if ( syntaxOK && (colon2 > ZERO) )
   {
      gs.substr( gslat, ZERO, colon2 ) ;
      gslat.strip() ;
      gslon = &gs.gstr()[colon2 + 1] ;
      gslon.strip() ;

      bool pvstat1 = this->gclaParseValue ( gslat, &cPtr->lat ) ;
      bool pvstat2 = this->gclaParseValue ( gslon, &cPtr->lon ) ;

      //* If both latitude and longitude captured AND in range, declare success.*
      if ( pvstat1 && pvstat2 )
         cPtr->valid = true ;
   }

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0
   short xcindx = abs( this->exitCode ) ;    // index the exit code text
   gsDebug.compose( "    xCode : %s  cPtr->valid:%hhd\n", 
                    xcodeText[xcindx], &cPtr->valid ) ;
   wcout << gsDebug.gstr() ;
   gsDebug.compose( "\"%S\" latitude", cPtr->label ) ;
   this->DisplayConvData ( &cPtr->lat, gsDebug.ustr() ) ;
   gsDebug.compose( "\"%S\" longitude", cPtr->label ) ;
   this->DisplayConvData ( &cPtr->lon, gsDebug.ustr() ) ;
   wcout.flush() ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE

   return cPtr->valid ;

}  //* End gclaParseRecord() *

//*************************
//*    gclaParseValue     *
//*************************
//********************************************************************************
//* Parse a coordinate value into its component parts, range check the latitude  *
//* and longitude input value(s), and convert them into all supported formats.   *
//*                                                                              *
//* The source string should contain between one(1) and three(3) alphanumeric    *
//* tokens. These tokens are converted to numeric values and all members of      *
//* the referenced 'gpsValue' object are initialized.                            *
//*                                                                              *
//* Note that only the degrees value may be negative.                            *
//* Absolute values for Minutes and Seconds will be used to avoid errors.        *
//* (That adjustment will be done silently without generating an error.)         *
//* Radian values must always be >= 0.0.                                         *
//*                                                                              *
//* Note that any out-of-range value(s) provided will be set within the limits   *
//* before calculations are performed.                                           *
//*                                                                              *
//* Input  : gsVal   : (by reference) coordinate value as a text string          *
//*          valPtr  : pointer to an gpsValue instance where parsed value        *
//*                    is to be stored                                           *
//*                                                                              *
//* Returns: 'true'  if conversion successful                                    *
//*          'false' if parsing error or value(s) out-of-range                   *
//*                  'exitCode' member indicates error type                      *
//********************************************************************************
//* Programmer's Notes:                                                          *
//* -------------------                                                          *
//* 1) Note on the 'trunc', 'truncf', 'truncl' and similar library functions:    *
//*    These functions require declaration of "namespace std". Otherwise, they   *
//*     will revert to C99 versions.                                             *
//* 2) Range checking for each format has been manually verified.                *
//*                                                                              *
//********************************************************************************

bool EarthPoints::gclaParseValue ( const gString& gsVal, gpsValue *valPtr )
{
   const ldbl_t baseSixty = ldbl_t(60.0) ; // convert minutes or seconds to degrees

   gString gs = gsVal ;          // data capture
   ldbl_t v1, v2, v3,            // receives scanned values
          vtmp ;                 // temporary storage
   short vCap = ZERO ;           // number of values captured
   bool radianSrc = false,       // 'true' if source value expressed in radians
        negZ = false,            // 'true' if -0.0 degree value
        rangeOK ;                // 'true' if value is in range

   valPtr->reset() ;             // initialize the target storage object

   //* If value is expressed in radians *
   radianSrc = ((gs.find( L'R', ZERO, false )) > ZERO) ;
   //* Scan the value(s) to our temp variables *
   vCap = gs.gscanf( "%Lf %Lf %Lf", &v1, &v2, &v3 ) ;
   //* If first value == zero, check for negative sign.*
   if ( (vCap >= 1) && (v1 == 0.0) && ((gs.find( L'-' )) == ZERO) )
      negZ = true ;

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0 && DEBUG_PVALUE != 0
   wcout << L" gclaParseValue(" << gs.gstr() << L")\n" ;
   wcout << L"   Captured:\n" ;
   if ( (vCap >=1) && (vCap <= 3) )
   {
      gsDebug.clear() ;
      if ( vCap >= 1 )  gsDebug.append( L"    deg/rad (v1): %Lf\n", &v1 ) ;
      if ( vCap >= 2 )  gsDebug.append( L"    minutes (v2): %Lf\n", &v2 ) ;
      if ( vCap >= 3 )  gsDebug.append( L"    seconds (v3): %Lf\n", &v3 ) ;
   }
   else
      wcout << L"    invalid data format\n" ;
   wcout << gsDebug.gstr() ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE && DEBUG_PVALUE

   //**************************************
   //* One numeric value captured.        *
   //* This is assumed to be the decimal  *
   //* number of degrees (or radians).    *
   //**************************************
   if ( vCap == 1 )
   {
      //* If 'v1' contains the arc in radians, *
      //* convert radians to decimal degrees.  *
      if ( radianSrc )
      {
         //* Range limit and save the captured radians value.  *
         //* If value is out-of-range, it will be set to the   *
         //* nearest limit and 'exitCode' will be set to xcOOR.*
         if ( (valPtr->rad = this->radRange ( v1 )) == v1 ) // range check radians
            valPtr->valid = true ;             // conversion status
         valPtr->fmt = vfRad ;                 // set the format identifier
         v1 = this->Rad2Deg ( valPtr->rad ) ;  // convert radians to degrees
      }
      //* Value is in decimal degrees. Also save as radians.*
      else
      {
         vtmp = v1 ;
         if ( (v1 = this->degRange ( v1 )) == vtmp ) // range check degrees
            valPtr->valid = true ;              // conversion status
         valPtr->rad = this->Deg2Rad ( v1 ) ;   // convert degrees to radians
         valPtr->fmt = vfDec ;                  // set the format identifier
      }

      //* Store the captured (or radian-derived) decimal degrees value.*
      valPtr->dec = v1 ;

      //* Isolate the degrees/minutes/seconds components of the decimal value.*
      //* 'deg' may be either positive or negative, but minutes and seconds   *
      //* are always stored as ABSOLUTE values.                               *
      v2 = modfl ( v1, &valPtr->deg ) ;   // 'deg' receives whole-number degrees
                                          // 'v2' receives fractional degrees
      v2 = fabsl ( v2 * baseSixty ) ;     // absolute fractional degrees (decimal minutes)
      v3 = modfl ( v2, &valPtr->min ) ;   // 'min' receives whole-number minutes
                                          // 'v3' receives fractional seconds
      valPtr->sec = fabsl (v3 * baseSixty ) ; // 'sec' receives absolute decimal seconds
   }

   //********************************************
   //* Two numeric values captured.             *
   //* These are assumed to be the whole-number *
   //* degrees and decimal minutes.             *
   //********************************************
   else if ( vCap == 2 )
   {
      rangeOK = true ;                    // hope for the best

      //* Create the decimal value and store in the decimal member *
      vtmp = v1 ;
      if ( (v1 = this->degRange ( v1 )) != vtmp )
         rangeOK = false ;                // value out-of-range
      v2 = fabsl ( v2 ) ;                 // absolute decimal minutes
      if ( v2 > baseSixty )               // range check the minutes value
      {
         v2 = baseSixty ;                 // set value to limit (for safety)
         this->SetWarningCode ( xcOOR ) ; // value out-of-range
         rangeOK = false ;
      }
      if ( v1 < 0.0 || negZ )
         valPtr->dec = v1 - (v2 / baseSixty) ;
      else
         valPtr->dec = v1 + (v2 / baseSixty) ;
      vtmp = valPtr->dec ;
      if ( (valPtr->dec = this->degRange ( valPtr->dec )) != vtmp )
         rangeOK = false ;                // value out-of-range
      valPtr->rad = this->Deg2Rad ( valPtr->dec ) ; // convert degrees to radians
      valPtr->deg = truncl( v1 ) ;        // 'deg' receives whole-number degrees
      v3 = modfl ( v2, &valPtr->min ) ;   // 'min' receives whole-number minutes
                                          // 'v3' receives fractional minutes
      valPtr->sec = v3 * baseSixty ;      // 'sec' receives decimal seconds
      valPtr->fmt = vfMin ;               // set the format identifier
      valPtr->valid = rangeOK ;           // conversion status
   }

   //****************************************************
   //* Three numeric values captured.                   *
   //* These are assumed to be the whole-number degrees,*
   //* whole-number minutes and decimal seconds.        *
   //****************************************************
   else if ( vCap == 3 )
   {
      rangeOK = true ;                    // hope for the best

      //* Create the decimal value and store in the decimal member *
      vtmp = v1 ;
      if ( (v1 = this->degRange ( v1 )) != vtmp )
         rangeOK = false ;                // value out-of-range
      v2 = fabsl ( v2 ) ;                 // absolute decimal minutes
      if ( v2 > baseSixty )               // range check the minutes value
      {
         v2 = baseSixty ;                 // set value to limit (for safety)
         this->SetWarningCode ( xcOOR ) ; // value out-of-range
         rangeOK = false ;                // value out-of-range
      }
      v3 = fabsl ( v3 ) ;                 // absolute decimal seconds
      if ( v3 > baseSixty )               // range check the seconds value
      {
         v3 = baseSixty ;                 // set value to limit (for safety)
         this->SetWarningCode ( xcOOR ) ; // value out-of-range
         rangeOK = false ;                // value out-of-range
      }
      if ( v1 < 0.0 || negZ )
         valPtr->dec = v1 - (v2 / baseSixty) - (v3 / (baseSixty * baseSixty)) ;
      else
         valPtr->dec = v1 + (v2 / baseSixty) + (v3 / (baseSixty * baseSixty)) ;

      //* Range check decimal degrees value. *
      // Programmer's Note: If 'dec' is the sum of 'deg' + 'min' + 'sec'.
      // The individual values are in range but the sum may not be.
      vtmp = valPtr->dec ;
      if ( (valPtr->dec = this->degRange ( valPtr->dec )) != vtmp )
      {
         this->SetWarningCode ( xcOOR ) ; // value out-of-range
         rangeOK = false ;
      }

      valPtr->rad = this->Deg2Rad ( valPtr->dec ) ; // convert degrees to radians
      valPtr->deg = truncl( v1 ) ;        // 'deg' receives whole-number degrees
      valPtr->min = truncl( v2 ) ;        // 'min' receives whole-number minutes
      valPtr->sec = v3 ;                  // 'sec' receives decimal seconds
      valPtr->fmt = vfSec ;               // set the format identifier
      valPtr->valid = rangeOK ;           // conversion status
   }
   else     // Error: no numeric value(s) found. valPtr->valid == false
      this->SetWarningCode ( xcRSYN ) ;

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0 && DEBUG_PVALUE != 0
   this->DisplayConvData ( valPtr, "Parsed:" ) ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE && DEBUG_PVALUE

   return valPtr->valid ;

}  //* End gclaParseValue() *

//*************************
//*  gclaScanRecordFile   *
//*************************
//********************************************************************************
//* Scan the specified source file, extracting the two(2) specified GPS          *
//* location records.                                                            *
//* 'gsParm' contains data in the format: "--file=SRC_FILE:LABEL_A:LABEL_B"      *
//*                                                                              *
//* Special Case: If LABEL_A contains the string "LABn" where 'n' is an          *
//*               integer, then the 'n' is the file index of record A.           *
//* Special Case: If LABEL_B contains the string "LABm" where 'm' is an          *
//*               integer, then 'm' is the file index of record B.               *
//*                                                                              *
//* Input  : gsParm : (by reference) command-line parameter and its arguments    *
//*                   This includes the source filespec and the record labels    *
//*                   for which to scan.                                         *
//*                                                                              *
//* Returns: 'true'  if file found, is accessible AND specified records both     *
//*                  found and validated                                         *
//*                  The 'coord_a' and 'coord_b' objects will be initialized     *
//*          'false' if error during scan                                        *
//*                  NOTE: The 'exitCode' member will be set to indicate the     *
//*                        type of error.                                        *
//********************************************************************************

bool EarthPoints::gclaScanRecordFile ( const gString& gsParm )
{
   gString gsFspec,                       // target file specification
           gsReca, gsRecb,                // label text for which to search (if specified)
           gsLaba, gsLabb,                // label index (if specified)
           gs = gsParm ;                  // working copy of command-line parameter
   short offset,                          // index into text string
         indxA = -1,                      // index of record A
         indxB = -1 ;                     // index of record B
   bool rstat1 = false, rstat2 = false ;  // status of record extraction and parsing

   //* Isolate the filespec and label names.*
   offset = gs.after( L'=' ) ;            // discard the "--file=" command 
   gs.shiftChars( -(offset) ) ;
   gs.strip() ;

   if ( (offset = gs.find( L':' )) > ZERO )  // locate the sub-token delimeter
   {
      gs.substr( gsFspec, ZERO, offset ) ;   // capture the filespec
      gsFspec.strip() ;                      // remove leading and trailing whitespace
      gs.shiftChars( -(offset + 1) ) ;       // discard filespec from source
      if ( (offset = gs.find( L':' )) > ZERO )// locate the sub-token delimeter
      {
         gs.substr( gsLaba, ZERO, offset ) ; // capture label A
         gsLaba.strip() ;                    // remove leading and trailing whitespace
         gs.shiftChars( -(offset + 1) ) ;    // discard label A from source
         gsLabb = gs ;                       // capture label B
         gsLabb.strip() ;                    // remove leading and trailing whitespace

         //* If target record(s) were specified by *
         //* their numeric index decode the offset.*
         if ( (gsLaba.gscanf( L"LAB%hd", &indxA )) != 1 ) // if no index value captured
         {
            gsReca = gsLaba ; // text description (record label)
            indxA = -1 ; // default the index to show that label text was specified
         }
         if ( (gsLabb.gscanf( L"LAB%hd", &indxB )) != 1 ) // if no index value captured
         {
            gsRecb = gsLabb ; // text description (record label)
            indxB = -1 ; // default the index to show that label text was specified
         }

         rstat1 = this->gclaExtractFileRecord ( gsFspec.ustr(), gsReca, indxA ) ;
         rstat2 = this->gclaExtractFileRecord ( gsFspec.ustr(), gsRecb, indxB ) ;

         #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0
         gsDebug.compose( "gclaScanRecordFile(fspec:'%S' labelA:'%S' labelB:'%S' indxA:%hd indxB:%hd)\n"
                          "  Record A) '%S' (status:%hhd)\n"
                          "  Record B) '%S' (status:%hhd)\n", 
                          gsFspec.gstr(), gsLaba.gstr(), gsLabb.gstr(), &indxA, &indxB, 
                          gsReca.gstr(), &rstat1, gsRecb.gstr(), &rstat2 ) ;
         wcout << gsDebug.gstr() ;
         wcout.flush() ;
         #endif   // DEBUG_EPNTS && DEBUG_PARSE

         //* Parse the captured records into their component *
         //* values, and initialize the gpsCoord objects.    *
         if ( rstat1 )
            rstat1 = this->gclaParseRecord ( gsReca, &this->coord_a ) ;
         if ( rstat2 )
            rstat2 = this->gclaParseRecord ( gsRecb, &this->coord_b ) ;

         #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0
         short xcindx = abs( this->exitCode ) ;    // index the exit code text
         gsDebug.compose( "gclaScanRecordFile(exitCode:%s(%d) A_valid:%hhd B_valid:%hhd)\n",
                          xcodeText[xcindx], &this->exitCode,
                          &this->coord_a.valid, &this->coord_b.valid ) ;
         wcout << gsDebug.gstr() ;
         wcout.flush() ;
         #endif   // DEBUG_EPNTS && DEBUG_PARSE
      }
      else
      {  //* Formatting error in invocation of "--file" command *
         this->exitCode = xcFCMD ;
      }
   }
   else
   {  //* Formatting error in invocation of "--file" command *
      this->exitCode = xcFCMD ;
   }

   //* If both records captured and validated, return success.*
   return ( (this->coord_a.valid && this->coord_b.valid) ) ;

}  //* End gclaScanRecordFile() *

//*************************
//* gclaExtractFileRecord *
//*************************
//********************************************************************************
//* Read the file containing test data and extract the specified record.         *
//*                                                                              *
//* See "testpoints.txt" for example records.                                    *
//*                                                                              *
//* NOTE: The AnsiCmd-class object has not been instantiated when this           *
//*       method is called.                                                      *
//*                                                                              *
//* Input  : fspec : file specification (relative or absolute).                  *
//*          gsRec : initially, contains name (full or partial record label)     *
//*                  for which to scan                                           *
//*                  If 'indx' specified (>= ZERO), 'gsRec' will be ignored.     *
//*                  NOTE: On return, 'gsRec' contains the scanned record.       *
//*          indx  : (optional, -1 by default) if specified, this is the         *
//*                  index of the record to be returned                          *
//*                                                                              *
//* Returns: 'true'  if file found, is accessible AND specified record           *
//*                  found AND decoded value(s) are within range.                *
//*                  'gsRec' contains the captured record                        *
//*          'false' if scan error, record not found ('gsRec' unmodified)        *
//*                  'exitCode' member indicates the type of error:              *
//*                    xcFNF  == file not found                                  *
//********************************************************************************

bool EarthPoints::gclaExtractFileRecord ( const char *fspec, gString& gsRec, short indx )
{
   bool status = false ;      // return value

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0 && DEBUG_VERBOSE != 0
   wcout << L"gclaExtractFileRecord(fspec:'" 
         << fspec << L"' gsRec:'" << gsRec.gstr() << L"' indx:" << indx ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE && DEBUG_VERBOSE

   ifstream ifsIn( fspec, ifstream::in ) ;
   if ( (ifsIn.is_open()) )
   {
      //* 'status' is set if specified record was found.     *
      //* Otherwise, 'exitCode' member gets record-not-found.*
      if ( (status = this->gclaGetRecord ( ifsIn, gsRec, indx )) == false )
         this->exitCode = xcRNF ;
      ifsIn.close() ;
   }
   else        // set exit code to file-not-found
      this->exitCode = xcFNF ;

   #if DEBUG_EPNTS != 0 && DEBUG_PARSE != 0 && DEBUG_VERBOSE != 0
   if ( status != false )
      wcout << L" -> \"" << gsRec.gstr() << L"\"" ;
   wcout << L")\n" ;
   wcout.flush() ;
   #endif   // DEBUG_EPNTS && DEBUG_PARSE && DEBUG_VERBOSE

   return status ;

}  //* End gclaExtractFileRecord() *

//*************************
//*     gclaGetRecord     *
//*************************
//********************************************************************************
//* Read a record from the test-record file.                                     *
//*                                                                              *
//* Input  : ifs  : (by reference) handle for open source file                   *
//*          gsRec : initially, contains name (full or partial record label)     *
//*                  for which to scan                                           *
//*                  On return, contains captured record                         *
//*          indx  : (optional, -1 by default) if specified, this is the         *
//*                  index of the record to be returned                          *
//*                                                                              *
//* Returns: 'true' if record captured                                           *
//*                  (gsRec will contain captured record)                        *
//*          'false; if file error or specified record not found                 *
//*                  (gsRec unmodified)                                          *
//********************************************************************************

bool EarthPoints::gclaGetRecord ( ifstream& ifs, gString& gsRec, short indx )
{
   const char Hash = '#' ;       // indicates comment line
   gString gs ;                  // string manipulation
   char lbuff[gsALLOCMIN] ;      // input buffer
   int  cnt ;                    // byte count
   short recIndex = ZERO ;       // index of scanned record
   bool status = false ;         // return value

   while ( true )
   {
      ifs.getline( lbuff, gsALLOCMIN ) ;  // get the next line of the file
      if ( ifs.good() || (ifs.gcount() > ZERO) )
      {
         //* If a non-empty, non-comment line was found,     *
         //* test for matching label field or matching index.*
         if ( ((cnt = ifs.gcount()) > 1) && (*lbuff != Hash) )
         {
            //* If specified record index matches the current index, OR *
            //* if the record label begins with the specified label.    *
            gs = lbuff ;
            if ( ((indx >= ZERO) && (indx == recIndex)) ||
                 ((gs.find( gsRec.gstr() )) == ZERO) )
            {
               gsRec = lbuff ;
               status = true ;
               break ;
            }

            ++recIndex ;      // advance the index
         }
         else if ( cnt == ZERO )    // read error
            break ;
      }
      else        // end-of-file reached
         break ;
   }

   return status ;

}  //* End gclaGetRecord() *

//*************************
//*     gclaScanValue     *
//*************************
//********************************************************************************
//* Parse a coordinate value into its component parts, range check the input     *
//* value(s), and convert them into all supported formats.                       *
//*                                                                              *
//* The source string contains the '--conv' option and it argument from the      *
//* command line. (See gclaParseValue() for details of the argument format.)     *
//* The tokens of the argument are converted to numeric values and all members   *
//* of the referenced 'gpsValue' object are initialized.                         *
//*                                                                              *
//* Input  : gsCmd   : (by reference) "--conv=VALUE" command                     *
//*          valPtr  : pointer to an gpsValue instance where parsed value        *
//*                    is to be stored                                           *
//*                                                                              *
//* Returns: 'true'  if conversion successful                                    *
//*          'false' if parsing error or value(s) out-of-range                   *
//*                  'exitCode' member indicates error type                      *
//********************************************************************************
//* Programmer's Notes:                                                          *
//* -------------------                                                          *
//* This method was used primarily during development of the gclaParseValue()    *
//* method, but it remains active so user can experiment with GPS coordinate     *
//* values.                                                                      *
//*                                                                              *
//********************************************************************************

bool EarthPoints::gclaScanValue ( const gString& gsCmd, gpsValue *valPtr )
{
   gString gs;                   // working copy of source text
   short offset ;                // offset to beginning of numeric data

   valPtr->reset() ;             // initialize the target storage object

   if ( (offset = (gsCmd.after( L'=' ))) > ZERO )
   {
      gs = &gsCmd.gstr()[offset] ;           // isolate numeric data
      this->gclaParseValue ( gs, valPtr ) ;  // parse the value string
   }

   return valPtr->valid ;

}  //* End gclaScanValue() *

//*************************
//*    DisplayConvData    *
//*************************
//********************************************************************************
//* Display the results of value conversion. (See gclaScanValue().)              *
//*                                                                              *
//* NOTE: The AnsiCmd-class object has not been instantiated when this           *
//*       method is called.                                                      *
//*                                                                              *
//* Input  : valPtr : pointer to gpsValue object contain data to be displayed    *
//*          title  : title to display at top of record dump                     *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void EarthPoints::DisplayConvData ( const gpsValue *valPtr, const char *title )
{
   //* Format and report the parsed data *
   gString gsOut( "   %s\n"
                  "    dec:% 2.6Lf\n"
                  "    deg:% 2.6Lf\n"
                  "    min:% 2.6Lf\n"
                  "    sec:% 2.6Lf\n"
                  "    rad:% 2.6Lf\n"
                  "    fmt: %s\n\n",
                  title,
                  &valPtr->dec, &valPtr->deg, &valPtr->min, &valPtr->sec,
                  &valPtr->rad, valFmtText[valPtr->fmt] ) ;
   wcout << gsOut ; wcout.flush();

}  //* End DisplayConvData() *

//*************************
//*   gclaSelectFormula   *
//*************************
//********************************************************************************
//* Select the formula for calculation of the arc distance (distance between     *
//* GPS coordinate points).                                                      *
//*                                                                              *
//* Note: Three(3) characters of the argument are sufficient to identify the     *
//*       argument" "hav" or "lam" or "all".                                     *
//*                                                                              *
//* Input  : gsCmd : (by reference) "--formula=FORMAT" command                   *
//*                                                                              *
//* Returns: 'true'  if valid formula specified (member of enum suppForm)        *
//*          'false' if invalid formula is specified                             *
//*                  ('formula' member set to default: sfLambert)                *
//********************************************************************************

bool EarthPoints::gclaSelectFormula ( const gString& gsCmd )
{
   bool status = true ;       // return value

   if ( (gsCmd.find( L"hav" )) > ZERO )
      this->formula = sfHaversine ;
   else if ( (gsCmd.find( L"lam" )) > ZERO )
      this->formula = sfLambert ;
   else if ( (gsCmd.find( L"all" )) > ZERO )
      this->formula = sfAll ;
   else
   { this->formula = sfLambert ; status = false ; }

   return status ;

}  //* End gclaSelectFormula() *

//*************************
//*    gclaColorScheme    *
//*************************
//********************************************************************************
//* Set text fgnd/bgnd color attributes for for the application.                 *
//* Optionally, set ACWin border attributes also.                                *
//*                                                                              *
//* Input  : gsCmd  : (by reference) "--color=COLOR" command                     *
//*          tCfg   : (by reference) a TermConfig object which receives          *
//*                   the specified foreground:background:                       *
//*                                 tCfg.foreground and tCfg.background          *
//*                                                                              *
//* Returns: 'true'  if valid syntax                                             *
//*          'false; if syntax error, (default attributes are used)              *
//********************************************************************************

bool EarthPoints::gclaColorScheme ( const gString& gsCmd, TermConfig& tCfg )
{
   gString gsAttr ;                 // color attribute text
   short   offset ;                 // offset to beginning of color name
   short   cnum ;                   // value returned from gclaAttrScan()
   bool    status  = true ;         // return value

   tCfg.foreground = aesFG_DFLT ;   // initialize caller's attribute fields
   tCfg.background = aesBG_DFLT ;

   if ( (offset = (gsCmd.after( L'=' ))) > ZERO )
   {
      gsAttr = &gsCmd.gstr()[offset] ;    // isolate color attribute text

      //* Decode the text foreground color argument. *
      switch ( (cnum = gclaAttrScan ( gsAttr )) )
      {
         case 0:  tCfg.foreground = aesFG_DFLT ;      break ;
         case 1:  tCfg.foreground = aesFG_BLACK ;     break ;
         case 2:  tCfg.foreground = aesFG_RED ;       break ;
         case 3:  tCfg.foreground = aesFG_GREEN ;     break ;
         case 4:  tCfg.foreground = aesFG_BROWN ;     break ;
         case 5:  tCfg.foreground = aesFG_BLUE ;      break ;
         case 6:  tCfg.foreground = aesFG_MAGENTA ;   break ;
         case 7:  tCfg.foreground = aesFG_CYAN ;      break ;
         case 8:  tCfg.foreground = aesFG_GREY ;      break ;
         default: status = false ;                    break ;
      }
      //* Decode the text background color argument. *
      if ( status && (gsAttr.gschars() > 1) )
      {
         switch ( (cnum = gclaAttrScan ( gsAttr )) )
         {
            case 0:  tCfg.background = aesBG_DFLT ;      break ;
            case 1:  tCfg.background = aesBG_BLACK ;     break ;
            case 2:  tCfg.background = aesBG_RED ;       break ;
            case 3:  tCfg.background = aesBG_GREEN ;     break ;
            case 4:  tCfg.background = aesBG_BROWN ;     break ;
            case 5:  tCfg.background = aesBG_BLUE ;      break ;
            case 6:  tCfg.background = aesFG_MAGENTA ;   break ;
            case 7:  tCfg.background = aesBG_CYAN ;      break ;
            case 8:  tCfg.background = aesBG_GREY ;      break ;
            default: status = false ;                    break ;
         }
      }
      //* Decode the border foreground color argument. *
      if ( status && (gsAttr.gschars() > 1) )
      {
         switch ( (cnum = gclaAttrScan ( gsAttr )) )
         {
            case 0:  this->bdrfAttr = acaFG_DFLT ;    break ;
            case 1:  this->bdrfAttr = acaFG_BLACK ;   break ;
            case 2:  this->bdrfAttr = acaFG_RED ;     break ;
            case 3:  this->bdrfAttr = acaFG_GREEN ;   break ;
            case 4:  this->bdrfAttr = acaFG_BROWN ;   break ;
            case 5:  this->bdrfAttr = acaFG_BLUE ;    break ;
            case 6:  this->bdrfAttr = acaFG_MAGENTA ; break ;
            case 7:  this->bdrfAttr = acaFG_CYAN ;    break ;
            case 8:  this->bdrfAttr = acaFG_GREY ;    break ;
            default: status = false ;                 break ;
         }
      }
      //* Decode the border background color argument. *
      if ( status && (gsAttr.gschars() > 1) )
      {
         switch ( (cnum = gclaAttrScan ( gsAttr )) )
         {
            case 0:  this->bdrbAttr = acaBG_DFLT ;    break ;
            case 1:  this->bdrbAttr = acaBG_BLACK ;   break ;
            case 2:  this->bdrbAttr = acaBG_RED ;     break ;
            case 3:  this->bdrbAttr = acaBG_GREEN ;   break ;
            case 4:  this->bdrbAttr = acaBG_BROWN ;   break ;
            case 5:  this->bdrbAttr = acaBG_BLUE ;    break ;
            case 6:  this->bdrbAttr = acaFG_MAGENTA ; break ;
            case 7:  this->bdrbAttr = acaBG_CYAN ;    break ;
            case 8:  this->bdrbAttr = acaBG_GREY ;    break ;
            default: status = false ;                 break ;
         }
      }
   }
   return status ;

}  //* End gclaColorScheme() *

//*************************
//*     gclaAttrScan      *
//*************************
//********************************************************************************
//* Non-member method called by gclaColorScheme():                               *
//* ----------------------------------------------                               *
//* Convert a text attribute to a numeric value so caller can easiy convert to   *
//* a color attribute.                                                           *
//*                                                                              *
//* On return, captured attribute token will have been shifted out, and next     *
//* token (if any) will be at offset zero.                                       *
//*                                                                              *
//* Programmer's Note: Yes, it's sloppy to return a numeric value that the       *
//* caller needs to interpret, but hey, "It's Only a Northern Song"              *
//*                                                                              *
//* Input  : gsAttr : (by reference) contains text data representing color.      *
//*                                                                              *
//* Returns: numeric value:                                                      *
//*          0 == default    5 == blue                                           *
//*          1 == black      6 == magenta                                        *
//*          2 == red        7 == cyan                                           *
//*          3 == green      8 == grey                                           *
//*          4 == brown      9 == syntax error                                   *
//*                                                                              *
//********************************************************************************

static short gclaAttrScan ( gString& gsAttr )
{
   short offset, 
         colorNumber = ZERO ;    // return value (zero by default)

   gsAttr.strip( true, false ) ; // strip leading whitespace

   //* This is a case-insensitive scan *
   // Programmer's Note: We need only the first three(3) characters
   // to identify the color. (note alternate spelling: grey/gray)
   if      ( (gsAttr.find( "dflt" )) == ZERO )   colorNumber = 0 ;
   else if ( (gsAttr.find( "bla" )) == ZERO )    colorNumber = 1 ;
   else if ( (gsAttr.find( "red" )) == ZERO )    colorNumber = 2 ;
   else if ( (gsAttr.find( "gree" )) == ZERO )   colorNumber = 3 ;
   else if ( (gsAttr.find( "bro" )) == ZERO )    colorNumber = 4 ;
   else if ( (gsAttr.find( "blu" )) == ZERO )    colorNumber = 5 ;
   else if ( (gsAttr.find( "mag" )) == ZERO )    colorNumber = 6 ;
   else if ( (gsAttr.find( "cya" )) == ZERO )    colorNumber = 7 ;
   else if ( ((gsAttr.find( "gray" )) == ZERO) ||
             ((gsAttr.find( "grey" )) == ZERO) ) colorNumber = 8 ;
   else  colorNumber = 9 ;    // syntax error

   //* Discard the processed token *
   if ( (offset = gsAttr.after( L':' )) > ZERO )
   {
      gsAttr.shiftChars( -offset ) ;
      gsAttr.strip() ;
   }
   //* If last token processed, clear the data.*
   else
      gsAttr.clear() ;
   return colorNumber ;

}  //* End gclaAttrScan() *

//*************************
//*     Dump_GPS_File     *
//*************************
//********************************************************************************
//* Read the specified file and report all GPS coordinate records -- including   *
//* whether the record can be successfully decoded.                              *
//*                                                                              *
//* Input  : none                                                                *
//*          member variable 'fspec' contains target filespec                    *
//*                                                                              *
//* Returns: nothing                                                             *
//*          if successful, 'exitCode' member contains xcGOOD                    *
//*          if file not found, 'exitCode' member contains                       *
//********************************************************************************

void EarthPoints::Dump_GPS_File ( void )
{
   gString  gsRecA,           // captured record string
            gsOut ;           // output formatting
   gpsCoord coord ;           // receives decoded record data
   short    indxA = -1 ;      // record index for which to search
   bool     status ;          // loop control

   //* Give user a clue *
   gsOut.compose( L"Validate contents of GPS record file: '%s'\n", this->fspec ) ;
   gsOut.padCols( (gsOut.gscols() * 2), L'-' ) ;
   this->ansi->ttyWrite ( gsOut, false, true ) ;

   //* Open the target file *
   ifstream ifsIn( this->fspec, ifstream::in ) ;
   if ( (ifsIn.is_open()) )
   {

      //* Scan the target file until record-not-found *
      do
      {
         gsRecA.clear() ;        // record name not specified
         ++indxA ;               // advance the index for which to scan

         //* Extract the next record from the file *
         if ( (status = this->gclaGetRecord ( ifsIn, gsRecA, ZERO )) != false )
         {
            //* Report the record text *
            gsOut.compose( L"%02hd) %S", &indxA, gsRecA.gstr() ) ;
            gsOut.padCols( 60 ) ;
            this->ansi->ttyWrite ( gsOut, false ) ;

            //* Validate the record *
            if ( (this->gclaParseRecord ( gsRecA, &coord )) )
               this->ansi->ttyWrite ( L"(OK)\n", false ) ;

            //* Decoding error (syntax or value out-of-range) *
            else
               this->ansi->ttyWrite ( L"(?)\n", false ) ;
         }
         else
            this->ansi->ttyWrite ( L"End-Of-File\n", false ) ;
      }
      while ( status != false ) ;

      //* Close the file *
      ifsIn.close() ;
   }
   else
   {
      this->ansi->ttyWrite ( L"Target file not found for: \"--dump\" command.\n\n" ) ;
   }

   this->ansi->acFlushOut () ;            // flush the output buffer

}  //* End Dump_GPS_File() *

#if DEBUG_EPNTS != 0 && DEBUG_VERBOSE != 0
//*************************
//*      gclaSysInfo      *
//*************************
//********************************************************************************
//* FOR DEBUGGING ONLY. Display basic information about the host system and      *
//* the size of floating-point types.                                            *
//*                                                                              *
//* NOTE: The AnsiCmd-class object has not been instantiated when this           *
//*       method is called.                                                      *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Example entry in /proc/cpuinfo:                                              *
//* -------------------------------                                              *
//* processor       : 0                                                          *
//* vendor_id       : GenuineIntel                                               *
//* cpu family      : 6                                                          *
//* model           : 60                                                         *
//* model name      : Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz                  *
//* stepping        : 3                                                          *
//* microcode       : 0x28                                                       *
//* cpu MHz         : 2300.000                                                   *
//* cache size      : 6144 KB                                                    *
//* physical id     : 0                                                          *
//* siblings        : 8                                                          *
//* core id         : 0                                                          *
//* cpu cores       : 4                                                          *
//********************************************************************************

void EarthPoints::gclaSysInfo ( void )
{
   //* Capture CPU info. This is located in the file "/proc/cpuinfo".*
   const char *cpuInfoName = "/proc/cpuinfo" ;
   const char *tmpName = "./hostinfo.txt" ;
   const char *fieldName = "^model name" ;
   char lbuff[PATH_MAX] = "unknown" ;
   gsDebug.compose( "grep --max-count=1 '%s' '%s' 1>\"%s\" 2>/dev/null", fieldName, cpuInfoName, tmpName ) ;
   system ( gsDebug.ustr() ) ;
   ifstream ifsInfo ( tmpName, ifstream::in ) ;
   if ( ifsInfo.is_open() )
   {
      ifsInfo.getline ( lbuff, PATH_MAX ) ;
      gsDebug = lbuff ;
      gsDebug.replace( L'\t', L' ', ZERO, false, true ) ;  // remove useless tab characters
      gsDebug.copy( lbuff, PATH_MAX ) ;
      ifsInfo.close() ;
   }

   system ( "clear" ) ;    // Clear the terminal window

   wcout << L"Host Environment Information:\n"
         << L" CPU: \"" << lbuff << L"\"\n" ;

   //* Display precision of the C++ library version of PI *
   const double m_pid = (double)M_PIl ;
   short sof = sizeof( float ), sod = sizeof( double ), sold = sizeof( long double ),
         imax = sizeof(intmax_t) ;
   gsDebug.compose( L" BYTES: float:%hd double:%hd long double:%hd intmax_t:%hd\n"
                    L" PI as defined (text): 3.141592653589793238462643383279502884\n"
                    L" PI (ll double def)  : %1.19Lf\n"
                    L" PI (double def)     : %1.19lf\n"
                    L" Press Enter to Continue...",
                    &sof, &sod, &sold, &imax, &m_pi, &m_pid ) ;
   wcout << gsDebug.gstr() ; wcout.flush() ;
   getchar () ;

   //* Delete the temp file *
   gsDebug.compose( "rm %s 1>/dev/null 2>/dev/null", tmpName ) ;
   system ( gsDebug.ustr() ) ;
}  //* End gclaSysInfo() *
#endif   // DEBUG_EPNTS && DEBUG_VERBOSE

//*************************
//*                       *
//*************************
//********************************************************************************
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Input  :                                                                     *
//*                                                                              *
//* Returns:                                                                     *
//********************************************************************************

