Awk program: trip

This Awk program does trip planning, given a file listing various cities, with stops.

The data comes from a text file that originally came from the US Census Bureau in 2002.
It has 154,568 named cities, with their latitudes, longitudes, and associated quadrangles.
The file is 9,530,947 bytes in size.


#!/usr/bin/env -S awk -f
# trip - road trip planner.
# trip is Copyright Daniel K. Allen, 2000-2026.
# All rights reserved.
#
# 29 Apr 2000 - Created by Dan Allen. (In Zanesville, OH while on Lower 48 states trip.)
#  1 May 2000 - Empirical constant changed from 7/5 to 1.33.
# 16 May 2000 - Writes a map track file.
#  3 Aug 2000 - Renamed trip.awk from plan.awk.
#  2 Apr 2001 - Boot variable used on Mac; duplicate cities bug fixed.
#  9 May 2001 - Statute miles, shows time, empirical constant gone, multiline comments allowed.
# 21 May 2002 - Ported to Mac OS X.
# 10 Jun 2002 - Cleaner output.
# 15 Jun 2002 - Even cleaner output. (Peter Tobin's, Rathdrum, ID, while on family x-country trip.)
# 18 Jun 2002 - Refueling rounded. (Spearfish,SD)
# 15 Nov 2002 - Added to bin.
# 13 Jan 2009 - Updated Mac OS X path.
# 24 Jul 2014 - Added total time. (Fairhaven,MA,41.643,70.883)
# 13 Oct 2016 - Uses env.
# 18 Oct 2016 - No longer uses env due to -S being non-portable.
# 23 Apr 2020 - Uses env -S.
# 31 May 2021 - Fixed USCities name.
# 14 Jul 2021 - Added isdir(); redid USCities file determination.
# 15 Jul 2021 - Added another file location.
# 29 Oct 2021 - AwkTrue -> awk.
# 16 Oct 2022 - Find extreme points; detabbed. (Davenport,IA - ETA 64th!)
# 17 Oct 2022 - Exit immediately if a place is not found, returning error code 2.
# 19 Apr 2023 - Allow "stay" as well as "overnight"; print tilde lines.
# 22 Apr 2023 - Prints comments; fixed bug with last line being a tilde comment.
#  6 Dec 2024 - ASCIIifed.
# 23 Jan 2026 - Nicer final output.
# 19 Jul 2026 - If no space after # then do not print.
#
# Usage: trip trip.txt
#
# Expects to parse tab-delimited USCities.txt file in this order:
#
#   City  County  State  Latitude  Longitude  Elevation  Quadrangles
#
# Expects trip.txt lines that start with # and ~ to be in certain format.
#

BEGIN {
  a["AL"] = "Alabama"
  a["AK"] = "Alaska"
  a["AZ"] = "Arizona"
  a["AR"] = "Arkansas"
  a["CA"] = "California"
  a["CO"] = "Colorado"
  a["CT"] = "Connecticut"
  a["DE"] = "Delaware"
  a["FL"] = "Florida"
  a["GA"] = "Georgia"
  a["HI"] = "Hawaii"
  a["ID"] = "Idaho"
  a["IL"] = "Illinois"
  a["IN"] = "Indiana"
  a["IA"] = "Iowa"
  a["KS"] = "Kansas"
  a["KY"] = "Kentucky"
  a["LA"] = "Louisiana"
  a["ME"] = "Maine"
  a["MD"] = "Maryland"
  a["MA"] = "Massachusetts"
  a["MI"] = "Michigan"
  a["MN"] = "Minnesota"
  a["MS"] = "Mississippi"
  a["MO"] = "Missouri"
  a["MT"] = "Montana"
  a["NE"] = "Nebraska"
  a["NV"] = "Nevada"
  a["NH"] = "New Hampshire"
  a["NJ"] = "New Jersey"
  a["NM"] = "New Mexico"
  a["NY"] = "New York"
  a["NC"] = "North Carolina"
  a["ND"] = "North Dakota"
  a["OH"] = "Ohio"
  a["OK"] = "Oklahoma"
  a["OR"] = "Oregon"
  a["PA"] = "Pennsylvania"
  a["RI"] = "Rhode Island"
  a["SC"] = "South Carolina"
  a["SD"] = "South Dakota"
  a["TN"] = "Tennessee"
  a["TX"] = "Texas"
  a["UT"] = "Utah"
  a["VT"] = "Vermont"
  a["VA"] = "Virginia"
  a["WA"] = "Washington"
  a["WV"] = "West Virginia"
  a["WI"] = "Wisconsin"
  a["WY"] = "Wyoming"
  a["DC"] = "District of Columbia"
  PI = atan2(1, 1)*4
  RTOD = 180/PI
  DTOR = PI/180
  if (!speed) speed = 60 # MPH
  if (!outputLabels) outputLabels = 0 # to write lat/lon to L48Labels.txt file
  northernMost = westernMost = -999
  southernMost = easternMost = 999
}

