Tools & Software

Comprehensive tooling reference for navigation computations, from CLI utilities to military-grade survey equipment.

CLI Tools

PROJ Library

The PROJ library (formerly PROJ.4) is the cartographic projections library used by virtually all GIS software.

Installation
# Arch Linux
sudo pacman -S proj

# Ubuntu/Debian
sudo apt install proj-bin proj-data

# macOS
brew install proj

# Verify installation
proj -V
cs2cs -V
geod -V
Table 1. Core PROJ Components
Tool Function Key Use

proj

Forward projection

Geographic → projected

invproj

Inverse projection

Projected → geographic

cs2cs

Coordinate system to coordinate system

Datum transformations

geod

Geodesic calculations

Distance, bearing, inverse

projinfo

CRS information

EPSG lookup, WKT output

cs2cs Transformations

The primary tool for coordinate transformations.

PROJ String Syntax
+proj=PROJECTION  +datum=DATUM  +zone=ZONE  +ellps=ELLIPSOID
+towgs84=dx,dy,dz,rx,ry,rz,s  (7-parameter transformation)
Geographic ↔ UTM
# Geographic to UTM Zone 11N (Los Angeles)
echo "-118.4081 33.9425" | cs2cs \
    +proj=longlat +datum=WGS84 \
    +to +proj=utm +zone=11 +datum=WGS84
# Output: 368916.29   3757142.35 0.00

# UTM to Geographic (output in DMS)
echo "368916 3757142" | cs2cs \
    +proj=utm +zone=11 +datum=WGS84 \
    +to +proj=longlat +datum=WGS84 -f "%.6f"
# Output: -118.408102   33.942503 0.000000

# UTM Zone 38N (Iraq/Afghanistan operations area)
echo "44.3661 33.3128" | cs2cs \
    +proj=longlat +datum=WGS84 \
    +to +proj=utm +zone=38 +datum=WGS84
# Output: 500000.xx   3686797.xx
Datum Transformations
# NAD27 to WGS84 (CONUS)
echo "-98.23 26.20" | cs2cs \
    +proj=longlat +datum=NAD27 \
    +to +proj=longlat +datum=WGS84 -f "%.8f"

# ED50 to WGS84 (Europe)
echo "13.4049 52.5200" | cs2cs \
    +proj=longlat +datum=ED50 \
    +to +proj=longlat +datum=WGS84 -f "%.8f"

# Tokyo datum to WGS84 (Japan)
echo "139.6917 35.6895" | cs2cs \
    +proj=longlat +ellps=bessel +towgs84=-148,507,685,0,0,0,0 \
    +to +proj=longlat +datum=WGS84 -f "%.8f"
State Plane Coordinates
# Geographic to Texas South Central (EPSG:2278, feet)
echo "-98.2300 26.2034" | cs2cs \
    +proj=longlat +datum=WGS84 \
    +to +init=epsg:2278 -f "%.2f"

# List available EPSG codes
projinfo EPSG:2278

# California Zone 5 (meters)
echo "-118.25 34.05" | cs2cs \
    +proj=longlat +datum=WGS84 \
    +to +init=epsg:2229 -f "%.3f"

geod - Geodesic Calculator

The geod tool performs ellipsoidal geodesic calculations.

Forward Geodesic (Position from bearing/distance)
# Given: Start point, bearing, distance
# Find: End point, back-azimuth

# Start at Los Angeles, bearing 066°, distance 3970 km
echo "33.9425 -118.4081 66 3970" | geod +ellps=WGS84 +units=km -f "%.6f"
# Output: lat lon back-azimuth

# Military convoy movement: 10 km bearing 045°
echo "33.3128 44.3661 45 10" | geod +ellps=WGS84 +units=km -f "%.6f"
Inverse Geodesic (Distance/bearing between points)
# Given: Two points
# Find: Distance, forward azimuth, back azimuth

# Los Angeles to JFK
geod +ellps=WGS84 -I +units=km <<< "33.9425 -118.4081 40.6413 -73.7781"
# Output: 66°xx'xx.xx" 250°xx'xx.xx" 3970.xxx

# Baghdad to Kabul
geod +ellps=WGS84 -I +units=km <<< "33.3128 44.3661 34.5553 69.2075"
# Output: bearing1 bearing2 distance

# Multiple points (batch processing)
cat << 'EOF' | geod +ellps=WGS84 -I +units=km
33.9425 -118.4081 40.6413 -73.7781
34.0522 -118.2437 51.5074 -0.1278
40.7128 -74.0060 48.8566 2.3522
EOF
Geodesic Line Points
# Generate waypoints along great circle route
# LA to JFK, 10 intermediate points
echo "33.9425 -118.4081 40.6413 -73.7781" | \
    geod +ellps=WGS84 -I +units=km -G 10 -f "%.6f"

# Create KML-compatible waypoint list
geod +ellps=WGS84 -I +units=km -G 20 <<< "33.9425 -118.4081 40.6413 -73.7781" | \
    awk 'NR>1 {printf "%.6f,%.6f,0\n", $2, $1}'

proj - Direct Projection Operations

Projection Examples
# Mercator projection
echo "-98.23 26.20" | proj +proj=merc +ellps=WGS84

# Transverse Mercator (UTM without zone)
echo "-98.23 26.20" | proj +proj=tmerc +lon_0=-99 +lat_0=0 +k=0.9996 +ellps=WGS84

# Lambert Conformal Conic (aviation charts)
echo "-98.23 26.20" | proj +proj=lcc +lat_1=33 +lat_2=45 +lat_0=39 +lon_0=-96 +ellps=WGS84

# Stereographic (polar regions)
echo "0 90" | proj +proj=stere +lat_0=90 +lon_0=0 +ellps=WGS84

PROJ Batch Processing

# Convert file of coordinates
cs2cs +proj=longlat +datum=WGS84 +to +proj=utm +zone=11 +datum=WGS84 \
    < input_latlon.txt > output_utm.txt

# AWK integration for formatted output
cat coordinates.txt | while read lon lat; do
    echo "$lon $lat" | cs2cs +proj=longlat +datum=WGS84 \
        +to +proj=utm +zone=11 +datum=WGS84 | \
        awk -v lon="$lon" -v lat="$lat" '{printf "%s %s -> %.2f E %.2f N\n", lat, lon, $1, $2}'
done

# Parallel processing for large datasets
parallel --pipe -N1000 "cs2cs +proj=longlat +datum=WGS84 +to +proj=utm +zone=11" \
    < massive_coords.txt > output.txt

GPS Receivers and NMEA Parsing

Military and civilian GPS receivers output data in NMEA 0183 format. Understanding this protocol is essential for integration with navigation systems.

NMEA Sentence Structure

NMEA 0183 Format
$TALKER_ID,SENTENCE_ID,DATA_FIELDS*CHECKSUM<CR><LF>
Table 2. Common Talker IDs
ID System Notes

GP

GPS (US)

Most common

GL

GLONASS (Russia)

24 satellites

GA

Galileo (EU)

30 satellites planned

BD/GB

BeiDou (China)

Regional + global

GN

Multi-GNSS

Combined solution

Essential NMEA Sentences

GGA - Global Positioning System Fix Data
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47
       ^^^^^^ ^^^^^^^^ ^ ^^^^^^^^^ ^ ^ ^^ ^^^ ^^^^^ ^ ^^^^
       Time   Lat      N Lon       E Q #S HD  Alt   M Geoid
Table 3. GGA Field Breakdown
Field Name Description

123519

UTC Time

12:35:19 UTC (HHMMSS)

4807.038,N

Latitude

48°07.038' North (DDMM.MMM)

01131.000,E

Longitude

011°31.000' East (DDDMM.MMM)

1

Quality

0=invalid, 1=GPS, 2=DGPS, 4=RTK, 5=Float RTK

08

Satellites

Number of satellites in use

0.9

HDOP

Horizontal Dilution of Precision

545.4,M

Altitude

545.4 meters above MSL

47.0,M

Geoid Height

Geoid separation from WGS84 ellipsoid

RMC - Recommended Minimum Navigation Data
$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
       ^^^^^^ ^ ^^^^^^^^ ^ ^^^^^^^^^ ^ ^^^^^ ^^^^^ ^^^^^^ ^^^^^
       Time   V Lat      N Lon       E Knots Track Date   MagVar
Table 4. RMC Fields
Field Name Description

A

Status

A=Active (valid), V=Void (warning)

022.4

Speed

Speed over ground (knots)

084.4

Track

Track made good (degrees true)

230394

Date

DDMMYY (23 March 1994)

003.1,W

Magnetic Variation

3.1° West

