class Main(QMainWindow):    
    haveData        = pyqtSignal()
    dataReadyToView = pyqtSignal()
        
    def __init__(self, argv):
        QMainWindow.__init__(self)
        
        #Normalize the data if true
        self._normalize_data=True
        
        if 'notnormalize' in sys.argv:
            print sys.argv
            self._normalize_data=False
            sys.argv.remove('notnormalize')

        self.opPredict = None
        self.opTrain = None
        self.opThreshold = None
        self._colorTable16 = self._createDefault16ColorColorTable()
        
        #self.g = Graph(7, 2048*1024**2*5)
        self.g = Graph()
        self.fixableOperators = []
        
        self.featureDlg=None

        
        #The old ilastik provided the following scale names:
        #['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Megahuge', 'Gigahuge']
        #The corresponding scales are:
        self.featScalesList=[0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0]
        
        self.initUic()
        
        #if the filename was specified on command line, load it
        if len(sys.argv) >= 2:
            def loadFile():
                self._openFile(sys.argv[1:])
            QTimer.singleShot(0, loadFile)
        
    def setIconToViewMenu(self):
            self.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap()))
        
    def initUic(self):
        p = os.path.split(__file__)[0]+'/'
        if p == "/": p = "."+p
        uic.loadUi(p+"MainWindow_cc.ui", self) 
        #connect the window and graph creation to the opening of the file
        self.actionOpen.triggered.connect(self.openFile)
        self.actionQuit.triggered.connect(qApp.quit)
        
        def toggleDebugPatches(show):
            self.editor.showDebugPatches = show
        def fitToScreen():
            shape = self.editor.posModel.shape
            for i, v in enumerate(self.editor.imageViews):
                s = list(copy.copy(shape))
                del s[i]
                v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s)))  
                
        def fitImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].fitImage()
                
        def restoreImageToOriginalSize():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo()
                    
        def rubberBandZoom():
            if hasattr(self.editor, '_lastImageViewFocus'):
                if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True
                    self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor()
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor)
                else:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup)
                
        
        def hideHud():
            if self.editor.imageViews[0]._hud.isVisible():
                hide = False
            else:
                hide = True
            for i, v in enumerate(self.editor.imageViews):
                v.hideHud(hide)
                
        def toggleSelectedHud():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud()
                
        def centerAllImages():
            for i, v in enumerate(self.editor.imageViews):
                v.centerImage()
                
        def centerImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].centerImage()
                self.actionOnly_for_current_view.setEnabled(True)
        
        self.actionCenterAllImages.triggered.connect(centerAllImages)
        self.actionCenterImage.triggered.connect(centerImage)
        self.actionToggleAllHuds.triggered.connect(hideHud)
        self.actionToggleSelectedHud.triggered.connect(toggleSelectedHud)
        self.actionShowDebugPatches.toggled.connect(toggleDebugPatches)
        self.actionFitToScreen.triggered.connect(fitToScreen)
        self.actionFitImage.triggered.connect(fitImage)
        self.actionReset_zoom.triggered.connect(restoreImageToOriginalSize)
        self.actionRubberBandZoom.triggered.connect(rubberBandZoom)
        
        self.haveData.connect(self.initGraph)
        self.dataReadyToView.connect(self.initEditor)
        
        self.layerstack = LayerStackModel()
        
        model = LabelListModel()
        self.labelListView.setModel(model)
        self.labelListModel=model
        
        self.labelListModel.rowsAboutToBeRemoved.connect(self.onLabelAboutToBeRemoved)
        self.labelListModel.labelSelected.connect(self.switchLabel)
        
        def onDataChanged(topLeft, bottomRight):
            firstRow = topLeft.row()
            lastRow  = bottomRight.row()
        
            firstCol = topLeft.column()
            lastCol  = bottomRight.column()
            
            if lastCol == firstCol == 0:
                assert(firstRow == lastRow) #only one data item changes at a time

                #in this case, the actual data (for example color) has changed
                self.switchColor(firstRow+1, self.labelListModel[firstRow].color)
                self.editor.scheduleSlicesRedraw()
            else:
                #this column is used for the 'delete' buttons, we don't care
                #about data changed here
                pass
            
        self.labelListModel.dataChanged.connect(onDataChanged)
        
        self.AddLabelButton.clicked.connect(self.addLabel)
        
        self.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked)
        self.StartClassificationButton.clicked.connect(self.startClassification)        
        self.StartClassificationButton.setEnabled(False)

        self.checkInteractive.setEnabled(False)
        self.checkInteractive.toggled.connect(self.toggleInteractive)   

        self.interactionComboBox.currentIndexChanged.connect(self.changeInteractionMode)
        self.interactionComboBox.setEnabled(False)

        self.AddThresholdButton.clicked.connect(self.addThresholdOperator)
        self.AddThresholdButton.setEnabled(False)
        self.CCButton.clicked.connect(self.addCCOperator)
        self.CCButton.setEnabled(False)

        self._initFeatureDlg()
        
    def toggleInteractive(self, checked):
        print "toggling interactive mode to '%r'" % checked
        
        #Check if the number of labels in the layer stack is equals to the number of Painted labels
        if checked==True:
            labels =numpy.unique(numpy.asarray(self.opLabels.outputs["nonzeroValues"][:].allocate().wait()[0]))           
            nPaintedLabels=labels.shape[0]
            nLabelsLayers = self.labelListModel.rowCount()
            selectedFeatures = numpy.asarray(self.featureDlg.featureTableWidget.createSelectedFeaturesBoolMatrix())
            
            if nPaintedLabels!=nLabelsLayers:
                self.checkInteractive.setCheckState(0)
                mexBox=QMessageBox()
                mexBox.setText("Did you forget to paint some labels?")
                mexBox.setInformativeText("Painted Labels %d \nNumber Active Labels Layers %d"%(nPaintedLabels,self.labelListModel.rowCount()))
                mexBox.exec_()
                return
            if (selectedFeatures==0).all():
                self.checkInteractive.setCheckState(0)
                mexBox=QMessageBox()
                mexBox.setText("The are no features selected ")
                mexBox.exec_()
                return
        else:
            self.g.stopGraph()
            self.g.resumeGraph()
                
        self.AddLabelButton.setEnabled(not checked)
        self.SelectFeaturesButton.setEnabled(not checked)
        for o in self.fixableOperators:
            o.inputs["fixAtCurrent"].setValue(not checked)
        self.labelListModel.allowRemove(not checked)
        
        self.editor.scheduleSlicesRedraw()

    def changeInteractionMode( self, index ):
        modes = {0: "navigation", 1: "brushing"}
        self.editor.setInteractionMode( modes[index] )
        self.interactionComboBox.setCurrentIndex(index)
        print "interaction mode switched to", modes[index]

    def switchLabel(self, row):
        print "switching to label=%r" % (self.labelListModel[row])
        #+1 because first is transparent
        #FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row+1)
        self.editor.brushingModel.setBrushColor(self.labelListModel[row].color)
        
    def switchColor(self, row, color):
        print "label=%d changes color to %r" % (row, color)
        self.labellayer.colorTable[row]=color.rgba()
        self.editor.brushingModel.setBrushColor(color)
        self.editor.scheduleSlicesRedraw()
    
    def addLabel(self):
        color = QColor(numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255))
        numLabels = len(self.labelListModel)
        if numLabels < len(self._colorTable16):
            color = self._colorTable16[numLabels]
        self.labellayer.colorTable.append(color.rgba())
        
        self.labelListModel.insertRow(self.labelListModel.rowCount(), Label("Label %d" % (self.labelListModel.rowCount() + 1), color))
        nlabels = self.labelListModel.rowCount()
        if self.opPredict is not None:
            print "Label added, changing predictions"
            #re-train the forest now that we have more labels
            self.opPredict.inputs['LabelsCount'].setValue(nlabels)
            self.addPredictionLayer(nlabels-1, self.labelListModel._labels[nlabels-1])
        
        #make the new label selected
        index = self.labelListModel.index(nlabels-1, 1)
        self.labelListModel._selectionModel.select(index, QItemSelectionModel.ClearAndSelect)
        
        #FIXME: this should watch for model changes   
        #drawing will be enabled when the first label is added  
        self.changeInteractionMode( 1 )
        self.interactionComboBox.setEnabled(True)
    
    def onLabelAboutToBeRemoved(self, parent, start, end):
        #the user deleted a label, reshape prediction and remove the layer
        #the interface only allows to remove one label at a time?
        
        nout = start-end+1
        ncurrent = self.labelListModel.rowCount()
        print "removing", nout, "out of ", ncurrent
        
        if self.opPredict is not None:
            self.opPredict.inputs['LabelsCount'].setValue(ncurrent-nout)
        for il in range(start, end+1):
            labelvalue = self.labelListModel._labels[il]
            self.removePredictionLayer(labelvalue)
            self.opLabels.inputs["deleteLabel"].setValue(il+1)
            self.editor.scheduleSlicesRedraw()
            
    
    def startClassification(self):
        if self.opTrain is None:
            #initialize all classification operators
            print "initializing classification..."
            opMultiL = Op5ToMulti(self.g)    
            opMultiL.inputs["Input0"].connect(self.opLabels.outputs["Output"])
            
            opMultiLblocks = Op5ToMulti(self.g)
            opMultiLblocks.inputs["Input0"].connect(self.opLabels.outputs["nonzeroBlocks"])
            self.opTrain = OpTrainRandomForestBlocked(self.g)
            self.opTrain.inputs['Labels'].connect(opMultiL.outputs["Outputs"])
            self.opTrain.inputs['Images'].connect(self.opFeatureCache.outputs["Output"])
            self.opTrain.inputs["nonzeroLabelBlocks"].connect(opMultiLblocks.outputs["Outputs"])
            self.opTrain.inputs['fixClassifier'].setValue(False)                
            
            opClassifierCache = OpArrayCache(self.g)
            opClassifierCache.inputs["Input"].connect(self.opTrain.outputs['Classifier'])
           
            ################## Prediction
            self.opPredict=OpPredictRandomForest(self.g)
            nclasses = self.labelListModel.rowCount()
            self.opPredict.inputs['LabelsCount'].setValue(nclasses)
            self.opPredict.inputs['Classifier'].connect(opClassifierCache.outputs['Output']) 
            self.opPredict.inputs['Image'].connect(self.opPF.outputs["Output"])

            pCache = OpSlicedBlockedArrayCache(self.g)
            pCache.inputs["fixAtCurrent"].setValue(False)
            pCache.inputs["innerBlockShape"].setValue(((1,256,256,1,2),(1,256,1,256,2),(1,1,256,256,2)))
            pCache.inputs["outerBlockShape"].setValue(((1,256,256,4,2),(1,256,4,256,2),(1,4,256,256,2)))
            pCache.inputs["Input"].connect(self.opPredict.outputs["PMaps"])
            self.pCache = pCache
  
            #add prediction results for all classes as separate channels
            for icl in range(nclasses):
                self.addPredictionLayer(icl, self.labelListModel._labels[icl])
        self.StartClassificationButton.setEnabled(False)
        self.checkInteractive.setEnabled(True)
        self.AddThresholdButton.setEnabled(True)
                                  
    def addPredictionLayer(self, icl, ref_label):
        
        selector=OpSingleChannelSelector(self.g)
        selector.inputs["Input"].connect(self.pCache.outputs['Output'])
        selector.inputs["Index"].setValue(icl)
                
        if self.checkInteractive.isChecked():
            self.pCache.inputs["fixAtCurrent"].setValue(False)
        else:
            self.pCache.inputs["fixAtCurrent"].setValue(True)
        
        predictsrc = LazyflowSource(selector.outputs["Output"][0])
        def srcName(newName):
            predictsrc.setObjectName("Prediction for %s" % ref_label.name)
        srcName("")
        
        predictLayer = AlphaModulatedLayer(predictsrc, tintColor=ref_label.color)
        predictLayer.nameChanged.connect(srcName)
        
        def setLayerColor(c):
            print "as the color of label '%s' has changed, setting layer's '%s' tint color to %r" % (ref_label.name, predictLayer.name, c)
            predictLayer.tintColor = c
        ref_label.colorChanged.connect(setLayerColor)
        def setLayerName(n):
            newName = "Prediction for %s" % ref_label.name
            print "as the name of label '%s' has changed, setting layer's '%s' name to '%s'" % (ref_label.name, predictLayer.name, newName)
            predictLayer.name = newName
        setLayerName(ref_label.name)
        ref_label.nameChanged.connect(setLayerName)
        
        predictLayer.ref_object = ref_label
        #make sure that labels (index = 0) stay on top!
        self.layerstack.insert(1, predictLayer )
        self.fixableOperators.append(self.pCache)
               
    def removePredictionLayer(self, ref_label):
        for il, layer in enumerate(self.layerstack):
            if layer.ref_object==ref_label:
                print "found the prediction", layer.ref_object, ref_label
                self.layerstack.removeRows(il, 1)
                break
    
    def addThresholdOperator(self):
        if self.opThreshold is None:
            self.opThreshold = OpThreshold(self.g)
            self.opThreshold.inputs["Input"].connect(self.pCache.outputs["Output"])
        #channel, value = ThresholdDlg(self.labelListModel._labels)
        channel = 0
        value = 0.5
        ref_label = self.labelListModel._labels[channel]
        self.opThreshold.inputs["Channel"].setValue(channel)
        self.opThreshold.inputs["Threshold"].setValue(value)
        threshsrc = LazyflowSource(self.opThreshold.outputs["Output"][0])
        
        threshsrc.setObjectName("Threshold for %s" % ref_label.name)
        transparent = QColor(0,0,0,0)
        white = QColor(255,255,255)
        colorTable = [transparent.rgba(), white.rgba()]
        threshLayer = ColortableLayer(threshsrc, colorTable = colorTable )
        threshLayer.name = "Threshold for %s" % ref_label.name
        self.layerstack.insert(1, threshLayer)
        self.CCButton.setEnabled(True)
        
    def addCCOperator(self):
        self.opCC = OpConnectedComponents(self.g)
        self.opCC.inputs["Input"].connect(self.opThreshold.outputs["Output"])
        #we shouldn't have to define these. But just in case...
        self.opCC.inputs["Neighborhood"].setValue(6)
        self.opCC.inputs["Background"].setValue(0)
        
        ccsrc = LazyflowSource(self.opCC.outputs["Output"][0])
        ccsrc.setObjectName("Connected Components")
        ctb = colortables.create_default_16bit()
        ctb.insert(0, QColor(0, 0, 0, 0).rgba()) # make background transparent
        ccLayer = ColortableLayer(ccsrc, ctb)
        ccLayer.name = "Connected Components"
        self.layerstack.insert(1, ccLayer)
        
            
    def openFile(self):
        fileNames = QFileDialog.getOpenFileNames(self, "Open Image", os.path.abspath(__file__), "Numpy and h5 files (*.npy *.h5)")
        if fileNames.count() == 0:
            return
        self._openFile(fileNames)
        
    def _openFile(self, fileNames):
        self.inputProvider = None
        fName, fExt = os.path.splitext(str(fileNames[0]))
        print "Opening Files %r" % fileNames
        if fExt=='.npy':
            fileName = fileNames[0]
            if len(fileNames)>1:
                print "WARNING: only the first file will be read, multiple file prediction not supported yet"
            fName, fExt = os.path.splitext(str(fileName))
            self.raw = numpy.load(str(fileName))
            self.min, self.max = numpy.min(self.raw), numpy.max(self.raw)
            self.inputProvider = OpArrayPiper(self.g)
            self.raw = self.raw.view(vigra.VigraArray)
            self.raw.axistags =  vigra.AxisTags(
                vigra.AxisInfo('t',vigra.AxisType.Time),
                vigra.AxisInfo('x',vigra.AxisType.Space),
                vigra.AxisInfo('y',vigra.AxisType.Space),
                vigra.AxisInfo('z',vigra.AxisType.Space),
                vigra.AxisInfo('c',vigra.AxisType.Channels))
            self.inputProvider.inputs["Input"].setValue(self.raw)
        elif fExt=='.h5':
            readerNew=OpH5ReaderBigDataset(self.g)
            
            
            readerNew.inputs["Filenames"].setValue(fileNames)
            readerNew.inputs["hdf5Path"].setValue("volume/data")

            readerCache =  OpSlicedBlockedArrayCache(self.g)
            readerCache.inputs["fixAtCurrent"].setValue(False)
            readerCache.inputs["innerBlockShape"].setValue(((1,256,256,1,2),(1,256,1,256,2),(1,1,256,256,2)))
            readerCache.inputs["outerBlockShape"].setValue(((1,256,256,4,2),(1,256,4,256,2),(1,4,256,256,2)))
            readerCache.inputs["Input"].connect(readerNew.outputs["Output"])

            self.inputProvider = OpArrayPiper(self.g)
            self.inputProvider.inputs["Input"].connect(readerCache.outputs["Output"])
        else:
            raise RuntimeError("opening filenames=%r not supported yet" % fileNames)
        
        self.haveData.emit()
       
    def initGraph(self):
        shape = self.inputProvider.outputs["Output"].shape
        srcs    = []
        minMax = []
        normalize = []
        
        print "* Data has shape=%r" % (shape,)
        
        #create a layer for each channel of the input:
        slicer=OpMultiArraySlicer2(self.g)
        slicer.inputs["Input"].connect(self.inputProvider.outputs["Output"])
        slicer.inputs["AxisFlag"].setValue('c')
       
        nchannels = shape[-1]
        for ich in xrange(nchannels):
            data=slicer.outputs['Slices'][ich][:].allocate().wait()
            #find the minimum and maximum value for normalization
            mm = (numpy.min(data), numpy.max(data))
            print "  - channel %d: min=%r, max=%r" % (ich, mm[0], mm[1])
            minMax.append(mm)
            if self._normalize_data:
                normalize.append(mm)
            else:
                normalize.append((0,255))
            layersrc = LazyflowSource(slicer.outputs['Slices'][ich], priority = 100)
            layersrc.setObjectName("raw data channel=%d" % ich)
            srcs.append(layersrc)
            
        #FIXME: we shouldn't merge channels automatically, but for now it's prettier
        layer1 = None
        if nchannels == 1:
            layer1 = GrayscaleLayer(srcs[0], range=minMax[0], normalize=normalize[0])
            print "  - showing raw data as grayscale"
        elif nchannels==2:
            layer1 = RGBALayer(red  = srcs[0], normalizeR=normalize[0],
                               green = srcs[1], normalizeG=normalize[1], range=minMax[0:2]+[(0,255), (0,255)])
            print "  - showing channel 1 as red, channel 2 as green"
        elif nchannels==3:
            layer1 = RGBALayer(red   = srcs[0], normalizeR=normalize[0],
                               green = srcs[1], normalizeG=normalize[1],
                               blue  = srcs[2], normalizeB=normalize[2],
                               range = minMax[0:3])
            print "  - showing channel 1 as red, channel 2 as green, channel 3 as blue"
        else:
            print "only 1,2 or 3 channels supported so far"
            return
        print
        
        layer1.name = "Input data"
        layer1.ref_object = None
        self.layerstack.append(layer1)
        
        opImageList = Op5ToMulti(self.g)    
        opImageList.inputs["Input0"].connect(self.inputProvider.outputs["Output"])
        
        #init the features operator
        opPF = OpPixelFeaturesPresmoothed(self.g)
        opPF.inputs["Input"].connect(opImageList.outputs["Outputs"])
        opPF.inputs["Scales"].setValue(self.featScalesList)
        self.opPF=opPF
        
        #Caches the features
        opFeatureCache = OpBlockedArrayCache(self.g)
        opFeatureCache.inputs["innerBlockShape"].setValue((1,32,32,32,16))
        opFeatureCache.inputs["outerBlockShape"].setValue((1,128,128,128,64))
        opFeatureCache.inputs["Input"].connect(opPF.outputs["Output"])
        opFeatureCache.inputs["fixAtCurrent"].setValue(False)  
        self.opFeatureCache=opFeatureCache
        
        
        self.initLabels()
        self.dataReadyToView.emit()
        
    def initLabels(self):
        #Add the layer to draw the labels, but don't add any labels
        shape=self.inputProvider.outputs["Output"].shape
        
        self.opLabels = OpBlockedSparseLabelArray(self.g)                                
        self.opLabels.inputs["shape"].setValue(shape[:-1] + (1,))
        self.opLabels.inputs["blockShape"].setValue((1, 32, 32, 32, 1))
        self.opLabels.inputs["eraser"].setValue(100)                
        
        self.labelsrc = LazyflowSinkSource(self.opLabels, self.opLabels.outputs["Output"], self.opLabels.inputs["Input"])
        self.labelsrc.setObjectName("labels")
        
        transparent = QColor(0,0,0,0)
        self.labellayer = ColortableLayer(self.labelsrc, colorTable = [transparent.rgba()] )
        self.labellayer.name = "Labels"
        self.labellayer.ref_object = None
        self.layerstack.append(self.labellayer)    
    
    def initEditor(self):
        shape=self.inputProvider.outputs["Output"].shape
        
        self.editor = VolumeEditor(self.layerstack, labelsink=self.labelsrc)
        self.editor.dataShape = shape

        self.editor.newImageView2DFocus.connect(self.setIconToViewMenu)
        #drawing will be enabled when the first label is added  
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)
        model = self.editor.layerStack
        self.layerWidget.init(model)
        self.UpButton.clicked.connect(model.moveSelectedUp)
        model.canMoveSelectedUp.connect(self.UpButton.setEnabled)
        self.DownButton.clicked.connect(model.moveSelectedDown)
        model.canMoveSelectedDown.connect(self.DownButton.setEnabled)
        self.DeleteButton.clicked.connect(model.deleteSelected)
        model.canDeleteSelected.connect(self.DeleteButton.setEnabled)     
        
        self.opLabels.inputs["eraser"].setValue(self.editor.brushingModel.erasingNumber)      
    
    def _createDefault16ColorColorTable(self):
        c = []
        c.append(QColor(0, 0, 255))
        c.append(QColor(255, 255, 0))
        c.append(QColor(255, 0, 0))
        c.append(QColor(0, 255, 0))
        c.append(QColor(0, 255, 255))
        c.append(QColor(255, 0, 255))
        c.append(QColor(255, 105, 180)) #hot pink
        c.append(QColor(102, 205, 170)) #dark aquamarine
        c.append(QColor(165,  42,  42)) #brown        
        c.append(QColor(0, 0, 128))     #navy
        c.append(QColor(255, 165, 0))   #orange
        c.append(QColor(173, 255,  47)) #green-yellow
        c.append(QColor(128,0, 128))    #purple
        c.append(QColor(192, 192, 192)) #silver
        c.append(QColor(240, 230, 140)) #khaki
        c.append(QColor(69, 69, 69))    # dark grey
        return c
    
    
    def onFeatureButtonClicked(self):
        self.featureDlg.show()
        def onDlgAccepted():
            self.StartClassificationButton.setEnabled(True)
        self.featureDlg.accepted.connect(onDlgAccepted)
    
    def _onFeaturesChosen(self):
        selectedFeatures = self.featureDlg.featureTableWidget.createSelectedFeaturesBoolMatrix()
        print "new feature set:", selectedFeatures
        self.opPF.inputs['Matrix'].setValue(numpy.asarray(selectedFeatures))
    
    def _initFeatureDlg(self):
        dlg = self.featureDlg = FeatureDlg()
        
        dlg.setWindowTitle("Features")
        dlg.createFeatureTable({"Features": [FeatureEntry("Gaussian smoothing"), \
                                             FeatureEntry("Laplacian of Gaussian"), \
                                             FeatureEntry("Structure Tensor Eigenvalues"), \
                                             FeatureEntry("Hessian of Gaussian EV"),  \
                                             FeatureEntry("Gaussian Gradient Magnitude"), \
                                             FeatureEntry("Difference Of Gaussian")]}, \
                               self.featScalesList)
        dlg.setImageToPreView(None)
        m = [[1,0,0,0,0,0,0],[1,0,0,0,0,0,0],[0,0,0,0,0,0,0],[1,0,0,0,0,0,0],[1,0,0,0,0,0,0],[1,0,0,0,0,0,0]]
        dlg.featureTableWidget.setSelectedFeatureBoolMatrix(m)
        dlg.accepted.connect(self._onFeaturesChosen)
