# $Id: importNOS_DodgyCode.rb 7145 2019-12-29 02:00:24Z flaterco $
# Functions for importNOS.rb:
# Dodgy code for filling in the fields and metadata that we don't get.

# *** NOTE this is tides-only right now; currents have their own blacklist.
# false: the station is in a US state, a territory or commonwealth of the US,
#   or a country in free association with the US.
# true: the station should be dropped.
#
# This is called before states are rationalized.  It has to work with what is
# in the record.  Longitude might be wrong too, but is used only for VI.
#
# // CA ends a little south of Imperial Beach.
# assert (lat >= 32.5);
def isMaybeNotFreeData(gt, sbgt, somerec)
  name = somerec["name"]
  badNameWords = [", B.C.", "Wales Island (Cannery)", "(N.Z.)", "(Chile)"]
  return true if badNameWords.any?{|badword| name.include?(badword)}

  blacklist = ["Antigua and Barbuda", "Bahamas", "Baja California",
    "Bermuda", "Bermuda Islands", "Cook Islands", "Cuba", "El Salvador",
    "Fiji", "French Polynesia", "Guatemala", "Haiti and Dominican Republic",
    "Honduras", "Jamaica", "Kiribati", "Mexico", "Nicaragua", "Tokelau",
    "United Kingdom",

    # 1778000 APIA, W. SAMOA / APIA (Observatory), Upolu Island
    # West Samoa became Independent State of Samoa in 1997
    "1778000"
  ]
  return true if blacklist.include?(somerec["state"])

  l6a = stationGeoLevelName(gt, sbgt, somerec["id"], 6)

  # "Lesser Antilles & Virgin Islands" is split between U.S. Virgin
  # Islands and everything else.  See also "British Columbia and Alaska."
  if l6a.length == 1 && l6a[0] == "Lesser Antilles & Virgin Islands"
    lat = somerec["lat"]
    lng = somerec["lng"]
    !((lat > 18.2 && lat < 18.38 && lng > -65.13 && lng < -64.65) ||
      (lat > 17.65 && lat < 17.82 && lng > -64.94 && lng < -64.54))

  else
    return true if !l6a.empty? && l6a.all?{|l6| blacklist.include?(l6)}

    l5a = stationGeoLevelName(gt, sbgt, somerec["id"], 5)
    return true if !l5a.empty? && l5a.all?{|l5| blacklist.include?(l5)}

    blacklist.include?(somerec["id"])
  end
end

# Detect western Aleutians before name gets finalized.
W_Aleut_Islands = [
  "Rat Islands",   # l7 geogroup, 3 stations; also, in currents.json,
                   #   "Krysi Pass, Rat Islands" and
                   #   "Sea Lion Pass, Rat Islands"
  "Attu Island",   # l7 geogroup, 5 stations (1 ref)
  "Shemya Island", # station name in tides.json, "Alcan Harbor, Shemya Island"
  "Agattu Island"  # station name in tides.json, "Otkriti Bay, Agattu Island"
]
def westernAleutian?(gt, sbgt, wAleutSids, tname, sid)
  return true if wAleutSids.include?(sid)
  l7a = stationGeoLevelName(gt, sbgt, sid, 7)
  return true unless (l7a & W_Aleut_Islands).empty?
  !tname.nil? && W_Aleut_Islands.any? {|island| tname.include?(island)}
end

