"""Parser for the FD (Temp Wind Aloft Forecasts)."""
import re
from datetime import timedelta
import numpy as np
import pandas as pd
from pyiem.nws.product import TextProduct
BASED_ON_RE = re.compile("^DATA BASED ON ([0-9]{6})Z", re.M)
VALID_RE = re.compile("^VALID ([0-9]{6})Z", re.M)
[docs]
def parse_encoding(text):
"""Convert the encoded text into drct, sknt, tmpc."""
tmpc = np.nan
drct = np.nan
sknt = np.nan
if len(text) not in [4, 6, 7]:
return drct, sknt, tmpc
drct = int(text[:2]) * 10
sknt = int(text[2:4])
# NWSI 10-812 section 6
if text.startswith("9900"):
drct = 0
sknt = 0
if len(text) > 4:
tmpc = int(text[-2:])
if text[4] == "-" or len(text) == 6:
tmpc *= -1
# Fun
if drct >= 500:
drct -= 500
sknt += 100
return drct, sknt, tmpc
[docs]
def compute_time(valid, tokens):
"""Figure out the timestamp of interest here."""
dd, hh, mi = int(tokens[0][:2]), int(tokens[0][2:4]), int(tokens[0][4:6])
valid2 = valid.replace(hour=hh, minute=mi)
if valid.day > 25 and dd < 5:
valid2 += timedelta(days=10)
if valid.day < 5 and dd > 25:
valid2 -= timedelta(days=10)
valid2 = valid2.replace(day=dd)
# In theory, we should not be far apart
if abs((valid2 - valid).days) > 5:
raise ValueError(f"Timestamp {valid2} too far from {valid}")
return valid2
[docs]
def make4(station, afos):
"""Make this 3 character station, 4!"""
ccode = afos[3:5]
if ccode == "US":
return f"K{station}"
if ccode == "CN":
return f"C{station}"
return f"P{station}"
[docs]
class FDProduct(TextProduct):
"""
Represents a FD Product
"""
def __init__(
self, text, utcnow=None, ugc_provider=None, nwsli_provider=None
):
"""constructor"""
text = text.replace("\x1e", "") # Aviation Control
super().__init__(text, utcnow, ugc_provider, nwsli_provider)
self.df = None
self.obtime = compute_time(
self.valid, BASED_ON_RE.findall(self.unixtext)
)
self.ftime = compute_time(self.valid, VALID_RE.findall(self.unixtext))
self.parser()
[docs]
def parser(self):
"""Do the parsing we need to do!"""
rows = []
levels = []
for line in self.unixtext.split("\n"):
if line.startswith("FT ") and not rows:
levels = line[3:].strip().split()
continue
if not levels:
continue
if len(line) < 10:
continue
tokens = line.strip().split(" ")
data = {"station": make4(tokens[0], self.afos)}
# fill right to left
for i in range(-1, -1 - len(levels), -1):
(
data[f"drct{levels[i]}"],
data[f"sknt{levels[i]}"],
data[f"tmpc{levels[i]}"],
) = parse_encoding(tokens[i])
rows.append(data)
if rows:
self.df = pd.DataFrame(rows).set_index("station")
[docs]
def sql(self, cursor):
"""Send the data to the database."""
if self.df is None or self.df.empty:
return
# Prevent NaN numbers from going to the database.
sql = ", ".join([f"{c} = %s" for c in self.df.columns])
for row in self.df.itertuples(index=True):
# Need upsert as data is split over products
cursor.execute(
"SELECT station from alldata_tempwind_aloft "
"where ftime = %s and station = %s and obtime = %s",
(self.ftime, row[0], self.obtime),
)
if cursor.rowcount == 0:
cursor.execute(
"INSERT into alldata_tempwind_aloft"
"(ftime, station, obtime) VALUES (%s, %s, %s)",
(self.ftime, row[0], self.obtime),
)
# np.nan + float64 columns + psycopg life is fun here
def _fint(val):
"""Force an int."""
if np.isnan(val):
return None
return int(val)
cursor.execute(
f"""
UPDATE alldata_tempwind_aloft SET {sql}
WHERE ftime = %s and station = %s and obtime = %s
""",
(
*[_fint(x) for x in row[1:]],
self.ftime,
row[0],
self.obtime,
),
)
[docs]
def parser(text, utcnow=None, ugc_provider=None, nwsli_provider=None):
"""Provide back FD objects based on the parsing of this text"""
return FDProduct(text, utcnow, ugc_provider, nwsli_provider)