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.
# 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
| 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=PROJECTION +datum=DATUM +zone=ZONE +ellps=ELLIPSOID
+towgs84=dx,dy,dz,rx,ry,rz,s (7-parameter transformation)
# 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
# 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"
# 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.
# 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"
# 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
# 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
# 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
$TALKER_ID,SENTENCE_ID,DATA_FIELDS*CHECKSUM<CR><LF>
| 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
$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
| 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 |
$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
| 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 |
$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
# 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
}'
# 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'
# 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
# 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")'
{
"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
}
| 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.
"""
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}°")
For typical GPS input uncertainty of \(\pm 5\) meters, the Vincenty distance error is approximately:
MGRS Batch Operations
#!/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.
| 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 |
The PLGR displays position as a confidence circle. The relationship between CEP, SEP, and 2DRMS:
AN/PSN-13 DAGR (Defense Advanced GPS Receiver)
Current standard handheld military GPS.
| 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) |
| 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 IMU │
│ (Ring Laser │
│ Gyroscopes) │
└────────┬────────┘
│ Angular rates, accelerations
▼
┌─────────────────┐
│ Navigation │
│ Computer │
│ (Kalman Filter)│
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌───────────┐
│Position │ │ Velocity │ │ Heading │
│ (MGRS) │ │ (m/s, mils) │ │ (mils) │
└─────────┘ └─────────────┘ └───────────┘
Inertial navigation systems accumulate error over time:
Where: - \(\epsilon_0\) = Initial alignment error - \(\epsilon_g\) = Gyro drift rate (typically 0.01°/hour for tactical grade) - \(\epsilon_a\) = Accelerometer bias contribution
| 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)
| Parameter | Value |
|---|---|
Angular Accuracy |
±1" (arc-second) |
Distance Accuracy |
±(2mm + 2ppm) |
Range (Prism) |
Up to 5,000m |
Range (Reflectorless) |
500-1,200m |
At distance \(D\), an angular error of \(\theta\) arc-seconds produces linear error:
For 1" error at 1000m:
RTK GPS (Real-Time Kinematic)
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) │
└──────────────┘ └──────────────┘
| 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 |
RTK accuracy depends on resolving the carrier phase integer ambiguity:
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)
| 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 |
GRID: 38S LB 12345 67890
ALT: 342M
TGT TYPE: TROOPS IN OPEN
ATTITUDE: LINEAR 450 MILS
The CEP is the radius within which 50% of rounds will fall:
For elongated patterns (typical of artillery):
Where \(\sigma_R\) = range error, \(\sigma_D\) = deflection error.
Verification and Validation
Coordinate Validation Scripts
#!/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
#!/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
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
GISGeography UTM Tool
-
UTM zone lookup
-
Visual zone 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
| 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 |
# 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:
-
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
-
-
Calculate the geodesic distance and bearing between each pair
-
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:
-
Reads NMEA sentences from a file
-
Extracts GGA sentences
-
Converts positions to decimal degrees
-
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:
-
Takes a start MGRS, end MGRS, and number of waypoints as arguments
-
Converts both endpoints to geographic coordinates
-
Calculates geodesic distance and initial bearing
-
Generates intermediate waypoints
-
Converts each waypoint back to 10-digit MGRS
-
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:
-
Reads gpsd JSON output continuously
-
Calculates running CEP statistics
-
Alerts when HDOP exceeds threshold
-
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:
-
Accepts observer position (MGRS)
-
Takes direction (mils) and distance (meters) to target
-
Computes target location in MGRS
-
Formats output for AFATDS entry
-
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:
-
Processing GGA sentences from GPS, GLONASS, and Galileo
-
Computing separate position solutions for each constellation
-
Calculating inter-constellation position differences
-
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
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
| 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 |
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
-
Complete Field Exercises module
-
Build personal navigation library from templates
-
Integrate with
dotfiles-optimusCLI mastery tools -
Practice fire mission coordinate procedures
-
Validate all tools against known survey points