2072 lines
80 KiB
Python
2072 lines
80 KiB
Python
##
|
|
# 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 software is in the public domain, furnished "as is", without technical
|
|
# support, and with no warranty, express or implied, as to its usefulness for
|
|
# any purpose.
|
|
#
|
|
# DiscretePhrases.py
|
|
# Methods for producing text forecast from SampleAnalysis statistics.
|
|
#
|
|
# Author: hansen
|
|
# ----------------------------------------------------------------------------
|
|
#
|
|
# SOFTWARE HISTORY
|
|
#
|
|
# Date Ticket# Engineer Description
|
|
# ------------- -------- --------- --------------------------------------------
|
|
# Apr 28, 2015 4027 randerso Changes for mixed case. Added sort for
|
|
# consistent ordering of multiple timezones
|
|
# Oct 02, 2017 20335 ryu Add storm surge w/w to tpcEvents so no timing
|
|
# phrase appear in headline
|
|
# Nov 16, 2018 20613 smoorthy Added extra vtecstr field in organizeHazards()
|
|
# to prevent similar segments from incorrectly
|
|
# combining
|
|
# Apr 29, 2020 8151 randerso Use SiteMap.getSite4LetterId()
|
|
#
|
|
##
|
|
|
|
##
|
|
# This is a base file that is not intended to be overridden.
|
|
##
|
|
|
|
import functools
|
|
import PhraseBuilder
|
|
import ModuleAccessor
|
|
import types, copy, time, os
|
|
import SampleAnalysis
|
|
import TimeRange, AbsTime
|
|
|
|
|
|
class DiscretePhrases(PhraseBuilder.PhraseBuilder):
|
|
|
|
def __init__(self):
|
|
PhraseBuilder.PhraseBuilder.__init__(self)
|
|
|
|
### Local non-VTEC headlines.
|
|
# To sample the Hazards grid and produce locally generated headlines
|
|
# independent of the VTEC Headlines structure, follow these steps:
|
|
#
|
|
# 1. Put an "allowedHeadlines" method into your product with the
|
|
# same format as the "allowedHazards" method.
|
|
#
|
|
# 2. Generate the headlines using "generateProduct" in, for example, the
|
|
# _preProcessArea method:
|
|
|
|
# headlines = self.generateProduct("Headlines", argDict, area = editArea,
|
|
# areaLabel=areaLabel,
|
|
# timeRange = self._timeRange)
|
|
# fcst = fcst + headlines
|
|
#
|
|
# 3. If desired, override "headlinesTiming" to adjust or remove the time descriptors
|
|
# for the headline.
|
|
#
|
|
|
|
def headlinesTiming(self, tree, node, key, timeRange, areaLabel, issuanceTime):
|
|
# Return
|
|
# "startPhraseType" and "endPhraseType"
|
|
# Each can be one of these phraseTypes:
|
|
# "EXPLICIT" will return words such as "5 PM"
|
|
# "FUZZY4" will return words such as "THIS EVENING"
|
|
# "DAY_NIGHT_ONLY" use only weekday or weekday "NIGHT" e.g.
|
|
# "SUNDAY" or "SUNDAY NIGHT" or "TODAY" or "TONIGHT"
|
|
# Note: You will probably want to set both the
|
|
# startPhraseType and endPhraseType to DAY_NIGHT_ONLY to
|
|
# have this work correctly.
|
|
# "NONE" will result in no words
|
|
# OR a method which takes arguments:
|
|
# issueTime, eventTime, timeZone, and timeType
|
|
# and returns:
|
|
# phraseType, (hourStr, hourTZstr, description)
|
|
# You can use "timingWordTableFUZZY8" as an example to
|
|
# write your own method.
|
|
#
|
|
# If you simply return None, no timing words will be used.
|
|
|
|
# Note that you can use the information given to determine which
|
|
# timing phrases to use. In particular, the "key" is the Hazard
|
|
# key so different local headlines can use different timing.
|
|
#
|
|
startPhraseType = "FUZZY"
|
|
endPhraseType = "FUZZY"
|
|
|
|
#Example code
|
|
#startTime = timeRange.startTime().unixTime()
|
|
#if startTime <= issuanceTime + 12 * 3600: # 12 hours past issuance
|
|
#startPhraseType = "EXPLICIT"
|
|
#endTime = timeRange.endTime().unixTime()
|
|
#if endTime <= issuanceTime + 12 * 3600: # 12 hours past issuance
|
|
#endPhraseType = "EXPLICIT"
|
|
|
|
#return startPhraseType, endPhraseType
|
|
return None, None
|
|
|
|
def Headlines(self):
|
|
return {
|
|
"type": "component",
|
|
"lineLength": 69,
|
|
"methodList": [
|
|
self.assembleChildWords,
|
|
self.wordWrap,
|
|
],
|
|
"analysisList":[
|
|
("Hazards",
|
|
SampleAnalysis.SampleAnalysis().discreteTimeRangesByKey),
|
|
],
|
|
|
|
"phraseList":[
|
|
self.headlines_phrase,
|
|
],
|
|
"autoSentence": 0,
|
|
}
|
|
|
|
def headlines_phrase(self):
|
|
return {
|
|
"setUpMethod": self.headlines_setUp,
|
|
"wordMethod": self.headlines_words,
|
|
"phraseMethods": [self.assembleSubPhrases,
|
|
self.postProcessPhrase,
|
|
]
|
|
}
|
|
|
|
def headlines_setUp(self, tree, node):
|
|
self.subPhraseSetUp(tree, node, [], self.scalarConnector)
|
|
return self.DONE()
|
|
|
|
def headlines_words(self, tree, node):
|
|
"Create the phrase for local headlines from the Hazards grids"
|
|
|
|
words = ""
|
|
areaLabel = tree.getAreaLabel()
|
|
headlines = tree.stats.get("Hazards", tree.getTimeRange(),
|
|
areaLabel, mergeMethod="List")
|
|
if headlines is None:
|
|
return self.setWords(node, "")
|
|
|
|
# Sort the headlines by startTime
|
|
temp = []
|
|
for h, tr in headlines:
|
|
temp.append((tr.startTime(), (h, tr)))
|
|
temp.sort()
|
|
newList = []
|
|
for t in temp:
|
|
newList.append(t[1])
|
|
headlines = newList
|
|
|
|
# Fetch the set of local headlines allowed for this product
|
|
allowedHeadlines = []
|
|
for key, allActions, cat in self.allowedHeadlines():
|
|
allowedHeadlines.append(key)
|
|
issuanceTime = self._issueTime.unixTime()
|
|
|
|
from com.raytheon.uf.viz.core.localization import LocalizationManager
|
|
siteId = LocalizationManager.getInstance().getSite()
|
|
for key, tr in headlines: # value == list of subkeys
|
|
if key not in allowedHeadlines:
|
|
continue
|
|
|
|
timeDescriptor = self.headlinesTimeRange_descriptor(
|
|
tree, node, key, tr, areaLabel, issuanceTime)
|
|
from com.raytheon.uf.common.dataplugin.gfe.discrete import DiscreteKey
|
|
headlineWords = DiscreteKey.discreteDefinition(siteId).keyDesc(
|
|
"Hazards" + "_SFC", key)
|
|
if headlineWords == "": # Don't process the "<None>" key
|
|
continue
|
|
hookWords = self.hazard_hook(tree, node, key, "", "", tr.startTime(), tr.endTime())
|
|
headlinePhrase = "..." + headlineWords + timeDescriptor + hookWords + "...\n"
|
|
words = words + headlinePhrase
|
|
|
|
words = self.convertToUpper(words)
|
|
return self.setWords(node, words)
|
|
|
|
def headlinesTimeRange_descriptor(self, tree, node, key, tr, areaLabel, issuanceTime):
|
|
# Return a time range descriptor for the headline
|
|
# This method can be overridden to customize timing descriptors for
|
|
# non-VTEC local headlines
|
|
|
|
headlinesTiming = self.headlinesTiming(tree, node, key, tr,
|
|
areaLabel, issuanceTime)
|
|
if headlinesTiming is None:
|
|
return ""
|
|
try:
|
|
startPhraseType, endPhraseType = headlinesTiming
|
|
except:
|
|
# For backward compatibility -- the startBoundary argument
|
|
# was formerly part of the headlinesTiming method
|
|
startPhraseType, endPhraseType, startBoundary = headlinesTiming
|
|
startTime = tr.startTime().unixTime()
|
|
endTime = tr.endTime().unixTime()
|
|
tree.combinations = self._combinations
|
|
areaList = self.getCurrentAreaNames(tree, areaLabel)
|
|
hazRec = {
|
|
'id': areaList,
|
|
'startTime': startTime,
|
|
'endTime': endTime,
|
|
'act': "NEW",
|
|
}
|
|
if startPhraseType == "FUZZY":
|
|
startPhraseType = "FUZZY4"
|
|
if endPhraseType == "FUZZY":
|
|
endPhraseType = "FUZZY4"
|
|
|
|
phrase = self.getTimingPhrase(
|
|
hazRec, issuanceTime, startPhraseType, endPhraseType)
|
|
return " " + phrase
|
|
|
|
############################################################################################
|
|
### WARNING!!!! VTEC CODE -- DO NOT OVERRIDE ANY CODE BELOW THIS POINT!!!!!
|
|
|
|
### IF YOU USE A METHOD BELOW THIS POINT AND WANT TO ALTER IT,
|
|
### COPY IT TO YOUR LOCAL FILE AND RE-NAME IT. THEN OVERRIDE ANY
|
|
### METHODS THAT CALL IT AND USE THE NEW NAME.
|
|
|
|
def getHazards(self, argDict, areaList):
|
|
# This is for setting up the argDict hazards entry AFTER the TextFormatter
|
|
# has created the Hazards Table.
|
|
# This is necessary for products that allow the user to specify through
|
|
# the GUI which edit areas will be sampled.
|
|
# Set up edit areas
|
|
editAreas = []
|
|
for area, label in areaList:
|
|
editAreas.append([area])
|
|
# Process the hazards
|
|
import HazardsTable
|
|
hazards = HazardsTable.HazardsTable(
|
|
argDict["ifpClient"], editAreas, self._pil[0:3],
|
|
self.filterMethod, argDict["databaseID"],
|
|
self._fullStationID,
|
|
activeTableName=argDict['vtecActiveTable'],
|
|
vtecMode=argDict['vtecMode'],
|
|
dataMgr=argDict['dataMgr'])
|
|
# Store hazards object for later use
|
|
argDict["hazards"] = hazards
|
|
|
|
def Hazards(self):
|
|
return {
|
|
"type": "component",
|
|
"lineLength": 66,
|
|
"methodList": [
|
|
self.assembleChildWords,
|
|
self.wordWrap,
|
|
],
|
|
"analysisList":[],
|
|
|
|
"phraseList":[
|
|
self.hazards_phrase,
|
|
],
|
|
"autoSentence": 0,
|
|
}
|
|
|
|
def hazards_phrase(self):
|
|
return {
|
|
"setUpMethod": self.hazards_setUp,
|
|
"wordMethod": self.hazards_words,
|
|
"phraseMethods": self.standard_phraseMethods(),
|
|
}
|
|
|
|
def hazards_setUp(self, tree, node):
|
|
self.subPhraseSetUp(tree, node, [], self.scalarConnector)
|
|
|
|
return self.DONE()
|
|
|
|
def hazards_words(self, tree, node):
|
|
"Create the phrase for any watches, warnings or advisories"
|
|
hazardsTable = self._hazards
|
|
tree.combinations = self._combinations
|
|
if self._combinations is None:
|
|
areaLabel = None
|
|
else:
|
|
areaLabel = tree.getAreaLabel()
|
|
|
|
editAreas = self.getCurrentAreaNames(tree, areaLabel)
|
|
try:
|
|
# Remove suffixes if necessary
|
|
if self._editAreaSuffix is not None:
|
|
editAreas = self.removeSuffixes(editAreas, self._editAreaSuffix)
|
|
except:
|
|
pass
|
|
|
|
# Check for a particular entry in argDict that is inserted when
|
|
# we're formatting hazards type products like WSW, NPW.
|
|
argDict = tree.get("argDict")
|
|
# look for segmentAreas in the argDict and override editAreas
|
|
if "segmentAreas" in argDict:
|
|
editAreas = argDict['segmentAreas'] # override editAreas
|
|
|
|
words = self.getHazardString(tree, node, editAreas)
|
|
|
|
words = self.convertToUpper(words) # convert to upper case
|
|
|
|
return self.setWords(node, words)
|
|
|
|
##### VTEC methods #####
|
|
|
|
# Return just a simple list of hazards in the form phen.sig (WS.W)
|
|
def getAllowedHazardList(self):
|
|
allowedHazardList = self.allowedHazards()
|
|
|
|
hazardList = []
|
|
for h in allowedHazardList:
|
|
if type(h) is tuple:
|
|
hazardList.append(h[0])
|
|
else:
|
|
hazardList.append(h)
|
|
|
|
return hazardList
|
|
|
|
# Return the list of action codes given the hazard, if hazard not found
|
|
# or actions not specified, return "ALL codes"
|
|
|
|
def getAllowedActionCodes(self, hazard):
|
|
allowedHazardList = self.allowedHazards()
|
|
|
|
for h in allowedHazardList:
|
|
if type(h) is tuple:
|
|
if h[0] == hazard:
|
|
return h[1]
|
|
return ["NEW", "EXA", "EXB", "EXT", "UPG", "CAN", "CON", "EXP"]
|
|
|
|
# Returns the words to be used in the headline for 'act' field in the
|
|
# specified hazard.
|
|
def actionControlWord(self, hazard, issuanceTime):
|
|
if 'act' not in hazard:
|
|
print("Error! No field act in hazard record.")
|
|
return "<noaction>"
|
|
|
|
actionCode = hazard['act']
|
|
if actionCode in ["NEW", "EXA", "EXB"]:
|
|
return "in effect"
|
|
elif actionCode == "CON":
|
|
return "remains in effect"
|
|
elif actionCode == "CAN":
|
|
return "is cancelled"
|
|
elif actionCode == "EXT":
|
|
return "now in effect"
|
|
elif actionCode == "EXP":
|
|
deltaTime = issuanceTime - hazard['endTime']
|
|
if deltaTime >= 0:
|
|
return "has expired"
|
|
else:
|
|
return "will expire"
|
|
elif actionCode == "UPG":
|
|
return "no longer in effect"
|
|
else:
|
|
print(actionCode, "not recognized in actionControlWord.")
|
|
return "<actionControlWord>"
|
|
|
|
#
|
|
# Determine the category for Hazard overrides
|
|
#
|
|
def getHazardCategory(self, hazard):
|
|
allowedHazardList = self.allowedHazards()
|
|
|
|
for h in allowedHazardList:
|
|
if h[0] == hazard:
|
|
if len(h) == 3:
|
|
if type(h[2]) is str:
|
|
return h[2]
|
|
elif len(h) == 4:
|
|
if type(h[3]) is str:
|
|
return h[3]
|
|
|
|
return None
|
|
#
|
|
# Determine the priority of a Hazard (lower count = higher priority)
|
|
#
|
|
|
|
def getHazardImportance(self, hazard):
|
|
allowedHazardList = self.allowedHazards()
|
|
count = 0
|
|
for h in allowedHazardList:
|
|
count = count + 1
|
|
if h[0] == hazard:
|
|
return count
|
|
|
|
return 1000 # no priority
|
|
|
|
# This method uses the allowedHazards() list to determine which
|
|
# hazardTable entry has the most important priority and removes
|
|
# the entry or piece thereof in place. Returns 1 if something was
|
|
# modified and 0 otherwise
|
|
def fixHazardConflict(self, index1, index2, hazardTable):
|
|
|
|
allowedHazardList = self.getAllowedHazardList()
|
|
phen1 = hazardTable[index1]['phen']
|
|
phen2 = hazardTable[index2]['phen']
|
|
sig1 = hazardTable[index1]['sig']
|
|
sig2 = hazardTable[index2]['sig']
|
|
act1 = hazardTable[index1]['act']
|
|
act2 = hazardTable[index2]['act']
|
|
haz1 = phen1 + "." + sig1
|
|
haz2 = phen2 + "." + sig2
|
|
ignoreList = ['CAN', 'EXP', 'UPG']
|
|
if haz1 in allowedHazardList and haz2 in allowedHazardList and \
|
|
act1 not in ignoreList and act2 not in ignoreList:
|
|
|
|
if (self.getHazardCategory(haz1) != self.getHazardCategory(haz2)) or \
|
|
self.getHazardCategory(haz1) is None or \
|
|
self.getHazardCategory(haz2) is None:
|
|
return 0
|
|
|
|
else:
|
|
return 0 # no changes were made
|
|
|
|
if self.getHazardImportance(haz1) < self.getHazardImportance(haz2):
|
|
lowIndex = index2
|
|
highIndex = index1
|
|
else:
|
|
lowIndex = index1
|
|
highIndex = index2
|
|
|
|
#
|
|
# Added to prevent a current lower TO.A from overiding a higher SV.A
|
|
#
|
|
|
|
if hazardTable[lowIndex]['phen'] == 'SV' and \
|
|
hazardTable[lowIndex]['sig'] == 'A' and \
|
|
hazardTable[highIndex]['phen'] == 'TO' and \
|
|
hazardTable[highIndex]['sig'] == 'A':
|
|
if (int(hazardTable[lowIndex]['etn']) > int(hazardTable[highIndex]['etn']) and
|
|
(int(hazardTable[highIndex]['etn']) - int(hazardTable[lowIndex]['etn'])) > 50):
|
|
lowIndexTemp = lowIndex
|
|
lowIndex = highIndex
|
|
highIndex = lowIndexTemp
|
|
|
|
lowStart = hazardTable[lowIndex]['startTime']
|
|
lowEnd = hazardTable[lowIndex]['endTime']
|
|
highStart = hazardTable[highIndex]['startTime']
|
|
highEnd = hazardTable[highIndex]['endTime']
|
|
|
|
# first check to see if high pri completely covers low pri
|
|
if highStart <= lowStart and highEnd >= lowEnd: # remove low priority
|
|
del hazardTable[lowIndex]
|
|
|
|
# next check to see if high pri lies within low pri
|
|
elif lowStart <= highStart and lowEnd >= highEnd: # high pri in middle
|
|
if lowStart < highStart:
|
|
h = copy.deepcopy(hazardTable[lowIndex])
|
|
# trim the early piece
|
|
hazardTable[lowIndex]['endTime'] = highStart
|
|
if lowEnd > highEnd:
|
|
# make a new end piece
|
|
h['startTime'] = highEnd
|
|
hazardTable.append(h)
|
|
elif lowStart == highStart:
|
|
hazardTable[lowIndex]['startTime'] = highEnd
|
|
|
|
elif highEnd >= lowStart:
|
|
hazardTable[lowIndex]['startTime'] = highEnd # change low start
|
|
|
|
elif highStart <= lowEnd:
|
|
hazardTable[lowIndex]['endTime'] = highStart # change low end
|
|
|
|
return 1
|
|
|
|
# This method removes all entries of the specified hazardTable that
|
|
# are not in the allowedHazards list.
|
|
def filterAllowedHazards(self, hazardTable):
|
|
|
|
newTable = []
|
|
allowedHazardList = self.getAllowedHazardList()
|
|
|
|
hazStr = ""
|
|
for i in range(len(hazardTable)):
|
|
if hazardTable[i]['sig'] != "": # VTEC
|
|
hazStr = hazardTable[i]['phen'] + "." + hazardTable[i]['sig']
|
|
else: #non-VTEC
|
|
hazStr = hazardTable[i]['phen']
|
|
|
|
if hazStr in allowedHazardList:
|
|
newTable.append(hazardTable[i])
|
|
return newTable
|
|
|
|
# This method searches all entries of the specified hazardTable for
|
|
# entries matching the specified zone. Then for each entry it finds
|
|
# it looks for a conflicting entry in time. If it finds one, it calls
|
|
# fixHazardsConflict, which fixes the table and then calls itself again
|
|
# recursively with the fixed table. If it doesn't find one it returns
|
|
# None.
|
|
def filterZoneHazards(self, zone, hazardTable):
|
|
for i in range(len(hazardTable)):
|
|
if hazardTable[i]['id'] == zone:
|
|
for j in range(len(hazardTable)):
|
|
if hazardTable[j]['id'] == zone and i != j:
|
|
tr1 = TimeRange.TimeRange(
|
|
AbsTime.AbsTime(int(hazardTable[i]['startTime'])),
|
|
AbsTime.AbsTime(int(hazardTable[i]['endTime'])))
|
|
tr2 = TimeRange.TimeRange(
|
|
AbsTime.AbsTime(int(hazardTable[j]['startTime'])),
|
|
AbsTime.AbsTime(int(hazardTable[j]['endTime'])))
|
|
if tr1.overlaps(tr2):
|
|
if self.fixHazardConflict(i, j, hazardTable):
|
|
self.filterZoneHazards(zone, hazardTable)
|
|
return None
|
|
return None
|
|
|
|
# Main method that drives the code to filter hazards that conflict in time.
|
|
# Only one hazard of the same phenomenon is allowed per zone per time.
|
|
# This method processes the table, removing any time conflicts, so the one
|
|
# hazard per zone, time rule is adhered to.
|
|
def filterMethod(self, hazardTable, allowedHazardsOnly=False):
|
|
# Remove hazards not in allowedHazards list
|
|
newTable = self.filterAllowedHazards(hazardTable)
|
|
if allowedHazardsOnly:
|
|
return newTable
|
|
|
|
# get a raw list of unique edit areas
|
|
zoneList = []
|
|
for t in newTable:
|
|
if t['id'] not in zoneList:
|
|
zoneList.append(t['id'])
|
|
|
|
for zone in zoneList:
|
|
# Remove lower priority hazards of the same type
|
|
self.filterZoneHazards(zone, newTable)
|
|
|
|
return newTable
|
|
|
|
# function returns the timing phrase to use for the area, hazard,
|
|
# and issuance time. Can force the type of timing phrase given the
|
|
# stype and etype. The stype/etype may be: NONE, EXPLICIT, FUZZY4,
|
|
# FUZZY8, or DAY_NIGHT_ONLY. Returns phrase like:
|
|
# FROM 4 PM MST THIS AFTERNOON THROUGH TUESDAY EVENING
|
|
def getTimingPhrase(self, hazRec, issueTime, stype=None, etype=None):
|
|
#Returns the timing phrase to use
|
|
|
|
# Get the timing type
|
|
if stype is None or etype is None:
|
|
stype, etype = self.getTimingType(hazRec, issueTime)
|
|
|
|
# Get the time zones for the areas
|
|
timeZones = self.hazardTimeZones(hazRec['id'])
|
|
|
|
# Get the starting time
|
|
stext = []
|
|
if type(stype) is types.MethodType:
|
|
for tz in timeZones:
|
|
newType, info = stype(
|
|
issueTime, hazRec['startTime'], tz, "start")
|
|
if info is not None and info not in stext:
|
|
stext.append(info)
|
|
stype = newType
|
|
elif stype == "EXPLICIT":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableEXPLICIT(issueTime,
|
|
hazRec['startTime'], tz, "start")
|
|
if info not in stext:
|
|
stext.append(info)
|
|
elif stype == "FUZZY4":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableFUZZY4(issueTime,
|
|
hazRec['startTime'], tz, "start")
|
|
if info not in stext:
|
|
stext.append(info)
|
|
elif stype == "FUZZY8":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableFUZZY8(issueTime,
|
|
hazRec['startTime'], tz, "start")
|
|
if info not in stext:
|
|
stext.append(info)
|
|
elif stype == "DAY_NIGHT_ONLY":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableDAYNIGHT(issueTime,
|
|
hazRec['startTime'], tz, "start")
|
|
if info not in stext:
|
|
stext.append(info)
|
|
|
|
# Get the ending time
|
|
etext = []
|
|
if type(etype) is types.MethodType:
|
|
for tz in timeZones:
|
|
newType, info = etype(
|
|
issueTime, hazRec['endTime'], tz, "end")
|
|
if info is not None and info not in etext:
|
|
etext.append(info)
|
|
etype = newType
|
|
elif etype == "EXPLICIT":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableEXPLICIT(issueTime,
|
|
hazRec['endTime'], tz, "end")
|
|
if info not in etext:
|
|
etext.append(info)
|
|
elif etype == "FUZZY4":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableFUZZY4(issueTime,
|
|
hazRec['endTime'], tz, "end")
|
|
if info not in etext:
|
|
etext.append(info)
|
|
elif etype == "FUZZY8":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableFUZZY8(issueTime,
|
|
hazRec['endTime'], tz, "end")
|
|
if info not in etext:
|
|
etext.append(info)
|
|
elif etype == "DAY_NIGHT_ONLY":
|
|
for tz in timeZones:
|
|
info = self.timingWordTableDAYNIGHT(issueTime,
|
|
hazRec['endTime'], tz, "end")
|
|
if info not in etext:
|
|
etext.append(info)
|
|
|
|
# timing connection types
|
|
startPrefix, endPrefix = self.getTimingConnectorType((stype, etype),
|
|
hazRec['act'])
|
|
|
|
# get the timing phrase
|
|
phrase = self.calculateTimingPhrase(stype, etype, stext, etext,
|
|
startPrefix, endPrefix)
|
|
|
|
return phrase
|
|
|
|
# calculates the timing phrase based on the timing type, the calculated
|
|
# timing words, and the prefixes
|
|
def calculateTimingPhrase(self, stype, etype, stext, etext, startPrefix,
|
|
endPrefix):
|
|
|
|
if (stype, etype) == ("NONE", "NONE"):
|
|
return "" #no timing phrase
|
|
|
|
elif (stype, etype) in [("NONE", "EXPLICIT")]:
|
|
return self.ctp_NONE_EXPLICIT(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("NONE", "FUZZY4"), ("NONE", "FUZZY8")]:
|
|
return self.ctp_NONE_FUZZY(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("EXPLICIT", "EXPLICIT")]:
|
|
return self.ctp_EXPLICIT_EXPLICIT(stext, etext, startPrefix,
|
|
endPrefix)
|
|
|
|
elif (stype, etype) in [("EXPLICIT", "FUZZY4"), ("EXPLICIT", "FUZZY8")]:
|
|
return self.ctp_EXPLICIT_FUZZY(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("FUZZY4", "FUZZY4"), ("FUZZY8", "FUZZY4"),
|
|
("FUZZY4", "FUZZY8"), ("FUZZY8", "FUZZY8")]:
|
|
return self.ctp_FUZZY_FUZZY(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("NONE", "DAY_NIGHT_ONLY")]:
|
|
return self.ctp_NONE_DAYNIGHT(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("EXPLICIT", "DAY_NIGHT_ONLY")]:
|
|
return self.ctp_EXPLICIT_DAYNIGHT(stext, etext, startPrefix,
|
|
endPrefix)
|
|
|
|
elif (stype, etype) in [("FUZZY4", "DAY_NIGHT_ONLY"),
|
|
("FUZZY8", "DAY_NIGHT_ONLY")]:
|
|
return self.ctp_FUZZY_DAYNIGHT(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("DAY_NIGHT_ONLY", "DAY_NIGHT_ONLY")]:
|
|
return self.ctp_DAYNIGHT_DAYNIGHT(stext, etext, startPrefix,
|
|
endPrefix)
|
|
|
|
elif (stype, etype) in [("DAY_NIGHT_ONLY", "NONE")]:
|
|
return self.ctp_DAYNIGHT_NONE(stext, etext, startPrefix, endPrefix)
|
|
|
|
elif (stype, etype) in [("DAY_NIGHT_ONLY", "EXPLICIT")]:
|
|
return self.ctp_DAYNIGHT_EXPLICIT(stext, etext, startPrefix,
|
|
endPrefix)
|
|
|
|
elif (stype, etype) in [("DAY_NIGHT_ONLY", "FUZZY4"),
|
|
("DAY_NIGHT_ONLY", "FUZZY8")]:
|
|
return self.ctp_DAYNIGHT_FUZZY(stext, etext, startPrefix, endPrefix)
|
|
|
|
else:
|
|
return "<UnknownPhraseType-" + stype + "/" + etype + ">"
|
|
|
|
#calculates the NONE/EXPLICIT timing phrase
|
|
def ctp_NONE_EXPLICIT(self, stext, etext, startPrefix, endPrefix):
|
|
#single time zone
|
|
if len(etext) == 1:
|
|
hourStr, hourTZstr, description = etext[0]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
return endPrefix + ' ' + hourStr + ' ' + hourTZstr + ' ' + \
|
|
description
|
|
|
|
#multiple time zones
|
|
elif len(etext) > 1:
|
|
hourStr, hourTZstr, description = etext[0]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = endPrefix + ' ' + hourStr + ' ' + hourTZstr + ' '
|
|
for x in range(1, len(etext)):
|
|
hourStr, hourTZstr, othDescription = etext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + description
|
|
return s
|
|
|
|
#calculates the NONE/FUZZY timing phrase
|
|
def ctp_NONE_FUZZY(self, stext, etext, startPrefix, endPrefix):
|
|
#returns phrase like: THROUGH THIS EVENING
|
|
hourStr, hourTZstr, description = etext[0] #ending text
|
|
s = endPrefix + ' ' + description
|
|
return s
|
|
|
|
#calculates the NONE/EXPLICIT timing phrase
|
|
def ctp_EXPLICIT_EXPLICIT(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like:
|
|
# FROM 2 AM WEDNESDAY TO 2 AM CST THURSDAY
|
|
# FROM 2 AM TO 5 AM CST THURSDAY
|
|
# FROM 2 AM CST /1 AM MST/ WEDNESDAY TO 2 AM CST /1 AM MST/ THURSDAY
|
|
# FROM 2 AM CST /1 AM MST/ TO 6 AM CST /5AM MST/ THURSDAY
|
|
|
|
shourStr, shourTZstr, sdescription = stext[0] #starting text
|
|
ehourStr, ehourTZstr, edescription = etext[0] #ending text
|
|
|
|
#special cases NOON
|
|
if shourStr == "12 PM":
|
|
shourStr = "noon"
|
|
|
|
#special cases NOON
|
|
if ehourStr == "12 PM":
|
|
ehourStr = "noon"
|
|
|
|
# special case EARLY THIS MORNING and THIS MORNING, replace with
|
|
# just THIS MORNING
|
|
if sdescription == "early this morning" and \
|
|
edescription == "this morning":
|
|
sdescription = "this morning" #combine two phrases
|
|
|
|
# single time zone, same time zone for start/end times - same day
|
|
if len(stext) == 1 and len(etext) == 1 and \
|
|
shourTZstr == ehourTZstr and sdescription == edescription:
|
|
return startPrefix + ' ' + shourStr + ' ' + endPrefix + ' ' + \
|
|
ehourStr + ' ' + ehourTZstr + ' ' + edescription
|
|
|
|
# single time zone, same time zone for start/end times - diff day
|
|
if len(stext) == 1 and len(etext) == 1 and \
|
|
shourTZstr == ehourTZstr and sdescription != edescription:
|
|
return startPrefix + ' ' + shourStr + ' ' + sdescription + \
|
|
' ' + endPrefix + ' ' + ehourStr + ' ' + ehourTZstr + \
|
|
' ' + edescription
|
|
|
|
# mult time zones, same day for start/end times
|
|
if sdescription == edescription:
|
|
s = startPrefix + ' ' + shourStr + ' ' + shourTZstr + ' '
|
|
for x in range(1, len(stext)):
|
|
hourStr, hourTZstr, description = stext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + endPrefix + ' ' + ehourStr + ' ' + ehourTZstr + ' '
|
|
for x in range(1, len(etext)):
|
|
hourStr, hourTZstr, description = etext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + edescription
|
|
return s
|
|
|
|
# mult time zones, different day for start/end times
|
|
else:
|
|
s = startPrefix + ' ' + shourStr + ' ' + shourTZstr + ' '
|
|
for x in range(1, len(stext)):
|
|
hourStr, hourTZstr, description = stext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + sdescription + ' ' + endPrefix + ' ' + ehourStr + \
|
|
' ' + ehourTZstr + ' '
|
|
for x in range(1, len(etext)):
|
|
hourStr, hourTZstr, description = etext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + edescription
|
|
return s
|
|
|
|
#calculates the NONE/EXPLICIT timing phrase
|
|
def ctp_EXPLICIT_FUZZY(self, stext, etext, startPrefix, endPrefix):
|
|
#returns phrase like:
|
|
# FROM 2 AM CST WEDNESDAY THROUGH LATE WEDNESDAY NIGHT
|
|
# FROM 2 AM CST /1 AM MST/ WEDNESDAY THROUGH LATE WEDNESDAY NIGHT
|
|
|
|
#start phrase
|
|
hourStr, hourTZstr, description0 = stext[0]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = startPrefix + ' ' + hourStr + ' ' + hourTZstr + ' '
|
|
for x in range(1, len(stext)):
|
|
hourStr, hourTZstr, description = stext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + description0 + ' '
|
|
|
|
#end phrase
|
|
hourStr, hourTZstr, description = etext[0]
|
|
s = s + endPrefix + ' ' + description
|
|
|
|
return s
|
|
|
|
#calculates the FUZZY/FUZZY timing phrase
|
|
def ctp_FUZZY_FUZZY(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like FROM THIS EVENING THROUGH LATE WEDNESDAY NIGHT
|
|
#return phrases like LATE WEDNESDAY NIGHT
|
|
|
|
hourStr, hourTZstr, s_description = stext[0] #starting text
|
|
hourStr, hourTZstr, e_description = etext[0] #ending text
|
|
|
|
#special case of description the same
|
|
if s_description == e_description:
|
|
return s_description
|
|
|
|
#normal case of different descriptions
|
|
s = startPrefix + ' ' + s_description + ' ' + endPrefix + ' ' + \
|
|
e_description
|
|
|
|
return s
|
|
|
|
def ctp_NONE_DAYNIGHT(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like THROUGH WEDNESDAY
|
|
|
|
hourStr, hourTZstr, e_description = etext[0] #ending text
|
|
|
|
s = endPrefix + ' ' + e_description
|
|
|
|
return s
|
|
|
|
def ctp_EXPLICIT_DAYNIGHT(self, stext, etext, startPrefix, endPrefix):
|
|
#returns phrase like:
|
|
# FROM 2 AM CST WEDNESDAY THROUGH WEDNESDAY
|
|
# FROM 2 AM CST /1 AM MST/ WEDNESDAY THROUGH WEDNESDAY
|
|
|
|
#start phrase
|
|
hourStr, hourTZstr, description0 = stext[0]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = startPrefix + ' ' + hourStr + ' ' + hourTZstr + ' '
|
|
for x in range(1, len(stext)):
|
|
hourStr, hourTZstr, description = stext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + description0 + ' '
|
|
|
|
#end phrase
|
|
hourStr, hourTZstr, description = etext[0]
|
|
s = s + endPrefix + ' ' + description
|
|
|
|
return s
|
|
|
|
def ctp_FUZZY_DAYNIGHT(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like FROM THIS EVENING THROUGH WEDNESDAY NIGHT
|
|
|
|
hourStr, hourTZstr, s_description = stext[0] #starting text
|
|
hourStr, hourTZstr, e_description = etext[0] #ending text
|
|
|
|
#special case of description the same
|
|
if s_description == e_description:
|
|
return s_description
|
|
|
|
#normal case of different descriptions
|
|
s = startPrefix + ' ' + s_description + ' ' + endPrefix + ' ' + \
|
|
e_description
|
|
|
|
return s
|
|
|
|
def ctp_DAYNIGHT_DAYNIGHT(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like FROM TONIGHT THROUGH WEDNESDAY
|
|
|
|
hourStr, hourTZstr, s_description = stext[0] #starting text
|
|
hourStr, hourTZstr, e_description = etext[0] #ending text
|
|
|
|
#special case of description the same
|
|
if s_description == e_description:
|
|
return s_description
|
|
|
|
#normal case of different descriptions
|
|
s = startPrefix + ' ' + s_description + ' ' + endPrefix + ' ' + \
|
|
e_description
|
|
|
|
return s
|
|
|
|
def ctp_DAYNIGHT_EXPLICIT(self, stext, etext, startPrefix, endPrefix):
|
|
#returns phrase like:
|
|
# FROM TUESDAY UNTIL 2 AM CST WEDNESDAY
|
|
# FROM TUESDAY UNTIL 2 AM CST /1 AM MST/ WEDNESDAY
|
|
|
|
#start phrase
|
|
hourStr, hourTZstr, description = stext[0]
|
|
s = startPrefix + ' ' + description + ' '
|
|
|
|
#end phrase
|
|
hourStr, hourTZstr, description0 = etext[0]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + endPrefix + ' ' + hourStr + ' ' + hourTZstr + ' '
|
|
for x in range(1, len(etext)):
|
|
hourStr, hourTZstr, description = etext[x]
|
|
#special cases NOON
|
|
if hourStr == "12 PM":
|
|
hourStr = "noon"
|
|
s = s + "/" + hourStr + ' ' + hourTZstr + "/ "
|
|
s = s + description0 + ' '
|
|
|
|
return s
|
|
|
|
def ctp_DAYNIGHT_NONE(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like FROM TONIGHT
|
|
|
|
hourStr, hourTZstr, s_description = stext[0] #starting text
|
|
|
|
s = startPrefix + ' ' + s_description
|
|
|
|
return s
|
|
|
|
def ctp_DAYNIGHT_FUZZY(self, stext, etext, startPrefix, endPrefix):
|
|
#return phrases like FROM TONIGHT THROUGH WEDNESDAY NIGHT
|
|
|
|
hourStr, hourTZstr, s_description = stext[0] #starting text
|
|
hourStr, hourTZstr, e_description = etext[0] #ending text
|
|
|
|
#special case of description the same
|
|
if s_description == e_description:
|
|
return s_description
|
|
|
|
#normal case of different descriptions
|
|
s = startPrefix + ' ' + s_description + ' ' + endPrefix + ' ' + \
|
|
e_description
|
|
|
|
return s
|
|
|
|
def getTimingConnectorType(self, timingType, action):
|
|
# Returns the start and end prefix for the given start and end phrase
|
|
# type and action code.
|
|
d = {("NONE", "NONE"): (None, None),
|
|
("NONE", "EXPLICIT"): (None, "until"),
|
|
("NONE", "FUZZY4"): (None, "through"),
|
|
("NONE", "FUZZY8"): (None, "through"),
|
|
("EXPLICIT", "EXPLICIT"): ("from", "to"),
|
|
("EXPLICIT", "FUZZY4"): ("from", "through"),
|
|
("EXPLICIT", "FUZZY8"): ("from", "through"),
|
|
("FUZZY4", "FUZZY4"): ("from", "through"),
|
|
("FUZZY4", "FUZZY8"): ("from", "through"),
|
|
("FUZZY8", "FUZZY4"): ("from", "through"),
|
|
("FUZZY8", "FUZZY8"): ("from", "through"),
|
|
("NONE", "DAY_NIGHT_ONLY"): (None, "through"),
|
|
("EXPLICIT", "DAY_NIGHT_ONLY"): ("from", "through"),
|
|
("FUZZY4", "DAY_NIGHT_ONLY"): ("from", "through"),
|
|
("FUZZY8", "DAY_NIGHT_ONLY"): ("from", "through"),
|
|
("DAY_NIGHT_ONLY", "DAY_NIGHT_ONLY"): ("from", "through"),
|
|
("DAY_NIGHT_ONLY", "NONE"): ("from", None),
|
|
("DAY_NIGHT_ONLY", "EXPLICIT"): ("from", "to"),
|
|
("DAY_NIGHT_ONLY", "FUZZY4"): ("from", "through"),
|
|
("DAY_NIGHT_ONLY", "FUZZY8"): ("from", "through"),
|
|
}
|
|
|
|
# special case for expirations.
|
|
if action == 'EXP':
|
|
return (None, "at")
|
|
|
|
return d.get(timingType, ("<startPrefix?>", "<endPrefix?>"))
|
|
|
|
def getTimingType(self, hazRec, issueTime):
|
|
#Returns the timing type based on the issuanceTime and hazard record
|
|
#Returns (startType, endType), which is NONE, EXPLICIT, FUZZY4, FUZZY8
|
|
|
|
# Get the local headlines customizable timing
|
|
tr = self.makeTimeRange(hazRec['startTime'], hazRec['endTime'])
|
|
locStart, locEnd = self.getLocalHeadlinesTiming(
|
|
None, None, hazRec['phen'], tr, hazRec['id'], issueTime)
|
|
|
|
#time from issuanceTime
|
|
deltaTstart = hazRec['startTime'] - issueTime #seconds past now
|
|
deltaTend = hazRec['endTime'] - issueTime #seconds past now
|
|
|
|
HR = 3600 #convenience constants
|
|
MIN = 60 #convenience constants
|
|
|
|
# record in the past, ignore
|
|
if deltaTend <= 0:
|
|
return ("NONE", "NONE")
|
|
|
|
# upgrades and cancels
|
|
if hazRec['act'] in ['UPG', 'CAN']:
|
|
return ("NONE", "NONE") #upgrades/cancels never get timing phrases
|
|
|
|
# expirations EXP codes are always expressed explictly, only end time
|
|
if hazRec['act'] == 'EXP':
|
|
return ('NONE', 'EXPLICIT')
|
|
|
|
phensig = hazRec['phen'] + '.' + hazRec['sig']
|
|
|
|
# SPC Watches always get explicit times, 3 hour start mention
|
|
spcWatches = ['TO.A', 'SV.A']
|
|
if phensig in spcWatches:
|
|
if deltaTstart < 3 * HR:
|
|
return ('NONE', 'EXPLICIT')
|
|
else:
|
|
return ('EXPLICIT', 'EXPLICIT')
|
|
|
|
# Tropical events never get times at all
|
|
tpcEvents = ['TY.A', 'TY.W', 'HU.A', 'HU.S', 'HU.W', 'TR.A', 'TR.W',
|
|
'SS.A', 'SS.W']
|
|
if phensig in tpcEvents:
|
|
return ('NONE', 'NONE')
|
|
|
|
# special marine case?
|
|
marineHazList = ["SC.Y", "SW.Y", "GL.W", "SR.W", 'HF.W', 'BW.Y',
|
|
'UP.W', 'UP.Y', 'RB.Y', 'SE.W', 'SI.Y'] #treat like watches
|
|
marinePils = ['CWF', 'OFF', 'NSH', 'GLF'] #specific marine pils
|
|
oconusSites = ['PGUM', 'PPPG', 'PHFO', 'PAFC', 'PAJK', 'PAFG']
|
|
|
|
# regular products - not marine
|
|
if hazRec['pil'] not in marinePils:
|
|
#advisories/warnings
|
|
if hazRec['sig'] in ['Y', 'W']: #advisories/warnings - explicit
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
else:
|
|
start = 'EXPLICIT' #explicit start time after 3 hours
|
|
end = 'EXPLICIT' #end time always explicit
|
|
|
|
#watches
|
|
elif hazRec['sig'] in ['A']: #watches - mix of explicit/fuzzy
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
#local hazards
|
|
elif locStart is not None and locEnd is not None:
|
|
start = locStart
|
|
end = locEnd
|
|
else:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
# marine - CONUS
|
|
elif hazRec['officeid'] not in oconusSites:
|
|
|
|
#advisories/warnings - explicit, but not some phensigs
|
|
if hazRec['sig'] in ['Y', 'W'] and phensig not in marineHazList:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
else:
|
|
start = 'EXPLICIT' #explicit start time after 3 hours
|
|
end = 'EXPLICIT' #end time always explicit
|
|
|
|
#watches - mix of explicit/fuzzy, some phensig treated as watches
|
|
elif hazRec['sig'] in ['A'] or phensig in marineHazList:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
#local hazards - treat as watches
|
|
elif locStart is not None and locEnd is not None:
|
|
start = locStart
|
|
end = locEnd
|
|
else:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
# marine - OCONUS
|
|
else:
|
|
|
|
#advisories/warnings - explicit, but not some phensigs
|
|
if hazRec['sig'] in ['Y', 'W'] and phensig not in marineHazList:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
else:
|
|
start = 'EXPLICIT' #explicit start time after 3 hours
|
|
end = 'EXPLICIT' #end time always explicit
|
|
|
|
#special marine phensigs - treat as watches, with fuzzy8
|
|
elif phensig in marineHazList:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
else:
|
|
start = 'FUZZY8' #fuzzy start times
|
|
end = 'FUZZY8' #always fuzzy end times
|
|
|
|
#regular watches - fuzzy4
|
|
elif hazRec['sig'] in ['A']:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
#local hazards - treat as watches
|
|
elif locStart is not None and locEnd is not None:
|
|
start = locStart
|
|
end = locEnd
|
|
else:
|
|
if deltaTstart < 3 * HR: #no start time in first 3 hours
|
|
start = 'NONE'
|
|
elif deltaTstart < 12 * HR:
|
|
start = 'EXPLICIT' #explicit start time 3-12 hours
|
|
else:
|
|
start = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
if deltaTend < 12 * HR: #explicit end time 0-12 hours
|
|
end = 'EXPLICIT'
|
|
else:
|
|
end = 'FUZZY4' #fuzzy times after 12 (4/day)
|
|
|
|
return (start, end)
|
|
|
|
def getLocalHeadlinesTiming(self, tree, node, key, tr,
|
|
areaLabel, issuanceTime):
|
|
headlinesTiming = self.headlinesTiming(tree, node, key, tr,
|
|
areaLabel, issuanceTime)
|
|
if headlinesTiming is None:
|
|
locStart = None
|
|
locEnd = None
|
|
else:
|
|
locStart, locEnd = headlinesTiming
|
|
if locStart == "FUZZY":
|
|
locStart = "FUZZY4"
|
|
if locEnd == "FUZZY":
|
|
locEnd = "FUZZY4"
|
|
return locStart, locEnd
|
|
|
|
def hazardTimeZones(self, areaList):
|
|
#returns list of time zones for the starting time
|
|
#and list of time zones for the ending time. The areaList provides
|
|
#a complete list of areas for this headline. startT, endT are the
|
|
#hazard times.
|
|
|
|
# sort the areaList so time zones are in consistent order
|
|
areaList.sort()
|
|
|
|
# get this time zone
|
|
thisTimeZone = os.environ["TZ"]
|
|
zoneList = []
|
|
|
|
# get the AreaDictionary that contains time zones per edit area
|
|
areaDictName = self._areaDictionary
|
|
accessor = ModuleAccessor.ModuleAccessor()
|
|
areaDict = accessor.variable(areaDictName, "AreaDictionary")
|
|
|
|
# check to see if we have any areas outside our time zone
|
|
for areaName in areaList:
|
|
if areaName in areaDict:
|
|
entry = areaDict[areaName]
|
|
if "ugcTimeZone" not in entry: #add your site id
|
|
if thisTimeZone not in zoneList:
|
|
zoneList.append(thisTimeZone)
|
|
continue # skip it
|
|
timeZoneList = entry["ugcTimeZone"]
|
|
if type(timeZoneList) is str: # a single value
|
|
timeZoneList = [timeZoneList] # make it into a list
|
|
for timeZone in timeZoneList:
|
|
if timeZone not in zoneList:
|
|
zoneList.append(timeZone)
|
|
|
|
# if the resulting zoneList is empty, put in our time zone
|
|
if len(zoneList) == 0:
|
|
zoneList.append(thisTimeZone)
|
|
|
|
# if the resulting zoneList has our time zone in it, be sure it
|
|
# is the first one in the list
|
|
try:
|
|
index = zoneList.index(thisTimeZone)
|
|
if index != 0:
|
|
del zoneList[index]
|
|
zoneList.insert(0, thisTimeZone)
|
|
except:
|
|
pass
|
|
|
|
return zoneList
|
|
|
|
def timingWordTableEXPLICIT(self, issueTime, eventTime, timezone,
|
|
timeType='start'):
|
|
#returns (timeValue, timeZone, descriptiveWord).
|
|
#eventTime is either the starting or ending time, based on
|
|
#the timeType flag. timezone is the time zone for the hazard area
|
|
|
|
HR = 3600
|
|
sameDay = [
|
|
(0 * HR, 6 * HR, "early this morning"), #midnght-559am
|
|
(6 * HR, 12 * HR - 1, "this morning"), #600am-1159am
|
|
(12 * HR, 12 * HR + 1, "today"), #noon
|
|
(12 * HR + 1, 18 * HR - 1, "this afternoon"), #1201pm-559pm
|
|
(18 * HR, 24 * HR, "this evening")] #6pm-1159pm
|
|
|
|
nextDay = [
|
|
(0 * HR, 0 * HR + 1, "tonight"), #midnght
|
|
(0 * HR, 24 * HR, "<dayOfWeek>"), ] #midnght-1159pm
|
|
|
|
subsequentDay = [
|
|
(0 * HR, 0 * HR + 1, "<dayOfWeek-1> night"), #midnght
|
|
(0 * HR, 24 * HR, "<dayOfWeek>"), ] #midnght-1159pm
|
|
|
|
#determine local time
|
|
myTimeZone = os.environ["TZ"] # save the defined time zone
|
|
os.environ["TZ"] = timezone # set the new time zone
|
|
ltissue = time.localtime(issueTime) # issuance local time
|
|
ltevent = time.localtime(eventTime) # event local time
|
|
#get the hour string (e.g., 8 PM)
|
|
hourStr = time.strftime("%I %p", ltevent)
|
|
if hourStr[0] == '0':
|
|
hourStr = hourStr[1:] #eliminate leading zero
|
|
|
|
#get the time zone (e.g., MDT)
|
|
hourTZstr = time.strftime("%Z", ltevent)
|
|
|
|
#determine the delta days from issuance to event
|
|
diffDays = ltevent[7] - ltissue[7] #julian day
|
|
if diffDays < 0: #year wrap around, assume Dec/Jan
|
|
diffDays = ltevent[2] + 31 - ltissue[2] #day of month
|
|
|
|
#get description time phrase
|
|
description = "<day>"
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if diffDays == 0:
|
|
for (startT, endT, desc) in sameDay:
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
|
|
else:
|
|
#choose proper table
|
|
if diffDays == 1:
|
|
table = nextDay
|
|
else:
|
|
table = subsequentDay
|
|
for (startT, endT, desc) in table:
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
dow = ltevent[6] #day of week
|
|
dowMinusOne = ltevent[6] - 1
|
|
if dowMinusOne < 0:
|
|
dowMinusOne = 6 #week wraparound
|
|
|
|
description = description.replace("<dayOfWeek>", self.asciiDayOfWeek(dow))
|
|
description = description.replace("<dayOfWeek-1>", self.asciiDayOfWeek(dowMinusOne))
|
|
|
|
#special cases NOON
|
|
if hourStr == "12 PM" and description == "today":
|
|
hourStr = "noon"
|
|
|
|
#special cases MIDNIGHT
|
|
if hourStr == "12 AM":
|
|
hourStr = "midnight"
|
|
|
|
os.environ["TZ"] = myTimeZone # reset the defined time zone
|
|
|
|
return (hourStr, hourTZstr, description)
|
|
|
|
def timingWordTableFUZZY4(self, issueTime, eventTime, timeZone,
|
|
timeType='start'):
|
|
#returns (timeValue, timeZone, descriptiveWord).
|
|
#eventTime is either the starting or ending time, based on
|
|
#the timeType flag. timezone is the time zone for the hazard area
|
|
#table is local time, start, end, descriptive phrase
|
|
HR = 3600
|
|
sameDay = [
|
|
(0 * HR, 6 * HR, "early this morning"), #midnght-559am
|
|
(6 * HR, 12 * HR, "this morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "this afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "this evening")] #6pm-1159pm
|
|
|
|
nextDay = [
|
|
(0 * HR, 0 * HR, "this evening"), #midnght tonight
|
|
(0 * HR, 6 * HR, "late tonight"), #midnght-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> evening")] #6pm-1159pm
|
|
|
|
subsequentDay = [
|
|
(0 * HR, 0 * HR, "<dayOfWeek-1> evening"), #midnght ystdy
|
|
(0 * HR, 6 * HR, "late <dayOfWeek-1> night"), #midnght-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> evening")] #6pm-1159pm
|
|
|
|
#determine local time
|
|
myTimeZone = os.environ["TZ"] # save the defined time zone
|
|
os.environ["TZ"] = timeZone # set the new time zone
|
|
ltissue = time.localtime(issueTime) # issuance local time
|
|
ltevent = time.localtime(eventTime) # event local time
|
|
|
|
#determine the delta days from issuance to event
|
|
diffDays = ltevent[7] - ltissue[7] #julian day
|
|
if diffDays < 0: #year wrap around, assume Dec/Jan
|
|
diffDays = ltevent[2] + 31 - ltissue[2] #day of month
|
|
|
|
#get description time phrase
|
|
description = "<day>"
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if diffDays == 0:
|
|
for (startT, endT, desc) in sameDay:
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
|
|
else:
|
|
#choose proper table
|
|
if diffDays == 1:
|
|
table = nextDay
|
|
else:
|
|
table = subsequentDay
|
|
for (startT, endT, desc) in table:
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
dow = ltevent[6] #day of week
|
|
dowMinusOne = ltevent[6] - 1
|
|
if dowMinusOne < 0:
|
|
dowMinusOne = 6 #week wraparound
|
|
description = description.replace("<dayOfWeek>", self.asciiDayOfWeek(dow))
|
|
description = description.replace("<dayOfWeek-1>", self.asciiDayOfWeek(dowMinusOne))
|
|
|
|
os.environ["TZ"] = myTimeZone # reset the defined time zone
|
|
|
|
hourStr = None
|
|
hourTZstr = None
|
|
return (hourStr, hourTZstr, description)
|
|
|
|
def timingWordTableFUZZY8(self, issueTime, eventTime, timeZone,
|
|
timeType='start'):
|
|
#returns the descriptive word for the event. eventTime is either
|
|
#the starting or ending time, based on the timeType flag.
|
|
#table is local time, start, end, descriptive phrase-A
|
|
|
|
HR = 3600
|
|
sameDay = [
|
|
(0 * HR, 3 * HR, "late <dayOfWeek-1> night"), #midnght-259am
|
|
(3 * HR, 6 * HR, "early this morning"), #300am-559am
|
|
(6 * HR, 9 * HR, "this morning"), #600am-859am
|
|
(9 * HR, 12 * HR, "late this morning"), #900am-1159am
|
|
(12 * HR, 15 * HR, "early this afternoon"), #noon-259pm
|
|
(15 * HR, 18 * HR, "late this afternoon"), #300pm-559pm
|
|
(18 * HR, 21 * HR, "this evening"), #600pm-859pm
|
|
(21 * HR, 24 * HR, "tonight")] #900pm-1159pm
|
|
|
|
nextDayStart = [
|
|
(0 * HR, 3 * HR, "late <dayOfWeek-1> night"), #midnght-259am
|
|
(3 * HR, 6 * HR, "early <dayOfWeek> morning"), #300am-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> evening")] #6pm-1159pm
|
|
|
|
nextDayEnd = [
|
|
(0 * HR, 0 * HR, "tonight"), #midnght tonight
|
|
(0 * HR, 3 * HR, "late <dayOfWeek-1> night"), #midnght-259am
|
|
(3 * HR, 6 * HR, "early <dayOfWeek> morning"), #300am-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> night")] #6pm-1159pm
|
|
|
|
subsequentDayStart = [
|
|
(0 * HR, 6 * HR, "late <dayOfWeek-1> night"), #midnght-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> evening")] #6pm-1159pm
|
|
|
|
subsequentDayEnd = [
|
|
(0 * HR, 0 * HR, "<dayOfWeek-1> night"), #midnght tonight
|
|
(0 * HR, 6 * HR, "early <dayOfWeek> morning"), #midnght-559am
|
|
(6 * HR, 12 * HR, "<dayOfWeek> morning"), #600am-noon
|
|
(12 * HR, 18 * HR, "<dayOfWeek> afternoon"), #1200pm-559pm
|
|
(18 * HR, 24 * HR, "<dayOfWeek> night")] #6pm-1159pm
|
|
|
|
#determine local time
|
|
myTimeZone = os.environ["TZ"] # save the defined time zone
|
|
os.environ["TZ"] = timeZone # set the new time zone
|
|
ltissue = time.localtime(issueTime) # issuance local time
|
|
ltevent = time.localtime(eventTime) # event local time
|
|
|
|
#determine the delta days from issuance to event
|
|
diffDays = ltevent[7] - ltissue[7] #julian day
|
|
if diffDays < 0: #year wrap around, assume Dec/Jan
|
|
diffDays = ltevent[2] + 31 - ltissue[2] #day of month
|
|
|
|
#get description time phrase
|
|
description = "<day>"
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if diffDays == 0:
|
|
for (startT, endT, desc) in sameDay:
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
|
|
else:
|
|
#choose proper table
|
|
if timeType == 'start':
|
|
if diffDays == 1:
|
|
table = nextDayStart
|
|
else:
|
|
table = subsequentDayStart
|
|
else:
|
|
if diffDays == 1:
|
|
table = nextDayEnd
|
|
else:
|
|
table = subsequentDayEnd
|
|
for (startT, endT, desc) in table:
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
|
|
#do substitution
|
|
dow = ltevent[6] #day of week
|
|
dowMinusOne = ltevent[6] - 1
|
|
if dowMinusOne < 0:
|
|
dowMinusOne = 6 #week wraparound
|
|
description = description.replace("<dayOfWeek>", self.asciiDayOfWeek(dow))
|
|
description = description.replace("<dayOfWeek-1>", self.asciiDayOfWeek(dowMinusOne))
|
|
|
|
os.environ["TZ"] = myTimeZone # reset the defined time zone
|
|
|
|
hourStr = None
|
|
hourTZstr = None
|
|
return (hourStr, hourTZstr, description)
|
|
|
|
def timingWordTableDAYNIGHT(self, issueTime, eventTime, timeZone,
|
|
timeType='start'):
|
|
#returns (timeValue, timeZone, descriptiveWord).
|
|
#eventTime is either the starting or ending time, based on
|
|
#the timeType flag. timezone is the time zone for the hazard area
|
|
#table is local time, start, end, descriptive phrase
|
|
HR = 3600
|
|
sameDay = [
|
|
(0 * HR, self.DAY() * HR, "early today"), #midnght-559am
|
|
(self.DAY() * HR, self.NIGHT() * HR, "today"), #600am-6pm
|
|
(self.NIGHT() * HR, 24 * HR, "tonight")] #6pm-midnight
|
|
|
|
nextDay = [
|
|
(0 * HR, self.DAY() * HR, "tonight"), #midnght-559am
|
|
(self.DAY() * HR, self.NIGHT() * HR, "<dayOfWeek>"), #600am-6pm
|
|
(self.NIGHT() * HR, 24 * HR, "<dayOfWeek> night")] #6pm-midnight
|
|
|
|
subsequentDay = [
|
|
(0 * HR, self.DAY() * HR, "<dayOfWeek-1> night"), #midnght-559am
|
|
(self.DAY() * HR, self.NIGHT() * HR, "<dayOfWeek>"), #600am-6pm
|
|
(self.NIGHT() * HR, 24 * HR, "<dayOfWeek> night")] #6pm-midnight
|
|
|
|
#determine local time
|
|
myTimeZone = os.environ["TZ"] # save the defined time zone
|
|
os.environ["TZ"] = timeZone # set the new time zone
|
|
ltissue = time.localtime(issueTime) # issuance local time
|
|
ltevent = time.localtime(eventTime) # event local time
|
|
|
|
#determine the delta days from issuance to event
|
|
diffDays = ltevent[7] - ltissue[7] #julian day
|
|
if diffDays < 0: #year wrap around, assume Dec/Jan
|
|
diffDays = ltevent[2] + 31 - ltissue[2] #day of month
|
|
|
|
#get description time phrase
|
|
description = "<day>"
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if diffDays == 0:
|
|
for (startT, endT, desc) in sameDay:
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
|
|
else:
|
|
#choose proper table
|
|
if diffDays == 1:
|
|
table = nextDay
|
|
else:
|
|
table = subsequentDay
|
|
for (startT, endT, desc) in table:
|
|
hourmin = ltevent[3] * 3600 + ltevent[4] * 60 #hour, minute
|
|
if hourmin >= startT and hourmin < endT and timeType == 'start':
|
|
description = desc
|
|
break
|
|
elif hourmin <= endT and timeType == 'end':
|
|
description = desc
|
|
break
|
|
dow = ltevent[6] #day of week
|
|
dowMinusOne = ltevent[6] - 1
|
|
if dowMinusOne < 0:
|
|
dowMinusOne = 6 #week wraparound
|
|
description = description.replace("<dayOfWeek>", self.asciiDayOfWeek(dow))
|
|
description = description.replace("<dayOfWeek-1>", self.asciiDayOfWeek(dowMinusOne))
|
|
|
|
os.environ["TZ"] = myTimeZone # reset the defined time zone
|
|
|
|
hourStr = None
|
|
hourTZstr = None
|
|
return (hourStr, hourTZstr, description)
|
|
|
|
def asciiDayOfWeek(self, number):
|
|
#converts number (0-Monday) to day of week
|
|
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
|
|
'Saturday', 'Sunday']
|
|
if number >= 0 and number < 7:
|
|
return days[number]
|
|
else:
|
|
return "?" + repr(number) + "?"
|
|
|
|
# Returns the headline phrase based on the specified hazard.
|
|
# The hazard record contains all geoIDs in the hazard['id'] field,
|
|
# not just a single one. Doesn't add the dots.
|
|
def makeStandardPhrase(self, hazard, issuanceTime):
|
|
|
|
# hdln field present?
|
|
if 'hdln' not in hazard:
|
|
return ""
|
|
|
|
# make sure the hazard is still in effect or within EXP critiera
|
|
if (hazard['act'] != 'EXP' and issuanceTime >= hazard['endTime']) or \
|
|
(hazard['act'] == 'EXP' and issuanceTime > 30 * 60 + hazard['endTime']):
|
|
return "" # no headline for expired hazards
|
|
|
|
#assemble the hazard type
|
|
hazStr = hazard['hdln']
|
|
|
|
# if the hazard is a convective watch, tack on the etn
|
|
phenSig = hazard['phen'] + "." + hazard['sig']
|
|
if phenSig in ["TO.A", "SV.A"]:
|
|
hazStr = hazStr + " " + str(hazard["etn"])
|
|
|
|
# add on the action
|
|
actionWords = self.actionControlWord(hazard, issuanceTime)
|
|
hazStr = hazStr + ' ' + actionWords
|
|
|
|
#get the timing words
|
|
timeWords = self.getTimingPhrase(hazard, issuanceTime)
|
|
if len(timeWords):
|
|
hazStr = hazStr + ' ' + timeWords
|
|
|
|
return hazStr
|
|
|
|
def timeCompare(self, haz1, haz2):
|
|
if haz1['startTime'] < haz2['startTime']:
|
|
return -1
|
|
elif haz1['startTime'] == haz2['startTime']:
|
|
return 0
|
|
else:
|
|
return 1
|
|
|
|
# Sorts headlines for marine products. sort algorithm
|
|
# cronological ordering by start time, then action,
|
|
# then significance, then phen alphabetically.
|
|
@property
|
|
def marineSortHazardAlg(self):
|
|
def cmpfunc(r1, r2):
|
|
#1st by start time
|
|
if r1['startTime'] < r2['startTime']:
|
|
return -1
|
|
elif r1['startTime'] > r2['startTime']:
|
|
return 1
|
|
|
|
#2nd by action
|
|
actionCodeOrder = ["CAN", "EXP", "UPG", "NEW", "EXB", "EXA",
|
|
"EXT", "ROU", "CON"]
|
|
try:
|
|
aIndex = actionCodeOrder.index(r1['act'])
|
|
except:
|
|
aIndex = 99
|
|
try:
|
|
bIndex = actionCodeOrder.index(r2['act'])
|
|
except:
|
|
bIndex = 99
|
|
if aIndex < bIndex:
|
|
return -1
|
|
elif aIndex > bIndex:
|
|
return 1
|
|
|
|
#3rd by significance
|
|
sig = ['W', 'Y', 'A']
|
|
try:
|
|
index1 = sig.index(r1['sig'])
|
|
except:
|
|
index1 = 99
|
|
try:
|
|
index2 = sig.index(r2['sig'])
|
|
except:
|
|
index2 = 99
|
|
if index1 < index2:
|
|
return -1
|
|
elif index1 > index2:
|
|
return 1
|
|
|
|
#4th by phen (alphabetically)
|
|
if r1['phen'] < r2['phen']:
|
|
return -1
|
|
elif r1['phen'] > r2['phen']:
|
|
return 1
|
|
|
|
#equal
|
|
return 0
|
|
return functools.cmp_to_key(cmpfunc)
|
|
# Sorts headlines for regular products.
|
|
@property
|
|
def regularSortHazardAlg(self):
|
|
def cmpfunc(r1, r2):
|
|
actActions = ["NEW", "EXB", "EXA", "EXT", "ROU", "CON"]
|
|
inactActions = ["CAN", "EXP", "UPG"]
|
|
actionCodeOrder = actActions + inactActions
|
|
|
|
# 1st by general action category
|
|
if r1['act'] in actActions and r2['act'] in inactActions:
|
|
return -1
|
|
elif r1['act'] in inactActions and r2['act'] in actActions:
|
|
return 1
|
|
|
|
# 2nd by chronological event starting time
|
|
if r1['startTime'] < r2['startTime']:
|
|
return -1
|
|
elif r1['startTime'] > r2['startTime']:
|
|
return 1
|
|
|
|
# 3rd by action code order
|
|
try:
|
|
aIndex = actionCodeOrder.index(r1['act'])
|
|
except:
|
|
aIndex = 99
|
|
try:
|
|
bIndex = actionCodeOrder.index(r2['act'])
|
|
except:
|
|
bIndex = 99
|
|
if aIndex < bIndex:
|
|
return -1
|
|
elif aIndex > bIndex:
|
|
return 1
|
|
|
|
#4th by significance
|
|
sig = ['W', 'Y', 'A']
|
|
try:
|
|
index1 = sig.index(r1['sig'])
|
|
except:
|
|
index1 = 99
|
|
try:
|
|
index2 = sig.index(r2['sig'])
|
|
except:
|
|
index2 = 99
|
|
if index1 < index2:
|
|
return -1
|
|
elif index1 > index2:
|
|
return 1
|
|
|
|
#5th by phen (alphabetically)
|
|
if r1['phen'] < r2['phen']:
|
|
return -1
|
|
elif r1['phen'] > r2['phen']:
|
|
return 1
|
|
|
|
#equal
|
|
return 0
|
|
return functools.cmp_to_key(cmpfunc)
|
|
|
|
# Makes multiple headlines based on the hazards list and returns the lot.
|
|
def makeHeadlinePhrases(self, tree, node, hazardList, issuanceTime,
|
|
testMode=0):
|
|
returnStr = ""
|
|
# make a deepcopy since we plan to mess with it.
|
|
hList = copy.deepcopy(hazardList)
|
|
|
|
# sort headlines in appropriate order
|
|
if len(hList):
|
|
if hList[0]['pil'] in ['CWF', 'NSH', 'OFF', 'GLF']:
|
|
hList.sort(key=self.marineSortHazardAlg)
|
|
else:
|
|
hList.sort(key=self.regularSortHazardAlg)
|
|
|
|
while len(hList) > 0:
|
|
hazard = hList[0]
|
|
|
|
# Can't make phrases with hazards with no 'hdln' entry
|
|
if hazard['hdln'] == "":
|
|
hList.remove(hazard)
|
|
continue
|
|
|
|
phenSig = hazard['phen'] + "." + hazard['sig']
|
|
actionCodeList = self.getAllowedActionCodes(phenSig)
|
|
|
|
# if the action is not in the actionCodeList, skip it
|
|
if hazard['sig'] != "": # it's not locally defined
|
|
if not hazard['act'] in actionCodeList:
|
|
print("...Ignoring action code:", hazard['act'], \
|
|
hazard['hdln'])
|
|
hList.remove(hazard)
|
|
continue
|
|
|
|
# get the headline phrase
|
|
hazStr = self.makeStandardPhrase(hazard, issuanceTime)
|
|
if len(hazStr):
|
|
# Call user hook
|
|
localStr = self.addSpace(self.hazard_hook(
|
|
tree, node, hazard['phen'], hazard['sig'], hazard['act'],
|
|
hazard['startTime'], hazard['endTime']), "leading")
|
|
returnStr = returnStr + "..." + hazStr + localStr + "...\n"
|
|
|
|
# always remove the main hazard from the list
|
|
hList.remove(hazard)
|
|
|
|
return returnStr
|
|
|
|
# Returns a formatted string announcing the hazards that are valid with
|
|
# timing phrases
|
|
def getHazardString(self, tree, node, fcstArea):
|
|
if len(fcstArea) <= 0:
|
|
return ""
|
|
hazardTable = self._hazards.getHazardList(fcstArea)
|
|
returnStr = ""
|
|
issuanceTime = self._issueTime.unixTime()
|
|
|
|
returnStr = self.makeHeadlinePhrases(tree, node, hazardTable,
|
|
issuanceTime)
|
|
#Test mode?
|
|
returnStr = self.headlinePhraseTESTcheck(tree.get("argDict"),
|
|
returnStr)
|
|
|
|
return returnStr.upper()
|
|
|
|
# The organizeHazard method brings in the raw analyzed table,
|
|
# then organizes it by edit area, returing a list of
|
|
# editArea lists. The first element of the list must the the first
|
|
# segment in a hazard based product. Ensures that a group of edit areas
|
|
# does not contain both zones and FIPS code - per 10-1702.
|
|
def organizeHazards(self, rawATable):
|
|
|
|
# Initialize data structures to be used.
|
|
byIdDict = {}
|
|
byHazardDict = {}
|
|
masterEditAreaList = []
|
|
|
|
# Loop over the activeTable, and organize by editArea
|
|
|
|
#
|
|
# Added code to discard segment identifer when cancelling a product.
|
|
# this was creating bogus segments.
|
|
#
|
|
|
|
for eachHazard in rawATable:
|
|
if eachHazard['id'] in byIdDict:
|
|
byIdDict[eachHazard['id']].append(\
|
|
(eachHazard['phen'], eachHazard['sig'], eachHazard['seg'],
|
|
eachHazard['act'], eachHazard['startTime'], eachHazard['endTime'],
|
|
eachHazard['etn'], eachHazard['vtecstr']))
|
|
else:
|
|
byIdDict[eachHazard['id']] = [(eachHazard['phen'],
|
|
eachHazard['sig'], eachHazard['seg'],
|
|
eachHazard['act'], eachHazard['startTime'], eachHazard['endTime'],
|
|
eachHazard['etn'], eachHazard['vtecstr'])]
|
|
|
|
#
|
|
# Go through the sorted dictionary, organize into combos
|
|
#
|
|
|
|
idsList = list(byIdDict.keys())
|
|
unsortedHazards = list(byIdDict.values())
|
|
sortedHazards = []
|
|
for eachHazard in unsortedHazards:
|
|
if not self.__sortedContains(eachHazard, sortedHazards):
|
|
sortedHazards.append(eachHazard)
|
|
|
|
#
|
|
# The following section determines the VTEC/segment ordering
|
|
#
|
|
|
|
weightedList = []
|
|
|
|
#
|
|
# this list ranks by 'sig' and 'act' from least [0] to most
|
|
# importance [n]. All CANs go at the end, because cancel is the
|
|
# most important action.
|
|
#
|
|
|
|
segmentVTECOrderList = [
|
|
# Place holder for local hazards
|
|
'LocalHazard',
|
|
'F.ROU', 'F.CON', 'F.EXT', 'F.EXA', 'F.EXB', 'F.NEW',
|
|
'F.UPG', 'S.ROU', 'S.CON', 'S.EXT', 'S.EXA', 'S.EXB',
|
|
'S.NEW', 'S.UPG', 'A.ROU', 'A.CON', 'A.EXT', 'A.EXA',
|
|
'A.EXB', 'A.NEW', 'A.UPG', 'Y.ROU', 'Y.CON', 'Y.EXT',
|
|
'Y.EXA', 'Y.EXB', 'Y.NEW', 'Y.UPG', 'W.ROU', 'W.CON',
|
|
'W.EXT', 'W.EXA', 'W.EXB', 'W.NEW', 'W.UPG', 'F.EXP',
|
|
'F.CAN', 'S.EXP', 'S.CAN', 'A.EXP', 'A.CAN', 'Y.EXP',
|
|
'Y.CAN', 'W.EXP', 'W.CAN']
|
|
|
|
for eachHazard in sortedHazards:
|
|
tempEditAreaList = []
|
|
tempElementWeight = -1.0
|
|
tempElementWeightCheck = -1.0
|
|
secondaryWeight = 0.0
|
|
segmentWeight = 0.0
|
|
timeWeight = 0.0
|
|
|
|
#
|
|
# Figure out the maximum weight based on each
|
|
# element in the hazard combination.
|
|
#
|
|
|
|
for eachElement in eachHazard:
|
|
|
|
#
|
|
# This section checks of the hazard's index in
|
|
# segmentVTECOrderList
|
|
#
|
|
|
|
if eachElement[1] is not None and eachElement[3] is not None:
|
|
sigAction = eachElement[1] + '.' + eachElement[3]
|
|
if sigAction in segmentVTECOrderList:
|
|
tempElementWeightCheck = float(segmentVTECOrderList.index(sigAction))
|
|
else:
|
|
# Local hazards are not in list so
|
|
# assign it least importance
|
|
tempElementWeightCheck = 0.0
|
|
|
|
#
|
|
# secondaryWeight is a cumulative value << 1 that allows
|
|
# combinations of actions and sigs to take precedence over
|
|
# single actions or sigs of the same primary importance. For
|
|
# instance, a BZ.W^WC.Y will come before BZ.W by itself,
|
|
# even though they are the same priority. It also takes
|
|
# into account the hazards position in the allowedHazardTable,
|
|
# so that a a blizzard warning will trump a winter storm
|
|
# warning
|
|
#
|
|
|
|
#
|
|
# from 1 (important) to 1001 (undefined). 1 is added to
|
|
# prevent division errors.
|
|
#
|
|
|
|
allowedHazardValue = float(self.getHazardImportance(\
|
|
eachElement[0] + '.' + eachElement[1])) + 1.0
|
|
#
|
|
# Ensure that secondary weight never approaches 1 (ten thousandths...)
|
|
#
|
|
|
|
secondaryWeight = secondaryWeight + 0.0001 / allowedHazardValue
|
|
|
|
#
|
|
# Check the tempElementWeightCheck against the
|
|
# tempElementWeight. If it's more, then the current hazard
|
|
# is the higher priority of the combo, and set
|
|
# tempElementWeight to it's index value. If it's less, then
|
|
# this hazard is of lower priority, but do give it a little
|
|
# weight (<< 1) so that for instance a warn + advisory
|
|
# segment will come before just a warn segment.
|
|
#
|
|
|
|
if tempElementWeightCheck > tempElementWeight:
|
|
# This hazard is more important
|
|
tempElementWeight = tempElementWeightCheck
|
|
|
|
#
|
|
# Add a factor for segment number. Lowest segments go first. Never
|
|
# Approach one (millionths...)
|
|
#
|
|
|
|
segmentWeight = 1.0 / (10000000.0 + float(eachElement[2]))
|
|
|
|
#
|
|
# Add a factor for time. Earliest start times go first. Never
|
|
# approach one (e-10)
|
|
#
|
|
|
|
timeWeight = 1.0 / float(eachElement[4] + 100.0)
|
|
|
|
#
|
|
# Assign the sum of weights before adding
|
|
# list for sorting
|
|
#
|
|
|
|
tempElementWeight = tempElementWeight + secondaryWeight + \
|
|
segmentWeight + timeWeight
|
|
|
|
secondaryWeight = 0.0
|
|
segmentWeight = 0.0
|
|
timeWeight = 0.0
|
|
|
|
for eachID in idsList:
|
|
if set(byIdDict[eachID]) == set(eachHazard):
|
|
tempEditAreaList.append(eachID)
|
|
|
|
weightedList.append((tempElementWeight, tempEditAreaList))
|
|
|
|
# Sort the list by weight
|
|
weightedList.sort(key=self._wtListSort)
|
|
|
|
# Make the list of geoareas
|
|
finalList = []
|
|
for w in weightedList:
|
|
finalList.append(w[1])
|
|
|
|
# Seperate out the zones and FIPS into separate UGC blocks
|
|
s = []
|
|
for s1 in finalList:
|
|
fips = []
|
|
zones = []
|
|
for s2 in s1:
|
|
if s2[2] == 'Z':
|
|
zones.append(s2)
|
|
elif s2[2] == 'C':
|
|
fips.append(s2)
|
|
if len(fips):
|
|
s.append(fips)
|
|
if len(zones):
|
|
s.append(zones)
|
|
finalList = s
|
|
|
|
return finalList
|
|
|
|
# Determines if hazard in sorted hazards. Hazard can be a list, thus we
|
|
# need to compare all elements for their inclusion, rather than simply
|
|
# using the "in" operator.
|
|
def __sortedContains(self, hazard, sorted_hazards):
|
|
hazard.sort()
|
|
for indSorted in sorted_hazards:
|
|
indSorted.sort()
|
|
if hazard == indSorted:
|
|
return 1
|
|
return 0
|
|
|
|
# Sorts tuples of (weight, list, time), by weight
|
|
@property
|
|
def _wtListSort(self):
|
|
def cmpfunc(a, b):
|
|
if a[0] > b[0]:
|
|
return -1
|
|
elif a[0] == b[0]:
|
|
return 0
|
|
else:
|
|
return 1
|
|
return functools.cmp_to_key(cmpfunc)
|
|
|
|
# Modifies string to have ...TEST... if we are in TEST mode. This
|
|
# to the MND header. Modifies string to have EXPERIMENTAL... if
|
|
# we are in EXPERIMENTAL mode.
|
|
def checkTestMode(self, argDict, str):
|
|
# testMode is set, then we are in product test mode.
|
|
# modify the str to have beginning and ending TEST indication.
|
|
if argDict.get('testMode', 0):
|
|
return "TEST..." + str + "...TEST"
|
|
elif argDict.get('experimentalMode', 0):
|
|
return "EXPERIMENTAL..." + str
|
|
else:
|
|
return str
|
|
|
|
# Modifies headline string to have TEST if we are in TEST mode.
|
|
def headlinePhraseTESTcheck(self, argDict, str):
|
|
if argDict.get('testMode', 0):
|
|
lines = str.split('\n')
|
|
str = "...THIS MESSAGE IS FOR TEST PURPOSES ONLY...\n"
|
|
for x in range(len(lines) - 1): #-1 for trailing new line
|
|
line = lines[x]
|
|
|
|
#beginning of line
|
|
if line.find("...") == 0:
|
|
line = line[0:3] + "TEST " + line[3:]
|
|
#end of line
|
|
index = line.rfind("...")
|
|
if index != 0 and index == len(line) - 3:
|
|
line = line[0:-3] + " TEST..."
|
|
|
|
lines[x] = line
|
|
|
|
return str + '\n'.join(lines)
|
|
|
|
#normal mode (not test mode)
|
|
else:
|
|
return str
|
|
|
|
# utility for attribution, takes hazard description ['hdln'] field and
|
|
# adds TEST if appropriate in test mode, adds "A" or "AN" as appropriate
|
|
# if desired.
|
|
def hazardName(self, name, argDict, addA=False):
|
|
|
|
if len(name) == 0:
|
|
return name
|
|
|
|
# test mode
|
|
if argDict.get('testMode', 0):
|
|
phrase = 'Test ' + name #test mode, prepend "TEST"
|
|
else:
|
|
phrase = name
|
|
|
|
# want A or AN?
|
|
if addA:
|
|
if phrase[0] in ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u']:
|
|
phrase = "an " + phrase
|
|
else:
|
|
phrase = "a " + phrase
|
|
return phrase
|
|
|