# From USPS 2-character codes and irregular forms appearing in harcon.json to
# long state names.
StateName = {
  "AK" => "Alaska",
  "AL" => "Alabama",
  "AS" => "American Samoa",
  "CA" => "California",
  "CT" => "Connecticut",
  "DC" => "DC",
  "DE" => "Delaware",
  "FL" => "Florida",
  "FM" => "FSM",
  "GA" => "Georgia",
  "GU" => "Guam",
  "HI" => "Hawaii",
  "LA" => "Louisiana",
  "MA" => "Massachusetts",
  "MD" => "Maryland",
  "ME" => "Maine",
  "MH" => "Marshall Islands",
  "MP" => "Marianas",     # or "Northern Mariana Islands" (vs. Guam)
  "MS" => "Mississippi",
  "NC" => "North Carolina",
  "NH" => "New Hampshire",
  "NJ" => "New Jersey",
  "NY" => "New York",
  "OR" => "Oregon",
  "PA" => "Pennsylvania",
  "PR" => "Puerto Rico",
  "PW" => "Palau",
  "RI" => "Rhode Island",
  "SC" => "South Carolina",
  "TX" => "Texas",
  "VA" => "Virginia",
  "VI" => "Virgin Islands",   # or "US Virgin Islands"
  "WA" => "Washington",

  # Irregular forms
  "United States of America" => nil,
  "US" => nil,
  "American Samoa" => "American Samoa",
  "Micronesia, Federated Sta" => "FSM",
  "Virgin Islands, US" => "Virgin Islands"

  # Other abbreviations that have been known to appear:
  #   BA = Bahamas (independent)
  #     but ISO 3166 says BA = Bosnia & Herzegovina.
  #   MT = Madero-Tampico (Tamaulipas, Mexico)
  #     but ISO 3166 says MT = Malta.
}

# From long state names, L5 geogroups, and variants to USPS 2-character codes.
# Tokelau (New Zealand) also appears in L5 for some reason.
StateCode = {
  "Alaska" => "AK",
  "Alabama" => "AL",
  "American Samoa" => "AS",
  "California" => "CA",
  "Connecticut" => "CT",
  "DC" => "DC",
  "D.C." => "DC",
  "Washington DC" => "DC",
  "Washington D.C." => "DC",
  "Washington, DC" => "DC",
  "Washington, D.C." => "DC",
  "Delaware" => "DE",
  "Florida" => "FL",
  "FSM" => "FM",
  "F.S.M." => "FM",
  "Georgia" => "GA",
  "Guam" => "GU",
  "Hawaii" => "HI",
  "Louisiana" => "LA",
  "Massachusetts" => "MA",
  "Maryland" => "MD",
  "Maine" => "ME",
  "Marshall Islands" => "MH",
  "Marianas" => "MP",
  "Mississippi" => "MS",
  "North Carolina" => "NC",
  "New Hampshire" => "NH",
  "New Jersey" => "NJ",
  "New York" => "NY",
  "Oregon" => "OR",
  "Palau" => "PW",
  "Pennsylvania" => "PA",
  "Puerto Rico" => "PR",
  "Rhode Island" => "RI",
  "South Carolina" => "SC",
  "Texas" => "TX",
  "Virginia" => "VA",
  "Virgin Islands" => "VI",
  "Washington" => "WA"
}

# Guess the state for new stations.  Takes coordinates redundantly with
# somerec because of W Aleutians longitude problem.
def guessState(gt, sbgt, stateMap, somerec, lat, lng)
  sid = somerec["id"]

  # Columbia River in WA:
  # "9440079": # Beacon Rock State Park
  # "9440357": # TEMCO Kalama Terminal
  # "9440483": # Barlow Point
  # Palau:
  # "TPT2631"  # Shonian Harbor
  # "TPT2633"  # Koror
  return "WA" if ["9440079", "9440357", "9440483"].include?(sid)
  return "PW" if ["TPT2631", "TPT2633"].include?(sid)

  l5a = stationGeoLevelName(gt, sbgt, sid, 5)
  if l5a.length == 1
    l5 = l5a[0]
    return StateCode[l5] if StateCode.include?(l5)
  end

  state = somerec["state"]
  return state unless state.nil? or state.empty?

  # So... it has come to this.
  xyz = to_xyz(lat, lng)
  res = stateMap.min_by{|k,v| distfn(xyz,k)}
  dist = Math::sqrt(distfn(xyz, res[0]))
  state = res[1]
  print "Guessing state %s (d=%.0e) for %s %s lat %0.4f lng %0.4f\n" %
    [state, dist, sid, somerec["name"], lat, lng]
  state
end