Esempio n. 2
0
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass
    
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus( self ):
        return [self.menuView] # From the .ui file

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()
            
        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)
        
        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = Op1ToMulti(graph=slot.operator.graph)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Outputs
                self._orphanOperators.append( opPromoteInput )

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append( slot )
            slot.notifyInserted( bind(self._handleLayerInsertion) )
            slot.notifyRemoved( bind(self._handleLayerRemoval) )
            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)
 
        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui.
        
    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers( self ):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)
                    
                    # Name the layer after the slot name.
                    if isinstance( multiLayerSlot.getRealOperator(), Op1ToMulti ):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator().Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    #layers.append(layer)
        return layers

    @traceLogged(traceLogger)
    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady( bind(self.updateAllLayers) )
        slot[slotIndex].notifyUnready( bind(self.updateAllLayers) )

    @traceLogged(traceLogger)
    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers()

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        def getRange(meta):
            if 'drange' in meta:
                return meta.drange
            if meta.dtype == numpy.uint8:
                return (0, 255)
            else:
                # If we don't know the range of the data, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return 'autoPercentiles'

        # Examine channel dimension to determine Grayscale vs. RGB
        shape = slot.meta.shape
        normalize = getRange(slot.meta)
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
        except:
            numChannels = 1

        if numChannels == 4:
            lastChannelIsAlpha = True
            
        if lastChannelIsAlpha:
            assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(numChannels)
        else:
            assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha).  Your image has {} channels.".format(numChannels)

        if numChannels == 1:
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            source = LazyflowSource(slot)
            normSource = NormalizingSource( source, bounds=normalize )
            return GrayscaleLayer(normSource)

        assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
            "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
        redProvider = OpSingleChannelSelector(graph=slot.graph)
        redProvider.Input.connect(slot)
        redProvider.Index.setValue( 0 )
        redSource = LazyflowSource( redProvider.Output )
        redNormSource = NormalizingSource( redSource, bounds=normalize )

        greenProvider = OpSingleChannelSelector(graph=slot.graph)
        greenProvider.Input.connect(slot)
        greenProvider.Index.setValue( 1 )
        greenSource = LazyflowSource( greenProvider.Output )
        greenNormSource = NormalizingSource( greenSource, bounds=normalize )

        blueNormSource = None
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            blueProvider = OpSingleChannelSelector(graph=slot.graph)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue( 2 )
            blueSource = LazyflowSource( blueProvider.Output )
            blueNormSource = NormalizingSource( blueSource, bounds=normalize )

        alphaNormSource = None
        if lastChannelIsAlpha:
            alphaProvider = OpSingleChannelSelector(graph=slot.graph)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( numChannels-1 )
            alphaSource = LazyflowSource( alphaProvider.Output )
            alphaNormSource = NormalizingSource( alphaSource, bounds=normalize )

        layer = RGBALayer( red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource )
        return layer

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self):
        if self._stopped or not self._initialized:
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str( [l.name for l in newGuiLayers] )
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
           
            #if  the image is 2D, do not show the HUD action (issue #190) 
            is2D = numpy.sum(numpy.asarray(newDataShape[1:4]) == 1) == 1
            self.menuGui.actionToggleSelectedHud.setVisible(not is2D)
            self.menuGui.actionToggleAllHuds.setVisible(not is2D)
            
            # Find the xyz midpoint
            midpos5d = [x/2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] )

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister( obsoleteShortcut )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    @traceLogged(traceLogger)
    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None and slot.ready() and slot.meta.axistags is not None:
                    # Use an Op5ifyer adapter to transpose the shape for us.
                    op5 = Op5ifyer( graph=slot.graph )
                    op5.input.connect( slot )
                    newDataShape = op5.output.meta.shape

                    # We just needed the operator to determine the transposed shape.
                    # Disconnect it so it can be garbage collected.
                    op5.input.disconnect()

        if newDataShape is not None:
            # For now, this base class combines multi-channel images into a single layer,
            # So, we want the volume editor to behave as though there is only one channel
            newDataShape = newDataShape[:-1] + (1,)
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

        # Menu is specified in a separate ui file with a dummy window
        self.menuGui = uic.loadUi(localDir+"/menu.ui") # Save as member so it doesn't get picked up by GC
        self.menuBar = self.menuGui.menuBar
        self.menuView = self.menuGui.menuView

        def toggleDebugPatches(show):
            self.editor.showDebugPatches = show

        def setCacheSize( cache_size ):
            dlg = QDialog(self)
            layout = QHBoxLayout()
            layout.addWidget( QLabel("Cached Slices Per View:") )

            cache_size = [self.editor.cacheSize]
            def parseCacheSize( strSize ):
                try:
                    cache_size[0] = int(strSize)
                except:
                    pass

            edit = QLineEdit( str(cache_size[0]), parent=dlg )
            edit.textChanged.connect( parseCacheSize )
            layout.addWidget( edit )
            okButton = QPushButton( "OK", parent=dlg )
            okButton.clicked.connect( dlg.accept )
            layout.addWidget( okButton )
            dlg.setLayout( layout )
            dlg.setModal(True)
            dlg.exec_()
            self.editor.cacheSize = cache_size[0]

        def enablePrefetching( enable ):
            for scene in self.editor.imageScenes:
                scene.setPrefetchingEnabled( enable )

        def fitToScreen():
            shape = self.editor.posModel.shape
            for i, v in enumerate(self.editor.imageViews):
                s = list(shape)
                del s[i]
                v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s)))

        def fitImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].fitImage()

        def restoreImageToOriginalSize():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo()

        '''
        def rubberBandZoom():
            if hasattr(self.editor, '_lastImageViewFocus'):
                if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True
                    self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor()
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor)
                else:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup)
        '''

        def hideHud():
            hide = not self.editor.imageViews[0]._hud.isVisible()
            for i, v in enumerate(self.editor.imageViews):
                v.setHudVisible(hide)

        def toggleSelectedHud():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud()

        def centerAllImages():
            for i, v in enumerate(self.editor.imageViews):
                v.centerImage()

        def centerImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].centerImage()
                self.menuGui.actionOnly_for_current_view.setEnabled(True)

        def resetAxes():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageScenes[self.editor._lastImageViewFocus].resetAxes()
                self.menuGui.actionOnly_for_current_view.setEnabled(True)

        def resetAllAxes():
            for i, s in enumerate(self.editor.imageScenes):
                s.resetAxes()

        self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages)
        self.menuGui.actionCenterImage.triggered.connect(centerImage)
        self.menuGui.actionToggleAllHuds.triggered.connect(hideHud)
        self.menuGui.actionResetAllAxes.triggered.connect(resetAllAxes)
        self.menuGui.actionToggleSelectedHud.triggered.connect(toggleSelectedHud)
        self.menuGui.actionResetAxes.triggered.connect(resetAxes)
        self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches)
        self.menuGui.actionFitToScreen.triggered.connect(fitToScreen)
        self.menuGui.actionFitImage.triggered.connect(fitImage)
        self.menuGui.actionReset_zoom.triggered.connect(restoreImageToOriginalSize)
        #FIXME: this needs bug fixing
        #self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom)
        self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize)
        self.menuGui.actionUsePrefetching.toggled.connect(enablePrefetching)

    @traceLogged(traceLogger)
    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )

        self.editor.newImageView2DFocus.connect(self._setIconToViewMenu)
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    @traceLogged(traceLogger)
    def _setIconToViewMenu(self):
        """
        In the "Only for Current View" menu item of the View menu,
        show the user which axis is the current one by changing the menu item icon.
        """
        self.menuGui.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap()))

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                if isinstance( datasource, NormalizingSource ):
                    datasource = datasource._rawSource
                if isinstance(datasource, LazyflowSource):
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break

        assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass
