//********************************************************************************
//* File       : EarthPoints.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, below)                                  *
//*                                                                              *
//* Description: Given two latitude/longitude coordinate pairs,calculate the     *
//*              distance between the two points.                                *
//*                                                                              *
//*              This application is also the platform on which the AnsiCmd      *
//*              class and the AnsiCmd.lib link library were developed.          *
//*                                                                              *
//* Calculating the distance between two points on earth:                        *
//* 1) The Great Circle Distance approach:                                       *
//*    This distance is known formally as the "orthodromic distance".            *
//*    The distance may be calculated using the "Haversine formula" which uses   *
//*    the mean radius of the earth and is accurate to within 0.5 percent.       *
//*    This is generally accepted as the best compromise between accuracy and    *
//*    computing power considerations.                                           *
//*                                                                              *
//* 2) The Haversine formula vs. the Lambert formula.                            *
//*    The Lambert formula is more accurate than Haversine, especially for       *
//*    short distances. We use the Lambert formula by default because it         *
//*    DOES NOT assume that the earth is a sphere (or near sphere).              *
//*                                                                              *
//* 3) Refer also to the "World Geodesic System" (WGS) used by satellite         *
//*    navigation systems. These algorithms are generally proprietary and        *
//*    far too complex for our simple application. (See Vincenty's formulae,     *
//*    or satellite geodesy, especially satellite gravimetry.)                   *
//*                                                                              *
//* 4) Considerations: The formulae themselves are self-consistent; however,     *
//*    the coordinate system introduces potential problems:                      *
//*    a) When theta approaches 180.0 degrees (pi radians), there is a potential *
//*       for zero or negative distances.                                        *
//*    b) The fact that the earth is not a perfect sphere causes errors which    *
//*       increase with distance. It is generally accepted that a deviation      *
//*       of up to 0.5% may occur.                                               *
//*    c) Longitude is undefined (zero or 180) at the poles.                     *
//*                                                                              *
//*                                                                              *
//* For More Information, see:                                                   *
//* https://www.geeksforgeeks.org/program-distance-two-points-earth/             *
//* https://en.wikipedia.org/wiki/Great-circle_distance                          *
//* https://en.wikipedia.org/wiki/Vincenty%27s_formulae                          *
//* https://en.wikipedia.org/wiki/Haversine_formula                              *
//* https://community.esri.com/t5/coordinate-reference-systems-blog/             *
//*                     distance-on-a-sphere-the-haversine-formula/ba-p/902128   *
//* https://www.movable-type.co.uk/scripts/latlong.html                          *
//* https://www.calculator.net/distance-calculator.html                          *
//* mathyourlife.github.io/spouting-jibberish/Haversine/Haversine.html           *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//* Development Tools:                  Current:                                 *
//* ----------------------------------  ---------------------------------------- *
//*  GNU G++ v:4.8.0 or higher (C++17)  G++ v:14.3.1 202508088                   *
//*  Fedora Linux v:39 or higher        Fedora v:41                              *
//*  GNOME Terminal v:3.2.1 or higher   GNOME Term v:3.54.0 (GNOME 47)           *
//*                                                                              *
//********************************************************************************
//* Copyright Notice:                                                            *
//* This program is free software: you can redistribute it and/or modify it      *
//* under the terms of the GNU General Public License as published by the Free   *
//* Software Foundation, either version 3 of the License, or (at your option)    *
//* any later version.                                                           *
//*                                                                              *
//* This program is distributed in the hope that it will be useful, but          *
//* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY   *
//* or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License     *
//* for more details.                                                            *
//*                                                                              *
//* You should have received a copy of the GNU General Public License along      *
//* with this program.  If not, see <http://www.gnu.org/licenses/>.              *
//*                                                                              *
//********************************************************************************
//* Version History:                                                             *
//* v: 0.00.07 20-Dec-2025                                                       *
//*    -- 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.                               *
//*                                                                              *
//* v: 0.00.06 24-Mar-2025                                                       *
//*    -- Update all references to the beta release of the gString library to    *
//*       reflect that the new mainline gString is now integrated into the       *
//*       package.                                                               *
//*       -- This includes updating key 'short' integers to 'int32_t' to match   *
//*          the new gString class definition.                                   *
//*                                                                              *
//* v: 0.00.05 21-Aug-2023                                                       *
//*    -- Address "singularities", that is, when the latitude or longitude       *
//*       approache the limits:                                                  *
//*        a) small angles (very short distances)                                *
//*        b) large angles (very long distances)                                 *
//*       Distance calculations at these extremes can cause wildly inaccurate    *
//*       results.                                                               *
//*    -- Bug Fix: If last record in source file was not terminated by a newline,*
//*       it was being discarded.                                                *
//*    -- Update version to synch with AnsiCmd library.                          *
//*                                                                              *
//* v: 0.00.04 14-Aug-2023                                                       *
//*    -- Update parsing of command line arguments.                              *
//*    -- Documentation update.                                                  *
//*                                                                              *
//* v: 0.00.03 08-May-2023                                                       *
//*    -- Implement ACWin output formatting option.                              *
//*    -- Update command-line help for --window and --color options.             *
//*    -- Synchronize with AnsiCmd version number.                               *
//*                                                                              *
//* v: 0.00.02 01-Jan-2023                                                       *
//*    -- Synchronize with AnsiCmd version number.                               *
//*                                                                              *
//* v: 0.00.01 01-Aug-2022                                                       *
//*    -- First Effort.                                                          *
//*       - Define the user interface (command-line arguments).                  *
//*       - Define data-input formats.                                           *
//*       - Implement and refine the arc-distance calculations.                  *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************
//* Programmer's Notes:                                                          *
//*                                                                              *
//*                                                                              *
//********************************************************************************

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

//***************
//* Definitions *
//***************


//**************
//* Local data *
//**************
static const wchar_t* const appVersion = L"0.0.07" ;
static const wchar_t* const crYears    = L"2022-2026" ;
static const wchar_t* const appName1   = L"EarthPoints (epts)" ;
static const char*    const titleTemplate =  
   "%S v:%S Copyright(c) %S  The Software Samurai\n"
   "========================================================================" ;


//********************
//* Local prototypes *
//********************


//*************************
//*        main           *
//*************************
//********************************************************************************
//* Program entry point.                                                         *
//*                                                                              *
//********************************************************************************

int main ( int argc, char* argv[], char* argenv[] )
{
   //* For capture and parsing of command-line arguments *
   commArgs ca( argc, argv, argenv ) ;

   //* Instantiate the application class *
   EarthPoints *ep = new EarthPoints ( ca ) ;

   //* Save a local copy of the exit code, then delete the EarthPoints object.*
   xCode xcode = ep->exitCode ;
   delete ep ;

   //* Exit, returning control to the shell.*
   exit ( xcode ) ;

}  //* End main() *

//*************************
//*      EarthPoints      *
//*************************
//********************************************************************************
//* EarthPoints class constructor.                                               *
//*                                                                              *
//* Input  : ca     : (by reference) copy of command-line arguments              *
//*                                                                              *
//* Returns: implicitly returns pointer to class                                 *
//********************************************************************************
//* Notes on terminal configuration:                                             *
//* Instantiation of The AnsiCmd class performs important configuration          *
//* operations which should be completed before the application writes to        *
//* stdout/stderr and before the application reads from stdin.                   *
//*                                                                              *
//* This is because the application's first access to stdout/stdin/stderr        *
//* will cause certain configuration options to be set for the duration of the   *
//* session. This automatic configuration may cause the application to act in    *
//* unexpected ways. For this reason, it is important to EXPLICITLY set the      *
//* terminal configuration options as early in the application as possible.      *
//* This is done during instantiation of the AnsiCmd class.                      *
//* The two most critical configuration options are described briefly here.      *
//* See the AnsiCmd constructor for additional details.                          *
//*                                                                              *
//* 1) The width of the I/O streams are set the first time they are used.        *
//*    The wide input/output streams are accessed through the 'wcin' and         *
//*    'wcout' classs, respectively.                                             *
//*    The narrow input/output streams are accessed through the 'cin' and        *
//*    'cout' classes, respectively.                                             *
//*    (Please try not to use the C-language I/O functions any more than         *
//*     absolutely necessary.)                                                   *
//*                                                                              *
//* 2) The terminal's "locale" determines the character encoding used, and the   *
//*    primary language expected. This determines the way the terminal will      *
//*    handle certain text formatting -- for instance, currency symbols          *
//*    and number formats.                                                       *
//*    It is important to understand that if the application does not            *
//*    explicitly set the desired local, the so-called "C-local" is used.        *
//*    The C-locale restricts the character set to an ASCII superset (7-8-bit    *
//*    characters), and is unacceptable for any serious application.             *
//*                                                                              *
//********************************************************************************