GSA - Satellite Status and DOP
$GPGSA,A,3,04,05,09,12,15,17,18,21,22,24,,,1.5,0.9,1.2*33
       ^ ^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^
       M F PRN numbers of satellites used   PDO HDO VDO

NMEA Parsing with CLI Tools

Extract Position from GGA
# Parse GGA sentence to decimal degrees
echo '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47' | \
awk -F',' '{
    # Latitude: DDMM.MMM to DD.DDDDDD
    lat_deg = int($3 / 100)
    lat_min = $3 - (lat_deg * 100)
    lat = lat_deg + (lat_min / 60)
    if ($4 == "S") lat = -lat

    # Longitude: DDDMM.MMM to DD.DDDDDD
    lon_deg = int($5 / 100)
    lon_min = $5 - (lon_deg * 100)
    lon = lon_deg + (lon_min / 60)
    if ($6 == "W") lon = -lon

    # Extract other fields
    quality = $7
    sats = $8
    hdop = $9
    alt = $10

    printf "Position: %.6f, %.6f\n", lat, lon
    printf "Altitude: %s m MSL\n", alt
    printf "Quality: %d, Satellites: %s, HDOP: %s\n", quality, sats, hdop
}'
NMEA Checksum Verification
# Verify NMEA checksum
nmea_verify() {
    local sentence="${1#\$}"          # Remove leading $
    sentence="${sentence%%\**}"       # Remove *XX checksum
    local given_cs="${1##*\*}"        # Extract given checksum

    local calc_cs=0
    for ((i=0; i<${#sentence}; i++)); do
        char="${sentence:$i:1}"
        calc_cs=$((calc_cs ^ $(printf '%d' "'$char")))
    done

    calc_cs=$(printf '%02X' $calc_cs)

    if [[ "$calc_cs" == "${given_cs^^}" ]]; then
        echo "Valid: $calc_cs"
        return 0
    else
        echo "Invalid: expected $calc_cs, got ${given_cs^^}"
        return 1
    fi
}

# Test
nmea_verify '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,47.0,M,,*47'
Real-Time GPS Monitoring
# Monitor GPS receiver on serial port
stty -F /dev/ttyUSB0 4800 raw
cat /dev/ttyUSB0 | while read line; do
    case "$line" in
        '$GPGGA'*|'$GNGGA'*)
            echo "$line" | awk -F',' '{printf "Fix: %.4f,%.4f Alt:%.1fm HDOP:%.1f Sats:%s\n", \
                int($3/100)+($3-int($3/100)*100)/60, \
                -(int($5/100)+($5-int($5/100)*100)/60), \
                $10, $9, $8}'
            ;;
        '$GPRMC'*|'$GNRMC'*)
            echo "$line" | awk -F',' '{printf "Speed: %.1fkn Track: %.1f° Status: %s\n", \
                $8, $9, $3}'
            ;;
    esac
done

gpsd - GPS Daemon

Installation and Setup
# Install gpsd
sudo pacman -S gpsd       # Arch
sudo apt install gpsd     # Ubuntu

# Start gpsd with USB GPS
sudo gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock

# Test with command-line tools
gpspipe -r                # Raw NMEA sentences
gpsmon                    # ncurses monitor
cgps                      # Simple client

# JSON output
gpspipe -w | jq 'select(.class=="TPV")'
gpsd JSON Data Structure
{
    "class": "TPV",
    "device": "/dev/ttyUSB0",
    "mode": 3,
    "time": "2026-03-26T18:45:23.000Z",
    "lat": 33.942500,
    "lon": -118.408100,
    "altHAE": 87.340,
    "altMSL": 52.100,
    "speed": 0.012,
    "track": 245.67,
    "climb": 0.000,
    "eps": 12.54,
    "epx": 5.123,
    "epy": 4.897,
    "epv": 15.234
}
Table 5. Error Estimates (ep* fields)
Field Meaning Unit

epx

Longitude error estimate (1σ)

meters

epy

Latitude error estimate (1σ)

meters

epv

Vertical error estimate (1σ)

meters

eps

Speed error estimate (1σ)

m/s

ept

Time error estimate (1σ)

seconds

timedatectl

System timezone management.

# List available timezones
timedatectl list-timezones | grep America

# Show current settings
timedatectl status

# Set timezone
sudo timedatectl set-timezone America/Chicago

date with TZ

# Current time in different zones
TZ=UTC date
TZ=America/Los_Angeles date
TZ=America/Chicago date
TZ=Asia/Tokyo date

# Convert between zones
TZ=America/New_York date -d "TZ=\"UTC\" 2026-03-14 18:00"

Python Libraries

Essential Packages

pip install geopy pyproj mgrs utm geomag

geopy - Geocoding and Distance

from geopy.distance import geodesic, great_circle
from geopy.geocoders import Nominatim

# Distance calculation
la = (33.9425, -118.4081)
jfk = (40.6413, -73.7781)

print(f"Geodesic: {geodesic(la, jfk).km:.2f} km")
print(f"Great circle: {great_circle(la, jfk).km:.2f} km")

# Geocoding
geolocator = Nominatim(user_agent="nav-study")
location = geolocator.geocode("McAllen, Texas")
print(f"McAllen: {location.latitude}, {location.longitude}")

pyproj - Coordinate Transformations

from pyproj import CRS, Transformer

# Define coordinate systems
wgs84 = CRS.from_epsg(4326)      # WGS84 Geographic
utm11n = CRS.from_epsg(32611)    # UTM Zone 11N
nad27 = CRS.from_epsg(4267)      # NAD27

# Transform coordinates
to_utm = Transformer.from_crs(wgs84, utm11n, always_xy=True)
lon, lat = -118.4081, 33.9425
x, y = to_utm.transform(lon, lat)
print(f"UTM 11N: {x:.2f} E, {y:.2f} N")

# Datum transformation
nad27_to_wgs84 = Transformer.from_crs(nad27, wgs84, always_xy=True)

mgrs - Military Grid Reference

import mgrs

m = mgrs.MGRS()

# Geographic to MGRS
lat, lon = 33.9425, -118.4081
mgrs_10 = m.toMGRS(lat, lon, MGRSPrecision=5)  # 10-digit
mgrs_8 = m.toMGRS(lat, lon, MGRSPrecision=4)   # 8-digit
mgrs_6 = m.toMGRS(lat, lon, MGRSPrecision=3)   # 6-digit

print(f"MGRS 10-digit: {mgrs_10}")
print(f"MGRS 8-digit:  {mgrs_8}")
print(f"MGRS 6-digit:  {mgrs_6}")

# MGRS to Geographic
lat, lon = m.toLatLon("11SLA6891657142")

utm - UTM Conversions

import utm

# Geographic to UTM
lat, lon = 33.9425, -118.4081
easting, northing, zone_number, zone_letter = utm.from_latlon(lat, lon)
print(f"{zone_number}{zone_letter} {easting:.0f}mE {northing:.0f}mN")

# UTM to Geographic
lat, lon = utm.to_latlon(368916, 3757142, 11, 'S')

geomag - Magnetic Declination

import geomag
from datetime import date

# Get declination for a location
lat, lon = 26.2034, -98.2300  # McAllen, TX
declination = geomag.declination(lat, lon)

print(f"Declination at McAllen: {declination:.2f}°")
print(f"Direction: {'East' if declination > 0 else 'West'}")

# Get declination for specific date
dec_2026 = geomag.declination(lat, lon, date(2026, 3, 14))

Vincenty Algorithm Implementation

The Vincenty algorithm provides sub-millimeter accuracy for geodesic calculations on the ellipsoid.

Complete Vincenty Implementation
"""
Vincenty's Formulae for Geodesic Calculations
Accuracy: ~0.5mm on the WGS84 ellipsoid
Reference: Vincenty, T. (1975). Direct and Inverse Solutions of Geodesics
           on the Ellipsoid with Application of Nested Equations.
           Survey Review, 23(176), 88-93.
"""

import math
from typing import Tuple, Optional
from dataclasses import dataclass

@dataclass
class WGS84:
    """WGS84 Ellipsoid Parameters"""
    a: float = 6378137.0              # Semi-major axis (m)
    b: float = 6356752.314245         # Semi-minor axis (m)
    f: float = 1 / 298.257223563      # Flattening

def vincenty_inverse(
    lat1: float, lon1: float,
    lat2: float, lon2: float,
    max_iterations: int = 200,
    tolerance: float = 1e-12
) -> Optional[Tuple[float, float, float]]:
    """
    Vincenty Inverse Problem: Given two points, find distance and azimuths.

    Args:
        lat1, lon1: First point (degrees)
        lat2, lon2: Second point (degrees)
        max_iterations: Maximum iterations for convergence
        tolerance: Convergence tolerance (radians)

    Returns:
        Tuple of (distance_m, azimuth1_deg, azimuth2_deg)
        or None if failed to converge (antipodal points)
    """
    e = WGS84()

    # Convert to radians
    φ1 = math.radians(lat1)
    φ2 = math.radians(lat2)
    L = math.radians(lon2 - lon1)

    # Reduced latitudes (parametric latitude)
    U1 = math.atan((1 - e.f) * math.tan(φ1))
    U2 = math.atan((1 - e.f) * math.tan(φ2))

    sin_U1 = math.sin(U1)
    cos_U1 = math.cos(U1)
    sin_U2 = math.sin(U2)
    cos_U2 = math.cos(U2)

    # Initial approximation
    λ = L

    for _ in range(max_iterations):
        sin_λ = math.sin(λ)
        cos_λ = math.cos(λ)

        sin_σ = math.sqrt(
            (cos_U2 * sin_λ) ** 2 +
            (cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_λ) ** 2
        )

        if sin_σ == 0:
            return (0.0, 0.0, 0.0)  # Coincident points

        cos_σ = sin_U1 * sin_U2 + cos_U1 * cos_U2 * cos_λ
        σ = math.atan2(sin_σ, cos_σ)

        sin_α = cos_U1 * cos_U2 * sin_λ / sin_σ
        cos²_α = 1 - sin_α ** 2

        if cos²_α == 0:
            cos_2σm = 0  # Equatorial line
        else:
            cos_2σm = cos_σ - 2 * sin_U1 * sin_U2 / cos²_α

        C = e.f / 16 * cos²_α * (4 + e.f * (4 - 3 * cos²_α))

        λ_prev = λ
        λ = L + (1 - C) * e.f * sin_α * (
            σ + C * sin_σ * (cos_2σm + C * cos_σ * (-1 + 2 * cos_2σm ** 2))
        )

        if abs(λ - λ_prev) < tolerance:
            break
    else:
        return None  # Failed to converge

    # Calculate distance
    u² = cos²_α * (e.a ** 2 - e.b ** 2) / e.b ** 2
    A = 1 + u² / 16384 * (4096 + u² * (-768 + u² * (320 - 175 * u²)))
    B = u² / 1024 * (256 + u² * (-128 + u² * (74 - 47 * u²)))

    Δσ = B * sin_σ * (
        cos_2σm + B / 4 * (
            cos_σ * (-1 + 2 * cos_2σm ** 2) -
            B / 6 * cos_2σm * (-3 + 4 * sin_σ ** 2) * (-3 + 4 * cos_2σm ** 2)
        )
    )

    s = e.b * A * (σ - Δσ)  # Distance in meters

    # Forward azimuth
    α1 = math.atan2(
        cos_U2 * sin_λ,
        cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_λ
    )

    # Back azimuth
    α2 = math.atan2(
        cos_U1 * sin_λ,
        -sin_U1 * cos_U2 + cos_U1 * sin_U2 * cos_λ
    )

    return (s, math.degrees(α1) % 360, (math.degrees(α2) + 180) % 360)


def vincenty_direct(
    lat1: float, lon1: float,
    azimuth: float, distance: float,
    max_iterations: int = 200,
    tolerance: float = 1e-12
) -> Tuple[float, float, float]:
    """
    Vincenty Direct Problem: Given start point, azimuth, and distance,
    find the destination point.

    Args:
        lat1, lon1: Start point (degrees)
        azimuth: Forward azimuth (degrees)
        distance: Distance to travel (meters)
        max_iterations: Maximum iterations
        tolerance: Convergence tolerance

    Returns:
        Tuple of (lat2, lon2, back_azimuth)
    """
    e = WGS84()

    φ1 = math.radians(lat1)
    α1 = math.radians(azimuth)
    s = distance

    # Reduced latitude
    U1 = math.atan((1 - e.f) * math.tan(φ1))

    sin_U1 = math.sin(U1)
    cos_U1 = math.cos(U1)
    sin_α1 = math.sin(α1)
    cos_α1 = math.cos(α1)

    # Azimuth of geodesic at equator
    σ1 = math.atan2(
        math.tan(U1),
        cos_α1
    )

    sin_α = cos_U1 * sin_α1
    cos²_α = 1 - sin_α ** 2

    u² = cos²_α * (e.a ** 2 - e.b ** 2) / e.b ** 2
    A = 1 + u² / 16384 * (4096 + u² * (-768 + u² * (320 - 175 * u²)))
    B = u² / 1024 * (256 + u² * (-128 + u² * (74 - 47 * u²)))

    # Initial approximation
    σ = s / (e.b * A)

    for _ in range(max_iterations):
        cos_2σm = math.cos(2 * σ1 + σ)
        sin_σ = math.sin(σ)
        cos_σ = math.cos(σ)

        Δσ = B * sin_σ * (
            cos_2σm + B / 4 * (
                cos_σ * (-1 + 2 * cos_2σm ** 2) -
                B / 6 * cos_2σm * (-3 + 4 * sin_σ ** 2) * (-3 + 4 * cos_2σm ** 2)
            )
        )

        σ_prev = σ
        σ = s / (e.b * A) + Δσ

        if abs(σ - σ_prev) < tolerance:
            break

    sin_σ = math.sin(σ)
    cos_σ = math.cos(σ)
    cos_2σm = math.cos(2 * σ1 + σ)

    # Final latitude
    φ2 = math.atan2(
        sin_U1 * cos_σ + cos_U1 * sin_σ * cos_α1,
        (1 - e.f) * math.sqrt(
            sin_α ** 2 + (sin_U1 * sin_σ - cos_U1 * cos_σ * cos_α1) ** 2
        )
    )

    # Longitude difference
    λ = math.atan2(
        sin_σ * sin_α1,
        cos_U1 * cos_σ - sin_U1 * sin_σ * cos_α1
    )

    C = e.f / 16 * cos²_α * (4 + e.f * (4 - 3 * cos²_α))
    L = λ - (1 - C) * e.f * sin_α * (
        σ + C * sin_σ * (cos_2σm + C * cos_σ * (-1 + 2 * cos_2σm ** 2))
    )

    lon2 = math.degrees(math.radians(lon1) + L)

    # Back azimuth
    α2 = math.atan2(
        sin_α,
        -sin_U1 * sin_σ + cos_U1 * cos_σ * cos_α1
    )

    return (math.degrees(φ2), lon2, (math.degrees(α2) + 180) % 360)


# Example usage
if __name__ == "__main__":
    # Los Angeles to JFK
    la = (33.9425, -118.4081)
    jfk = (40.6413, -73.7781)

    result = vincenty_inverse(*la, *jfk)
    if result:
        dist, az1, az2 = result
        print(f"Distance: {dist/1000:.3f} km")
        print(f"Forward azimuth: {az1:.4f}°")
        print(f"Back azimuth: {az2:.4f}°")

    # Direct: 100 km bearing 045° from LA
    lat2, lon2, back_az = vincenty_direct(33.9425, -118.4081, 45, 100000)
    print(f"\nDestination: {lat2:.6f}, {lon2:.6f}")
    print(f"Back azimuth: {back_az:.4f}°")
Vincenty Error Analysis
\[\sigma_s = \sqrt{\sigma_{\phi_1}^2 + \sigma_{\phi_2}^2 + \sigma_{\lambda_1}^2 + \sigma_{\lambda_2}^2} \cdot \frac{\partial s}{\partial \text{inputs}}\]

For typical GPS input uncertainty of \(\pm 5\) meters, the Vincenty distance error is approximately:

\[\sigma_s \approx \sqrt{2} \cdot 5 \text{ m} \approx 7.07 \text{ m}\]

MGRS Batch Operations

Batch Coordinate Conversion Script
#!/usr/bin/env python3
"""
MGRS Batch Conversion Utility
For processing large coordinate datasets.
"""

import mgrs
import csv
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import Iterator, Tuple

m = mgrs.MGRS()

def convert_to_mgrs(lat: float, lon: float, precision: int = 5) -> str:
    """Convert lat/lon to MGRS with specified precision."""
    try:
        return m.toMGRS(lat, lon, MGRSPrecision=precision)
    except Exception as e:
        return f"ERROR: {e}"

def convert_from_mgrs(mgrs_str: str) -> Tuple[float, float]:
    """Convert MGRS to lat/lon."""
    try:
        lat, lon = m.toLatLon(mgrs_str)
        return (lat, lon)
    except Exception as e:
        return (None, None)

def batch_convert(
    input_file: str,
    output_file: str,
    direction: str = 'to_mgrs',
    precision: int = 5,
    workers: int = 4
) -> None:
    """
    Batch convert coordinates.

    Args:
        input_file: CSV with lat,lon or mgrs
        output_file: Output CSV
        direction: 'to_mgrs' or 'from_mgrs'
        precision: MGRS precision (1-5)
        workers: Parallel workers
    """
    with open(input_file, 'r') as infile, \
         open(output_file, 'w', newline='') as outfile:

        reader = csv.reader(infile)
        writer = csv.writer(outfile)

        if direction == 'to_mgrs':
            writer.writerow(['lat', 'lon', 'mgrs'])
            for row in reader:
                lat, lon = float(row[0]), float(row[1])
                result = convert_to_mgrs(lat, lon, precision)
                writer.writerow([lat, lon, result])
        else:
            writer.writerow(['mgrs', 'lat', 'lon'])
            for row in reader:
                mgrs_str = row[0].strip()
                lat, lon = convert_from_mgrs(mgrs_str)
                writer.writerow([mgrs_str, lat, lon])

if __name__ == "__main__":
    # Example: Convert list of coordinates
    coords = [
        (33.9425, -118.4081),   # Los Angeles
        (40.7128, -74.0060),    # New York
        (51.5074, -0.1278),     # London
        (33.3128, 44.3661),     # Baghdad
        (34.5553, 69.2075),     # Kabul
    ]

    print("Coordinate to MGRS Conversion:")
    print("-" * 60)
    for lat, lon in coords:
        mgrs_10 = convert_to_mgrs(lat, lon, 5)
        mgrs_6 = convert_to_mgrs(lat, lon, 3)
        print(f"{lat:9.4f}, {lon:10.4f} → {mgrs_10} ({mgrs_6})")

Military Survey Equipment

Military operations require specialized survey equipment that provides higher accuracy and operates in denied environments. Understanding these systems is essential for military navigators.

GPS Navigation Sets

AN/PSN-11 PLGR (Precision Lightweight GPS Receiver)

The "Plugger" was the standard military GPS receiver from 1994-2010.

Table 6. PLGR Specifications
Parameter Value

Channels

5 (L1 C/A + P(Y))

Accuracy (SPS)

16m SEP (95%)

Accuracy (PPS)

5.3m SEP (95%)

TTFF (Cold)

< 2.5 minutes

TTFF (Hot)

< 1 minute

Weight

1.25 kg

Operating Temp

-40°C to +60°C

PLGR Position Uncertainty

The PLGR displays position as a confidence circle. The relationship between CEP, SEP, and 2DRMS:

\[\text{SEP} = \sqrt{\sigma_E^2 + \sigma_N^2 + \sigma_h^2}\]
\[\text{CEP} = 0.589 \cdot (\sigma_E + \sigma_N) \quad \text{(circular assumption)}\]
\[\text{2DRMS} = 2 \cdot \sqrt{\sigma_E^2 + \sigma_N^2}\]

AN/PSN-13 DAGR (Defense Advanced GPS Receiver)

Current standard handheld military GPS.

Table 7. DAGR Specifications
Parameter Value

Channels

12 (L1/L2 P(Y))

Accuracy (PPS)

< 3.4m SEP (95%)

Selective Availability Anti-Spoofing Module (SAASM)

Yes (crypto-protected)

TTFF (Cold)

< 90 seconds

Weight

0.45 kg

Battery Life

14-16 hours (AA lithium)

Display

Sunlight readable LCD

Datum Storage

200+ (including WGS84, ED50, Tokyo)

Table 8. DAGR Operational Modes
Mode Description

COARSE/ACQUISITION (C/A)

Civilian signal only, reduced accuracy

PRECISE (P/Y)

Encrypted military signal, full accuracy

DGPS

Differential corrections via radio

SAASM

Selective Availability Anti-Spoofing Module

AN/PVS-501 TALIN (Tactical Advanced Land Inertial Navigator)

Inertial navigation system for GPS-denied environments.

TALIN Operation
                    ┌─────────────────┐
                    │   TALIN IMU     │
                    │ (Ring Laser     │
                    │  Gyroscopes)    │
                    └────────┬────────┘
                             │ Angular rates, accelerations
                             ▼
                    ┌─────────────────┐
                    │  Navigation     │
                    │  Computer       │
                    │  (Kalman Filter)│
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
        ▼                    ▼                    ▼
   ┌─────────┐        ┌─────────────┐      ┌───────────┐
   │Position │        │  Velocity   │      │  Heading  │
   │ (MGRS)  │        │ (m/s, mils) │      │  (mils)   │
   └─────────┘        └─────────────┘      └───────────┘
INS Error Growth

Inertial navigation systems accumulate error over time:

\[\epsilon(t) = \epsilon_0 + \epsilon_g \cdot t + \frac{1}{2} \epsilon_a \cdot t^2\]

Where: - \(\epsilon_0\) = Initial alignment error - \(\epsilon_g\) = Gyro drift rate (typically 0.01°/hour for tactical grade) - \(\epsilon_a\) = Accelerometer bias contribution

Table 9. Typical TALIN Performance
Parameter Short-Term (5 min) Long-Term (1 hour)

Position Error

< 20m

< 400m

Velocity Error

< 0.3 m/s

< 1.5 m/s

Heading Error

< 2 mils

< 5 mils

Survey Grade Equipment

Total Station (Electronic Theodolite + EDM)

Table 10. Accuracy Specifications (Typical Survey Grade)
Parameter Value

Angular Accuracy

±1" (arc-second)

Distance Accuracy

±(2mm + 2ppm)

Range (Prism)

Up to 5,000m

Range (Reflectorless)

500-1,200m

Angular to Linear Error

At distance \(D\), an angular error of \(\theta\) arc-seconds produces linear error:

\[e = D \cdot \tan\left(\frac{\theta}{3600} \cdot \frac{\pi}{180}\right) \approx D \cdot \frac{\theta}{206265}\]
Example Calculation

For 1" error at 1000m:

\[e = 1000 \cdot \frac{1}{206265} = 0.00485 \text{ m} = 4.85 \text{ mm}\]

RTK GPS (Real-Time Kinematic)

RTK System Components
    Base Station                              Rover
   (Known Point)                          (Unknown Point)
  ┌──────────────┐                       ┌──────────────┐
  │ GPS Receiver │                       │ GPS Receiver │
  │   (L1/L2)    │                       │   (L1/L2)    │
  └──────┬───────┘                       └──────┬───────┘
         │                                      │
         │    Carrier Phase                     │
         │    Corrections                       │
         ▼                                      ▼
  ┌──────────────┐    UHF Radio         ┌──────────────┐
  │  Radio Modem │◄───────────────────►│  Radio Modem │
  │   (450 MHz)  │    (1-10 km range)   │   (450 MHz)  │
  └──────────────┘                       └──────────────┘
Table 11. RTK Accuracy
Mode Horizontal Vertical

Float Solution

20-50 cm

30-80 cm

Fixed Solution

1-2 cm

2-4 cm

Post-Processing

< 1 cm

< 2 cm

Integer Ambiguity Resolution

RTK accuracy depends on resolving the carrier phase integer ambiguity:

\[\Phi = \rho + c \cdot (dt_r - dt_s) + \lambda \cdot N + I + T + \epsilon\]

Where: - \(\Phi\) = Observed carrier phase - \(\rho\) = Geometric range - \(c\) = Speed of light - \(\lambda\) = Carrier wavelength (L1: 19.0 cm, L2: 24.4 cm) - \(N\) = Integer ambiguity (unknown) - \(I\) = Ionospheric delay - \(T\) = Tropospheric delay

Fire Control Integration

Military fire control systems require precise coordinate inputs with specific formats and accuracy requirements.

AFATDS (Advanced Field Artillery Tactical Data System)

Table 12. Coordinate Input Requirements
Field Format

Grid Zone Designator

2 digits + letter (e.g., "38S")

100km Grid Square

2 letters (e.g., "LB")

Easting/Northing

5+5 digits (10m) or 6+6 digits (1m)

Altitude

Meters MSL

Target Description

Free text

Fire Mission Coordinate Example
GRID:      38S LB 12345 67890
ALT:       342M
TGT TYPE:  TROOPS IN OPEN
ATTITUDE:  LINEAR 450 MILS
Circular Error Probable (CEP) for Fire Control

The CEP is the radius within which 50% of rounds will fall:

\[\text{CEP} = 0.674 \cdot \sigma \quad \text{(for circular normal distribution)}\]

For elongated patterns (typical of artillery):

\[\text{CEP} \approx 0.615 \cdot \sqrt{\sigma_R^2 + \sigma_D^2}\]

Where \(\sigma_R\) = range error, \(\sigma_D\) = deflection error.

Verification and Validation

Coordinate Validation Scripts

Complete Coordinate Validator
#!/bin/bash
# coord_validate.sh - Validate coordinates in multiple formats

validate_decimal() {
    local lat="$1" lon="$2"

    # Check latitude range [-90, 90]
    if ! awk -v lat="$lat" 'BEGIN { exit !(lat >= -90 && lat <= 90) }'; then
        echo "ERROR: Latitude $lat out of range [-90, 90]"
        return 1
    fi

    # Check longitude range [-180, 180]
    if ! awk -v lon="$lon" 'BEGIN { exit !(lon >= -180 && lon <= 180) }'; then
        echo "ERROR: Longitude $lon out of range [-180, 180]"
        return 1
    fi

    echo "VALID: $lat, $lon"
    return 0
}

validate_utm() {
    local zone="$1" easting="$2" northing="$3"

    # Zone must be 1-60
    zone_num="${zone%[A-Z]}"
    zone_letter="${zone#$zone_num}"

    if [[ ! "$zone_num" =~ ^[1-9]$|^[1-5][0-9]$|^60$ ]]; then
        echo "ERROR: Zone number $zone_num out of range [1, 60]"
        return 1
    fi

    # Easting typically 100000-900000
    if ! awk -v e="$easting" 'BEGIN { exit !(e >= 100000 && e <= 900000) }'; then
        echo "WARNING: Easting $easting outside typical range [100000, 900000]"
    fi

    # Northing 0-10000000 (varies by hemisphere)
    if ! awk -v n="$northing" 'BEGIN { exit !(n >= 0 && n <= 10000000) }'; then
        echo "ERROR: Northing $northing out of range [0, 10000000]"
        return 1
    fi

    echo "VALID: $zone $easting mE $northing mN"
    return 0
}

validate_mgrs() {
    local mgrs="$1"

    # Basic format check
    # Format: ZONE_NUM ZONE_LETTER 100KM_SQUARE EASTING NORTHING
    # Examples: 11SLA6891657142 (10-digit), 11SLA68915714 (8-digit)

    if [[ ! "$mgrs" =~ ^[0-9]{1,2}[A-Z][A-Z]{2}[0-9]+$ ]]; then
        echo "ERROR: Invalid MGRS format: $mgrs"
        return 1
    fi

    # Extract components
    local zone_num zone_letter grid_square coords
    zone_num=$(echo "$mgrs" | grep -oP '^\d{1,2}')
    zone_letter=$(echo "$mgrs" | grep -oP '^\d{1,2}\K[A-Z]')
    grid_square=$(echo "$mgrs" | grep -oP '^\d{1,2}[A-Z]\K[A-Z]{2}')
    coords=$(echo "$mgrs" | grep -oP '^\d{1,2}[A-Z][A-Z]{2}\K\d+')

    # Coordinate digits must be even (equal easting/northing)
    if (( ${#coords} % 2 != 0 )); then
        echo "ERROR: MGRS must have even number of coordinate digits"
        return 1
    fi

    # Valid zone letters (C-X, excluding I and O)
    if [[ ! "$zone_letter" =~ ^[C-HJ-NP-X]$ ]]; then
        echo "ERROR: Invalid zone letter: $zone_letter"
        return 1
    fi

    echo "VALID: $zone_num$zone_letter $grid_square ${coords:0:${#coords}/2} ${coords:${#coords}/2}"
    return 0
}

validate_dms() {
    local dms="$1"

    # Expected format: DD°MM'SS.S"N/S or DDD°MM'SS.S"E/W
    if [[ "$dms" =~ ^([0-9]+)°([0-9]+)\'([0-9.]+)\"([NSEW])$ ]]; then
        local deg="${BASH_REMATCH[1]}"
        local min="${BASH_REMATCH[2]}"
        local sec="${BASH_REMATCH[3]}"
        local dir="${BASH_REMATCH[4]}"

        # Validate degrees
        local max_deg=90
        [[ "$dir" =~ [EW] ]] && max_deg=180

        if (( deg > max_deg )); then
            echo "ERROR: Degrees $deg exceed maximum $max_deg"
            return 1
        fi

        # Validate minutes and seconds
        if (( min >= 60 )); then
            echo "ERROR: Minutes $min must be < 60"
            return 1
        fi

        if ! awk -v sec="$sec" 'BEGIN { exit !(sec >= 0 && sec < 60) }'; then
            echo "ERROR: Seconds $sec must be < 60"
            return 1
        fi

        echo "VALID: $deg°$min'$sec\"$dir"
        return 0
    else
        echo "ERROR: Invalid DMS format: $dms"
        return 1
    fi
}

# Round-trip validation using PROJ
round_trip_validate() {
    local lat="$1" lon="$2"
    local tolerance="${3:-0.0001}"

    # Convert to UTM and back
    local zone=$((((${lon%.*} + 180) / 6) + 1))

    # Forward transformation
    local utm=$(echo "$lon $lat" | cs2cs +proj=longlat +datum=WGS84 \
        +to +proj=utm +zone=$zone +datum=WGS84 -f "%.3f")
    local easting=$(echo "$utm" | awk '{print $1}')
    local northing=$(echo "$utm" | awk '{print $2}')

    # Inverse transformation
    local result=$(echo "$easting $northing" | cs2cs \
        +proj=utm +zone=$zone +datum=WGS84 \
        +to +proj=longlat +datum=WGS84 -f "%.8f")
    local lon2=$(echo "$result" | awk '{print $1}')
    local lat2=$(echo "$result" | awk '{print $2}')

    # Compare
    local diff_lat=$(awk -v a="$lat" -v b="$lat2" 'BEGIN { printf "%.8f", (a-b) < 0 ? -(a-b) : (a-b) }')
    local diff_lon=$(awk -v a="$lon" -v b="$lon2" 'BEGIN { printf "%.8f", (a-b) < 0 ? -(a-b) : (a-b) }')

    if awk -v d="$diff_lat" -v t="$tolerance" 'BEGIN { exit !(d < t) }' && \
       awk -v d="$diff_lon" -v t="$tolerance" 'BEGIN { exit !(d < t) }'; then
        echo "ROUND-TRIP OK: Δlat=$diff_lat° Δlon=$diff_lon°"
        return 0
    else
        echo "ROUND-TRIP FAIL: Δlat=$diff_lat° Δlon=$diff_lon° (tolerance: $tolerance°)"
        return 1
    fi
}

# Main
case "$1" in
    decimal)  validate_decimal "$2" "$3" ;;
    utm)      validate_utm "$2" "$3" "$4" ;;
    mgrs)     validate_mgrs "$2" ;;
    dms)      validate_dms "$2" ;;
    roundtrip) round_trip_validate "$2" "$3" "$4" ;;
    *)
        echo "Usage: $0 {decimal|utm|mgrs|dms|roundtrip} <args>"
        echo "  decimal <lat> <lon>"
        echo "  utm <zone> <easting> <northing>"
        echo "  mgrs <mgrs_string>"
        echo "  dms <dms_string>"
        echo "  roundtrip <lat> <lon> [tolerance]"
        ;;
