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