def __init__(self, parent=None):
        # First set up the flood detection stuff
        self.detectParams = FloodDetectParams()
        self.qtDate         = None # Date of the flood to analyze.
        self.floodDate      = None # Date of the flood to analyze.
        self.modisCloudMask = None # Cloud mask from 500m MODIS
        self.modisPrior     = None # First cloud free  MODIS image < date.
        self.modisPost      = None # First cloud light MODIS image >= the date.
        self.landsatPrior   = None # First cloud free  Landsat image < date.
        self.landsatPost    = None # First cloud light Landsat image >= the date.
        self.sentinel1Prior = None # First Sentinel1 image < date.
        self.sentinel1Post  = None # First Sentinel1 image >= date.
        self.demImage       = None # DEM image
        self.permWaterMask  = None # The permanent water mask, never changes.
        self.guestImage     = None # A manually loaded image
        self.eeFunction     = None # Flood detection results TODO: Rename variable!
        self.landsatType    = None # One of the types at the top of the file
        self.classWindow    = None # Handle for class training window
        
        
        # Init a tile manager and load it with cache from disk
        self.tileManager = TileManager(DEFAULT_MAP_URL_PATTERN)
        if os.path.exists(LOCAL_MAP_CACHE_PATH):
            self.tileManager.LoadCacheFromDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH

        # Now set up all the GUI stuff
        QtGui.QWidget.__init__(self, parent)
        self.mapWidget = MapViewWidget(self.tileManager)
        
        # Set up all the components in a vertical layout
        vbox = QtGui.QVBoxLayout()
        
        # Add a horizontal row of widgets at the top
        topHorizontalBox = QtGui.QHBoxLayout()
        
        TOP_BUTTON_HEIGHT = 30
        TOP_LARGE_BUTTON_WIDTH  = 150
        TOP_SMALL_BUTTON_WIDTH  = 100
        
        # Add a date selector to the top row of widgets
        DEFAULT_START_DATE = ee.Date.fromYMD(2015, 11, 15)
        self.floodDate = DEFAULT_START_DATE
        dateString     = '2015/11/15' # TODO: Generate from the default start date
        self.dateButton = QtGui.QPushButton(dateString, self)
        self.dateButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.clicked[bool].connect(self._showCalendar)
        topHorizontalBox.addWidget(self.dateButton)
        
        # Add a "Set Region" button to the top row of widgets
        self.regionButton = QtGui.QPushButton('Set Processing Region', self)
        self.regionButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT) 
        self.regionButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.regionButton.clicked[bool].connect(self._setRegionToView)
        topHorizontalBox.addWidget(self.regionButton)

        # Add a "Load Images" button to the top row of widgets
        self.loadImagesButton = QtGui.QPushButton('Load Images', self)
        self.loadImagesButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.clicked[bool].connect(self._loadImageData)
        topHorizontalBox.addWidget(self.loadImagesButton)

        # Add a "Detect Flood" button to the top row of widgets
        self.loadFloodButton1 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton1.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.clicked[bool].connect(self._loadFloodDetect)
        topHorizontalBox.addWidget(self.loadFloodButton1)

        # Add a "Load ME Image" button to the top row of widgets
        self.loadMeImageButton = QtGui.QPushButton('Load ME Image', self)
        self.loadMeImageButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.clicked[bool].connect(self._loadMapsEngineImage)
        topHorizontalBox.addWidget(self.loadMeImageButton)

        # Add a "Open Class Trainer" button to the top row of widgets
        self.openTrainerButton = QtGui.QPushButton('Open Class Trainer', self)
        self.openTrainerButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.clicked[bool].connect(self._openClassTrainer)
        topHorizontalBox.addWidget(self.openTrainerButton)

        # Add a "Clear All" button to the top row of widgets
        self.clearButton = QtGui.QPushButton('Clear Map', self)
        self.clearButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.clicked[bool].connect(self._unloadCurrentImages)
        topHorizontalBox.addWidget(self.clearButton)

        # Add the row of widgets on the top of the GUI
        vbox.addLayout(topHorizontalBox)
        # Add the main map widget
        vbox.addWidget(self.mapWidget)
                
        # Set up a horizontal box below the map
        bottomHorizontalBox = QtGui.QHBoxLayout()
        # On the left side is a vertical box for the parameter controls
        paramControlBoxV    = QtGui.QVBoxLayout()
        
        # First set up some sliders to adjust thresholds
        # - Currently we have two thresholds
        sliderParams = ['Change Detection Threshold', 'Water Mask Threshold']
        paramMin     = [-10, -10]
        paramMax     = [ 10,  10]
        defaultVal   = [-3, 3]
        
        # Build each of the parameter sliders
        self.sliderList = []
        for name, minVal, maxVal, default in zip(sliderParams, paramMin, paramMax, defaultVal):
            # Stick the horizontal box on the bottom of the main vertical box
            paramControlBoxV = self._addParamSlider(name, maxVal, minVal, default, paramControlBoxV)
        bottomHorizontalBox.addLayout(paramControlBoxV) # Add sliders to bottom horizontal box

        # Add a "Detect Flood" button to the right of the parameter controls
        # - This is identical to the button above the map.
        self.loadFloodButton2 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton2.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.clicked[bool].connect(self._loadFloodDetect)
        bottomHorizontalBox.addWidget(self.loadFloodButton2)

        # Add all the stuff at the bottom to the main layout
        vbox.addLayout(bottomHorizontalBox)

        # QMainWindow requires that its layout be set in this manner
        mainWidget = QtGui.QWidget()
        mainWidget.setLayout(vbox)
        self.setCentralWidget(mainWidget)
        
        # This is the initial window size, but the user can resize it.
        self.setGeometry(100, 100, 720, 720) 
        self.setWindowTitle('EE Flood Detector Tool')
        self.show()