# Assign a country on the assumption that MaybeNotFreeData has been filtered
# out and the state code has been rationalized by guessState.  Territories
# and commonwealths of the US are called USA.  We just have to identify other
# "associated states."
def guessCountry(state, name)
  if state.nil?
    if name.include?("FSM") or
	name.include?("F.S.M.") or
	name.include?("Micronesia")
      "FSM"   # Federated States of Micronesia
    elsif name.include?("Marshall Islands")
      "RMI"   # Republic of the Marshall Islands
    elsif name.include?("Palau")
      "Palau"
    else
      "USA"
    end
  else
    case state
    when "FM"
      "FSM"
    when "MH"
      "RMI"
    when "PW"
      "Palau"
    else
      "USA"
    end
  end
end

# Create name based on harcon name, tides name, state, and geogroup, as
# applicable.
#
# FIXME:
#
# 1. New code in importNOS_CurSubs avoids repetition by not adding geogroups
# if name.include?(region) already.  This function instead does
# removeRepetition at the end, which is not quite the same but works even if
# the repetition came from upstream.  Maybe switch to the new way.
#
# 2. "Entrance" and "[north/south/east/west] jetty" often are capitalized
# inappropriately and could be fixed here.
def guessLongStationName(hname, tname, state, geog)
  mname = nil

  raise "Nil hname" if hname.nil?
  if tname.nil? or hname.length > tname.length
    mnamebase = hname
  else
    mnamebase = tname
  end
  mnamebase = deAllCapsify(mnamebase) if isAllCaps(mnamebase)

  mstate = nil
  unless state.nil?
    if StateName.include?(state)
      mstate = StateName[state]
    else
      print "Missing state: ", state, "\n"
      raise "Missing state"
    end
  end

  if geog.length > 1
    # Resolve Columbia and Potomac River using state names.
    unless mstate.nil?
      # Fix up spelling disagreement with geogroups
      nstate = (mstate == "DC" ? "Washington DC" : mstate)
      mgeog = geog.select{|g| g.include?(nstate)}
      if mgeog.length == 1
        mname = mnamebase + ", " + mgeog[0]
      elsif mgeog.length > 1
	raise "Ambiguous resolution even with state name"
      end
    end
    if mname.nil?
      # print "WARNING: making arbitrary selection from multiple geogroups\n"
      # mname = mnamebase + ", " + geog[0]
      raise "Arbitration happening in guessLongStationName"
    end
  elsif geog.length == 1
    mname = mnamebase + ", " + geog[0]
  elsif !mstate.nil?
    mname = mnamebase + ", " + mstate
  else
    mname = mnamebase
  end

  removeRepetition(mname)
end

def guessTimezone(state, name, lat, lng)
  case state

  # States with only one time zone
  when "PR"
    ":America/Puerto_Rico"
  when "VI"
    ":America/St_Thomas"
  when "ME", "NH", "MA", "RI", "CT", "NY", "PA", "NJ", "DE", "MD", "DC", "VA",
      "NC", "SC", "GA"
    ":America/New_York"
  when "AL", "MS", "LA", "TX"
    ":America/Chicago"
  when "WA", "OR", "CA"
    ":America/Los_Angeles"
  when "HI"
    ":Pacific/Honolulu"
  when "AS"
    ":Pacific/Pago_Pago"
  when "GU"
    ":Pacific/Guam"
  when "MP"
    ":Pacific/Saipan"
  when "PW"
    ":Pacific/Palau"

  # Hard cases
  when "FL"
    # Port St. Joe is the westernmost spot on Eastern time.
    # As of 2017, longitude is quoted as -85.3133.
    lng < -85.31334 ? ":America/Chicago" : ":America/New_York"
  when "FM"
    # Three zones (Chuuk, Pohnpei, and Kosrae).  Only support 1.
    # Chuuk used to be called Truk.
    if name.include?("Chuuk")
      ":Pacific/Chuuk"
    else
      raise "Unsupported FM case in guessTimeZone"
    end
  when "MH"
    if name.include?("Kwajalein")
      ":Pacific/Kwajalein"
    elsif name.include?("Wake")
      ":Pacific/Wake"
    else
      raise "Unsupported MH case in guessTimeZone"
    end
  when nil
    if name.include?("Johnston")
      ":Pacific/Johnston"
    elsif name.include?("Midway")
      ":Pacific/Midway"
    else
      raise "Unsupported nil case in guessTimeZone"
    end

  # The *really* hard case.