Esempio n. 3
0
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass
    
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus( self ):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [ self.volumeEditorWidget.getViewMenu(debug_mode) ]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()
            
        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)
        
        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append( opPromoteInput )

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append( slot )
            slot.notifyInserted( bind(self._handleLayerInsertion) )
            slot.notifyRemoved( bind(self._handleLayerRemoval) )
            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)
 
        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui.
        
    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers( self ):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)
                    
                    # Name the layer after the slot name.
                    if isinstance( multiLayerSlot.getRealOperator(), OpWrapSlot ):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator().Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    @traceLogged(traceLogger)
    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady( bind(self.updateAllLayers) )
        slot[slotIndex].notifyUnready( bind(self.updateAllLayers) )

    @traceLogged(traceLogger)
    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        
        def getRange(meta):
            if meta.drange is not None and meta.normalizeDisplay is False:
                # do not normalize if the user provided a range and set normalization to False
                return meta.drange
            else:
                # If we don't know the range of the data and normalization is allowed
                # by the user, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return None # means autoNormalize
                   
        shape = slot.meta.shape
        
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
            axisinfo = slot.meta.axistags["c"].description
        except:
            numChannels = 1
            axisinfo = "" # == no info on channels given
        
        rindex = None
        bindex = None
        gindex = None
        aindex = None
        
        if axisinfo == "" or axisinfo == "default":
            # Examine channel dimension to determine Grayscale vs. RGB

            if numChannels == 4:
                lastChannelIsAlpha = True
                
            if lastChannelIsAlpha:
                assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(numChannels)

            if numChannels == 1 or (numChannels > 4):
                assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
                source = LazyflowSource(slot)
                layer = GrayscaleLayer(source)
                layer.numberOfChannels = numChannels
                normalize = getRange(slot.meta)
                layer.set_normalize(0,normalize)
                return layer

            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
            
            rindex = 0
            gindex = 1
            if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
                bindex = 2
            if lastChannelIsAlpha:
                aindex = numChannels-1
        
        elif axisinfo == "grayscale":
            source = LazyflowSource(slot)
            layer = GrayscaleLayer(source)
            layer.numberOfChannels = numChannels
            normalize = getRange(slot.meta)
            layer.set_normalize(0,normalize)            
            return layer
        
        elif axisinfo == "rgba":
            rindex = 0
            if numChannels>=2:
                gindex = 1
            if numChannels>=3:
                bindex = 2
            if numChannels>=4:
                aindex = numChannels-1
        else:
            raise RuntimeError("unknown channel display mode")

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue( rindex )
            redSource = LazyflowSource( redProvider.Output )
        
        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue( gindex )
            greenSource = LazyflowSource( greenProvider.Output )
        
        blueSource = None
        if bindex is not None:
                blueProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
                blueProvider.Input.connect(slot)
                blueProvider.Index.setValue( bindex )
                blueSource = LazyflowSource( blueProvider.Output )

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( aindex )
            alphaSource = LazyflowSource( alphaProvider.Output )
        
        layer = RGBALayer( red=redSource, green=greenSource, blue=blueSource, alpha=alphaSource)
        

        
        normalize = getRange(slot.meta)
        print "createLayer normalize", normalize
        for i in xrange(4):
            if [redSource,greenSource,blueSource,alphaSource][i]:
                layer.set_normalize(i,normalize)
        return layer

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str( [l.name for l in newGuiLayers] )
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
                       
            # Find the xyz midpoint
            midpos5d = [x/2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] )

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister( obsoleteShortcut )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    @traceLogged(traceLogger)
    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None and slot.ready() and slot.meta.axistags is not None:
                    # Use an OpReorderAxes adapter to transpose the shape for us.
                    op5 = OpReorderAxes( parent=slot.getRealOperator().parent )
                    op5.Input.connect( slot )
                    newDataShape = op5.Output.meta.shape

                    # We just needed the operator to determine the transposed shape.
                    # Disconnect it so it can be garbage collected.
                    op5.Input.disconnect()
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

    @traceLogged(traceLogger)
    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )
        
        clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel )
        clickReporter2.rightClickReceived.connect( self._handleEditorRightClick )
        self.editor.brushingInterpreter = clickReporter2
        
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try: # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if(dataTags is None):
            raise RuntimeError("Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer.")
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass
Esempio n. 4
0
class LayerViewerGui(QMainWindow):
    """
    Implements an applet GUI whose central widget is a VolumeEditor 
      and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.
    
    Provides: - Central widget (viewer)
              - View Menu
              - Layer controls
              
    Does NOT provide an applet drawer widget.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawers(self):
        return [('Viewer', QWidget())]

    def menus(self):
        return [self.menuView]  # From the .ui file

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def setImageIndex(self, index):
        self._setImageIndex(index)

    def reset(self):
        # Remove all layers
        self.layerstack.clear()

    ###########################################
    ###########################################

    def operatorForCurrentImage(self):
        try:
            return self.topLevelOperator[self.imageIndex]
        except IndexError:
            return None

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperator):
        """
        Args:
            observedSlots   - A list of slots that we'll listen for changes on.
                               Each must be a multislot with level=1 or level=2.
                               The first index in the multislot is the image index. 
        """
        super(LayerViewerGui, self).__init__()

        self.threadRouter = ThreadRouter(self)  # For using @threadRouted

        self.topLevelOperator = topLevelOperator

        observedSlots = []

        for slot in topLevelOperator.inputs.values(
        ) + topLevelOperator.outputs.values():
            if slot.level == 1 or slot.level == 2:
                observedSlots.append(slot)

        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 1:
                # The user gave us a slot that is indexed as slot[image]
                # Wrap the operator so it has the right level.  Indexed as: slot[image][0]
                opPromoteInput = OperatorWrapper(Op1ToMulti,
                                                 graph=slot.operator.graph)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Outputs

            # Each slot should now be indexed as slot[image][sub_index]
            assert slot.level == 2
            self.observedSlots.append(slot)

        self.layerstack = LayerStackModel()

        self.initAppletDrawerUi(
        )  # Default implementation loads a blank drawer.
        self.initCentralUic()
        self.__viewerControlWidget = None
        self.initViewerControlUi()

        self.initEditor()

        self.imageIndex = -1
        self.lastUpdateImageIndex = -1

        def handleDatasetInsertion(slot, imageIndex):
            if self.imageIndex == -1 and self.areProvidersInSync():
                self.setImageIndex(imageIndex)

        for provider in self.observedSlots:
            provider.notifyInserted(bind(handleDatasetInsertion))

        def handleDatasetRemoval(slot, index, finalsize):
            if finalsize == 0:
                # Clear everything
                self.setImageIndex(-1)
            elif index == self.imageIndex:
                # Our currently displayed image is being removed.
                # Switch to the first image (unless that's the one being removed!)
                newIndex = 0
                if index == newIndex:
                    newIndex = 1
                self.setImageIndex(newIndex)

        for provider in self.observedSlots:
            provider.notifyRemove(bind(handleDatasetRemoval))

    def setupLayers(self, currentImageIndex):
        layers = []
        for multiImageSlot in self.observedSlots:
            if 0 <= currentImageIndex < len(multiImageSlot):
                multiLayerSlot = multiImageSlot[currentImageIndex]
                for j, slot in enumerate(multiLayerSlot):
                    if slot.ready():
                        layer = self.createStandardLayerFromSlot(slot)
                        layer.name = multiImageSlot.name + " " + str(j)
                        layers.append(layer)
        return layers

    @traceLogged(traceLogger)
    def _setImageIndex(self, imageIndex):
        if self.imageIndex != -1:
            for provider in self.observedSlots:
                # We're switching datasets.  Unsubscribe from the old one's notifications.
                provider[self.imageIndex].unregisterInserted(
                    bind(self.handleLayerInsertion))
                provider[self.imageIndex].unregisterRemove(
                    bind(self.handleLayerRemoval))

        self.imageIndex = imageIndex

        # Don't repopulate the GUI if there isn't a current dataset.  Stop now.
        if imageIndex is -1:
            self.layerstack.clear()
            return

        # Update the GUI for all layers in the current dataset
        self.updateAllLayers()

        # For layers that already exist, subscribe to ready notifications
        for provider in self.observedSlots:
            for slotIndex, slot in enumerate(provider):
                slot.notifyReady(bind(self.updateAllLayers))
                slot.notifyUnready(bind(self.updateAllLayers))

        # Make sure we're notified if a layer is inserted in the future so we can subscribe to its ready notifications
        for provider in self.observedSlots:
            if self.imageIndex < len(provider):
                provider[self.imageIndex].notifyInserted(
                    bind(self.handleLayerInsertion))
                provider[self.imageIndex].notifyRemoved(
                    bind(self.handleLayerRemoval))

    def handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        with Tracer(traceLogger):
            # When the slot is ready, we'll replace the blank layer with real data
            slot[slotIndex].notifyReady(bind(self.updateAllLayers))
            slot[slotIndex].notifyUnready(bind(self.updateAllLayers))

    def handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        with Tracer(traceLogger):
            self.updateAllLayers()

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @traceLogged(traceLogger)
    def createStandardLayerFromSlot(self, slot, lastChannelIsAlpha=False):
        """
        Generate a volumina layer using the given slot.
        Choose between grayscale or RGB depending on the number of channels.
        """
        def getRange(meta):
            if 'drange' in meta:
                return meta.drange
            if numpy.issubdtype(meta.dtype, numpy.integer):
                # We assume that ints range up to their max possible value,
                return (0, numpy.iinfo(meta.dtype).max)
            else:
                # If we don't know the range of the data, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                return 'autoPercentiles'

        # Examine channel dimension to determine Grayscale vs. RGB
        shape = slot.meta.shape
        normalize = getRange(slot.meta)
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
        except:
            numChannels = 1

        if lastChannelIsAlpha:
            assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(
                numChannels)
        else:
            assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha).  Your image has {} channels.".format(
                numChannels)

        if numChannels == 1:
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            source = LazyflowSource(slot)
            normSource = NormalizingSource(source, bounds=normalize)
            return GrayscaleLayer(normSource)

        assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha)
        redProvider = OpSingleChannelSelector(graph=slot.graph)
        redProvider.Input.connect(slot)
        redProvider.Index.setValue(0)
        redSource = LazyflowSource(redProvider.Output)
        redNormSource = NormalizingSource(redSource, bounds=normalize)

        greenProvider = OpSingleChannelSelector(graph=slot.graph)
        greenProvider.Input.connect(slot)
        greenProvider.Index.setValue(1)
        greenSource = LazyflowSource(greenProvider.Output)
        greenNormSource = NormalizingSource(greenSource, bounds=normalize)

        blueNormSource = None
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            blueProvider = OpSingleChannelSelector(graph=slot.graph)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue(2)
            blueSource = LazyflowSource(blueProvider.Output)
            blueNormSource = NormalizingSource(blueSource, bounds=normalize)

        alphaNormSource = None
        if lastChannelIsAlpha:
            alphaProvider = OpSingleChannelSelector(graph=slot.graph)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue(numChannels - 1)
            alphaSource = LazyflowSource(alphaProvider.Output)
            alphaNormSource = NormalizingSource(alphaSource, bounds=normalize)

        layer = RGBALayer(red=redNormSource,
                          green=greenNormSource,
                          blue=blueNormSource,
                          alpha=alphaNormSource)
        return layer

    @traceLogged(traceLogger)
    def areProvidersInSync(self):
        """
        When an image is appended to the workflow, not all slots are resized simultaneously.
        We should avoid calling setupLayers() until all the slots have been resized with the new image.
        """
        try:
            numImages = len(self.observedSlots[0])
        except IndexError:  # observedSlots is empty
            pass

        inSync = True
        for slot in self.observedSlots:
            # Check each slot for out-of-sync status except:
            # - slots that are optional and unconnected
            # - slots that are not images (e.g. a classifier or other object)
            if not (slot._optional and slot.partner is None):
                if len(slot) == 0:
                    inSync = False
                    break
                elif len(slot[0]) > 0 and slot[0][0].meta.axistags is not None:
                    inSync &= (len(slot) == numImages)
        return inSync

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self):
        # Check to make sure all layers are in sync
        # (During image insertions, outputs are resized one at a time.)
        if not self.areProvidersInSync():
            return

        if self.imageIndex >= 0:
            # Ask the subclass for the updated layer list
            newGuiLayers = self.setupLayers(self.imageIndex)
        else:
            newGuiLayers = []

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += [l.name for l in newGuiLayers]
            raise RuntimeError()

        # Copy the old visibilities and opacities
        if self.imageIndex != self.lastUpdateImageIndex:
            existing = {l.name: l for l in self.layerstack}
            for layer in newGuiLayers:
                if layer.name in existing.keys():
                    layer.visible = existing[layer.name].visible
                    layer.opacity = existing[layer.name].opacity

            # Clear all existing layers.
            self.layerstack.clear()
            self.lastUpdateImageIndex = self.imageIndex

            # Zoom at a 1-1 scale to avoid loading big datasets entirely...
            for view in self.editor.imageViews:
                view.doScaleTo(1)

        # If the datashape changed, tell the editor
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
            # Find the xyz midpoint
            midpos5d = [x / 2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2])

            # If one of the xyz dimensions is 1, the data is 2d.
            singletonDims = filter(lambda (i, dim): dim == 1,
                                   enumerate(newDataShape[1:4]))
            if len(singletonDims) == 1:
                # Maximize the slicing view for this axis
                axis = singletonDims[0][0]
                self.volumeEditorWidget.quadview.ensureMaximized(axis)

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name,
                                  newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister(obsoleteShortcut)
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert(index, layer)

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register(*layer.shortcutRegistration)
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct positon
                stackIndex = self.layerstack.findMatchingIndex(
                    lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    @traceLogged(traceLogger)
    def determineDatashape(self):
        if self.imageIndex < 0:
            return None

        newDataShape = None
        for provider in self.observedSlots:
            if self.imageIndex < len(provider):
                for i, slot in enumerate(provider[self.imageIndex]):
                    if newDataShape is None and slot.ready(
                    ) and slot.meta.axistags is not None:
                        # Use an Op5ifyer adapter to transpose the shape for us.
                        op5 = Op5ifyer(graph=slot.graph)
                        op5.input.connect(slot)
                        newDataShape = op5.output.meta.shape

                        # We just needed the operator to determine the transposed shape.
                        # Disconnect it so it can be garbage collected.
                        op5.input.disconnect()

        if newDataShape is not None:
            # For now, this base class combines multi-channel images into a single layer,
            # So, we want the volume editor to behave as though there is only one channel
            newDataShape = newDataShape[:-1] + (1, )
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = uic.loadUi(localDir +
                                                "/viewerControls.ui")

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir + "/centralWidget.ui", self)

        # Menu is specified in a separate ui file with a dummy window
        self.menuGui = uic.loadUi(
            localDir +
            "/menu.ui")  # Save as member so it doesn't get picked up by GC
        self.menuBar = self.menuGui.menuBar
        self.menuView = self.menuGui.menuView

        def toggleDebugPatches(show):
            self.editor.showDebugPatches = show

        def setCacheSize(cache_size):
            dlg = QDialog(self)
            layout = QHBoxLayout()
            layout.addWidget(QLabel("Cached Slices Per View:"))

            cache_size = [self.editor.cacheSize]

            def parseCacheSize(strSize):
                try:
                    cache_size[0] = int(strSize)
                except:
                    pass

            edit = QLineEdit(str(cache_size[0]), parent=dlg)
            edit.textChanged.connect(parseCacheSize)
            layout.addWidget(edit)
            okButton = QPushButton("OK", parent=dlg)
            okButton.clicked.connect(dlg.accept)
            layout.addWidget(okButton)
            dlg.setLayout(layout)
            dlg.setModal(True)
            dlg.exec_()
            self.editor.cacheSize = cache_size[0]

        def fitToScreen():
            shape = self.editor.posModel.shape
            for i, v in enumerate(self.editor.imageViews):
                s = list(shape)
                del s[i]
                v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0, 0,
                                                                     *s)))

        def fitImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[
                    self.editor._lastImageViewFocus].fitImage()

        def restoreImageToOriginalSize():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[
                    self.editor._lastImageViewFocus].doScaleTo()

        def rubberBandZoom():
            if hasattr(self.editor, '_lastImageViewFocus'):
                if not self.editor.imageViews[
                        self.editor._lastImageViewFocus]._isRubberBandZoom:
                    self.editor.imageViews[
                        self.editor.
                        _lastImageViewFocus]._isRubberBandZoom = True
                    self.editor.imageViews[
                        self.editor.
                        _lastImageViewFocus]._cursorBackup = self.editor.imageViews[
                            self.editor._lastImageViewFocus].cursor()
                    self.editor.imageViews[
                        self.editor._lastImageViewFocus].setCursor(
                            Qt.CrossCursor)
                else:
                    self.editor.imageViews[
                        self.editor.
                        _lastImageViewFocus]._isRubberBandZoom = False
                    self.editor.imageViews[
                        self.editor._lastImageViewFocus].setCursor(
                            self.editor.imageViews[
                                self.editor._lastImageViewFocus]._cursorBackup)

        def hideHud():
            hide = not self.editor.imageViews[0]._hud.isVisible()
            for i, v in enumerate(self.editor.imageViews):
                v.setHudVisible(hide)

        def toggleSelectedHud():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[
                    self.editor._lastImageViewFocus].toggleHud()

        def centerAllImages():
            for i, v in enumerate(self.editor.imageViews):
                v.centerImage()

        def centerImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[
                    self.editor._lastImageViewFocus].centerImage()
                self.actionOnly_for_current_view.setEnabled(True)

        self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages)
        self.menuGui.actionCenterImage.triggered.connect(centerImage)
        self.menuGui.actionToggleAllHuds.triggered.connect(hideHud)
        self.menuGui.actionToggleSelectedHud.triggered.connect(
            toggleSelectedHud)
        self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches)
        self.menuGui.actionFitToScreen.triggered.connect(fitToScreen)
        self.menuGui.actionFitImage.triggered.connect(fitImage)
        self.menuGui.actionReset_zoom.triggered.connect(
            restoreImageToOriginalSize)
        self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom)
        self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize)

    @traceLogged(traceLogger)
    def initEditor(self):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter(
            self.editor.navInterpret, self.editor.posModel)
        self.editor.setNavigationInterpreter(self.clickReporter)
        self.clickReporter.rightClickReceived.connect(
            self._handleEditorRightClick)
        self.clickReporter.leftClickReceived.connect(
            self._handleEditorLeftClick)

        self.editor.newImageView2DFocus.connect(self.setIconToViewMenu)
        self.editor.setInteractionMode('navigation')
        self.volumeEditorWidget.init(self.editor)

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            model.canMoveSelectedUp.connect(
                self.__viewerControlWidget.UpButton.setEnabled)
            model.canMoveSelectedDown.connect(
                self.__viewerControlWidget.DownButton.setEnabled)
            model.canDeleteSelected.connect(
                self.__viewerControlWidget.DeleteButton.setEnabled)

            # Connect our layer movement buttons to the appropriate layerstack actions
            self.__viewerControlWidget.layerWidget.init(model)
            self.__viewerControlWidget.UpButton.clicked.connect(
                model.moveSelectedUp)
            self.__viewerControlWidget.DownButton.clicked.connect(
                model.moveSelectedDown)
            self.__viewerControlWidget.DeleteButton.clicked.connect(
                model.deleteSelected)

        self.editor._lastImageViewFocus = 0

    @traceLogged(traceLogger)
    def setIconToViewMenu(self):
        """
        In the "Only for Current View" menu item of the View menu, 
        show the user which axis is the current one by changing the menu item icon.
        """
        self.actionOnly_for_current_view.setIcon(
            QIcon(self.editor.imageViews[
                self.editor._lastImageViewFocus]._hud.axisLabel.pixmap()))

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                if isinstance(datasource, NormalizingSource):
                    datasource = datasource._rawSource
                if isinstance(datasource, LazyflowSource):
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break

        assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key], )

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(self.imageIndex, dataPosition,
                                    globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(self.imageIndex, dataPosition,
                                   globalWindowCoordinate)

    def handleEditorRightClick(self, currentImageIndex, position5d,
                               globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, currentImageIndex, position5d,
                              globalWindowCoordiante):
        # Override me
        pass
class LayerViewerGui(QMainWindow):
    """
    Implements an applet GUI whose central widget is a VolumeEditor 
      and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.
    
    Provides: - Central widget (viewer)
              - View Menu
              - Layer controls
              
    Does NOT provide an applet drawer widget.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################
    
    def centralWidget( self ):
        return self

    def appletDrawers(self):
        return [('Viewer', QWidget())]

    def menus( self ):
        return [self.menuView] # From the .ui file

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def setImageIndex(self, index):
        self._setImageIndex(index)
        
    def reset(self):
        # Remove all layers
        self.layerstack.clear()
    
    ###########################################
    ###########################################

    def operatorForCurrentImage(self):
        try:
            return self.topLevelOperator[self.imageIndex]
        except IndexError:
            return None

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperator):
        """
        Args:
            observedSlots   - A list of slots that we'll listen for changes on.
                               Each must be a multislot with level=1 or level=2.
                               The first index in the multislot is the image index. 
        """
        super(LayerViewerGui, self).__init__()

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperator = topLevelOperator

        observedSlots = []

        for slot in topLevelOperator.inputs.values() + topLevelOperator.outputs.values():
            if slot.level == 1 or slot.level == 2:
                observedSlots.append(slot)
        
        self.observedSlots = []        
        for slot in observedSlots:
            if slot.level == 1:
                # The user gave us a slot that is indexed as slot[image]
                # Wrap the operator so it has the right level.  Indexed as: slot[image][0]
                opPromoteInput = OperatorWrapper( Op1ToMulti, graph=slot.operator.graph )
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Outputs

            # Each slot should now be indexed as slot[image][sub_index]
            assert slot.level == 2
            self.observedSlots.append( slot )

        self.layerstack = LayerStackModel()

        self.initAppletDrawerUi() # Default implementation loads a blank drawer.
        self.initCentralUic()
        self.__viewerControlWidget = None
        self.initViewerControlUi()
        
        self.initEditor()
        
        self.imageIndex = -1
        self.lastUpdateImageIndex = -1
        
        def handleDatasetInsertion(slot, imageIndex):
            if self.imageIndex == -1 and self.areProvidersInSync():
                self.setImageIndex( imageIndex )
        
        for provider in self.observedSlots:
            provider.notifyInserted( bind( handleDatasetInsertion ) )
        
        def handleDatasetRemoval(slot, index, finalsize):
            if finalsize == 0:
                # Clear everything
                self.setImageIndex(-1)
            elif index == self.imageIndex:
                # Our currently displayed image is being removed.
                # Switch to the first image (unless that's the one being removed!)
                newIndex = 0
                if index == newIndex:
                    newIndex = 1
                self.setImageIndex(newIndex)
            
        for provider in self.observedSlots:
            provider.notifyRemove( bind( handleDatasetRemoval ) )

    def setupLayers( self, currentImageIndex ):
        layers = []
        for multiImageSlot in self.observedSlots:
            if 0 <= currentImageIndex < len(multiImageSlot):
                multiLayerSlot = multiImageSlot[currentImageIndex]
                for j, slot in enumerate(multiLayerSlot):
                    if slot.ready():
                        layer = self.createStandardLayerFromSlot(slot)
                        layer.name = multiImageSlot.name + " " + str(j)
                        layers.append(layer)        
        return layers

    @traceLogged(traceLogger)
    def _setImageIndex(self, imageIndex):
        if self.imageIndex != -1:
            for provider in self.observedSlots:
                # We're switching datasets.  Unsubscribe from the old one's notifications.
                provider[self.imageIndex].unregisterInserted( bind(self.handleLayerInsertion) )
                provider[self.imageIndex].unregisterRemove( bind(self.handleLayerRemoval) )

        self.imageIndex = imageIndex
        
        # Don't repopulate the GUI if there isn't a current dataset.  Stop now. 
        if imageIndex is -1:
            self.layerstack.clear()
            return

        # Update the GUI for all layers in the current dataset
        self.updateAllLayers()

        # For layers that already exist, subscribe to ready notifications
        for provider in self.observedSlots:
            for slotIndex, slot in enumerate(provider):
                slot.notifyReady( bind(self.updateAllLayers) )
                slot.notifyUnready( bind(self.updateAllLayers) )
        
        # Make sure we're notified if a layer is inserted in the future so we can subscribe to its ready notifications
        for provider in self.observedSlots:
            if self.imageIndex < len(provider):
                provider[self.imageIndex].notifyInserted( bind(self.handleLayerInsertion) )
                provider[self.imageIndex].notifyRemoved( bind(self.handleLayerRemoval) )

    def handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        with Tracer(traceLogger):
            # When the slot is ready, we'll replace the blank layer with real data
            slot[slotIndex].notifyReady( bind(self.updateAllLayers) )
            slot[slotIndex].notifyUnready( bind(self.updateAllLayers) )
    
    def handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        with Tracer(traceLogger):
            self.updateAllLayers()

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @traceLogged(traceLogger)
    def createStandardLayerFromSlot(self, slot, lastChannelIsAlpha=False):
        """
        Generate a volumina layer using the given slot.
        Choose between grayscale or RGB depending on the number of channels.
        """
        def getRange(meta):
            if 'drange' in meta:
                return meta.drange
            if numpy.issubdtype(meta.dtype, numpy.integer):
                # We assume that ints range up to their max possible value,
                return (0, numpy.iinfo( meta.dtype ).max)
            else:
                # If we don't know the range of the data, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                return 'autoPercentiles'

        # Examine channel dimension to determine Grayscale vs. RGB
        shape = slot.meta.shape
        normalize = getRange(slot.meta)
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
        except:
            numChannels = 1
        
        if lastChannelIsAlpha:
            assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(numChannels)
        else:
            assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha).  Your image has {} channels.".format(numChannels)

        if numChannels == 1:
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            source = LazyflowSource(slot)
            normSource = NormalizingSource( source, bounds=normalize )
            return GrayscaleLayer(normSource)

        assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha)
        redProvider = OpSingleChannelSelector(graph=slot.graph)
        redProvider.Input.connect(slot)
        redProvider.Index.setValue( 0 )
        redSource = LazyflowSource( redProvider.Output )
        redNormSource = NormalizingSource( redSource, bounds=normalize )
        
        greenProvider = OpSingleChannelSelector(graph=slot.graph)
        greenProvider.Input.connect(slot)
        greenProvider.Index.setValue( 1 )
        greenSource = LazyflowSource( greenProvider.Output )
        greenNormSource = NormalizingSource( greenSource, bounds=normalize )
                        
        blueNormSource = None
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            blueProvider = OpSingleChannelSelector(graph=slot.graph)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue( 2 )
            blueSource = LazyflowSource( blueProvider.Output )
            blueNormSource = NormalizingSource( blueSource, bounds=normalize )

        alphaNormSource = None
        if lastChannelIsAlpha:
            alphaProvider = OpSingleChannelSelector(graph=slot.graph)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( numChannels-1 )
            alphaSource = LazyflowSource( alphaProvider.Output )
            alphaNormSource = NormalizingSource( alphaSource, bounds=normalize )
        
        layer = RGBALayer( red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource )
        return layer

    @traceLogged(traceLogger)
    def areProvidersInSync(self):
        """
        When an image is appended to the workflow, not all slots are resized simultaneously.
        We should avoid calling setupLayers() until all the slots have been resized with the new image.
        """
        try:
            numImages = len(self.observedSlots[0])
        except IndexError: # observedSlots is empty
            pass

        inSync = True
        for slot in self.observedSlots:
            # Check each slot for out-of-sync status except:
            # - slots that are optional and unconnected
            # - slots that are not images (e.g. a classifier or other object)
            if not (slot._optional and slot.partner is None):
                if len(slot) == 0:
                    inSync = False
                    break
                elif len(slot[0]) > 0 and slot[0][0].meta.axistags is not None:
                    inSync &= (len(slot) == numImages)
        return inSync

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self):
        # Check to make sure all layers are in sync
        # (During image insertions, outputs are resized one at a time.)
        if not self.areProvidersInSync():
            return

        if self.imageIndex >= 0:        
            # Ask the subclass for the updated layer list
            newGuiLayers = self.setupLayers(self.imageIndex)
        else:
            newGuiLayers = []
            
        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += [l.name for l in newGuiLayers]
            raise RuntimeError()

        # Copy the old visibilities and opacities
        if self.imageIndex != self.lastUpdateImageIndex:
            existing = {l.name : l for l in self.layerstack}
            for layer in newGuiLayers:
                if layer.name in existing.keys():
                    layer.visible = existing[layer.name].visible
                    layer.opacity = existing[layer.name].opacity

            # Clear all existing layers.
            self.layerstack.clear()
            self.lastUpdateImageIndex = self.imageIndex

            # Zoom at a 1-1 scale to avoid loading big datasets entirely...
            for view in self.editor.imageViews:
                view.doScaleTo(1)
        
        # If the datashape changed, tell the editor
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
            # Find the xyz midpoint
            midpos5d = [x/2 for x in newDataShape]
            midpos3d = midpos5d[1:4]
            
            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] )
            
            # If one of the xyz dimensions is 1, the data is 2d.
            singletonDims = filter( lambda (i,dim): dim == 1, enumerate(newDataShape[1:4]) )
            if len(singletonDims) == 1:
                # Maximize the slicing view for this axis
                axis = singletonDims[0][0]
                self.volumeEditorWidget.quadview.ensureMaximized(axis)

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister( obsoleteShortcut )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )
                
                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct positon
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1
                
    @traceLogged(traceLogger)
    def determineDatashape(self):
        if self.imageIndex < 0:
            return None

        newDataShape = None
        for provider in self.observedSlots:
            if self.imageIndex < len(provider):
                for i, slot in enumerate(provider[self.imageIndex]):
                    if newDataShape is None and slot.ready() and slot.meta.axistags is not None:
                        # Use an Op5ifyer adapter to transpose the shape for us.
                        op5 = Op5ifyer( graph=slot.graph )
                        op5.input.connect( slot )
                        newDataShape = op5.output.meta.shape
    
                        # We just needed the operator to determine the transposed shape.
                        # Disconnect it so it can be garbage collected.
                        op5.input.disconnect()

        if newDataShape is not None:
            # For now, this base class combines multi-channel images into a single layer,
            # So, we want the volume editor to behave as though there is only one channel 
            newDataShape = newDataShape[:-1] + (1,)
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = uic.loadUi(localDir + "/viewerControls.ui")

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")
    
    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

        # Menu is specified in a separate ui file with a dummy window
        self.menuGui = uic.loadUi(localDir+"/menu.ui") # Save as member so it doesn't get picked up by GC
        self.menuBar = self.menuGui.menuBar
        self.menuView = self.menuGui.menuView
            
        def toggleDebugPatches(show):
            self.editor.showDebugPatches = show
        
        def setCacheSize( cache_size ):
            dlg = QDialog(self)
            layout = QHBoxLayout()
            layout.addWidget( QLabel("Cached Slices Per View:") )

            cache_size = [self.editor.cacheSize]
            def parseCacheSize( strSize ):
                try:
                    cache_size[0] = int(strSize)
                except:
                    pass
                
            edit = QLineEdit( str(cache_size[0]), parent=dlg )
            edit.textChanged.connect( parseCacheSize )
            layout.addWidget( edit )
            okButton = QPushButton( "OK", parent=dlg )
            okButton.clicked.connect( dlg.accept )
            layout.addWidget( okButton )
            dlg.setLayout( layout )
            dlg.setModal(True)
            dlg.exec_()
            self.editor.cacheSize = cache_size[0]
        
        def fitToScreen():
            shape = self.editor.posModel.shape
            for i, v in enumerate(self.editor.imageViews):
                s = list(shape)
                del s[i]
                v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s)))  
                
        def fitImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].fitImage()
                
        def restoreImageToOriginalSize():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo()
                    
        def rubberBandZoom():
            if hasattr(self.editor, '_lastImageViewFocus'):
                if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True
                    self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor()
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor)
                else:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup)
                
        def hideHud():
            hide = not self.editor.imageViews[0]._hud.isVisible()
            for i, v in enumerate(self.editor.imageViews):
                v.setHudVisible(hide)
                
        def toggleSelectedHud():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud()
                
        def centerAllImages():
            for i, v in enumerate(self.editor.imageViews):
                v.centerImage()
                
        def centerImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].centerImage()
                self.actionOnly_for_current_view.setEnabled(True)
        
        self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages)
        self.menuGui.actionCenterImage.triggered.connect(centerImage)
        self.menuGui.actionToggleAllHuds.triggered.connect(hideHud)
        self.menuGui.actionToggleSelectedHud.triggered.connect(toggleSelectedHud)
        self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches)
        self.menuGui.actionFitToScreen.triggered.connect(fitToScreen)
        self.menuGui.actionFitImage.triggered.connect(fitImage)
        self.menuGui.actionReset_zoom.triggered.connect(restoreImageToOriginalSize)
        self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom)        
        self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize)
                
    @traceLogged(traceLogger)
    def initEditor(self):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )

        self.editor.newImageView2DFocus.connect(self.setIconToViewMenu)
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)
        
        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            model.canMoveSelectedUp.connect(self.__viewerControlWidget.UpButton.setEnabled)
            model.canMoveSelectedDown.connect(self.__viewerControlWidget.DownButton.setEnabled)
            model.canDeleteSelected.connect(self.__viewerControlWidget.DeleteButton.setEnabled)     

            # Connect our layer movement buttons to the appropriate layerstack actions
            self.__viewerControlWidget.layerWidget.init(model)
            self.__viewerControlWidget.UpButton.clicked.connect(model.moveSelectedUp)
            self.__viewerControlWidget.DownButton.clicked.connect(model.moveSelectedDown)
            self.__viewerControlWidget.DeleteButton.clicked.connect(model.deleteSelected)
        
        self.editor._lastImageViewFocus = 0

    @traceLogged(traceLogger)
    def setIconToViewMenu(self):
        """
        In the "Only for Current View" menu item of the View menu, 
        show the user which axis is the current one by changing the menu item icon.
        """
        self.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap()))

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}
        
        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                if isinstance( datasource, NormalizingSource ):
                    datasource = datasource._rawSource
                if isinstance(datasource, LazyflowSource):
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break

        assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)
            
        return position
    
    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(self.imageIndex, dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(self.imageIndex, dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, currentImageIndex, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, currentImageIndex, position5d, globalWindowCoordiante):
        # Override me
        pass
Esempio n. 6
0
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus(self):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [self.volumeEditorWidget.getViewMenu(debug_mode)]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Unsubscribe to all signals
        for fn in self.__cleanup_fns:
            fn()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()

        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    def __init__(self,
                 topLevelOperatorView,
                 additionalMonitoredSlots=[],
                 centralWidgetOnly=False,
                 crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False
        self.__cleanup_fns = []

        self.threadRouter = ThreadRouter(self)  # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values(
        ) + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)

        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [
        ]  # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(
                    parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append(opPromoteInput)

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append(slot)
            slot.notifyInserted(bind(self._handleLayerInsertion))
            self.__cleanup_fns.append(
                partial(slot.unregisterInserted,
                        bind(self._handleLayerInsertion)))

            slot.notifyRemoved(bind(self._handleLayerRemoval))
            self.__cleanup_fns.append(
                partial(slot.unregisterRemoved,
                        bind(self._handleLayerRemoval)))

            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)

        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi(
            )  # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi(
            )  # Default implementation loads a blank drawer from drawer.ui.

    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers(self):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)

                    # Name the layer after the slot name.
                    if isinstance(multiLayerSlot.getRealOperator(),
                                  OpWrapSlot):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator(
                        ).Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady(bind(self.updateAllLayers))
        slot[slotIndex].notifyUnready(bind(self.updateAllLayers))

        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterReady,
                    bind(self.updateAllLayers)))
        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterUnready,
                    bind(self.updateAllLayers)))

    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        def getRange(meta):
            return meta.drange

        def getNormalize(meta):
            if meta.drange is not None and meta.normalizeDisplay is False:
                # do not normalize if the user provided a range and set normalization to False
                return False
            else:
                # If we don't know the range of the data and normalization is allowed
                # by the user, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return None  # means autoNormalize

        shape = slot.meta.shape

        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
            axisinfo = slot.meta.axistags["c"].description
        except:
            numChannels = 1
            axisinfo = ""  # == no info on channels given

        rindex = None
        bindex = None
        gindex = None
        aindex = None

        if axisinfo == "" or axisinfo == "default":
            # Examine channel dimension to determine Grayscale vs. RGB

            if numChannels == 4:
                lastChannelIsAlpha = True

            if lastChannelIsAlpha:
                assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(
                    numChannels)

            if numChannels == 1 or (numChannels > 4):
                assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
                source = LazyflowSource(slot)
                layer = GrayscaleLayer(source)
                layer.numberOfChannels = numChannels
                normalize = getNormalize(slot.meta)
                range = getRange(slot.meta)
                layer.set_range(0, range)
                layer.set_normalize(0, normalize)
                return layer

            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )

            rindex = 0
            gindex = 1
            if numChannels > 3 or (numChannels == 3
                                   and not lastChannelIsAlpha):
                bindex = 2
            if lastChannelIsAlpha:
                aindex = numChannels - 1

        elif axisinfo == "grayscale":
            source = LazyflowSource(slot)
            layer = GrayscaleLayer(source)
            layer.numberOfChannels = numChannels
            normalize = getNormalize(slot.meta)
            range = getRange(slot.meta)
            layer.set_range(0, range)
            layer.set_normalize(0, normalize)
            return layer

        elif axisinfo == "rgba":
            rindex = 0
            if numChannels >= 2:
                gindex = 1
            if numChannels >= 3:
                bindex = 2
            if numChannels >= 4:
                aindex = numChannels - 1
        else:
            raise RuntimeError("unknown channel display mode")

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue(rindex)
            redSource = LazyflowSource(redProvider.Output)
            redSource.additional_owned_ops.append(redProvider)

        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue(gindex)
            greenSource = LazyflowSource(greenProvider.Output)
            greenSource.additional_owned_ops.append(greenProvider)

        blueSource = None
        if bindex is not None:
            blueProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue(bindex)
            blueSource = LazyflowSource(blueProvider.Output)
            blueSource.additional_owned_ops.append(blueProvider)

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue(aindex)
            alphaSource = LazyflowSource(alphaProvider.Output)
            alphaSource.additional_owned_ops.append(alphaProvider)

        layer = RGBALayer(red=redSource,
                          green=greenSource,
                          blue=blueSource,
                          alpha=alphaSource)

        normalize = getNormalize(slot.meta)
        range = getRange(slot.meta)
        for i in xrange(4):
            if [redSource, greenSource, blueSource, alphaSource][i]:
                layer.set_range(i, range)
                layer.set_normalize(i, normalize)

        return layer

    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        for layer in newGuiLayers:
            assert not filter( lambda l: l is layer, self.layerstack ), \
                "You are attempting to re-use a layer ({}).  " \
                "Your setupOutputs() function may not re-use layer objects.  " \
                "The layerstack retains ownership of the layers you provide and " \
                "may choose to clean and delete them without your knowledge.".format( layer.name )

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str([l.name for l in newGuiLayers])
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape

            # Find the xyz midpoint
            midpos5d = [x / 2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2])

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name,
                                  newGuiLayers)[0]
                needDelete = newLayer.isDifferentEnough(oldLayer)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister(obsoleteShortcut)
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert(index, layer)

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register(*layer.shortcutRegistration)
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)
                    layer.clean_up()

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(
                    lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None:
                    newDataShape = self.getVoluminaShapeForSlot(slot)
        return newDataShape

    @classmethod
    def getVoluminaShapeForSlot(self, slot):
        shape = None
        if slot.ready() and slot.meta.axistags is not None:
            # Use an OpReorderAxes adapter to transpose the shape for us.
            op5 = OpReorderAxes(parent=slot.getRealOperator().parent)
            op5.Input.connect(slot)
            shape = op5.Output.meta.shape

            # We just needed the operator to determine the transposed shape.
            # Disconnect it so it can be garbage collected.
            op5.Input.disconnect()
            op5.cleanUp()
        return shape

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir + "/centralWidget.ui", self)

    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter(
            self.editor.navInterpret, self.editor.posModel)
        self.editor.setNavigationInterpreter(self.clickReporter)
        self.clickReporter.rightClickReceived.connect(
            self._handleEditorRightClick)
        self.clickReporter.leftClickReceived.connect(
            self._handleEditorLeftClick)

        clickReporter2 = ClickReportingInterpreter(
            self.editor.brushingInterpreter, self.editor.posModel)
        clickReporter2.rightClickReceived.connect(self._handleEditorRightClick)
        self.editor.brushingInterpreter = clickReporter2

        self.editor.setInteractionMode('navigation')
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try:  # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if (dataTags is None):
            raise RuntimeError(
                "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
            )
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key], )

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass