Exemple #1
0
class PlotImage(FigureCanvas):
    def __init__(self, model, parent, main):

        super(FigureCanvas, self).__init__(Figure())

        FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)

        FigureCanvas.updateGeometry(self)

        self.model = model
        self.mw = main
        self.parent = parent

        self.rubber_band = QRubberBand(QRubberBand.Rectangle, self)
        self.band_origin = QtCore.QPoint()
        self.x_plot_origin = None
        self.y_plot_origin = None

        self.menu = QMenu(self)

    def enterEvent(self, event):
        self.setCursor(QtCore.Qt.CrossCursor)
        self.mw.coord_label.show()

    def leaveEvent(self, event):
        self.mw.coord_label.hide()
        self.mw.statusBar().showMessage("")

    def mousePressEvent(self, event):
        self.mw.coord_label.hide()

        # Set rubber band absolute and relative position
        self.band_origin = event.pos()
        self.x_plot_origin, self.y_plot_origin = self.getPlotCoords(
            event.pos())

        # Create rubber band
        self.rubber_band.setGeometry(
            QtCore.QRect(self.band_origin, QtCore.QSize()))

        FigureCanvas.mousePressEvent(self, event)

    def getPlotCoords(self, pos):

        cv = self.model.currentView

        # get the normalized axis coordinates from the event display units
        xPlotCoord, yPlotCoord = self.ax.transAxes.inverted().transform(
            (pos.x(), pos.y()))
        # flip the y-axis (its zero is in the upper left)
        yPlotCoord = 1 - yPlotCoord

        # scale axes using the plot extents
        xPlotCoord = self.ax.dataLim.x0 + xPlotCoord * self.ax.dataLim.width
        yPlotCoord = self.ax.dataLim.y0 + yPlotCoord * self.ax.dataLim.height

        # set coordinate label if pointer is in the axes
        if self.ax.contains_point((pos.x(), pos.y())):
            self.mw.coord_label.show()
            self.mw.showCoords(xPlotCoord, yPlotCoord)
        else:
            self.mw.coord_label.hide()

        return (xPlotCoord, yPlotCoord)

    def getIDinfo(self, event):

        cv = self.model.currentView

        # get origin in axes coordinates
        x0, y0 = self.ax.transAxes.transform((0., 0.))

        # get the extents of the axes box in axes coordinates
        bbox = self.ax.get_window_extent().transformed(
            self.figure.dpi_scale_trans.inverted())
        # get dimensions and scale using dpi
        width, height = bbox.width, bbox.height
        width *= self.figure.dpi
        height *= self.figure.dpi

        # use factor to get proper x,y position in pixels
        factor = (width / cv.h_res, height / cv.v_res)
        xPos = int((event.pos().x() - x0 + 0.05) / factor[0])
        yPos = int((event.pos().y() - y0 + 0.05) / factor[1])

        # check that the position is in the axes view
        if yPos < self.model.currentView.v_res \
            and xPos < self.model.currentView.h_res:
            id = f"{self.model.ids[yPos][xPos]}"
            temp = f"{self.model.props[yPos][xPos][0]:g}"
            density = f"{self.model.props[yPos][xPos][1]:g}"
        else:
            id = '-1'
            density = '-1'
            temp = '-1'

        if self.model.currentView.colorby == 'cell':
            domain = self.model.activeView.cells
            domain_kind = 'Cell'
        else:
            domain = self.model.activeView.materials
            domain_kind = 'Material'

        properties = {'density': density, 'temperature': temp}

        return id, properties, domain, domain_kind

    def mouseDoubleClickEvent(self, event):

        xCenter, yCenter = self.getPlotCoords(event.pos())
        self.mw.editPlotOrigin(xCenter, yCenter, apply=True)

        FigureCanvas.mouseDoubleClickEvent(self, event)

    def mouseMoveEvent(self, event):

        # Show Cursor position relative to plot in status bar
        xPlotPos, yPlotPos = self.getPlotCoords(event.pos())

        # Show Cell/Material ID, Name in status bar
        id, properties, domain, domain_kind = self.getIDinfo(event)
        if self.ax.contains_point((event.pos().x(), event.pos().y())):

            if id != str(_NOT_FOUND_) and domain[id].name:
                domainInfo = (f"{domain_kind} {id}: \"{domain[id].name}\"\t "
                              f"Density: {properties['density']} g/cm3\t"
                              f"Temperature: {properties['temperature']} K")
            elif id != str(_NOT_FOUND_):
                domainInfo = (f"{domain_kind} {id}\t"
                              f"Density: {properties['density']} g/cm3\t"
                              f"Temperature: {properties['temperature']} K")

            else:
                domainInfo = ""
        else:
            domainInfo = ""

        self.mw.statusBar().showMessage(f" {domainInfo}")

        # Update rubber band and values if mouse button held down
        if event.buttons() == QtCore.Qt.LeftButton:
            self.rubber_band.setGeometry(
                QtCore.QRect(self.band_origin, event.pos()).normalized())

            # Show rubber band if both dimensions > 10 pixels
            if self.rubber_band.width() > 10 and self.rubber_band.height(
            ) > 10:
                self.rubber_band.show()
            else:
                self.rubber_band.hide()

            # Update plot X Origin
            xCenter = (self.x_plot_origin + xPlotPos) / 2
            yCenter = (self.y_plot_origin + yPlotPos) / 2
            self.mw.editPlotOrigin(xCenter, yCenter)

            modifiers = event.modifiers()

            # Zoom out if Shift held
            if modifiers == QtCore.Qt.ShiftModifier:
                cv = self.model.currentView
                bandwidth = abs(self.band_origin.x() - event.pos().x())
                width = cv.width * (cv.h_res / max(bandwidth, .001))
                bandheight = abs(self.band_origin.y() - event.pos().y())
                height = cv.height * (cv.v_res / max(bandheight, .001))
            else:  # Zoom in
                width = max(abs(self.x_plot_origin - xPlotPos), 0.1)
                height = max(abs(self.y_plot_origin - yPlotPos), 0.1)

            self.mw.editWidth(width)
            self.mw.editHeight(height)

    def mouseReleaseEvent(self, event):

        if self.rubber_band.isVisible():
            self.rubber_band.hide()
            self.mw.applyChanges()
        else:
            self.mw.revertDockControls()

    def wheelEvent(self, event):

        if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier:
            numDegrees = event.delta() / 8

            if 24 < self.mw.zoom + numDegrees < 5001:
                self.mw.editZoom(self.mw.zoom + numDegrees)

    def contextMenuEvent(self, event):

        self.menu.clear()

        self.mw.undoAction.setText(f'&Undo ({len(self.model.previousViews)})')
        self.mw.redoAction.setText(
            f'&Redo ({len(self.model.subsequentViews)})')

        id, properties, domain, domain_kind = self.getIDinfo(event)

        if id != '-1':

            # Domain ID
            domainID = self.menu.addAction(f"{domain_kind} {id}")
            domainID.setDisabled(True)

            # Domain Name (if any)
            if domain[id].name:
                domainName = self.menu.addAction(domain[id].name)
                domainName.setDisabled(True)

            self.menu.addSeparator()
            self.menu.addAction(self.mw.undoAction)
            self.menu.addAction(self.mw.redoAction)
            self.menu.addSeparator()

            colorAction = self.menu.addAction(f'Edit {domain_kind} Color...')
            colorAction.setDisabled(self.model.currentView.highlighting)
            colorAction.setToolTip(f'Edit {domain_kind} color')
            colorAction.setStatusTip(f'Edit {domain_kind} color')
            colorAction.triggered.connect(
                lambda: self.mw.editDomainColor(domain_kind, id))

            maskAction = self.menu.addAction(f'Mask {domain_kind}')
            maskAction.setCheckable(True)
            maskAction.setChecked(domain[id].masked)
            maskAction.setDisabled(not self.model.currentView.masking)
            maskAction.setToolTip(f'Toggle {domain_kind} mask')
            maskAction.setStatusTip(f'Toggle {domain_kind} mask')
            maskAction.triggered[bool].connect(
                lambda bool=bool: self.mw.toggleDomainMask(
                    bool, domain_kind, id))

            highlightAction = self.menu.addAction(f'Highlight {domain_kind}')
            highlightAction.setCheckable(True)
            highlightAction.setChecked(domain[id].highlighted)
            highlightAction.setDisabled(
                not self.model.currentView.highlighting)
            highlightAction.setToolTip(f'Toggle {domain_kind} highlight')
            highlightAction.setStatusTip(f'Toggle {domain_kind} highlight')
            highlightAction.triggered[bool].connect(
                lambda bool=bool: self.mw.toggleDomainHighlight(
                    bool, domain_kind, id))

        else:
            self.menu.addAction(self.mw.undoAction)
            self.menu.addAction(self.mw.redoAction)
            self.menu.addSeparator()
            bgColorAction = self.menu.addAction('Edit Background Color...')
            bgColorAction.setToolTip('Edit background color')
            bgColorAction.setStatusTip('Edit plot background color')
            bgColorAction.triggered.connect(
                lambda: self.mw.editBackgroundColor(apply=True))

        self.menu.addSeparator()
        self.menu.addAction(self.mw.saveImageAction)
        self.menu.addAction(self.mw.saveViewAction)
        self.menu.addAction(self.mw.openAction)
        self.menu.addSeparator()
        self.menu.addMenu(self.mw.basisMenu)
        self.menu.addMenu(self.mw.colorbyMenu)
        self.menu.addSeparator()
        self.menu.addAction(self.mw.maskingAction)
        self.menu.addAction(self.mw.highlightingAct)
        self.menu.addSeparator()
        self.menu.addAction(self.mw.dockAction)

        self.mw.maskingAction.setChecked(self.model.currentView.masking)
        self.mw.highlightingAct.setChecked(self.model.currentView.highlighting)

        if self.mw.dock.isVisible():
            self.mw.dockAction.setText('Hide &Dock')
        else:
            self.mw.dockAction.setText('Show &Dock')

        self.menu.exec_(event.globalPos())

    def setPixmap(self, w, h):

        # clear out figure
        self.figure.clear()

        cv = self.model.currentView
        # set figure bg color to match window
        window_background = self.parent.palette().color(
            QtGui.QPalette.Background)
        self.figure.patch.set_facecolor(
            rgb_normalize(window_background.getRgb()))
        # set figure width
        self.figure.set_figwidth(0.99 * w / self.figure.get_dpi())
        self.figure.set_figheight(0.99 * h / self.figure.get_dpi())
        # set data extents for automatic reporting of pointer location
        data_bounds = [
            cv.origin[self.mw.xBasis] - cv.width / 2.,
            cv.origin[self.mw.xBasis] + cv.width / 2.,
            cv.origin[self.mw.yBasis] - cv.height / 2.,
            cv.origin[self.mw.yBasis] + cv.height / 2.
        ]

        # make sure we have an image to load
        if not hasattr(self.model, 'image'):
            self.model.generatePlot()
        c = self.figure.subplots().imshow(self.model.image,
                                          extent=data_bounds,
                                          alpha=cv.plotAlpha)
        self.ax = self.figure.axes[0]
        self.ax.margins(0.0, 0.0)
        self.figure.set_tight_layout({'pad': 1.0})
        # set axis labels
        axis_label_str = "{} (cm)"
        self.ax.set_xlabel(axis_label_str.format(cv.basis[0]))
        self.ax.set_ylabel(axis_label_str.format(cv.basis[1]))
        self.draw()