esac

Accuracy Assessment

CEP Calculator
#!/usr/bin/env python3
"""
Calculate Circular Error Probable (CEP) from a set of positions.
Used for GPS/GNSS accuracy assessment.
"""

import math
from typing import List, Tuple
import statistics

def calculate_cep(
    points: List[Tuple[float, float]],
    true_pos: Tuple[float, float] = None
) -> dict:
    """
    Calculate CEP statistics from measured positions.

    Args:
        points: List of (x, y) positions in meters
        true_pos: True position if known, otherwise uses mean

    Returns:
        Dictionary with CEP statistics
    """
    if true_pos is None:
        # Use mean as reference
        ref_x = statistics.mean(p[0] for p in points)
        ref_y = statistics.mean(p[1] for p in points)
    else:
        ref_x, ref_y = true_pos

    # Calculate radial errors
    radial_errors = []
    for x, y in points:
        error = math.sqrt((x - ref_x)**2 + (y - ref_y)**2)
        radial_errors.append(error)

    radial_errors.sort()
    n = len(radial_errors)

    # CEP = 50th percentile of radial errors
    cep_idx = int(n * 0.5)
    cep = radial_errors[cep_idx]

    # 95th percentile (2DRMS approximation)
    p95_idx = int(n * 0.95)
    p95 = radial_errors[p95_idx]

    # Calculate standard deviations
    errors_x = [p[0] - ref_x for p in points]
    errors_y = [p[1] - ref_y for p in points]

    sigma_x = statistics.stdev(errors_x) if len(errors_x) > 1 else 0
    sigma_y = statistics.stdev(errors_y) if len(errors_y) > 1 else 0

    # Theoretical CEP (assuming circular distribution)
    sigma_r = math.sqrt(sigma_x**2 + sigma_y**2)
    cep_theoretical = 0.674 * (sigma_x + sigma_y) / 2

    # 2DRMS
    drms_2 = 2 * math.sqrt(sigma_x**2 + sigma_y**2)

    return {
        'n_samples': n,
        'reference': (ref_x, ref_y),
        'sigma_x': sigma_x,
        'sigma_y': sigma_y,
        'sigma_r': sigma_r,
        'cep_measured': cep,
        'cep_theoretical': cep_theoretical,
        '2drms': drms_2,
        'p95': p95,
        'mean_error': statistics.mean(radial_errors),
        'max_error': max(radial_errors),
    }