=begin
  ALASKAN TIME ZONES

  Both before and after 1983, there was a time zone change as you headed
  out the Aleutians.  However, the location of the dividing line moved.
  Five distinct zones result (tzdata2004g):

  Zone America/Juneau      15:02:19 -     LMT     1867 Oct 18
			   -8:57:41 -     LMT     1900 Aug 20 12:00
			   -8:00  -       PST     1942
			   -8:00  US      P%sT    1946
			   -8:00  -       PST     1969
			   -8:00  US      P%sT    1983 Oct 30 2:00
			   -9:00  US      Y%sT    1983 Nov 30
			   -9:00  US      AK%sT
  Zone America/Yakutat     14:41:05 -     LMT     1867 Oct 18
			   -9:18:55 -     LMT     1900 Aug 20 12:00
			   -9:00  -       YST     1942
			   -9:00  US      Y%sT    1946
			   -9:00  -       YST     1969
			   -9:00  US      Y%sT    1983 Nov 30
			   -9:00  US      AK%sT
  Zone America/Anchorage   14:00:24 -     LMT     1867 Oct 18
			   -9:59:36 -     LMT     1900 Aug 20 12:00
			  -10:00  -       CAT     1942
			  -10:00  US      CAT/CAWT 1946
			  -10:00  -       CAT     1967 Apr
			  -10:00  -       AHST    1969
			  -10:00  US      AH%sT   1983 Oct 30 2:00
			   -9:00  US      Y%sT    1983 Nov 30
			   -9:00  US      AK%sT
  Zone America/Nome        12:58:21 -     LMT     1867 Oct 18
			  -11:01:38 -     LMT     1900 Aug 20 12:00
			  -11:00  -       NST     1942
			  -11:00  US      N%sT    1946
			  -11:00  -       NST     1967 Apr
			  -11:00  -       BST     1969
			  -11:00  US      B%sT    1983 Oct 30 2:00
			   -9:00  US      Y%sT    1983 Nov 30
			   -9:00  US      AK%sT
  Zone America/Adak        12:13:21 -     LMT     1867 Oct 18
			  -11:46:38 -     LMT     1900 Aug 20 12:00
			  -11:00  -       NST     1942
			  -11:00  US      N%sT    1946
			  -11:00  -       NST     1967 Apr
			  -11:00  -       BST     1969
			  -11:00  US      B%sT    1983 Oct 30 2:00
			  -10:00  US      AH%sT   1983 Nov 30
			  -10:00  US      HA%sT

  Statutory time zone boundaries in Alaska, pre-1983
  --------------------------------------------------

  Excerpted from Code of Federal Regulations, 1982.  This language came
  from 35 FR 12318, 1970-08-01, which was a rewrite of previous text
  that had become chaotic from too many amendments.

  49 CFR 71.10

    The fifth zone, Pacific standard time zone, includes the part of the
    United States that is west of the boundary line between the mountain
    and Pacific standard time zones described in § 71.9 and east of 137
    degrees W. longitude.

  49 CFR 71.11

    The sixth zone, the Yukon Standard Time Zone, includes the part of
    the United States that is between 137 degrees W. longitude and 141
    degrees W. longitude.

  49 CFR 71.12

    The seventh zone, the Alaska-Hawaii standard time zone, includes
    that part of the United States that is between 141 deg. W. longitude
    and 162 deg. W longitude and including all of the State of Hawaii.

  49 CFR 71.13

    The eighth zone, the Bering standard time zone, includes that part
    of the United States that is between 162 deg. W. longitude and 172
    deg. 30' W. longitude and that part of the Aleutian Islands that is
    west of 172 deg. 30' W. longitude, but does not include any part of
    the State of Hawaii.

  Statutory time zone boundaries in Alaska, post-1983
  ---------------------------------------------------

  Excerpted from Code of Federal Regulations, 2003.

  49 CFR 71.11

    The sixth zone, the Alaska standard time zone, includes the entire
    State of Alaska, except as provided in § 71.12 of this title.

  49 CFR 71.12

    The seventh zone, the Hawaii-Aleutian standard time zone, includes
    the entire State of Hawaii and, in the State of Alaska, that part of
    the Aleutian Islands that is west of 169 degrees 30 minutes west
    longitude.

  tzdata2004g does not reflect a temporary shift of Juneau to Yukon time
  that occurred the summer of 1980.

  Numerous current sources report that St. Lawrence Island observes
  Hawaii-Aleutian time (Google "St. Lawrence Island" "Hawaii-Aleutian").
  The 2003 CFR do not clearly address St. Lawrence Island at all, though
  by default, it appears that it should be on Alaska time.
