awips2/edexOsgi/com.raytheon.edex.plugin.warning/WarningDecoder.py
2017-04-21 18:33:55 -06:00

1183 lines
48 KiB
Python

#!/usr/local/python/bin/python
# This program decodes a product's UGC and VTEC strings
##
# This software was developed and / or modified by Raytheon Company,
# pursuant to Contract DG133W-05-CQ-1067 with the US Government.
#
# U.S. EXPORT CONTROLLED TECHNICAL DATA
# This software product contains export-restricted data whose
# export/transfer/disclosure is restricted by U.S. law. Dissemination
# to non-U.S. persons whether in the United States or abroad requires
# an export license or other authorization.
#
# Contractor Name: Raytheon Company
# Contractor Address: 6825 Pine Street, Suite 340
# Mail Stop B8
# Omaha, NE 68106
# 402.291.0100
#
# See the AWIPS II Master Rights File ("Master Rights File.pdf") for
# further licensing information.
##
##
# This program decodes a product's UGC and VTEC strings
#
# SOFTWARE HISTORY
#
# Date Ticket# Engineer Description
# ------------- -------- -------------- --------------------------
# Initial creation
# Feb 19, 2013 1636 rferrel Use TimeTools to get file timestamp.
# May 07, 2013 1973 rferrel Adjust Issue and Purge times to be
# relative to start time.
# Jun 24, 2013 16317 D. Friedman If no storm line, parse storm motion
# from event text.
# Aug 21, 2013 16501 mgamazaychikov Adjusted calculation of Purge time in
# NoVTECWarningDecoder.
# Sep 12, 2013 2249 rferrel When incoming file from warngen adjust
# start time from file's timestamp.
# Oct 03, 2013 2402 bsteffen Make PythonDecoder more extendable.
# May 15, 2014 2536 bclement moved WMO time parsing to WMOTimeParser
# May 15, 2014 3157 dgilling Update location of WclInfo class.
# Jun 10, 2014 3268 dgilling Update location of WclInfo class.
# Dec 17, 2014 4953 randerso Fixed decoding of non-VTEC from command line
# May 14, 2015 4027 randerso Changed regex used to decode date line to accept mixed case
# Mar 24, 2015 4320 dgilling Fix NullPointerException in StdWarningDecoder.__init__()
# when decoding product not from a file.
# Mar 26, 2015 4324 dgilling Improve handling of all 0s time values in HVTEC strings.
# Sep 23, 2015 4848 nabowle Handle UGC-like lines in the text segment.
# Nov 10, 2015 17068 ryu Improve handling of lines starting with a UGC code
# but do not really begin new segments
# Jul 18, 2016 5749 randerso Remove replacement of commas with ellipses in segment text
#
#
# @author rferrel
# @version 1.0
##
import sys, os, time, re, string, getopt
import copy
import calendar
import LogStream
from awips import TimeUtil
from com.raytheon.uf.edex.decodertools.time import TimeTools
from com.raytheon.uf.common.wmo import WMOTimeParser
ACCURATE_CITIES_PILS = ['CFW', 'FFA', 'NPW', 'RFW', 'WSW']
REGIONS = {
"AK":"ALASKA", "AL":"SOUTHERN", "AR":"SOUTHERN",
"AS":"PACIFIC", "AZ":"WESTERN", "CA":"WESTERN",
"CO":"CENTRAL", "CT":"EASTERN", "DC":"EASTERN",
"DE":"EASTERN", "FL":"SOUTHERN", "GA":"SOUTHERN",
"GU":"PACIFIC", "HI":"PACIFIC", "IA":"CENTRAL",
"ID":"WESTERN", "IL":"CENTRAL", "IN":"CENTRAL",
"KS":"CENTRAL", "KY":"CENTRAL", "LA":"SOUTHERN",
"MA":"EASTERN", "MD":"EASTERN", "ME":"EASTERN",
"MI":"CENTRAL", "MN":"CENTRAL", "MO":"CENTRAL",
"MS":"SOUTHERN", "MT":"WESTERN", "NC":"EASTERN",
"ND":"CENTRAL", "NE":"CENTRAL", "NH":"EASTERN",
"NJ":"EASTERN", "NM":"SOUTHERN", "NV":"WESTERN",
"NY":"EASTERN", "OH":"EASTERN", "OK":"SOUTHERN",
"OR":"WESTERN", "PA":"EASTERN", "PR":"SOUTHERN",
"RI":"EASTERN", "SC":"EASTERN", "SD":"CENTRAL",
"TN":"SOUTHERN", "TX": "SOUTHERN", "UT":"WESTERN",
"VA":"EASTERN", "VI":"SOUTHERN", "VT":"EASTERN",
"WA":"WESTERN", "WI":"CENTRAL", "WV":"EASTERN",
"WY":"CENTRAL", "GM":"SOUTHERN" }
BEARINGS = {
"NORTH": 0, "NORTHEAST": 45, "EAST": 90, "SOUTHEAST": 135,
"SOUTH": 180, "SOUTHWEST": 225, "WEST": 270, "NORTWEST": 315 }
SPEED_CONVERSION = {
"MPH": 0.86898,
"KNOTS": 1,
"KTS": 1,
"KPH": 0.53996
}
PHENSIGS_IGNORE_HVTEC = ["FL.A", "FF.A", "FA.Y", "FA.W", "FF.W"]
class StdWarningDecoder():
def __init__(self, text=None, filePath=None, command=None):
self._hasDTG = None
self._officeFromWMO = None
#to ensure time calls are based on Zulu
os.environ['TZ'] = "GMT0"
self._deleteAfterProcessing = 0
if filePath is None:
self._incomingFilename = None
else:
self._incomingFilename = filePath
self._notifyGFE = True
self._makeBackups = 1 #make backups after each "update"
self._wmoid = None
self._command = command
self._timeOffset = 0
#decode the command line
if command is not None:
self._decodeCommandLine()
self._rawMessage = None
checkForWmo = True
else:
self._rawMessage = text
checkForWmo = True
# default time is just present time
self._time = time.time() + self._timeOffset
if self._incomingFilename:
# base time for decoder
warningTimestamp = TimeTools.getWarningTimestamp(self._incomingFilename)
if warningTimestamp is not None:
self._time = long(warningTimestamp)
else:
if WMOTimeParser.allowArchive():
try:
yyyymmddhh = WMOTimeParser.getTimestamp(self._incomingFilename)
if len(yyyymmddhh) < 10:
timeTuple = time.strptime(yyyymmddhh, "%Y%m%d")
else :
timeTuple = time.strptime(yyyymmddhh, "%Y%m%d%H")
self._time = time.mktime(timeTuple)
except :
LogStream.logProblem('Unable to get timestamp from filename: "%s"' % (self._incomingFilename))
os.umask(0) #ensure proper permissions
self._mappedPils = {}
#get product
self._rawMessage, self._lines = self._getProduct(text, checkForWmo)
if len(self._lines) < 2:
raise Exception, "Empty Product -- ABORTING"
#Delete incoming product if requested.
if self._deleteAfterProcessing:
os.remove(self._incomingFilename)
#set up vtec regular expressions
self._vtecRE = r'/[OTEX]\.([A-Z]{3})\.([A-Z]{4})\.([A-Z]{2})\.' + \
r'([WAYSOFN])\.([0-9]{4})\.([0-9]{6})T([0-9]{4})Z\-' + \
r'([0-9]{6})T([0-9]{4})Z/'
self._hVtecRE = r'/([0-9A-Z]{5})\.([0-3NU])\.([A-Z]{2})\.' + \
r'([0-9]{6})T([0-9]{4})Z\.' + \
r'([0-9]{6})T([0-9]{4})Z\.([0-9]{6})T([0-9]{4})Z\.([A-Z][A-Z])/'
self._badVtecRE = r'^\s?/.*/\s?$'
self._endSegmentRE = r'^\$\$'
self._dlineRE = r"^1?[0-9]{3} [AP]M [A-Z][A-Z]?[DS]T.*[A-Z][A-Z,a-z]{2} " + \
r"[0123]?[0-9] 2[0-9]{3}.*$"
self._ugcRE = r'^[A-Z][A-Z][CZ][0-9]{3}[->]'
self._endTimeRE = r'-[0-3][0-9][0-2][0-9][0-5][0-9]-$'
#maximum future time (used for until further notice)
self._maxFutureTime = float(2**31 - 1) #max signed int
def decode(self):
#get pil and date-time group
self._productPil, self._issueTime, linePos,\
self._completeProductPil = self._getPilAndDTG()
# If this is a WCL - don't go any further. Run WCL procedure and exit.
if self._productPil[0:3] == "WCL":
endpoint = "WCLWatch"
# build a Java object for the warning
from com.raytheon.edex.plugin.gfe.watch import WclInfo
import JUtil
lines = JUtil.pyValToJavaObj(self._lines)
warning = WclInfo(long(self._issueTime * 1000),
self._completeProductPil, lines, self._notifyGFE)
from com.raytheon.uf.edex.core import EDEXUtil
EDEXUtil.getMessageProducer().sendAsync(endpoint, warning)
LogStream.logEvent("%s forwarded to WCLWatch" % self._productPil)
return []
# Determine if this is a segmented product
segmented = self._determineSegmented(linePos)
# Get overview text
if segmented == 1:
self._overviewText, linePos = self._getOverviewText(linePos)
else:
self._overviewText = ''
LogStream.logDebug("OverviewText: ", self._overviewText)
#find all UGCs, VTEC strings, and segment text
ugcVTECList = self._getUGCAndVTECStrings(linePos)
self._polygon = self._getPolygon(linePos)
self._storm = self._getStorm(linePos)
#convert UGC strings into UGC list
ugcVTECSegText = []
segCount = 1
for ugcString, vtecStrings, segText, cities in ugcVTECList:
purgeTime = None
self._checkForDTG(ugcString)
if self._hasDTG:
purgeTime = ugcString[-7:-1]
else:
purgeTime = self._getPurgeTimeStrFromVTEC(vtecStrings)
vtecList = self._expandVTEC(ugcString, vtecStrings, segCount,
segText, cities, purgeTime)
segCount = segCount + 1
for r in vtecList:
ugcVTECSegText.append(r)
if len(ugcVTECSegText) == 0:
LogStream.logVerbose("No VTEC Found in product")
return
return ugcVTECSegText
def _checkForDTG(self, ugcString):
if re.search(r'.*[0-3][0-9][0-2][0-9][0-5][0-9]', ugcString):
self._hasDTG = True
else:
self._hasDTG = False
def _getPurgeTimeStrFromVTEC(self, vtecStrings):
for vtecS, hvtec in vtecStrings:
search = re.search(self._vtecRE, vtecS)
#print "getting time from ", search.group(8)[-2:] + search.group(9)
timeStr = str(search.group(8)[-2:]) + str(search.group(9))
return timeStr
return None
def _usage(self):
#Prints out usage information if started without sufficient command
#line arguments.
s = """
usage: VTECDecoder -f productfilename -d -a activeTableName
-f productfilename: location of the file to be decoded
-d: delete input file after processing flag
-g: Notify GFE when configured items arrive
-w: wmoid WMOid for product, (optional, for info only)
-n: Do not make backups (optional)
-z: drtInfo Run in DRT mode
"""
print s
LogStream.logProblem(s)
def _decodeCommandLine(self):
#Routine to decode the command line.
if self._command is None:
self.command = sys.argv
if len(self._command) < 2:
self._usage()
sys.exit(1)
LogStream.logVerbose("Command line: ", self._command[1:])
try:
optionlist, arglist = getopt.getopt(self._command[1:], 'f:da:gw:nz:')
except getopt.error, val:
LogStream.logProblem(val)
self._usage()
sys.exit(1)
self._notifyGFE = False
for each_option in optionlist:
if each_option[0] == '-f':
self._incomingFilename = each_option[1]
elif each_option[0] == '-w':
self._wmoid = each_option[1]
elif each_option[0] == '-d':
self._deleteAfterProcessing = 1
elif each_option[0] == '-n':
self._makeBackups = 0
elif each_option[0] == '-g':
self._notifyGFE = True
elif each_option[0] == '-z':
self._timeOffset = TimeUtil.determineDrtOffset(each_option[1])[0]
#import offsetTime
#offsetTime.setDrtOffset(each_option[1])
if self._incomingFilename is None:
LogStream.logProblem("Invalid command line specified", self._command[1:])
self._usage()
sys.exit(1)
LogStream.logVerbose("WMOID: ", self._wmoid)
def _getProduct(self, text=None, checkForWmo=False):
#Opens, reads the product. Splits into lines, strips whitespace,
if text is None:
fd = open(self._incomingFilename, 'r')
if fd == -1:
s = "Unable to open incoming file: " + self._incomingFilename
LogStream.logProblem(s)
raise Exception, s
buf = fd.read()
fd.close()
else:
buf = text
raw = buf
if checkForWmo:
wmore = r'(\w{6} (\w{4}) (\d{6})(?: \w{3})?)'
wmosearch = re.search(wmore, buf)
if wmosearch:
self._officeFromWMO = wmosearch.group(2)
self._wmoid = wmosearch.group(1)
#eliminate junk characters and change some
if self._wmoid is not None:
index = string.find(buf, self._wmoid)
if index != -1:
buf = buf[index:] #remove garbage before start of real prod
# buf = re.sub(r",", r"...", buf) #eliminate commas, replace with ...
buf = re.sub(r"\.\.\. r", "...", buf) #'... ' becomes '...'
buf = re.sub(r"\r", r"", buf) #eliminate carriage returns
#LogStream.logVerbose("Product:\n", buf)
#process the file into lines, eliminate trailing whitespace
lines = string.split(buf, '\n')
for n in xrange(len(lines)):
lines[n] = string.rstrip(lines[n])
return raw, lines
def _determineSegmented(self, startLine):
#
# Determine if this is a segmented product or not
#
count = startLine
dlineFlag = 0
while count < 12 and count < len(self._lines):
if re.search(self._ugcRE,
self._lines[count]):
if dlineFlag == 0:
return 0
if re.search(self._dlineRE, self._lines[count]):
dlineFlag = 1
count += 1
return 1
def _getOverviewText(self, startLine):
#searches through the product from the startLine to the date-time
#group, then until the first UGC line. Extracts out the text for
#the overview text (which is after the MND header. Returns the
#overviewText.
count = startLine
ugcLine = None
#search for the MND header date line
while 1:
dline_search = re.search(self._dlineRE, self._lines[count])
count = count + 1
if dline_search:
break
if count >= len(self._lines)-1:
raise Exception, "No MND date line to start overview text"
startOverviewLine = count #next line after MND date line
#search for the 1st UGC line
while 1:
if self.checkForBeginSegment(count):
stopOverviewLine = count - 1
break
count = count + 1
if count >= len(self._lines)-1:
raise Exception, "No UGC line to end overview text"
#now eliminate any blank lines between the start/stop overview line
while startOverviewLine <= stopOverviewLine:
if len(string.strip(self._lines[startOverviewLine])) != 0:
break
startOverviewLine = startOverviewLine + 1
while startOverviewLine <= stopOverviewLine:
if len(string.strip(self._lines[stopOverviewLine])) != 0:
break
stopOverviewLine = stopOverviewLine - 1
LogStream.logDebug("start/stop overview: ", startOverviewLine,
stopOverviewLine)
#put together the text
if startOverviewLine <= stopOverviewLine:
overviewLines = self._lines[startOverviewLine:stopOverviewLine+1]
overviewText = string.join(overviewLines, '\n')
return (overviewText, stopOverviewLine)
else:
return ("", startLine)
def _getPilAndDTG(self):
#searches through the product (lines) and extracts out the product
#pil and date-time group. Returns (pil, issueTime, lineEnd, fullpil).
# The line end is how far the processing got for the PIL line.
count = 0
while 1:
dtg_search = re.search(r' ([0123][0-9][012][0-9][0-5][0-9])',
self._lines[count])
pil_search = re.search(r'^([A-Z]{3})(\w{3}|\w{2}|\w{1})',
self._lines[count+1])
if dtg_search and pil_search:
LogStream.logVerbose("Dtg=", dtg_search.group(0))
LogStream.logVerbose("Pil=", pil_search.group(0))
return (self._lines[count+1][0:3],
self._dtgFromDDHHMM(dtg_search.group(1)), count+2,
pil_search.group(0))
count = count + 1
if count >= len(self._lines)-1:
LogStream.logProblem("Did not find either the product DTG" +\
" or the pil: ", string.join(self._lines, sep='\n'),
LogStream.exc())
raise Exception, "Product DTG or Pil missing"
def _dtgFromDDHHMM(self, dtgString, baseTime=None):
if dtgString is None:
return 0.0
if baseTime is None :
baseTime = self._time
#utility function taking a ddhhmm string
#group1=day, group2=hour, group3=minute
#returns a time object
wmo_day = int(dtgString[0:2])
wmo_hour = int(dtgString[2:4])
wmo_min = int(dtgString[4:6])
gmtuple = time.gmtime(baseTime)
wmo_year = gmtuple[0]
wmo_month = gmtuple[1]
current_day = gmtuple[2]
if current_day - wmo_day > 15:
# next month
wmo_month = wmo_month + 1
if wmo_month > 12:
wmo_month = 1
wmo_year = wmo_year + 1
elif current_day - wmo_day < -15:
# previous month
wmo_month = wmo_month -1
if wmo_month < 1:
wmo_month = 12
wmo_year = wmo_year - 1
s = `wmo_year` + "%02i" % wmo_month + "%02i" % wmo_day + \
"%02i" % wmo_hour + "%02i" % wmo_min + "UTC"
timeTuple = time.strptime(s, "%Y%m%d%H%M%Z")
wmoTime = time.mktime(timeTuple) #TZ is GMT0, so this mktime works
LogStream.logVerbose("DTG=",dtgString, "IssueTime=",
time.asctime(time.gmtime(wmoTime)))
return wmoTime
def _getUGCAndVTECStrings(self, lineStart):
#goes through the product, extracts UGC and VTEC strings and the
#segment text, returns a list of (UGC keys, VTEC strings, segText).
#Segment number is determined by order in the list.
ugcList = []
count = lineStart #start on line following PIL
while 1:
#look for the first UGC line
if self.checkForBeginSegment(count):
LogStream.logDebug("First line of UGC found on line: ", count,
'[' + self._lines[count] + ']')
#print "ugc found on line ", count
#find the line with the terminating ugc (dtg), might be
#the same one. Terminating line has -mmddhh
#combine all of the UGC lines that are split across
nxt = 0 #number of lines from the first UGC line
ugc = "" #final UGC codes
while count+nxt < len(self._lines):
if re.search(self._endTimeRE, self._lines[count+nxt]):
LogStream.logDebug("Last line of UGC found on line: ",
count+nxt, '[' + self._lines[count+nxt] + ']')
ugc = string.join(self._lines[count:count+nxt+1],
sep="")
break
nxt = nxt + 1
# if we hit the end, break out and let the len(ugc) check fail
if count+nxt == len(self._lines):
break;
#after incrementing check if the next line is not a
#continuation of the ugc because it is a vtec string
#print "checking for non-ugc"
#print self._lines[count+nxt]
if re.search(self._vtecRE, self._lines[count+nxt]):
LogStream.logDebug("Last line of UCG was previous line: ",
count+nxt,
'[' + self._lines[count+nxt-1] + ']')
nxt = nxt - 1
ugc = string.join(self._lines[count:count+nxt+1],sep="")
break
if len(ugc) == 0:
s = "Did not find end of UGC line which started on " +\
" line " + `count`
LogStream.logProblem(s)
raise Exception, "Aborting due to bad UGC lines"
#find the VTEC codes following the ugc line(s)
nxt = nxt + 1 #go the 1st line after ugc
vtectext = []
while count+nxt < len(self._lines):
if re.search(self._vtecRE, self._lines[count+nxt]):
hvtec = None
if re.search(self._hVtecRE, self._lines[count+nxt+1]):
hvtec = self._lines[count+nxt+1]
vtectext.append((self._lines[count+nxt], hvtec))
LogStream.logDebug("VTEC found on line: ",
count+nxt, self._lines[count+nxt])
elif (re.search(self._badVtecRE, self._lines[count+nxt]) \
and not re.search(self._hVtecRE, self._lines[count+nxt])):
LogStream.logProblem("Bad VTEC line detected on line#",
count+nxt, '[' + self._lines[count+nxt] + ']',
'UGC=', ugc)
raise Exception,"Aborting due to bad VTEC line"
else:
break #no more VTEC lines for this ugc
nxt = nxt + 1 #go to next line
# for capturing the city names
cityFirst = count+nxt
cityLast = cityFirst - 1
#capture the text from dtg to the $$ at the beginning of
#the line. Just in case there isn't a $$, we also look
#for a new VTEC or UGC line, or the end of file.
textFirst = count+nxt
dtgFound = 0
segmentText = ""
while count+nxt < len(self._lines):
# Date-TimeGroup
if dtgFound == 0 and re.search(self._dlineRE,
self._lines[count+nxt]):
cityLast = count+nxt-1
textFirst = count+nxt+2 #first text line
dtgFound = 1
# found the $$ line
elif re.search(self._endSegmentRE, self._lines[count+nxt]):
segmentText = self._prepSegmentText(\
self._lines[textFirst:count+nxt])
break
# found a probable UGC line, terminate the segment
# if a DDMMHH or VTEC can be found
elif self.checkForBeginSegment(count+nxt):
segmentText = self._prepSegmentText(\
self._lines[textFirst:count+nxt])
nxt = nxt - 1 #back up one line to redo UGC outer loop
break
# end of file, terminate the segment
elif count+nxt+1 == len(self._lines):
segmentText = self._prepSegmentText(\
self._lines[textFirst:count+nxt+1])
break
nxt = nxt + 1 #next line
# capture cities
cityText = ''
for i in range(cityFirst, cityLast+1):
line = self._lines[i].rstrip()
if line.startswith("INCLUDING THE"):
cityText = line
elif cityText != '':
cityText += line
cities = []
if cityText != '':
cities = cityText.split('...')[1:]
#add the ugc and vtec text to the list
ugcList.append((ugc, vtectext, segmentText, cities))
count = count + nxt
count = count + 1
if count >= len(self._lines):
break
for e in ugcList:
LogStream.logVerbose("UGC/VTEC found: ", e[0], e[1])
return ugcList
def checkForBeginSegment(self, start):
if re.search(self._ugcRE, self._lines[start]) and \
not self.checkForFalseUGCLine(start):
return True
return False
def checkForFalseUGCLine(self, toCheck):
# tries to determine if an apparent UGC line encountered in a text
# segment is an actual UGC line, or is a UGC-like line in the free-text
# such as:
#THE AFFECTED AREAS WERE...
#GMZ074-GMZ054-
#STRAITS OF FLORIDA FROM WEST END OF SEVEN MILE BRIDGE TO HALFMOON
#SHOAL OUT 60 NM...
falsePositive = True
while toCheck < len(self._lines):
# look for the end date or vtec line before "$$" or the end of the
# file
if re.search(self._endTimeRE,
self._lines[toCheck]) or \
re.search(self._vtecRE, self._lines[toCheck]):
falsePositive = False
break;
# blank line
if not self._lines[toCheck]:
break
if re.search(self._endSegmentRE, self._lines[toCheck]):
break;
toCheck = toCheck + 1
return falsePositive
def _expandUGC(self, ugcString):
#expand a UGC string into its individual UGC codes, returns the list.
ugc_list = [] #returned list of ugc codes
ugc_line = None
if ( self._hasDTG ):
ugc_line = ugcString[0:-8] #eliminate dtg at end of ugc string
else:
ugc_line = ugcString
#print "ugc_line is ", ugc_line
working_ugc_list = ugc_line.split('-') #individual parts
state = ''
code_type = ''
for ugc in working_ugc_list:
try:
# Case One (i.e., WIZ023)...
if len(ugc) == 6:
state, code_type, decUGCs = self._ugcCaseOne(ugc)
for d in decUGCs:
ugc_list.append(d)
# Case Two (i.e., 023)...
elif len(ugc) == 3:
decUGCs = self._ugcCaseTwo(ugc, state, code_type)
for d in decUGCs:
ugc_list.append(d)
# Case Three (ie. 044>067)
elif len(ugc) == 7:
decUGCs = self._ugcCaseThree(ugc, state, code_type)
for d in decUGCs:
ugc_list.append(d)
# Case Four (ie. WIZ044>067)
elif len(ugc) == 10:
state, code_type, decUGCs = self._ugcCaseFour(ugc)
for d in decUGCs:
ugc_list.append(d)
# Problem - malformed UGC
else:
raise Exception, "Malformed UGC Found"
except:
LogStream.logProblem("Failure to decode UGC [" + ugc + \
"] in [" + ugc_line + "]", `self._lines`, LogStream.exc())
raise Exception, "Failure to decode UGC"
#print "ugc_list is ", ugc_list
return ugc_list
def _ugcCaseOne(self, ugc):
#Decodes the WIZ023 case. Returns state, code type, and list of
#decoded UGC codes. Returns None on failure.
subtype_search = re.search(r'[A-Z][A-Z][CZ][0-9][0-9][0-9]', ugc)
if not subtype_search:
return None
state = ugc[0:2]
code_type = ugc[2]
code = ugc[3:6]
decodedUgcs = [(state + code_type + code)]
return (state, code_type, decodedUgcs)
def _ugcCaseTwo(self, ugc, state, code_type):
#Decodes the 034 case. Current state and code_type are provided in
#order to generate ugc code. Returns list of decoded UGC codes.
#Returns None on failure.
subtype_search = re.search(r'[0-9][0-9][0-9]', ugc)
if not subtype_search:
return None
decodedUgcs = [(state + code_type + ugc)]
return decodedUgcs
def _ugcCaseThree(self, ugc, state, code_type):
#Decodes the 044>067 case. Current state and code_type are provided
#in order to generate ugc code. Returns list of decoded UGC codes.
#Returns None on failure.
subtype_search = re.search(r'[0-9][0-9][0-9]>[0-9][0-9][0-9]', ugc)
if not subtype_search:
return None
start_code = int(ugc[0:3])
end_code = int(ugc[4:7])
ugcList = []
for code in xrange(start_code, end_code + 1):
codeString = "%03i" % code
ugcList.append(state + code_type + codeString)
return ugcList
def _ugcCaseFour(self, ugc):
#Decodes the WIZ023>056 case. Returns state, code type, and list of
#decoded UGC codes. Returns None on failure.
searchString = r'[A-Z][A-Z][CZ][0-9][0-9][0-9]>[0-9][0-9][0-9]'
subtype_search = re.search(searchString, ugc)
if not subtype_search:
return None
state = ugc[0:2]
code_type = ugc[2]
ugcList = self._ugcCaseThree(ugc[3:10], state, code_type)
return (state, code_type, ugcList)
def _getPolygon(self, linePos):
poly = r'LAT\.\.\.LON [\d\s]*'
dec = r'[\d\s]+'
count = linePos
latlon = None
while count < len(self._lines):
if re.search(poly, self._lines[count]):
nxt = 0 #number of lines from the first latlon line
while count+nxt+1 < len(self._lines):
if re.match(dec, self._lines[count+nxt+1]):
nxt = nxt + 1
else:
break
n = 0
latlon = ""
while n <= nxt:
latlon += self._lines[count + n] + " "
n += 1
latlon = latlon.rstrip()
latlon = latlon[10:]
break
count += 1
return latlon
def _getStorm(self, linePos):
stormre = r'TIME\.\.\.MOT\.\.\.LOC \d+Z (\d+)DEG (\d+)KT ((?:\d{4,5}\s?)*)'
nextline = r' *((?:\d{4,5}\s?)+)'
storm = None
count = linePos
while count < len(self._lines):
search = re.search(stormre, self._lines[count])
if search:
coordString = search.group(3)
# check the next line
if re.search(nextline, self._lines[count + 1]):
coordString = coordString + " " + self._lines[count + 1].lstrip()
counter = 0
coords = coordString.split(" ")
buf = "MULTIPOINT("
tempbuf = ""
for coord in coords:
counter += 1
if counter % 2 == 0:
buf = buf + str(float(coord) / -100) + " " + tempbuf + ", "
tempbuf = ""
else:
tempbuf = str(float(coord) / 100)
buf = buf[0:-2]
buf = buf + ")"
storm = (int(search.group(1)), int(search.group(2)), buf)
break
count = count + 1
if storm is None:
storm = self._getStormFromEvent()
LogStream.logEvent('returning storm=%s' % repr(storm))
return storm
def _getStormFromEvent(self):
event_pattern = re.compile(r'^(?:\* )?AT (?:NOON|MIDNIGHT|\d+)(.*?)\n[ \r\t\f\v]*\n',
re.M | re.DOTALL)
m = event_pattern.search(self._rawMessage)
if m is None:
return None
event_text = m.group(1)
motion_pattern = re.compile(r'\bMOVING\s*(\w+)\s*AT\s*(\d+(?:\.\d*)?)\s*(\w+)')
m = motion_pattern.search(event_text)
if m is not None:
bearing = BEARINGS.get(m.group(1).upper())
if bearing is not None:
# Bearing is stored as the direction the storm is coming FROM
bearing -= 180
if bearing < 0:
bearing += 360
else:
LogStream.logProblem("could not parse bearing '%s' in '%s'" %
(m.group(1), m.group(0)))
conversion_factor = SPEED_CONVERSION.get(m.group(3))
converted_speed = None
if conversion_factor is not None:
try:
converted_speed = int(round(float(m.group(2)) * conversion_factor))
except StandardError, e:
LogStream.logProblem("error processing speed in '%s': %s" %
(m.group(0), e))
else:
LogStream.logProblem("unknown unit '%s' in '%s'" %
(m.group(3), m.group(0)))
else:
# find "STATIONARY in the last sentence
motion_pattern = re.compile(r'\bSTATIONARY\s*.\s*')
if motion_pattern.search(event_text) is not None:
bearing = converted_speed = 0
else:
bearing = converted_speed = None
# TODO: Storm location: This require parsing (potentially) multiple
# references to cities (or marine locations) with distance/bearing
# offsets. Need access to geospatial information to get the actual
# lat/lons.
if bearing is not None and converted_speed is not None:
return (bearing, converted_speed, None)
else:
return None
def _calcTime(self, yymmdd, hhmm, allZeroValue):
#Returns tuple of time, and allZeroFlag. Time is based on the
#two time strings. If all zeros, then return allZeroValue
if yymmdd == "000000" and hhmm == "0000":
return (allZeroValue, True)
else:
timeString = yymmdd + hhmm
timeTuple = time.strptime(timeString, "%y%m%d%H%M")
return (long(calendar.timegm(timeTuple) * 1000), False)
def _expandVTEC(self, ugcstring, vtecStrings, segment, segmentText, cities,
purgeTime):
#Routine takes a list of vtec strings and expands them out into
#the format of the active table.
#Returns the records.
ugcs = self._expandUGC(ugcstring)
records = []
for vtecS, hvtec in vtecStrings:
search = re.search(self._vtecRE, vtecS)
#construct the active table entries, without the geography
template = {}
template['vtecstr'] = search.group(0)
template['etn'] = search.group(5)
template['sig'] = search.group(4)
template['phen'] = search.group(3)
template['segText'] = segmentText
template['overviewText'] = self._overviewText
template['phensig'] = template['phen'] + '.' + template['sig']
template['act'] = search.group(1)
template['seg'] = segment
startTime, zeros = self._calcTime(search.group(6),
search.group(7), self._issueTime * 1000)
endTime, ufn = self._calcTime(search.group(8),
search.group(9), self._maxFutureTime * 1000)
template['startTime'] = long(startTime)
template['endTime'] = long(endTime)
template['ufn'] = ufn
template['officeid'] = search.group(2)
template['purgeTime'] = long(self._dtgFromDDHHMM(purgeTime, self._issueTime)*1000)
template['issueTime'] = long(self._issueTime * 1000)
template['state'] = "Decoded"
template['xxxid'] = self._completeProductPil[3:]
if ( self._hasDTG ):
template['countyheader'] = ugcstring[:-8]
else:
template['countyheader'] = ugcstring
if self._productPil[:3] in ACCURATE_CITIES_PILS:
template['cities'] = cities
#remap pil if in mappedPils table to relate events that are
#issued in one product, and updated in another product
template['pil'] = self._remapPil(template['phen'],
template['sig'], self._productPil)
template['ugcZoneList'] = ", ".join(ugcs)
state = ugcstring[0:2]
if REGIONS.has_key(state):
template['region'] = REGIONS[state]
else:
template['region'] = 'DEFAULT'
template['wmoid'] = self._wmoid
template['productClass'] = template['vtecstr'][1]
template['geometry'] = self._polygon
try:
from com.raytheon.uf.common.time import DataTime, TimeRange
valid = TimeRange(long(startTime), long(endTime))
template['dataTime'] = DataTime(long(startTime), valid)
except:
template['dataTime'] = long(endTime)
template['rawmessage'] = self._rawMessage
if self._storm is not None:
template['motdir'] = self._storm[0]
template['motspd'] = self._storm[1]
template['loc'] = self._storm[2]
if hvtec is not None:
hsearch = re.search(self._hVtecRE, hvtec)
template['locationID'] = hsearch.group(1)
template['floodSeverity'] = hsearch.group(2)
template['immediateCause'] = hsearch.group(3)
template['floodRecordStatus'] = hsearch.group(10)
if template['phensig'] in PHENSIGS_IGNORE_HVTEC:
fakeBeginTime = None
fakeEndTime = None
fakeCrestTime = None
else:
fakeBeginTime = template['issueTime']
fakeEndTime = long(self._maxFutureTime * 1000)
fakeCrestTime = fakeEndTime
template['floodBegin'] = self._calcTime(hsearch.group(4), hsearch.group(5), fakeBeginTime)[0]
template['floodCrest'] = self._calcTime(hsearch.group(6), hsearch.group(7), fakeCrestTime)[0]
template['floodEnd'] = self._calcTime(hsearch.group(8), hsearch.group(9), fakeEndTime)[0]
records.append(template)
#expand the template out by the ugcs
#for geo in ugcs:
# dict = copy.deepcopy(template)
# dict['id'] = geo
# records.append(dict)
return records
def _remapPil(self, phen, sig, pil):
# remaps the product pil for certain phen/sig/pils. The VTECDecoder
# needs to relate hazards through all states from the same pil. Some
# short-fused hazards issue in one pil and followup/cancel in
# another pil.
key = (phen, sig, pil)
rPil = self._mappedPils.get(key, pil)
if rPil != pil:
LogStream.logEvent("Remapped Pil", key, "->", rPil)
return rPil
def _prepSegmentText(self, lines):
# eliminate leading and trailing blank lines from the set of
# lines, the joins the lines and returns one long string.
# eliminate leading blank lines
while len(lines) and len(lines[0]) == 0:
del lines[0]
# eliminate trailing blank lines
while len(lines) and len(lines[-1]) == 0:
del lines[-1]
# assemble into long string
return string.join(lines, '\n')
# time contains, if time range (tr) contains time (t), return 1
def __containsT(self, tr, t):
return (t >= tr[0] and t < tr[1])
# time overlaps, if tr1 overlaps tr2 (adjacent is not an overlap)
def __overlaps(self, tr1, tr2):
if self.__containsT(tr2, tr1[0]) or self.__containsT(tr1, tr2[0]):
return 1
return 0
class NoVTECWarningDecoder(StdWarningDecoder):
#this is a special case, we do not want to make changes to this class that will break parsing
#warnings that do have vtec strings. If needed all non-vtec specific code should be split out
#into new classes
def _makeRecordWithoutVTEC(self, ugcstring, vtecStrings, segment, segmentText, cities, purgeTime, ugcs):
records = []
#for vtecS, hvtec in vtecStrings:
#search = re.search(self._vtecRE, vtecS)
#construct the active table entries, without the geography
template = {}
#template['vtecstr'] = search.group(0)
#template['etn'] = search.group(5)
#template['sig'] = search.group(4)
#template['phen'] = search.group(3)
template['segText'] = segmentText
template['overviewText'] = self._overviewText
#template['phensig'] = template['phen'] + '.' + template['sig']
#template['act'] = search.group(1)
template['seg'] = segment
#startTime, zeros = self._calcTime(search.group(6),
# search.group(7), self._issueTime * 1000)
startTime = self._issueTime * 1000
#endTime, ufn = self._calcTime(search.group(8),
# search.group(9), self._maxFutureTime * 1000)
ddhhmmz = ugcstring[-7:-1]
endTime = self._dtgFromDDHHMM(ddhhmmz) * 1000
template['startTime'] = long(startTime)
template['endTime'] = long(endTime)
#if ufn:
template['ufn'] = True
#else:
# template['ufn'] = False
#template['officeid'] = search.group(2)
if self._officeFromWMO:
template['officeid'] = self._officeFromWMO
template['purgeTime'] = long(self._dtgFromDDHHMM(purgeTime, self._issueTime)*1000)
template['issueTime'] = long(self._issueTime * 1000)
template['state'] = "Decoded"
template['xxxid'] = self._completeProductPil[3:]
template['countyheader'] = ugcstring[:-8]
if self._productPil[:3] in ACCURATE_CITIES_PILS:
template['cities'] = cities
#remap pil if in mappedPils table to relate events that are
#issued in one product, and updated in another product
#template['pil'] = self._remapPil(template['phen'],
# template['sig'], self._productPil)
template['pil'] = self._productPil
template['ugcs'] = ugcs
state = ugcstring[0:2]
if REGIONS.has_key(state):
template['region'] = REGIONS[state]
else:
template['region'] = 'DEFAULT'
template['wmoid'] = self._wmoid
#template['productClass'] = template['vtecstr'][1]
template['geometry'] = self._polygon
try:
from com.raytheon.uf.common.time import DataTime, TimeRange
valid = TimeRange(long(startTime), long(endTime))
template['dataTime'] = DataTime(long(startTime), valid)
except:
template['dataTime'] = long(endTime)
template['rawmessage'] = self._rawMessage
if self._storm is not None:
template['motdir'] = self._storm[0]
template['motspd'] = self._storm[1]
template['loc'] = self._storm[2]
#if hvtec is not None:
# hsearch = re.search(self._hVtecRE, hvtec)
# template['locationID'] = hsearch.group(1)
# template['floodSeverity'] = hsearch.group(2)
# template['immediateCause'] = hsearch.group(3)
# template['floodRecordStatus'] = hsearch.group(10)
# template['floodBegin'] = long(self._calcTime(hsearch.group(4), hsearch.group(5), self._issueTime)[0])
# template['floodCrest'] = long(self._calcTime(hsearch.group(6), hsearch.group(7), self._issueTime)[0])
# template['floodEnd'] = long(self._calcTime(hsearch.group(8), hsearch.group(9), self._issueTime)[0])
records.append(template)
#expand the template out by the ugcs
#for geo in ugcs:
# dict = copy.deepcopy(template)
# dict['id'] = geo
# records.append(dict)
return records
def _expandVTEC(self, ugcstring, vtecStrings, segment, segmentText, cities,
purgeTime):
#Routine takes a list of vtec strings and expands them out into
#the format of the active table.
#Returns the records.
ugcs = self._expandUGC(ugcstring)
return self._makeRecordWithoutVTEC(ugcstring, vtecStrings, segment, segmentText, cities, purgeTime, ugcs)
class WarningDecoder():
def __init__(self, text=None, filePath=None, command=None):
#hold on to text and filePath
self.text = text
self.filePath = filePath
self._vtecRE = r'/[OTEX]\.([A-Z]{3})\.([A-Z]{4})\.([A-Z]{2})\.' + \
r'([WAYSOFN])\.([0-9]{4})\.([0-9]{6})T([0-9]{4})Z\-' + \
r'([0-9]{6})T([0-9]{4})Z/'
self._hVtecRE = r'/([0-9A-Z]{5})\.([0-3NU])\.([A-Z]{2})\.' + \
r'([0-9]{6})T([0-9]{4})Z\.' + \
r'([0-9]{6})T([0-9]{4})Z\.([0-9]{6})T([0-9]{4})Z\.([A-Z][A-Z])/'
self._badVtecRE = r'^\s?/.*/\s?$'
self._stdWarningDecode = None
if str == type(command):
command = command.split()
if command and str == type(command[0]) and command[0].startswith('-'):
self._command = [''] + command
else:
self._command = command
def decode(self):
#discover which type of warning to decode
self._checkForVTEC()
#create appropraite class and decode
if ( self._stdWarningDecode ):
#print "using standard warning decoder reason: ", self._stdWarningDecode
decoder = StdWarningDecoder(self.text, self.filePath, self._command)
return decoder.decode()
else:
#print "using no vtec warning decoder"
decoder = NoVTECWarningDecoder(self.text, self.filePath, self._command)
return decoder.decode()
def _checkForVTEC(self):
contents = ""
if ( self.text ):
contents = self.text
elif (self.filePath):
fd = open(self.filePath, 'r')
contents = fd.read()
fd.close()
else:
#define self._stdWarningDecode as something so the std decoder is used
self._stdWarningDecode = 'no file or text'
return
#search through contents to find a vtec string
lines = string.split(contents, '\n')
for line in lines:
if re.search(self._vtecRE, line):
self._stdWarningDecode = 'found a vtec string'
return
elif (re.search(self._badVtecRE, line) \
and not re.search(self._hVtecRE, line)):
self._stdWarningDecode = 'found bad vtec string'
return
if __name__ == "__main__":
sys.exit(0)