function Abs(x) {
  return x < 0 ? -x : x
}

function Cos(x) { # Cosine, argument in degrees
  return cos(x*DTOR)
}

function AGD(x, y, z) { # Inverse Gudermannian, argument in degrees
  y = PI/4+x/2*DTOR
  z = sin(y) /cos(y)
  return z <= 0 ? "infinity" : log(z)
}

function NextDay(dmy, d, m, y) { # Expects date as "dd mmm yyyy", i.e., "30 Apr 2000"
  d = substr(dmy, 1, index(dmy, " ") -1)
  m = substr(dmy, index(dmy, " ")+1, 3)
  y = substr(dmy, length(dmy) -3)
  d++
  if (d == 32 && m == "Jan") return "1 Feb " y
  if (d == 29 && m == "Feb" && y % 4 || d == 30 && m == "Feb") return "1 Mar " y
  if (d == 32 && m == "Mar") return "1 Apr " y
  if (d == 31 && m == "Apr") return "1 May " y
  if (d == 32 && m == "May") return "1 Jun " y
  if (d == 31 && m == "Jun") return "1 Jul " y
  if (d == 32 && m == "Jul") return "1 Aug " y
  if (d == 32 && m == "Aug") return "1 Sep " y
  if (d == 31 && m == "Sep") return "1 Oct " y
  if (d == 32 && m == "Oct") return "1 Nov " y
  if (d == 31 && m == "Nov") return "1 Dec " y
  if (d == 32 && m == "Dec") return "1 Jan "++ y
  return d " " m " " y
}

function DMS2Dec(dms, neg) { # ddmmssx--> dd.ddddd
  CONVFMT = "%.5f"
  if (length(dms) == 7) {
    neg =(substr(dms, 7, 1) == "S") ? -1 : 1
    return neg *(substr(dms, 1, 2)+substr(dms, 3, 2) /60+substr(dms, 5, 2) /3600)
  }
  else if (length(dms) == 8) {
    neg =(substr(dms, 7, 1) == "E") ? -1 : 1
    return neg *(substr(dms, 1, 3)+substr(dms, 4, 2) /60+substr(dms, 6, 2) /3600)
  }
}

function HR2HMS(hr, h, m) { # hr -> hh:mm
  h = int(hr)
  m = int(( hr-h)*60+0.5)
  if (m == 60) { m = 0; h++ }
  if (length(m) == 1) m = "0" m
  return h ":" m
}

function RhumbDist(lat1, lon1, lat2, lon2, dist) { # arguments in degrees, result in statute mi
  course = RTOD*atan2(DTOR *(lon1-lon2), AGD(lat2)-AGD(lat1))
  if (course < 0) course+= 360
  dist = lat1-lat2 == 0 ? Abs(lon2-lon1)*Cos(lat2) :(lat2-lat1) / Cos(course);
  CONVFMT = "%.1f"
  return dist*69.046766881413
}

{ # parse stdin with a list of places to visit, one per line
  if (index($0, "#") == 1) { # comments are ignored
    if (index($0, " ") == 2) print $0
    if (n == 0) { # exception: a comment as the first line of a file is a starting date
      date = substr($0, 3)
    }
    if (index($0, "#skip") == 1) inComment = 1
    if (index($0, "#end") == 1) inComment = 0
    next
  }
  if (inComment == 1) next
  trip[++n] = $0
  if (index($0, "~") == 1) next
  i = index($0, ", ")
  if (i > 0)
    city = substr($0, 1, i+1) a[substr($0, i+2)]
  else
    city = $0
  cities[city] = n
}