class ProductionGui(QtGui.QMainWindow):
    '''This sets up the main viewing window in QT, fills it up with a MapView,
       and then forwards all function calls to it.'''
    
    def __init__(self, parent=None):
        # First set up the flood detection stuff
        self.detectParams = FloodDetectParams()
        self.qtDate         = None # Date of the flood to analyze.
        self.floodDate      = None # Date of the flood to analyze.
        self.modisCloudMask = None # Cloud mask from 500m MODIS
        self.modisPrior     = None # First cloud free  MODIS image < date.
        self.modisPost      = None # First cloud light MODIS image >= the date.
        self.landsatPrior   = None # First cloud free  Landsat image < date.
        self.landsatPost    = None # First cloud light Landsat image >= the date.
        self.sentinel1Prior = None # First Sentinel1 image < date.
        self.sentinel1Post  = None # First Sentinel1 image >= date.
        self.demImage       = None # DEM image
        self.permWaterMask  = None # The permanent water mask, never changes.
        self.guestImage     = None # A manually loaded image
        self.eeFunction     = None # Flood detection results TODO: Rename variable!
        self.landsatType    = None # One of the types at the top of the file
        self.classWindow    = None # Handle for class training window
        
        
        # Init a tile manager and load it with cache from disk
        self.tileManager = TileManager(DEFAULT_MAP_URL_PATTERN)
        if os.path.exists(LOCAL_MAP_CACHE_PATH):
            self.tileManager.LoadCacheFromDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH

        # Now set up all the GUI stuff
        QtGui.QWidget.__init__(self, parent)
        self.mapWidget = MapViewWidget(self.tileManager)
        
        # Set up all the components in a vertical layout
        vbox = QtGui.QVBoxLayout()
        
        # Add a horizontal row of widgets at the top
        topHorizontalBox = QtGui.QHBoxLayout()
        
        TOP_BUTTON_HEIGHT = 30
        TOP_LARGE_BUTTON_WIDTH  = 150
        TOP_SMALL_BUTTON_WIDTH  = 100
        
        # Add a date selector to the top row of widgets
        DEFAULT_START_DATE = ee.Date.fromYMD(2015, 11, 15)
        self.floodDate = DEFAULT_START_DATE
        dateString     = '2015/11/15' # TODO: Generate from the default start date
        self.dateButton = QtGui.QPushButton(dateString, self)
        self.dateButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.clicked[bool].connect(self._showCalendar)
        topHorizontalBox.addWidget(self.dateButton)
        
        # Add a "Set Region" button to the top row of widgets
        self.regionButton = QtGui.QPushButton('Set Processing Region', self)
        self.regionButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT) 
        self.regionButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.regionButton.clicked[bool].connect(self._setRegionToView)
        topHorizontalBox.addWidget(self.regionButton)

        # Add a "Load Images" button to the top row of widgets
        self.loadImagesButton = QtGui.QPushButton('Load Images', self)
        self.loadImagesButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.clicked[bool].connect(self._loadImageData)
        topHorizontalBox.addWidget(self.loadImagesButton)

        # Add a "Detect Flood" button to the top row of widgets
        self.loadFloodButton1 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton1.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.clicked[bool].connect(self._loadFloodDetect)
        topHorizontalBox.addWidget(self.loadFloodButton1)

        # Add a "Load ME Image" button to the top row of widgets
        self.loadMeImageButton = QtGui.QPushButton('Load ME Image', self)
        self.loadMeImageButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.clicked[bool].connect(self._loadMapsEngineImage)
        topHorizontalBox.addWidget(self.loadMeImageButton)

        # Add a "Open Class Trainer" button to the top row of widgets
        self.openTrainerButton = QtGui.QPushButton('Open Class Trainer', self)
        self.openTrainerButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.clicked[bool].connect(self._openClassTrainer)
        topHorizontalBox.addWidget(self.openTrainerButton)

        # Add a "Clear All" button to the top row of widgets
        self.clearButton = QtGui.QPushButton('Clear Map', self)
        self.clearButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.clicked[bool].connect(self._unloadCurrentImages)
        topHorizontalBox.addWidget(self.clearButton)

        # Add the row of widgets on the top of the GUI
        vbox.addLayout(topHorizontalBox)
        # Add the main map widget
        vbox.addWidget(self.mapWidget)
                
        # Set up a horizontal box below the map
        bottomHorizontalBox = QtGui.QHBoxLayout()
        # On the left side is a vertical box for the parameter controls
        paramControlBoxV    = QtGui.QVBoxLayout()
        
        # First set up some sliders to adjust thresholds
        # - Currently we have two thresholds
        sliderParams = ['Change Detection Threshold', 'Water Mask Threshold']
        paramMin     = [-10, -10]
        paramMax     = [ 10,  10]
        defaultVal   = [-3, 3]
        
        # Build each of the parameter sliders
        self.sliderList = []
        for name, minVal, maxVal, default in zip(sliderParams, paramMin, paramMax, defaultVal):
            # Stick the horizontal box on the bottom of the main vertical box
            paramControlBoxV = self._addParamSlider(name, maxVal, minVal, default, paramControlBoxV)
        bottomHorizontalBox.addLayout(paramControlBoxV) # Add sliders to bottom horizontal box

        # Add a "Detect Flood" button to the right of the parameter controls
        # - This is identical to the button above the map.
        self.loadFloodButton2 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton2.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.clicked[bool].connect(self._loadFloodDetect)
        bottomHorizontalBox.addWidget(self.loadFloodButton2)

        # Add all the stuff at the bottom to the main layout
        vbox.addLayout(bottomHorizontalBox)

        # QMainWindow requires that its layout be set in this manner
        mainWidget = QtGui.QWidget()
        mainWidget.setLayout(vbox)
        self.setCentralWidget(mainWidget)
        
        # This is the initial window size, but the user can resize it.
        self.setGeometry(100, 100, 720, 720) 
        self.setWindowTitle('EE Flood Detector Tool')
        self.show()

    def closeEvent(self, event):
        '''Cleanup all other windows and dump cache to disk'''
        if self.classWindow:
            self.classWindow.close()

        #try:
        print 'Attempting to save tile cache...'
        self.tileManager.SaveCacheToDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH


    def _addParamSlider(self, name, maxVal, minVal, defaultVal, container):
        '''Adds a single parameter slider to the passed in container.'''
        # All parameter sliders are handled by the _handleParamChange function
    
        NAME_WIDTH    = 250
        SLIDER_HEIGHT = 20
        SLIDER_WIDTH  = 400
        NUM_TICKS     = 4
        
        # Set up this value slider
        slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
        slider.setRange(minVal, maxVal) 
        slider.setValue(defaultVal)
        valRange = maxVal - minVal
        slider.setTickInterval(valRange/NUM_TICKS) # Add five tick marks
        slider.setMinimumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        slider.setMaximumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        # Use 'partial' to send the param name to the callback function
        callbackFunction = functools.partial(self._handleParamChange, parameterName=name) 
        slider.valueChanged.connect(callbackFunction) # Whenever the slider is moved, trigger callback function
        self.sliderList.append(slider) # TODO: Do we need this?
    
        # Make box with the name
        nameBox = QtGui.QLabel(name, self)
        nameBox.setMinimumSize(NAME_WIDTH, SLIDER_HEIGHT)
        nameBox.setMaximumSize(NAME_WIDTH, SLIDER_HEIGHT)
        
        # Put the name to the left of the slider
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(nameBox)
        hbox.addWidget(slider)
        
        # Stick the horizontal box on the bottom of the main vertical box
        container.addLayout(hbox)
        return container
    
    
    def _openClassTrainer(self):
        '''Open a new window to define classifier training regions'''
        # Create the class trainer window and connect it to the map widget
        
        if not self.classWindow:
            self.classWindow = FeatureTrainerWindow(self.mapWidget)
            self.classWindow.show()
        else:
            self.classWindow.show()
        
        

    def _unloadCurrentImages(self):
        '''Just unload all the current images. Low level function'''
        if self.modisPrior:
            self.mapWidget.removeFromMap(self.modisPrior)
            self.modisPrior = None
        if self.modisPost:
            self.mapWidget.removeFromMap(self.modisPost)
            self.modisPost = None
        if self.modisCloudMask:
            self.mapWidget.removeFromMap(self.modisCloudMask)
            self.modisCloudMask = None
        if self.landsatPrior:
            self.mapWidget.removeFromMap(self.landsatPrior)
            self.landsatPrior = None
        if self.landsatPost:
            self.mapWidget.removeFromMap(self.landsatPost)
            self.landsatPost = None
        if self.sentinel1Prior:
            self.mapWidget.removeFromMap(self.sentinel1Prior)
            self.sentinel1Prior = None
        if self.sentinel1Post:
            self.mapWidget.removeFromMap(self.sentinel1Post)
            self.sentinel1Post = None
        if self.demImage:
            self.mapWidget.removeFromMap(self.demImage)
            self.demImage = None
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)
            self.eeFunction = None

    class GuestImageDialog(QtGui.QDialog):
        '''Popup window for the user to fill in information about an image loaded in Maps Engine'''
        def __init__(self, parent = None):
            super(ProductionGui.GuestImageDialog, self).__init__(parent)
            
            # Get the list of available sensors
            self.radioNames = miscUtilities.getDefinedSensorNames()
            if not self.radioNames:
                raise Exception('Could not load any sensors!')
            # Set up sensor selection buttons
            self.radioButtons = []
            for name in self.radioNames:
                self.radioButtons.append(QtGui.QRadioButton(name))
            self.radioButtons[0].setChecked(True)
            
            # Set up the other controls
            self.lineIn       = QtGui.QLineEdit("Asset ID")
            self.okButton     = QtGui.QPushButton('Ok',     self)
            self.cancelButton = QtGui.QPushButton('Cancel', self)            
            self.okButton.clicked.connect(self.accept)
            self.cancelButton.clicked.connect(self.reject)
            
            # Lay everything out
            vbox = QtGui.QVBoxLayout(self)
            vbox.addWidget(self.lineIn)
            for button in self.radioButtons:
               vbox.addWidget(button)
            vbox.addWidget(self.okButton)
            vbox.addWidget(self.cancelButton)
            vbox.addStretch(1)
        
        def getValues(self):
            '''Return the selected values'''
            for button in self.radioButtons: # Should always be exactly one button checked!
                if button.isChecked():
                    return (str(button.text()), str(self.lineIn.text()))
            raise Exception('GUI buttons are broken!')
        
    def _loadMapsEngineImage(self):
        '''Loads in an image stored in Google Maps Engine'''

        raise Exception('TODO: Update for the death of Maps Engine!')
    
        # Pop up a dialog and get the input values back
        dialog = self.GuestImageDialog(self)
        result = dialog.exec_()
        sensorName, eeID = dialog.getValues()
        
        if not result: # Do nothing if the cancel button was pressed
            return
        
        # Extract the asset ID from the Earth Engine ID
        # - The Earth Engine ID looks like this: GME/images/18108519531116889794-15007110928626476628
        pt = eeID.rfind('/')
        if not pt:
            print 'Invalid Earth Engine ID entered!'
        assetID = eeID[pt+1:]
        
        # Clear any existing guest image from the map
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None

        # Retrieve the sensor information for this image from the XML file
        sensorXmlPath = os.path.join(SENSOR_FILE_DIR, sensorName + ".xml")
        print 'Reading file: ' + sensorXmlPath
        sensorInfo = cmt.domain.SensorObservation(xml_source=sensorXmlPath, manual_ee_ID=assetID)
        
        # Add the new guest image to the map
        self.guestImage, vis_params, im_name, show = sensorInfo.visualize()
        self.mapWidget.addToMap(self.guestImage, vis_params, sensorName, True)
        
        



    def _displayCurrentImages(self):
        '''Add all the current images to the map. Low level function'''
        
        MODIS_RANGE  = [0, 3000]
        DEM_RANGE    = [0, 1000]
        
        # TODO: Come up with a method for setting the intensity bounds!
        landsatVisParams = {'bands': ['red', 'green', 'blue'], 'min': 0, 'max': 0.75}
            
        sentinel1VisParams = {'bands': ['vv'], 'min': -30, 'max': 5}
        
        modisVisParams = {'bands': ['sur_refl_b01', 'sur_refl_b02', 'sur_refl_b06'],
                          'min': MODIS_RANGE[0], 'max': MODIS_RANGE[1]}

        if self.landsatPrior:
            self.mapWidget.addToMap(self.landsatPrior, landsatVisParams, 'LANDSAT Pre-Flood', False)
        if self.landsatPost:
            self.mapWidget.addToMap(self.landsatPost, landsatVisParams, 'LANDSAT Post-Flood', True)
           
        if self.sentinel1Prior:
            self.mapWidget.addToMap(self.sentinel1Prior, sentinel1VisParams, 'Sentinel-1 Pre-Flood', False)
        if self.sentinel1Post:
            self.mapWidget.addToMap(self.sentinel1Post, sentinel1VisParams, 'Sentinel-1 Post-Flood', False)
         
        if self.modisPrior:
            self.mapWidget.addToMap(self.modisPrior, modisVisParams, 'MODIS Pre-Flood', False)
        if self.modisPost:
            self.mapWidget.addToMap(self.modisPost, modisVisParams, 'MODIS Post-Flood', False)
            
        # This one works a little differently since it never changes
        # - We just add this once and never remove it.
        if not self.permWaterMask:
            self.permWaterMask = cmt.modis.modis_utilities.get_permanent_water_mask()
            self.mapWidget.addToMap(self.permWaterMask.mask(self.permWaterMask),
                                    {'min': 0 , 'max': 1, 'palette': '000000, 0000FF'}, 'Permanent Water Mask', False)
            
        if self.modisCloudMask:
            vis_params = {'min': 0, 'max': 1, 'palette': '000000, FF0000'}
            self.mapWidget.addToMap(self.modisCloudMask, vis_params, '1km Bad MODIS pixels', False)

        if self.demImage:
            vis_params = {'min': DEM_RANGE[0], 'max': DEM_RANGE[1]}
            self.mapWidget.addToMap(self.demImage, vis_params, 'Digital Elevation Map', False)

    def _loadImageData(self):
        '''Updates the MODIS and LANDSAT images for the current date'''
        
        # Check that we have all the information we need
        bounds = self.detectParams.statisticsRegion
        if (not self.floodDate) or (not bounds):
            print "Can't load any images until the date and bounds are set!"
            return
 
        # Unload all the current images, including any flood detection results.
        self._unloadCurrentImages()

        # Check if we are inside the US.
        # - Some higher res data is only available within the US.
        boundsInsideTheUS = miscUtilities.regionIsInUnitedStates(self.detectParams.statisticsRegion)

        # Set up the search range of dates for each image type
        PRIOR_SEARCH_RANGE_DAYS = 20 # Not picky about the pre-flooding image
        POST_SEARCH_RANGE_DAYS  = 20  # After too many days the flood will have receded.
        priorStartDate = self.floodDate.advance(-1*PRIOR_SEARCH_RANGE_DAYS, 'day') # Prior stops before the date
        postStartDate  = self.floodDate # Post starts at the date

        # Load before and after Landsat data
        # - We can afford to be pickier about clouds in the prior image than in the post image.
        try:
            self.landsatPrior = getCloudFreeLandsat(bounds, priorStartDate, PRIOR_SEARCH_RANGE_DAYS, 
                                                    maxCloudPercentage=0.05, searchMethod='decreasing')
            priorLsDate = cmt.util.miscUtilities.getDateFromLandsatInfo(self.landsatPrior.getInfo())
            print 'Found prior Landsat date: ' + priorLsDate
        except Exception as e:
            print 'Failed to find prior Landsat image!'
            print str(e)
            print(sys.exc_info()[0])
            self.landsatPrior = None
        try:
            self.landsatPost  = getCloudFreeLandsat(bounds, postStartDate,  POST_SEARCH_RANGE_DAYS,  
                                                    maxCloudPercentage=0.25, searchMethod='increasing')
            postLsDate = cmt.util.miscUtilities.getDateFromLandsatInfo(self.landsatPost.getInfo())
            print 'Found post Landsat date: ' +postLsDate
        except Exception as e:
            print 'Failed to find post Landsat image!' 
            print str(e)
            print(sys.exc_info()[0])
            self.landsatPost = None


        # Load before and after Sentinel-1 data
        try:
            self.sentinel1Prior = getNearestSentinel1(bounds, priorStartDate, PRIOR_SEARCH_RANGE_DAYS, 
                                                     searchMethod='decreasing')
            priorS1Date = cmt.util.miscUtilities.getDateFromSentinel1Info(self.sentinel1Prior.getInfo())
            print 'Found prior Sentinel-1 date: ' + priorS1Date
        except Exception as e:
            print 'Failed to find prior Sentinel-1 image!'
            print str(e)
            print(sys.exc_info()[0])
            self.sentinel1Prior = None
        try:
            self.sentinel1Post  = getNearestSentinel1(bounds, postStartDate,  POST_SEARCH_RANGE_DAYS,  
                                                     searchMethod='increasing')
            postS1Date = cmt.util.miscUtilities.getDateFromSentinel1Info(self.sentinel1Post.getInfo())
            print 'Found post Sentinel-1 date: ' + postS1Date
        except Exception as e:
            print 'Failed to find post Sentinel-1 image!' 
            print str(e)
            print(sys.exc_info()[0])
            self.sentinel1Post = None
            
        
        # Load before and after MODIS data
        # - We can afford to be pickier about clouds in the prior image than in the post image.
        try:
            self.modisPrior = getCloudFreeModis(bounds, priorStartDate, PRIOR_SEARCH_RANGE_DAYS, 
                                                maxCloudPercentage=0.05, searchMethod='decreasing')
            priorModisDate = cmt.util.miscUtilities.getDateFromModisInfo(self.modisPrior.getInfo())
            print 'Found prior MODIS date: ' + priorModisDate
        except Exception as e:
            print 'Failed to find prior MODIS image!'
            print str(e)
            print(sys.exc_info()[0])
            self.modisPrior = None
        try:
            self.modisPost  = getCloudFreeModis(bounds, postStartDate,  POST_SEARCH_RANGE_DAYS,  
                                                maxCloudPercentage=0.25, searchMethod='increasing')
            postModisDate = cmt.util.miscUtilities.getDateFromModisInfo(self.modisPost.getInfo())
            print 'Found post MODIS date: ' + postModisDate
        except Exception as e:
            print 'Failed to find post MODIS image!'
            print str(e)
            print(sys.exc_info()[0])
            self.modisPost = None

        if self.modisPost:
            # Extract the MODIS cloud mask
            # - We only use the cloud mask from the POST image
            self.modisCloudMask = cmt.modis.modis_utilities.getModisBadPixelMask(self.modisPost)
            self.modisCloudMask = self.modisCloudMask.mask(self.modisCloudMask)

        # Load a DEM
        demName = 'CGIAR/SRTM90_V4' # The default 90m global DEM
        if (boundsInsideTheUS):
            demName = 'ned_13' # The US 10m DEM
        self.demImage = ee.Image(demName)
        
        # Now add all the images to the map!
        self._displayCurrentImages()


    def _loadFloodDetect(self):
        '''Creates the Earth Engine flood detection function and adds it to the map'''
        
        # Check prerequisites
        if (not self.modisPost) or (not self.floodDate) or (not self.detectParams.statisticsRegion):
            print "Can't detect floods without image data and flood date!"
            return
        
        # Remove the last EE function from the map
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)
        
        print 'Starting flood detection with the following parameters:'
        print '--> Water mask threshold       = ' + str(self.detectParams.waterMaskThreshold)
        print '--> Change detection threshold = ' + str(self.detectParams.changeDetectThreshold)
        
        # Generate a new EE function
        self.eeFunction = cmt.modis.misc_algorithms.history_diff_core(self.modisPost,
                                        self.floodDate, self.detectParams.waterMaskThreshold,
                                        self.detectParams.changeDetectThreshold, self.detectParams.statisticsRegion)
        self.eeFunction = self.eeFunction.mask(self.eeFunction)
        
        # Add the new EE function to the map
        # TODO: Set display parameters with widgets
        OPACITY = 0.5
        COLOR   = '00FFFF'
        self.mapWidget.addToMap(self.eeFunction, {'min': 0, 'max': 1, 'opacity': OPACITY, 'palette': COLOR}, 'Flood Detection Results', True)

    def _handleParamChange(self, value, parameterName='DEBUG'):
        '''Reload an EE algorithm when one of its parameters is set in the GUI'''
        if parameterName == 'Change Detection Threshold':
            self.detectParams.changeDetectThreshold = value
            return
        if parameterName == 'Water Mask Threshold':
            self.detectParams.waterMaskThreshold = value
            return
        print 'WARNING: Parameter ' + parameterName + ' is set to: ' + str(value)
        
    def _setDate(self, date):
        '''Sets the current date'''
        self.qtDate = date
        self.floodDate = ee.Date.fromYMD(date.year(), date.month(), date.day()) # Load into an EE object
        self.dateButton.setText(date.toString('yyyy/MM/dd')) # Format for humans to read
        
    def _setRegionToView(self):
        '''Sets the processing region to the current viewable area'''
        # Extract the current viewing bounds as [minLon, minLat, maxLon, maxLat]
        lonLatBounds = self.mapWidget.GetMapBoundingBox() # TODO: This function does not work!!!!
        print 'Setting region to: ' + str(lonLatBounds)
        self.detectParams.statisticsRegion = apply(ee.geometry.Geometry.Rectangle, lonLatBounds)

    def _showCalendar(self):
        '''Pop up a little calendar window so the user can select a date'''
        menu   = QtGui.QMenu(self)
        action = QtGui.QWidgetAction(menu)
        item   = DatePickerWidget(self._setDate, self.qtDate) # Pass in callback function
        action.setDefaultWidget(item)
        menu.addAction(action)
        menu.popup(QtGui.QCursor.pos())

    def _showAboutText(self):
        '''Pop up a little text box to display legal information'''
        QtGui.QMessageBox.about(self, 'about', ABOUT_TEXT)

    def keyPressEvent(self, event):
        """Handle keypress events."""
        if event.key() == QtCore.Qt.Key_Q:
            QtGui.QApplication.quit()

    def __getattr__(self, attr):
        '''Forward any undefined function call to the main map widget'''
        try:
            return getattr(self.mapWidget, attr) # Forward the call to the MapViewWidget class
        except:
            print str(attr)
            raise AttributeError(attr) # This happens if the MapViewWidget class does not support the call
    def __init__(self, parent=None):
        # First set up the flood detection stuff
        self.detectParams = FloodDetectParams()
        self.qtDate = None  # Date of the flood to analyze.
        self.floodDate = None  # Date of the flood to analyze.
        self.modisCloudMask = None  # Cloud mask from 500m MODIS
        self.modisPrior = None  # First cloud free  MODIS image < date.
        self.modisPost = None  # First cloud light MODIS image >= the date.
        self.landsatPrior = None  # First cloud free  Landsat image < date.
        self.landsatPost = None  # First cloud light Landsat image >= the date.
        self.sentinel1Prior = None  # First Sentinel1 image < date.
        self.sentinel1Post = None  # First Sentinel1 image >= date.
        self.demImage = None  # DEM image
        self.permWaterMask = None  # The permanent water mask, never changes.
        self.guestImage = None  # A manually loaded image
        self.eeFunction = None  # Flood detection results TODO: Rename variable!
        self.landsatType = None  # One of the types at the top of the file
        self.classWindow = None  # Handle for class training window

        # Init a tile manager and load it with cache from disk
        self.tileManager = TileManager(DEFAULT_MAP_URL_PATTERN)
        if os.path.exists(LOCAL_MAP_CACHE_PATH):
            self.tileManager.LoadCacheFromDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH

        # Now set up all the GUI stuff
        QtGui.QWidget.__init__(self, parent)
        self.mapWidget = MapViewWidget(self.tileManager)

        # Set up all the components in a vertical layout
        vbox = QtGui.QVBoxLayout()

        # Add a horizontal row of widgets at the top
        topHorizontalBox = QtGui.QHBoxLayout()

        TOP_BUTTON_HEIGHT = 30
        TOP_LARGE_BUTTON_WIDTH = 150
        TOP_SMALL_BUTTON_WIDTH = 100

        # Add a date selector to the top row of widgets
        DEFAULT_START_DATE = ee.Date.fromYMD(2015, 11, 15)
        self.floodDate = DEFAULT_START_DATE
        dateString = '2015/11/15'  # TODO: Generate from the default start date
        self.dateButton = QtGui.QPushButton(dateString, self)
        self.dateButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                       TOP_BUTTON_HEIGHT)
        self.dateButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                       TOP_BUTTON_HEIGHT)
        self.dateButton.clicked[bool].connect(self._showCalendar)
        topHorizontalBox.addWidget(self.dateButton)

        # Add a "Set Region" button to the top row of widgets
        self.regionButton = QtGui.QPushButton('Set Processing Region', self)
        self.regionButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH,
                                         TOP_BUTTON_HEIGHT)
        self.regionButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH,
                                         TOP_BUTTON_HEIGHT)
        self.regionButton.clicked[bool].connect(self._setRegionToView)
        topHorizontalBox.addWidget(self.regionButton)

        # Add a "Load Images" button to the top row of widgets
        self.loadImagesButton = QtGui.QPushButton('Load Images', self)
        self.loadImagesButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadImagesButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadImagesButton.clicked[bool].connect(self._loadImageData)
        topHorizontalBox.addWidget(self.loadImagesButton)

        # Add a "Detect Flood" button to the top row of widgets
        self.loadFloodButton1 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton1.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.clicked[bool].connect(self._loadFloodDetect)
        topHorizontalBox.addWidget(self.loadFloodButton1)

        # Add a "Load ME Image" button to the top row of widgets
        self.loadMeImageButton = QtGui.QPushButton('Load ME Image', self)
        self.loadMeImageButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.clicked[bool].connect(self._loadMapsEngineImage)
        topHorizontalBox.addWidget(self.loadMeImageButton)

        # Add a "Open Class Trainer" button to the top row of widgets
        self.openTrainerButton = QtGui.QPushButton('Open Class Trainer', self)
        self.openTrainerButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.openTrainerButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.openTrainerButton.clicked[bool].connect(self._openClassTrainer)
        topHorizontalBox.addWidget(self.openTrainerButton)

        # Add a "Clear All" button to the top row of widgets
        self.clearButton = QtGui.QPushButton('Clear Map', self)
        self.clearButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                        TOP_BUTTON_HEIGHT)
        self.clearButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                        TOP_BUTTON_HEIGHT)
        self.clearButton.clicked[bool].connect(self._unloadCurrentImages)
        topHorizontalBox.addWidget(self.clearButton)

        # Add the row of widgets on the top of the GUI
        vbox.addLayout(topHorizontalBox)
        # Add the main map widget
        vbox.addWidget(self.mapWidget)

        # Set up a horizontal box below the map
        bottomHorizontalBox = QtGui.QHBoxLayout()
        # On the left side is a vertical box for the parameter controls
        paramControlBoxV = QtGui.QVBoxLayout()

        # First set up some sliders to adjust thresholds
        # - Currently we have two thresholds
        sliderParams = ['Change Detection Threshold', 'Water Mask Threshold']
        paramMin = [-10, -10]
        paramMax = [10, 10]
        defaultVal = [-3, 3]

        # Build each of the parameter sliders
        self.sliderList = []
        for name, minVal, maxVal, default in zip(sliderParams, paramMin,
                                                 paramMax, defaultVal):
            # Stick the horizontal box on the bottom of the main vertical box
            paramControlBoxV = self._addParamSlider(name, maxVal, minVal,
                                                    default, paramControlBoxV)
        bottomHorizontalBox.addLayout(
            paramControlBoxV)  # Add sliders to bottom horizontal box

        # Add a "Detect Flood" button to the right of the parameter controls
        # - This is identical to the button above the map.
        self.loadFloodButton2 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton2.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.clicked[bool].connect(self._loadFloodDetect)
        bottomHorizontalBox.addWidget(self.loadFloodButton2)

        # Add all the stuff at the bottom to the main layout
        vbox.addLayout(bottomHorizontalBox)

        # QMainWindow requires that its layout be set in this manner
        mainWidget = QtGui.QWidget()
        mainWidget.setLayout(vbox)
        self.setCentralWidget(mainWidget)

        # This is the initial window size, but the user can resize it.
        self.setGeometry(100, 100, 720, 720)
        self.setWindowTitle('EE Flood Detector Tool')
        self.show()
class ProductionGui(QtGui.QMainWindow):
    '''This sets up the main viewing window in QT, fills it up with aMapView,
       and then forwards all function calls to it.'''
    
    def __init__(self, parent=None):
        # First set up the flood detection stuff
        self.detectParams = FloodDetectParams()
        self.qtDate         = None # Date of the flood to analyze.
        self.floodDate      = None # Date of the flood to analyze.
        self.lowResModis    = None # 250m MODIS image on the date.
        self.highResModis   = None # 500m MODIS image on the date.
        self.modisCloudMask = None # Cloud mask from 500m MODIS
        self.compositeModis = None # MODIS display image consisting of bands 1, 2, 6
        self.landsatPrior   = None # First Landsat image < date.
        self.landsatPost    = None # First Landsat image >= the date.
        self.demImage       = None # DEM image
        self.permWaterMask  = None # The permanent water mask, never changes.
        self.guestImage     = None # A manually loaded image
        self.eeFunction     = None # Flood detection results TODO: Rename variable!
        self.landsatType    = None # One of the types at the top of the file
        self.classWindow    = None # Handle for class training window
        
        # Now set up all the GUI stuff!
        QtGui.QWidget.__init__(self, parent)
        self.mapWidget  = MapViewWidget()
        
        # Set up all the components in a vertical layout
        vbox = QtGui.QVBoxLayout()
        
        # Add a horizontal row of widgets at the top
        topHorizontalBox = QtGui.QHBoxLayout()
        
        TOP_BUTTON_HEIGHT = 30
        TOP_LARGE_BUTTON_WIDTH  = 150
        TOP_SMALL_BUTTON_WIDTH  = 100
        
        # Add a date selector to the top row of widgets
        DEFAULT_START_DATE = ee.Date.fromYMD(2010, 8, 25)
        self.floodDate = DEFAULT_START_DATE
        dateString     = '2006/7/18' # TODO: Generate from the default start date
        self.dateButton = QtGui.QPushButton(dateString, self)
        self.dateButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.dateButton.clicked[bool].connect(self._showCalendar)
        topHorizontalBox.addWidget(self.dateButton)
        
        # Add a "Set Region" button to the top row of widgets
        self.regionButton = QtGui.QPushButton('Set Processing Region', self)
        self.regionButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT) 
        self.regionButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.regionButton.clicked[bool].connect(self._setRegionToView)
        topHorizontalBox.addWidget(self.regionButton)

        # Add a "Load Images" button to the top row of widgets
        self.loadImagesButton = QtGui.QPushButton('Load Images', self)
        self.loadImagesButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadImagesButton.clicked[bool].connect(self._loadImageData)
        topHorizontalBox.addWidget(self.loadImagesButton)

        # Add a "Detect Flood" button to the top row of widgets
        self.loadFloodButton1 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton1.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.clicked[bool].connect(self._loadFloodDetect)
        topHorizontalBox.addWidget(self.loadFloodButton1)

        # Add a "Load ME Image" button to the top row of widgets
        self.loadMeImageButton = QtGui.QPushButton('Load ME Image', self)
        self.loadMeImageButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.clicked[bool].connect(self._loadMapsEngineImage)
        topHorizontalBox.addWidget(self.loadMeImageButton)

        # Add a "Open Class Trainer" button to the top row of widgets
        self.openTrainerButton = QtGui.QPushButton('Open Class Trainer', self)
        self.openTrainerButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.openTrainerButton.clicked[bool].connect(self._openClassTrainer)
        topHorizontalBox.addWidget(self.openTrainerButton)

        # Add a "Clear All" button to the top row of widgets
        self.clearButton = QtGui.QPushButton('Clear Map', self)
        self.clearButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.clearButton.clicked[bool].connect(self._unloadCurrentImages)
        topHorizontalBox.addWidget(self.clearButton)

        # Add the row of widgets on the top of the GUI
        vbox.addLayout(topHorizontalBox)
        # Add the main map widget
        vbox.addWidget(self.mapWidget)
                
        # Set up a horizontal box below the map
        bottomHorizontalBox = QtGui.QHBoxLayout()
        # On the left side is a vertical box for the parameter controls
        paramControlBoxV    = QtGui.QVBoxLayout()
        
        # First set up some sliders to adjust thresholds
        # - Currently we have two thresholds
        sliderParams = ['Change Detection Threshold', 'Water Mask Threshold']
        paramMin     = [-10, -10]
        paramMax     = [ 10,  10]
        defaultVal   = [-3, 3]
        
        # Build each of the parameter sliders
        self.sliderList = []
        for name, minVal, maxVal, default in zip(sliderParams, paramMin, paramMax, defaultVal):
            # Stick the horizontal box on the bottom of the main vertical box
            paramControlBoxV = self._addParamSlider(name, maxVal, minVal, default, paramControlBoxV)
        bottomHorizontalBox.addLayout(paramControlBoxV) # Add sliders to bottom horizontal box

        # Add a "Detect Flood" button to the right of the parameter controls
        # - This is identical to the button above the map.
        self.loadFloodButton2 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton2.setMinimumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.setMaximumSize(TOP_SMALL_BUTTON_WIDTH, TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.clicked[bool].connect(self._loadFloodDetect)
        bottomHorizontalBox.addWidget(self.loadFloodButton2)

        # Add all the stuff at the bottom to the main layout
        vbox.addLayout(bottomHorizontalBox)

        # QMainWindow requires that its layout be set in this manner
        mainWidget = QtGui.QWidget()
        mainWidget.setLayout(vbox)
        self.setCentralWidget(mainWidget)
        
        # This is the initial window size, but the user can resize it.
        self.setGeometry(100, 100, 720, 720) 
        self.setWindowTitle('EE Flood Detector Tool')
        self.show()

    def closeEvent(self, event):
        '''Cleanup all other windows'''
        if self.classWindow:
            self.classWindow.close()

    def _addParamSlider(self, name, maxVal, minVal, defaultVal, container):
        '''Adds a single parameter slider to the passed in container.'''
        # All parameter sliders are handled by the _handleParamChange function
    
        NAME_WIDTH    = 250
        SLIDER_HEIGHT = 20
        SLIDER_WIDTH  = 400
        NUM_TICKS     = 4
        
        # Set up this value slider
        slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
        slider.setRange(minVal, maxVal) 
        slider.setValue(defaultVal)
        valRange = maxVal - minVal
        slider.setTickInterval(valRange/NUM_TICKS) # Add five tick marks
        slider.setMinimumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        slider.setMaximumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        # Use 'partial' to send the param name to the callback function
        callbackFunction = functools.partial(self._handleParamChange, parameterName=name) 
        slider.valueChanged.connect(callbackFunction) # Whenever the slider is moved, trigger callback function
        self.sliderList.append(slider) # TODO: Do we need this?
    
        # Make box with the name
        nameBox = QtGui.QLabel(name, self)
        nameBox.setMinimumSize(NAME_WIDTH, SLIDER_HEIGHT)
        nameBox.setMaximumSize(NAME_WIDTH, SLIDER_HEIGHT)
        
        # Put the name to the left of the slider
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(nameBox)
        hbox.addWidget(slider)
        
        # Stick the horizontal box on the bottom of the main vertical box
        container.addLayout(hbox)
        return container
    
    
    def _openClassTrainer(self):
        '''Open a new window to define classifier training regions'''
        # Create the class trainer window and connect it to the map widget
        
        if not self.classWindow:
            self.classWindow = FeatureTrainerWindow(self.mapWidget)
            self.classWindow.show()
        else:
            self.classWindow.show()
        
        

    def _unloadCurrentImages(self):
        '''Just unload all the current images. Low level function'''
        if self.compositeModis: # Note: Individual MODIS images are not added to the map
            self.mapWidget.removeFromMap(self.compositeModis)
            self.compositeModis = None
        if self.modisCloudMask:
            self.mapWidget.removeFromMap(self.modisCloudMask)
            self.modisCloudMask = None
        if self.landsatPrior:
            self.mapWidget.removeFromMap(self.landsatPrior)
            self.landsatPrior = None
        if self.landsatPost:
            self.mapWidget.removeFromMap(self.landsatPost)
            self.landsatPost = None
        if self.demImage:
            self.mapWidget.removeFromMap(self.demImage)
            self.demImage = None
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)
            self.eeFunction = None

    class GuestImageDialog(QtGui.QDialog):
        '''Popup window for the user to fill in information about an image loaded in Maps Engine'''
        def __init__(self, parent = None):
            super(ProductionGui.GuestImageDialog, self).__init__(parent)
            
            # Get the list of available sensors
            self.radioNames = miscUtilities.getDefinedSensorNames()
            if not self.radioNames:
                raise Exception('Could not load any sensors!')
            # Set up sensor selection buttons
            self.radioButtons = []
            for name in self.radioNames:
                self.radioButtons.append(QtGui.QRadioButton(name))
            self.radioButtons[0].setChecked(True)
            
            # Set up the other controls
            self.lineIn       = QtGui.QLineEdit("GME/images/00979750194450688595-02912990955588156238")#QtGui.QLineEdit("Earth Engine ID")
            self.okButton     = QtGui.QPushButton('Ok',     self)
            self.cancelButton = QtGui.QPushButton('Cancel', self)            
            self.okButton.clicked.connect(self.accept)
            self.cancelButton.clicked.connect(self.reject)
            
            # Lay everything out
            vbox = QtGui.QVBoxLayout(self)
            vbox.addWidget(self.lineIn)
            for button in self.radioButtons:
               vbox.addWidget(button)
            vbox.addWidget(self.okButton)
            vbox.addWidget(self.cancelButton)
            vbox.addStretch(1)
        
        def getValues(self):
            '''Return the selected values'''
            for button in self.radioButtons: # Should always be exactly one button checked!
                if button.isChecked():
                    return (str(button.text()), str(self.lineIn.text()))
            raise Exception('GUI buttons are broken!')
        
    def _loadMapsEngineImage(self):
        '''Loads in an image stored in Google Maps Engine'''
    
        # Pop up a dialog and get the input values back
        dialog = self.GuestImageDialog(self)
        result = dialog.exec_()
        sensorName, eeID = dialog.getValues()
        
        if not result: # Do nothing if the cancel button was pressed
            return
        
        # Extract the asset ID from the Earth Engine ID
        # - The Earth Engine ID looks like this: GME/images/18108519531116889794-15007110928626476628
        pt = eeID.rfind('/')
        if not pt:
            print 'Invalid Earth Engine ID entered!'
        assetID = eeID[pt+1:]
        
        # Clear any existing guest image from the map
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None

        # Retrieve the sensor information for this image from the XML file
        sensorXmlPath = os.path.join(SENSOR_FILE_DIR, sensorName + ".xml")
        print 'Reading file: ' + sensorXmlPath
        sensorInfo = cmt.domain.SensorObservation(xml_source=sensorXmlPath, manual_ee_ID=assetID)
        
        # Add the new guest image to the map
        self.guestImage, vis_params, im_name, show = sensorInfo.visualize()
        self.mapWidget.addToMap(self.guestImage, vis_params, sensorName, True)
        
        



    def _displayCurrentImages(self):
        '''Add all the current images to the map. Low level function'''
        # TODO: Come up with a method for setting the intensity bounds!
        if self.landsatType == LANDSAT_5:
            landsatVisParams = {'bands': ['B3', 'B2', 'B1'], 'min': 0, 'max': 0.7}
        elif self.landsatType == LANDSAT_7:
            landsatVisParams = {'bands': ['B3', 'B2', 'B1'], 'min': 0, 'max': 0.7}
        else: # LANDSAT_8
            landsatVisParams = {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 0.8}
        MODIS_RANGE  = [0, 3000]
        DEM_RANGE    = [0, 1000]
        if self.landsatPrior:
            self.mapWidget.addToMap(self.landsatPrior, landsatVisParams, 'LANDSAT Pre-Flood', False)
        else:
            print 'Failed to find prior LANDSAT image!'
        if self.landsatPost:
            self.mapWidget.addToMap(self.landsatPost, landsatVisParams, 'LANDSAT Post-Flood', True)
        else:
            print 'Failed to find post LANDSAT image!'
        if self.compositeModis:
            vis_params = {'bands': ['sur_refl_b01', 'sur_refl_b02', 'sur_refl_b06'],
                          'min': MODIS_RANGE[0], 'max': MODIS_RANGE[1]}
            self.mapWidget.addToMap(self.compositeModis, vis_params, 'MODIS Channels 1/2/6', False)
        else:
            print 'Failed to find MODIS image!'
            
        # This one works a little differently since it never changes
        # - We just add this once and never remove it.
        if not self.permWaterMask:
            self.permWaterMask = cmt.modis.modis_utilities.get_permanent_water_mask()
            self.mapWidget.addToMap(self.permWaterMask.mask(self.permWaterMask),
                                    {'min': 0 , 'max': 1, 'palette': '000000, 0000FF'}, 'Permanent Water Mask', False)
            
        if self.modisCloudMask:
            vis_params = {'min': 0, 'max': 1, 'palette': '000000, FF0000'}
            self.mapWidget.addToMap(self.modisCloudMask, vis_params, '1km Bad MODIS pixels', False)
        else:
            print 'Failed to find MODIS Cloud Mask image!'

        if self.demImage:
            vis_params = {'min': DEM_RANGE[0], 'max': DEM_RANGE[1]}
            self.mapWidget.addToMap(self.demImage, vis_params, 'Digital Elevation Map', False)
        else:
            print 'Failed to find DEM!'


    def _selectLandsatBands(self, eeLandsatFunc):
        '''Given a raw landsat image, pick which bands to view'''
        if not eeLandsatFunc:
            return None
        
        # Select the bands to view
        if self.landsatType == LANDSAT_5:
            bandNamesIn  = ['10', '20', '30', '40', '50', '60', '70']
            bandNamesOut = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7']
        elif self.landsatType == LANDSAT_7:
            bandNamesIn  = ['10', '20', '30', '40', '50', '60', '70']
            bandNamesOut = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7']
        else: # LANDSAT_8
            bandNamesIn  = ['10', '20', '30', '40', '50', '60', '70', '80']
            bandNamesOut = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8']    
            
        return eeLandsatFunc


    def _pickLandsatImage(self, eeImageCollection, bounds, chooseLast=False):
        '''Picks the best image from an ImageCollection of Landsat images'''
        # We require the LANDSAT image to contain the center of the analysis region,
        #  otherwise we tend to get images with minimal overlap.
        geoLimited = eeImageCollection.filterBounds(bounds.centroid())
        
        dateList = []
        index    = 0
        for f in geoLimited.getInfo()['features']:
            imageDate = None
            # Landsat images do not have consistent header information so try multiple names here.
            if 'DATE_ACQUIRED' in f['properties']:
               imageDate = f['properties']['DATE_ACQUIRED']
            elif 'ACQUISITION_DATE' in f['properties']:
               imageDate = f['properties']['ACQUISITION_DATE']
               
            if not imageDate: # Failed to extract the date!
                print '===================================\n\n'
                print geoLimited.getInfo()['features']
            else: # Found the date
                dateList.append((imageDate, index))
            index += 1
        if not dateList: # Could not read any dates, just pick the first image.
            return (geoLimited.limit(1).mean(), 'NA')

        # Now select the first or last image, sorting on date but also retrieving the index.
        if chooseLast: # Latest date
            bestDate = max(dateList)
        else: # First date
            bestDate = min(dateList)

        return (ee.Image(geoLimited.toList(255).get(bestDate[1])), bestDate)

    def _pickLandsatSat(self, date):
        '''Pick the best landsat satellite to use for the given date'''
        # Try to avoid LANDSAT_7 since it produces ugly images
        
        month = date.get('month').getInfo()
        year  = date.get('year').getInfo()
        if (year > 2013) or ((year == 2013) and (month > 5)):
            return LANDSAT_8
        else:
            return LANDSAT_5


    def _loadImageData(self):
        '''Updates the MODIS and LANDSAT images for the current date'''
        
        # Check that we have all the information we need
        bounds = self.detectParams.statisticsRegion
        if (not self.floodDate) or (not bounds):
            print "Can't load any images until the date and bounds are set!"
            return
 
        # Unload all the current images, including any flood detection results.
        self._unloadCurrentImages()

        # Check if we are inside the US.
        # - Some higher res data is only available within the US.
        boundsInsideTheUS = miscUtilities.regionIsInUnitedStates(self.detectParams.statisticsRegion)

        # Set up the search range of dates for each image type
        MODIS_SEARCH_RANGE_DAYS   = 1  # MODIS updates frequently so we can have a narrow range
        LANDSAT_SEARCH_RANGE_DAYS = 30 # LANDSAT does not update often so we need a large search range
        modisStartDate        = self.floodDate # Modis date range starts from the date
        modisEndDate          = self.floodDate.advance(   MODIS_SEARCH_RANGE_DAYS,   'day')
        landsatPriorStartDate = self.floodDate.advance(-1*LANDSAT_SEARCH_RANGE_DAYS, 'day') # Prior landsat stops before the date
        landsatPriorEndDate   = self.floodDate.advance(-1,                           'day')
        landsatPostStartDate  = self.floodDate # Post landsat starts at the date
        landsatPostEndDate    = self.floodDate.advance(   LANDSAT_SEARCH_RANGE_DAYS, 'day')
        
        # Load the two LANDSAT images
        self.landsatType = self._pickLandsatSat(self.floodDate)
        if self.landsatType == LANDSAT_8:
            print 'Using Landsat 8'
            landsatCode = 'LANDSAT/LC8_L1T_TOA'
        elif self.landsatType == LANDSAT_7:
            print 'Using Landsat 7'
            landsatCode = 'LANDSAT/LE7_L1T_TOA'
        else:
            print 'Using Landsat 5'
            landsatCode = 'LANDSAT/LT5_L1T_TOA'
        priorLandsatCollection = ee.ImageCollection(landsatCode).filterDate(landsatPriorStartDate, landsatPriorEndDate)
        postLandsatCollection  = ee.ImageCollection(landsatCode).filterDate(landsatPostStartDate,  landsatPostEndDate)

        self.landsatPrior, priorLsDate = self._pickLandsatImage(priorLandsatCollection, bounds, chooseLast=True)
        self.landsatPost,  postLsDate  = self._pickLandsatImage(postLandsatCollection,  bounds)
        
        if priorLsDate:
            print 'Selected prior landsat date: ' + str(priorLsDate)
        if postLsDate:
            print 'Selected post  landsat date: ' + str(postLsDate)
        
        # Select the bands to view
        self.landsatPrior = self._selectLandsatBands(self.landsatPrior)
        self.landsatPost  = self._selectLandsatBands(self.landsatPost)
        
        # Load the two MODIS images and create a composite
        self.highResModis   = ee.ImageCollection('MOD09GQ').filterBounds(bounds).filterDate(modisStartDate, modisEndDate).limit(1).mean();
        self.lowResModis    = ee.ImageCollection('MOD09GA').filterBounds(bounds).filterDate(modisStartDate, modisEndDate).limit(1).mean();
        self.compositeModis = self.highResModis.addBands(self.lowResModis.select('sur_refl_b06'))

        # Extract the MODIS cloud mask
        self.modisCloudMask = cmt.modis.modis_utilities.getModisBadPixelMask(self.lowResModis)
        self.modisCloudMask = self.modisCloudMask.mask(self.modisCloudMask)

        # Load a DEM
        demName = 'CGIAR/SRTM90_V4' # The default 90m global DEM
        if (boundsInsideTheUS):
            demName = 'ned_13' # The US 10m DEM
        self.demImage = ee.Image(demName)
        
        # Now add all the images to the map!
        self._displayCurrentImages()


    def _loadFloodDetect(self):
        '''Creates the Earth Engine flood detection function and adds it to the map'''
        
        # Check prerequisites
        if (not self.highResModis) or (not self.floodDate) or (not self.detectParams.statisticsRegion):
            print "Can't detect floods without image data and flood date!"
            return
        
        # Remove the last EE function from the map
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)
        
        print 'Starting flood detection with the following parameters:'
        print '--> Water mask threshold       = ' + str(self.detectParams.waterMaskThreshold)
        print '--> Change detection threshold = ' + str(self.detectParams.changeDetectThreshold)
        
        # Generate a new EE function
        self.eeFunction = cmt.modis.misc_algorithms.history_diff_core(self.highResModis,
                                        self.floodDate, self.detectParams.waterMaskThreshold,
                                        self.detectParams.changeDetectThreshold, self.detectParams.statisticsRegion)
        self.eeFunction = self.eeFunction.mask(self.eeFunction)
        
        # Add the new EE function to the map
        # TODO: Set display parameters with widgets
        OPACITY = 0.5
        COLOR   = '00FFFF'
        self.mapWidget.addToMap(self.eeFunction, {'min': 0, 'max': 1, 'opacity': OPACITY, 'palette': COLOR}, 'Flood Detection Results', True)

    def _handleParamChange(self, value, parameterName='DEBUG'):
        '''Reload an EE algorithm when one of its parameters is set in the GUI'''
        if parameterName == 'Change Detection Threshold':
            self.detectParams.changeDetectThreshold = value
            return
        if parameterName == 'Water Mask Threshold':
            self.detectParams.waterMaskThreshold = value
            return
        print 'WARNING: Parameter ' + parameterName + ' is set to: ' + str(value)
        
    def _setDate(self, date):
        '''Sets the current date'''
        self.qtDate = date
        self.floodDate = ee.Date.fromYMD(date.year(), date.month(), date.day()) # Load into an EE object
        self.dateButton.setText(date.toString('yyyy/MM/dd')) # Format for humans to read
        
    def _setRegionToView(self):
        '''Sets the processing region to the current viewable area'''
        # Extract the current viewing bounds as [minLon, minLat, maxLon, maxLat]
        lonLatBounds = self.mapWidget.GetMapBoundingBox() # TODO: This function does not work!!!!
        print 'Setting region to: ' + str(lonLatBounds)
        self.detectParams.statisticsRegion = apply(ee.geometry.Geometry.Rectangle, lonLatBounds)

    def _showCalendar(self):
        '''Pop up a little calendar window so the user can select a date'''
        menu   = QtGui.QMenu(self)
        action = QtGui.QWidgetAction(menu)
        item   = DatePickerWidget(self._setDate, self.qtDate) # Pass in callback function
        action.setDefaultWidget(item)
        menu.addAction(action)
        menu.popup(QtGui.QCursor.pos())

    def _showAboutText(self):
        '''Pop up a little text box to display legal information'''
        QtGui.QMessageBox.about(self, 'about', ABOUT_TEXT)

    def keyPressEvent(self, event):
        """Handle keypress events."""
        if event.key() == QtCore.Qt.Key_Q:
            QtGui.QApplication.quit()

    def __getattr__(self, attr):
        '''Forward any undefined function call to the main map widget'''
        try:
            return getattr(self.mapWidget, attr) # Forward the call to the MapViewWidget class
        except:
            print str(attr)
            raise AttributeError(attr) # This happens if the MapViewWidget class does not support the call
class ProductionGui(QtGui.QMainWindow):
    '''This sets up the main viewing window in QT, fills it up with a MapView,
       and then forwards all function calls to it.'''
    def __init__(self, parent=None):
        # First set up the flood detection stuff
        self.detectParams = FloodDetectParams()
        self.qtDate = None  # Date of the flood to analyze.
        self.floodDate = None  # Date of the flood to analyze.
        self.modisCloudMask = None  # Cloud mask from 500m MODIS
        self.modisPrior = None  # First cloud free  MODIS image < date.
        self.modisPost = None  # First cloud light MODIS image >= the date.
        self.landsatPrior = None  # First cloud free  Landsat image < date.
        self.landsatPost = None  # First cloud light Landsat image >= the date.
        self.sentinel1Prior = None  # First Sentinel1 image < date.
        self.sentinel1Post = None  # First Sentinel1 image >= date.
        self.demImage = None  # DEM image
        self.permWaterMask = None  # The permanent water mask, never changes.
        self.guestImage = None  # A manually loaded image
        self.eeFunction = None  # Flood detection results TODO: Rename variable!
        self.landsatType = None  # One of the types at the top of the file
        self.classWindow = None  # Handle for class training window

        # Init a tile manager and load it with cache from disk
        self.tileManager = TileManager(DEFAULT_MAP_URL_PATTERN)
        if os.path.exists(LOCAL_MAP_CACHE_PATH):
            self.tileManager.LoadCacheFromDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH

        # Now set up all the GUI stuff
        QtGui.QWidget.__init__(self, parent)
        self.mapWidget = MapViewWidget(self.tileManager)

        # Set up all the components in a vertical layout
        vbox = QtGui.QVBoxLayout()

        # Add a horizontal row of widgets at the top
        topHorizontalBox = QtGui.QHBoxLayout()

        TOP_BUTTON_HEIGHT = 30
        TOP_LARGE_BUTTON_WIDTH = 150
        TOP_SMALL_BUTTON_WIDTH = 100

        # Add a date selector to the top row of widgets
        DEFAULT_START_DATE = ee.Date.fromYMD(2015, 11, 15)
        self.floodDate = DEFAULT_START_DATE
        dateString = '2015/11/15'  # TODO: Generate from the default start date
        self.dateButton = QtGui.QPushButton(dateString, self)
        self.dateButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                       TOP_BUTTON_HEIGHT)
        self.dateButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                       TOP_BUTTON_HEIGHT)
        self.dateButton.clicked[bool].connect(self._showCalendar)
        topHorizontalBox.addWidget(self.dateButton)

        # Add a "Set Region" button to the top row of widgets
        self.regionButton = QtGui.QPushButton('Set Processing Region', self)
        self.regionButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH,
                                         TOP_BUTTON_HEIGHT)
        self.regionButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH,
                                         TOP_BUTTON_HEIGHT)
        self.regionButton.clicked[bool].connect(self._setRegionToView)
        topHorizontalBox.addWidget(self.regionButton)

        # Add a "Load Images" button to the top row of widgets
        self.loadImagesButton = QtGui.QPushButton('Load Images', self)
        self.loadImagesButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadImagesButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadImagesButton.clicked[bool].connect(self._loadImageData)
        topHorizontalBox.addWidget(self.loadImagesButton)

        # Add a "Detect Flood" button to the top row of widgets
        self.loadFloodButton1 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton1.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton1.clicked[bool].connect(self._loadFloodDetect)
        topHorizontalBox.addWidget(self.loadFloodButton1)

        # Add a "Load ME Image" button to the top row of widgets
        self.loadMeImageButton = QtGui.QPushButton('Load ME Image', self)
        self.loadMeImageButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.loadMeImageButton.clicked[bool].connect(self._loadMapsEngineImage)
        topHorizontalBox.addWidget(self.loadMeImageButton)

        # Add a "Open Class Trainer" button to the top row of widgets
        self.openTrainerButton = QtGui.QPushButton('Open Class Trainer', self)
        self.openTrainerButton.setMinimumSize(TOP_LARGE_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.openTrainerButton.setMaximumSize(TOP_LARGE_BUTTON_WIDTH,
                                              TOP_BUTTON_HEIGHT)
        self.openTrainerButton.clicked[bool].connect(self._openClassTrainer)
        topHorizontalBox.addWidget(self.openTrainerButton)

        # Add a "Clear All" button to the top row of widgets
        self.clearButton = QtGui.QPushButton('Clear Map', self)
        self.clearButton.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                        TOP_BUTTON_HEIGHT)
        self.clearButton.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                        TOP_BUTTON_HEIGHT)
        self.clearButton.clicked[bool].connect(self._unloadCurrentImages)
        topHorizontalBox.addWidget(self.clearButton)

        # Add the row of widgets on the top of the GUI
        vbox.addLayout(topHorizontalBox)
        # Add the main map widget
        vbox.addWidget(self.mapWidget)

        # Set up a horizontal box below the map
        bottomHorizontalBox = QtGui.QHBoxLayout()
        # On the left side is a vertical box for the parameter controls
        paramControlBoxV = QtGui.QVBoxLayout()

        # First set up some sliders to adjust thresholds
        # - Currently we have two thresholds
        sliderParams = ['Change Detection Threshold', 'Water Mask Threshold']
        paramMin = [-10, -10]
        paramMax = [10, 10]
        defaultVal = [-3, 3]

        # Build each of the parameter sliders
        self.sliderList = []
        for name, minVal, maxVal, default in zip(sliderParams, paramMin,
                                                 paramMax, defaultVal):
            # Stick the horizontal box on the bottom of the main vertical box
            paramControlBoxV = self._addParamSlider(name, maxVal, minVal,
                                                    default, paramControlBoxV)
        bottomHorizontalBox.addLayout(
            paramControlBoxV)  # Add sliders to bottom horizontal box

        # Add a "Detect Flood" button to the right of the parameter controls
        # - This is identical to the button above the map.
        self.loadFloodButton2 = QtGui.QPushButton('Detect Flood', self)
        self.loadFloodButton2.setMinimumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.setMaximumSize(TOP_SMALL_BUTTON_WIDTH,
                                             TOP_BUTTON_HEIGHT)
        self.loadFloodButton2.clicked[bool].connect(self._loadFloodDetect)
        bottomHorizontalBox.addWidget(self.loadFloodButton2)

        # Add all the stuff at the bottom to the main layout
        vbox.addLayout(bottomHorizontalBox)

        # QMainWindow requires that its layout be set in this manner
        mainWidget = QtGui.QWidget()
        mainWidget.setLayout(vbox)
        self.setCentralWidget(mainWidget)

        # This is the initial window size, but the user can resize it.
        self.setGeometry(100, 100, 720, 720)
        self.setWindowTitle('EE Flood Detector Tool')
        self.show()

    def closeEvent(self, event):
        '''Cleanup all other windows and dump cache to disk'''
        if self.classWindow:
            self.classWindow.close()

        #try:
        print 'Attempting to save tile cache...'
        self.tileManager.SaveCacheToDisk(LOCAL_MAP_CACHE_PATH)
        #except:
        #    print 'Unable to load cache information from ' + LOCAL_MAP_CACHE_PATH

    def _addParamSlider(self, name, maxVal, minVal, defaultVal, container):
        '''Adds a single parameter slider to the passed in container.'''
        # All parameter sliders are handled by the _handleParamChange function

        NAME_WIDTH = 250
        SLIDER_HEIGHT = 20
        SLIDER_WIDTH = 400
        NUM_TICKS = 4

        # Set up this value slider
        slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
        slider.setRange(minVal, maxVal)
        slider.setValue(defaultVal)
        valRange = maxVal - minVal
        slider.setTickInterval(valRange / NUM_TICKS)  # Add five tick marks
        slider.setMinimumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        slider.setMaximumSize(SLIDER_WIDTH, SLIDER_HEIGHT)
        # Use 'partial' to send the param name to the callback function
        callbackFunction = functools.partial(self._handleParamChange,
                                             parameterName=name)
        slider.valueChanged.connect(
            callbackFunction
        )  # Whenever the slider is moved, trigger callback function
        self.sliderList.append(slider)  # TODO: Do we need this?

        # Make box with the name
        nameBox = QtGui.QLabel(name, self)
        nameBox.setMinimumSize(NAME_WIDTH, SLIDER_HEIGHT)
        nameBox.setMaximumSize(NAME_WIDTH, SLIDER_HEIGHT)

        # Put the name to the left of the slider
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(nameBox)
        hbox.addWidget(slider)

        # Stick the horizontal box on the bottom of the main vertical box
        container.addLayout(hbox)
        return container

    def _openClassTrainer(self):
        '''Open a new window to define classifier training regions'''
        # Create the class trainer window and connect it to the map widget

        if not self.classWindow:
            self.classWindow = FeatureTrainerWindow(self.mapWidget)
            self.classWindow.show()
        else:
            self.classWindow.show()

    def _unloadCurrentImages(self):
        '''Just unload all the current images. Low level function'''
        if self.modisPrior:
            self.mapWidget.removeFromMap(self.modisPrior)
            self.modisPrior = None
        if self.modisPost:
            self.mapWidget.removeFromMap(self.modisPost)
            self.modisPost = None
        if self.modisCloudMask:
            self.mapWidget.removeFromMap(self.modisCloudMask)
            self.modisCloudMask = None
        if self.landsatPrior:
            self.mapWidget.removeFromMap(self.landsatPrior)
            self.landsatPrior = None
        if self.landsatPost:
            self.mapWidget.removeFromMap(self.landsatPost)
            self.landsatPost = None
        if self.sentinel1Prior:
            self.mapWidget.removeFromMap(self.sentinel1Prior)
            self.sentinel1Prior = None
        if self.sentinel1Post:
            self.mapWidget.removeFromMap(self.sentinel1Post)
            self.sentinel1Post = None
        if self.demImage:
            self.mapWidget.removeFromMap(self.demImage)
            self.demImage = None
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)
            self.eeFunction = None

    class GuestImageDialog(QtGui.QDialog):
        '''Popup window for the user to fill in information about an image loaded in Maps Engine'''
        def __init__(self, parent=None):
            super(ProductionGui.GuestImageDialog, self).__init__(parent)

            # Get the list of available sensors
            self.radioNames = miscUtilities.getDefinedSensorNames()
            if not self.radioNames:
                raise Exception('Could not load any sensors!')
            # Set up sensor selection buttons
            self.radioButtons = []
            for name in self.radioNames:
                self.radioButtons.append(QtGui.QRadioButton(name))
            self.radioButtons[0].setChecked(True)

            # Set up the other controls
            self.lineIn = QtGui.QLineEdit("Asset ID")
            self.okButton = QtGui.QPushButton('Ok', self)
            self.cancelButton = QtGui.QPushButton('Cancel', self)
            self.okButton.clicked.connect(self.accept)
            self.cancelButton.clicked.connect(self.reject)

            # Lay everything out
            vbox = QtGui.QVBoxLayout(self)
            vbox.addWidget(self.lineIn)
            for button in self.radioButtons:
                vbox.addWidget(button)
            vbox.addWidget(self.okButton)
            vbox.addWidget(self.cancelButton)
            vbox.addStretch(1)

        def getValues(self):
            '''Return the selected values'''
            for button in self.radioButtons:  # Should always be exactly one button checked!
                if button.isChecked():
                    return (str(button.text()), str(self.lineIn.text()))
            raise Exception('GUI buttons are broken!')

    def _loadMapsEngineImage(self):
        '''Loads in an image stored in Google Maps Engine'''

        raise Exception('TODO: Update for the death of Maps Engine!')

        # Pop up a dialog and get the input values back
        dialog = self.GuestImageDialog(self)
        result = dialog.exec_()
        sensorName, eeID = dialog.getValues()

        if not result:  # Do nothing if the cancel button was pressed
            return

        # Extract the asset ID from the Earth Engine ID
        # - The Earth Engine ID looks like this: GME/images/18108519531116889794-15007110928626476628
        pt = eeID.rfind('/')
        if not pt:
            print 'Invalid Earth Engine ID entered!'
        assetID = eeID[pt + 1:]

        # Clear any existing guest image from the map
        if self.guestImage:
            self.mapWidget.removeFromMap(self.guestImage)
            self.guestImage = None

        # Retrieve the sensor information for this image from the XML file
        sensorXmlPath = os.path.join(SENSOR_FILE_DIR, sensorName + ".xml")
        print 'Reading file: ' + sensorXmlPath
        sensorInfo = cmt.domain.SensorObservation(xml_source=sensorXmlPath,
                                                  manual_ee_ID=assetID)

        # Add the new guest image to the map
        self.guestImage, vis_params, im_name, show = sensorInfo.visualize()
        self.mapWidget.addToMap(self.guestImage, vis_params, sensorName, True)

    def _displayCurrentImages(self):
        '''Add all the current images to the map. Low level function'''

        MODIS_RANGE = [0, 3000]
        DEM_RANGE = [0, 1000]

        # TODO: Come up with a method for setting the intensity bounds!
        landsatVisParams = {
            'bands': ['red', 'green', 'blue'],
            'min': 0,
            'max': 0.75
        }

        sentinel1VisParams = {'bands': ['vv'], 'min': -30, 'max': 5}

        modisVisParams = {
            'bands': ['sur_refl_b01', 'sur_refl_b02', 'sur_refl_b06'],
            'min': MODIS_RANGE[0],
            'max': MODIS_RANGE[1]
        }

        if self.landsatPrior:
            self.mapWidget.addToMap(self.landsatPrior, landsatVisParams,
                                    'LANDSAT Pre-Flood', False)
        if self.landsatPost:
            self.mapWidget.addToMap(self.landsatPost, landsatVisParams,
                                    'LANDSAT Post-Flood', True)

        if self.sentinel1Prior:
            self.mapWidget.addToMap(self.sentinel1Prior, sentinel1VisParams,
                                    'Sentinel-1 Pre-Flood', False)
        if self.sentinel1Post:
            self.mapWidget.addToMap(self.sentinel1Post, sentinel1VisParams,
                                    'Sentinel-1 Post-Flood', False)

        if self.modisPrior:
            self.mapWidget.addToMap(self.modisPrior, modisVisParams,
                                    'MODIS Pre-Flood', False)
        if self.modisPost:
            self.mapWidget.addToMap(self.modisPost, modisVisParams,
                                    'MODIS Post-Flood', False)

        # This one works a little differently since it never changes
        # - We just add this once and never remove it.
        if not self.permWaterMask:
            self.permWaterMask = cmt.modis.modis_utilities.get_permanent_water_mask(
            )
            self.mapWidget.addToMap(
                self.permWaterMask.mask(self.permWaterMask), {
                    'min': 0,
                    'max': 1,
                    'palette': '000000, 0000FF'
                }, 'Permanent Water Mask', False)

        if self.modisCloudMask:
            vis_params = {'min': 0, 'max': 1, 'palette': '000000, FF0000'}
            self.mapWidget.addToMap(self.modisCloudMask, vis_params,
                                    '1km Bad MODIS pixels', False)

        if self.demImage:
            vis_params = {'min': DEM_RANGE[0], 'max': DEM_RANGE[1]}
            self.mapWidget.addToMap(self.demImage, vis_params,
                                    'Digital Elevation Map', False)

    def _loadImageData(self):
        '''Updates the MODIS and LANDSAT images for the current date'''

        # Check that we have all the information we need
        bounds = self.detectParams.statisticsRegion
        if (not self.floodDate) or (not bounds):
            print "Can't load any images until the date and bounds are set!"
            return

        # Unload all the current images, including any flood detection results.
        self._unloadCurrentImages()

        # Check if we are inside the US.
        # - Some higher res data is only available within the US.
        boundsInsideTheUS = miscUtilities.regionIsInUnitedStates(
            self.detectParams.statisticsRegion)

        # Set up the search range of dates for each image type
        PRIOR_SEARCH_RANGE_DAYS = 20  # Not picky about the pre-flooding image
        POST_SEARCH_RANGE_DAYS = 20  # After too many days the flood will have receded.
        priorStartDate = self.floodDate.advance(
            -1 * PRIOR_SEARCH_RANGE_DAYS, 'day')  # Prior stops before the date
        postStartDate = self.floodDate  # Post starts at the date

        # Load before and after Landsat data
        # - We can afford to be pickier about clouds in the prior image than in the post image.
        try:
            self.landsatPrior = getCloudFreeLandsat(bounds,
                                                    priorStartDate,
                                                    PRIOR_SEARCH_RANGE_DAYS,
                                                    maxCloudPercentage=0.05,
                                                    searchMethod='decreasing')
            priorLsDate = cmt.util.miscUtilities.getDateFromLandsatInfo(
                self.landsatPrior.getInfo())
            print 'Found prior Landsat date: ' + priorLsDate
        except Exception as e:
            print 'Failed to find prior Landsat image!'
            print str(e)
            print(sys.exc_info()[0])
            self.landsatPrior = None
        try:
            self.landsatPost = getCloudFreeLandsat(bounds,
                                                   postStartDate,
                                                   POST_SEARCH_RANGE_DAYS,
                                                   maxCloudPercentage=0.25,
                                                   searchMethod='increasing')
            postLsDate = cmt.util.miscUtilities.getDateFromLandsatInfo(
                self.landsatPost.getInfo())
            print 'Found post Landsat date: ' + postLsDate
        except Exception as e:
            print 'Failed to find post Landsat image!'
            print str(e)
            print(sys.exc_info()[0])
            self.landsatPost = None

        # Load before and after Sentinel-1 data
        try:
            self.sentinel1Prior = getNearestSentinel1(
                bounds,
                priorStartDate,
                PRIOR_SEARCH_RANGE_DAYS,
                searchMethod='decreasing')
            priorS1Date = cmt.util.miscUtilities.getDateFromSentinel1Info(
                self.sentinel1Prior.getInfo())
            print 'Found prior Sentinel-1 date: ' + priorS1Date
        except Exception as e:
            print 'Failed to find prior Sentinel-1 image!'
            print str(e)
            print(sys.exc_info()[0])
            self.sentinel1Prior = None
        try:
            self.sentinel1Post = getNearestSentinel1(bounds,
                                                     postStartDate,
                                                     POST_SEARCH_RANGE_DAYS,
                                                     searchMethod='increasing')
            postS1Date = cmt.util.miscUtilities.getDateFromSentinel1Info(
                self.sentinel1Post.getInfo())
            print 'Found post Sentinel-1 date: ' + postS1Date
        except Exception as e:
            print 'Failed to find post Sentinel-1 image!'
            print str(e)
            print(sys.exc_info()[0])
            self.sentinel1Post = None

        # Load before and after MODIS data
        # - We can afford to be pickier about clouds in the prior image than in the post image.
        try:
            self.modisPrior = getCloudFreeModis(bounds,
                                                priorStartDate,
                                                PRIOR_SEARCH_RANGE_DAYS,
                                                maxCloudPercentage=0.05,
                                                searchMethod='decreasing')
            priorModisDate = cmt.util.miscUtilities.getDateFromModisInfo(
                self.modisPrior.getInfo())
            print 'Found prior MODIS date: ' + priorModisDate
        except Exception as e:
            print 'Failed to find prior MODIS image!'
            print str(e)
            print(sys.exc_info()[0])
            self.modisPrior = None
        try:
            self.modisPost = getCloudFreeModis(bounds,
                                               postStartDate,
                                               POST_SEARCH_RANGE_DAYS,
                                               maxCloudPercentage=0.25,
                                               searchMethod='increasing')
            postModisDate = cmt.util.miscUtilities.getDateFromModisInfo(
                self.modisPost.getInfo())
            print 'Found post MODIS date: ' + postModisDate
        except Exception as e:
            print 'Failed to find post MODIS image!'
            print str(e)
            print(sys.exc_info()[0])
            self.modisPost = None

        if self.modisPost:
            # Extract the MODIS cloud mask
            # - We only use the cloud mask from the POST image
            self.modisCloudMask = cmt.modis.modis_utilities.getModisBadPixelMask(
                self.modisPost)
            self.modisCloudMask = self.modisCloudMask.mask(self.modisCloudMask)

        # Load a DEM
        demName = 'CGIAR/SRTM90_V4'  # The default 90m global DEM
        if (boundsInsideTheUS):
            demName = 'ned_13'  # The US 10m DEM
        self.demImage = ee.Image(demName)

        # Now add all the images to the map!
        self._displayCurrentImages()

    def _loadFloodDetect(self):
        '''Creates the Earth Engine flood detection function and adds it to the map'''

        # Check prerequisites
        if (not self.modisPost) or (not self.floodDate) or (
                not self.detectParams.statisticsRegion):
            print "Can't detect floods without image data and flood date!"
            return

        # Remove the last EE function from the map
        if self.eeFunction:
            self.mapWidget.removeFromMap(self.eeFunction)

        print 'Starting flood detection with the following parameters:'
        print '--> Water mask threshold       = ' + str(
            self.detectParams.waterMaskThreshold)
        print '--> Change detection threshold = ' + str(
            self.detectParams.changeDetectThreshold)

        # Generate a new EE function
        self.eeFunction = cmt.modis.misc_algorithms.history_diff_core(
            self.modisPost, self.floodDate,
            self.detectParams.waterMaskThreshold,
            self.detectParams.changeDetectThreshold,
            self.detectParams.statisticsRegion)
        self.eeFunction = self.eeFunction.mask(self.eeFunction)

        # Add the new EE function to the map
        # TODO: Set display parameters with widgets
        OPACITY = 0.5
        COLOR = '00FFFF'
        self.mapWidget.addToMap(self.eeFunction, {
            'min': 0,
            'max': 1,
            'opacity': OPACITY,
            'palette': COLOR
        }, 'Flood Detection Results', True)

    def _handleParamChange(self, value, parameterName='DEBUG'):
        '''Reload an EE algorithm when one of its parameters is set in the GUI'''
        if parameterName == 'Change Detection Threshold':
            self.detectParams.changeDetectThreshold = value
            return
        if parameterName == 'Water Mask Threshold':
            self.detectParams.waterMaskThreshold = value
            return
        print 'WARNING: Parameter ' + parameterName + ' is set to: ' + str(
            value)

    def _setDate(self, date):
        '''Sets the current date'''
        self.qtDate = date
        self.floodDate = ee.Date.fromYMD(date.year(), date.month(),
                                         date.day())  # Load into an EE object
        self.dateButton.setText(
            date.toString('yyyy/MM/dd'))  # Format for humans to read

    def _setRegionToView(self):
        '''Sets the processing region to the current viewable area'''
        # Extract the current viewing bounds as [minLon, minLat, maxLon, maxLat]
        lonLatBounds = self.mapWidget.GetMapBoundingBox(
        )  # TODO: This function does not work!!!!
        print 'Setting region to: ' + str(lonLatBounds)
        self.detectParams.statisticsRegion = apply(
            ee.geometry.Geometry.Rectangle, lonLatBounds)

    def _showCalendar(self):
        '''Pop up a little calendar window so the user can select a date'''
        menu = QtGui.QMenu(self)
        action = QtGui.QWidgetAction(menu)
        item = DatePickerWidget(self._setDate,
                                self.qtDate)  # Pass in callback function
        action.setDefaultWidget(item)
        menu.addAction(action)
        menu.popup(QtGui.QCursor.pos())

    def _showAboutText(self):
        '''Pop up a little text box to display legal information'''
        QtGui.QMessageBox.about(self, 'about', ABOUT_TEXT)

    def keyPressEvent(self, event):
        """Handle keypress events."""
        if event.key() == QtCore.Qt.Key_Q:
            QtGui.QApplication.quit()

    def __getattr__(self, attr):
        '''Forward any undefined function call to the main map widget'''
        try:
            return getattr(self.mapWidget,
                           attr)  # Forward the call to the MapViewWidget class
        except:
            print str(attr)
            raise AttributeError(
                attr
            )  # This happens if the MapViewWidget class does not support the call