1256 lines
47 KiB
Python
1256 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
|