END {
  # look for the data file in a variety of places, depending upon setup and OS
  if (ENVIRON["Boot"])
    filename = ENVIRON["Boot"] "Sources:Text:USCensus2000Cities.txt"
  else if (ENVIRON["windir"])
    filename = "c:\\Sources\\Text\\USCensus2000Cities.txt"
  else if (isdir(ENVIRON["HOME"] "/src/awk"))
    filename = ENVIRON["HOME"] "/src/awk/USCities.txt"
  else if (isdir(ENVIRON["HOME"] "/arc"))
    filename = ENVIRON["HOME"] "/arc/USCities.txt"
  else
    filename = "USCities.txt"

  # lookup cities
  while (getline < filename) {
    split($0, temp, "\t")
    if (temp[4] == "UNKNOWN") continue
    t = temp[1] ", " temp[3]
    if (t in cities) { # for duplicate cities, use the one with city name in the quad field 
      if (length(out[cities[t]]) && !index(temp[7], temp[1])) continue
      out[cities[t]] = t "\t" temp[4] "\t" temp[5]
    }
  }
  
  # add info for duplicate stops
  for (i = 2; i <= n; i++) {
    if (length(out[i]) == 0) {
      for (j = 1; j <= n; j++)
        if (trip[i] == trip[j] && length(out[j]) > 0)
          out[i] = out[j]
    }
  }

  # print cities with lat/lon and calculate distances
  dayNum = 1
  t = out[trip[1] == trip[n] ? n : 1] # reuse if you return to the city you started from
  if (t == "") t = out[n-1]
  if (t == "") t = out[n-2]
  print t
  split(t, temp, "\t")
  lat1 = DMS2Dec(temp[2])
  lon1 = DMS2Dec(temp[3])
  OFMT = CONVFMT = "%.5f"
  Extreme()
  sub(/, .*/, "", temp[1])
  if (outputLabels) print lat1, lon1, temp[1] > "L48Label.txt"
  for (i = 2; i <= n; i++) {
    if (index(trip[i], "~") == 1) {
      if (!index(trip[i], "fuel")) printf("%s\n", trip[i])
      if (index(trip[i], "stay") || index(trip[i], "over")) { # as in "layover" or "overnight"
        printf("# %11s Day %2d Distance %4.0f mi Time %s\n", date, dayNum++, daily, HR2HMS(daily/speed))
        date = NextDay(date)
        daily = 0
        OFMT = CONVFMT = "%.5f"
        sub(/, .*/, "", temp[1])
        if (outputLabels) print lat2, lon2, temp[1] > "L48Label.txt"
      }
      if (index(trip[i], "fuel")) { # as in "refuel" or "fuel"
        CONVFMT = "%.0f"
        print " refuel after " tank " mi"
        tank = 0
      }
      continue
    }
    if (length(trip[i]) == 0)
      continue
    else if (length(out[i]) == 0) {
      print "### NOT FOUND: " trip[i]
      exit(2)
    }
    else { # calculate distance
      split(out[i], temp, "\t")
      lat2 = DMS2Dec(temp[2])
      lon2 = DMS2Dec(temp[3])
      dist = RhumbDist(lat1, lon1, lat2, lon2)
      tank+= dist
      daily+= dist
      sum+= dist
      print " " dist " mi @ " course "\xA1"
      print out[i]
      OFMT = CONVFMT = "%.5f"
      if (outputLabels) print lat2, lon2 > "L48Label.txt"
      lat1 = lat2
      lon1 = lon2
      Extreme()
    }
  }
  printf("# %11s Day %2d Distance %4.0f mi Time %s\n", date, dayNum++, daily, HR2HMS(daily/speed))
  printf("# Total Dist %6.0f mi-Total Time %.1f hr\n", sum, sum/speed)
  printf("# Farthest N: %7.3f\xA1-%s\n", northernMost, northernName)
  printf("# Farthest S: %7.3f\xA1-%s\n", southernMost, southernName)
  printf("# Farthest W: %7.3f\xA1-%s\n", westernMost, westernName)
  printf("# Farthest E: %7.3f\xA1-%s\n", easternMost, easternName)
}

function isdir(p) {
  return !system("test -d \""p"\"")
}

function Extreme() {
  if (lat1 > northernMost) {
    northernMost = lat1
    northernName = temp[1]
  }
  if (lon1 > westernMost) {
    westernMost = lon1
    westernName = temp[1]
  }
  if (lat1 < southernMost) {
    southernMost = lat1
    southernName = temp[1]
  }
  if (lon1 < easternMost) {
    easternMost = lon1
    easternName = temp[1]
  }
}


Back to Dan Allen's home page.
Created:   8 Aug 2026
Modified:  8 Aug 2026