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

2516 lines
88 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.
# ----------------------------------------------------------------------------
##
# Author: lefebvre
#
# HSF
#
# SOFTWARE HISTORY
# Date Ticket# Engineer Description
# ----------- ---------- ----------- --------------------------
# 03/09/2015 - tlefebvre modified formatPolygonEditArea to add
# leading zero when lat < 10
# 07/29/2016 - tlefebvre Changed edit area retrieval and storage to
# work outside CAVE so edit areas could be shared.
# 12/20/2017 DCS17686 tlefebvre Initial baseline version.
# 05/22/2018 DR20724 tlefebvre Remove from menus by default since most
# sites will not use these tools
#
##
# ----------------------------------------------------------------------------
# The MenuItems list defines the GFE menu item(s) under which the
# Procedure is to appear.
# Possible items are: Populate, Edit, Consistency, Verify, Hazards
MenuItems = []
import SmartScript
import tkinter
import tkinter.messagebox
import time
import math
import MetLib
import numpy as np
import pickle, os, copy, stat
import TimeRange
import AbsTime
import EditAreaUtilities
import sys
from com.raytheon.uf.common.dataplugin.gfe.reference import ReferenceData
class Procedure (SmartScript.SmartScript):
def __init__(self, dbss):
SmartScript.SmartScript.__init__(self, dbss)
# The following section reduces a polygon to just the essential points
# Calculate the angle formed by the center and y, x
def calcAngle(self, centerY, centerX, y, x):
lat1 = centerY * self._degToRad
lon1 = centerX * self._degToRad
lat2 = y * self._degToRad
lon2 = x * self._degToRad
yDiff = (lat2 - lat1) * self._degToRad
xDiff = (lon2 - lon1) * self._degToRad
angle = math.atan2(yDiff, xDiff) / self._degToRad
if angle < 0:
angle = angle + 360.0
return angle
# calculate the great circle distance between the two specified points
# WARNING: resulting distances can be negative
def distance(self, lat1, lon1, lat2, lon2):
lat1 = lat1 * self._degToRad
lon1 = lon1 * self._degToRad
lat2 = lat2 * self._degToRad
lon2 = lon2 * self._degToRad
# some issue with acos and large numbers throws an except
if abs(lon2 - lon1) < 0.000001:
return (lat2 - lat1) / self._degToRad
dist = math.acos(np.sin(lat1) * np.sin(lat2) + np.cos(lat1) * np.cos(lat2) * \
np.cos(lon2 - lon1)) / self._degToRad
return dist
# Debugging method to print coordinates in an easy to read format
def printCoords(self, i, x0, y0, x1, y1, x2, y2):
print("%d---" % i, "%.2f" % y0, "%.2f" % x0, "%.2f" % y1, "%.2f" % x1, "%.2f" % y2, "%.2f" % x2)
return
# Returns a list of lat/lons that represent the specified edit area.
# These points approximate the edit area with no more than maxPoints
# points.
def simplifyEditArea(self, editArea):
polygons = editArea.getPolygons(ReferenceData.CoordinateType.LATLON)
coords = polygons.getCoordinates()
coordList = []
for c in coords:
coordList.append((c.x, c.y))
# Algorithm configuration section. These values will affect how
# well (or poorly) the algorithm works.
# Define the number of slices. More gives better radial resolution
# at the price of slightly slower execution.
slices = 36
# The resulting simplified area will contain no more than
# this number of points
maxPoints = 6
# Any triangle whose area is less than this ratio, the middle
# point gets tossed as being not significant.
areaThreshold = 0.05
### End configuration section
# Make fixed size arrays of length pieSlices for each access
maxDist = slices * [0]
# Initialize this list to None so we know if no points were
# found in each slice.
rawLoc = slices * [None]
# calculate the center of the polygon
centroid = polygons.getCentroid()
latCenter = centroid.getY()
lonCenter = centroid.getX()
originalArea = polygons.getArea()
# reduce the number of points to a manageable number by
# breaking the edit area up into pie slices and removing
# all points in each slice except the outer most point in
# each pie slice.
for x, y in coordList:
angle = self.calcAngle(latCenter, lonCenter, y, x)
angleIndex = int(angle / (360.0 / slices))
dist = self.distance(latCenter, lonCenter, y, x)
if dist > maxDist[angleIndex]:
maxDist[angleIndex] = dist
rawLoc[angleIndex] = (y, x)
# it's possible no points were found in some sices, so filter them out
loc = []
for i in rawLoc:
if not i is None:
loc.append(i)
while True: # loop until we can't remove any more points
prevCount = len(loc)
minArea = originalArea
areaList = []
for i in range(len(loc)):
# figure out the index of the previous and next points
last = i - 1
next = i + 1
if next >= len(loc):
next = 0
x0 = loc[i][1]
y0 = loc[i][0]
x1 = loc[last][1]
y1 = loc[last][0]
x2 = loc[next][1]
y2 = loc[next][0]
# make sure the slopes are not zero or infinity
if x2 == x1:
x2 = x2 + 0.000001
if y2 == y1:
y2 = y2 + 0.000001
# it's now safe to calculate the slope of the base of the triagle
slope = (y2 - y1) / (x2 - x1)
# calculate the point on a line connecting last and next
# points that is perpendicular to the middle point
x = (slope * x1 - y1 + (x0 / slope) + y0) / (slope + (1 / slope))
y = ((x0 - x) / slope) + y0
# calculate the area of the triangle formed by the three points:
# next, current, and previous
height = self.distance(y, x, y0, x0)
base = self.distance(y1, x1, y2, x2)
area = abs(base * height / 2.0) # note area can be negative!
areaList.append(area)
# save the area if it's the smallest
if area < minArea:
minArea = area
delIndex = i
######################## end looping through all points ##############
# calculate the ratio of the area and the original area of the edit area
# Use this later to decide whether to remove the point
ratio = minArea / originalArea
if len(loc) > maxPoints:
del loc[delIndex]
continue
else:
if ratio < areaThreshold:
del loc[delIndex]
# If we're down to 3 points: stop
if len(loc) <= 3:
break
#If no new points have been removed, we're done
if prevCount == len(loc):
break
return loc
################### End polygon code ##################################
# Fetches the inventory in the form of a timeRange list
def getWEInventory(self, modelName, weName, timeRange=None):
if timeRange is None:
timeRange = TimeRange.allTimes()
trList = []
# getGridInfo will just die if the modelName or weName is not valid
# so wrap it in a try block and return [] if it fails
try:
gridInfo = self.getGridInfo(modelName, weName, "SFC", timeRange)
except:
return trList
for g in gridInfo:
if timeRange.overlaps(g.gridTime()):
trList.append(g.gridTime())
return trList
# Create the list box that holds the names of the scripts
def makeListBox(self):
labelList = self.getAllEditAreaNames()
labelList.sort()
label = tkinter.Label(self._listFrame, text="Edit Area List")
label.grid(row=0, padx=15, pady=10)
scrollbar = tkinter.Scrollbar(self._listFrame, orient=tkinter.VERTICAL)
self._listbox = tkinter.Listbox(self._listFrame, yscrollcommand=scrollbar.set,
selectmode=tkinter.MULTIPLE)
self._listbox.bind("<ButtonRelease-1>", self.listBoxClick)
#self._listbox.bind("<ButtonPress-1>", self.listBoxClick)
for i in range(len(labelList)):
self._listbox.insert(tkinter.END, labelList[i])
self._listbox.grid(row=1, padx=5, pady=5)
scrollbar.config(command=self._listbox.yview)
scrollbar.grid(row=1, column=1, sticky=tkinter.N+tkinter.S)
return
# Make the time period buttons
def makeTimeButtons(self, timeFrame):
label = tkinter.Label(timeFrame, text="Select Time\nPeriod")
label.grid(row=0, padx=15, pady=10)
labelList = []
for i in range(0, 49, 6):
label = str(i)
if len(label) < 2:
label = "0" + label
label = label + "h"
labelList.append(label)
buttonRow = 1
for i in range(len(labelList)):
b = tkinter.Radiobutton(timeFrame, text=labelList[i],
value=labelList[i])
if labelList[i] == self._defaultTime:
b.select()
self._currentTimePeriod = self._defaultTime
b.grid(row=buttonRow, column=0, sticky=tkinter.N+tkinter.S, pady=2)
buttonRow = buttonRow + 1
b.bind("<ButtonRelease-1>", self.timeButtonClick)
return
# Callback that fires when a list selector is clicked.
def listBoxClick(self, event=None):
if self._textBoxCurSelection is not None:
self.textBoxReturnPress()
# Disable all but the Add New button
if len(self._listbox.curselection()) == 0:
self._clearSelButton.configure(state=tkinter.DISABLED)
self._replaceSelButton.configure(state=tkinter.DISABLED)
self._saveSelButton.configure(state=tkinter.DISABLED)
self._killSelButton.configure(state=tkinter.DISABLED)
# return
# Display the edit area(s) on the GFE
gridSize = (self._gridLoc.gridSize().y, self._gridLoc.gridSize().x)
mask = np.zeros(gridSize, bool)
for i in self._listbox.curselection():
curIndex = int(i)
editAreaName = self._listbox.get(curIndex)
mask = self._eaUtils.fetchEditArea(editAreaName)
if mask is None:
self.statusBarMsg("Edit Area: " + editAreaName + " not found in HighSeas repository.", "S")
return
editArea = self.decodeEditArea(mask)
self.setActiveEditArea(editArea)
# Update the textBox
if sum(sum(mask)) > 0:
editAreaDesc = self.getEditAreaDesc(editAreaName)
self._textBox.configure(font=self._boldFont)
self.updateTextBoxSaveState("Saved")
self.displayAreaDesc(editAreaDesc)
if len(self._listbox.curselection()) != 1:
# Disable the Replace and Save Buttons
self._replaceSelButton.configure(state=tkinter.DISABLED)
self._saveSelButton.configure(state=tkinter.DISABLED)
self._killSelButton.configure(state=tkinter.DISABLED)
else:
self._replaceSelButton.configure(state=tkinter.NORMAL)
self._clearSelButton.configure(state=tkinter.NORMAL)
self._saveSelButton.configure(state=tkinter.NORMAL)
self._killSelButton.configure(state=tkinter.NORMAL)
return
def enterEvent(self, event=None):
sys.stdout.flush()
self._enterCount = self._enterCount + 1
if self._enterCount > 4:
self.setActiveEditArea(self._initialEditArea)
self._master.unbind("<Enter>")
return
def redrawListBox(self):
self._listbox.delete(0, tkinter.END)
listItems = []
for fDict in self._hsDesc:
for areaDict in fDict["areaList"]:
listItems.append(areaDict["areaName"])
listItems.sort()
for i in range(len(listItems)):
self._listbox.insert(tkinter.END, listItems[i])
return
def selectListBoxItem(self, item):
listBoxItems = self._listbox.get(0, tkinter.END)
listBoxItems = list(listBoxItems)
selectedIndex = listBoxItems.index(item)
self._listbox.selection_set(selectedIndex)
return
def setSelectButtonState(self, buttonState):
self._clearSelButton.configure(state=buttonState)
self._replaceSelButton.configure(state=buttonState)
self._saveSelButton.configure(state=buttonState)
self._killSelButton.configure(state=buttonState)
return
def timeButtonClick(self, event):
# the event's widget value is a tuple and we want the last one
buttonText = event.widget.config("value")[-1]
# for some reason, inserting a \n makes that entry a tuple
# so just concatenate the tuple elements into a single string
if type(buttonText) is tuple:
valueText = ""
for t in buttonText:
valueText = valueText + t + " "
valueText = valueText[0:-1]
else:
valueText = buttonText
self._currentTimePeriod = valueText
return
# Called when the pie is click upon by the mouse
def pieClick(self, event):
xCenter = self._circleRadius + (self._circleOffset / 2.0)
yCenter = self._circleRadius + (self._circleOffset / 2.0)
xDiff = event.x - xCenter
yDiff = event.y - yCenter
angle = -math.atan2(yDiff, xDiff) * 360 / (2 * math.pi)
x1, y1, x2, y2 = self.getCircleCoords()
# truncate the angle between the center and the click location
angleStart = int(angle / 45.0) * 45.0
angleExtent = 45.0
if angle < 0:
angleExtent = -45.0
sliceAngle = angle
if angle < 0:
sliceAngle = angle + 360
sliceNum = int(sliceAngle / 45.0)
fillColor = self._pieDeselectColor
ButtonDownEvent = "4"
ButtonUpEvent = "5"
if event.type == ButtonDownEvent and not self._pieState[sliceNum] :
self._selectingPieSlices = True
self._deselectingPieSlices = False
elif event.type == ButtonDownEvent and self._pieState[sliceNum]:
self._selectingPieSlices = False
self._deselectingPieSlices = True
elif event.type == ButtonUpEvent:
self._selectingPieSlices = False
self._deselectingPieSlices = False
self.makePolarEditArea()
return
# select a pieSlice if appropriate
if self._selectingPieSlices:
self._pieState[sliceNum] = True
elif self._deselectingPieSlices:
self._pieState[sliceNum] = False
# If we're not selecting or deselecting, do nothing
if not self._selectingPieSlices and not self._deselectingPieSlices:
return
if self._selectingPieSlices:
fillColor = self._pieSelectColor
elif self._deselectingPieSlices:
fillColor = self._pieDeselectColor
self._canvas.create_arc(x1, y1, x2, y2, style=tkinter.PIESLICE,
start=angleStart, extent=angleExtent, fill=fillColor)
return
# Turns on one pieSlice if they are all off. This is so it shows up on the
# screen
def activatePie(self):
# Don't bother if any slices are already on
for state in self._pieState:
if state:
return
# Make a fake event so pieClick will be called.
xCenter = self._circleRadius + (self._circleOffset / 2.0)
yCenter = self._circleRadius + (self._circleOffset / 2.0)
xPos = xCenter + 10
yPos = yCenter + 3
self._canvas.event_generate("<ButtonRelease-1>", x=xPos, y=yPos)
return
def prettyLatString(self, latValue):
if latValue >= 0:
return str(latValue) + "N"
return str(latValue)[1:] + "S"
def prettyLonString(self, lonValue):
if lonValue >= 0:
return str(lonValue) + "E"
else:
if lonValue < -180.0:
lonValue = lonValue + 360.0
return str(lonValue) + "E"
return str(lonValue)[1:] + "W"
def latScaleMotion(self, value):
value = float(value)
if value == self._latScaleValue:
return
self._latScaleValue = value
latStr = self.prettyLatString(value)
self._latEntry.delete(0, tkinter.END)
self._latEntry.insert(0, latStr)
self.activatePie()
self.makePolarEditArea()
return
def lonScaleMotion(self, value):
value = float(value)
if value == self._lonScaleValue:
return
self._lonScaleValue = value
lonStr = self.prettyLonString(value)
self._lonEntry.delete(0, tkinter.END)
self._lonEntry.insert(0, lonStr)
self.activatePie()
self.makePolarEditArea()
return
def getLatLonGrids(self):
trList = self.getWEInventory("Fcst", "lat")
if len(trList) == 0:
gridLoc = self.getGridLoc()
latGrid, lonGrid = MetLib.getLatLonGrids(gridLoc)
else:
latGrid = self.getGrids("Fcst", "lat", "SFC", trList[0])
lonGrid = self.getGrids("Fcst", "lon", "SFC", trList[0])
return latGrid, lonGrid
def radiusScaleMotion(self, value):
value = float(value)
if value == self._radiusScaleValue:
return
self._radVar.set(str(value))
self._radEntry.delete(0, tkinter.END)
self._radEntry.insert(0, str(value))
self._radiusScaleValue = value
self.activatePie()
self.makePolarEditArea()
return
# A single place where the circle coords can be had.
def getCircleCoords(self):
topLeftX = self._circleOffset
topLeftY = self._circleOffset
bottomRightX = self._circleRadius * 2
bottomRightY = self._circleRadius * 2
return topLeftX, topLeftY, bottomRightX, bottomRightY
def filterTextEntry(self, entryStr, allowedChars):
returnStr = ""
for c in entryStr:
if allowedChars.find(c) > -1:
returnStr = returnStr + c
return returnStr
def parseLatitude(self):
latStr = self._latEntry.get()
sign = 1.0
if latStr.find("S") > -1 or latStr.find("s") > -1:
sign = -1.0
latStr = latStr[0:-1]
elif latStr.find("N") > -1 or latStr.find("n") > -1:
latStr = latStr[0:-1]
try:
latValue = float(latStr) * sign
except:
self.statusBarMsg("Invalid latitude value: " + str(self._lonEntry.get()), "S")
return
return latValue
def parseLongitude(self):
lonStr = copy.copy(self._lonEntry.get())
sign = 1.0
if lonStr.find("W") > -1 or lonStr.find("w") > -1:
sign = -1.0
lonStr = lonStr[0:-1]
elif lonStr.find("E") > -1 or lonStr.find("e") > -1:
lonStr = lonStr[0:-1]
try:
lonValue = float(lonStr) * sign
if lonValue > 0:
lonValue = lonValue - 360
except:
self.statusBarMsg("Invalid longitude value: " + str(self._lonEntry.get()), "S")
return
return lonValue
def latKeyPress(self, event):
# Ignore certain special characters
ignore = ["BackSpace"]
if event.keysym in ignore:
return
if event.keysym == "Return":
self._latScaleValue = self.parseLatitude()
self.activatePie()
self.makePolarEditArea()
self._latScale.set(self._latScaleValue)
return
allowedChars = "0123456789-.NSns"
entryStr = self._latEntry.get() + event.char
filterStr = self.filterTextEntry(entryStr, allowedChars)
# replace the string if not valid
if filterStr != entryStr:
self._latVar.set(str(filterStr))
# Insert a backspace in the queue to remove the offending char
self._latEntry.event_generate("<KeyPress>", keysym="BackSpace",
when="tail")
return
def lonKeyPress(self, event):
# Ignore certain special characters
ignore = ["BackSpace"]
if event.keysym in ignore:
return
if event.keysym == "Return":
self._lonScaleValue = self.parseLongitude()
self.activatePie()
self.makePolarEditArea()
self._lonScale.set(self._lonScaleValue)
return
allowedChars = "0123456789-.EWew"
entryStr = self._lonEntry.get() + event.char
filterStr = self.filterTextEntry(entryStr, allowedChars)
# replace the string if not valid
if filterStr != entryStr:
self._lonVar.set(str(filterStr))
# Insert a backspace in the queue to remove the offending char
self._lonEntry.event_generate("<KeyPress>", keysym="BackSpace",
when="tail")
return
def radKeyPress(self, event):
# Ignore certain special characters
ignore = ["BackSpace"]
if event.keysym in ignore:
return
allowedChars = "0123456789."
entryStr = self._radEntry.get() + event.char
filterStr = self.filterTextEntry(entryStr, allowedChars)
try:
self._radiusScaleValue = float(filterStr)
except:
# replace the string if not valid
self._radVar.set(str(filterStr))
# Insert a backspace in the queue to remove the offending char
self._radEntry.event_generate("<KeyPress>", keysym="BackSpace", when="tail")
return
if event.keysym == "Return":
self._radiusScale.set(self._radiusScaleValue)
self.makePolarEditArea()
return
def makePolarWidgets(self):
# make the pie slices
x1, y1, x2, y2 = self.getCircleCoords()
canvasWidth = x2 + self._circleOffset
canvasHeight = y2 + self._circleOffset
self._canvas = tkinter.Canvas(self._polarFrame, width=canvasWidth,
height=canvasHeight)
self._canvas.grid(row=0, column=1, columnspan=2)
for i in range(0, 8):
self._canvas.create_arc(x1, y1, x2, y2, style=tkinter.PIESLICE,
start=i*45, extent=45, fill=self._pieDeselectColor)
self._canvas.bind("<Button-1>", self.pieClick)
self._canvas.bind("<ButtonRelease-1>", self.pieClick)
self._canvas.bind("<Motion>", self.pieClick)
# change from/to to minmax lat/lon values
self._latScale = tkinter.Scale(self._polarFrame, from_=self._maxLat, to=self._minLat,
orient=tkinter.VERTICAL, showvalue=0,
resolution=0.5, length=160,
command=self.latScaleMotion)
self._latScale.grid(row=0, column=0, sticky=tkinter.N)
self._latScale.set(self._latScaleValue)
self._latVar = tkinter.StringVar()
self._latVar.set(str(self._latScaleValue))
self._latEntry = tkinter.Entry(self._polarFrame, width=6, textvariable=self._latVar)
self._latEntry.grid(row=1, column=0, padx=10, sticky=tkinter.N)
self._latEntry.bind("<KeyPress>", self.latKeyPress)
latLabel = tkinter.Label(self._polarFrame, text="Latitude")
latLabel.grid(row=2, column=0, sticky=tkinter.N)
self._lonScale = tkinter.Scale(self._polarFrame, from_=self._minLon, to=self._maxLon,
orient=tkinter.HORIZONTAL, showvalue=0,
resolution=0.5, length=160, command=self.lonScaleMotion)
self._lonScale.grid(row=1, column=2, sticky=tkinter.N)
self._lonScale.set(self._lonScaleValue)
self._lonVar = tkinter.StringVar()
self._lonEntry = tkinter.Entry(self._polarFrame, width=6, textvariable=self._lonVar)
self._lonEntry.grid(row=1, column=1, sticky=tkinter.N)
self._lonEntry.bind("<KeyPress>", self.lonKeyPress)
lonLabel = tkinter.Label(self._polarFrame, text="Longitude")
lonLabel.grid(row=2, column=1, sticky=tkinter.NW)
self._radiusScale = tkinter.Scale(self._polarFrame, from_=30, to=900,
orient=tkinter.HORIZONTAL, showvalue=0,
resolution=10, length=160, command=self.radiusScaleMotion)
self._radiusScale.grid(row=3, column=2, pady=10, sticky=tkinter.W)
self._radiusScale.set(self._defaultRadius)
self._radVar = tkinter.StringVar()
self._radEntry = tkinter.Entry(self._polarFrame, width=6, textvariable=self._radVar)
self._radEntry.grid(row=3, column=0, pady=10, columnspan=2)
self._radEntry.bind("<Key>", self.radKeyPress)
label = tkinter.Label(self._polarFrame, text="Radius (nm)")
label.grid(row=4, column=0, columnspan=2, sticky=tkinter.N)
return
# Create the Execute and Cancel buttons
def makeBottomButtons(self):
button = tkinter.Button(self._bottomFrame, text="Cancel",
command=self.cancelCommand)
button.grid(row=0, column=0, padx=30, pady=10, sticky=tkinter.E)
button = tkinter.Button(self._bottomFrame, text="Save",
command=self.saveOnlyCommand)
button.grid(row=0, column=1, padx=30, pady=10, sticky=tkinter.E)
button = tkinter.Button(self._bottomFrame, text="Save and Exit",
command=self.saveExitCommand)
button.grid(row=0, column=2, padx=30, pady=10, sticky=tkinter.E)
self._bottomFrame.grid()
return
def saveOnlyCommand(self):
self.saveHSDescToFile()
return
def saveExitCommand(self):
self.saveHSDescToFile()
print("saving desc to file")
self.cancelCommand()
return
# Cancels the GUI and the tool.
def cancelCommand(self):
msg = "Your edits have not been saved. Cancel Anyway?"
if not self._editsMade:
self._master.destroy()
self._tkmaster.destroy()
return
if tkinter.messagebox.askokcancel("High Sea - Cancel", msg, parent=self._bottomFrame):
self._master.destroy()
self._tkmaster.destroy()
return
def getPolarInfo(self, editArea):
lat = None
lon = None
radius = None
pieState = None
try:
eaMask = self.encodeEditArea(editArea)
except:
eaMask = editArea
eaMask = eaMask & self._clipMask
polarMask = self.encodeEditArea(self._polarEditArea) & self._clipMask
if sum(sum(eaMask)) == sum(sum(polarMask)): # they are the same editArea
lat = self._latScaleValue
lon = self._lonScaleValue
radius = self._radiusScaleValue
pieState = self._pieState
return lat, lon, radius, pieState
# Return the max width and height of the edit area in lat/lon coordinates
def editAreaExtent(self, editArea):
polygons = editArea.getPolygons(ReferenceData.CoordinateType.LATLON)
coords = polygons.getCoordinates()
minX = 10000.0
minY = 10000.0
maxX = -10000.0
maxY = -10000.0
for c in coords:
minX = min(c.x, minX)
minY = min(c.y, minY)
maxX = max(c.x, maxX)
maxY = max(c.y, maxY)
diffX = maxX - minX
diffY = maxY - minY
return diffX, diffY
# Called when the save button is clicked.
def saveSelCommand(self):
editArea = self.getActiveEditArea()
# Check for null area
mask = self.encodeEditArea(editArea) & self._clipMask
if sum(sum(mask)) == 0:
self.statusBarMsg("Please select an edit area before saving.", "S")
return
# Check to ensure the edit area is large enough
diffLon, diffLat = self.editAreaExtent(editArea)
if diffLon < self._minEASize or diffLat < self._minEASize:
self.statusBarMsg("Please select a larger edit area before saving", "S")
return
editArea = self.decodeEditArea(mask)
self.setActiveEditArea(editArea)
# get the selected button
curSel = self._listbox.curselection()
if len(curSel) == 0:
return
listIndex = self._listbox.curselection()[0]
buttonStr = self._listbox.get(listIndex)
featureName = self.extractFeature(buttonStr)
timePeriod = self._currentTimePeriod
editAreaName = self.getNewAreaName(featureName, timePeriod)
# get the polar parameters
polarInfo = self.getPolarInfo(editArea)
if polarInfo is not None:
lat, lon, radius, pieState = polarInfo
else:
lat = None
lon = None
radius = None
pieState = None
editAreaDesc = self.makeEditAreaDesc(editArea, editAreaName, lat, lon, radius, pieState)
# save the area
activeArea = self.getActiveEditArea()
activeMask = self.encodeEditArea(activeArea)
self._eaUtils.saveEditArea(editAreaName, activeMask)
self.displayAreaDesc(editAreaDesc)
self.addNewArea(featureName, timePeriod, editAreaName, editAreaDesc,
lat, lon, radius, pieState)
self.redrawListBox()
self.selectListBoxItem(editAreaName)
self.setSelectButtonState(tkinter.NORMAL)
self._editsMade = True
def saveNewCommand(self):
editArea = self.getActiveEditArea()
# Check for null area
mask = self.encodeEditArea(editArea) & self._clipMask # clip to domain mask
if sum(sum(mask)) == 0:
self.statusBarMsg("Please select an edit area before saving.", "S")
return
# Check to ensure the edit area is large enough
diffLon, diffLat = self.editAreaExtent(editArea)
if diffLon < self._minEASize or diffLat < self._minEASize:
self.statusBarMsg("Please select a larger edit area before saving", "S")
return
editArea = self.decodeEditArea(mask)
self.setActiveEditArea(editArea)
timePeriod = self._currentTimePeriod
featureName = self.getNewFeatureName()
editAreaName = self.getNewAreaName(featureName, timePeriod)
# get the polar parameters
polarInfo = self.getPolarInfo(editArea)
if polarInfo is not None:
lat, lon, radius, pieState = polarInfo
else:
lat = None
lon = None
radius = None
pieState = None
editAreaDesc = self.makeEditAreaDesc(editArea, editAreaName, lat, lon, radius, pieState)
# save the area
activeArea = self.getActiveEditArea()
activeMask = self.encodeEditArea(activeArea)
self._eaUtils.saveEditArea(editAreaName, activeMask)
self.addNewFeature(featureName, timePeriod, editAreaName, editAreaDesc,
lat, lon, radius, pieState)
self.redrawListBox()
self.selectListBoxItem(editAreaName)
self.setSelectButtonState(tkinter.NORMAL)
self.displayAreaDesc(editAreaDesc)
self._editsMade = True
return
def replaceSelCommand(self):
listIndex = self._listbox.curselection()[0]
editAreaName = self._listbox.get(listIndex)
editArea = self.getActiveEditArea()
# Check to ensure the edit area is large enough
diffLon, diffLat = self.editAreaExtent(editArea)
if diffLon < self._minEASize or diffLat < self._minEASize:
self.statusBarMsg("Please select a larger edit area before saving", "S")
return
mask = self.encodeEditArea(editArea) & self._clipMask # clip to domain mask
editArea = self.decodeEditArea(mask)
self.setActiveEditArea(editArea)
self._eaUtils.saveEditArea(editAreaName, mask)
# get the polar parameters
lat, lon, radius, pieState = self.getPolarInfo(editArea)
editAreaDesc = self.makeEditAreaDesc(editArea, editAreaName, lat, lon, radius, pieState)
self.replaceEditAreaDesc(editAreaName, editAreaDesc)
self.displayAreaDesc(editAreaDesc)
self._editsMade = True
return
def clearSelCommand(self):
self.displayAreaDesc("")
while True:
if len(self._listbox.curselection()) == 0:
break
index = self._listbox.curselection()[0]
editAreaName = self._listbox.get(index)
self.removeEditArea(editAreaName)
self._listbox.delete(index)
self.setSelectButtonState(tkinter.DISABLED)
self._editsMade = True
return
def clearAllCommand(self):
self.displayAreaDesc("")
listSize = self._listbox.size()
for i in range(listSize):
editAreaName = self._listbox.get(i)
self.removeEditArea(editAreaName)
self._listbox.delete(0, listSize)
self.setSelectButtonState(tkinter.DISABLED)
self._editsMade = True
return
# Called when the Reasons window is closed. Just destroy the window.
def closeReasonWindow(self):
self._killWindow.destroy()
self.addKilledArea()
return
# Called when any reason button is clicked. Updates the reason to the GUI
def reasonButtonClick(self, event):
self._killDesc = self._killVar.get()
# Update the display on the GUI
self.displayAreaDesc(self._killDesc)
return
def displayKillReasonsDialog(self, killReasons):
self._killWindow = tkinter.Toplevel(self._master)
self._killWindow.title("Reason")
self._killFrame = tkinter.Frame(self._killWindow, width=500, height=500) # can't get this to resize
self._killFrame.pack()
self._killVar = tkinter.StringVar()
self._killVar.set(self._killDesc)
for i in range(len(killReasons)):
b = tkinter.Radiobutton(self._killFrame, text=killReasons[i],
variable=self._killVar, value=killReasons[i])
if b == self._defaultKillReason:
b.select()
b.grid(row=i, column=0, sticky=tkinter.W, pady=2)
b.bind("<ButtonRelease-1>", self.reasonButtonClick)
self._doneButton = tkinter.Button(self._killFrame, text= "DONE", command=self.closeReasonWindow)
self._doneButton.grid(row=len(killReasons), sticky=tkinter.E+tkinter.W)
# Update the display on the GUI
self.displayAreaDesc(self._killDesc)
self._killFrame.grid()
# make this dialog modal so no other buttons can be clicked.
self._killWindow.transient(self._master)
self._killWindow.grab_set()
return
def killSelCommand(self):
# make sure something is selected (should never happen)
if len(self._listbox.curselection()) == 0:
return
self.displayKillReasonsDialog(self._killReasons)
return
def addKilledArea(self):
# Get the selected button names
buttonStr = self._currentTimePeriod
listIndex = self._listbox.curselection()[0]
editAreaName = self._listbox.get(listIndex)
# Create the editArea name from the button name
pos = editAreaName.find("_")
editAreaName = editAreaName[0:pos+1] + buttonStr + editAreaName[pos+4:]
activeArea = self.getActiveEditArea()
activeMask = self.encodeEditArea(activeArea)
self._eaUtils.saveEditArea(editAreaName, activeMask)
featureName = self.extractFeature(editAreaName)
timePeriod = self.extractTimePeriod(editAreaName)
# get the polar parameters
polarInfo = self.getPolarInfo(activeArea)
if polarInfo is not None:
lat, lon, radius, pieState = polarInfo
else:
lat = None
lon = None
radius = None
pieState = None
editAreaDesc = self._killDesc
self.addNewFeature(featureName, timePeriod, editAreaName, editAreaDesc,
lat, lon, radius, pieState)
for hs in self._hsDesc:
if hs["featureName"] == featureName and hs["timePeriod"] == timePeriod:
for area in hs["areaList"]:
if area["areaName"] == editAreaName:
area["areaDesc"] = editAreaDesc
self.setSelectButtonState(tkinter.NORMAL)
self._editsMade = True
self.redrawListBox()
return
def makePolarEditArea(self):
lat1 = self._latScaleValue * self._degToRad
lon1 = self._lonScaleValue * self._degToRad
radius = self._radiusScaleValue
earthRadius = 6371.0 / 1.852 # Nautical miles
distanceGrid = np.arccos(np.sin(lat1) * np.sin(self._latGrid) + \
np.cos(lat1) * np.cos(self._latGrid) * \
np.cos(self._lonGrid - lon1)) * earthRadius
distanceMask = np.less(distanceGrid, radius)
xDist = self._latGrid - lat1
yDist = self._lonGrid - lon1
tanGrid = np.arctan2(xDist, yDist) / self._degToRad
tanGrid = np.where(np.less(tanGrid, 0.0), tanGrid + 360.0, tanGrid)
tanGrid = tanGrid / 45.0 # convert to 8 "quardants
# mask off all but the selected quadrants.
circleMask = np.zeros(tanGrid.shape, dtype=np.int8)
for i in range(len(self._pieState)):
if self._pieState[i]:
tanMask = np.logical_and(np.greater_equal(tanGrid, i), np.less(tanGrid, i+1))
circleMask |= tanMask
mask = np.logical_and(distanceMask, circleMask)
editArea = self.decodeEditArea(mask)
self.setActiveEditArea(editArea)
self._polarEditArea = editArea
return
def dirSort(self, dirA, dirB):
aDir, aCount = dirA
bDir, bCount = dirB
if aCount > bCount:
return 1
elif aCount == bCount:
return 0
else:
return -1
# Returns a string that describes a circular edit area
def formatCircleEditArea(self, lat, lon, radius, pieState):
dirList = ["E", "NE", "N", "NW", "W", "SW", "S", "SE"]
# Check to see if they're all selected (the whole pie)
if sum(pieState) == len(pieState):
latStr = self.latLonStr(lat, "lat")
lonStr = self.latLonStr(lon, "lon")
return "WITHIN " + str(int(radius)) + " NM OF CENTER AT " + \
latStr + lonStr
quad = [0] * len(pieState)
semi = [0] * len(pieState)
# Find out if we have six consecutive slices selected
# This is because the rules are different if 3/4 of the
# pie is selected.
sixConsec = False
if sum(pieState) >= 6:
for i in range(-2, len(pieState)):
if not pieState[i] and not pieState[i + 1]:
sixConsec = True
break
for i in range(len(pieState)):
if pieState[i] and pieState[i - 1]:
quad[i] = 1
# Begin with the first selected pieSlice
start = 0
while quad[start]:
start = start - 1
for i in range(start, start + len(quad)):
last = i - 1
this = i
next = i + 1
afterNext = next + 1
if next >= len(quad):
next = next - len(quad)
if afterNext >= len(quad):
afterNext = afterNext - len(quad)
if quad[last] and quad[this] and quad[next]:
semi[this] = 1
quad[last] = 0
quad[this] = 0
quad[next] = 0
# if 3/4 of the pie is selected then turn off on extra quad
if sixConsec:
quad[afterNext] = 0
# Format the slices into words
finalDir = ""
# Direction order must be clockwise starting from N
# This list orders directions properly.
pieOrderList = [2, 1, 0, 7, 6, 5, 4, 3]
## for i in range(len(semi)):
for i in pieOrderList:
if semi[i]:
finalDir = dirList[i] + " SEMICIRCLE"
if sum(semi) > 0 and sum(quad) > 0:
finalDir = finalDir + " AND "
## for i in range(len(quad)):
for i in pieOrderList:
if quad[i]:
finalDir = finalDir + dirList[i] + " AND "
if sum(quad) > 0:
finalDir = finalDir[0:-5] # remove the last AND
if sum(quad) > 0:
finalDir = finalDir + " QUADRANT"
if sum(quad) > 1:
finalDir = finalDir + "S"
latStr = self.latLonStr(lat, "lat")
lonStr = self.latLonStr(lon, "lon")
desc = "WITHIN " + str(int(radius)) + " NM " + finalDir + \
" OF CENTER AT " + latStr + lonStr
return desc
# Formats a lat lon string with appropriate direction labels
def latLonStr(self, coord, latOrLon, precision=1.0):
if latOrLon == "lat":
if coord >= 0:
dirStr = "N"
else:
dirStr = "S"
# Exception for southern boundary of EP3 area to force 18.5S output
# This code is needed only for NH1 domain
if coord < -18.5:
precision = 0.5
# Exception for northern boundary of EP3 area to force 3.4S output
# This code is needed only for NH1 domain
if coord < -3.36 and coord > -3.38:
coord = -3.4
coordStr = "0" + "%.1f" % abs(coord) + dirStr
return coordStr
elif latOrLon == "lon":
if coord >= 0:
dirStr = "E"
else:
dirStr = "W"
coordStr = "%.0f" % abs(self.round(coord, "Nearest", 1)) + dirStr
# if the precision is 0.5, round to the nearest 0.5 degrees
if precision == 0.5:
coord = int((abs(coord) + 0.25) * 2.0) / 2.0
# Keep the 0.5 if we have it after rounding
if coord != int(coord):
if abs(coord) < 10:
coordStr = "0" + "%.1f" % abs(coord) + dirStr
else:
coordStr = "%.1f" % abs(coord) + dirStr
return coordStr
# Determines the precision based on the max extent in both lat and lon
# Uses self._marginalEASize to define the threshold for the precision.
def determinePrecision(self, points):
latMin = 10000.0
latMax = -10000.0
lonMin = 10000.0
lonMax = -10000.0
# Calculate the min/max extent of these points.
for lat, lon in points:
latMin = min(lat, latMin)
latMax = max(lat, latMax)
lonMin = min(lon, lonMin)
lonMax = max(lon, lonMax)
# Calculate the difference between the maxes and mins
latDiff = latMax - latMin
lonDiff = lonMax - lonMin
# If we have a marginally small area, set the precision to 0.5 otherwise 1.0 degrees.
precision= 1.0
if latDiff < self._marginalEASize or lonDiff < self._marginalEASize:
precision = 0.5
return precision
# Returns a string that describes a polygon edit area
# modified to add leading "0" when lat < 10
def formatPolygonEditArea(self, editArea):
points = self.simplifyEditArea(editArea)
precision = self.determinePrecision(points)
coordList = []
fullStr = ""
for y, x in points:
latStr = self.latLonStr(y, "lat", precision)
lonStr = self.latLonStr(x, "lon", precision)
coordList.append((latStr, lonStr))
# close the polygon
coordList.append(coordList[0])
lastLat = ""
lastLon = ""
fullStr = "WITHIN AREA BOUNDED BY "
for latStr, lonStr in coordList:
if latStr == lastLat and lonStr == lastLon:
continue
if len(latStr) < 3:
latStr = latStr.zfill(3)
fullStr = fullStr + latStr + lonStr + " TO "
lastLat = latStr
lastLon = lonStr
# remove the last " TO "
fullStr = fullStr[0:-4]
return fullStr
def makeNamedAreaDesc(self, editArea):
descList = []
editAreaMask = self.encodeEditArea(editArea)
for (eaName, mask) in self._namedAreaMasks.items():
overlap = mask & editAreaMask
if overlap.any():
descList.append(self._namedAreaDescip[eaName])
# Commented out below as was requiring that ALL namedAreas be overlapped in order to return (JL/TL 07/21/2016)
# else:
# return ""
if len(descList) == 0:
return ""
finalStr = "...INCLUDING "
for desc in descList:
finalStr = finalStr + desc
if desc != descList[-1]: # add an AND if we're not at the last one
finalStr = finalStr + " AND "
else:
finalStr = finalStr + "..."
return finalStr
def makeEditAreaDesc(self, editArea, editAreaName, lat=None, lon=None, radius=None, pieState=None):
namedAreaDesc = self.makeNamedAreaDesc(editArea)
if lat is not None:
desc = self.formatCircleEditArea(lat, lon, radius, pieState)
else:
desc = self.formatPolygonEditArea(editArea)
# Add delimiters around the lat/lon descriptor so we can replace it when needed
return self._descDelimStart + desc + namedAreaDesc + self._descDelimEnd
def displayAreaDesc(self, editAreaDesc):
self._textBox.configure(state=tkinter.NORMAL)
self._textBox.delete("1.0", tkinter.END)
self._textBox.insert("1.0", editAreaDesc)
self._textBox.configure(state=tkinter.DISABLED)
return
def textBoxClick(self, event=None):
self._textBox.configure(font=self._normalFont)
self._textBox.configure(state=tkinter.NORMAL)
self._textBoxCurSelection = self._listbox.curselection()[0]
return
def textBoxReturnPress(self, event=None):
#listIndex = int(self._listbox.curselection()[0])
listIndex = self._textBoxCurSelection
editAreaName = self._listbox.get(listIndex, None)
editAreaDesc = self._textBox.get("1.0", tkinter.END)
editAreaDesc = editAreaDesc.replace(chr(10), "")
self.displayAreaDesc(editAreaDesc)
self.replaceEditAreaDesc(editAreaName, editAreaDesc)
self._textBox.configure(font=self._boldFont)
self._textBox.configure(state=tkinter.DISABLED)
self.updateTextBoxSaveState("Saved")
self._textBoxCurSelection = None
def textBoxKeyPress(self, event):
if event.char.isprintable():
self.updateTextBoxSaveState("NOT Saved")
return
def updateTextBoxSaveState(self, state):
msg = "Edit Area Description..." + state
if state == "NOT Saved":
color = "red"
else:
color = "black"
self._textLabel.configure(text=msg, foreground=color)
return
##########################################################################
#### Data structure code
##########################################################################
def getBasinName(self, editAreaName):
# Figure out which basin this area is in
basinName = ""
basinList = []
for basin in self._basinNames:
if basin in self._allEditAreaNames:
basinMask = self._eaUtils.fetchEditArea(basin)
else:
continue
areaMask = self._eaUtils.fetchEditArea(editAreaName)
if areaMask is None:
self.statusBarMsg("Edit area: " + " not found in repository.", "S")
continue
if sum(sum(basinMask & areaMask)) > 0:
basinList.append(basin)
basinList.sort()
for b in basinList:
if basinName == "":
basinName = b
else:
basinName = basinName + " AND " + b
if basinName == "":
self.statusBarMsg("No Basin identified for last edit area.", "S")
return basinName
def addNewFeature(self, featureName, timePeriod, editAreaName, editAreaDesc,
lat=None, lon=None, radius=None, pieState=None):
if self._siteID in self._sitesWithBasins:
basinName = self.getBasinName(editAreaName)
else:
basinName = None
if basinName is not None:
if basinName.find("_") > -1:
basinName = basinName.replace("_", " ")
# make the low level data structure
areaDict = {"areaName" : editAreaName,
"areaDesc" : editAreaDesc,
"basin" : basinName,
"lat" : lat,
"lon" : lon,
"radius" : radius,
"pieState" : pieState,
}
featureDict = {
"featureName" : featureName,
"timePeriod" : timePeriod,
"areaList" : [areaDict], # list of areaDict
"basin" : basinName,
}
self._hsDesc.append(featureDict)
return featureName
def addNewArea(self, featureName, timePeriod, editAreaName, editAreaDesc,
lat=None, lon=None, radius=None, pieState=None):
if self._siteID in self._sitesWithBasins:
basinName = self.getBasinName(editAreaName)
else:
basinName = None
if basinName is not None:
if basinName.find("_") > -1:
basinName = basinName.replace("_", " ")
areaDict = {"areaName" : editAreaName,
"areaDesc" : editAreaDesc,
"basin" : basinName,
"lat" : lat,
"lon" : lon,
"radius" : radius,
"pieState" : pieState,
}
# Find any feature/time
for fDict in self._hsDesc:
if fDict["featureName"] == featureName:
if fDict["timePeriod"] == timePeriod:
# add it to the existing list
fDict["areaList"].append(areaDict)
newAreaList = []
for d in fDict["areaList"]:
newAreaList.append(d['areaName'])
newAreaList.sort()
return
# featureName and time not found so make a new one
self.addNewFeature(featureName, timePeriod, editAreaName,
editAreaDesc, lat, lon, radius, pieState)
return
def removeEditArea(self, editAreaName):
# Find the feature
for fDict in self._hsDesc:
# look for the areaName
for areaDict in fDict["areaList"]:
if areaDict["areaName"] == editAreaName:
fDict["areaList"].remove(areaDict)
if len(fDict["areaList"]) == 0:
self._hsDesc.remove(fDict)
return
return
def replaceEditAreaDesc(self, editAreaName, editAreaDesc):
# Find the feature
if self._siteID in self._sitesWithBasins:
basinName = self.getBasinName(editAreaName)
else:
basinName = None
if basinName is not None:
if basinName.find("_") > -1:
basinName = basinName.replace("_", " ")
for fDict in self._hsDesc:
# look for the areaName
for areaDict in fDict["areaList"]:
if areaDict["areaName"] == editAreaName:
areaDict["areaDesc"] = editAreaDesc
areaDict["basin"] = basinName
return
# Didn't find areaName
return
def getEditAreaDesc(self, editAreaName):
for fDict in self._hsDesc:
# look for the areaName
for areaDict in fDict["areaList"]:
if areaDict["areaName"] == editAreaName:
return areaDict["areaDesc"]
return
def getAllEditAreaNames(self):
areaNameList = []
for fDict in self._hsDesc:
for areaDict in fDict["areaList"]:
areaNameList.append(areaDict["areaName"])
return areaNameList
def extractFeature(self, name):
parts = name.split("_")
return parts[0]
def extractTimePeriod(self, name):
parts = name.split("_")
return parts[1]
# Extracts the directory and removes the fileName
def extractDirName(self, path):
parts = path.split("/")
dirName = ""
for i in range(len(parts) - 1):
dirName = dirName + parts[i] + "/"
return dirName
def getNonLatLonText(self, areaDesc):
areaDescOld = copy.copy(areaDesc)
openBrace = areaDescOld.find(self._descDelimStart)
closeBrace = areaDescOld.find(self._descDelimEnd)
preText = areaDescOld[0:openBrace]
postText = areaDescOld[closeBrace+1:]
return preText, postText
def clipDescAreas(self, hsDesc, clipAreaName):
# make a copy so we don't corrupt the original version of the descriptor
descList = []
if clipAreaName in self._allEditAreaNames:
clipMask = self._eaUtils.fetchEditArea(clipAreaName)
else:
self.statusBarMsg("Edit area: " + clipAreaName + " not found in repository.", "S")
return descList
for desc in hsDesc:
# Process the editAreas first
areaList = desc["areaList"]
newAreaList = []
for area in areaList:
# Get the original edit area and clip to the clipArea
oldAreaMask = self._eaUtils.fetchEditArea(area["areaName"])
if oldAreaMask is None:
self.statusBarMsg("Edit area: " + clipAreaName + " not found in repository.", "S")
continue
clippedMask = oldAreaMask & clipMask
if sum(sum(clippedMask)) == 0: # Don't process this area
continue
newEditArea = self.decodeEditArea(clippedMask)
# save this newly clipped area with a different name
newAreaName = area["areaName"] + clipAreaName
self._eaUtils.saveEditArea(newAreaName, clippedMask)
lat = None
lon = None
radius = None
pieState = None
if area["lat"] is not None:
lat = area["lat"]
lon = area["lon"]
radius = area["radius"]
pieState = area["pieState"]
if len(area["areaDesc"]) < 100:
freshAreaDesc = area["areaDesc"]
else:
# Extract the positions of the text before and after the lat/lon text
oldAreaDesc = copy.copy(area["areaDesc"])
newAreaDesc = self.makeEditAreaDesc(newEditArea, newAreaName, lat, lon, radius, pieState)
if area["areaDesc"].find(self._descDelimStart) > -1:
# Extract the positions of the text before and after the lat/lon text
preText, postText = self.getNonLatLonText(oldAreaDesc)
# Restore the pre and post text
freshAreaDesc = preText + newAreaDesc + postText
newAreaDict = {
"areaName" : newAreaName,
"areaDesc" : freshAreaDesc,
"basin" : area["basin"],
"lat" : None,
"lon" : None,
"radius" : None,
"pieState" : None,
}
newAreaList.append(newAreaDict)
if len(newAreaList) > 0:
descDict = {}
descDict["timePeriod"] = desc["timePeriod"]
descDict["featureName"] = desc["featureName"]
descDict["basin"] = desc["basin"]
descDict["areaList"] = newAreaList
descList.append(descDict)
return descList
def removeTextBoxDelims(self, highSeasDescriptor):
for descDict in highSeasDescriptor:
for areaDict in descDict["areaList"]:
areaDict['areaDesc'] = areaDict['areaDesc'].replace("{", "")
areaDict['areaDesc'] = areaDict['areaDesc'].replace("}", "")
return
# This method does the work of writing the descriptor information to a
# file. This may be called several times one for the tool info and again for
# the formatter info. In addition some sites may create two descriptor files
# one north of the equator and one south.
def writeHSDescriptorToFile(self, highSeasDescriptor, highSeasFileName):
if not os.path.exists(highSeasFileName):
# See if the directory exists and if not make it
dirOnly = self.extractDirName(highSeasFileName)
# make the fill directory path
os.makedirs(dirOnly, exist_ok=True)
# open the file for write
setPermissions = False
if not os.path.exists(highSeasFileName): # it's a new file set permissions
setPermissions = True
# open the file for write
try:
with open(highSeasFileName, "wb") as f:
pickle.dump(highSeasDescriptor, f)
if setPermissions:
os.chmod(highSeasFileName, self._permissions)
except:
msg = "Error opening " + highSeasFileName + " for write."
self.statusBarMsg(msg, "U")
# Contains the logic for saving the descriptor info to files.
def saveHSDescToFile(self):
print("in saveHSDescToFile................................................")
# First save the info for the tool
highSeasFileName = self.descriptorFileName("Tool")
self.writeHSDescriptorToFile(self._hsDesc, highSeasFileName)
# Now save the info for the formatter.
# Find out what we need to save
saveAreas = []
if self._siteID in self._multiProductSites:
saveAreas = self._multiProductSites[self._siteID]
# Otherwise we're making two files, but we need to make two new
# descriptors with the areas clipped to the savedAreas
for area in saveAreas:
newHSDesc = self.clipDescAreas(self._hsDesc, area)
# remove the delimiters from the descriptors for the formatter version
self.removeTextBoxDelims(newHSDesc)
highSeasFileName = self.descriptorFileName("Formatter", area)
self.writeHSDescriptorToFile(newHSDesc, highSeasFileName)
# Now sample the weather grids for interesting weather, save that in
# a different list of dictionaries and save that to a file using the
# same structure.
print("interesting weather.............................................")
self.getInterestingWeather()
# reset the editsMade flag
self._editsMade = False
return
def fetchWxGrids(self):
startTime = int(time.time() / (6 * 3600)) * (6 * 3600) # truncated to last 6 hour period
endTime = startTime + (48 * 3600) + 1 # 60 hours later
trList = self.getWEInventory("Fcst", "Wx")
wxGridList = []
for t in range(startTime, endTime, 24*3600):
# Find the grid that overlaps the first hour of the period in which we're interested
tr = TimeRange.TimeRange(AbsTime.AbsTime(t), AbsTime.AbsTime(t+ 3600))
for invTR in trList:
if tr.overlaps(invTR):
wxGrid = self.getGrids("Fcst", "Wx", "SFC", tr)
timePeriod = (t - startTime) // 3600 # hours from start
wxGridList.append((wxGrid, timePeriod))
return wxGridList
def getToolWxTypeIntens(self, wxType):
intens = wxType[-1]
if intens == "+" or intens == "-":
typeOnly = wxType[:-1]
else:
intens = "m"
typeOnly = wxType
#JL Testing "T"; currently reporting based on Intensity and not Coverage for "T" 05/30/2016
if typeOnly in ["T", "K", "VA", "F"] and intens == "m":
intens = "<NoInten>"
return typeOnly, intens
# Returns a mask where all points with the specified wxType are set to 1
def getMaskForWxType(self, wxGrid, wxType):
byteGrid, wxKeys = wxGrid
wxMask = np.zeros(byteGrid.shape, np.bool)
for wxKey in wxKeys:
# get subKeys
subKeys = wxKey.split("^")
for subKey in subKeys:
wxParts = subKey.split(":")
wxStr = wxParts[1] + wxParts[2]
if wxStr == wxType:
wxIndex = self.getIndex(wxKey, wxKeys)
mask = np.equal(byteGrid, wxIndex)
wxMask = mask | wxMask
return wxMask
# Discovers the individual sub edit areas of the specified editArea and
# saves them as individual edit areas.
def getContiguousEditAreas(self, editArea):
# Make a dummy empty editArea that we will reset
emptyEditAreaName = "EmptyEditArea"
bitGrid = editArea.getGrid()
locs = bitGrid.getContiguousBitArrayLocations()
editAreaList = []
for i in range(len(locs)):
newGrid = bitGrid.contiguousBitArray(locs[i])
# Make a dummy empty editArea that we will reset
emptyMask = np.zeros(self.getGridShape(), np.bool)
newEditArea = self.decodeEditArea(emptyMask)
self.saveEditArea(emptyEditAreaName, newEditArea)
# Now use the edit area
newEditArea = self.getEditArea(emptyEditAreaName)
newEditArea.setGrid(newGrid)
# eaMask = self.encodeEditArea(newEditArea) # Why do this?
editAreaList.append(newEditArea)
return editAreaList
# Determines if the specified WxKey matches the weather combinations defined in
# wxTypeList. If a match is found, return the wx coverage, type, and intensity,
# otherwise return None.
def matchingWxKey(self, wxKey, wxTypeList):
# Examine each sub key
subKeys = wxKey.split("^")
for subKey in subKeys:
wxParts = subKey.split(":")
# Extract the coverage, type and intensity
if len(wxParts) >= 4:
wxCov = wxParts[0]
wxType = wxParts[1]
wxIntens = wxParts[2]
# Check each allowed type for a match
for wxTypeIntens in wxTypeList:
allowedType, allowedIntens = self.getToolWxTypeIntens(wxTypeIntens)
if wxType == allowedType and wxIntens == allowedIntens:
# We're only interested in T if coverage matches one in self._thunderCoverages EXCEPT
# for the case Iso T m (moderate) where we will convert to Sct T- to represent
# Scattered thunderstorms.
if "T" in wxType and wxCov == "Iso":
return wxCov, allowedType, allowedIntens
if "T" in wxType and wxCov not in self._thunderCoverages:
continue
return wxCov, allowedType, allowedIntens
# If we get to here, no match was found
return None
def makePeriodList(self, wxGrid, timePeriodStr, area):
# Dictionary used to convert GFE intensities since it's part of an edit area name
intenDict = {
"+" : "h",
"m" : "m",
"<NoInten>" : "n",
"-" : "l",
}
byteGrid, wxKeys = wxGrid
# Figure out the Wx types and intensities allowed for the HSF
# Filter out all other Wx type and intensities but these.
wxTypeList = self._allPeriodWxTypes
if timePeriodStr == "00h":
wxTypeList = wxTypeList + self._firstPeriodWxTypes
areaList = [] # There's a chance that we won't find any wx so define this here
areaMask = self.encodeEditArea(area)
for wxKey in wxKeys:
# Returns coverage, type, and intensity of any matching sub key
covTypeIntens = self.matchingWxKey(wxKey, wxTypeList)
if covTypeIntens is None:
continue
wxCov, wxType, wxIntens = covTypeIntens # extract the components
# Change IsoTm to SctT- so we can report scattered thunderstorms
if wxCov == "Iso" and wxType == "T" and wxIntens == "<NoInten>":
wxCov = "Sct"
wxIntens = "-"
wxIndex = self.getIndex(wxKey, wxKeys)
mask = (byteGrid == wxIndex)
mask = mask & self._clipMask
if np.sum(mask) == 0: # no points found with this wxType
continue
# Make an edit area from the mask
wxEditArea = self.decodeEditArea(mask)
# Get the individual contiguous areas from the edit area
wxEAList = self.getContiguousEditAreas(wxEditArea)
for eaNum, ea in enumerate(wxEAList):
# Only include edit areas that overlap the current domain
eaMask = self.encodeEditArea(ea)
overlap = eaMask & areaMask
if not overlap.any():
print("No overlap for ", wxType)
continue
intenStr = intenDict[wxIntens]
eaName = "Z" + timePeriodStr + wxCov + wxType + intenStr + str(eaNum).zfill(2)
self._eaUtils.saveEditArea(eaName, eaMask)
eaDesc = self.makeEditAreaDesc(ea, eaName)
areaDict = {}
areaDict["timePeriod"] = timePeriodStr
areaDict["areaName"] = eaName
areaDict["areaDesc"] = eaDesc
areaDict["wxType"] = wxType
areaDict["wxCoverage"] = wxCov
areaDict["intensity"] = wxIntens
areaList.append(areaDict)
return areaList
def getInterestingWeather(self):
# Fetch the Wx grids we need
wxGridList = self.fetchWxGrids()
saveDomains = []
if self._siteID in self._multiProductSites:
saveDomains = self._multiProductSites[self._siteID]
else:
self.statusBarMsg("Domains not defined for site:" + siteID, "S")
self.statusBarMsg("Grid based features will not be saved." "S")
return
for domain in saveDomains:
gridBasedFeatures = []
for wxGrid, timePeriod in wxGridList:
timePeriodStr = str(timePeriod).zfill(2) + "h"
periodList = self.makePeriodList(wxGrid, timePeriodStr, domain)
gridBasedFeatures = gridBasedFeatures + periodList
# Strip and leading and trailing braces
for gFeature in gridBasedFeatures:
gFeature["areaDesc"] = gFeature["areaDesc"].replace("{", "")
gFeature["areaDesc"] = gFeature["areaDesc"].replace("}", "")
wxFeaturesFileName = self.gridBasedFileName(domain[-3:])
self.writeFeatureList(gridBasedFeatures, wxFeaturesFileName)
return
def similarArea(self, area1, area2):
eaName1 = area1["areaName"]
eaName2 = area2["areaName"]
mask1 = self._eaUtils.fetchEditArea(eaName1)
mask2 = self._eaUtils.fetchEditArea(eaName2)
overlap = mask1 & mask2
if overlap.any():
return True
return False
def findSimilarFeature(self, area):
# Extract the part from the area
wxType = area["wxType"]
wxCov = area["wxCoverage"]
wxIntens = area["intensity"]
for group in self._sortedFeatures:
# Check for the same weather
if group[0]["wxType"] != wxType or group[0]["wxCoverage"] != wxCov or group[0]["intensity"] != wxIntens:
continue
# Check for similar area
if self.similarArea(area, group[0]):
return group
return None
def groupGridBasedFeatures(self, gridBasedFeatures):
# Populate the sorted features with the first area if we have any features
self._sortedFeatures = []
if len(gridBasedFeatures) == 0:
return
self._sortedFeatures = [[copy.copy(gridBasedFeatures[0])]]
for i in range(1, len(gridBasedFeatures)):
simGroup = self.findSimilarFeature(gridBasedFeatures[i])
if simGroup is not None: # make a new group
simGroup.append(copy.copy(gridBasedFeatures[i]))
continue
else:
self._sortedFeatures.append([copy.copy(gridBasedFeatures[i])])
return
def gridBasedFileName(self, subArea):
dirPath = "/data/local/HighSeas/Formatter/" + self._siteID + "/HSF_" + subArea + "GridBasedFeatures.pic"
return dirPath
# Writes the gridBased features to a file.
def writeFeatureList(self, gridBasedFeatures, gridBasedFileName):
self._sortedFeatures = []
self.groupGridBasedFeatures(gridBasedFeatures)
if not os.path.exists(gridBasedFileName):
# See if the directory exists and if not make it
dirOnly = self.extractDirName(gridBasedFileName)
# make the fill directory path
os.makedirs(dirOnly, exist_ok=True)
# open the file for write
setPermissions = False
if not os.path.exists(gridBasedFileName): # it's a new file set set permissions
setPermissions = True
try:
with open(gridBasedFileName, "wb") as f:
pickle.dump(self._sortedFeatures, f)
if setPermissions:
os.chmod(gridBasedFileName, self._permissions)
except:
msg = "Error opening " + gridBasedFileName + " for write."
self.statusBarMsg(msg, "U")
return
def dumpArea(self, areaName):
for fDict in self._hsDesc:
for areaDict in fDict["areaList"]:
if areaDict["areaName"] == areaName:
print("-------------------------------------")
print("Feature Name:", fDict["featureName"])
print("Time Period:", fDict["timePeriod"])
print("AreaName:", areaDict["areaName"])
print("AreaDesc:", areaDict["areaDesc"])
print("Basin:", areaDict["basin"])
print("Lat:", areaDict["lat"])
print("Lon:", areaDict["lon"])
print("Radius:", areaDict["radius"])
print("PieState", areaDict["pieState"])
return
def dumpDatabase(self):
areaList = []
print()
print("+++++++++++++++ START Dump of database +++++++++++++++")
for fDict in self._hsDesc:
for areaDict in fDict["areaList"]:
areaList.append(areaDict["areaName"])
areaList.sort()
for areaName in areaList:
self.dumpArea(areaName)
print("+++++++++++++++ END Dump of database +++++++++++++++")
return
def getNewFeatureName(self):
# make a list of possibleNames
featureNameList = []
for fDict in self._hsDesc:
featureNameList.append(fDict["featureName"])
num = 1
while True:
featureName = "Feature" + str(num)
if featureName not in featureNameList:
return featureName
num = num + 1
if num > 1000: # ininite loop prevention
break
print("Error, no new feature for getNewFeatureName")
return ""
def getNewAreaName(self, featureName, timePeriod):
count = 0
for fDict in self._hsDesc:
if fDict["featureName"] == featureName and fDict["timePeriod"] == timePeriod:
count = count + len(fDict["areaList"])
name = featureName + "_" + str(timePeriod) + "_" + "EA" + str(count+1)
return name
# Reads the descriptor from the file.
def initializeHSDescriptor(self):
highSeasFileName = self.descriptorFileName("Tool")
try:
with open(highSeasFileName, "rb") as f:
self._hsDesc = pickle.load(f)
except:
msg = "Descriptor file not found. Starting with an empty descriptor."
self.statusBarMsg(msg, "S")
self._hsDesc = []
self._editsMade = False
def makeClipMask(self, clipEditAreas):
clipMask = None
for editArea in clipEditAreas:
if editArea in self._allEditAreaNames:
mask = self._eaUtils.fetchEditArea(editArea)
if clipMask is None:
clipMask = mask
else:
clipMask = clipMask | mask
else:
self.statusBarMsg(editArea + " edit area not found for clip area", "S")
return clipMask
##########################################################################
#### END -----Data structure code
##########################################################################
# Main method that glues all of the GIU pieces together. Creates
# all the frames used by other widgets and calls other methods
# to create buttonsn listboxes, and the status window.
def setUpUI(self):
# create the main objects
self._tkmaster = tkinter.Tk()
self._master = tkinter.Toplevel(self._tkmaster)
self._tkmaster.withdraw()
# Capture the "x" click to close the GUI
self._master.protocol('WM_DELETE_WINDOW', self.cancelCommand)
self._master.title('High Sea Edit Areas')
self._topFrame = tkinter.Frame(self._master)
self._topFrame.grid()
self._timeFrame = tkinter.Frame(self._topFrame, bd=2, relief=tkinter.GROOVE)
self._timeFrame.grid(row=0, column=0, sticky=tkinter.N)
self._timeVar = tkinter.StringVar()
self.makeTimeButtons(self._timeFrame)
self._listFrame = tkinter.Frame(self._topFrame)
self._listFrame.grid(row=0, column=1, padx=15)
self.makeListBox()
self._listButtonFrame = tkinter.Frame(self._listFrame, bd=2, relief=tkinter.GROOVE)
self._listButtonFrame.grid(row=2, column=0, columnspan=2, pady=5)
self._saveNewButton = tkinter.Button(self._listButtonFrame, text="ADD New Feature",
command=self.saveNewCommand, width=25)
self._saveNewButton.grid(row=0, column=0)
self._saveSelButton = tkinter.Button(self._listButtonFrame, text="SAVE To Selected Feature",
command=self.saveSelCommand, width=25,
state=tkinter.DISABLED)
self._saveSelButton.grid(row=1, column=0)
self._replaceSelButton = tkinter.Button(self._listButtonFrame,
text="REPLACE Selected Area", command=self.replaceSelCommand,
width=25, state=tkinter.DISABLED)
self._replaceSelButton.grid(row=2, column=0)
self._clearSelButton = tkinter.Button(self._listButtonFrame,
text="REMOVE Selected Area(s)",
command=self.clearSelCommand, width=25,
state=tkinter.DISABLED)
self._clearSelButton.grid(row=3, column=0)
self._clearAllButton = tkinter.Button(self._listButtonFrame,
text="REMOVE All Areas",
command=self.clearAllCommand, width=25)
self._clearAllButton.grid(row=4, column=0)
self._killSelButton = tkinter.Button(self._listButtonFrame,
text="KILL Selected Area",
command=self.killSelCommand, width=25,
state=tkinter.DISABLED)
self._killSelButton.grid(row=5, column=0)
self._circleRadius = 80
self._circleOffset = 20
self._polarFrame = tkinter.Frame(self._topFrame, bd=2, relief=tkinter.GROOVE)
self._polarFrame.grid(row=0, column=2, padx=20)
self._pieSelectColor = "red"
self._pieDeselectColor = "gray80"
if self._displayPolarWidgets:
self.makePolarWidgets()
self._textFrame = tkinter.Frame(self._topFrame)
self._textFrame.grid(row=2, column=0, columnspan=3, pady=20)
self._textBox = tkinter.Text(self._textFrame, height=3, width=60, wrap=tkinter.CHAR,
foreground="black", font=self._boldFont)
self._textBox.grid(row=0, column=0, padx=10)
self._textBox.bind("<ButtonRelease-1>", self.textBoxClick)
self._textBox.bind("<KeyRelease-Return>", self.textBoxReturnPress)
self._textBox.bind("<KeyPress>", self.textBoxKeyPress)
self._textLabel = tkinter.Label(self._textFrame, text="Edit Area Description")
self._textLabel.grid(row=1, column=0)
self._bottomFrame = tkinter.Frame(self._topFrame)
self._bottomFrame.grid(row=3, column=0, columnspan=3)
self.makeBottomButtons()
self._master.bind("<Enter>", self.enterEvent)
return
# Returns the name of the file used to store the edit area information.
# The appType can be "Tool" or "Formatter" only
# The subArea parameter will be used for sites creating more than one
# High Seas product
def descriptorFileName(self, appType, subArea = ""):
dirPath = "/data/local/HighSeas/" + appType + "/"
fileName = dirPath + self._siteID + "/" + subArea + "HighSeasDescriptors.pic"
return fileName
def makeNamedAreaMasks(self, namedAreaDict):
namedAreaMasks = {}
for eaName in namedAreaDict:
eaMask = self._eaUtils.fetchEditArea(eaName)
if eaMask is None:
self.statusBarMsg("Edit area: " + eaName + " not found in repository.", "S")
continue
namedAreaMasks[eaName] = eaMask
return namedAreaMasks
# Defines the local effect edit areas. These will be added to the lat/lon descriptors
# to better identify the area.
# These are areas for which we look for local effects. Format: Edit area name : Description
def defineNamedAreas(self):
allAreas = {
"le_cabocorrientes" : "CABO CORRIENTES",
"CALIFORNIA" : "GULF OF CALIFORNIA",
"le_pmz011" : "SEBASTIAN VIZCAINO BAY",
"le_pmz013" : "WITHIN 60 NM OF SHORE",
"le_pmz015" : "WITHIN 60 NM OF SHORE",
"le_tehuantepec" : "THE GULF OF TEHUANTEPEC",
"le_panama" : "THE GULF OF PANAMA",
"le_pmz115" : "NEAR THE AZUERO PENINSULA",
"le_papagayo" : "THE GULF OF PAPAGAYO",
"le_pmz119" : "THE GULF OF GUAYAQUIL",
"le_pmz123" : "LEE OF GALAPAGOS ISLANDS",
"le_gmz021_straits_of_florida" : "STRAITS OF FLORIDA",
"le_gmz023_s_of_21n_w_of_95w" : "WITHIN 60 NM OF COAST OF VERACRUZ",
"le_gmz025_60nm_of_campeche" : "WITHIN 60 NM OF COAST OF CAMPECHE",
"le_amz011_yucatan_channel" : "IN YUCATAN CHANNEL",
"le_amz013_cuba_jamaica" : "BETWEEN CUBA AND JAMAICA",
"le_gulf_of_honduras" : "GULF OF HONDURAS",
"le_amz023_mona_swell" : "IN MONA PASSAGE",
"le_amz025_atlc_exposures_and_passages" : "IN ATLANTIC EXPOSURES AND PASSAGES",
"le_amz029_nicaraguan_coast" : "WITHIN 60 NM OF COAST OF NICARAGUA",
"le_amz031_colombian_coast" : "WITHIN 90 NM OF COAST OF COLOMBIA",
"le_amz033_gulf_of_venezuela" : "GULF OF VENEZUELA",
"le_amz035_atlantic" : "ATLANTIC EXPOSURES",
"le_amz117_atlc_exposures" :"ATLANTIC EXPOSURES",
"le_windward_passage" :"APPROACH TO WINDWARD PASSAGE",
}
# Make sure we hvae the edit area before including it in the returned list
allEANames = self._eaUtils.allEditAreaNames()
namedAreaDescrip = {}
for (eaName, value) in allAreas.items():
if eaName in allEANames:
namedAreaDescrip[eaName] = value
return namedAreaDescrip
def execute(self, timeRange):
self._timeRange = timeRange
siteID = self.getSiteID()
#editAreasPath = "/scratch/local/HighSeas/EditAreas/" # for Boulder development
editAreasPath = "/data/local/HighSeas/" + siteID + "/EditAreas/"
self._eaUtils = EditAreaUtilities.EditAreaUtilities(editAreasPath)
self._gridLoc = self.getGridLoc()
self._latGrid, self._lonGrid = self.getLatLonGrids()
self._minLat = int(min(self._latGrid.flat))
self._minLon = int(min(self._lonGrid.flat))
self._maxLat = int(max(self._latGrid.flat)) + 1.0
self._maxLon = int(max(self._lonGrid.flat)) + 1.0
self._degToRad = 2.0 * math.pi / 360.0
self._latGrid = self._latGrid * self._degToRad
self._lonGrid = self._lonGrid * self._degToRad
self._defaultLat = int((self._minLat + self._maxLat) / 2.0)
self._defaultLon = int((self._minLon + self._maxLon) / 2.0)
self._defaultRadius = 200.0
self._latScaleValue = self._defaultLat + 0.01
self._lonScaleValue = self._defaultLon + 0.01
self._radiusScaleValue = self._defaultRadius + 0.01
self._pieState = [False, False, False, False, False, False, False, False]
self._defaultTime = "00h"
self._normalFont = ("helvetica", "12", "normal")
self._boldFont = ("helvetica", "12", "bold")
self._textBoxCurSelection = None
self._allEditAreaNames = self._eaUtils.allEditAreaNames()
# Configurable section. Probably will get moved to separate config file.
self._siteID = self.getSiteID()
self._sitesWithBasins = ["NH2"]
self._basinNames = ["ATLC", "GULF_OF_MEXICO", "CARIBBEAN", "HSF_NP", "HSF_SP", "HSF_AT1", "HSF_EP1", "HSF_EPi"]
self._killReasons = ["INLAND.", "MOVED ... OF AREA.", "CONDITIONS MERGED.", "ABSORBED.", "CONDITIONS IMPROVE.",
"LITTLE CHANGE.", "NONE."]
self._defaultKillReason = self._killReasons[-1]
self._killDesc = self._defaultKillReason
if self._siteID == "HPA":
self._clipEditAreas = ["HSF_NP", "HSF_SP"]
elif self._siteID == "NH1":
self._clipEditAreas = ["HSF_EP2", "HSF_EP3"]
elif self._siteID == "NH2":
self._clipEditAreas = ["ATLC", "GOM", "CARIB"]
elif self._siteID == "ONA":
self._clipEditAreas = ["HSF_AT1"]
elif self._siteID == "ONP":
self._clipEditAreas = ["HSF_EP1"]
else:
self._clipEditAreas = []
self._clipMask = self.makeClipMask(self._clipEditAreas)
if self._clipMask is None:
self._clipMask = np.ones(self.getGridShape(), np.bool)
# Define data for sites that make two descriptor files, one for the NH and another for the SH
# These sites make two products from the same GFE domain. The identifiers for each define the
# edit area or mask over which the product is valid. All defined features will be clipped to
# these areas when the descriptors are generated and save to to the file.
self._multiProductSites = {
"HPA" : ["HSF_NP", "HSF_SP"],
"NH1" : ["HSF_EP2", "HSF_EP3"],
"NH2" : ["HSF_AT2"],
"ONA" : ["HSF_AT1"],
"ONP" : ["HSF_EP1"],
}
self._thunderCoverages = ["Sct", "Num", "Wide"]
self._firstPeriodWxTypes = ["T", "T+", "VA"]
self._allPeriodWxTypes = ["F+", "K", "ZY-", "ZY", "ZY+"]
self._descDelimStart = "{"
self._descDelimEnd = "}"
self._selectingPieSlices = False
self._deselectingPieSlices = False
self._displayPolarWidgets = True
self._editsMade = False
self._selectedEditArea = ""
self._minEASize = 0.75
self._marginalEASize = 1.0
self._initialEditArea = self.getActiveEditArea()
self._enterCount = 0
self._polarEditArea = self._initialEditArea
self._namedAreaDescip = self.defineNamedAreas()
self._namedAreaMasks = self.makeNamedAreaMasks(self._namedAreaDescip)
self._permissions = stat.S_IRWXU + stat.S_IRWXG + stat.S_IROTH # 775 permissions on dirs and files
self.initializeHSDescriptor()
self.setUpUI()
tkinter.mainloop()
return