=end
  when "AK"
    if lng > -137 && lng < 0 # The Aleutians cross 180
      # Formerly Pacific time (-8), now Alaskan time (-9).
      ":America/Juneau"

      # Other than the first ("east of 137"), the pre-1983 disposition of
      # places on the boundary lines is not defined.  Let them go west.

    elsif lng > -141 && lng <= -137
      # Formerly Yukon time (-9), now Alaskan time (-9).
      ":America/Yakutat"

    elsif lng > -162 && lng <= -141
      # Formerly Alaska-Hawaii time (-10), now Alaskan time (-9).
      ":America/Anchorage"

      # 2003 CFR puts places exactly at 169.5 in Alaskan time.
      # (HAST is "west of 169 degrees 30 minutes")

      # Exceptions for islands north of Aleutians.  No place on the mainland
      # is west of -169.5.  The exclusion box is St. Lawrence Island.
    elsif ((lng >= -169.5 && lng <= -162) || (lng < -169.5 && lat > 56)) &&
    !(lat >= 62.8314 && lat <= 63.8614 && lng >= -171.9927 && lng <= -168.3973)
      # Formerly Bering time (-11), now Alaskan time (-9).
      ":America/Nome"

    else
      # Formerly Bering time (-11), now Hawaii-Aleutian time (-10).
      ":America/Adak"
    end

  else
    raise "Unrecognized state in guessTimeZone: " + state
  end
end

# Map back to standard time meridians (as of 2017) for validation.
# Uninhabited Johnston was arbitrated to Hawaii time by zoneinfo.
# Some other uninhabited islands don't have zones, get :Pacific/Honolulu.
TZmerids = {
  ":America/Puerto_Rico" => -4,
  ":America/St_Thomas" => -4,
  ":America/New_York" => -5,
  ":America/Chicago" => -6,
  ":America/Los_Angeles" => -8,
  ":America/Juneau" => -9,
  ":America/Yakutat" => -9,
  ":America/Anchorage" => -9,
  ":America/Nome" => -9,
  ":America/Adak" => -10,
  ":Pacific/Honolulu" => -10,
  ":Pacific/Johnston" => -10,
  ":Pacific/Midway" => -11,
  ":Pacific/Pago_Pago" => -11,
  ":Pacific/Kwajalein" => 12,
  ":Pacific/Majuro" => 12,
  ":Pacific/Wake" => 12,
  ":Pacific/Guam" => 10,
  ":Pacific/Saipan" => 10,
  ":Pacific/Truk" => 10,
  ":Pacific/Chuuk" => 10,
  ":Pacific/Yap" => 10,
  ":Pacific/Palau" => 9
}

# Back the meridian out of the M2 phases for validation.
def meridFromM2(m2utc, m2lst)
  raise "Negative phases in meridFromM2" if m2utc < 0 or m2lst < 0

  for merid in -13..13
    guess = (m2utc + 28.9841042*merid) % 360
    return merid if (guess - m2lst).abs <= 0.15
  end

  # There is a tiny minor chance that roundoff will cause guess and m2lst to
  # end up on opposite sides of the 0/360 line, but it hasn't happened yet.
  raise "meridFromM2 failed"
end