# Example usage
if __name__ == "__main__":
    import random
    random.seed(42)

    # Simulate GPS measurements with 5m sigma
    true_position = (0, 0)
    measurements = [
        (random.gauss(0, 5), random.gauss(0, 5))
        for _ in range(1000)
    ]

    results = calculate_cep(measurements, true_position)

    print("CEP Analysis Results")
    print("=" * 40)
    print(f"Samples:       {results['n_samples']}")
    print(f"σ_x:           {results['sigma_x']:.2f} m")
    print(f"σ_y:           {results['sigma_y']:.2f} m")
    print(f"CEP (measured):{results['cep_measured']:.2f} m")
    print(f"CEP (theory):  {results['cep_theoretical']:.2f} m")
    print(f"2DRMS:         {results['2drms']:.2f} m")
    print(f"95%:           {results['p95']:.2f} m")
    print(f"Mean error:    {results['mean_error']:.2f} m")
    print(f"Max error:     {results['max_error']:.2f} m")

Your Navigation Library

Located in examples/navigation/:

coordinates.py

Full-featured Python coordinate library:

# Run examples
python examples/navigation/coordinates.py

Features:

  • DMS ↔ Decimal conversion

  • Geographic ↔ ECEF

  • Haversine distance

  • Vincenty distance (high precision)

  • Bearing calculations

  • Destination point

  • Declination application