EarthPoints::EarthPoints ( commArgs& ca )
{
   gString gsOut ;                        // text formatting

   //* Initialize all class data members.*
   this->reset () ;

   //* Parse command-line arguments *
   clArgs userOption = this->GetCommandLineArgs ( ca ) ;

   //* Clear the terminal window.                              *
   //* (This needs to happen before color attributes are set.) *
   system ( "clear" ) ;

   //* Initialize the ANSI terminal attributes for input/output.*
   //* Note that color attributes are set during initialization.*
   //* See notes above about terminal configuration.            *
   this->ansi = new AnsiCmd ( ca.tConfig ) ;

   //* Report the length of the arc connecting two GPS points *
   if ( userOption == clArc )
   {
      //* If no errors, perform calculation *
      if ( this->exitCode == xcGOOD )
      {
         this->ArcDistance () ;
      }
      //* Report errors *
      else
      {
         this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;
         this->ansi->acSetMod ( aesBOLD ) ;

         switch ( this->exitCode )
         {
            case xcOOR:          // value(s) out-of-range
               this->ansi->ttyWrite ( "Warning!: One or more coordinate values out-of-range.\n" ) ;
               break ;
            case xcFNF:          // target file not found
               this->ansi->ttyWrite ( "Warning!: Specified record file not found.\n" ) ;
               break ;
            case xcRNF:          // specified record not found in target file
               this->ansi->ttyWrite ( "Warning!: Specified GPS record not found in target file.\n" ) ;
               break ;
            case xcRSYN:         // syntax error in coordinate record
               this->ansi->ttyWrite ( 
                  "Warning!: Syntax or formatting error in specified GPS coordinate(s).\n"
                  "          Example: --coa=\"Miami, FL : 25.775163 : -80.208615\"\n" ) ;
               break ;
            case xcFCMD:         // formatting error in "--file" command
               this->ansi->ttyWrite (
                  "Warning!: Formatting error when specifying test-data file.\n"
                  "          Example: --file='test.txt : Miami : Buenos Aires'\n" ) ;
               break ;
            default:             // this should not happen
               break ;
         }
         //* Disable bold text (and add a newline) *
         this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;
         this->ansi->ttyWrite ( '\n' ) ;
      }
   }  // (clArc)

   //* Convert from one coordinate format to another.*
   else if ( userOption == clConvert )
   {
      if ( this->exitCode == xcGOOD )
         this->DisplayConvData ( &this->coord_a.lat, "Converted Value:" ) ;

      //* Report errors *
      else
      {
         this->ComposeTitle ( gsOut, true ) ;
         this->ansi->ttyWrite ( gsOut, false ) ;
         this->ansi->acSetMod ( aesBOLD ) ;
         gsOut.compose( 
               "Warning!: Unable to decode specified value or value out-of-range.\n"
               "  %3.3Lf <= degrees <= %3.3Lf and %3.5Lf <= radians <= %3.5Lf\n"
               "  Examples: --conv='25.775163'  --conv='-25 46.50978'\n"
               "            --conv='3.05782R'   --conv-'-2.8275R'\n\n",
               &minDegrees, &maxDegrees, &minRadians, &maxRadians ) ;
               this->ansi->ttyWrite ( gsOut, false ) ;
         this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;
      }
   }  // (clConvert)

   //* Validate contents of the specified file containing GPS-coordinate records.*
   else if ( userOption == clDump )
   {
      this->ComposeTitle ( gsOut, true ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;
      this->Dump_GPS_File () ;
   }  // (clDump)

   //* Execute the specified test of AnsiCmd-class functionality. *
   //* NOTE: If the "DEBUG_ANSICMD" conditional-compile directive *
   //*       is disabled, this call will simply display a message *
   //*       indicating that the test code is not available.      *
   else if ( userOption == clAnsi )
   {
      #if DEBUG_ANSICMD == 0
      this->ComposeTitle ( gsOut, false ) ;     // display application title
      this->ansi->ttyWrite ( gsOut, false, true ) ;
      #endif   // DEBUG_ANSICMD

      this->ansi->ansiTest ( this->act ) ;
   }  // (clAnsi)
   else
   {
      //* Set default foreground/background *
      this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;

      //* Display copyright and version information, then exit *
      if ( (userOption == clVersion) || (userOption == clVersiona) )
         this->DisplayVersion ( userOption == clVersiona ? true : false ) ;

      //* Display command-line help *
      else if ( (userOption == clHelp) || (userOption == clHelpless) )
         this->DisplayHelp ( userOption ) ;

      //* userOption == clQuickHelp (no command-line options specified), *
      //* OR misc. user command syntax error.                            *
      else
      {
         this->ComposeTitle ( gsOut, true ) ;
         this->ansi->ttyWrite ( gsOut, false ) ;
         this->ansi->acSetMod ( aesBOLD ) ;
         gsOut = L"Please specify a pair of GPS coordinates "
                  "or type: \"epts --help\"\n\n" ;
         this->ansi->ttyWrite ( gsOut, false ) ;
      }
   }

   this->ansi->acFlushStreams () ;     // flush the I/O streams

}  //* End EarthPoints() *

//*************************
//*     ~EarthPoints      *
//*************************
//********************************************************************************
//* EarthPoints class destructor.                                                *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

EarthPoints::~EarthPoints ( void )
{
   //* Restore original terminal settings *
   if ( this->ansi != NULL )
      delete ( this->ansi ) ;

}  //* End ~EarthPoints() *

//*************************
//*      ArcDistance      *
//*************************
//********************************************************************************
//* Calculate the distance between the two GPS location points using the         *
//* specified formula. (see enum suppForm)                                       *
//*                                                                              *
//* Input  : none                                                                *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void EarthPoints::ArcDistance ( void )
{
   ldbl_t lamKm = 0.0,        // arc distance in kilometers (Lambert)
          lamMi = 0.0,        // arc distance in miles (Lambert)
          havKm = 0.0,        // arc distance in kilometers (Haversine)
          havMi = 0.0,        // arc distance in miles (Haversine)
          caRad = 0.0 ;       // central angle in radians

   if ( this->formula == sfLambert )               // use the Lambert formula
   {
      this->CalculateArcLambert ( lamKm, lamMi, caRad ) ;
      this->arcDistKm = lamKm ;
      this->arcDistMi = lamMi ;
      this->arcTheta  = caRad ;
   }

   else if ( this->formula == sfHaversine )        // use the Haversine formula
   {
      this->CalculateArcHaversine ( havKm, havMi ) ;
      this->arcDistKm = havKm ;
      this->arcDistMi = havMi ;
   }

   else // (this->formula == sfAll) -- calculate using EACH supported formulae
   {
      this->CalculateArcLambert ( lamKm, lamMi, caRad ) ;
      this->arcDistKm = lamKm ;
      this->arcDistMi = lamMi ;
      this->arcTheta  = caRad ;
      this->CalculateArcHaversine ( havKm, havMi ) ;
   }

   //*****************************************
   //** Display results of the calculations **
   //*****************************************
   #if INCLUDE_API != 0    //** If API methods are included in library build
   if ( this->windowOutput )
      this->ACWinReport ( lamKm, lamMi, havKm, havMi ) ;
   else
   #endif   // INCLUDE_API
      this->TTY_Report ( lamKm, lamMi, havKm, havMi ) ;

   //* Reset to default fg/bg attributes and flush the output buffer *
   this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;

}  //* End ArcDistance() *

//*************************
//*  CalculateArcLambert  *
//*************************
//********************************************************************************
//* Calculate the distance between the two GPS location points using the         *
//* Lambert formula for calculating the shortest distance two points along the   *
//* surface of an ellipsoid.                                                     *
//* The two GPS locations are in the 'coord_a' and 'coord_b' members.            *
//*                                                                              *
//* Note that coord_x.valid indicates whether the data are valid.                *
//* ** DO NOT calculate based on invalid data. It is uncertain whether the       *
//*    trig functions can manage divide-by-zero and other math errors.           *
//*                                                                              *
//* Input  : adKm   : (by reference) receives the calculated arc distance        *
//*                   in kilometers                                              *
//*          adMi   : (by reference) receives the calculated arc distance        *
//*                   in miles                                                   *
//*          caRad  : (by reference) receives the calculated central angle       *
//*                   in radians                                                 *
//*                                                                              *
//* Returns: 'true' if both 'coord_a' and 'coord_' are valid                     *
//*            (arc distance calculated and returned in 'adKm' and 'adMi')       *
//*          'false' otherwise (arc distance not calculated)                     *
//********************************************************************************
//* Lambert Formula:              f                                              *
//*                    d = a(σ - --- (X + Y))                                    *
//*                               2                                              *
//*                                                                              *
//* a == equatorial radius of the ellipsoid (6,378 km or 3,963 mi)               *
//* σ == central angle in radians between the points                             *
//*      0° < σ < 360°  or  0 < σ < 2π (radians)                                 *
//*      σ = (180L / πR) = L / R                                                 *
//*        where L is is the length of the minor arc of the circle               *
//*        and R is the radius of the circle.                                    *
//* f == flattening of the earth (ratio of equatorial and polar radii)           *
//*      Calculated values (Wikipedia):                                          *
//*       Meridional radius: (b*b)/a == 6335.439km                               *
//*       Polar radius     : (a*a)/b == 6399.594km                               *
//*       f = (6399.594 - 6335.439) / 6399.594 = 0.010024855                     *
//*      NASA direct measurement:                                                *
//*       Equatorial radius: 6378.0km                                            *
//*       Polar radius     : 6357.0km                                            *
//*       f = (6378.0 - 6357.0) / 6378.0 = 0.003292568                           *
//*                                                                              *
//*                       2       2                                              *
//*                    sin (P) cos (Q)                                           *
//* X == (σ - sin(σ)) -------------------------                                  *
//*                        2  σ                                                  *
//*                     cos (---)                                                *
//*                           2                                                  *
//*                                                                              *
//*                       2       2                                              *
//*                    cos (P) sin (Q)                                           *
//* Y == (σ + sin(σ)) ------------------------                                   *
//*                        2  σ                                                  *
//*                     sin (---)                                                *
//*                           2                                                  *
//*                                                                              *
//* P == (β  + β ) / 2   and   Q == (β  - β ) / 2                                *
//*        1    2                     2    1                                     *
//*                                                                              *
//* β  and  β   are "reduced (parametric) latitudes" using the formula:          *
//*  1       2                                                                   *
//*                                                                              *
//*  tan(β) = (1 - f)tan(Φ)   where Φ is the latitude of a point                 *
//*                                                                              *
//*                   --------------------------                                 *
//* The description of the Lambert function, above is adapted from the           *
//* description given at:                                                        *
//*   https://en.wikipedia.org/wiki/Geographical_distance                        *
//*                                         #Lambert's_formula_for_long_lines    *
//*                                                                              *
//* The results returned by this method have been compared to the results        *
//* provided by the calculator at:                                               *
//*   https://www.calculator.net/distance-calculator.html                        *
//*   (Maple Tech. International LLC),                                           *
//* The differences are small, and may be due to differences in the underlying   *
//* math libraries used by the GNU C++ compiler and the Javascript libraries     *
//* used by the calculator(s) on that site.                                      *
//* ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---  ---    *
//*                                                                              *
//* A) Be aware of the APPARENT singularities and discontinuities encountered    *
//*    in spherical coordinate calculations, specifically:                       *
//*     1) Longitude is not uniquely defined at the poles (+/- 90° latitude).    *
//*     2) On the antimeridian (+/-180.0° longitude) there is a discontinuity.   *
//*    In these special cases, we need to report that calculations using         *
//*    points near these positions may give inacurate results.                   *
//*                                                                              *
//* B) A "central angle" is an angle whose apex (where the legs meet) is the     *
//*    center of a circle and the legs are radii intersecting the circle.        *
//*       Φ = d / r                                                              *
//*    where d == distance between the two points (length of great circle arc)   *
//*          r == radius of the circle (on a sphere)                             *
//*                                                                              *
//* C) Arc Length    = radius * central angle(radians)                           *
//*    Central Angle = Arc length(AB) / Radius(OA) = (s × 360°) / 2πr,           *
//*     where 's' is arc length, and 'r' is radius of the circle.                *
//*    Radius        = arc length / central angle                                *
//*                                                                              *
//* D) There is a difference in the radius of the earth at different latitudes.  *
//*    This is due to the "flattening" of the earth into an ellipsoid.           *
//*    This "flattening" is defined as a constant below.                         *
//*    The earth's radius is at its greatest at the equator (latitude 0.0R).     *
//*    At the poles, of course, the earth's radius approaches zero.              *
//*    Can we quantify the effects of differences in radius based on latitude?   *
//*    Are these difference significant? Do they cause meaningful errors in      *
//*    calculation of the arc distance?                                          *
//*                                                                              *
//* E) Which equatorial and polar radii should be used?                          *
//*    The "accepted" values are : a == 6335.439km  and  b == 6399.594km         *
//*    The NASA (measured) values: a == 6378km      and  b == 6357km             *
//*    Globally averaged value   : 6371km (3959mi)                               *
//*     where 'a' is the equatorial radius and 'b' is the polar radius           *
//*    The choice of radius also affects 'f' (flattening).                       *
//*                                                                              *
//*                                                                              *
//********************************************************************************

bool EarthPoints::CalculateArcLambert ( ldbl_t& adKm, ldbl_t& adMi, ldbl_t& caRad )
{
   #define DEBUG_CAL (0)

   #if 1    // Authalic mean radius
   const ldbl_t e_radiusKm = eradKm ;           // equatorial radius (kilometers)
   const ldbl_t e_radiusMi = eradMi ;           // equatorial radius (miles)
   #else    // NASA measured radius
   const ldbl_t e_radiusKm = 6378.0 ;           // equatorial radius (kilometers)
   const ldbl_t e_radiusMi = 3963.0 ;           // equatorial radius (miles)
   #endif   // earth's radius
   //* Flattening of the earth (ellipsoid ratio) *
   const ldbl_t e_radMinKm = 6357.0 ;           // minimum radius
   const ldbl_t e_radMaxKm = 6378.0 ;           // maximum radius
   const ldbl_t f = (e_radMaxKm - e_radMinKm) / e_radMaxKm ;

   adKm = adMi = caRad = 0.0 ;                  // initialize caller's variables

   //* Calculate the central angle between the points *
   #if 1    // Use chord length to calculate angle
   ldbl_t theta = this->chordAngle ( this->coord_a.lat.rad, this->coord_a.lon.rad,
                                     this->coord_b.lat.rad, this->coord_b.lon.rad ) ;
   #else    // Use Haversine formula to calculate angle (less accurate)
   ldbl_t theta = this->havAngle ( this->coord_a.lat.rad, this->coord_a.lon.rad,
                                   this->coord_b.lat.rad, this->coord_b.lon.rad ) ;
   #endif   // chord length

   caRad = theta ;                              // save central angle to caller's variable

   ldbl_t lat_a = this->coord_a.lat.rad ;       // latitude, point A (radians)
   ldbl_t lat_b = this->coord_b.lat.rad ;       // latitude, point B (radians)
   ldbl_t B1    = (1 - f) * tanl(lat_a) ;       // "reduced" latitude for point A
   ldbl_t B2    = (1 - f) * tanl(lat_b) ;       // "reduced" latitude for point B
   ldbl_t P     = (B1 + B2) / 2.0 ;             // latitude average for A+B
   ldbl_t Q     = (B2 - B1) / 2.0 ;             // half the difference in latitude for A and B
   ldbl_t stheta = sinl ( theta ) ;             // sin of central angle

#if 0    // EXPERIMENTAL
ldbl_t s2p = powl( (sinl( P )), 2.0 ),
       c2q = powl( (cosl( Q )), 2.0 ),
       c2t = powl( (cosl( theta / 2.0 )), 2.0 ) ;
   ldbl_t X = (theta - stheta) *                // Average in latitude
               ((s2p * c2q) / c2t) ;

   ldbl_t Y = (theta + stheta) *                // Difference in latutude
               ((powl( (cosl( P )), 2.0 ) * powl( (sinl( Q )), 2.0 )) /
                (powl( (sinl( theta / 2.0 )), 2.0 ))) ;
#else    // Seperated sin(theta)
   ldbl_t X = (theta - stheta) *                // Average in latitude
               ((powl( (sinl( P )), 2.0 ) * powl( (cosl( Q )), 2.0 )) /
                (powl( (cosl( theta / 2.0 )), 2.0 ))) ;

   ldbl_t Y = (theta + stheta) *                // Difference in latutude
               ((powl( (cosl( P )), 2.0 ) * powl( (sinl( Q )), 2.0 )) /
                (powl( (sinl( theta / 2.0 )), 2.0 ))) ;
#endif   // Seperated sin(theta)

   #if DEBUG_EPNTS != 0 && DEBUG_CAL != 0
   gString gsdbg( "    CAL: lat_a:%3.6Lf lat_b:%3.6Lf\n"
                  "            B1:%3.6Lf    B2:%3.6Lf\n"
                  "             P:%3.6Lf     Q:%3.6Lf\n"
                  "             X:%3.6Lf     Y:%3.6Lf\n",
                  &lat_a, &lat_b, &B1, &B2, &P, &Q, &X, &Y ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   ldbl_t slxm = theta - stheta, slxp = theta + stheta ;
   gsdbg.compose( "         theta:%3.6Lf sin(theta):%3.6Lf th-sth:%3.6Lf  th+tsh:%3.6Lf\n",
                  &theta, &stheta, &slxm, &slxp ) ;
   this->ansi->ttyWrite ( gsdbg ) ;
   #endif   // DEBUG_EPNTS && DEBUG_CAL

   //* Arc Distance in Kilometers *
   adKm = e_radiusKm * (theta - (f / 2.0) * (X + Y)) ;

   //* Arc Distance in Miles *
   adMi = e_radiusMi * (theta - (f / 2.0) * (X + Y)) ;

   bool status = false ;                         // return value

   return status ;

   #undef DEBUG_CAL
}  //* End CalculateArcLambert() *

//*************************
//* CalculateArcHaversine *
//*************************
//********************************************************************************
//* Calculate the distance between the two GPS location points using the         *
//* Haversine formula for calculating the length of an arc on a sphere, given    *
//* their latitude and longitude.                                                *
//* The two GPS locations are in the 'coord_a' and 'coord_b' members.            *
//*                                                                              *
//* Note that coord_x.valid indicates whether the data are valid.                *
//* ** DO NOT calculate based on invalid data. It is uncertain whether the       *
//*    trig functions can manage divide-by-zero and other math errors.           *
//*                                                                              *
//* Input  : adKm   : (by reference) receives the calculated arc distance        *
//*                   in kilometers                                              *
//*          adMi   : (by reference) receives the calculated arc distance        *
//*                   in miles                                                   *
//*                                                                              *
//* Returns: 'true' if both 'coord_a' and 'coord_b' are valid                    *
//*            (arc distance calculated and returned in 'adKm' and 'adMi')       *
//*          'false' otherwise (arc distance not calculated)                     *
//********************************************************************************
//* Haversine Formula:  https://en.wikipedia.org/wiki/Haversine_formula          *
// ====================================================================          *
//*                                      d        (distance between points)      *
//* θ (theta) == the central angle i.e. ---                                      *
//*                                      r        (radius of the earth)          *
//*             2  ( θ )     1 - cos(θ)                                          *
//* hav(θ) = sin   (---)  =  ----------                                          *
//*                ( 2 )         2                                               *
//*                                                                              *
//* hav θ = hav(delta*latitude) + cos(latitude sub1) * cos(latitude sub2)        *
//*           * hav(delta*longitude)                                             *
//* (delta * latitude) == lat, sub2 - lat,sub1  and                              *
//* (delta * longitude) == lon,sub2 - lon,sub1                                   *
//*                                                                              *
//* Finally, the haversine function hav(θ), applied above to both the            *
//* central angle θ and the differences in latitude and longitude, is:           *
//*                                                                              *
//* hav θ = sin²(θ / 32) = (1 - cos(θ)) / 2                                      *
//*                                                                              *
//* Haversine formula:                                                           *
//* a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2)                                *
//* c = 2 ⋅ atan2( √a, √(1−a) )                                                  *
//* d = R ⋅ c                                                                    *
//* where 	φ is latitude, λ is longitude, R is earth’s radius                   *
//*                   (earth's mean radius = 6,371km);                           *
//* note that angles need to be in radians to pass to trig functions!            *
//*                   ---  ---  ---  ---  ---  ---  ---                          *
//* Programmer's Note: This is a rough approximation only. We can do better.     *
//********************************************************************************

bool EarthPoints::CalculateArcHaversine ( ldbl_t& adKm, ldbl_t& adMi )
{
   bool status = false ;

   adKm = adMi = 0.0 ;        // initialize caller's variables
   if ( this->coord_a.valid && this->coord_b.valid )
   {
      //* Absolute difference in latitude and longitude. *
      ldbl_t absDiffLat = fabsl ( this->coord_a.lat.rad - this->coord_b.lat.rad ) ;
      ldbl_t absDiffLon = fabsl ( this->coord_a.lon.rad - this->coord_b.lon.rad ) ;

      //* Sine of half the latitude difference (in radians) *
      ldbl_t sinHalfLatDist = sinl ( absDiffLat / 2.0 ) ;
      //* Sine of half the longitude difference (in radians) *
      ldbl_t sinHalfLonDist = sinl ( absDiffLon / 2.0 ) ;

      //* Square of latitude sine *
      ldbl_t sqLat = pow ( sinHalfLatDist, 2 ) ;
      //* Square of longitude sine *
      ldbl_t sqLon = pow ( sinHalfLonDist, 2 ) ;

      //* Cosine of latitude A *
      ldbl_t cosLatA = cosl ( this->coord_a.lat.rad ) ;
      //* Cosine of latitude B *
      ldbl_t cosLatB = cosl ( this->coord_b.lat.rad ) ;

      //* Calculate what-is-it? *
      ldbl_t tempval = sqLat + (cosLatA * cosLatB) * sqLon ;
      ldbl_t arcDist = asinl ( sqrtl ( tempval ) ) ;

      ldbl_t radiusKm = 6371.0 ;
      ldbl_t radiusMi = 3956.0 ;
//      this->arcDistKm = arcDist * radiusKm ;
//      this->arcDistMi = arcDist * radiusMi ;
      adKm = arcDist * radiusKm * 2.0 ;
      adMi = arcDist * radiusMi * 2.0 ;

      status = true ;
   }
   return status ;

//======== REFERENCE =============================================================
#if 0   // REFERENCE
long double distance(long double lat1, long double long1,
                     long double lat2, long double long2)
{
// https://community.esri.com/t5/coordinate-reference-systems-blog/distance-on-a-sphere-the-haversine-formula/ba-p/902128
// powl() == base raised to a power
// sinl() == sine of the value
// cosl() == cosine of value
// haversine(θ) = sin²(θ/2)
    // Convert the latitudes
    // and longitudes
    // from degree to radians.
    lat1 = toRadians(lat1);
    long1 = toRadians(long1);
    lat2 = toRadians(lat2);
    long2 = toRadians(long2);
     
    // Haversine Formula
    long double dlong = long2 - long1;
    long double dlat = lat2 - lat1;
 
    long double ans = pow(sin(dlat / 2), 2) +
                          cos(lat1) * cos(lat2) *
                          pow(sin(dlong / 2), 2);
 
    ans = 2 * asin(sqrt(ans));
 
    // Radius of Earth in
    // Kilometers, R = 6371
    // Use R = 3956 for miles
    long double R = 6371;
     
    // Calculate the result
    ans = ans * R;
 
    return ans;
}
// ==============================
function getDistanceFromLatLonInKm(lat1,lon1,lat2,lon2) {
  var R = 6371; // Radius of the earth in km
  var dLat = deg2rad(lat2-lat1);  // deg2rad below
  var dLon = deg2rad(lon2-lon1); 
  var a = 
    Math.sin(dLat/2) * Math.sin(dLat/2) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * 
    Math.sin(dLon/2) * Math.sin(dLon/2)
    ; 
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
  var d = R * c; // Distance in km
  return d;

}

function deg2rad(deg) {
  return deg * (Math.PI/180) }
#endif  // REFERENCE
//======== REFERENCE =============================================================
}  //* End CalculateArcHaversine() *

//*************************
//*       havAngle        *
//*************************
//******************************************************************************
//* Given two(2) latitude/longitude points, calculate the central angle,       *
//* theta (θ).                                                                 *
//*                                                                            *
//* The "central angle" is the angle at the center of the spheroid with the    *
//* legs being the radii (in kilometers) connecting to the specified points    *
//* on the sphere.                                                             *
//*                                                                            *
//* Important Note: The Haversine formula is NOTORIOUSLY INACCURATE.           *
//*                 See notes below.                                           *
//*                                                                            *
//* Input  : latA  : point A latitude                                          *
//*          lonA  : point A longitude                                         *
//*          latB  : point B latitude                                          *
//*          lonB  : point B longitude                                         *
//*                                                                            *
//* Returns: central angle                                                     *
//******************************************************************************
//* Notes:                                                                     *
//* The Haversine formula is notoriously inaccurate, but widely used.          *
//* Although the literature claims that the Haversize formula is accurate to   *
//* within 0.5%, this is highly suspect. It may _average_ 0.5% deviation;      *
//* however, we have determined that the straightforward implementation here   *
//* yields a central angle that is accurate to only three(3) decimal places    *
//* which is wholly unacceptable.                                              *
//*                                                                            *
//* Calculating the distance between two cities that are approximately 100km   *
//* apart, we get a central angle (theta) of 0.014253012.                      *
//* Using the calculators.net calculator, we solved for the same two cities    *
//* and (indirectly) got a central angle of: 0.014859503.                      *
//*   calculators.net returns 94.67km (58.82mi).                               *
//*   Solving for the central angle, we get:                                   *
//*   d / r == theta == 94.67 / 6371.0072 == 0.014859503                       *
//*                   Compare to our result: 0.01425301 (dist off by ~ 4.0km)  *
//*   This is a difference of 0.96%. We may be able to tweak this somewhat to  *
//*   compensate for weaknesses in the math library functions, but we really   *
//*   don't want to create a table of fudge factors!!                          *
//*   Note: When theta==0.0148804, it yields a distance OF 94.67Km and 58.82Mi *
//*         when called by the Lambert method, above.                          *
//*                                                                            *
//******************************************************************************

ldbl_t EarthPoints::havAngle ( ldbl_t latA, ldbl_t lonA, ldbl_t latB, ldbl_t lonB )
{
   ldbl_t hLatDelta   = (fabsl( latB - latA )) / 2.0,    // half the latitude difference
          hLonDelta   = (fabsl( latB - latA )) / 2.0,    // half the longitude difference
          latSinSqr   = powl( hLatDelta, 2 ),            // sin squared of hLatDelta
          lonSinSqr   = powl( hLonDelta, 2 ),            // sin squared of hLonDelta
          cosLatA     = cosl( latA ),                    // cosine of latitude A
          cosLatB     = cosl( latB ),                    // cosine of latitude B
          radicand = latSinSqr + cosLatA * cosLatB * lonSinSqr, // data inside radical
          sqrroot  = sqrtl( radicand ),                  // square root value
          theta    = 2 * asinl( sqrroot ) ;              // central angle

   return theta ;

}  //* End havAngle() *

//*************************
//*      chordAngle       *
//*************************
//********************************************************************************
//* Given two(2) latitude/longitude points, calculate the central angle,         *
//* theta (θ).                                                                   *
//*                                                                              *
//* The "central angle" is the angle at the center of the spheroid with the      *
//* legs being the radii (in kilometers) connecting to the specified points      *
//* on the sphere.                                                               *
//*                                                                              *
//* Input  : latA  : point A latitude                                            *
//*          lonA  : point A longitude                                           *
//*          latB  : point B latitude                                            *
//*          lonB  : point B longitude                                           *
//*                                                                              *
//* Returns: central angle in radians ( 0 <= θ <= 2π )                           *
//********************************************************************************
//* Notes:                                                                       *
//* Calculating the central angle by first calculating the chord length is a     *
//* basic trig function. It uses the two points on a sphere and the straight-    *
//* line distance between those points (the chord length) to calculate the       *
//* central angle.                                                               *
//*      https://en.wikipedia.org/wiki/Great-circle_distance                     *
//*                                                                              *
//* Using the calculators.net calculator, we solved for the distance between     *
//* two cities that are approximately 100km (59mi) apart:                        *
//*   City A latitude/longitude: 39.768611 : -86.158056                          *
//*   City B latitude/longitude: 40.417222 : -86.878611                          *
//*                                                                              *
//* The arc distance returned by calculators.net is: 94.67km (58.82mi).          *
//* Solving for the central angle, theta = d / r  where 'd' is the arc distance  *
//* and 'r' is the radius of the sphere (earth), we get:                         *
//*           theta = d / r = 94.67 / 6371.0072 = 0.014859503                    *
//* We accept this as our goal in calculating the central angle for this pair    *
//* of coordinates.                                                              *
//*                                                                              *
//* - - - - -                                                                    *
//* Calculating the central angle from the chord length, the distance between    *
//* two cities represented by these points, we get a central angle (theta) of    *
//* 0.014856164. This accuracy to five(5) decimal places is considerably better  *
//* than the three(3) decimal place accuracy of using the Haversine formula      *
//* to calculate the central angle.                                              *
//*                                                                              *
//* - - - - -                                                                    *
//* Using the basic spherical law of cosines, to calculate the central angle,    *
//* we should get a highly accurate result (assuming a true sphere).             *
//* theta =                                                                      *
//*  arccos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon2 - lon1))    *
//* It follows that for 'r' == earth's radius, the arc distance 'd' is then:     *
//*   d = r * theta                                                              *
//* NOTE: For the test pair of coordinates, this calculation yields identical    *
//*       result (to nine(9) decimal places) as the chordAngle() calculation.    *
//*                                                                              *
//*                                                                              *
//* Experimentation:                                                             *
//* This method calculates the central angle for the 100km-distance test pair    *
//* as follows:                                                                  *
//*   chord method   : 0.014856164                                               *
//*   law of cosines : 0.014856164                                               *
//*   calculators.net: 0.014859503                                               *
//*                    -----------                                               *
//* difference(theta): 0.000003339 or 94.670000km - 94.414438km == 0.255562km    *
//* This is still a fairly large error (0.27%), but the generally accepted       *
//* error using the Haversine formula is 0.5%.                                   *
//* We need to create a chart showing the deviations for distances from          *
//* 1.0km to 10,000km. With this chart, we can begin to get some clarity on      *
//* the percentage error we are forced to accept.                                *
//*                                                                              *
//*                                                                              *
//*                                                                              *
//********************************************************************************

ldbl_t EarthPoints::chordAngle ( ldbl_t latA, ldbl_t lonA, ldbl_t latB, ldbl_t lonB )
{
   #if 1    // See notes above 
   ldbl_t deltaX = (cosl( latB ) * cosl( lonB )) - (cosl( latA ) * cosl( lonA )),
          deltaY = (cosl( latB ) * sinl( lonB )) - (cosl( latA ) * sinl( lonA )),
          deltaZ = sinl( latB ) - sinl( latA ),
          chord  = sqrtl( powl( deltaX, 2 ) + powl( deltaY, 2 ) + powl( deltaZ, 2 ) ),
          theta  = 2.0 * asinl( chord / 2.0 ) ;
   #elif 1  // Spherical Law of Cosines
   ldbl_t theta = acosl( (sinl( latA ) * sinl( latB )) +
                         (cosl( latA ) * cosl( latB ) * cosl( lonB - lonA ))
                       ) ;
   #else    // EXPERIMENTAL

   #endif   // See notes above

   return theta ;

}  //* End chordAngle() *

//*************************
//*        Deg2Rad        *
//*************************
//******************************************************************************
//* Convert degrees to radians.                                                *
//* It is assumed that the 'degree' value has been range checked.              *
//*                                                                            *
//* The formula is:                         pi                                 *
//*                    radians = degrees * -----                               *
//*                                        180.0                               *
//*                                                                            *
//* Input  : degree : degree value to be converted                             *
//*                                                                            *
//* Returns: radian value                                                      *
//******************************************************************************

ldbl_t EarthPoints::Deg2Rad ( ldbl_t degree )
{
#if 0    // WHY must radians be positive?
   //* Radians must be a positive value, so if negative *
   //* degrees, use complement for calculation.         *
   if ( degree < 0.0 )
      degree = maxDegrees + degree ;
#endif   // SUSPECT

   ldbl_t rad = degree * radsPerDeg ;
   return rad ;

}  //* End Deg2Rad() *

//*************************
//*        Rad2Deg        *
//*************************
//******************************************************************************
//* Convert radians to degrees.                                                *
//* It is assumed that the 'radian' value has been range checked.              *
//*                                                                            *
//* The formula is:                        180.0                               *
//*                    degrees = radians * -----                               *
//*                                         pi                                 *
//*                                                                            *
//* Input  : radian : radian value to be converted                             *
//*                                                                            *
//* Returns: degree value                                                      *
//******************************************************************************

ldbl_t EarthPoints::Rad2Deg ( ldbl_t radian )
{

   ldbl_t deg = radian * semiCircle / m_pi ;
   if ( deg > maxDegrees )          // valid degree range -180.0 <= deg <= 180.0
      deg = maxDegrees - deg ;      // use complement value
   return deg ;

}  //* End Rad2Deg() *

//*************************
//*       degRange        *
//*************************
//******************************************************************************
//* Range check (range limit) for degree values.                               *
//* If the 'degree' value is in range, it is returned unchanged.               *
//* For values out-of-range low (< 0.0), minDegrees is returned.               *
//* For values out-of-range high (> 2*pi), maxDegrees is returned.             *
//*                                                                            *
//* Input  : degree : degree value to be range-checked                         *
//*                                                                            *
//* Returns: range-limited degree value                                        *
//*          If range error, application exit code is updated (see 'exitCode').*
//******************************************************************************

ldbl_t EarthPoints::degRange ( ldbl_t degree )
{
   ldbl_t deg = degree ;      // return value (initially assumed to be in range)

   if ( degree < minDegrees )
   { deg = minDegrees ; this->SetWarningCode ( xcOOR ) ; }
   else if ( degree > maxDegrees )
   { deg = maxDegrees ; this->SetWarningCode ( xcOOR ) ; }

   return deg ;

}  //* End degRange() *

//*************************
//*       radRange        *
//*************************
//******************************************************************************
//* Range check (range limit) for radian values.                               *
//* If the 'radian' value is in range, it is returned unchanged.               *
//* For values out-of-range low (< -PI), minRadians is returned.               *
//* For values out-of-range high (> PI), maxRadians is returned.               *
//*                                                                            *
//* Input  : radian : radian value to be range-checked                         *
//*                                                                            *
//* Returns: range-limited radian value                                        *
//*          If range error, application exit code is updated (see 'exitCode').*
//******************************************************************************

ldbl_t EarthPoints::radRange ( ldbl_t radian )
{
   ldbl_t rad = radian ;      // return value (initially assumed to be in range)

   if ( radian < minRadians )
   { rad = minRadians ; this->SetWarningCode ( xcOOR ) ; }
   else if ( radian > maxRadians )
   { rad = maxRadians ; this->SetWarningCode ( xcOOR ) ; }

   return rad ;

}  //* End radRange() *

//*************************
//*      TTY_Report       *
//*************************
//******************************************************************************
//* Format the output using TTY-style (Teletype) formatting.                   *
//*                                                                            *
//*                                                                            *
//* Input  : lamKm  : calculated arc distance in kilometers (Lambert formula)  *
//*          lamMi  : calculated arc distance in miles (Lambert formula)       *
//*          havKm  : calculated arc distance in kilometers (Haversine formula)*
//*          havMi  : calculated arc distance in miles (Haversine formula)     *
//*                                                                            *
//* Returns: nothing                                                           *
//******************************************************************************

void EarthPoints::TTY_Report ( ldbl_t lamKm, ldbl_t lamMi, ldbl_t havKm, ldbl_t havMi )
{
   gString gsOut ;                  // Text formatting
   short txtRows = ZERO,            // Number of text rows (height of window interior)
         txtCols = ZERO,            // Number of text columns (width of window interior)
         outPad  = ZERO ;           // Number of columns of padding

   //* Format and display the window title *
   //* and the underlying source data.     *
   this->FormatSourceData ( gsOut, txtRows, txtCols ) ;
   this->ansi->ttyWrite ( gsOut, false ) ;

   //* If default foreground, set 'bold' attribute.*
   //* (Bright colors have insufficient contrast.) *
   if ( this->fgndAttr == aesFG_DFLT )
      this->ansi->acSetMod ( aesBOLD ) ;

   //* Format and display the results of   *
   //* the arc-distance calculation(s).    *
   if ( this->formula == sfAll )    // Report results of all calculation methods
   {
      const short indentLam = 11 ;
      const short indentHav = 9 ;

      //* Format and output the Lambert report.*
      gsOut.clear() ;                           // Indent the output
      gsOut.padCols( indentLam ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;   // Write the indentation string
      this->ansi->acSetMod ( aesUNDERLINE ) ;   // Set 'underlined' attributes
      gsOut.compose( "Lambert Distance: %-3.6Lf km  (%-3.6Lf mi)", &lamKm, &lamMi ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;
      outPad = txtCols - indentLam - (gsOut.gscols()) ;
      this->ansi->acSetMod ( aesUNDERLINE_OFF ) ; // Reset 'underlined' attribute
      gsOut.clear() ;                           // Pad to full width
      gsOut.padCols( outPad ) ;
      this->ansi->ttyWrite ( gsOut, false, true ) ;

      //* Format and output the Haversine report.*
      gsOut.clear() ;                           // Indent the output
      gsOut.padCols( indentHav ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;   // Write the indentation string
      this->ansi->acSetMod ( aesUNDERLINE ) ;   // Set 'underlined' attributes
      gsOut.compose( "Haversine Distance: %-3.6Lf km  (%-3.6Lf mi)", &havKm, &havMi ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;
      outPad = txtCols - indentHav - (gsOut.gscols()) ;
      this->ansi->acSetMod ( aesUNDERLINE_OFF ) ; // Reset 'underlined' attribute
      gsOut.clear() ;                           // Pad to full width
      gsOut.padCols( outPad ) ;
      gsOut.append( L"\n\n" ) ;
      this->ansi->ttyWrite ( gsOut, false ) ;
   }
   else                             // Report the specified results only
   {
      const short indentCols = 13 ;
      gsOut.clear() ;                           // Indent the output
      gsOut.padCols( indentCols ) ;
      this->ansi->ttyWrite ( gsOut ) ;          // Write the indentation string
      this->ansi->acSetMod ( aesUNDERLINE ) ;   // Set 'underlined' attribute

      //* Format and output the distance report.*
      gsOut.compose( "Distance: %-3.6Lf km  (%-3.6Lf mi)",
                     &this->arcDistKm, &this->arcDistMi ) ;
      this->ansi->ttyWrite ( gsOut, true ) ;
      this->ansi->acSetMod ( aesUNDERLINE_OFF ) ; // Reset 'underlined' attribute

      outPad = txtCols - indentCols - (gsOut.gscols()) ;
      gsOut.clear() ;
      gsOut.padCols( outPad ) ;
      gsOut.append( L"\n\n" ) ;
      this->ansi->ttyWrite ( gsOut ) ;
   }

   //* Reset attributes to terminal defaults.*
   this->ansi->acSetFgBg ( aesFG_DFLT, aesBG_DFLT ) ;

}  //* End TTY_Report() *

//*************************
//*      ACWinReport      *
//*************************
//********************************************************************************
//* Format the output using an AcWin-class window format.                        *
//*                                                                              *
//*                                                                              *
//* Input  : lamKm  : calculated arc distance in kilometers (Lambert formula)    *
//*          lamMi  : calculated arc distance in miles (Lambert formula)         *
//*          havKm  : calculated arc distance in kilometers (Haversine formula)  *
//*          havMi  : calculated arc distance in miles (Haversine formula)       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Notes:                                                                       *
//* ------                                                                       *
//* Window position == this->winpos                                              *
//*                                                                              *
//* Window dimensions:                                                           *
//*    Height == height of data                                                  *
//*    Width  == width of window title + 6 (this is more that the data requires) *
//*                                                                              *
//* Window color attributes:                                                     *
//* There are up to four(4) source colors provided:                              *
//*    1) this->fgndAttr == text foreground (aeSeq)                              *
//*    2) this->bgndAttr == text background (aeSeq)                              *
//*    3) this->bdrfAttr == window border background (acAttr)                    *
//*    4) this->bdrbAttr == window border foreground (acAttr)                    *
//* If non-default color attributes provided, convert the provided aeSeq         *
//* attributes to acAttr bitfield attributes. Perform a logical OR of the        *
//* bitfields to create foreground/background for border and interior attributes.*
//* -- If default fgnd/bgnd attributes, then use the our locally-defined         *
//*    attributes.                                                               *
//* -- The 'butAttr' attribute (user instructions i.e. Pushbutton) is always set *
//*    to white-on-intense-red. This should work because interior color cannot   *
//*    be specified with the 'intense' bit set, so contrast is sufficient.       *
//*                                                                              *
//********************************************************************************

#if INCLUDE_API != 0    //** If API methods are included in library build
void EarthPoints::ACWinReport ( ldbl_t lamKm, ldbl_t lamMi, ldbl_t havKm, ldbl_t havMi )
{
   const wchar_t * buttonText = L"  Press Any Key To Exit...  " ;
   const short indentArc = 10 ;        // single-record indent
   const short indentLam = 8 ;         // Lambert-record indent
   const short indentHav = 6 ;         // Haversize-record indent

   gString gsOut, gsSrc, gsTit, gsArc ;// Text formatting
   acAttr acaTxtFg = acaFG_BROWN,      // Text attributes
          acaTxtBg = acaBGb_GREY,
          acaBdrFg = acaFGb_BROWN,     // Border attributes
          acaBdrBg = acaBG_BLUE ;
   short  txtRows = ZERO,              // Number of text rows in formatted output
          txtCols = ZERO ;             // Number of text columns formatted output

   //* Format the window title *
   this->ComposeTitle ( gsTit ) ;
   short off = gsTit.find( NEWLINE ) ;
   if ( off > ZERO )
      gsTit.limitChars( off ) ;
   gsTit.erase( L"(epts) " ) ;
   gsTit.erase( L"Copyright" ) ;
   gsTit.insert( L' ' ) ;
   gsTit.append( L' ' ) ;

   //* Format the underlying source data.*
   this->FormatSourceData ( gsSrc, txtRows, txtCols, (gsTit.gscols() + 4) ) ;

   //* Append the arc calculation(s) *
   if ( this->formula == sfAll )    // Report results of all calculation methods
   {
      gsOut.clear() ;
      gsOut.padCols( indentLam ) ;
      gsArc.compose( "%SLambert Distance: %-3.6Lf km  (%-3.6Lf mi)\n", 
                     gsOut.gstr(), &lamKm, &lamMi ) ;

      gsOut.clear() ;
      gsOut.padCols( indentHav ) ;
      gsArc.append( "%SHaversine Distance: %-3.6Lf km  (%-3.6Lf mi)", 
                    gsOut.gstr(), &havKm, &havMi ) ;
      gsSrc.append( gsArc.gstr() ) ;
      ++txtRows ;
   }
   else                             // Report the specified results only
   {
      gsOut.clear() ;
      gsArc.padCols( indentArc ) ;
      gsArc.append( "%SDistance: %-3.6Lf km  (%-3.6Lf mi)",
                    gsOut.gstr(), &this->arcDistKm, &this->arcDistMi ) ;
      gsSrc.append( gsArc.gstr() ) ;
   }

   //**************************
   //* Define an ACWin object *
   //**************************
   short winHeight = txtRows + 5,         // window dimensions
         winWidth  = txtCols + 2,
         termRows, termCols ;             // terminal dimensions
   this->ansi->acGetTermDimensions ( termRows, termCols ) ;

   //* If window is to be centered, either horizontally or vertically *
   if ( this->winpos.row == ZERO )
      this->winpos.row = termRows / 2 - winHeight / 2 ;
   if ( this->winpos.col == ZERO )
      this->winpos.col = termCols / 2 - winWidth / 2 ;

   //* Range check: (Minimum values have been verified   *
   //* during capture of command-line arguments.)        *
   //* Although the ACWin constructor does a range check *
   //* and automagically adjusts the position if         *
   //* necessary, don't be a lazy slob; do it yourself.  *
   if ( (this->winpos.row + winHeight - 1) > termRows )
      this->winpos.row = termRows - winHeight + 1 ;
   if ( (this->winpos.col + winWidth - 1) > termCols )
      this->winpos.col = termCols - winWidth + 1 ;

   //* Convert asSeq attributes to acAttr bitfield attributes. *
   //* Logical OR of fgnd/bgnd values. (see notes above)       *
   bool setFlag ;       // required for call, not referenced
   acaExpand acax ;     // attribute format conversion
   if ( this->fgndAttr != aesFG_DFLT )
      acaTxtFg = acAttr(acax.aeseq2bits( this->fgndAttr, setFlag )) ;
   if ( this->bgndAttr != aesBG_DFLT )
      acaTxtBg = acAttr(acax.aeseq2bits( this->bgndAttr, setFlag )) ;
   if ( this->bdrfAttr != acaFG_DFLT )
      acaBdrFg = acAttr(this->bdrfAttr | acaBOLD) ;
   if ( this->bdrbAttr != acaBG_DFLT )
      acaBdrBg = this->bdrbAttr ;
   acAttr txtAttr = acAttr(acaTxtFg | acaTxtBg),         // window interior attribute
          bdrAttr = acAttr(acaBdrFg | acaBdrBg),         // window border attribute
          butAttr = acAttr(acaFGb_RED | acaBGb_GREY) ;   // Pushbutton attribute

   //* Define the window controls. *
   skForm *skfPtr = new skForm( 2 ) ;
   skfPtr->ins  = true ;
   skfPtr->togg = true ;
   skfPtr->beep = true ;
   skfPtr->insc = true ;
   skfPtr->fi   = ZERO ;

   gsOut = buttonText ;
   WinPos wpc( short(winHeight - 3), short((winWidth / 2) - ((gsOut.gscols()) / 2) -1) ) ;
   short f = ZERO ;                       // field index 0
   skfPtr->fld[f].type = ftPB ;           // field type == pushbutton
   skfPtr->fld[f].orig = wpc ;            // field position
   skfPtr->fld[f].hgt  = 1 ;              // single row field
   skfPtr->fld[f].wid  = gsOut.gscols() ; // width (in columns)
   skfPtr->fld[f].ip   = ZERO ;           // insertion point (and cursor) at origin
   skfPtr->fld[f].aca.decode(butAttr);    // terminal default color attributes
   skfPtr->fld[f].flow = floShift ;       // line break at character (default)
   skfPtr->fld[f].ro   = false ;          // not read-only (default)
   skfPtr->fld[f].txt = buttonText ;      // field text

   ++f ;                                  // field index 1
   skfPtr->fld[f].type = ftTB ;           // field type == text box
   skfPtr->fld[f].orig = { 1, 0 } ;       // field position
   skfPtr->fld[f].hgt  = txtRows ;        // height (in rows)
   skfPtr->fld[f].wid  = txtCols ;        // width (in columns)
   skfPtr->fld[f].ip   = ZERO ;           // insertion point (and cursor) at origin
   skfPtr->fld[f].aca.decode(txtAttr) ;   // terminal default color attributes
   //skfPtr->fld[f].aca.decode(acAttr(acaTxtFg | acaBGb_GREEN)) ; // (verify field position)
   skfPtr->fld[f].flow = floShift ;       // line break at character (default)
   skfPtr->fld[f].ro   = true ;           // not read-only (default)
   skfPtr->fld[f].txt = gsSrc.gstr() ;    // field text

   WinPos wpos ;                                // cursor position within window
   ACWin *winPtr = new ACWin                    // define an ACWin object
         ( this->winpos.row, this->winpos.col,  // position (upper-left corner)
           winHeight, winWidth,                 // window dimensions
           ltSINGLE,                            // border style
           bdrAttr,                             // border attributes
           txtAttr,                             // interior attributes
           gsTit.ustr(),                        // window title
           skfPtr
         ) ;

   //*************************************
   //* Open the window (make it visible) *
   //*************************************
   //* (Returned cursor is at left edge of first open line.) *
   wpos = winPtr->OpenWindow () ;

   winPtr->acSetEchoOption ( noEcho ) ;   // disable key echo to display

   winPtr->acRead () ;          // Wait for user response

   //* Close the window and delete the object.                   *
   //* Note: Deleting the object DOES NOT clear the display area.*
   //*       To clear the display area, call CloseWindow() before*
   //*       deleting the object.                                *
   //winPtr->CloseWindow () ;
   delete winPtr ;

   short exitrow = this->winpos.row + winHeight + 1 ;
   if ( exitrow > termRows )  exitrow = 1 ;
   this->ansi->acSetCursor ( exitrow, 1 ) ;

}  //* End ACWinReport() *
#endif   // INCLUDE_API

//*************************
//*   FormatSourceData    *
//*************************
//********************************************************************************
//* Format for display the radians and degrees data used to calculate of the     *
//* arc distance.                                                                *
//*                                                                              *
//* -- If verbose output has been specified, ALSO report the                     *
//*    degrees/minutes/seconds data for each GPS coordinate record.              *
//* -- If TTY-style output has been specified, include the application title     *
//*    text in the formatted data.                                               *
//* -- If ACWin-style output has been specified, caller provides the maximum     *
//*    data width and is responsible for formatting the title text.              *
//*                                                                              *
//*                                                                              *
//* Input  : gsFmt   : (by reference) receives the formatted data                *
//*          txtRows : (by reference) receives height (rows) of formatted data   *
//*          txtCols : (by reference) receives width (columns) of formatted data *
//*          maxWidth: for window-data formatting only, maximum data width       *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* Programmer's Note: This method makes extensive use of the gString class      *
//* to format the output. Please refer to the chapter in the documentation which *
//* describes the gString class. The gString documentation is also integrated    *
//* into the NcDialog API documentation.                                         *
//********************************************************************************

void EarthPoints::FormatSourceData ( gString& gsFmt, short& txtRows, short& txtCols, 
                                     short maxWidth )
{
   #define REPORT_ANGLE (0)      // append the central angle to report

   const wchar_t * ColumnHeading1 = 
   L"======================            ======================" ;
   const wchar_t * ColumnHeading2 = 
   L"LATITUDE    LONGITUDE             LATITUDE    LONGITUDE" ;
   const wchar_t * ColumnHeading3 = 
   L"----------  ----------            ----------  ----------" ;
   const char * radText = "  Radians   " ;
   const char * degText = "  Degrees   " ;
   const char * deiText = "  Deg(int)  " ;
   const char * minText = "  Minutes   " ;
   const char * secText = "  Seconds   " ;
   const wchar_t * dfltRecA = L"GPS Record A" ; // default label text
   const wchar_t * dfltRecB = L"GPS Record B" ;
   const short valueWidth  = 10,             // value column width
               recordWidth = 22 ;            // record column width
   const short winINDENT = 3,                // width of left intentation
               ttyINDENT = 5 ;

   gString gsOut, gsVal1, gsVal2 ;           // Text formatting
   wchar_t indentCols[6] ;                   // left indent
   short colAoffset ;                        // column count for left indent

   gsFmt.clear() ;                           // Clear caller's buffer
   txtRows = 7 ;                             // Rows of formatted text
   txtCols = ZERO ;                          // Columns of formatted text

   //* Construct left indent *
   gsOut.padCols( this->windowOutput ? winINDENT : ttyINDENT ) ;
   gsOut.copy( indentCols, 6 ) ;
   colAoffset = gsOut.gscols() ;
   const short colBoffset  = colAoffset + recordWidth + 12 ; // offset to column B

   //* Calculate the width of the display area *
   if ( this->windowOutput )  // Window output format
      txtCols = maxWidth ;
   else                       // TTY output format
   {
      this->ComposeTitle ( gsOut ) ;
      txtCols = gsOut.gscols() / 2 ;
      gsFmt.compose( "%S\n", gsOut.gstr() ) ; // Format the application title *
   }

   //* Format the record labels. If label not specfied, use generic label.*
   gsOut.compose( "%S%S", indentCols,     // Report the column A label 
                  ((this->coord_a.label[0] != NULLCHAR) ? this->coord_a.label : dfltRecA) ) ;
   gsOut.limitCols( colAoffset + recordWidth ) ;      // Truncate label (if necessary)
   gsOut.padCols( colBoffset ) ;          // Pad to beginning of column B
   gsOut.append( ((this->coord_b.label[0] != NULLCHAR) ? // Append the right-column label
                   this->coord_b.label : dfltRecB) ) ;
   gsOut.padCols( txtCols ) ;             // Pad to full width of area
   gsOut.limitCols( txtCols ) ;           // Truncate the lline (if necessary
   gsOut.append( NEWLINE ) ;              // Append newline

   gsFmt.append( gsOut.gstr() ) ;         // Add to formatted data

   //* Append the column headings *
   gsOut.compose( "%S%S", indentCols, ColumnHeading1 ) ;
   gsOut.padCols( txtCols ) ;
   gsFmt.append( "%S\n", gsOut.gstr() ) ;
   gsOut.compose( "%S%S", indentCols, ColumnHeading2 ) ;
   gsOut.padCols( txtCols ) ;
   gsFmt.append( "%S\n", gsOut.gstr() ) ;
   gsOut.compose( "%S%S", indentCols, ColumnHeading3 ) ;
   gsOut.padCols( txtCols ) ;
   gsFmt.append( "%S\n", gsOut.gstr() ) ;

   //* Format the record radians *
   gsVal1.compose( "% 3.5Lf", &this->coord_a.lat.rad ) ; // Format record A latitude
   gsVal1.rightJustify( valueWidth ) ;
   gsVal2.compose( "% 3.5Lf", &this->coord_a.lon.rad ) ; // Format record B longitude
   gsVal2.rightJustify( valueWidth ) ;
   gsOut.compose( "%S%S  %S%s", indentCols, gsVal1.gstr(), gsVal2.gstr(), radText ) ;

   gsVal1.compose( "% 3.5Lf", &this->coord_b.lat.rad ) ; // Format record B latitude
   gsVal1.rightJustify( valueWidth ) ;
   gsVal2.compose( "% 3.5Lf", &this->coord_b.lon.rad ) ; // Format record B longitude
   gsVal2.rightJustify( valueWidth ) ;
   gsOut.append( "%S  %S", gsVal1.gstr(), gsVal2.gstr() ) ;
   gsOut.padCols( txtCols ) ;
   gsOut.append( NEWLINE ) ;

   gsFmt.append( gsOut.gstr() ) ;         // Add to formatted data

   //* Format the record degrees (decimal) *
   gsVal1.compose( "% 3.5Lf", &this->coord_a.lat.dec ) ; // Format record A latitude
   gsVal1.rightJustify( valueWidth ) ;
   gsVal2.compose( "% 3.5Lf", &this->coord_a.lon.dec ) ; // Format record B longitude
   gsVal2.rightJustify( valueWidth ) ;
   gsOut.compose( "%S%S  %S%s", indentCols, gsVal1.gstr(), gsVal2.gstr(), degText ) ;

   gsVal1.compose( "% 3.5Lf", &this->coord_b.lat.dec ) ; // Format record B latitude
   gsVal1.rightJustify( valueWidth ) ;
   gsVal2.compose( "% 3.5Lf", &this->coord_b.lon.dec ) ; // Format record B longitude
   gsVal2.rightJustify( valueWidth ) ;
   gsOut.append( "%S  %S", gsVal1.gstr(), gsVal2.gstr() ) ;
   gsOut.padCols( txtCols ) ;
   gsOut.append( NEWLINE ) ;

   gsFmt.append( gsOut.gstr() ) ;         // Add to formatted data

   if ( this->verboseOutput )
   {
      //* Format integer degrees *
      gsVal1.compose( "% 3.0Lf", &this->coord_a.lat.deg ) ; // Format record A lat deg
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_a.lon.deg ) ; // Format record A lon deg
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.compose( "%S%S  %S%s", indentCols, gsVal1.gstr(), gsVal2.gstr(), deiText ) ;

      gsVal1.compose( "% 3.0Lf", &this->coord_b.lat.deg ) ; // Format record B lat deg
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_b.lon.deg ) ; // Format record B lon deg
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.append( "%S  %S", gsVal1.gstr(), gsVal2.gstr() ) ;
      gsOut.padCols( txtCols ) ;
      gsOut.append( NEWLINE ) ;

      gsFmt.append( gsOut.gstr() ) ;      // Add to formatted data

      //* Format integer minutes *
      gsVal1.compose( "% 3.0Lf", &this->coord_a.lat.min ) ; // Format record A lat min
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_a.lon.min ) ; // Format record A lon min
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.compose( "%S%S  %S%s", indentCols, gsVal1.gstr(), gsVal2.gstr(), minText ) ;

      gsVal1.compose( "% 3.0Lf", &this->coord_b.lat.min ) ; // Format record B lat min
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_b.lon.min ) ; // Format record B lon min
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.append( "%S  %S", gsVal1.gstr(), gsVal2.gstr() ) ;
      gsOut.padCols( txtCols ) ;
      gsOut.append( NEWLINE ) ;

      gsFmt.append( gsOut.gstr() ) ;      // Add to formatted data

      //* Format decimal seconds *
      gsVal1.compose( "% 3.0Lf", &this->coord_a.lat.sec ) ; // Format record A lat sec
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_a.lon.sec ) ; // Format record A lon sec
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.compose( "%S%S  %S%s", indentCols, gsVal1.gstr(), gsVal2.gstr(), secText ) ;

      gsVal1.compose( "% 3.0Lf", &this->coord_b.lat.sec ) ; // Format record B lat sec
      gsVal1.rightJustify( valueWidth ) ;
      gsVal2.compose( "% 3.0Lf", &this->coord_b.lon.sec ) ; // Format record B lon sec
      gsVal2.rightJustify( valueWidth ) ;
      gsOut.append( "%S  %S", gsVal1.gstr(), gsVal2.gstr() ) ;
      gsOut.padCols( txtCols ) ;
      gsOut.append( NEWLINE ) ;

      gsFmt.append( gsOut.gstr() ) ;      // Add to formatted data

      txtRows += 3 ;       // rows of formatted text

      #if REPORT_ANGLE != 0
      //* Include central angle (radians) in the report *
      gsVal1.compose( "% 1.6Lf", &this->arcTheta ) ;  // central angle
      gsVal1.rightJustify( valueWidth ) ;
      gsOut.compose( "%STheta(rad)%S\n", indentCols, gsVal1.gstr() ) ;
      gsOut.shiftCols( valueWidth * 2 + 4 ) ;
      gsFmt.append( gsOut.gstr() ) ;      // Add to formatted data
      ++txtRows ;
      #endif   // REPORT_ANGLE
      #undef REPORT_ANGLE
   }  // verboseOutput

}  //* End FormatSourceData() *

//*************************
//*    SetWarningCode     *
//*************************
//********************************************************************************
//* Set the error/warning code in the 'exitCode' data member.                    *
//* This method ensures that the first error condition found will be the one     *
//* reported. The warning will be set ONLY if 'exitCode' currently contains      *
//* 'xcGOOD' indicating that no errors were previously set.                      *
//*                                                                              *
//* Input  : warn : member of enum xCode indicating the type of warning          *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void EarthPoints::SetWarningCode ( xCode warn )
{

   if ( this->exitCode == xcGOOD )
      this->exitCode = warn ;

}  //* End SetWarningCode() *

//*************************
//*     ComposeTitle      *
//*************************
//********************************************************************************
//* Create a text string including the application title, application version    *
//* number and copywrite years.                                                  *
//*                                                                              *
//* Input  : gs     : (by reference) receives the formatting data                *
//*          tailNL : (optional, 'false' by default)                             *
//*                   if 'true',  append a newline to the end of the string      *
//*                   if 'false', no trailing newline                            *
//*          headNL : (optional, 'false' by default)                             *
//*                   if 'true',  insert a newline at the beginning of string    *
//*                   if 'false', no leading newline                             *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void EarthPoints::ComposeTitle ( gString& gs, bool tailNL, bool headNL )
{
   gs.compose( titleTemplate, appName1, appVersion, crYears ) ;
   if ( tailNL )
      gs.append( NEWLINE ) ;
   if ( headNL )
      gs.insert( NEWLINE ) ;

}  //* End ComposeTitle() *

//*************************
//*    DisplayVersion     *
//*************************
//********************************************************************************
//* Display the application version number and copyright information.            *
//*                                                                              *
//* Input  : verbose : 'false', display only application version and copyright   *
//*                    'true',  include the AnsiCmd-class version number         *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************

void EarthPoints::DisplayVersion ( bool verbose )
{
   static const char* const freeSoftware = 
   "License GPLv3+: GNU GPL version 3 <http://gnu.org/licenses/gpl.html>\n"
   "This is free software: you are free to modify and/or redistribute it\n"
   "under the terms set out in the license.\n"
   "There is NO WARRANTY, to the extent permitted by law.\n\n" ;

   gString gsOut ;
   this->ComposeTitle ( gsOut, true ) ;
   this->ansi->ttyWrite ( gsOut, false ) ;
   if ( verbose )
   {
      int width = gsOut.find( NEWLINE ) ;
      gsOut.compose( "AnsiCmd Link Library v:%s  (gString v:%s)",
                     this->ansi->acVersion (),
                     gsOut.Get_gString_Version() ) ;
      gsOut.padCols( width ) ;         // (too fancy, but fun)
      gsOut.append( NEWLINE ) ;
      gsOut.padCols( width * 2, L'-' ) ;
      this->ansi->ttyWrite ( gsOut, true, true ) ;
   }
   this->ansi->ttyWrite ( freeSoftware ) ;

}  //* End DisplayVersion() *

//*************************
//*      DisplayHelp      *
//*************************
//********************************************************************************
//* Display command-line help message.                                           *
//*                                                                              *
//* Input  : userOption:                                                         *
//*               clHelp - display message to stdout                             *
//*               clHelpless - redirect stdout to the 'less' utility             *
//*                                                                              *
//* Returns: nothing                                                             *
//********************************************************************************
//* GPS locations used for test purposes.                                        *
//* The values for the various formats have been calculated by hand.             *
//*                                                                              *
//* Miami, Florida:            LAT: 25.775163       LON: -80.208615              *
//*                            LAT: 25 46.50978     LON: -80 57.948611           *
//*                            LAT: 25 46 30.5868   LON: -80 57 56.91666         *
//*                            LAT: (radians)       LON: (radians)               *
//* Buenos Aires, Argentina:   LAT: -34.603333333   LON: -58.381666667           *
//*                            LAT: -34 36.20       LON: -58 22.90               *
//*                            LAT: -34 36 12.0     LON: -58 22 54.0             *
//*                            LAT: (radians)       LON: (radians)               *
//*                                                                              *
//********************************************************************************

void EarthPoints::DisplayHelp ( clArgs userOption )
{
    //123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-
   const char* Help1 = 
   "\nCalculate the distance in kilometers and/or miles between point A and"
   "\npoint B which are two global latitude/longitude points."
   "\n "
   "\nUsage: epts [OPTIONS]"
   "\n  For each option that requires one or more arguments, the option name and"
   "\n  its argument(s) may be separated by either '=' or ' ' (space)."
   "\n  Note that if the argument contains whitespace, then the entire argument"
   "\n  sequence must be enclosed within single-quotes or double-quotes."
   "\n    Examples: --coa=Miami:25.775163:80.208615"
   "\n              --coa 'Miami: 25.775163 : 80.208615'"
   "\n              --cob=\"Buenos Aires: -34 36 12.0 : -58.381666667\""
   "\n "
   "\n --coa='[LABEL:] LATITUDE : LONGITUDE'"
   "\n --cob='[LABEL:] LATITUDE : LONGITUDE'"
   "\n    Specify the coordinates for the first (--coa) position, or"
   "\n    the second (--cob) position."
   "\n    LABEL: (optional): city or place name associated with the coordinates"
   "\n           followed by the colon character (':') which separates the"
   "\n           label from the latitude field."
   "\n    LATITUDE : (required) Specify the latitude for the position."
   "\n    ':' (colon) (required): Separates latitude and longitude values."
   "\n    LONGITUDE: (required) Specify the longitude for the position."
   "\n"
   "\n Latitude and longitude values may be expressed in any of four(4) formats:"
   "\n    1) a pair of decimal degree values."
   "\n       --coa='Miami: 25.775163 : -80.208615'"
   "\n       --cob='Buenos Aires: -34.603333333 : -58.381666667'"
   "\n    2) a pair of decimal radian values."
   "\n       --coa='Miami: 0.449861R : -80.208615R'"
   "\n       --cob='Buenos Aires: 5.679243R : 5.264233R'"
   "\n      Note that the trailing 'R' character of each value indicates radians."
//   "\n      Note also that an arc in radians is expressed as a positive value only."
   "\n    3) an integer degree value followed by a decimal minutes value."
   "\n       --coa='Miami: 25 46.50978 : -80 57.948611'"
   "\n       --cob='Buenos Aires: -34 36.20 : -58 22.90'"
   "\n    4) an integer degree value followed by an integer minutes value,"
   "\n       followed by a decimal seconds value."
   "\n       --coa='Miami: 25 46 30.5868 : -80 57 56.91666'"
   "\n       --cob='Buenos Aires: -34 36 12 : -58 22 54.0'"
   "\n  Note that positive degree values indicate North latitude or East longitude,"
   "\n  while negative values indicate South latitude or West longitude."
   "\n "
   "\n --file=FILENAME:LABEL_A:LABEL_B"
   "\n   Specify a file containing two or more sets of position coordinates,"
   "\n   and optionally, the labels indicating which records to read from the file."
   "\n    Example: --file='testpoints.txt:Miami, FL:Buenos Aires, AR'"
   "\n   The special labels, \"LABn\" and \"LABm\" may be used to indicate that"
   "\n   the 'nth' and 'mth' records should be selected."
   "\n    Example: --file='testpoints.txt:LAB1:LAB2'"
   "\n   Note that the specified filename may not contain a colon."
   "\n   This is a 'regular' plain-text file."
   "\n   The format of each record in the file follows the format for command"
   "\n   line options."
   "\n     Example: Miami, FL: 25 46 30.5868 : -80 57 56.91666"
   "\n   Blank lines and lines beginning with the hash character ('#'),"
   "\n   will be ignored."
   "\n "
   "\n --formula=[lambert | haversine | all] (default: lambert)"
   "\n   Select the formula to be used for calculation of the distance between"
   "\n   the two specified GPS coordinates. (at least three(3) characters)"
   "\n   The Lambert formula calculates the arc on an ellipsoid."
   "\n   The Haversine formula calculates the arc on a sphere."
   "\n   'all' displays the calculations for all supported formulae."
   "\n "
   "\n --convert=VALUE"
   "\n   Convert one(1) latitude or longitude value from the given format to the"
   "\n   alternate supported formats and display the results."
   "\n   This option is used primarily for debugging purposes; however, it may be"
   "\n   useful for experimenting with the various GPS coordinate value formats."
   "\n     Examples: --convert='-80 57 56.91666'    --convert=1.2579R"
   "\n "
   "\n --color=[FGND [:BGND [:BORDER_FGND [:BORDER_BGND]]]]"
   "\n   Specify text color attribute for final report; and if report is to be"
   "\n   displayed in a window, optionally specify window border attributes."
   "\n     Colors : [black|red|green|brown|blue|magenta|cyan|grey|dflt]"
   "\n     Example: --color='blue:grey'    --color='green:brown:dflt:blue'"
   "\n     Default: terminal default attributes."
   "\n "
   "\n --window=[ROW:COL]"
   "\n   Specify that the GPS arc distance be reported in a window rather than"
   "\n   the default TTY output. Optionally, specify position of the window"
   "\n   as row:column offsets. Default position: 1:4"
   "\n   A row value or column value of zero indicates that window is to be"
   "\n   centered in the terminal window for that dimension."
   "\n     Example: --window    --window=2:24    --window='1:0'"
   "\n "
   "\n --dump=FILESPEC"
   "\n   Read, decode and report validity of all GPS coordinate records in"
   "\n   the specified file. Useful when creating a file containing a list"
   "\n   of GPS coordinates. (see example file: US_Capitols.txt)"
   "\n "
   "\n --verbose (-v)"
   "\n   Display verbose results of the distance calculation."
   "\n "
   "\n --help (-h) Display application help, then exit."
   "\n --helpless (-hl) Display application help using the 'less' utility."
   "\n --version[a]   Display application version and copyright information."
   "\n " ;  // End Help1

   gString gs ;
   this->ComposeTitle ( gs ) ;
   bool write2stdout = true ;

   //* If specified, pipe the Help text to the 'less' utility.*
   if ( userOption == clHelpless )
   {
      FILE *output = popen ( "less -c", "w" ) ;
      if ( output != NULL )
      {
         fprintf ( output, gs.ustr() ) ;
         fprintf ( output, Help1 ) ;
         fprintf ( output, HelpTerm ) ;
         #if DEBUG_ANSICMD != 0
         fprintf ( output, HelpAnsi ) ;
         #endif   // DEBUG_ANSICMD
         pclose ( output ) ;
         write2stdout = false ;
      }
      //* If popen() fails, write to stdout instead.*
   }

   //* Write Help text to stdout *
   if ( write2stdout != false )
   {
      this->ansi->ttyWrite ( gs, false ) ;
      this->ansi->ttyWrite ( Help1, false ) ;
      this->ansi->ttyWrite ( HelpTerm, false ) ;
      #if DEBUG_ANSICMD != 0
      this->ansi->ttyWrite ( HelpAnsi, false ) ;
      #endif   // DEBUG_ANSICMD
      this->ansi->ttyWrite ( '\n' ) ;
   }

}  //* End DisplayHelp() *



// REFERENCE ONLY --- REFERENCE ONLY --- REFERENCE ONLY --- REFERENCE ONLY --- 
#if 0    // REFERENCE ONLY
// C++ program to calculate Distance
// Between Two Points on Earth
//#include <bits/stdc++.h>
//using namespace std;
 
// Utility function for
// converting degrees to radians
long double toRadians(const long double degree)
{
    // cmath library in C++
    // defines the constant
    // M_PI as the value of
    // pi accurate to 1e-30
    long double one_deg = (M_PI) / 180;
    return (one_deg * degree);
}
 
long double distance(long double lat1, long double long1,
                     long double lat2, long double long2)
{
    // Convert the latitudes
    // and longitudes
    // from degree to radians.
    lat1 = toRadians(lat1);
    long1 = toRadians(long1);
    lat2 = toRadians(lat2);
    long2 = toRadians(long2);
     
    // Haversine Formula
    long double dlong = long2 - long1;
    long double dlat = lat2 - lat1;

    long double ans = pow(sin(dlat / 2), 2) +
                          cos(lat1) * cos(lat2) *
                          pow(sin(dlong / 2), 2);

    ans = 2 * asin(sqrt(ans));

    // Radius of Earth in
    // Kilometers, R = 6371
    // Use R = 3956 for miles
    long double R = 6371;
     
    // Calculate the result
    ans = ans * R;
 
    return ans;
}
 
// Driver Code
int main()
{
    long double lat1 = 53.32055555555556;
    long double long1 = -1.7297222222222221;
    long double lat2 = 53.31861111111111;
    long double long2 = -1.6997222222222223;
     
    // call the distance function
    cout << setprecision(15) << fixed;
    cout << distance(lat1, long1,
                     lat2, long2) << " K.M";
 
    return 0;
}
 
// This code is contributed
// by Aayush Chaturvedi

=====================================

Python haversine calculation:
-----------------------------

//'''
Calculate distance using the Haversine Formula
//'''

def haversine(coord1: object, coord2: object):
    import math

    # Coordinates in decimal degrees (e.g. 2.89078, 12.79797)
    lon1, lat1 = coord1
    lon2, lat2 = coord2

    R = 6371000  # radius of Earth in meters
    phi_1 = math.radians(lat1)
    phi_2 = math.radians(lat2)

    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    a = math.sin(delta_phi / 2.0) ** 2 + math.cos(phi_1) * math.cos(phi_2) * math.sin(delta_lambda / 2.0) ** 2
    
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    meters = R * c  # output distance in meters
    km = meters / 1000.0  # output distance in kilometers

    meters = round(meters, 3)
    km = round(km, 3)

    print(f"Distance: {meters} m")
    print(f"Distance: {km} km")‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

#endif   // REFERENCE ONLY

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