Exemple #2
0
class PlotImage(FigureCanvas):
    def __init__(self, model, parent, main_window):

        self.figure = Figure(dpi=main_window.logicalDpiX())
        super().__init__(self.figure)

        FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)

        FigureCanvas.updateGeometry(self)
        self.model = model
        self.main_window = main_window
        self.parent = parent

        self.rubber_band = QRubberBand(QRubberBand.Rectangle, self)
        self.band_origin = QtCore.QPoint()
        self.x_plot_origin = None
        self.y_plot_origin = None

        self.colorbar = None
        self.data_indicator = None
        self.tally_data_indicator = None
        self.image = None

        self.menu = QMenu(self)

    def enterEvent(self, event):
        self.setCursor(QtCore.Qt.CrossCursor)
        self.main_window.coord_label.show()

    def leaveEvent(self, event):
        self.main_window.coord_label.hide()
        self.main_window.statusBar().showMessage("")

    def mousePressEvent(self, event):
        self.main_window.coord_label.hide()
        position = event.pos()
        # Set rubber band absolute and relative position
        self.band_origin = position
        self.x_plot_origin, self.y_plot_origin = self.getPlotCoords(position)

        # Create rubber band
        self.rubber_band.setGeometry(
            QtCore.QRect(self.band_origin, QtCore.QSize()))

    def getPlotCoords(self, pos):
        x, y = self.mouseEventCoords(pos)

        # get the normalized axis coordinates from the event display units
        transform = self.ax.transAxes.inverted()
        xPlotCoord, yPlotCoord = transform.transform((x, y))
        # flip the y-axis (its zero is in the upper left)

        # scale axes using the plot extents
        xPlotCoord = self.ax.dataLim.x0 + xPlotCoord * self.ax.dataLim.width
        yPlotCoord = self.ax.dataLim.y0 + yPlotCoord * self.ax.dataLim.height

        # set coordinate label if pointer is in the axes
        if self.parent.underMouse():
            self.main_window.coord_label.show()
            self.main_window.showCoords(xPlotCoord, yPlotCoord)
        else:
            self.main_window.coord_label.hide()

        return (xPlotCoord, yPlotCoord)

    def _resize(self):
        z = self.main_window.zoom / 100.0
        # manage scroll bars
        if z <= 1.0:
            self.parent.verticalScrollBar().hide()
            self.parent.horizontalScrollBar().hide()
            self.parent.cornerWidget().hide()
            self.parent.verticalScrollBar().setEnabled(False)
            self.parent.horizontalScrollBar().setEnabled(False)
        else:
            self.parent.verticalScrollBar().show()
            self.parent.horizontalScrollBar().show()
            self.parent.cornerWidget().show()
            self.parent.verticalScrollBar().setEnabled(True)
            self.parent.horizontalScrollBar().setEnabled(True)

        # resize plot
        self.resize(self.parent.width() * z, self.parent.height() * z)

    def getDataIndices(self, event):
        cv = self.model.currentView

        x, y = self.mouseEventCoords(event.pos())

        # get origin in axes coordinates
        x0, y0 = self.ax.transAxes.transform((0.0, 0.0))

        # get the extents of the axes box in axes coordinates
        bbox = self.ax.get_window_extent().transformed(
            self.figure.dpi_scale_trans.inverted())
        # get dimensions and scale using dpi
        width, height = bbox.width, bbox.height
        width *= self.figure.dpi
        height *= self.figure.dpi

        # use factor to get proper x,y position in pixels
        factor = (width / cv.h_res, height / cv.v_res)
        xPos = int((x - x0 + 0.01) / factor[0])
        # flip y-axis
        yPos = cv.v_res - int((y - y0 + 0.01) / factor[1])

        return xPos, yPos

    def getTallyIndices(self, event):

        xPos, yPos = self.getPlotCoords(event.pos())

        ext = self.model.tally_extents

        x0 = ext[0]
        y0 = ext[2]

        v_res, h_res = self.model.tally_data.shape

        dx = (ext[1] - ext[0]) / h_res
        dy = (ext[3] - ext[2]) / v_res

        i = int((xPos - x0) // dx)
        j = v_res - int((yPos - y0) // dy) - 1

        return i, j

    def getTallyInfo(self, event):
        cv = self.model.currentView

        xPos, yPos = self.getTallyIndices(event)

        if self.model.tally_data is None:
            return -1, None

        if not cv.selectedTally or not cv.tallyDataVisible:
            return -1, None

        # don't look up mesh filter data (for now)
        tally = self.model.statepoint.tallies[cv.selectedTally]

        # check that the position is in the axes view
        v_res, h_res = self.model.tally_data.shape
        if 0 <= yPos < v_res and 0 <= xPos < h_res:
            value = self.model.tally_data[yPos][xPos]
        else:
            value = None

        return cv.selectedTally, value

    def getIDinfo(self, event):

        xPos, yPos = self.getDataIndices(event)

        # check that the position is in the axes view
        if 0 <= yPos < self.model.currentView.v_res \
           and 0 <= xPos and xPos < self.model.currentView.h_res:
            id = self.model.ids[yPos][xPos]
            temp = "{:g}".format(self.model.properties[yPos][xPos][0])
            density = "{:g}".format(self.model.properties[yPos][xPos][1])
        else:
            id = _NOT_FOUND
            density = str(_NOT_FOUND)
            temp = str(_NOT_FOUND)

        if self.model.currentView.colorby == 'cell':
            domain = self.model.activeView.cells
            domain_kind = 'Cell'
        elif self.model.currentView.colorby == 'temperature':
            domain = self.model.activeView.materials
            domain_kind = 'Temperature'
        elif self.model.currentView.colorby == 'density':
            domain = self.model.activeView.materials
            domain_kind = 'Density'
        else:
            domain = self.model.activeView.materials
            domain_kind = 'Material'

        properties = {'density': density, 'temperature': temp}

        return id, properties, domain, domain_kind

    def mouseDoubleClickEvent(self, event):
        xCenter, yCenter = self.getPlotCoords(event.pos())
        self.main_window.editPlotOrigin(xCenter, yCenter, apply=True)

    def mouseMoveEvent(self, event):
        cv = self.model.currentView
        # Show Cursor position relative to plot in status bar
        xPlotPos, yPlotPos = self.getPlotCoords(event.pos())

        # Show Cell/Material ID, Name in status bar
        id, properties, domain, domain_kind = self.getIDinfo(event)

        domainInfo = ""
        tallyInfo = ""

        if self.parent.underMouse():

            if domain_kind.lower() in _MODEL_PROPERTIES:
                line_val = float(properties[domain_kind.lower()])
                line_val = max(line_val, 0.0)
                self.updateDataIndicatorValue(line_val)
                domain_kind = 'Material'

            temperature = properties['temperature']
            density = properties['density']

            if id == _VOID_REGION:
                domainInfo = ("VOID")
            elif id == _OVERLAP:
                domainInfo = ("OVERLAP")
            elif id != _NOT_FOUND and domain[id].name:
                domainInfo = ("{} {}: \"{}\"\t Density: {} g/cc\t"
                              "Temperature: {} K".format(
                                  domain_kind, id, domain[id].name, density,
                                  temperature))
            elif id != _NOT_FOUND:
                domainInfo = ("{} {}\t Density: {} g/cc\t"
                              "Temperature: {} K".format(
                                  domain_kind, id, density, temperature))
            else:
                domainInfo = ""

            if self.model.tally_data is not None:
                tid, value = self.getTallyInfo(event)
                if value is not None and value != np.nan:
                    self.updateTallyDataIndicatorValue(value)
                    tallyInfo = "Tally {} {}: {:.5E}".format(
                        tid, cv.tallyValue, value)
                else:
                    self.updateTallyDataIndicatorValue(0.0)
        else:
            self.updateTallyDataIndicatorValue(0.0)
            self.updateDataIndicatorValue(0.0)

        if domainInfo:
            self.main_window.statusBar().showMessage(" " + domainInfo +
                                                     "      " + tallyInfo)
        else:
            self.main_window.statusBar().showMessage(" " + tallyInfo)

        # Update rubber band and values if mouse button held down
        if event.buttons() == QtCore.Qt.LeftButton:
            self.rubber_band.setGeometry(
                QtCore.QRect(self.band_origin, event.pos()).normalized())

            # Show rubber band if both dimensions > 10 pixels
            if self.rubber_band.width() > 10 and self.rubber_band.height(
            ) > 10:
                self.rubber_band.show()
            else:
                self.rubber_band.hide()

            # Update plot X Origin
            xCenter = (self.x_plot_origin + xPlotPos) / 2
            yCenter = (self.y_plot_origin + yPlotPos) / 2
            self.main_window.editPlotOrigin(xCenter, yCenter)

            modifiers = event.modifiers()

            # Zoom out if Shift held
            if modifiers == QtCore.Qt.ShiftModifier:
                cv = self.model.currentView
                bandwidth = abs(self.band_origin.x() - event.pos().x())
                width = cv.width * (cv.h_res / max(bandwidth, .001))
                bandheight = abs(self.band_origin.y() - event.pos().y())
                height = cv.height * (cv.v_res / max(bandheight, .001))
            # Zoom in
            else:
                width = max(abs(self.x_plot_origin - xPlotPos), 0.1)
                height = max(abs(self.y_plot_origin - yPlotPos), 0.1)

            self.main_window.editWidth(width)
            self.main_window.editHeight(height)

    def mouseReleaseEvent(self, event):

        if self.rubber_band.isVisible():
            self.rubber_band.hide()
            self.main_window.applyChanges()
        else:
            self.main_window.revertDockControls()

    def wheelEvent(self, event):

        if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier:
            numDegrees = event.delta() / 8

            if 24 < self.main_window.zoom + numDegrees < 5001:
                self.main_window.editZoom(self.main_window.zoom + numDegrees)

    def contextMenuEvent(self, event):

        self.menu.clear()

        self.main_window.undoAction.setText('&Undo ({})'.format(
            len(self.model.previousViews)))
        self.main_window.redoAction.setText('&Redo ({})'.format(
            len(self.model.subsequentViews)))

        id, properties, domain, domain_kind = self.getIDinfo(event)

        cv = self.model.currentView

        # always provide undo option
        self.menu.addSeparator()
        self.menu.addAction(self.main_window.undoAction)
        self.menu.addAction(self.main_window.redoAction)
        self.menu.addSeparator()

        if int(id) not in (_NOT_FOUND, _OVERLAP) and \
           cv.colorby not in _MODEL_PROPERTIES:

            # Domain ID
            if domain[id].name:
                domainID = self.menu.addAction("{} {}: \"{}\"".format(
                    domain_kind, id, domain[id].name))
            else:
                domainID = self.menu.addAction("{} {}".format(domain_kind, id))

            self.menu.addSeparator()

            colorAction = self.menu.addAction(
                'Edit {} Color...'.format(domain_kind))
            colorAction.setDisabled(cv.highlighting)
            colorAction.setToolTip('Edit {} color'.format(domain_kind))
            colorAction.setStatusTip('Edit {} color'.format(domain_kind))
            domain_color_connector = partial(self.main_window.editDomainColor,
                                             domain_kind, id)
            colorAction.triggered.connect(domain_color_connector)

            maskAction = self.menu.addAction('Mask {}'.format(domain_kind))
            maskAction.setCheckable(True)
            maskAction.setChecked(domain[id].masked)
            maskAction.setDisabled(not cv.masking)
            maskAction.setToolTip('Toggle {} mask'.format(domain_kind))
            maskAction.setStatusTip('Toggle {} mask'.format(domain_kind))
            mask_connector = partial(self.main_window.toggleDomainMask,
                                     kind=domain_kind,
                                     id=id)
            maskAction.toggled.connect(mask_connector)

            highlightAction = self.menu.addAction(
                'Highlight {}'.format(domain_kind))
            highlightAction.setCheckable(True)
            highlightAction.setChecked(domain[id].highlight)
            highlightAction.setDisabled(not cv.highlighting)
            highlightAction.setToolTip(
                'Toggle {} highlight'.format(domain_kind))
            highlightAction.setStatusTip(
                'Toggle {} highlight'.format(domain_kind))
            highlight_connector = partial(
                self.main_window.toggleDomainHighlight,
                kind=domain_kind,
                id=id)
            highlightAction.toggled.connect(highlight_connector)

        else:
            self.menu.addAction(self.main_window.undoAction)
            self.menu.addAction(self.main_window.redoAction)

            if cv.colorby not in _MODEL_PROPERTIES:
                self.menu.addSeparator()
                if int(id) == _NOT_FOUND:
                    bgColorAction = self.menu.addAction(
                        'Edit Background Color...')
                    bgColorAction.setToolTip('Edit background color')
                    bgColorAction.setStatusTip('Edit plot background color')
                    connector = partial(self.main_window.editBackgroundColor,
                                        apply=True)
                    bgColorAction.triggered.connect(connector)
                elif int(id) == _OVERLAP:
                    olapColorAction = self.menu.addAction(
                        'Edit Overlap Color...')
                    olapColorAction.setToolTip('Edit overlap color')
                    olapColorAction.setStatusTip('Edit plot overlap color')
                    connector = partial(self.main_window.editOverlapColor,
                                        apply=True)
                    olapColorAction.triggered.connect(connector)

        self.menu.addSeparator()
        self.menu.addAction(self.main_window.saveImageAction)
        self.menu.addAction(self.main_window.saveViewAction)
        self.menu.addAction(self.main_window.openAction)
        self.menu.addSeparator()
        self.menu.addMenu(self.main_window.basisMenu)
        self.menu.addMenu(self.main_window.colorbyMenu)
        self.menu.addSeparator()
        if domain_kind.lower() not in ('density', 'temperature'):
            self.menu.addAction(self.main_window.maskingAction)
            self.menu.addAction(self.main_window.highlightingAct)
            self.menu.addAction(self.main_window.overlapAct)
            self.menu.addSeparator()
        self.menu.addAction(self.main_window.dockAction)

        self.main_window.maskingAction.setChecked(cv.masking)
        self.main_window.highlightingAct.setChecked(cv.highlighting)
        self.main_window.overlapAct.setChecked(cv.color_overlaps)

        if self.main_window.dock.isVisible():
            self.main_window.dockAction.setText('Hide &Dock')
        else:
            self.main_window.dockAction.setText('Show &Dock')

        self.menu.exec_(event.globalPos())

    def generatePixmap(self, update=False):
        self.model.generatePlot()
        if update:
            self.updatePixmap()

    def updatePixmap(self):

        # clear out figure
        self.figure.clear()

        cv = self.model.currentView
        # set figure bg color to match window
        window_bg = self.parent.palette().color(QtGui.QPalette.Background)
        self.figure.patch.set_facecolor(rgb_normalize(window_bg.getRgb()))

        # set data extents for automatic reporting of pointer location
        # in model units
        data_bounds = [
            cv.origin[self.main_window.xBasis] - cv.width / 2.,
            cv.origin[self.main_window.xBasis] + cv.width / 2.,
            cv.origin[self.main_window.yBasis] - cv.height / 2.,
            cv.origin[self.main_window.yBasis] + cv.height / 2.
        ]

        # make sure we have a domain image to load
        if not hasattr(self.model, 'image'):
            self.model.generatePlot()

        ### DRAW DOMAIN IMAGE ###

        # still generate the domain image if the geometric
        # plot isn't visible so mouse-over info can still
        # be shown
        alpha = cv.domainAlpha if cv.domainVisible else 0.0
        if cv.colorby in ('material', 'cell'):
            self.image = self.figure.subplots().imshow(self.model.image,
                                                       extent=data_bounds,
                                                       alpha=alpha)
        else:
            cmap = cv.colormaps[cv.colorby]
            if cv.colorby == 'temperature':
                idx = 0
                cmap_label = "Temperature (K)"
            else:
                idx = 1
                cmap_label = "Density (g/cc)"

            norm = SymLogNorm(1E-10) if cv.color_scale_log[
                cv.colorby] else None

            data = self.model.properties[:, :, idx]
            self.image = self.figure.subplots().imshow(data,
                                                       cmap=cmap,
                                                       norm=norm,
                                                       extent=data_bounds,
                                                       alpha=cv.domainAlpha)

            # add colorbar
            self.colorbar = self.figure.colorbar(self.image, anchor=(1.0, 0.0))
            self.colorbar.set_label(cmap_label, rotation=-90, labelpad=15)
            # draw line on colorbar
            dl = self.colorbar.ax.dataLim.get_points()
            self.data_indicator = mlines.Line2D(dl[:][0], [0.0, 0.0],
                                                linewidth=3.,
                                                color='blue',
                                                clip_on=True)
            self.colorbar.ax.add_line(self.data_indicator)
            self.colorbar.ax.margins(0.0, 0.0)
            self.updateDataIndicatorVisibility()
            self.updateColorMinMax(cv.colorby)

        self.ax = self.figure.axes[0]
        self.ax.margins(0.0, 0.0)

        # set axis labels
        axis_label_str = "{} (cm)"
        self.ax.set_xlabel(axis_label_str.format(cv.basis[0]))
        self.ax.set_ylabel(axis_label_str.format(cv.basis[1]))

        # generate tally image
        image_data, extents, data_min, data_max, units = self.model.create_tally_image(
        )

        ### DRAW TALLY IMAGE ###

        # draw tally image
        if image_data is not None:

            if not cv.tallyDataUserMinMax:
                cv.tallyDataMin = data_min
                cv.tallyDataMax = data_max
            else:
                data_min = cv.tallyDataMin
                data_max = cv.tallyDataMax

            # always mask out negative values
            image_mask = image_data < 0.0

            if cv.clipTallyData:
                image_mask |= image_data < data_min
                image_mask |= image_data > data_max

            if cv.tallyMaskZeroValues:
                image_mask |= image_data == 0.0

            # mask out invalid values
            image_data = np.ma.masked_where(image_mask, image_data)

            if extents is None:
                extents = data_bounds

            self.model.tally_data = image_data
            self.model.tally_extents = extents if extents is not None else data_bounds

            norm = SymLogNorm(1E-30) if cv.tallyDataLogScale else None

            if cv.tallyContours:
                # parse the levels line
                levels = self.parseContoursLine(cv.tallyContourLevels)
                self.tally_image = self.ax.contour(image_data,
                                                   origin='image',
                                                   levels=levels,
                                                   alpha=cv.tallyDataAlpha,
                                                   cmap=cv.tallyDataColormap,
                                                   norm=norm,
                                                   extent=extents)

            else:
                self.tally_image = self.ax.imshow(image_data,
                                                  alpha=cv.tallyDataAlpha,
                                                  cmap=cv.tallyDataColormap,
                                                  norm=norm,
                                                  extent=extents)
            # add colorbar
            self.tally_colorbar = self.figure.colorbar(self.tally_image,
                                                       anchor=(1.0, 0.0))

            if cv.tallyContours:
                fmt = "%.2E"
                self.ax.clabel(self.tally_image,
                               self.tally_image.levels,
                               inline=True,
                               fmt=fmt)

            # draw line on colorbar
            self.tally_data_indicator = mlines.Line2D([0.0, 1.0], [0.0, 0.0],
                                                      linewidth=3.,
                                                      color='blue',
                                                      clip_on=True)
            self.tally_colorbar.ax.add_line(self.tally_data_indicator)
            self.tally_colorbar.ax.margins(0.0, 0.0)

            self.tally_data_indicator.set_visible(cv.tallyDataIndicator)

            self.main_window.updateTallyMinMax()

            self.tally_colorbar.mappable.set_clim(data_min, data_max)
            self.tally_colorbar.set_label(units, rotation=-90, labelpad=15)

        # annotate outlines
        self.add_outlines()

        # always make sure the data bounds are set correctly
        self.ax.set_xbound(data_bounds[0], data_bounds[1])
        self.ax.set_ybound(data_bounds[2], data_bounds[3])
        self.ax.dataLim.x0 = data_bounds[0]
        self.ax.dataLim.x1 = data_bounds[1]
        self.ax.dataLim.y0 = data_bounds[2]
        self.ax.dataLim.y1 = data_bounds[3]

        self.draw()
        return "Done"

    def add_outlines(self):
        cv = self.model.currentView
        # draw outlines as isocontours
        if cv.outlines:
            # set data extents for automatic reporting of pointer location
            data_bounds = [
                cv.origin[self.main_window.xBasis] - cv.width / 2.,
                cv.origin[self.main_window.xBasis] + cv.width / 2.,
                cv.origin[self.main_window.yBasis] - cv.height / 2.,
                cv.origin[self.main_window.yBasis] + cv.height / 2.
            ]
            levels = np.unique(self.model.ids)
            self.contours = self.ax.contour(self.model.ids,
                                            origin='upper',
                                            colors='k',
                                            linestyles='solid',
                                            levels=levels,
                                            extent=data_bounds)

    @staticmethod
    def parseContoursLine(line):
        # if there are any commas in the line, treat as level values
        line = line.strip()
        if ',' in line:
            return [float(val) for val in line.split(",") if val != '']
        else:
            return int(line)

    def updateColorbarScale(self):
        self.updatePixmap()

    def updateTallyDataIndicatorValue(self, y_val):
        cv = self.model.currentView

        if not cv.tallyDataVisible or not cv.tallyDataIndicator:
            return

        if self.tally_data_indicator is not None:
            data = self.tally_data_indicator.get_data()
            # use norm to get axis value if log scale
            if cv.tallyDataLogScale:
                y_val = self.tally_image.norm(y_val)
            self.tally_data_indicator.set_data([data[0], [y_val, y_val]])
            dl_color = invert_rgb(self.tally_image.get_cmap()(y_val), True)
            self.tally_data_indicator.set_c(dl_color)
            self.draw()

    def updateDataIndicatorValue(self, y_val):
        cv = self.model.currentView

        if cv.colorby not in _MODEL_PROPERTIES or \
           not cv.data_indicator_enabled[cv.colorby]:
            return

        if self.data_indicator:
            data = self.data_indicator.get_data()
            # use norm to get axis value if log scale
            if cv.color_scale_log[cv.colorby]:
                y_val = self.image.norm(y_val)
            self.data_indicator.set_data([data[0], [y_val, y_val]])
            dl_color = invert_rgb(self.image.get_cmap()(y_val), True)
            self.data_indicator.set_c(dl_color)
            self.draw()

    def updateDataIndicatorVisibility(self):
        cv = self.model.currentView
        if self.data_indicator and cv.colorby in _MODEL_PROPERTIES:
            val = cv.data_indicator_enabled[cv.colorby]
            self.data_indicator.set_visible(val)
            self.draw()

    def updateColorMap(self, colormap_name, property_type):
        if self.colorbar and property_type == self.model.activeView.colorby:
            self.image.set_cmap(colormap_name)
            self.colorbar.draw_all()
            self.draw()

    def updateColorMinMax(self, property_type):
        av = self.model.activeView
        if self.colorbar and property_type == av.colorby:
            clim = av.getColorLimits(property_type)
            self.colorbar.mappable.set_clim(*clim)
            self.data_indicator.set_data(clim[:2], (0.0, 0.0))
            self.colorbar.draw_all()
            self.draw()
class PlotImage(QLabel):
    def __init__(self, model, FM, parent=None):
        super(PlotImage, self).__init__(parent)

        self.model = model
        self.FM = FM
        self.mw = parent

        self.setAlignment(QtCore.Qt.AlignCenter)
        self.setMouseTracking(True)

        self.rubberBand = QRubberBand(QRubberBand.Rectangle, self)
        self.bandOrigin = QtCore.QPoint()
        self.xPlotOrigin = None
        self.yPlotOrigin = None

        self.menu = QMenu(self)

    def enterEvent(self, event):
        self.setCursor(QtCore.Qt.CrossCursor)
        self.mw.coordLabel.show()

    def leaveEvent(self, event):
        self.mw.coordLabel.hide()
        self.mw.statusBar().showMessage('')

    def mousePressEvent(self, event):

        # Set rubber band absolute and relative position
        self.bandOrigin = event.pos()
        self.xPlotOrigin, self.yPlotOrigin = self.getPlotCoords(event.pos())

        # Create rubber band
        self.rubberBand.setGeometry(
            QtCore.QRect(self.bandOrigin, QtCore.QSize()))

        QLabel.mousePressEvent(self, event)

    def mouseDoubleClickEvent(self, event):

        xCenter, yCenter = self.getPlotCoords(event.pos())
        self.mw.editPlotOrigin(xCenter, yCenter, apply=True)

        QLabel.mouseDoubleClickEvent(self, event)

    def mouseMoveEvent(self, event):

        # Show Cursor position relative to plot in status bar
        xPlotPos, yPlotPos = self.getPlotCoords(event.pos())

        # Show Cell/Material ID, Name in status bar
        id, domain, domain_kind = self.getIDinfo(event)
        if id != '-1' and domain[id].name:
            domainInfo = f"{domain_kind} {id}: {domain[id].name}"
        elif id != '-1':
            domainInfo = f"{domain_kind} {id}"
        else:
            domainInfo = ""
        self.mw.statusBar().showMessage(f" {domainInfo}")

        # Update rubber band and values if mouse button held down
        if event.buttons() == QtCore.Qt.LeftButton:
            self.rubberBand.setGeometry(
                QtCore.QRect(self.bandOrigin, event.pos()).normalized())

            # Show rubber band if both dimensions > 10 pixels
            if self.rubberBand.width() > 10 and self.rubberBand.height() > 10:
                self.rubberBand.show()
            else:
                self.rubberBand.hide()

            # Update plot X Origin
            xCenter = (self.xPlotOrigin + xPlotPos) / 2
            yCenter = (self.yPlotOrigin + yPlotPos) / 2
            self.mw.editPlotOrigin(xCenter, yCenter)

            modifiers = event.modifiers()

            # Zoom out if Shift held
            if modifiers == QtCore.Qt.ShiftModifier:
                cv = self.model.currentView
                bandwidth = abs(self.bandOrigin.x() - event.pos().x())
                width = cv.width * (cv.hRes / max(bandwidth, .001))
                bandheight = abs(self.bandOrigin.y() - event.pos().y())
                height = cv.height * (cv.vRes / max(bandheight, .001))
            else:  # Zoom in
                width = max(abs(self.xPlotOrigin - xPlotPos), 0.1)
                height = max(abs(self.yPlotOrigin - yPlotPos), 0.1)

            self.mw.editWidth(width)
            self.mw.editHeight(height)

    def mouseReleaseEvent(self, event):

        if self.rubberBand.isVisible():
            self.rubberBand.hide()
            self.mw.applyChanges()
        else:
            self.mw.revertDockControls()

    def wheelEvent(self, event):

        if event.delta() and event.modifiers() == QtCore.Qt.ShiftModifier:
            numDegrees = event.delta() / 8

            if 24 < self.mw.zoom + numDegrees < 5001:
                self.mw.editZoom(self.mw.zoom + numDegrees)

    def contextMenuEvent(self, event):

        self.menu.clear()

        self.mw.undoAction.setText(f'&Undo ({len(self.model.previousViews)})')
        self.mw.redoAction.setText(
            f'&Redo ({len(self.model.subsequentViews)})')

        id, domain, domain_kind = self.getIDinfo(event)

        if id != '-1':

            # Domain ID
            domainID = self.menu.addAction(f"{domain_kind} {id}")
            domainID.setDisabled(True)

            # Domain Name (if any)
            if domain[id].name:
                domainName = self.menu.addAction(domain[id].name)
                domainName.setDisabled(True)

            self.menu.addSeparator()
            self.menu.addAction(self.mw.undoAction)
            self.menu.addAction(self.mw.redoAction)
            self.menu.addSeparator()

            colorAction = self.menu.addAction(f'Edit {domain_kind} Color...')
            colorAction.setDisabled(self.model.currentView.highlighting)
            colorAction.setToolTip(f'Edit {domain_kind} color')
            colorAction.setStatusTip(f'Edit {domain_kind} color')
            colorAction.triggered.connect(
                lambda: self.mw.editDomainColor(domain_kind, id))

            maskAction = self.menu.addAction(f'Mask {domain_kind}')
            maskAction.setCheckable(True)
            maskAction.setChecked(domain[id].masked)
            maskAction.setDisabled(not self.model.currentView.masking)
            maskAction.setToolTip(f'Toggle {domain_kind} mask')
            maskAction.setStatusTip(f'Toggle {domain_kind} mask')
            maskAction.triggered[bool].connect(
                lambda bool=bool: self.mw.toggleDomainMask(
                    bool, domain_kind, id))

            highlightAction = self.menu.addAction(f'Highlight {domain_kind}')
            highlightAction.setCheckable(True)
            highlightAction.setChecked(domain[id].highlighted)
            highlightAction.setDisabled(
                not self.model.currentView.highlighting)
            highlightAction.setToolTip(f'Toggle {domain_kind} highlight')
            highlightAction.setStatusTip(f'Toggle {domain_kind} highlight')
            highlightAction.triggered[bool].connect(
                lambda bool=bool: self.mw.toggleDomainHighlight(
                    bool, domain_kind, id))

        else:
            self.menu.addAction(self.mw.undoAction)
            self.menu.addAction(self.mw.redoAction)
            self.menu.addSeparator()
            bgColorAction = self.menu.addAction('Edit Background Color...')
            bgColorAction.setToolTip('Edit background color')
            bgColorAction.setStatusTip('Edit plot background color')
            bgColorAction.triggered.connect(
                lambda: self.mw.editBackgroundColor(apply=True))

        self.menu.addSeparator()
        self.menu.addAction(self.mw.saveImageAction)
        self.menu.addAction(self.mw.saveViewAction)
        self.menu.addAction(self.mw.openAction)
        self.menu.addSeparator()
        self.menu.addMenu(self.mw.basisMenu)
        self.menu.addMenu(self.mw.colorbyMenu)
        self.menu.addSeparator()
        self.menu.addAction(self.mw.maskingAction)
        self.menu.addAction(self.mw.highlightingAct)
        self.menu.addSeparator()
        self.menu.addAction(self.mw.dockAction)

        self.mw.maskingAction.setChecked(self.model.currentView.masking)
        self.mw.highlightingAct.setChecked(self.model.currentView.highlighting)

        if self.mw.dock.isVisible():
            self.mw.dockAction.setText('Hide &Dock')
        else:
            self.mw.dockAction.setText('Show &Dock')

        self.menu.exec_(event.globalPos())

    def getPlotCoords(self, pos):

        cv = self.model.currentView

        factor = (self.width() / cv.hRes, self.height() / cv.vRes)

        # Cursor position in pixels relative to center of plot image
        xPos = (pos.x() + 0.5) / factor[0] - cv.hRes / 2
        yPos = (-pos.y() - 0.5) / factor[1] + cv.vRes / 2

        # Curson position in plot coordinates
        xPlotCoord = (xPos / self.mw.scale[0]) + cv.origin[self.mw.xBasis]
        yPlotCoord = (yPos / self.mw.scale[1]) + cv.origin[self.mw.yBasis]

        self.mw.showCoords(xPlotCoord, yPlotCoord)

        return (xPlotCoord, yPlotCoord)

    def getIDinfo(self, event):

        cv = self.model.currentView
        factor = (self.width() / cv.hRes, self.height() / cv.vRes)
        xPos = int((event.pos().x() + .05) / factor[0])
        yPos = int((event.pos().y() + .05) / factor[1])

        if yPos < self.model.currentView.vRes \
            and xPos < self.model.currentView.hRes:

            id = f"{self.model.ids[yPos][xPos]}"
        else:
            id = '-1'

        if self.model.currentView.colorby == 'cell':
            domain = self.model.activeView.cells
            domain_kind = 'Cell'
        else:
            domain = self.model.activeView.materials
            domain_kind = 'Material'

        return id, domain, domain_kind