nav-calc.sh

Bash functions for quick calculations:

source examples/navigation/nav-calc.sh

# Distance
haversine 33.9425 -118.4081 40.6413 -73.7781 km

# Bearing
bearing 33.9425 -118.4081 40.6413 -73.7781

# Conversions
dms_to_decimal 33 45 30 N
decimal_to_dms 33.758333 lat

# Declination
apply_declination 45 12 E

# Demo
nav_demo

mgrs-convert.py

MGRS/UTM conversion examples:

python examples/navigation/mgrs-convert.py

GIS Software

QGIS (Free, Open Source)

Full-featured GIS for desktop.

# Arch Linux
sudo pacman -S qgis

# Ubuntu
sudo apt install qgis qgis-plugin-grass

Key features:

  • Load military maps (GeoTIFF, MrSID)

  • Coordinate display in any CRS

  • Measure distances and bearings

  • Create routes and waypoints

Google Earth Pro (Free)

# Arch Linux (AUR)
yay -S google-earth-pro

# Or download from Google

Key features:

  • 3D terrain visualization

  • Path planning

  • KML/KMZ import/export

  • Historical imagery

Mobile Apps

Avenza Maps

  • Load offline geo-referenced maps

  • GPS tracking

  • Coordinate display (multiple formats)

  • Plot waypoints

