awips2/cave/com.raytheon.viz.gfe/localization/gfe/userPython/utilities/TropicalUtility.py
2022-05-05 12:34:50 -05:00

1255 lines
47 KiB
Python

# ------------------------------------------------------------------------------
# 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.
#
# TropicalUtility - Version 4.0
#
# Authors: Matthew H. Belk (BOX), Shannon White (AWIPS), Pablo Santos (MFL),
# Tom LeFebvre (GSD)
#
# Created: 03/03/2012 Last Modified: 04/26/2016
#
# 04/26/2016 - Modified to add the displayProduct method supplied by Ron
# Anderson (Raytheon)
# ------------------------------------------------------------------------------
#
# SOFTWARE HISTORY
#
# Date Ticket# Engineer Description
# ------------ ---------- ----------- ------------------------------------------
# Mar 03, 2012 Initial creation
# Apr 26, 2016 mbelk Modified to add the displayProduct method
# supplied by Ron Anderson (Raytheon)
# Sep 19, 2016 19293 randerso Initial baseline check in
# Feb 21, 2017 29544 randerso Fix possible RuntimeError caused by
# discarding from set while looping over it
# Aug 10, 2018 20727 ryu Check in change from Tom LeFebvre
# Aug 24, 2018 20727 ryu Remove 4 SR WFOs and add 4 ER WFOs
# Aug 27, 2018 20727 ryu Add 3 new ER WFOs.
# Dec 19, 2019 swhite Added new WFOs and removed ALY constraints
# Apr 19, 2020 20727 lefebvre Added a way to stop filtering all but AT*
# Apr 22, 2020 20727 lefebvre saveAdvisory now saves to all active sites.
# JSON files are time stamped when they are
# saved not when they are read. Added new
# method to fetch active siteIDs.
# May 14, 2020 22033 lefebvre Added Central, WestPac sites to activeSites
# May 21, 2020 22033 lefebvre Addressed code review comments.
# May 21, 2020 22033 lefebvre Minor adjustments to a few things.
# Jun 03, 2020 22033 lefebvre Addressed code review comments.
# Jan 05, 2021 8313 randerso Moved "Bad Map result" message inside result
# check so it's only displayed when no result
# is returned.
# Feb 15, 2021 8362 randerso Fix exception handling in getWfosAttention()
# check so it's only displayed when no result
# is returned.
################################################################################
##
# This is an absolute override file, indicating that a higher priority version
# of the file will completely replace a lower priority version of the file.
##
import collections
import errno
import os
import re
from ufpy.dataaccess import DataAccessLayer
import GridManipulation
import HazardUtils
import JsonSupport
import LocalizationSupport
import LogStream
import ProcessVariableList
import TimeRange
import numpy as np
import pprint as pp
class TropicalUtility(GridManipulation.GridManipulation):
def __init__(self, dbss):
GridManipulation.GridManipulation.__init__(self, dbss)
self._dbss = dbss
# Make an instance of the HazardUtils
self._hazUtils = HazardUtils.HazardUtils(dbss, None)
# Define a base for the ETN issued by a national center
self._natlBaseETN = 1000 # Not used in current tools/procedures
# Get the current mutable database ID
self._mutableID = self.mutableID()
# Make lists of all WFOs we might want to send a message to from NHC.
# The offices are split into coastal offices which deal with storm
# surge, and inland offices which only deal with wind hazards from NHC
self._surgeWfos = ["CAR", "GYX", "BOX", "OKX", "PHI", "LWX", "AKQ",
"MHX", "ILM", "CHS", "JAX", "MLB", "MFL", "KEY",
"TBW", "TAE", "MOB", "LIX", "LCH", "HGX", "CRP",
"BRO"]
self._windWfos = ["ALY", "MRX", "FFC", "OHX", "HUN", "BMX", "MEG",
"JAN", "LZK", "SHV", "CAE", "FWD", "RAH", "GSP",
"EWX", "RNK", "BGM", "BTV", "CTP"]
# Toggle for debugging output
self._debug = False # True = On / False = Off
# Define test mode for procedure which communicate with WFOs
#self._testMode = True # if True, the command is only printed (test)
self._testMode = False # if False, messages get sent to WFOs (live)
# JSON files are always saved to all of the following sites on the local
# EDEX cluster.
self._activeSiteIDs = ["NHA", "NHZ", "NHP", "HPA", "GUM", "PQE", "PQW"]
#===========================================================================
# Utility methods to create common dialog buttons and actions
### Makes the Run button
def makeRunButton(self, buttonFrame):
Tkinter.Button(buttonFrame, text="Run", width=10,
command=self.runCommand,
state=Tkinter.NORMAL).pack(side=Tkinter.LEFT, pady=5,
padx=50, fill=Tkinter.X)
### Makes the Run/Dismiss button
def makeRunDismissButton(self, buttonFrame):
Tkinter.Button(buttonFrame, text="Run/Dismiss", width=10,
command=self.runDismissCommand,
state=Tkinter.NORMAL).pack(side=Tkinter.LEFT, pady=5,
padx=50, fill=Tkinter.X)
### Makes the Cancel button
def makeCancelButton(self, buttonFrame):
Tkinter.Button(buttonFrame, text="Cancel", width=10,
command=self.cancelCommand,
state=Tkinter.NORMAL).pack(side=Tkinter.LEFT, pady=5,
padx=50, fill=Tkinter.X)
### Action when "Run" button is clicked
def runCommand(self):
LogStream.logUse("Run")
self.makeHazardGrid()
return
### Action when "Run/Dismiss" button is clicked
def runDismissCommand(self):
LogStream.logUse("Run/Dismiss")
if self.makeHazardGrid() == 1:
self.cancelIt()
### Action when "Cancel" button is clicked
def cancelCommand(self):
# unregister the maps
LogStream.logUse("Cancel")
self.cancelIt()
### Actual steps required to cancel/exit
def cancelIt(self):
# unregister the maps
for key in self._registeredMaps:
self._mapManager.unregisterMapSet(self._registeredMaps[key].mapId())
self.__master.destroy()
def getSubKeys(self, key):
parts = key.split("^")
if "<None>" in parts:
parts = parts.remove("<None>")
return parts
#===========================================================================
# Utility methods to manipulate Hazard grids
# Returns the phen for specified hazard key.
# If not a VTEC hazard, returns ""
def keyPhen(self, key):
pos = key.find(".")
if pos == -1: # not found
return ""
return key[0:pos]
# Parses specified key and returns the sig field.
def keySig(self, key):
pos = key.find(".")
if pos == -1: # not found
return ""
return key[pos + 1]
# Parse the specified key and return the ETN. If none found,
# return an empty string ("")
def getETN(self, key):
subKeys = key.split("^")
subKey = subKeys[0]
parts = subKey.split(":")
if len(parts) < 2:
return ""
else:
return parts[1]
def activeSiteIDs(self):
return self._activeSiteIDs
# Checks the specified hazard and proposed keys over the selectedMask
# for any conflicting hazards. If found, returns True, otherwise
# return False.
def anyHazardConflicts(self, hazard, proposed, selectedMask):
# Make the list of tropical hazards
tropicalHazList = ["TR.W", "TR.A", "HU.W", "HU.A", "SS.A", "SS.W"]
localHazList = ["CF"]
hazGrid, hazKeys = hazard
propGrid, propKeys = proposed
# Make the list of hazard subKeys found in the hazard grid over the
# selectedMask
hazList = []
for hazKey in hazKeys:
if hazKey == "<None>": # Ignore the <None> key
continue
# Identify the area where this hazard exists
hazIndex = self.getIndex(hazKey, hazKeys)
mask = hazGrid == hazIndex
# Check for overlapping points
overlap = mask & selectedMask
# If there is any overlap
if overlap.any():
# These keys can have subKeys so separate those, too
subKeyList = self.getSubKeys(hazKey)
for subKey in subKeyList:
if subKey not in hazList:
hazList.append(subKey)
# Look over the proposed hazards keys
for propKey in propKeys:
if propKey == "<None>": # Ignore the <None> key
continue
# Check for overlapping points
propIndex = self.getIndex(propKey, propKeys)
propMask = propGrid == propIndex
overlap = propMask & selectedMask
if not overlap.any(): # no points in selectedMask
continue
# Parse the phen, sig, and ETN
propPhen = self.keyPhen(propKey)
propSig = self.keySig(propKey)
propETN = self.getETN(propKey)
for hazKey in hazList:
# See if this hazard overlaps with the current proposed hazard
hazIndex = self.getIndex(hazKey, hazKeys)
hazMask = hazGrid == hazIndex
hazOverlap = hazMask & propMask
if not hazOverlap.any():
continue
# Parse the hazKey
hazETN = self.getETN(hazKey)
hazPhen = self.keyPhen(hazKey)
hazSig = self.keySig(hazKey)
if propPhen == "SS" and hazPhen in localHazList:
return True
# reconstruct the phen and sig
hazPhenSig = hazPhen + "." + hazSig
propPhenSig = propPhen + "." + propSig
# If the hazard keys are both tropical one the ETNs must match
if hazPhenSig in tropicalHazList and \
propPhenSig in tropicalHazList:
if hazETN != propETN:
return True
# Otherwise if the phenSigs match, the ETNs must match
if hazPhenSig == propPhenSig:
if propETN != hazETN:
return True
return False
# Check for hazard conflicts on a point by point basis. Uses the method
# anyHazadConflicts to do the logic for checking hazard phen, sig and ETN.
def anyHazardConflictsByPoint(self, hazardGrid, proposedGrid, selectedMask):
# Make the list of tropical hazards
tropicalHazList = ["TR.W", "TR.A", "HU.W", "HU.A", "SS.A", "SS.W"]
hazGrid, hazKeys = hazardGrid
propGrid, propKeys = proposedGrid
# Make the list of hazards found in the hazard grid over the
# selectedMask
hazList = []
for hazKey in hazKeys:
if hazKey == "<None>":
continue
hazIndex = self.getIndex(hazKey, hazKeys)
hazMask = hazGrid == hazIndex
# Now check propKeys
for propKey in propKeys:
if propKey == "<None>":
continue
propIndex = self.getIndex(propKey, propKeys)
propMask = propGrid == propIndex
overlap = hazMask & propMask & selectedMask
if overlap.any():
if self.anyHazardConflicts(hazardGrid, proposedGrid, overlap):
start = int(self._gmtime().unixTime() / 3600) * 3600
end = start + 3600
timeRange = self.GM_makeTimeRange(start, end)
return True
return False
# Calculates a difference grid (added versus removed)
# Calculates a difference grid (added versus removed)
def calcDiffGrid(self, initialGrid, proposedGrid, diffName, timeRange,
isWFO=False):
# If this is a WFO
if isWFO:
# Filter the Hazards to only keep the Storm Surge hazards
ssKeys = ["SS.W", "SS.A"]
initialGrid = self.filterHazardGrid(initialGrid, ssKeys)
proposedGrid = self.filterHazardGrid(proposedGrid, ssKeys)
# Split these grids into their components
initGrid, initKeys = initialGrid
propGrid, propKeys = proposedGrid
# Identify where there are no hazards in both grids
initNone = self.getIndex("<None>", initKeys)
propNone = self.getIndex("<None>", propKeys)
# Mask of these areas
initNoneMask = (initGrid == initNone)
propNoneMask = (propGrid == propNone)
# Make an empty grid to hold difference indicator
diffGrid = np.zeros(self.getGridShape(), np.float32)
# Calculate hazards that were removed
diffGrid[propNoneMask & ~initNoneMask] = -1
# Calculate hazards that were added
diffGrid[~propNoneMask & initNoneMask] = 1
# Find areas that had some hazard and it changed to another hazard
for initKey in initKeys:
for propKey in propKeys:
if initKey == "<None>" or propKey == "<None>": # ignore any <None> cases
continue
if initKey == propKey: # ignore cases where the keys are the same
continue
# Now we know the keys are different and neither is <None>
initIndex = self.getIndex(initKey, initKeys)
propIndex = self.getIndex(propKey, propKeys)
initMask = (initGrid == initIndex)
propMask = (propGrid == propIndex)
# The intersection is where they changed
diffGrid[initMask & propMask] = 2
# Add this temporary grid to the grid manager so it can be seen
self.createGrid("Fcst", diffName, "SCALAR", diffGrid, timeRange,
descriptiveName="Diff Between NHC and WFO",
precision=0, minAllowedValue=-1.0, maxAllowedValue=2.0)
return
def filterHazardGrid(self, hazardGrid, filterKeys):
filteredGrid = self.empty(np.int8)
filteredKeys = ["<None>"]
hazGrid, hazKeys = hazardGrid
# Find the hazard keys that contain any filter key
for filterKey in filterKeys:
for hazKey in hazKeys:
if filterKey not in hazKey:
continue
hazIndex = self.getIndex(hazKey, hazKeys)
mask = (hazGrid == hazIndex) # get the points that are set to this mask
# Cleanse the hazKey of all keys except filterKeys
newKey = ""
splitKeys = hazKey.split("^")
for splitKey in splitKeys:
phenSig = splitKey.split(":")[0]
if phenSig not in filterKeys:
continue
newKey = newKey + splitKey + "^"
if newKey[-1] == "^":
newKey = newKey[0:-1] # trim the trailing "^"
newIndex = self.getIndex(newKey, filteredKeys)
filteredGrid[mask] = newIndex
return filteredGrid, filteredKeys
# Calculates a difference grid (added versus removed) for WFOs
#==============================================================================
# Methods for sending messages to WFOs
#==============================================================================
# Define method to send a message to WFOs
def sendMessageToWfos(self, wfos, message, testMode=True):
SendMessageResult = collections.namedtuple('SendMessageResult',
('success', 'wfo', 'output'))
if len(wfos) == 0:
msg = "sendMessageToWfos called with empty WFO list, nothing to do."
self.statusBarMsg(msg, 'A')
return
# Look at each WFO which needs a message
results = []
for wfo in wfos:
# Start constructing the final message
final_message = "{} - {} have been sent by NHC.".format(wfo.strip(),
message.strip())
# If the ProposedSS grids are mentioned, send one message
if "ProposedSS" in message:
final_message += " Please join the tropical_collaboration1"
final_message += " chat room in NWSChat. You should run "
final_message += "the Populate -> CopyNHCProposed procedure "
final_message += "now, and start the collaboration process."
# Otherwise, let the WFO's know we're finished with this round
elif "ProposedTropWindWW" in message:
final_message += " NHC has sent you a ProposedTropWindWWnc guidance "
final_message += "grid via ISC. You can view it as guidance when "
final_message += "making your inland watches and warnings with Make Hazards."
else:
final_message += " The collaboration process is now done. "
final_message += "You should run the Populate -> "
final_message += "MergeProposedSS procedure now, and finish "
final_message += "preparing the grids for the WFO TCV."
# If we are in test mode, just display the command which
# would be executed
if testMode:
msg = "Test message to WFO {}: '{}'".format(wfo, final_message)
LogStream.logDebug(msg)
result = "" # Simulate a successful transfer
# Otherwise, actually send this message
else:
msg = "Live message to WFO {}: '{}'".format(wfo, final_message)
LogStream.logDebug(msg)
result = self.sendWFOMessage(wfo, final_message)
# Keep track of which offices successfully got the message
results.append(SendMessageResult(result == "", wfo, result))
total_count = 0
fail_count = 0
details = ""
# Construct a final status message of the message send status
for result in sorted(results, key=lambda x: (x.success, x.wfo)):
total_count += 1
if result.success:
details += "\nMessage successfully sent to site {}.".format(result.wfo)
else:
fail_count += 1
details += "\nCould not send message to site {}. Command output:\n{}".format(result.wfo, result.output)
if fail_count:
msg = "{} of {} server(s) failed to receive WFO message. Site-by-site detail: \n{}".format(fail_count, total_count, details)
self.statusBarMsg(msg, 'A')
else:
msg = "WFO message sent to all {} sites successfully. Site-by-site detail: \n{}".format(total_count, details)
self.statusBarMsg(msg, 'R')
# Define method to determine WFOs which should get a message from NHC
def getWfosAttention(self, WEname, anyChanges=None, percentThresh=3):
# anyChanges is a mask, where True means a change in hazards happened
# Make a list of WFOs NHC might communicate with
searchWfos = set(self._surgeWfos + self._windWfos)
# Make sets to track WFOs with only surge hazards, those with only
# wind hazards, and those with both
surgeWfos = set()
windWfos = set()
bothWfos = set()
# Make a dictionary of masks for all of these offices
officeMasks = {}
for wfo in searchWfos:
try:
officeMasks[wfo] = self.encodeEditArea("ISC_%s" % (wfo.upper()))
# If we are looking for any changes to the underlying field
if anyChanges is not None:
# See if there are any changes in hazards for this WFO
overlay = (anyChanges & officeMasks[wfo])
if overlay.any():
msg = "Adding to surge - " + wfo + " for changes"
self.statusBarMsg(msg, 'R')
surgeWfos.add(wfo)
except:
msg = "No edit area found. Removing " + wfo + \
" from further processing."
self.statusBarMsg(msg, 'U')
# Get the Hazards grid
hazardGridList = self.getGrids(self._mutableID, WEname, "SFC",
TimeRange.allTimes(), mode="List",
noDataError=0)
# print "hazardGridList =", hazardGridList
# If there are no hazard grids
if hazardGridList is None:
hazardGridList = []
# Look at each WFO which needs a message
for (hazardBytes, hazardKeys) in hazardGridList:
# print "Starting to examine hazards"
# Look at each hazard key in this grid - except the first, <None>
for (index, key) in enumerate(hazardKeys):
# Ignore the <None> and <Invalid> keys
if key in ["<None>", "<Invalid>"]:
continue # do not bother looking further
# print "\n\nLooking at ", index, key
# Check this key for either storm surge (SS), or wind (HU, TR)
# hazards
if re.search("(SS|HU|TR).[AW]", key) is not None:
# print "found a tropical hazard"
hazardType = "both" # assume both hazards are here
#-----------------------------------------------------------
# See if which type of hazard this is
# Wind hazard, no surge hazard
if re.search("(HU|TR).[AW]", key) is not None and \
re.search("SS.[AW]", key) is None:
hazardType = "wind-only"
# Surge hazard, no wind hazard
elif re.search("SS.[AW]", key) is not None and \
re.search("(HU|TR).[AW]", key) is None:
hazardType = "surge-only"
# See where this hazard is on the grid
hazardMask = hazardBytes == index
# Now determine which offices we need to notify
for wfo, wfoMask in officeMasks.items():
# See if this office has a current hazard
overlay = (officeMasks[wfo] & hazardMask)
# If there are any points which overlap
if overlay.any():
# We need to look at all the zones associated
# with this WFO, get them
zoneList = self.findWfoZones(wfo)
if len(zoneList) == 0:
msg = "\tCould not get zones for " + wfo
LogStream.logProblem(msg)
continue
# Now, process each zone
for zone in zoneList:
# Get the mask for this zone
try:
zoneMask = self.encodeEditArea(zone)
except:
msg = "\tCould not get zone mask for " + wfo
LogStream.logProblem(msg, LogStream.exc())
continue
# # If we did not get this mask - move on
# if zoneMask is None:
# continue
# See if there is an overlap with current
# hazard type
zoneOverlap = zoneMask & hazardMask
#=======================================================================
# This code kept in case we need to enforce the 3% area of a zone
# requirement in the future. This would mimic the process of the text
# formatters.
# Count all the points of the masks
# countOverlap = np.count_nonzero(zoneOverlap)
# countMask = np.count_nonzero(zoneMask)
#
# # See if there are enough points to justify
# # keeping this zone in the list
# zonePercent = (
# float(countOverlap) / float(countMask)
# )
# print "overlap = %d\tmask = %d\tpercent =%.2f" % \
# (countOverlap, countMask, zonePercent)
#
# If the percentage is high enough
# if int((zonePercent*100.0) + 0.5) >= percentThresh:
#
#=======================================================================
# For now, notify any zone which has a
# possibility for a storm surge hazard
if zoneOverlap.any():
# We need to notify this WFO
if hazardType == "wind-only":
msg = "Adding to wind - " + wfo
windWfos.add(wfo)
elif hazardType == "surge-only":
msg = "Adding to surge - " + wfo
surgeWfos.add(wfo)
else:
msg = "Adding to both - " + wfo
bothWfos.add(wfo)
self.statusBarMsg(msg, 'R')
print(msg)
# No point in looking at further zones
break
#=======================================================================
# Now ensure we do not duplicate WFOs with both hazards in the
# individual hazard sets. Use this code when we are no longer using
# the text TCV to notify WFOs of tropical wind hazards.
# for wfo in bothWfos:
# if wfo in windWfos:
# windWfos.discard(wfo)
# if wfo in surgeWfos:
# surgeWfos.discard(wfo)
#=======================================================================
# Now ensure we do not duplicate WFOs with both hazards in the
# individual hazard sets - this is for the 2016 season
for wfo in bothWfos:
surgeWfos.add(wfo)
# Reset the sets for "both" and "wind-only" WFOs
bothWfos = set()
windWfos = set()
# Return the completed WFO notification list
return (list(bothWfos), list(windWfos), list(surgeWfos))
# Define a method to find zones associated with a WFO
def findWfoZones(self, wfo):
# Construct the SQL to get these attributes from the maps database
reqParms = {'datatype' : 'maps',
'table' : 'mapdata.zone',
'locationField' : 'cwa',
'geomField' : 'the_geom',
'locationNames' : [wfo.strip()],
'parameters' : ['state', 'zone'],
}
# Create the Data Access request
req = DataAccessLayer.newDataRequest(**reqParms)
# Process the response
result = DataAccessLayer.getGeometryData(req)
# Check if we got a response
if not result:
self.statusBarMsg("Bad result in Map request for map: " + wfo, "S")
# Get ready to track matching zones
zoneSet = set()
# Process the response contents
for record in result:
# Retrieve state and zone
state = record.getString('state')
zone = record.getString('zone')
# Construct a UGC code and store it for later
zoneSet.add(state + "Z" + zone)
# Return the completed zone set
return zoneSet
# Define a method to find zones associated with a WFO
def notifyWFOs(self, field, anyChanges=None, testMode=None):
# anyChanges is a mask, where True means a change in hazards happened
# Ensure the test mode status is set - one way or the other
if testMode is None:
testMode = self._testMode
# # Get the status of each WFO's communications
# wfoStatus = self.getWfoStatus()
# See which WFOs we need to notify
(bothWfos, windWfos, surgeWfos) = self.getWfosAttention(field,
anyChanges)
# Send a message to each office
# message = "%s grids containing tropical, wind and storm surge hazards"%\
# (field)
# self.sendMessageToWfos(bothWfos, message, self._testMode)
# message = "%s grids containing tropical, wind hazards" % (field)
# self.sendMessageToWfos(windWfos, message, self._testMode)
message = "%s grids containing tropical, storm surge hazards" % (field)
self.sendMessageToWfos(surgeWfos, message, testMode)
#===============================================================================
# Code to process StormInfo files -
#===============================================================================
def _synchronizeAdvisories(self):
# Retrieving a directory causes synching to occur.
# This code can throw an exception but don't catch it
# so that forecasters can be made aware of the issue.
file = LocalizationSupport.getLocalizationFile(
LocalizationSupport.CAVE_STATIC,
LocalizationSupport.SITE, self.getSiteID(),
self._getAdvisoryPath()).getFile()
return file
# Constructs the absolute path to the JSON files for this site
def _getLocalAdvisoryDirectoryPath(self):
file = self._synchronizeAdvisories()
path = file.getPath()
try:
os.makedirs(path)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise
return path
# Retrieves the names of the active storm JSON files for further processing
def _getStormAdvisoryNames(self, filterATOnly=True):
advisoryDirectoryPath = self._getLocalAdvisoryDirectoryPath()
filenames = os.listdir(advisoryDirectoryPath)
allAdvisories = [filename for filename in filenames if filename[-5:] == ".json"]
if filterATOnly:
stormAdvisories = [filename for filename in allAdvisories if filename[:2] == "AT"]
return stormAdvisories
return allAdvisories
# Loads a JSON storm record
def _loadAdvisory(self, advisoryName):
self._synchronizeAdvisories()
fileName = self._getAdvisoryFilename(advisoryName)
try:
pythonDict = JsonSupport.loadFromJson(LocalizationSupport.CAVE_STATIC,
self.getSiteID(), fileName)
statFileName = os.path.join(os.environ["HOME"], "caveData", "etc",
"site", self.getSiteID(), fileName)
return pythonDict
except Exception as e:
self.statusBarMsg("Error when loading JSON file:" + advisoryName, "S")
return None
# Saves a JSON storm record
def _saveAdvisory(self, advisoryName, advisoryDict):
self._synchronizeAdvisories()
fileName = self._getAdvisoryFilename(advisoryName)
print("Saving %s to %s" % (advisoryName, fileName))
print("advisoryDict: %s" % (pp.pformat(advisoryDict)))
# Time stamp the storminfo
now = self._gmtime().unixTime()
advisoryDict["lastModified"] = now
# Save this advisory for all active sites
for siteID in self._activeSiteIDs:
try:
JsonSupport.saveToJson(LocalizationSupport.CAVE_STATIC,
siteID, fileName, advisoryDict)
except Exception as e:
print("Save Exception for %s : %s" % (fileName, e))
else: # No exceptions occurred
print("Wrote file contents for: %s" % (fileName))
# Purposely allow this to throw
self._synchronizeAdvisories()
# Helper method which identifies where the JSON records go, based on GFE
# operating mode. PRACTICE mode requires the files be placed in a
# different location in the Localization store
def _getAdvisoryPath(self):
gfeMode = self.gfeOperatingMode()
if gfeMode == "PRACTICE":
return os.path.join("gfe", "tcvAdvisories", "practice")
else:
return os.path.join("gfe", "tcvAdvisories")
# Helper method which constructs the absolute filename for a JSON record
def _getAdvisoryFilename(self, advisoryName):
advisoryFilename = os.path.join(self._getAdvisoryPath(), advisoryName)
if not advisoryFilename.endswith(".json"):
advisoryFilename += ".json"
return advisoryFilename
# Helper method which coordinates the actual extraction of JSON records
# into our Python environment
def extractStormInfo(self, filterATOnly=True):
# Sync the CAVE localization store
self._synchronizeAdvisories()
# Get the list of all available storm advisories
fileList = self._getStormAdvisoryNames(filterATOnly)
# Get the storm information from each advisory
stormList = []
for f in fileList:
# Load this storm info
curStorm = self._loadAdvisory(f)
# Create a dictionary for this storm
stormList.append(curStorm)
return stormList
def determineStorm (self, stormList, bogusStormName):
# Decide if this is a new storm or if we need to pre-populate info from existing storm
## stormList = self.extractStormInfo()
stormNames = []
for sDict in stormList:
stormNames.append(sDict["stormName"])
stormNames.append("New")
# Make the variableList dynamically based on the storm info
variableList = []
variableList.append(("Choose Storm", bogusStormName, "radio",
stormNames))
# Display the GUI
varDict = {}
processVarList = ProcessVariableList.ProcessVariableList(
"Choose Existing Storm or New Storm", variableList, varDict)
status = processVarList.status()
varDict = processVarList.varDict()
if status.upper() != "OK":
self.cancel()
# Make sure they only choose one storm
selectedName = varDict["Choose Storm"]
return selectedName
#===============================================================================
# Miscellaneous helper methods
#===============================================================================
# Extract just the wind hazards from the specified hazard grid.
def extractWindHazards(self, hazardGridList,
windHazards=["TR.W", "TR.A", "HU.W", "HU.A"]):
#hazGrid, hazKeys = hazardGridList[hazWindIndex]
# Make new empty wind hazard grid
windHazGrid = self.empty(np.int8)
windKeys = ["<None>"]
# Find the hazardGrid that contains any windHazards.
# Reverse the list first so we search backwards
hazardGridList.reverse()
hazardGrid = None
for grid, keys in hazardGridList:
if hazardGrid is not None:
break
for key in keys:
for windHaz in windHazards:
# If we find a windHazard, save that grid
if key.find(windHaz):
hazardGrid = (grid, keys)
# If we didn't find any wind hazards above, return the empty grid
if hazardGrid is None:
return (windHazGrid, windKeys)
# Extract just the wind hazards from the grid we found
hazGrid, hazKeys = hazardGrid
for hazKey in hazKeys:
phen = self.keyPhen(hazKey)
sig = self.keySig(hazKey)
phenSig = phen + "." + sig
if phenSig in windHazards:
hazIndex = self.getIndex(hazKey, hazKeys)
windIndex = self.getIndex(hazKey, windKeys)
windHazGrid[hazGrid == hazIndex] = windIndex
return (windHazGrid, windKeys)
# Merge the specified Discrete grid into the Hazard grid.
def mergeDiscreteGrid(self, mergeHazGrid, timeRange):
mergeGrid, mergeKeys = mergeHazGrid
for mergeKey in mergeKeys:
mergeIndex = self.getIndex(mergeKey, mergeKeys)
mask = mergeGrid == mergeIndex
self._hazUtils._addHazard("Hazards", timeRange, mergeKey, mask)
return
def variableExists(self, modelName, weName, weLevel):
# it turns out the the modelName will not match the dbID().model()
# directly, so it needs to be massaged.
modelPos = modelName.find("_D2D_")
if modelPos > -1:
modelName = modelName[modelPos+5:]
availParms = self.availableParms()
for pName, level, dbID in availParms:
if modelName in dbID.model():
if weName in pName and weLevel in level:
return True
return False
def getAvgTopoGrid(self, topodb):
siteID = self.getSiteID()
dbName = siteID + "_D2D_" + topodb
weName = "avgTopo"
trList = self.GM_getWEInventory(weName, dbName)
# Get the GFE topo
topoGrid = self.getGrids(dbName, weName, "SFC",
trList[0], mode="First")
# Convert from meters to feet
topoGrid /= 0.3048
topoGrid[topoGrid < -16000] = -80.0
mask = topoGrid > 16000
topoGrid[mask] = self.getTopo()[mask]
return topoGrid
def removeEarlierTRs(self, weName):
# Get an inventory of all the grids
trList = self.GM_getWEInventory(weName, self._mutableID)
# Keep the latest grid
del trList[-1]
# Remove all other grid we found
for tr in trList:
self.deleteCmd([weName], tr)
return
def getParmMinMaxLimits(self, modelName, weName):
# Get the info for this parameter
parm = self.getParm(modelName, weName, "SFC")
# Return the valid min and max values
return (parm.getGridInfo().getMinValue(),
parm.getGridInfo().getMaxValue())
# Define a method to sort breakpoint record keys
def sortBreakpoints(self, a, b):
# Make a list of valid string parts
validTypes = [
"LN", # mainland segments
"KEY", # Florida Keys
"ISL", # islands
"CUBA", # Cuba
"HISP", # Hispaniola
"NAI", # North Atlantic islands
"WTDE", # Deleware Bay
"WTTP", # Tidal Potomac
"WTCP", # Chesapeake Bay
"WTPT", # Generic water points
"GYC", # Guyana
"VEC", # Venezuela
"COC", # Colombia
"PAC", # Panama
"CRC", # Costa Rica
"NIC", # Nicaragua
"HNC", # Honduras
"GTC", # Guatemala
"BZC", # Belize
"MXC", # Mexico
"USC", # United States
"CNC", # Canada
"KEC", # Dry Tortugas
"AWC", # Aruba
"CWC", # Curacao
"TTC", # Trinidad and Tobago
"BBC", # Barbados
"LCC", # St. Lucia
"MQC", # France - Caribbean
"AGC", # Antigua and Barbuda
"BSC", # Bahamas
"BMC", # Bermuda
"JMC", # Jamaica
"KYC", # Cayman Islands
"CUC", # Cuba
"DOC", # Dominican Republic
"HTC", # Haiti
"PMC", # France - North Atlantic
"LOC", # Lake_Okeechobee
"FBC", # Florida Bay
"PSC", # Pamlico Sound
"ASC", # Albemarle Sound
"TXZ", # Texas
"LAZ", # Louisiana
"MSZ", # Mississippi
"ALZ", # Alabama
"FLZ", # Florida
"GAZ", # Georgia
"SCZ", # South Carolina
"NCZ", # North Carolina
"VAZ", # Virginia
"MDZ", # Maryland
"DCZ", # District of Columbia
"DEZ", # Deleware
"NJZ", # New Jersey
"NYZ", # New York
"CTZ", # Connecticut
"RIZ", # Rhode Island
"MAZ", # Massachusetts
"NHZ", # New Hampshire
"MEZ", # Maine
"NMZ", # New Mexico
"ARZ", # Arkansas
"OKZ", # Oklahoma
"MOZ", # Missouri
"TNZ", # Tennessee
"WVZ", # West Virginia
"PAZ", # Pennsylvania
"VTZ", # Vermont
"PRZ", # Puerto Rico
"VIZ", # U.S. Virgin Islands
"RE", # General edit area collection
]
aSeg = a.split("_")[0]
bSeg = b.split("_")[0]
aSegType = ""
bSegType = ""
aSegNum = ""
bSegNum = ""
for c in aSeg:
if c.isalpha():
aSegType += c
for c in bSeg:
if c.isalpha():
bSegType += c
for c in aSeg:
if c.isdigit():
aSegNum += c
for c in bSeg:
if c.isdigit():
bSegNum += c
aTypeIndex = validTypes.index(aSegType)
bTypeIndex = validTypes.index(bSegType)
if aTypeIndex < bTypeIndex:
return -1
elif bTypeIndex < aTypeIndex:
return 1
if int(aSegNum) < int(bSegNum):
return -1
elif int(bSegNum) < int(aSegNum):
return 1
else:
self.statusBarMsg("ERROR!!!!!!! Segment names are equal!!!!!!!", "S")
return 0
#===============================================================================
# Hazard grid helper methods
#===============================================================================
# Extracts the specified hazard from the hazardGrid. Returns a list of
# keys, mask pairs where each hazard exists.
def extractHazards(self, hazardGrid, hazard):
hazGrid, hazKeys = hazardGrid
keyMaskList = []
for hazIndex, hazKey in enumerate(hazKeys):
if hazard in hazKey:
# See if this key covers any portion of the domain
mask = hazGrid == hazIndex
if not mask.any():
continue
# Pair this key with its mask
keyMaskList.append((hazKey, mask))
return keyMaskList
def purifyKey(self, hazKey, allowedKeys):
# Get ready to process some subkeys
subKeyList = set()
subKeys = hazKey.split("^")
# Process all the hazard subkeys
for subKey in subKeys:
# Go over all the allowed Hazard keys
for allowedKey in allowedKeys:
# If this is one of them
if allowedKey in subKey:
# Add it to the subkey list - if not already there
if allowedKey not in subKeyList:
subKeyList.add(subKey)
# Return the final key
return "^".join(subKeyList)
def mergeCertainHazards(self, initalGrid, gridToMerge, hazTR,
selectedHazards=["SS.W", "SS.A"]):
# Use the Proposed grid is now the one to use for GFE hazards, for now
HazardUtils.ELEMENT = "ProposedSS"
# Split the initial grid into its components
initialBytes, initialKeys = initalGrid
# Look for all the hazards we wish to keep
for haz in selectedHazards:
# Find all the areas in the domain where this hazard exists
keyMaskList = self.extractHazards(gridToMerge, haz)
# Process all areas identified to have current tropical hazard
for hazKey, hazMask in keyMaskList:
# Filter out the hazards we do not want
pureHazKey = self.purifyKey(hazKey, selectedHazards)
# If there is nothing left to do, move on to next hazard
if pureHazKey == "":
continue
# Merge these hazards into the initial grid
hazIndex = self.getIndex(pureHazKey, initialKeys)
self._hazUtils._addHazard("ProposedSS", hazTR, pureHazKey,
hazMask, combine=1)
# Make sure the Hazards grid is now the one to use for GFE hazards
HazardUtils.ELEMENT = "Hazards"
# Return the merged grid
return (initialBytes, initialKeys)
#===============================================================================
# Generic method to display product text via a GFE procedure/smartTool
#===============================================================================
def displayProduct(self, product):
"""
Displays the product text. Returns true if forecaster clicked OK
"""
from com.raytheon.viz.gfe.ui.runtimeui import ValuesDialog
varList = []
varList.append(("Product Text:", "", "label"))
varList.append((product, "", "label"))
varList.append(("Click OK to transmit the product", "", "label"))
widgetList = self.getVariableListInputs(varList)
dialog = ValuesDialog.openDialog("Text Product", widgetList, None)
return dialog.getReturnCode() == 0 # 0 is OK, 1 is CANCEL