From 3600d980d8e781a06346bab465ddba78f8f4f0e8 Mon Sep 17 00:00:00 2001 From: "(no author)" <(no author)@8a9318a1-56ba-4d59-b755-99d26321be01> Date: Mon, 30 Jul 2012 20:09:44 +0000 Subject: [PATCH] adding a bunch of changes for the new settings tab, but turning the lon/lat stuff back off for now; tweaked the validator for the numbers in the gui so it will allow None; cleaned up bugs in the 1D figure plotting code; added filters to reverse 2D data horizontally or vertically; more cleanup from moving constant values away to a separate module git-svn-id: https://svn.ssec.wisc.edu/repos/glance/trunk@191 8a9318a1-56ba-4d59-b755-99d26321be01 --- pyglance/glance/data.py | 8 + pyglance/glance/figures.py | 9 +- pyglance/glance/filters.py | 18 +++ pyglance/glance/gui_controller.py | 29 ++++ pyglance/glance/gui_figuremanager.py | 225 ++++++++++++++++++++++----- pyglance/glance/gui_model.py | 145 ++++++++++++++++- pyglance/glance/gui_view.py | 134 ++++++++++++---- 7 files changed, 491 insertions(+), 77 deletions(-) diff --git a/pyglance/glance/data.py b/pyglance/glance/data.py index 417a7ba..4d14db4 100644 --- a/pyglance/glance/data.py +++ b/pyglance/glance/data.py @@ -152,6 +152,14 @@ class DataObject (object) : self.have_analyzed = False + def copy (self) : + """ + return a copy of this data object + """ + + return DataObject(self.data.copy(), fillValue=self.fill_value, ignoreMask=self.masks.ignore_mask, + overrideFillValue=self.override_fill_value, defaultFillValue=self.default_fill_value) + def self_analysis(self) : """ Gather some basic information about a data set diff --git a/pyglance/glance/figures.py b/pyglance/glance/figures.py index ee3b295..8777caf 100644 --- a/pyglance/glance/figures.py +++ b/pyglance/glance/figures.py @@ -568,6 +568,7 @@ def create_simple_figure(data, figureTitle, invalidMask=None, tagData=None, colo # if our colorbar has limits set those if colorbarLimits is not None : + LOG.debug("setting colorbar limits: " + str(colorbarLimits)) clim(vmin=colorbarLimits[0], vmax=colorbarLimits[-1]) # make a color bar cbar = colorbar(format='%.3g') @@ -614,7 +615,7 @@ def create_line_plot_figure(dataList, figureTitle) : # if we don't have these, set them to defaults if invalidMask is None : - invalidMask = zeros(dataSet.shape, dtype=bool) + invalidMask = zeros(dataSet.size, dtype=bool) if labelName is None : labelName = 'data' + str(dataSetLabelNumber) dataSetLabelNumber = dataSetLabelNumber + 1 @@ -627,8 +628,8 @@ def create_line_plot_figure(dataList, figureTitle) : if minTagPts < 0 : minTagPts = dataSet.size + 1 - indexData = ma.array(range(dataSet.size), mask=invalidMask) - cleanData = ma.array(dataSet, mask=invalidMask) + indexData = ma.array(range(dataSet.size), mask=invalidMask.ravel()) + cleanData = ma.array(dataSet.ravel(), mask=invalidMask.ravel()) # plot the tag data and gather information about it if tagData is not None : @@ -644,7 +645,7 @@ def create_line_plot_figure(dataList, figureTitle) : # if we have mismatch points, we need to show them if numMismatchPoints > 0: - cleanTagData = ma.array(dataSet, mask=~tagData | invalidMask) + cleanTagData = ma.array(dataSet.ravel(), mask=~tagData.ravel() | invalidMask.ravel()) axes.plot(indexData, cleanTagData, 'yo', label='mismatch point') if str.lower(str(units)) !="none" : diff --git a/pyglance/glance/filters.py b/pyglance/glance/filters.py index 55f7973..2efcbee 100644 --- a/pyglance/glance/filters.py +++ b/pyglance/glance/filters.py @@ -45,6 +45,24 @@ def trim_off_of_left (data, num_elements_to_trim) : return data[:, num_elements_to_trim:] +def reverse_2D_data_vertically (data) : + """ + Reverse two dimensional data along it's first dimension. + For most satellite data this will result in it flipping vertically + when glance displays it. + """ + + return data.copy()[::-1] + +def reverse_2D_data_horizontally (data) : + """ + Reverse two dimensional data along it's second dimension. + For most satellite data this will result in it flipping horizontally + when glance displays it. + """ + + return data.copy()[:, ::-1] + def flatten_data_into_bins (data, ranges, new_values, missing_value, return_data_type) : """ Sort the data into the given ranges. Each range should correspond to a value diff --git a/pyglance/glance/gui_controller.py b/pyglance/glance/gui_controller.py index 4f52810..d7bda0d 100644 --- a/pyglance/glance/gui_controller.py +++ b/pyglance/glance/gui_controller.py @@ -166,6 +166,34 @@ class GlanceGUIController (object) : self.model.updateSettingsDataSelection(useSharedRangeForOriginals=should_use_shared_range) + def userToggledRestrictRange(self, file_prefix, should_restrict_range) : + """ + the user has toggled whether or not to restrict the data to a fixed range + """ + + self.model.updateFileSettings(file_prefix, doRestrictRange=should_restrict_range) + + def userChangedRangeMin(self, file_prefix, new_range_min) : + """ + the user changed the minimum of the acceptable data range + """ + + self.model.updateFileSettings(file_prefix, newRangeMin=new_range_min) + + def userChangedRangeMax(self, file_prefix, new_range_max) : + """ + the user changed the maximum of the acceptable data range + """ + + self.model.updateFileSettings(file_prefix, newRangeMax=new_range_max) + + def userToggledIsAWIPS(self, file_prefix, data_is_AWIPS) : + """ + the user has toggled whether or not the file should be treated as AWIPS formatted data + """ + + self.model.updateFileSettings(file_prefix, doCorrectForAWIPS=data_is_AWIPS) + def userRequestsStats (self) : """ the user has asked for stats information @@ -185,6 +213,7 @@ class GlanceGUIController (object) : self.figs.spawnPlot() except (IncompatableDataObjects, ValueError), idove : self.handleWarning(str(idove)) + #raise ################# end of methods to handle user input reporting ################# diff --git a/pyglance/glance/gui_figuremanager.py b/pyglance/glance/gui_figuremanager.py index 21b5624..e42f3d8 100644 --- a/pyglance/glance/gui_figuremanager.py +++ b/pyglance/glance/gui_figuremanager.py @@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__) # colormaps that are available in the GUI # TODO, if this changes the list of colormap names in the constants module needs to be kept up AVAILABLE_COLORMAPS = {CM_RAINBOW: cm.jet, - CM_GRAY: cm.gray} + CM_GRAY: cm.bone} class GlanceGUIFigures (object) : """ @@ -62,19 +62,21 @@ class GlanceGUIFigures (object) : if objectToRegister not in self.errorHandlers : self.errorHandlers.append(objectToRegister) - def _getVariableInformation (self, filePrefix) : + def _getVariableInformation (self, filePrefix, variableName=None) : """ Pull the name, data, and units for the variable currently selected in the given file prefix """ + varNameToUse = variableName + if varNameToUse is None : + varNameToUse = self.dataModel.getVariableName(filePrefix) # get the currently selected variable - selectedVariableName = self.dataModel.getVariableName(filePrefix) - dataObject = self.dataModel.getVariableData(filePrefix, selectedVariableName) - unitsText = self.dataModel.getUnitsText(filePrefix, selectedVariableName) + dataObject = self.dataModel.getVariableData(filePrefix, varNameToUse, doCorrections=True) + unitsText = self.dataModel.getUnitsText (filePrefix, varNameToUse) if dataObject is not None : dataObject.self_analysis() - return selectedVariableName, dataObject, unitsText + return varNameToUse, dataObject, unitsText def _getVariableInfoSmart (self, filePrefix, imageType) : """ @@ -84,9 +86,10 @@ class GlanceGUIFigures (object) : varName, dataObject, unitsText = None, None, None # only load the data if it will be needed for the plot - if ((imageType == model.ORIGINAL_A) and (filePrefix == "A") or - (imageType == model.ORIGINAL_B) and (filePrefix == "B") or - (imageType in model.COMPARISON_IMAGES)) : + if ( self.dataModel.getShouldShowOriginalPlotsInSameRange() or + ((imageType == ORIGINAL_A) and (filePrefix == "A") or + (imageType == ORIGINAL_B) and (filePrefix == "B") or + (imageType in COMPARISON_IMAGES))) : varName, dataObject, unitsText = self._getVariableInformation(filePrefix) return varName, dataObject, unitsText @@ -102,7 +105,7 @@ class GlanceGUIFigures (object) : diffObject = None # only build the difference if we need to compare the data - if imageType in model.COMPARISON_IMAGES : + if imageType in COMPARISON_IMAGES : # check to see if our data is minimally compatable; this call may raise an IncompatableDataObjects exception dataobjects.DiffInfoObject.verifyDataCompatability (dataObjectA, dataObjectB, varNameA, varNameB) @@ -113,6 +116,72 @@ class GlanceGUIFigures (object) : return diffObject + def _load_and_analyse_lonlat (self, listOfFilePrefixes=["A", "B"], lonNames=None, latNames=None, stopIfComparisonFails=False) : + """ + load information on the longidue and latitude, + if there are multiple file prefixes given: + find the shared range + analyse how different the navigation is between the files + (if there is a lon/lat epsilon defined and the difference is more than that, either stop with an error or log a warning) + + lonNames and latNames should be dictionaries giving the names of the longitude and latitude variables indexed by the file prefixes + + This method may raise an IncompatableDataObjects exception if multiple file prefixes are passed in the listOfFilePrefixes + and the longitude and latidues for those files can not be compared. + """ + + lonlatData = { } + lonRange = None + latRange = None + + # load and compare stuff for each file prefix + for filePrefix in listOfFilePrefixes : + + # get information on the lon/lat from the current file + currentLonObj, currentLatObj, currentLonRange, currentLatRange = self._load_lonlat(filePrefix, lonNames[filePrefix], latNames[filePrefix]) + + # TODO, this will currently crash if there's a problem, we don't really want that + assert currentLonObj.data.shape == currentLatObj.data.shape + + # expand our lon/lat ranges if we need to + if lonRange is None : + lonRange = currentLonRange + else : + lonRange[0] = min(currentLonRange[0], lonRange[0]) + lonRange[1] = max(currentLonRange[1], lonRange[1]) + if latRange is None: + latRange = currentLatRange + else : + latRange[0] = min(currentLatRange[0], latRange[0]) + latRange[1] = max(currentLatRange[1], latRange[1]) + + # compare this file to whatever other data we have + for filePrefixToCompare in lonlatData.keys() : + lonToCompare, latToCompare = lonlatData[filePrefixToCompare] + # TODO, this is going to crash if there's a problem, we don't really want that + assert lonToCompare.data.shape == currentLatObj.data.shape + assert lonToCompare.data.shape == currentLonObj.data.shape + + # add this data to the list of lonlat data + lonlatData[filePrefix] = [currentLonObj, currentLatObj] + + # return longitude and latitude information and the shared ranges + return lonlatData, lonRange, latRange + + def _load_lonlat (self, filePrefix, lonName, latName) : + """ + load the longitude and latitude information for the file and determine the ranges + present in both + """ + + _, lonObject, _ = self._getVariableInformation(filePrefix, lonName) + _, latObject, _ = self._getVariableInformation(filePrefix, latName) + + lonRange = [lonObject.get_min(), lonObject.get_max()] + latRange = [latObject.get_min(), latObject.get_max()] + + return lonObject, latObject, lonRange, latRange + def spawnPlot (self) : """ create a matplotlib plot using the current model information @@ -123,85 +192,169 @@ class GlanceGUIFigures (object) : for reasons not encompassed by an IncompatableDataObjects exception """ - imageType = self.dataModel.getImageType() - dataForm = self.dataModel.getDataForm() + # retrieve some plotting settings + imageType = self.dataModel.getImageType() + dataForm = self.dataModel.getDataForm() + colorMapToUse = AVAILABLE_COLORMAPS[self.dataModel.getColormapName()] LOG.info ("Preparing variable data for plotting...") + # load the variable data aVarName, aDataObject, aUnitsText = self._getVariableInfoSmart("A", imageType) bVarName, bDataObject, bUnitsText = self._getVariableInfoSmart("B", imageType) - + # compare the variables diffData = self._buildDiffInfoObjectSmart(imageType, aDataObject, bDataObject, aVarName, bVarName, epsilon_value=self.dataModel.getEpsilon(), epsilon_percent=self.dataModel.getEpsilonPercent()) + # if we need to build a shared range, do that now + rangeInfo = None + if (self.dataModel.getShouldShowOriginalPlotsInSameRange() and (aDataObject is not None) and (bDataObject is not None)) : + rangeInfo = [min(aDataObject.get_min(), bDataObject.get_min()), max(aDataObject.get_max(), bDataObject.get_max())] + + if (dataForm == MAPPED_2D) and (imageType != HISTOGRAM) and (imageType != model.SCATTER) and (imageType != model.HEX_PLOT) : + lonNames = { + "A": self.dataModel.getLongitudeName("A"), + "B": self.dataModel.getLongitudeName("B") + } + latNames = { + "A": self.dataModel.getLatitudeName("A"), + "B": self.dataModel.getLatitudeName("B") + } + lonlatData, lonRange, latRange = self._load_and_analyse_lonlat(listOfFilePrefixes=["A", "B"], + lonNames=lonNames, latNames=latNames) + + # double check that lon/lat are compatable with the data + if aDataObject is not None : + assert(lonlatData["A"][0].shape == aDataObject.shape) + if bDataObject is not None : + assert(lonlatData["B"][0].shape == bDataObject.shape) + # make composite valid mask + allValidMask = ( lonlatData["A"][0].masks.valid_mask & lonlatData["A"][1].masks.valid_mask & + lonlatData["B"][0].masks.valid_mask & lonlatData["B"][1].masks.valid_mask ) + + # build basemap, FUTURE, don't hard code so much of this stuff + basemapObject = Basemap(llcrnrlon=lonRange[0], llcrnrlat=latRange[0], urcrnrlon=lonRange[1], urcrnrlat=latRange[1], + resolution='i', area_thresh=10000, projection="merc") + # TODO get all these variables outside the if statement + + LOG.info("Spawning plot window: " + imageType) plt.ion() # make sure interactive plotting is on - # create the plot + # create whichever type of plot was asked for - if imageType == model.ORIGINAL_A : + if imageType == ORIGINAL_A : # if the data doesn't exist, we can't make this plot if aDataObject is None : raise ValueError(NO_DATA_MESSAGE) - tempFigure = figures.create_simple_figure(aDataObject.data, aVarName + "\nin File A", - invalidMask=~aDataObject.masks.valid_mask, colorMap=cm.jet, units=aUnitsText) - # TODO, this is a hack to show AWIPS data, make an option for this at some point on the second tab - #tempFigure = figures.create_simple_figure(aDataObject.data.astype(np.uint8), aVarName + "\nin File A", - # invalidMask=~aDataObject.masks.valid_mask, colorMap=cm.bone, units=aUnitsText) + if dataForm == SIMPLE_2D : + tempFigure = figures.create_simple_figure(aDataObject.data, aVarName + "\nin File A", + invalidMask=~aDataObject.masks.valid_mask, colorMap=colorMapToUse, + colorbarLimits=rangeInfo, units=aUnitsText) + elif dataForm == MAPPED_2D : + #_, tempLatObj, _ = self._getVariableInformation("A", variableName=self.dataModel.getLatitudeName ("A")) + #_, tempLonObj, _ = self._getVariableInformation("A", variableName=self.dataModel.getLongitudeName("A")) + # TODO *** + #tempFigure = figures.create_mapped_figure(aDataObject.data, tempLatObj.data, tempLonObj.data, baseMapInstance, boundingAxes, title, + # invalidMask=None, colorMap=None, tagData=None, + # dataRanges=None, dataRangeNames=None, dataRangeColors=None, units=None, **kwargs) + pass + + elif dataForm == ONLY_1D : + temp = [(aDataObject.data, ~aDataObject.masks.valid_mask, 'b', None, None, None)] + tempFigure = figures.create_line_plot_figure(temp, aVarName + "\n in File A") + else : + raise ValueError(UNKNOWN_DATA_FORM) - elif imageType == model.ORIGINAL_B : + elif imageType == ORIGINAL_B : # if the data doesn't exist, we can't make this plot if bDataObject is None : raise ValueError(NO_DATA_MESSAGE) - tempFigure = figures.create_simple_figure(bDataObject.data, bVarName + "\nin File B", - invalidMask=~bDataObject.masks.valid_mask, colorMap=cm.jet, units=bUnitsText) + if dataForm == SIMPLE_2D : + tempFigure = figures.create_simple_figure(bDataObject.data, bVarName + "\nin File B", + invalidMask=~bDataObject.masks.valid_mask, colorMap=colorMapToUse, + colorbarLimits=rangeInfo, units=bUnitsText) + elif dataForm == MAPPED_2D : + pass # TODO + elif dataForm == ONLY_1D : + temp = [(bDataObject.data, ~bDataObject.masks.valid_mask, 'c', None, None, None)] + tempFigure = figures.create_line_plot_figure(temp, bVarName + "\n in File B") + else : + raise ValueError(UNKNOWN_DATA_FORM) - elif imageType in model.COMPARISON_IMAGES : + elif imageType in COMPARISON_IMAGES : # if we're making the absolute or raw difference image, do that - if (imageType == model.ABS_DIFF) or (imageType == model.RAW_DIFF) : + if (imageType == ABS_DIFF) or (imageType == RAW_DIFF) : # now choose between the raw and abs diff dataToUse = diffData.diff_data_object.data titlePrefix = "Value of (Data File B - Data File A)\nfor " - if imageType == model.ABS_DIFF : + if imageType == ABS_DIFF : dataToUse = np.abs(dataToUse) titlePrefix = "Absolute value of difference\nin " - tempFigure = figures.create_simple_figure(dataToUse, titlePrefix + aVarName, - invalidMask=~diffData.diff_data_object.masks.valid_mask, - colorMap=cm.jet, units=aUnitsText) - elif imageType == model.MISMATCH : + if dataForm == SIMPLE_2D : + tempFigure = figures.create_simple_figure(dataToUse, titlePrefix + aVarName, + invalidMask=~diffData.diff_data_object.masks.valid_mask, + colorMap=colorMapToUse, units=aUnitsText) + elif dataForm == MAPPED_2D : + pass # TODO + elif dataForm == ONLY_1D : + tempTitle = titlePrefix + aVarName + if aVarName != bVarName : + tempTitle = tempTitle + " / " + bVarName + temp = [(dataToUse, ~diffData.diff_data_object.masks.valid_mask, 'm', None, None, None)] + tempFigure = figures.create_line_plot_figure(temp, tempTitle) + else : + raise ValueError(UNKNOWN_DATA_FORM) + + elif imageType == MISMATCH : mismatchMask = diffData.diff_data_object.masks.mismatch_mask - tempFigure = figures.create_simple_figure(aDataObject.data, "Areas of mismatch data\nin " + aVarName, - invalidMask=~aDataObject.masks.valid_mask, tagData=mismatchMask, - colorMap=figures.MEDIUM_GRAY_COLOR_MAP, units=aUnitsText) + if dataForm == SIMPLE_2D : + tempFigure = figures.create_simple_figure(aDataObject.data, "Areas of mismatch data\nin " + aVarName, + invalidMask=~aDataObject.masks.valid_mask, tagData=mismatchMask, + colorMap=figures.MEDIUM_GRAY_COLOR_MAP, units=aUnitsText) + # TODO, change colormap? + elif dataForm == MAPPED_2D : + pass # TODO + elif dataForm == ONLY_1D : + + temp = [(aDataObject.data, ~aDataObject.masks.valid_mask, 'k', None, mismatchMask, None)] + tempFigure = figures.create_line_plot_figure(temp, "Areas of mismatch data\nin " + aVarName) + + else : + raise ValueError(UNKNOWN_DATA_FORM) + + elif imageType == HISTOGRAM : - elif imageType == model.HISTOGRAM : + # Note: histograms don't care about data format requested, they are histogram formatted rawDiffDataClean = diffData.diff_data_object.data[diffData.diff_data_object.masks.valid_mask] tempFigure = figures.create_histogram(rawDiffDataClean, DEFAULT_NUM_BINS, "Difference in\n" + aVarName, "Value of (B - A) at each data point", "Number of points with a given difference", units=aUnitsText) - elif (imageType == model.SCATTER) or (imageType == model.HEX_PLOT) : + elif (imageType == SCATTER) or (imageType == HEX_PLOT) : + + # Note: scatter and hex plots don't care about data format requested, they're scatter or hex plots tempCleanMask = aDataObject.masks.valid_mask & bDataObject.masks.valid_mask aDataClean = aDataObject.data[tempCleanMask] bDataClean = bDataObject.data[tempCleanMask] - if imageType == model.SCATTER : + if imageType == SCATTER : cleanMismatchMask = diffData.diff_data_object.masks.mismatch_mask[tempCleanMask] figures.create_scatter_plot(aDataClean, bDataClean, "Value in File A vs Value in File B", diff --git a/pyglance/glance/gui_model.py b/pyglance/glance/gui_model.py index f4c02b6..6877b9f 100644 --- a/pyglance/glance/gui_model.py +++ b/pyglance/glance/gui_model.py @@ -95,10 +95,23 @@ class GlanceGUIModel (object) : self.useSharedRange - True if images for the original data should be displayed in a range that includeds the data from both files, False if not + self.fileSettings - a dictionary of settings specific to a file, in the form: + { + "doRange": True or False for whether or not the range of the data should be restricted, + "minRange": The minimum acceptable value if the range is restricted, + "maxRange": The maximum acceptable value if the range is restricted, + "isAWIPS": True or False for whether or not the data is in AWIPS format + } + self.dataListeners - objects that want to be notified when data changes self.errorHandlers - objects that want to be notified when there's a serious error """ + DO_RANGE = "doRange" + MIN_RANGE = "minRange" + MAX_RANGE = "maxRange" + IS_AWIPS = "isAWIPS" + def __init__ (self) : """ set up the basic model with our initial default values and empty listener lists @@ -112,12 +125,28 @@ class GlanceGUIModel (object) : # general settings self.epsilon = 0.0 self.epsilonPercent = None - self.llEpsilon = 0.0 + self.llEpsilon = None self.imageType = IMAGE_TYPES[0] self.colormap = COLORMAP_NAMES[0] self.dataForm = SIMPLE_2D self.useSharedRange = False + # This is obviously only going to work for these two prefixes, would need + # to add a fully formed sub-class to make this more general + self.fileSettings = { } + self.fileSettings["A"] = { + GlanceGUIModel.DO_RANGE: False, + GlanceGUIModel.MIN_RANGE: None, + GlanceGUIModel.MAX_RANGE: None, + GlanceGUIModel.IS_AWIPS: False + } + self.fileSettings["B"] = { + GlanceGUIModel.DO_RANGE: False, + GlanceGUIModel.MIN_RANGE: None, + GlanceGUIModel.MAX_RANGE: None, + GlanceGUIModel.IS_AWIPS: False + } + # this represents all the people who want to hear about data updates # these people can register and will get data related messages self.dataListeners = [ ] @@ -171,6 +200,10 @@ class GlanceGUIModel (object) : self.fileData[filePrefix].latitude = DEFAULT_LATITUDE if DEFAULT_LONGITUDE in variableList : self.fileData[filePrefix].longitude = DEFAULT_LONGITUDE + # make sure the longitude and latitude are loaded into our local cache + # TODO, it would be good to background this task at some point + _ = self._load_variable_data(filePrefix, self.fileData[filePrefix].latitude) + _ = self._load_variable_data(filePrefix, self.fileData[filePrefix].longitude) # load info on the current variable tempDataObj = self._load_variable_data(filePrefix, tempVariable) @@ -259,6 +292,22 @@ class GlanceGUIModel (object) : dataListener.updateColormaps(self.colormap, list=COLORMAP_NAMES) dataListener.updateDataForms(self.dataForm, list=DATA_FORMS) dataListener.updateUseSharedRange(self.useSharedRange) + + self.sendFileSettings("A") + self.sendFileSettings("B") + + def sendFileSettings (self, file_prefix) : + """ + send out settings data that's related to the individual files but not data selections + """ + + # let our data listeners know about these values + for listener in self.dataListeners : + listener.updateDoRestrictRange (file_prefix, self.fileSettings[file_prefix][GlanceGUIModel.DO_RANGE ]) + listener.updateRestrictRangeMin(file_prefix, self.fileSettings[file_prefix][GlanceGUIModel.MIN_RANGE]) + listener.updateRestrictRangeMax(file_prefix, self.fileSettings[file_prefix][GlanceGUIModel.MAX_RANGE]) + listener.updateIsAWIPS (file_prefix, self.fileSettings[file_prefix][GlanceGUIModel.IS_AWIPS ]) + pass def updateFileDataSelection (self, file_prefix, newVariableText=None, newOverrideValue=None, newFillValue=np.nan) : """ @@ -318,25 +367,26 @@ class GlanceGUIModel (object) : """ someone has changed one or more of the general settings related data values - Note: if an input value is left at it's default (None or nan) then it's assumed that it was not externally changed + Note: if an input value is left at it's default (None or nan) + then it's assumed that it was not externally changed """ didUpdate = False # update the epsilon if needed - if newEpsilonValue is not np.nan : + if (newEpsilonValue is not np.nan) and (newEpsilonValue != self.epsilon) : LOG.debug("Setting epsilon to: " + str(newEpsilonValue)) self.epsilon = newEpsilonValue didUpdate = True # update the epsilon % - if newEpsilonPercent is not np.nan : + if (newEpsilonPercent is not np.nan) and (newEpsilonPercent != self.epsilonPercent) : LOG.debug("Setting epsilon percent to: " + str(newEpsilonPercent)) self.epsilonPercent = newEpsilonPercent didUpdate = True # update the lon/lat epsilon if needed - if newllEpsilon is not np.nan : + if (newllEpsilon is not np.nan) and (newllEpsilon != self.llEpsilon) : LOG.debug("Setting lon/lat epsilon to: " + str(newllEpsilon)) self.llEpsilon = newllEpsilon didUpdate = True @@ -380,6 +430,48 @@ class GlanceGUIModel (object) : listener.updateDataForms(self.dataForm) listener.updateUseSharedRange(self.useSharedRange) + def updateFileSettings (self, file_prefix, doRestrictRange=None, + newRangeMin=np.nan, newRangeMax=np.nan, + doCorrectForAWIPS=None) : + """ + someone has changed one or more of the file specific settings + + Note: if an input value is left at it's default (None or nan) + then it's assumed that it was not externally changed + """ + + didUpdate = False + + if file_prefix not in self.fileSettings.keys() : + LOG.warn("Unknown file prefix (" + str(file_prefix) + ") in updateFileSettings.") + return + + # update the range restriction setting if needed + if (doRestrictRange is not None) and (self.fileSettings[file_prefix][GlanceGUIModel.DO_RANGE] != doRestrictRange) : + LOG.debug("Setting use range restriction for file " + str(file_prefix) + " to: " + str(doRestrictRange)) + self.fileSettings[file_prefix][GlanceGUIModel.DO_RANGE] = doRestrictRange + didUpdate = True + + if (newRangeMin is not np.nan) and (self.fileSettings[file_prefix][GlanceGUIModel.MIN_RANGE] != newRangeMin) : + LOG.debug("Setting minimum value for range restriction in file " + str(file_prefix) + " to: " + str(newRangeMin)) + self.fileSettings[file_prefix][GlanceGUIModel.MIN_RANGE] = newRangeMin + didUpdate = True + + if (newRangeMax is not np.nan) and (self.fileSettings[file_prefix][GlanceGUIModel.MAX_RANGE] != newRangeMax) : + LOG.debug("Setting maximum value for range restriction in file " + str(file_prefix) + " to: " + str(newRangeMax)) + self.fileSettings[file_prefix][GlanceGUIModel.MAX_RANGE] = newRangeMax + didUpdate = True + + if (doCorrectForAWIPS is not None) and (self.fileSettings[file_prefix][GlanceGUIModel.IS_AWIPS] != doCorrectForAWIPS) : + if (doCorrectForAWIPS is True) or (doCorrectForAWIPS is False) : + LOG.debug("Setting do AWIPS data correction for file " + str(file_prefix) + " to: " + str(doCorrectForAWIPS)) + self.fileSettings[file_prefix][GlanceGUIModel.IS_AWIPS] = doCorrectForAWIPS + didUpdate = True + + # let our data listeners know about any changes + if didUpdate : + self.sendFileSettings(file_prefix) + def updateLonLatSelections (self, file_prefix, new_latitude_name=None, new_longitude_name=None) : """ someone has changed one or more of the file specific longitude/latitude related values @@ -448,11 +540,14 @@ class GlanceGUIModel (object) : return toReturn - def getVariableData (self, filePrefix, variableName) : + def getVariableData (self, filePrefix, variableName, doCorrections=False) : """ get the data object for the variable of variableName associated with the file prefix or None if that variable is not loaded + If doCorrections is True, data filtering for AWIPS and range corrections will be done + by this function based on the currently selected settings for that file. + Note: this is not a copy, but the original object, so any manipulations done to it will be reflected in the model """ @@ -460,6 +555,18 @@ class GlanceGUIModel (object) : if (filePrefix in self.fileData) and (variableName in self.fileData[filePrefix].var_data_cache) : toReturn = self.fileData[filePrefix].var_data_cache[variableName] + + if (self.fileSettings[filePrefix][GlanceGUIModel.IS_AWIPS] or + self.fileSettings[filePrefix][GlanceGUIModel.DO_RANGE]) : + toReturn = toReturn.copy() + + if self.fileSettings[filePrefix][GlanceGUIModel.IS_AWIPS] : + toReturn.data = toReturn.data.astype(np.uint8) # TODO, will setting this break anything? + if self.fileSettings[filePrefix][GlanceGUIModel.DO_RANGE] : + if self.fileSettings[filePrefix][GlanceGUIModel.MIN_RANGE] is not None : + toReturn.data[toReturn.data < self.fileSettings[filePrefix][GlanceGUIModel.MIN_RANGE]] = toReturn.fill_value + if self.fileSettings[filePrefix][GlanceGUIModel.MAX_RANGE] is not None : + toReturn.data[toReturn.data > self.fileSettings[filePrefix][GlanceGUIModel.MAX_RANGE]] = toReturn.fill_value return toReturn @@ -511,11 +618,28 @@ class GlanceGUIModel (object) : """ return self.imageType + def getColormapName (self) : + """ + get the name of the colormap to use + + the return will be on of the constants from gui_constants: + COLORMAP_NAMES = [CM_RAINBOW, CM_GRAY] + """ + + return str(self.colormap) + + def getIsAWIPS (self, filePrefix) : + """ + get whether or not the data is in AWIPS format + """ + + return self.fileSettings[filePrefix][GlanceGUIModel.IS_AWIPS] + def getDataForm (self) : """ get the text string describing the data form currently selected - the return will correspond to one of the constants from this module: + the return will correspond to one of the constants from glance.gui_constants: SIMPLE_2D MAPPED_2D @@ -524,6 +648,13 @@ class GlanceGUIModel (object) : return self.dataForm + def getShouldShowOriginalPlotsInSameRange (self) : + """ + get whether or not the original plots should be shown in a shared range + """ + + return self.useSharedRange + def registerDataListener (self, objectToRegister) : """ add the given object to our list of data listeners diff --git a/pyglance/glance/gui_view.py b/pyglance/glance/gui_view.py index 2e7b657..9482f6b 100644 --- a/pyglance/glance/gui_view.py +++ b/pyglance/glance/gui_view.py @@ -49,12 +49,19 @@ class _DoubleOrNoneValidator (QtGui.QDoubleValidator) : correct the input if needed """ - value = QtCore.QString(value) + ok = False - if (value == "") or (value == "N") or (value == "No") or (value == "Non") : + if (value == "N") or (value == "No") or (value == "Non") : value = QtCore.QString("None") + ok = True + if (value == "") or (value == None) : + value = QtCore.QString("") + ok = True + + value = QtCore.QString(value) - super(self.__class__, self).fixup(value) + if not ok : + super(self.__class__, self).fixup(value) class GlanceGUIView (QtGui.QWidget) : @@ -110,8 +117,7 @@ class GlanceGUIView (QtGui.QWidget) : # add a tab that allows more detailed, optional settings tempWidget = QtGui.QWidget() tempWidget.setLayout(self._build_settings_tab()) - self.tempHangOnToTab = tempWidget # TODO, remove when line below is uncommented - #self.tabWidget.addTab(tempWidget, "settings") #TODO, uncomment this when this tab is ready to go + self.tabWidget.addTab(tempWidget, "settings") tempLayout = QtGui.QGridLayout() tempLayout.addWidget(self.tabWidget) @@ -147,9 +153,8 @@ class GlanceGUIView (QtGui.QWidget) : # set up the epsilon input box layoutToUse.addWidget(QtGui.QLabel("epsilon:"), currentRow, 0) self.epsilonWidget = QtGui.QLineEdit() - self.epsilonWidget.setToolTip("Maximum acceptible difference between the variable data in the two files.") + self.epsilonWidget.setToolTip("Maximum acceptible difference between the variable data in the two files. Leave blank or enter None for no comparison.") tempValidator = _DoubleOrNoneValidator(self.epsilonWidget) - #tempValidator = QtGui.QDoubleValidator(self.epsilonWidget) tempValidator.setBottom(0.0) # only accept positive epsilons tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) self.epsilonWidget.setValidator(tempValidator) @@ -161,9 +166,8 @@ class GlanceGUIView (QtGui.QWidget) : # set up the epsilon percent input box layoutToUse.addWidget(QtGui.QLabel("epsilon percent:"), currentRow, 0) self.epsilonPerWidget = QtGui.QLineEdit() - self.epsilonPerWidget.setToolTip("Maximum acceptible difference between the variable data in terms of % of each data point in the A file.") + self.epsilonPerWidget.setToolTip("Maximum acceptible difference between the variable data in terms of % of each data point in the A file. Leave blank or enter None for no comparison.") tempValidator = _DoubleOrNoneValidator(self.epsilonPerWidget) - #tempValidator = QtGui.QDoubleValidator(self.epsilonPerWidget) tempValidator.setBottom(0.0) # only accept positive epsilon percents tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) self.epsilonPerWidget.setValidator(tempValidator) @@ -266,7 +270,7 @@ class GlanceGUIView (QtGui.QWidget) : grid_layout.addWidget(QtGui.QLabel("fill value:"), currentRow+1, 1) fillValue = QtGui.QLineEdit() fillValue.setToolTip("The fill value that will be used.") - tempValidator = QtGui.QDoubleValidator(fillValue) + tempValidator = _DoubleOrNoneValidator(fillValue) tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) fillValue.setValidator(tempValidator) fillValue.setDisabled(True) @@ -316,26 +320,27 @@ class GlanceGUIView (QtGui.QWidget) : # add lon/lat controls - # add the lon/lat controls that are separated by file - currentRow = self._add_lon_lat_controls("A", layoutToUse, currentRow) - currentRow = self._add_lon_lat_controls("B", layoutToUse, currentRow) + # add the lon/lat controls that are separated by file TODO add these back in + #currentRow = self._add_lon_lat_controls("A", layoutToUse, currentRow) + #currentRow = self._add_lon_lat_controls("B", layoutToUse, currentRow) # add the filtering related controls currentRow = self._add_filter_controls("A", layoutToUse, currentRow) currentRow = self._add_filter_controls("B", layoutToUse, currentRow) + """ TODO, add this back when the lon/lat epsilon is being used # add box to enter lon/lat epsilon layoutToUse.addWidget(QtGui.QLabel("lon/lat epsilon:"), currentRow, 0) llepsilonWidget = QtGui.QLineEdit() self.llepsilonWidget = llepsilonWidget - llepsilonWidget.setToolTip("Maximum acceptible difference between the longitudes or latitudes in the two files.") + llepsilonWidget.setToolTip("Maximum acceptible difference between the longitudes or latitudes in the two files. Leave blank or enter None for no comparison.") tempValidator = _DoubleOrNoneValidator(llepsilonWidget) - #tempValidator = QtGui.QDoubleValidator(llepsilonWidget) tempValidator.setBottom(0.0) # only accept positive epsilons tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) llepsilonWidget.setValidator(tempValidator) llepsilonWidget.editingFinished.connect(self.reportLLEpsilonChanged) layoutToUse.addWidget(llepsilonWidget, currentRow, 1, 1, 2) + """ return layoutToUse @@ -366,8 +371,8 @@ class GlanceGUIView (QtGui.QWidget) : # first add the min minRangeValue = QtGui.QLineEdit() - minRangeValue.setToolTip("Minimum acceptable data value for " + str(file_prefix) + " data") - tempValidator = QtGui.QDoubleValidator(minRangeValue) + minRangeValue.setToolTip("Minimum acceptable data value for " + str(file_prefix) + " data. (Leave blank or enter None for no limit.)") + tempValidator = _DoubleOrNoneValidator(minRangeValue) # at some point the bottom and top of the ranges will need to be set on this validator # do I need to save it or can I recover it from the widget? tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) @@ -381,8 +386,8 @@ class GlanceGUIView (QtGui.QWidget) : # first add the min maxRangeValue = QtGui.QLineEdit() - maxRangeValue.setToolTip("Maximum acceptable data value for " + str(file_prefix) + " data") - tempValidator = QtGui.QDoubleValidator(maxRangeValue) + maxRangeValue.setToolTip("Maximum acceptable data value for " + str(file_prefix) + " data. (Leave blank or enter None for no limit.)") + tempValidator = _DoubleOrNoneValidator(maxRangeValue) # at some point the bottom and top of the ranges will need to be set on this validator # do I need to save it or can I recover it from the widget? tempValidator.setNotation(QtGui.QDoubleValidator.StandardNotation) @@ -518,6 +523,8 @@ class GlanceGUIView (QtGui.QWidget) : when the lon/lat epsilon changes, report it to user update listeners """ + pass + """ TODO uncomment when this is needed newLLEpsilon = self.llepsilonWidget.text() # it's still possible for epsilon to not be a number, so fix that newLLEpsilon = self._extra_number_validation(newLLEpsilon) @@ -526,6 +533,7 @@ class GlanceGUIView (QtGui.QWidget) : # let our user update listeners know the llepsilon changed for listener in self.userUpdateListeners : listener.userChangedLLEpsilon(newLLEpsilon) + """ def reportEpsilonPercentChanged (self) : @@ -619,28 +627,60 @@ class GlanceGUIView (QtGui.QWidget) : the user toggled the "restrict data to range" check box """ - print ("*** restrict range checkbox toggled TODO " + file_prefix) + # this must be recorded before we tamper with the focus, because that will + # trigger other events that may erase this information temporarily + shouldRestrictRange = self.widgetInfo[file_prefix]["doRestrictRangeCheckbox"].isChecked() + + # first we need to clean up focus in case it's in one of the line-edit boxes + self.setFocus() + + # let our listeners know the user changed an overload setting + for listener in self.userUpdateListeners : + listener.userToggledRestrictRange(file_prefix, shouldRestrictRange) def reportMinRangeValChanged (self, file_prefix=None) : """ the user changed the minimum value for the acceptable range """ - print ("*** minimum range value changed TODO " + file_prefix) + newRangeMin = self.widgetInfo[file_prefix]["minRangeRestriction"].text() + # it's still possible for this to not be a number, so fix that + newRangeMin = self._extra_number_validation(newRangeMin) + self.widgetInfo[file_prefix]["minRangeRestriction"].setText(str(newRangeMin)) + + # let our user update listeners know the value changed + for listener in self.userUpdateListeners : + listener.userChangedRangeMin(file_prefix, newRangeMin) def reportMaxRangeValChanged (self, file_prefix=None) : """ the user changed the maximum value for the acceptable range """ - print ("*** maximum range value changed TODO " + file_prefix) + newRangeMax = self.widgetInfo[file_prefix]["maxRangeRestriction"].text() + # it's still possible for this to not be a number, so fix that + newRangeMax = self._extra_number_validation(newRangeMax) + self.widgetInfo[file_prefix]["maxRangeRestriction"].setText(str(newRangeMax)) + + # let our user update listeners know the value changed + for listener in self.userUpdateListeners : + listener.userChangedRangeMax(file_prefix, newRangeMax) def reportIsAWIPSToggled (self, file_prefix=None) : """ the user toggled the "correct for AWIPS data types" check box """ - print ("*** AWIPS check box toggled TODO " + file_prefix) + # this must be recorded before we tamper with the focus, because that will + # trigger other events that may erase this information temporarily + dataIsAWIPS = self.widgetInfo[file_prefix]["isAWIPScheckbox"].isChecked() + + # first we need to clean up focus in case it's in one of the line-edit boxes + self.setFocus() + + # let our listeners know the user changed an overload setting + for listener in self.userUpdateListeners : + listener.userToggledIsAWIPS(file_prefix, dataIsAWIPS) def reportDisplayStatsClicked (self) : """ @@ -673,13 +713,14 @@ class GlanceGUIView (QtGui.QWidget) : toReturn = None - try : - toReturn = int(string_that_should_be_a_number) - except ValueError : + if toReturn != "" : try : - toReturn = float(string_that_should_be_a_number) + toReturn = int(string_that_should_be_a_number) except ValueError : - pass # in this case we can't convert it, so just toss it + try : + toReturn = float(string_that_should_be_a_number) + except ValueError : + pass # in this case we can't convert it, so just toss it return toReturn @@ -751,7 +792,8 @@ class GlanceGUIView (QtGui.QWidget) : Update the latitude and longitude names that are selected in the drop down, if a list is given, then replace the list of options that are being displayed for that file. """ - + pass + """ TODO uncomment this when these controls are finished # if we got a new list, set up the appropriate drop down lists if lonlatList is not None : @@ -768,12 +810,15 @@ class GlanceGUIView (QtGui.QWidget) : # set the selected longitude tempPosition = self.widgetInfo[filePrefix]['lonName'].findText(newLongitude) self.widgetInfo[filePrefix]['lonName'].setCurrentIndex(tempPosition) + """ def updateEpsilon (self, epsilon) : """ update the comparison epsilon displayed to the user """ + stringToUse = str(epsilon) if epsilon is not None else "" + self.epsilonWidget.setText(str(epsilon)) def updateEpsilonPercent (self, epsilonPercent) : @@ -788,7 +833,8 @@ class GlanceGUIView (QtGui.QWidget) : update the epsilon for longitude and latitude displayed to the user """ - self.llepsilonWidget.setText(str(newLonLatEpsilon)) + #self.llepsilonWidget.setText(str(newLonLatEpsilon)) TODO, uncomment when we need this contro + pass def updateImageTypes (self, imageType, list=None) : """ @@ -842,6 +888,34 @@ class GlanceGUIView (QtGui.QWidget) : self.useSameRangeWidget.setChecked(doUseSharedRange) + def updateDoRestrictRange (self, filePrefix, doRestrictRange) : + """ + update our control to reflect whether or not the range is going to be restricted + """ + + self.widgetInfo[filePrefix]["doRestrictRangeCheckbox"].setChecked(doRestrictRange) + + def updateRestrictRangeMin (self, filePrefix, newRangeMin) : + """ + update the minimum for the range restriction + """ + + self.widgetInfo[filePrefix]["minRangeRestriction"].setText(str(newRangeMin)) + + def updateRestrictRangeMax (self, filePrefix, newRangeMax) : + """ + update the maximum for the range restriction + """ + + self.widgetInfo[filePrefix]["maxRangeRestriction"].setText(str(newRangeMax)) + + def updateIsAWIPS (self, filePrefix, isAWIPS) : + """ + update whether or not this should be treated as an AWIPS file + """ + + self.widgetInfo[filePrefix]["isAWIPScheckbox"].setChecked(isAWIPS) + ################# end data model update related methods ################# def showWarning (self, warningMessage): -- GitLab