Gaia GPS

  • Offline topo maps

  • Track recording

  • Waypoint management

  • UTM/MGRS grid overlay

GPS Test

  • Raw GPS data

  • Satellite visibility

  • Coordinates in multiple formats

  • Magnetic declination display

Online Tools

NOAA Magnetic Declination

  • Current and historical declination

  • Annual change

  • Worldwide coverage

Coordinates Converter

  • Multiple format conversion

  • Geocoding

  • Interactive map

NGA MGRS Info

  • Official MGRS documentation

  • Grid zone designator maps

Integration with dotfiles-optimus

Your CLI mastery libraries can integrate with navigation:

AWK for Coordinate Parsing

From ~/.local/share/awk/:

# Using network.awk concepts for coordinates
# Parse coordinates from various formats

echo "33°45'30\"N 118°24'29\"W" | awk '
{
    # Parse DMS format
    gsub(/[°'\''"NSEW]/, " ")
    split($0, parts)
    lat = parts[1] + parts[2]/60 + parts[3]/3600
    lon = parts[4] + parts[5]/60 + parts[6]/3600
    printf "%.6f, %.6f\n", lat, -lon
}'

Bash Library Extensions

Consider adding to ~/.local/share/bash/:

# ~/.local/share/bash/nav.sh
# Source this for navigation functions

source ~/.local/share/bash/math.sh  # For floating point

haversine() {
    # See examples/navigation/nav-calc.sh for implementation
    ...
}

Shell Aliases

# ~/.zshrc additions
alias nav='source ~/path/to/examples/navigation/nav-calc.sh && nav_demo'
alias declination='python -c "import geomag; print(geomag.declination($1, $2))"'

Quick Reference

Table 13. Tool Selection
Task CLI Python

Coordinate conversion

cs2cs (PROJ)

pyproj, utm

Distance calculation

nav-calc.sh

geopy, coordinates.py

MGRS

(use Python)

mgrs library

Declination

(use Python)

geomag

Timezone

timedatectl, date

pytz, zoneinfo

Installation Summary
# System packages
sudo pacman -S proj qgis

# Python packages
pip install geopy pyproj mgrs utm geomag

# Your libraries
source examples/navigation/nav-calc.sh

Exercises

Level 1: Tool Proficiency

Exercise 1.1: PROJ Mastery

Task: Using only cs2cs and geod, complete the following:

  1. Convert these coordinates to UTM Zone 38N (Iraq operations area):

    • Baghdad: 33.3128°N, 44.3661°E

    • Fallujah: 33.3535°N, 43.7834°E

    • Mosul: 36.3351°N, 43.1189°E

  2. Calculate the geodesic distance and bearing between each pair

  3. Generate 5 intermediate waypoints for a route from Baghdad to Mosul

Solution:

# 1. Convert to UTM Zone 38N
echo "44.3661 33.3128" | cs2cs +proj=longlat +datum=WGS84 +to +proj=utm +zone=38 +datum=WGS84
echo "43.7834 33.3535" | cs2cs +proj=longlat +datum=WGS84 +to +proj=utm +zone=38 +datum=WGS84
echo "43.1189 36.3351" | cs2cs +proj=longlat +datum=WGS84 +to +proj=utm +zone=38 +datum=WGS84

# 2. Inverse geodesic
geod +ellps=WGS84 -I +units=km <<< "33.3128 44.3661 33.3535 43.7834"
geod +ellps=WGS84 -I +units=km <<< "33.3128 44.3661 36.3351 43.1189"
geod +ellps=WGS84 -I +units=km <<< "33.3535 43.7834 36.3351 43.1189"

# 3. Waypoints Baghdad to Mosul
echo "33.3128 44.3661 36.3351 43.1189" | geod +ellps=WGS84 -I +units=km -G 5 -f "%.6f"
Exercise 1.2: NMEA Processing Pipeline

Task: Create a pipeline that:

  1. Reads NMEA sentences from a file

  2. Extracts GGA sentences

  3. Converts positions to decimal degrees

  4. Outputs CSV with timestamp, lat, lon, altitude, HDOP

Solution:

cat nmea_log.txt | \
grep '^\$G.GGA' | \
awk -F',' '{
    # Parse time
    time = substr($2, 1, 2) ":" substr($2, 3, 2) ":" substr($2, 5, 2)

    # Latitude
    lat_deg = int($3 / 100)
    lat_min = $3 - (lat_deg * 100)
    lat = lat_deg + (lat_min / 60)
    if ($4 == "S") lat = -lat

    # Longitude
    lon_deg = int($5 / 100)
    lon_min = $5 - (lon_deg * 100)
    lon = lon_deg + (lon_min / 60)
    if ($6 == "W") lon = -lon

    # Print CSV
    printf "%s,%.8f,%.8f,%s,%s\n", time, lat, lon, $10, $9
}'

Level 2: Integration Challenges

Exercise 2.1: Mission Planning Tool

Task: Create a bash script that:

  1. Takes a start MGRS, end MGRS, and number of waypoints as arguments

  2. Converts both endpoints to geographic coordinates

  3. Calculates geodesic distance and initial bearing

  4. Generates intermediate waypoints

  5. Converts each waypoint back to 10-digit MGRS

  6. Outputs a mission route table

Solution:

#!/bin/bash
# mission_route.sh - Generate waypoint route between MGRS coordinates

start_mgrs="$1"
end_mgrs="$2"
num_waypoints="${3:-5}"

# Convert MGRS to lat/lon using Python mgrs library
start_coords=$(python3 -c "
import mgrs
m = mgrs.MGRS()
lat, lon = m.toLatLon('$start_mgrs')
print(f'{lat} {lon}')
")

end_coords=$(python3 -c "
import mgrs
m = mgrs.MGRS()
lat, lon = m.toLatLon('$end_mgrs')
print(f'{lat} {lon}')
")

start_lat=$(echo "$start_coords" | awk '{print $1}')
start_lon=$(echo "$start_coords" | awk '{print $2}')
end_lat=$(echo "$end_coords" | awk '{print $1}')
end_lon=$(echo "$end_coords" | awk '{print $2}')

# Get distance and bearing
route_info=$(geod +ellps=WGS84 -I +units=km <<< "$start_lat $start_lon $end_lat $end_lon")
bearing=$(echo "$route_info" | awk '{print $1}')
distance=$(echo "$route_info" | awk '{print $3}')

echo "======================================"
echo "MISSION ROUTE PLANNING"
echo "======================================"
echo "Start:    $start_mgrs"
echo "End:      $end_mgrs"
echo "Distance: ${distance} km"
echo "Bearing:  $bearing"
echo "======================================"
printf "%-4s %-18s %-12s %-12s %s\n" "WPT" "MGRS" "LAT" "LON" "CUM_DIST"
echo "--------------------------------------"

# Generate waypoints
waypoint_num=0
echo "$start_lat $start_lon $end_lat $end_lon" | \
geod +ellps=WGS84 -I +units=km -G "$num_waypoints" -f "%.6f" | \
while read lat lon back_az; do
    if [[ -n "$lat" && -n "$lon" ]]; then
        mgrs_coord=$(python3 -c "
import mgrs
m = mgrs.MGRS()
print(m.toMGRS($lat, $lon, MGRSPrecision=5))
")
        cum_dist=$(awk -v n="$waypoint_num" -v d="$distance" -v w="$num_waypoints" \
            'BEGIN { printf "%.1f", d * n / (w + 1) }')
        printf "%-4s %-18s %-12.6f %-12.6f %s km\n" \
            "WP$waypoint_num" "$mgrs_coord" "$lat" "$lon" "$cum_dist"
        ((waypoint_num++))
    fi
done
Exercise 2.2: GPS Quality Monitor

Task: Create a monitoring tool that:

  1. Reads gpsd JSON output continuously

  2. Calculates running CEP statistics

  3. Alerts when HDOP exceeds threshold

  4. Logs all positions for post-analysis

Solution:

#!/bin/bash
# gps_quality_monitor.sh - Real-time GPS quality monitoring

HDOP_THRESHOLD="${1:-2.0}"
LOG_FILE="${2:-/tmp/gps_quality.log}"

echo "time,lat,lon,alt,hdop,satellites,mode" > "$LOG_FILE"

gpspipe -w | jq --unbuffered -r '
    select(.class == "TPV" and .mode >= 2) |
    [.time, .lat, .lon, .altMSL // "N/A", .hdop // "N/A", .sats // "N/A", .mode] |
    @csv
' | while IFS=, read -r time lat lon alt hdop sats mode; do
    # Remove quotes
    time="${time//\"/}"
    lat="${lat//\"/}"
    lon="${lon//\"/}"
    alt="${alt//\"/}"
    hdop="${hdop//\"/}"
    sats="${sats//\"/}"
    mode="${mode//\"/}"

    # Log to file
    echo "$time,$lat,$lon,$alt,$hdop,$sats,$mode" >> "$LOG_FILE"

    # Check HDOP threshold
    if [[ "$hdop" != "N/A" ]]; then
        if awk -v h="$hdop" -v t="$HDOP_THRESHOLD" 'BEGIN { exit !(h > t) }'; then
            echo "⚠️  HDOP WARNING: $hdop > $HDOP_THRESHOLD at $time"
        fi
    fi

    # Display status
    printf "\r%s | Lat: %10.6f | Lon: %11.6f | Alt: %6.1fm | HDOP: %4.1f | Mode: %d" \
        "$time" "$lat" "$lon" "$alt" "$hdop" "$mode"
done

Level 3: Military Applications

Exercise 3.1: Fire Mission Coordinate Converter

Task: Create a complete fire mission coordinate processing tool that:

  1. Accepts observer position (MGRS)

  2. Takes direction (mils) and distance (meters) to target

  3. Computes target location in MGRS

  4. Formats output for AFATDS entry

  5. Calculates grid-magnetic angle correction

Solution:

#!/usr/bin/env python3
"""
Fire Mission Coordinate Calculator
Computes target coordinates from observer position, direction, and distance.
"""

import mgrs
import math
from datetime import datetime
import geomag

def mils_to_degrees(mils: float) -> float:
    """Convert NATO mils to degrees."""
    return mils * (360 / 6400)

def degrees_to_mils(degrees: float) -> float:
    """Convert degrees to NATO mils."""
    return degrees * (6400 / 360)

def compute_target(
    observer_mgrs: str,
    direction_mils: float,
    distance_m: float,
    apply_gm_angle: bool = True
) -> dict:
    """
    Compute target position from observer location.

    Args:
        observer_mgrs: Observer position in MGRS
        direction_mils: Direction to target in mils (magnetic)
        distance_m: Distance to target in meters
        apply_gm_angle: Apply grid-magnetic angle correction

    Returns:
        Dictionary with target information
    """
    m = mgrs.MGRS()

    # Convert observer MGRS to lat/lon
    obs_lat, obs_lon = m.toLatLon(observer_mgrs)

    # Get magnetic declination
    declination = geomag.declination(obs_lat, obs_lon)

    # Compute grid convergence (simplified approximation)
    # True convergence requires UTM zone central meridian
    zone_num = int(observer_mgrs[:2]) if observer_mgrs[1].isdigit() else int(observer_mgrs[0])
    central_meridian = (zone_num - 1) * 6 - 180 + 3
    grid_convergence = math.degrees(
        math.atan(math.tan(math.radians(obs_lon - central_meridian)) *
                  math.sin(math.radians(obs_lat)))
    )

    # Grid-Magnetic angle
    gm_angle = declination - grid_convergence

    # Convert mils to degrees
    direction_deg = mils_to_degrees(direction_mils)

    # Apply GM angle correction if needed
    if apply_gm_angle:
        grid_direction = direction_deg + gm_angle
    else:
        grid_direction = direction_deg

    # Compute target using forward geodesic
    # Using simplified spherical formula (for tactical distances)
    R = 6371000  # Earth radius in meters
    d = distance_m

    obs_lat_rad = math.radians(obs_lat)
    obs_lon_rad = math.radians(obs_lon)
    bearing_rad = math.radians(grid_direction)

    tgt_lat_rad = math.asin(
        math.sin(obs_lat_rad) * math.cos(d/R) +
        math.cos(obs_lat_rad) * math.sin(d/R) * math.cos(bearing_rad)
    )

    tgt_lon_rad = obs_lon_rad + math.atan2(
        math.sin(bearing_rad) * math.sin(d/R) * math.cos(obs_lat_rad),
        math.cos(d/R) - math.sin(obs_lat_rad) * math.sin(tgt_lat_rad)
    )

    tgt_lat = math.degrees(tgt_lat_rad)
    tgt_lon = math.degrees(tgt_lon_rad)

    # Convert to MGRS
    tgt_mgrs = m.toMGRS(tgt_lat, tgt_lon, MGRSPrecision=5)

    return {
        'observer': observer_mgrs,
        'target': tgt_mgrs,
        'direction_mils': direction_mils,
        'distance_m': distance_m,
        'declination': declination,
        'grid_convergence': grid_convergence,
        'gm_angle': gm_angle,
        'gm_angle_mils': degrees_to_mils(gm_angle),
        'target_lat': tgt_lat,
        'target_lon': tgt_lon,
        'dtg': datetime.utcnow().strftime('%d%H%MZ %b %y').upper()
    }

def format_afatds(result: dict) -> str:
    """Format target for AFATDS entry."""
    output = []
    output.append("=" * 50)
    output.append("FIRE MISSION - TARGET DATA")
    output.append("=" * 50)
    output.append(f"DTG:           {result['dtg']}")
    output.append(f"OBSERVER:      {result['observer']}")
    output.append(f"OT DIRECTION:  {result['direction_mils']:.0f} MILS")
    output.append(f"OT DISTANCE:   {result['distance_m']:.0f}M")
    output.append("-" * 50)
    output.append(f"TARGET GRID:   {result['target']}")
    output.append(f"ALTITUDE:      [FILL]M")
    output.append("-" * 50)
    output.append(f"DECLINATION:   {result['declination']:.1f}° "
                  f"({degrees_to_mils(result['declination']):.0f} MILS)")
    output.append(f"GRID CONV:     {result['grid_convergence']:.1f}°")
    output.append(f"GM ANGLE:      {result['gm_angle']:.1f}° "
                  f"({result['gm_angle_mils']:.0f} MILS)")
    output.append("=" * 50)

    return "\n".join(output)

if __name__ == "__main__":
    # Example: Observer at 38S LB 12345 67890, target at 4800 mils, 3500m
    result = compute_target("38SLB1234567890", 4800, 3500)
    print(format_afatds(result))
Exercise 3.2: Multi-GNSS Position Comparison

Task: Analyze position accuracy from multiple GNSS constellations by:

  1. Processing GGA sentences from GPS, GLONASS, and Galileo

  2. Computing separate position solutions for each constellation

  3. Calculating inter-constellation position differences

  4. Producing a statistical comparison report

Solution:

#!/usr/bin/env python3
"""
Multi-GNSS Position Analyzer
Compare positions from different satellite constellations.
"""

import statistics
import math
from typing import List, Dict, Tuple
from collections import defaultdict
import re

def parse_nmea_position(sentence: str) -> Tuple[str, float, float, float, int]:
    """
    Parse GGA sentence and return constellation, lat, lon, hdop, sats.
    """
    parts = sentence.strip().split(',')
    if len(parts) < 10:
        return None

    # Determine constellation from talker ID
    talker = parts[0][1:3]
    constellation_map = {
        'GP': 'GPS',
        'GL': 'GLONASS',
        'GA': 'Galileo',
        'BD': 'BeiDou',
        'GB': 'BeiDou',
        'GN': 'Multi-GNSS'
    }
    constellation = constellation_map.get(talker, 'Unknown')

    # Parse latitude
    lat_raw = float(parts[2]) if parts[2] else 0
    lat_deg = int(lat_raw / 100)
    lat_min = lat_raw - (lat_deg * 100)
    lat = lat_deg + (lat_min / 60)
    if parts[3] == 'S':
        lat = -lat

    # Parse longitude
    lon_raw = float(parts[4]) if parts[4] else 0
    lon_deg = int(lon_raw / 100)
    lon_min = lon_raw - (lon_deg * 100)
    lon = lon_deg + (lon_min / 60)
    if parts[5] == 'W':
        lon = -lon

    # HDOP and satellite count
    hdop = float(parts[8]) if parts[8] else 99.9
    sats = int(parts[7]) if parts[7] else 0

    return (constellation, lat, lon, hdop, sats)

def analyze_gnss_data(nmea_lines: List[str]) -> Dict:
    """
    Analyze NMEA data and compare constellations.
    """
    positions = defaultdict(list)

    for line in nmea_lines:
        if 'GGA' in line:
            result = parse_nmea_position(line)
            if result and result[1] != 0:
                const, lat, lon, hdop, sats = result
                positions[const].append({
                    'lat': lat,
                    'lon': lon,
                    'hdop': hdop,
                    'sats': sats
                })

    results = {}
    for const, data in positions.items():
        if len(data) < 2:
            continue

        lats = [p['lat'] for p in data]
        lons = [p['lon'] for p in data]
        hdops = [p['hdop'] for p in data]

        mean_lat = statistics.mean(lats)
        mean_lon = statistics.mean(lons)
        std_lat = statistics.stdev(lats) * 111000  # Convert to meters
        std_lon = statistics.stdev(lons) * 111000 * math.cos(math.radians(mean_lat))

        results[const] = {
            'n_fixes': len(data),
            'mean_lat': mean_lat,
            'mean_lon': mean_lon,
            'std_lat_m': std_lat,
            'std_lon_m': std_lon,
            'std_2d': math.sqrt(std_lat**2 + std_lon**2),
            'mean_hdop': statistics.mean(hdops),
            'mean_sats': statistics.mean([p['sats'] for p in data])
        }

    # Compute inter-constellation differences
    constellations = list(results.keys())
    comparisons = {}
    for i, c1 in enumerate(constellations):
        for c2 in constellations[i+1:]:
            # Distance between mean positions
            dlat = (results[c1]['mean_lat'] - results[c2]['mean_lat']) * 111000
            dlon = (results[c1]['mean_lon'] - results[c2]['mean_lon']) * 111000 * \
                   math.cos(math.radians(results[c1]['mean_lat']))
            dist = math.sqrt(dlat**2 + dlon**2)
            comparisons[f"{c1} vs {c2}"] = dist

    return {
        'constellations': results,
        'comparisons': comparisons
    }

def print_gnss_report(analysis: Dict) -> None:
    """Print formatted GNSS comparison report."""
    print("=" * 70)
    print("MULTI-GNSS POSITION ANALYSIS REPORT")
    print("=" * 70)

    for const, data in analysis['constellations'].items():
        print(f"\n{const}")
        print("-" * 40)
        print(f"  Fixes:       {data['n_fixes']}")
        print(f"  Mean Lat:    {data['mean_lat']:.8f}°")
        print(f"  Mean Lon:    {data['mean_lon']:.8f}°")
        print(f"  σ (Lat):     {data['std_lat_m']:.2f} m")
        print(f"  σ (Lon):     {data['std_lon_m']:.2f} m")
        print(f"  σ (2D):      {data['std_2d']:.2f} m")
        print(f"  Mean HDOP:   {data['mean_hdop']:.2f}")
        print(f"  Mean Sats:   {data['mean_sats']:.1f}")

    print(f"\n{'=' * 70}")
    print("INTER-CONSTELLATION POSITION DIFFERENCES")
    print("-" * 40)
    for comparison, distance in analysis['comparisons'].items():
        print(f"  {comparison}: {distance:.2f} m")

if __name__ == "__main__":
    # Example with simulated data
    test_data = [
        "$GPGGA,120000.00,3356.5500,N,11824.4860,W,1,08,1.2,100.0,M,-34.0,M,,*5A",
        "$GLGGA,120000.00,3356.5520,N,11824.4850,W,1,07,1.4,100.5,M,-34.0,M,,*5B",
        "$GAGGA,120000.00,3356.5510,N,11824.4855,W,1,06,1.3,100.2,M,-34.0,M,,*5C",
        # ... more sentences
    ]

    analysis = analyze_gnss_data(test_data)
    print_gnss_report(analysis)

Summary

This module has equipped you with military-grade tooling knowledge for navigation operations:

Key Formulas

Distance Calculations
\[\text{Vincenty}: s = b \cdot A \cdot (\sigma - \Delta\sigma)\]
Error Metrics
\[\text{CEP} = 0.674 \cdot \sigma_r \quad\quad \text{2DRMS} = 2\sqrt{\sigma_E^2 + \sigma_N^2}\]
Angular to Linear
\[e = D \cdot \frac{\theta}{206265} \quad \text{(arc-seconds to meters)}\]

Tool Selection Matrix

Task CLI Tool Python Library Accuracy

Datum transformation

cs2cs (PROJ)

pyproj

Sub-centimeter

Geodesic distance

geod

geographiclib/vincenty

<1mm (Vincenty)

MGRS conversion

 — 

mgrs

<1m

Declination

 — 

geomag

<0.1°

NMEA parsing

awk/sed

pynmea2

 — 

GPS monitoring

gpsd

gps3

 — 

Critical Equipment Specifications

Table 14. GPS Receivers
System Horizontal (95%) Vertical (95%) Notes

PLGR (P/Y)

5.3m SEP

 — 

Legacy

DAGR

3.4m SEP

 — 

Current military

Civilian L1

~5m CEP

~10m

SA disabled

RTK (Fixed)

1-2cm

2-4cm

Survey grade

INS Error Growth
\[\epsilon(t) \approx \epsilon_0 + 0.01^\circ/\text{hr} \cdot t\]

Integration Checklist

  • PROJ library installed and tested

  • gpsd configured for GPS receiver

  • Python navigation libraries installed

  • Coordinate validation scripts tested

  • NMEA parsing pipeline functional

  • Round-trip conversion verified < 0.0001°

Next Steps

  1. Complete Field Exercises module

  2. Build personal navigation library from templates

  3. Integrate with dotfiles-optimus CLI mastery tools

  4. Practice fire mission coordinate procedures

  5. Validate all tools against known survey points