Example #1
0
    def add_image(self, image, label):
        w = QFrame()
        w.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        w.setLineWidth(2)
        w.setFixedSize(QSize(500, 500))
        w.setLayout(QVBoxLayout())
        gv = ScrollableGraphicsLayoutWidget()
        vb = ViewBox(lockAspect=True)
        ii = ActivatableImageItem(image=image)
        ii.sigActivated.connect(self.set_current_imageitem)
        self.hist_widget.item.setImageItem(ii)
        self.current_image_item = ii
        self.image_items.append(ii)
        self.views.append(vb)
        vb.sigRangeChangedManually.connect(self.propagate_axes)
        vb.addItem(ii)
        gv.addItem(vb)
        self.set_current_imageitem(ii)

        w.layout().addWidget(gv)
        l = QLabel(label)
        # l.setStyleSheet("color: white;")
        w.layout().addWidget(l)

        self.flow_layout.addWidget(w)
        self.last_vb = vb
Example #2
0
def add_source_id_label(view_box: pyqtgraph.ViewBox,
                        context: Context) -> pyqtgraph.TextItem:
    """Add a translucent TextItem pinned to the bottom left of the view box displaying
    the context source id string.
    """
    text_item = pyqtgraph.TextItem(text="",
                                   anchor=(0, 1),
                                   color=(255, 255, 255),
                                   fill=(0, 0, 0))
    text_item.setZValue(1000)
    text_item.setOpacity(0.3)
    view_box.addItem(text_item, ignoreBounds=True)

    def update_text(*args):
        text_item.setText(" " + context.get_source_id() + " ")

    context.source_id_changed.connect(update_text)
    update_text()

    def update_text_pos(*args):
        ((x, _), (y, _)) = view_box.viewRange()
        text_item.setPos(x, y)

    view_box.sigRangeChanged.connect(update_text_pos)
    update_text_pos()

    return text_item
Example #3
0
 def __init__(self):
     super().__init__()
     viewbox = ViewBox()
     self.graph = GridItem()
     viewbox.setAspectLocked()
     viewbox.addItem(self.graph)
     self.setBackground('w')
     self.setCentralItem(viewbox)
Example #4
0
    def addItem(self, item, ignoreBounds=False):
        """Reimplemented from :class:`pyqtgraph.ViewBox`"""
        ViewBox.addItem(self, item, ignoreBounds=ignoreBounds)

        if len(self.addedItems) == 1:
            # when the first curve is added to self (axis Y2), we must
            # add Y2 to main scene(), show the axis and link X axis to self.
            self.plotItem.showAxis('right')
            self.plotItem.scene().addItem(self)
            self.plotItem.getAxis('right').linkToView(self)
            self.setXLink(self.plotItem)

        if (len(self.addedItems) > 0
                and item.getFullModelNames() not in self._curvesModelNames):
            self._curvesModelNames.append(item.getFullModelNames())
Example #5
0
    def addItem(self, item, ignoreBounds=False):
        """Reimplemented from :class:`pyqtgraph.ViewBox`"""

        # first add it to plotItem and then move it from main viewbox to y2
        if self.plotItem is not None:
            if item not in self.plotItem.listDataItems():
                self.plotItem.addItem(item)
            if item in self.plotItem.getViewBox().addedItems:
                self.plotItem.getViewBox().removeItem(item)
        ViewBox.addItem(self, item, ignoreBounds=ignoreBounds)

        if self.plotItem is not None:
            self.plotItem.showAxis("right", show=bool(self.addedItems))

            # set the item log mode to match this view:
            if hasattr(item, "setLogMode"):
                item.setLogMode(
                    self.plotItem.getAxis("bottom").logMode,
                    self.plotItem.getAxis("right").logMode,
                )
Example #6
0
    def addItem(self, item, ignoreBounds=False):
        """Reimplemented from :class:`pyqtgraph.ViewBox`"""
        ViewBox.addItem(self, item, ignoreBounds=ignoreBounds)

        if len(self.addedItems) == 1:
            # when the first curve is added to self (axis Y2), we must
            # add Y2 to main scene(), show the axis and link X axis to self.
            self.plotItem.showAxis("right")
            self.plotItem.scene().addItem(self)
            self.plotItem.getAxis("right").linkToView(self)
            self.setXLink(self.plotItem)

        # set the item log mode to match this view:
        if hasattr(item, "setLogMode"):
            item.setLogMode(
                self.plotItem.getAxis("bottom").logMode,
                self.plotItem.getAxis("right").logMode,
            )

        if hasattr(item, "getFullModelNames") and (
            len(self.addedItems) > 0
            and item.getFullModelNames() not in self._curvesModelNames
        ):
            self._curvesModelNames.append(item.getFullModelNames())
Example #7
0
 def image_in_vb(self, name=None):
     im = ImageItem()
     vb = ViewBox(invertY=True, lockAspect=True, name=name)
     vb.addItem(im)
     hist = HistogramLUTItem(im)
     return im, vb, hist
Example #8
0
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap,
                            ReadingOrder):
    """
    A SpectrogramView with support for Channels and more from PyDM.

    If there is no :attr:`channelWidth` it is possible to define the width of
    the image with the :attr:`width` property.

    The :attr:`normalizeData` property defines if the colors of the images are
    relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
    the minimum and maximum values of the image.

    Use the :attr:`newImageSignal` to hook up to a signal that is emitted when
    a new image is rendered in the widget.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    image_channel : str, optional
        The channel to be used by the widget for the image data.
    xaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Clike), and to set the xaxis values
    yaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Fortranlike), and to set the yaxis values
    background : QColor, optional
        QColor to set the background color of the GraphicsView
    """

    Q_ENUMS(PyDMColorMap)
    Q_ENUMS(ReadingOrder)

    color_maps = cmaps

    def __init__(self,
                 parent=None,
                 image_channel=None,
                 xaxis_channel=None,
                 yaxis_channel=None,
                 roioffsetx_channel=None,
                 roioffsety_channel=None,
                 roiwidth_channel=None,
                 roiheight_channel=None,
                 title='',
                 background='w',
                 image_width=0,
                 image_height=0):
        """Initialize widget."""
        GraphicsLayoutWidget.__init__(self, parent)
        PyDMWidget.__init__(self)
        self.thread = None
        self._imagechannel = None
        self._xaxischannel = None
        self._yaxischannel = None
        self._roioffsetxchannel = None
        self._roioffsetychannel = None
        self._roiwidthchannel = None
        self._roiheightchannel = None
        self._channels = 7 * [
            None,
        ]
        self.image_waveform = np.zeros(0)
        self._image_width = image_width if not xaxis_channel else 0
        self._image_height = image_height if not yaxis_channel else 0
        self._roi_offsetx = 0
        self._roi_offsety = 0
        self._roi_width = 0
        self._roi_height = 0
        self._normalize_data = False
        self._auto_downsample = True
        self._last_yaxis_data = None
        self._last_xaxis_data = None
        self._auto_colorbar_lims = True
        self.format_tooltip = '{0:.4g}, {1:.4g}'

        # ViewBox and imageItem.
        self._view = ViewBox()
        self._image_item = ImageItem()
        self._view.addItem(self._image_item)

        # ROI
        self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
        self.ROIColor = QColor('red')
        pen = mkPen()
        pen.setColor(QColor('transparent'))
        pen.setWidth(1)
        self.ROICurve.setPen(pen)
        self._view.addItem(self.ROICurve)

        # Axis.
        self.xaxis = AxisItem('bottom')
        self.xaxis.setPen(QColor(0, 0, 0))
        if not xaxis_channel:
            self.xaxis.setVisible(False)
        self.yaxis = AxisItem('left')
        self.yaxis.setPen(QColor(0, 0, 0))
        if not yaxis_channel:
            self.yaxis.setVisible(False)

        # Colorbar legend.
        self.colorbar = _GradientLegend()

        # Title.
        start_row = 0
        if title:
            self.title = LabelItem(text=title, color='#000000')
            self.addItem(self.title, 0, 0, 1, 3)
            start_row = 1

        # Set layout.
        self.addItem(self._view, start_row, 1)
        self.addItem(self.yaxis, start_row, 0)
        self.addItem(self.colorbar, start_row, 2)
        self.addItem(self.xaxis, start_row + 1, 1)
        self.setBackground(background)
        self.ci.layout.setColumnSpacing(0, 0)
        self.ci.layout.setRowSpacing(start_row, 0)

        # Set color map limits.
        self.cm_min = 0.0
        self.cm_max = 255.0

        # Set default reading order of numpy array data to Clike.
        self._reading_order = ReadingOrder.Clike

        # Make a right-click menu for changing the color map.
        self.cm_group = QActionGroup(self)
        self.cmap_for_action = {}
        for cm in self.color_maps:
            action = self.cm_group.addAction(cmap_names[cm])
            action.setCheckable(True)
            self.cmap_for_action[action] = cm

        # Set the default colormap.
        self._cm_colors = None
        self.colorMap = PyDMColorMap.Inferno

        # Setup the redraw timer.
        self.needs_redraw = False
        self.redraw_timer = QTimer(self)
        self.redraw_timer.timeout.connect(self.redrawImage)
        self._redraw_rate = 30
        self.maxRedrawRate = self._redraw_rate
        self.newImageSignal = self._image_item.sigImageChanged

        # Set Channels.
        self.imageChannel = image_channel
        self.xAxisChannel = xaxis_channel
        self.yAxisChannel = yaxis_channel
        self.ROIOffsetXChannel = roioffsetx_channel
        self.ROIOffsetYChannel = roioffsety_channel
        self.ROIWidthChannel = roiwidth_channel
        self.ROIHeightChannel = roiheight_channel

    # --- Context menu ---
    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu.

        It will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by
            `assemble_tools_menu`.
        """
        self.menu = ViewBoxMenu(self._view)
        cm_menu = self.menu.addMenu("Color Map")
        for act in self.cmap_for_action.keys():
            cm_menu.addAction(act)
        cm_menu.triggered.connect(self._changeColorMap)
        return self.menu

    # --- Colormap methods ---
    def _changeColorMap(self, action):
        """
        Method invoked by the colormap Action Menu.

        Changes the current colormap used to render the image.

        Parameters
        ----------
        action : QAction
        """
        self.colorMap = self.cmap_for_action[action]

    @Property(float)
    def colorMapMin(self):
        """
        Minimum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_min

    @colorMapMin.setter
    @Slot(float)
    def colorMapMin(self, new_min):
        """
        Set the minimum value for the colormap.

        Parameters
        ----------
        new_min : float
        """
        if self.cm_min != new_min:
            self.cm_min = new_min
            if self.cm_min > self.cm_max:
                self.cm_max = self.cm_min

    @Property(float)
    def colorMapMax(self):
        """
        Maximum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_max

    @colorMapMax.setter
    @Slot(float)
    def colorMapMax(self, new_max):
        """
        Set the maximum value for the colormap.

        Parameters
        ----------
        new_max : float
        """
        if self.cm_max != new_max:
            self.cm_max = new_max
            if self.cm_max < self.cm_min:
                self.cm_min = self.cm_max

    def setColorMapLimits(self, mn, mx):
        """
        Set the limit values for the colormap.

        Parameters
        ----------
        mn : int
            The lower limit
        mx : int
            The upper limit
        """
        if mn >= mx:
            return
        self.cm_max = mx
        self.cm_min = mn

    @Property(PyDMColorMap)
    def colorMap(self):
        """
        Return the color map used by the SpectrogramView.

        Returns
        -------
        PyDMColorMap
        """
        return self._colormap

    @colorMap.setter
    def colorMap(self, new_cmap):
        """
        Set the color map used by the SpectrogramView.

        Parameters
        -------
        new_cmap : PyDMColorMap
        """
        self._colormap = new_cmap
        self._cm_colors = self.color_maps[new_cmap]
        self.setColorMap()
        for action in self.cm_group.actions():
            if self.cmap_for_action[action] == self._colormap:
                action.setChecked(True)
            else:
                action.setChecked(False)

    def setColorMap(self, cmap=None):
        """
        Update the image colormap.

        Parameters
        ----------
        cmap : ColorMap
        """
        if not cmap:
            if not self._cm_colors.any():
                return
            # Take default values
            pos = np.linspace(0.0, 1.0, num=len(self._cm_colors))
            cmap = ColorMap(pos, self._cm_colors)
        self._view.setBackgroundColor(cmap.map(0))
        lut = cmap.getLookupTable(0.0, 1.0, alpha=False)
        self.colorbar.setIntColorScale(colors=lut)
        self._image_item.setLookupTable(lut)

    # --- Connection Slots ---
    @Slot(bool)
    def image_connection_state_changed(self, conn):
        """
        Callback invoked when the Image Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        if conn:
            self.redraw_timer.start()
        else:
            self.redraw_timer.stop()

    @Slot(bool)
    def yaxis_connection_state_changed(self, connected):
        """
        Callback invoked when the TimeAxis Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        self._timeaxis_connected = connected

    @Slot(bool)
    def roioffsetx_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetX Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsetx = 0

    @Slot(bool)
    def roioffsety_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetY Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsety = 0

    @Slot(bool)
    def roiwidth_connection_state_changed(self, conn):
        """
        Run when the ROIWidth Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_width = 0

    @Slot(bool)
    def roiheight_connection_state_changed(self, conn):
        """
        Run when the ROIHeight Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_height = 0

    # --- Value Slots ---
    @Slot(np.ndarray)
    def image_value_changed(self, new_image):
        """
        Callback invoked when the Image Channel value is changed.

        We try to do as little as possible in this method, because it
        gets called every time the image channel updates, which might
        be extremely often.  Basically just store the data, and set
        a flag requesting that the image be redrawn.

        Parameters
        ----------
        new_image : np.ndarray
            The new image data.  This can be a flat 1D array, or a 2D array.
        """
        if new_image is None or new_image.size == 0:
            return
        logging.debug("SpectrogramView Received New Image: Needs Redraw->True")
        self.image_waveform = new_image
        self.needs_redraw = True
        if not self._image_height and self._image_width:
            self._image_height = new_image.size / self._image_width
        elif not self._image_width and self._image_height:
            self._image_width = new_image.size / self._image_height

    @Slot(np.ndarray)
    @Slot(float)
    def xaxis_value_changed(self, new_array):
        """
        Callback invoked when the Image Width Channel value is changed.

        Parameters
        ----------
        new_array : np.ndarray
            The new x axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_xaxis_data = new_array
        if self._reading_order == self.Clike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(np.ndarray)
    @Slot(float)
    def yaxis_value_changed(self, new_array):
        """
        Callback invoked when the TimeAxis Channel value is changed.

        Parameters
        ----------
        new_array : np.array
            The new y axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_yaxis_data = new_array
        if self._reading_order == self.Fortranlike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(int)
    def roioffsetx_value_changed(self, new_offset):
        """
        Run when the ROIOffsetX Channel value changes.

        Parameters
        ----------
        new_offsetx : int
            The new image ROI horizontal offset

        """
        if new_offset is None:
            return
        self._roi_offsetx = new_offset
        self.redrawROI()

    @Slot(int)
    def roioffsety_value_changed(self, new_offset):
        """
        Run when the ROIOffsetY Channel value changes.

        Parameters
        ----------
        new_offsety : int
            The new image ROI vertical offset

        """
        if new_offset is None:
            return
        self._roi_offsety = new_offset
        self.redrawROI()

    @Slot(int)
    def roiwidth_value_changed(self, new_width):
        """
        Run when the ROIWidth Channel value changes.

        Parameters
        ----------
        new_width : int
            The new image ROI width

        """
        if new_width is None:
            return
        self._roi_width = int(new_width)
        self.redrawROI()

    @Slot(int)
    def roiheight_value_changed(self, new_height):
        """
        Run when the ROIHeight Channel value changes.

        Parameters
        ----------
        new_height : int
            The new image ROI height

        """
        if new_height is None:
            return
        self._roi_height = int(new_height)
        self.redrawROI()

    # --- Image update methods ---
    def process_image(self, image):
        """
        Boilerplate method.

        To be used by applications in order to add calculations and also modify
        the image before it is displayed at the widget.

        .. warning::
           This code runs in a separated QThread so it **MUST** not try to
           write to QWidgets.

        Parameters
        ----------
        image : np.ndarray
            The Image Data as a 2D numpy array

        Returns
        -------
        np.ndarray
            The Image Data as a 2D numpy array after processing.
        """
        return image

    def redrawImage(self):
        """
        Set the image data into the ImageItem, if needed.

        If necessary, reshape the image to 2D first.
        """
        if self.thread is not None and not self.thread.isFinished():
            logger.warning(
                "Image processing has taken longer than the refresh rate.")
            return
        self.thread = SpectrogramUpdateThread(self)
        self.thread.updateSignal.connect(self._updateDisplay)
        logging.debug("SpectrogramView RedrawImage Thread Launched")
        self.thread.start()

    @Slot(list)
    def _updateDisplay(self, data):
        logging.debug("SpectrogramView Update Display with new image")

        # Update axis
        if self._last_xaxis_data is not None:
            szx = self._last_xaxis_data.size
            xMin = self._last_xaxis_data.min()
            xMax = self._last_xaxis_data.max()
        else:
            szx = self.imageWidth if self.readingOrder == self.Clike \
                else self.imageHeight
            xMin = 0
            xMax = szx

        if self._last_yaxis_data is not None:
            szy = self._last_yaxis_data.size
            yMin = self._last_yaxis_data.min()
            yMax = self._last_yaxis_data.max()
        else:
            szy = self.imageHeight if self.readingOrder == self.Clike \
                else self.imageWidth
            yMin = 0
            yMax = szy

        self.xaxis.setRange(xMin, xMax)
        self.yaxis.setRange(yMin, yMax)
        self._view.setLimits(xMin=0,
                             xMax=szx,
                             yMin=0,
                             yMax=szy,
                             minXRange=szx,
                             maxXRange=szx,
                             minYRange=szy,
                             maxYRange=szy)

        # Update image
        if self.autoSetColorbarLims:
            self.colorbar.setLimits(data)
        mini, maxi = data[0], data[1]
        img = data[2]
        self._image_item.setLevels([mini, maxi])
        self._image_item.setImage(img,
                                  autoLevels=False,
                                  autoDownsample=self.autoDownsample)

    # ROI update methods
    def redrawROI(self):
        startx = self._roi_offsetx
        endx = self._roi_offsetx + self._roi_width
        starty = self._roi_offsety
        endy = self._roi_offsety + self._roi_height
        self.ROICurve.setData([startx, startx, endx, endx, startx],
                              [starty, endy, endy, starty, starty])

    def showROI(self, show):
        """Set ROI visibility."""
        pen = mkPen()
        if show:
            pen.setColor(self.ROIColor)
        else:
            pen.setColor(QColor('transparent'))
        self.ROICurve.setPen(pen)

    # --- Properties ---
    @Property(bool)
    def autoDownsample(self):
        """
        Return if we should or not apply the autoDownsample option.

        Return
        ------
        bool
        """
        return self._auto_downsample

    @autoDownsample.setter
    def autoDownsample(self, new_value):
        """
        Whether we should or not apply the autoDownsample option.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_downsample:
            self._auto_downsample = new_value

    @Property(bool)
    def autoSetColorbarLims(self):
        """
        Return if we should or not auto set colorbar limits.

        Return
        ------
        bool
        """
        return self._auto_colorbar_lims

    @autoSetColorbarLims.setter
    def autoSetColorbarLims(self, new_value):
        """
        Whether we should or not auto set colorbar limits.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_colorbar_lims:
            self._auto_colorbar_lims = new_value

    @Property(int)
    def imageWidth(self):
        """
        Return the width of the image.

        Return
        ------
        int
        """
        return self._image_width

    @imageWidth.setter
    def imageWidth(self, new_width):
        """
        Set the width of the image.

        Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`.

        Parameters
        ----------
        new_width: int
        """
        boo = self._image_width != int(new_width)
        boo &= not self._xaxischannel
        boo &= not self._yaxischannel
        if boo:
            self._image_width = int(new_width)

    @Property(int)
    def imageHeight(self):
        """
        Return the height of the image.

        Return
        ------
        int
        """
        return self._image_height

    @Property(int)
    def ROIOffsetX(self):
        """
        Return the ROI offset in X axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsetx

    @ROIOffsetX.setter
    def ROIOffsetX(self, new_offset):
        """
        Set the ROI offset in X axis in pixels.

        Can be overridden by :attr:`ROIOffsetXChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsetx != int(new_offset)
        boo &= not self._roioffsetxchannel
        if boo:
            self._roi_offsetx = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIOffsetY(self):
        """
        Return the ROI offset in Y axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsety

    @ROIOffsetY.setter
    def ROIOffsetY(self, new_offset):
        """
        Set the ROI offset in Y axis in pixels.

        Can be overridden by :attr:`ROIOffsetYChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsety != int(new_offset)
        boo &= not self._roioffsetychannel
        if boo:
            self._roi_offsety = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIWidth(self):
        """
        Return the ROI width in pixels.

        Return
        ------
        int
        """
        return self._roi_width

    @ROIWidth.setter
    def ROIWidth(self, new_width):
        """
        Set the ROI width in pixels.

        Can be overridden by :attr:`ROIWidthChannel`.

        Parameters
        ----------
        new_width: int
        """
        if new_width is None:
            return
        boo = self._roi_width != int(new_width)
        boo &= not self._roiwidthchannel
        if boo:
            self._roi_width = int(new_width)
            self.redrawROI()

    @Property(int)
    def ROIHeight(self):
        """
        Return the ROI height in pixels.

        Return
        ------
        int
        """
        return self._roi_height

    @ROIHeight.setter
    def ROIHeight(self, new_height):
        """
        Set the ROI height in pixels.

        Can be overridden by :attr:`ROIHeightChannel`.

        Parameters
        ----------
        new_height: int
        """
        if new_height is None:
            return
        boo = self._roi_height != int(new_height)
        boo &= not self._roiheightchannel
        if boo:
            self._roi_height = int(new_height)
            self.redrawROI()

    @Property(bool)
    def normalizeData(self):
        """
        Return True if the colors are relative to data maximum and minimum.

        Returns
        -------
        bool
        """
        return self._normalize_data

    @normalizeData.setter
    @Slot(bool)
    def normalizeData(self, new_norm):
        """
        Define if the colors are relative to minimum and maximum of the data.

        Parameters
        ----------
        new_norm: bool
        """
        if self._normalize_data != new_norm:
            self._normalize_data = new_norm

    @Property(ReadingOrder)
    def readingOrder(self):
        """
        Return the reading order of the :attr:`imageChannel` array.

        Returns
        -------
        ReadingOrder
        """
        return self._reading_order

    @readingOrder.setter
    def readingOrder(self, order):
        """
        Set reading order of the :attr:`imageChannel` array.

        Parameters
        ----------
        order: ReadingOrder
        """
        if self._reading_order != order:
            self._reading_order = order

        if order == self.Clike:
            if self._last_xaxis_data is not None:
                self._image_width = self._last_xaxis_data.size
            if self._last_yaxis_data is not None:
                self._image_height = self._last_yaxis_data.size
        elif order == self.Fortranlike:
            if self._last_yaxis_data is not None:
                self._image_width = self._last_yaxis_data.size
            if self._last_xaxis_data is not None:
                self._image_height = self._last_xaxis_data.size

    @Property(int)
    def maxRedrawRate(self):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Returns
        -------
        int
        """
        return self._redraw_rate

    @maxRedrawRate.setter
    def maxRedrawRate(self, redraw_rate):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Parameters
        -------
        redraw_rate : int
        """
        self._redraw_rate = redraw_rate
        self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))

    # --- Events rederivations ---
    def keyPressEvent(self, ev):
        """Handle keypress events."""
        return

    def mouseMoveEvent(self, ev):
        if not self._image_item.width() or not self._image_item.height():
            super().mouseMoveEvent(ev)
            return
        pos = ev.pos()
        posaux = self._image_item.mapFromDevice(ev.pos())
        if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \
                posaux.y() < 0 or posaux.y() >= self._image_item.height():
            super().mouseMoveEvent(ev)
            return

        pos_scene = self._view.mapSceneToView(pos)
        x = round(pos_scene.x())
        y = round(pos_scene.y())

        if self.xAxisChannel and self._last_xaxis_data is not None:
            maxx = len(self._last_xaxis_data) - 1
            x = x if x < maxx else maxx
            valx = self._last_xaxis_data[x]
        else:
            valx = x

        if self.yAxisChannel and self._last_yaxis_data is not None:
            maxy = len(self._last_yaxis_data) - 1
            y = y if y < maxy else maxy
            valy = self._last_yaxis_data[y]
        else:
            valy = y

        txt = self.format_tooltip.format(valx, valy)
        QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(),
                          5000)
        super().mouseMoveEvent(ev)

    # --- Channels ---
    @Property(str)
    def imageChannel(self):
        """
        The channel address in use for the image data .

        Returns
        -------
        str
            Channel address
        """
        if self._imagechannel:
            return str(self._imagechannel.address)
        else:
            return ''

    @imageChannel.setter
    def imageChannel(self, value):
        """
        The channel address in use for the image data .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._imagechannel != value:
            # Disconnect old channel
            if self._imagechannel:
                self._imagechannel.disconnect()
            # Create and connect new channel
            self._imagechannel = PyDMChannel(
                address=value,
                connection_slot=self.image_connection_state_changed,
                value_slot=self.image_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[0] = self._imagechannel
            self._imagechannel.connect()

    @Property(str)
    def xAxisChannel(self):
        """
        The channel address in use for the x-axis of image.

        Returns
        -------
        str
            Channel address
        """
        if self._xaxischannel:
            return str(self._xaxischannel.address)
        else:
            return ''

    @xAxisChannel.setter
    def xAxisChannel(self, value):
        """
        The channel address in use for the x-axis of image.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._xaxischannel != value:
            # Disconnect old channel
            if self._xaxischannel:
                self._xaxischannel.disconnect()
            # Create and connect new channel
            self._xaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.connectionStateChanged,
                value_slot=self.xaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[1] = self._xaxischannel
            self._xaxischannel.connect()

    @Property(str)
    def yAxisChannel(self):
        """
        The channel address in use for the time axis.

        Returns
        -------
        str
            Channel address
        """
        if self._yaxischannel:
            return str(self._yaxischannel.address)
        else:
            return ''

    @yAxisChannel.setter
    def yAxisChannel(self, value):
        """
        The channel address in use for the time axis.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._yaxischannel != value:
            # Disconnect old channel
            if self._yaxischannel:
                self._yaxischannel.disconnect()
            # Create and connect new channel
            self._yaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.yaxis_connection_state_changed,
                value_slot=self.yaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[2] = self._yaxischannel
            self._yaxischannel.connect()

    @Property(str)
    def ROIOffsetXChannel(self):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetxchannel:
            return str(self._roioffsetxchannel.address)
        else:
            return ''

    @ROIOffsetXChannel.setter
    def ROIOffsetXChannel(self, value):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetxchannel != value:
            # Disconnect old channel
            if self._roioffsetxchannel:
                self._roioffsetxchannel.disconnect()
            # Create and connect new channel
            self._roioffsetxchannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsetx_connection_state_changed,
                value_slot=self.roioffsetx_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[3] = self._roioffsetxchannel
            self._roioffsetxchannel.connect()

    @Property(str)
    def ROIOffsetYChannel(self):
        """
        Return the channel address in use for the image ROI vertical offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetychannel:
            return str(self._roioffsetychannel.address)
        else:
            return ''

    @ROIOffsetYChannel.setter
    def ROIOffsetYChannel(self, value):
        """
        Return the channel address in use for the image ROI vertical offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetychannel != value:
            # Disconnect old channel
            if self._roioffsetychannel:
                self._roioffsetychannel.disconnect()
            # Create and connect new channel
            self._roioffsetychannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsety_connection_state_changed,
                value_slot=self.roioffsety_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[4] = self._roioffsetychannel
            self._roioffsetychannel.connect()

    @Property(str)
    def ROIWidthChannel(self):
        """
        Return the channel address in use for the image ROI width.

        Returns
        -------
        str
            Channel address

        """
        if self._roiwidthchannel:
            return str(self._roiwidthchannel.address)
        else:
            return ''

    @ROIWidthChannel.setter
    def ROIWidthChannel(self, value):
        """
        Return the channel address in use for the image ROI width.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiwidthchannel != value:
            # Disconnect old channel
            if self._roiwidthchannel:
                self._roiwidthchannel.disconnect()
            # Create and connect new channel
            self._roiwidthchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiwidth_connection_state_changed,
                value_slot=self.roiwidth_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[5] = self._roiwidthchannel
            self._roiwidthchannel.connect()

    @Property(str)
    def ROIHeightChannel(self):
        """
        Return the channel address in use for the image ROI height.

        Returns
        -------
        str
            Channel address

        """
        if self._roiheightchannel:
            return str(self._roiheightchannel.address)
        else:
            return ''

    @ROIHeightChannel.setter
    def ROIHeightChannel(self, value):
        """
        Return the channel address in use for the image ROI height.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiheightchannel != value:
            # Disconnect old channel
            if self._roiheightchannel:
                self._roiheightchannel.disconnect()
            # Create and connect new channel
            self._roiheightchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiheight_connection_state_changed,
                value_slot=self.roiheight_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[6] = self._roiheightchannel
            self._roiheightchannel.connect()

    def channels(self):
        """
        Return the channels being used for this Widget.

        Returns
        -------
        channels : list
            List of PyDMChannel objects
        """
        return self._channels

    def channels_for_tools(self):
        """Return channels for tools."""
        return [self._imagechannel]
 def image_in_vb(name=None) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]:
     im = ImageItem()
     vb = ViewBox(invertY=True, lockAspect=True, name=name)
     vb.addItem(im)
     hist = HistogramLUTItem(im)
     return im, vb, hist
Example #10
0
class MIMiniImageView(GraphicsLayout, BadDataOverlay):
    def __init__(self, name: str = "MIMiniImageView"):
        super().__init__()

        self.name = name.title()
        self.im = ImageItem()
        self.vb = ViewBox(invertY=True, lockAspect=True, name=name)
        self.vb.addItem(self.im)
        self.hist = HistogramLUTItem(self.im)
        graveyard.append(self.vb)

        # Sub-layout prevents resizing issues when details text changes
        image_layout = self.addLayout(colspan=2)
        image_layout.addItem(self.vb)
        image_layout.addItem(self.hist)
        self.hist.setFixedWidth(100)  # HistogramLUTItem used pixel sizes

        self.nextRow()
        self.details = self.addLabel("", colspan=2)
        self.im.hoverEvent = lambda ev: self.mouse_over(ev)

        self.axis_siblings: "WeakSet[MIMiniImageView]" = WeakSet()
        self.histogram_siblings: "WeakSet[MIMiniImageView]" = WeakSet()

    @property
    def image_item(self) -> ImageItem:
        return self.im

    @property
    def viewbox(self) -> ViewBox:
        return self.vb

    def clear(self):
        self.im.clear()

    def setImage(self, *args, **kwargs):
        self.im.setImage(*args, **kwargs)
        self.check_for_bad_data()

    @staticmethod
    def set_siblings(sibling_views: List["MIMiniImageView"],
                     axis=False,
                     hist=False):
        for view1 in sibling_views:
            for view2 in sibling_views:
                if view2 is not view1:
                    if axis:
                        view1.add_axis_sibling(view2)
                    if hist:
                        view1.add_hist_sibling(view2)

    def add_axis_sibling(self, sibling: "MIMiniImageView"):
        self.axis_siblings.add(sibling)

    def add_hist_sibling(self, sibling: "MIMiniImageView"):
        self.histogram_siblings.add(sibling)

    def get_parts(self) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]:
        return self.im, self.vb, self.hist

    def mouse_over(self, ev):
        # Ignore events triggered by leaving window or right clicking
        if ev.exit:
            return
        pos = CloseEnoughPoint(ev.pos())

        self.show_value(pos)
        for img_view in self.axis_siblings:
            img_view.show_value(pos)

    def show_value(self, pos):
        image = self.im.image
        if image is not None and pos.y < image.shape[
                0] and pos.x < image.shape[1]:
            pixel_value = image[pos.y, pos.x]
            value_string = ("%.6f" % pixel_value)[:8]
            self.details.setText(f"{self.name}: {value_string}")

    def link_sibling_axis(self):
        # Linking multiple viewboxes with locked aspect ratios causes
        # odd resizing behaviour. Use workaround from
        # https://github.com/pyqtgraph/pyqtgraph/issues/1348
        self.vb.setAspectLocked(True)
        for view1, view2 in pairwise(chain([self], self.axis_siblings)):
            view2.vb.linkView(ViewBox.XAxis, view1.vb)
            view2.vb.linkView(ViewBox.YAxis, view1.vb)
            view2.vb.setAspectLocked(False)

    def unlink_sibling_axis(self):
        for img_view in chain([self], self.axis_siblings):
            img_view.vb.linkView(ViewBox.XAxis, None)
            img_view.vb.linkView(ViewBox.YAxis, None)
            img_view.vb.setAspectLocked(True)

    def link_sibling_histogram(self):
        for view1, view2 in pairwise(chain([self], self.histogram_siblings)):
            view1.hist.vb.linkView(ViewBox.YAxis, view2.hist.vb)
        for img_view in chain([self], self.histogram_siblings):
            img_view.hist.sigLevelChangeFinished.connect(
                img_view.update_sibling_histograms)

    def unlink_sibling_histogram(self):
        for img_view in chain([self], self.histogram_siblings):
            img_view.hist.vb.linkView(ViewBox.YAxis, None)
            try:
                img_view.hist.sigLevelChangeFinished.disconnect()
            except TypeError:
                # This is expected if there are slots currently connected
                pass

    def update_sibling_histograms(self):
        hist_range = self.hist.getLevels()
        for img_view in self.histogram_siblings:
            with BlockQtSignals(img_view.hist):
                img_view.hist.setLevels(*hist_range)
 def image_in_vb(self, name=None):
     im = ImageItem()
     im.setAutoDownsample(False)
     vb = ViewBox(invertY=True, lockAspect=True, name=name)
     vb.addItem(im)
     return im, vb
Example #12
0
class SiriusTimePlot(PyDMTimePlot):
    """PyDMTimePlot with some extra features."""

    bufferReset = Signal()
    timeSpanChanged = Signal()

    def __init__(self, *args, show_tooltip=False, **kws):
        super().__init__(*args, **kws)
        self._filled_with_arch_data = dict()
        self._show_tooltip = show_tooltip

        self.vb2 = ViewBox()
        self.plotItem.scene().addItem(self.vb2)
        self.vb2.setXLink(self.plotItem)
        self.plotItem.getAxis('right').linkToView(self.vb2)
        self._updateViews()
        self.plotItem.vb.sigResized.connect(self._updateViews)

        self.carch = None

        # show auto adjust button
        self.plotItem.showButtons()

        # use pan mouse mode (3-button)
        self.plotItem.getViewBox().setMouseMode(ViewBox.PanMode)

        # connect sigMouseMoved
        self.plotItem.scene().sigMouseMoved.connect(self._handle_mouse_moved)

        # add new actions to menu
        rst_act = QAction("Clear buffers")
        rst_act.triggered.connect(self._resetBuffers)
        tsp_act = QAction("Change time span")
        tsp_act.triggered.connect(self._changeTimeSpan)
        self.plotItem.scene().contextMenu.extend([rst_act, tsp_act])

    @Property(bool)
    def showToolTip(self):
        """
        Whether to show or not tooltip with curve values.

        Returns
        -------
        use : bool
            Tooltip enable status in use
        """
        return self._show_tooltip

    @showToolTip.setter
    def showToolTip(self, new_show):
        """
        Whether to show or not tooltip with curve values.

        Parameters
        ----------
        new_show : bool
            The new tooltip enable status to use
        """
        self._show_tooltip = new_show

    def addCurve(self, plot_item, axis='left', curve_color=None):
        """Reimplement to use right axis."""
        if curve_color is None:
            curve_color = utilities.colors.default_colors[len(
                self._curves) % len(utilities.colors.default_colors)]
            plot_item.color_string = curve_color
        self._curves.append(plot_item)
        if axis == 'left':
            self.plotItem.addItem(plot_item)
        elif axis == 'right':
            if not self.plotItem.getAxis('right').isVisible():
                self.plotItem.showAxis('right')
            self.vb2.addItem(plot_item)
        else:
            raise ValueError('Choose a valid axis!')

        # Connect channels
        for chan in plot_item.channels():
            if chan:
                chan.connect()

    def addYChannel(self,
                    y_channel=None,
                    name=None,
                    color=None,
                    lineStyle=None,
                    lineWidth=None,
                    symbol=None,
                    symbolSize=None,
                    axis='left'):
        """Reimplement to use SiriusTimePlotItem and right axis."""
        plot_opts = dict()
        plot_opts['symbol'] = symbol
        if symbolSize is not None:
            plot_opts['symbolSize'] = symbolSize
        if lineStyle is not None:
            plot_opts['lineStyle'] = lineStyle
        if lineWidth is not None:
            plot_opts['lineWidth'] = lineWidth

        # Add curve
        new_curve = SiriusTimePlotItem(
            self,
            y_channel,
            plot_by_timestamps=self._plot_by_timestamps,
            name=name,
            color=color,
            **plot_opts)
        new_curve.setUpdatesAsynchronously(self.updatesAsynchronously)
        new_curve.setBufferSize(self._bufferSize, initialize_buffer=True)

        self.update_timer.timeout.connect(new_curve.asyncUpdate)
        self.addCurve(new_curve, axis, curve_color=color)

        new_curve.data_changed.connect(self.set_needs_redraw)
        self.redraw_timer.start()

        return new_curve

    def updateXAxis(self, update_immediately=False):
        """Reimplement to show only existing range."""
        if len(self._curves) == 0:
            return

        if self._plot_by_timestamps:
            if self._update_mode == PyDMTimePlot.SynchronousMode:
                maxrange = max([curve.max_x() for curve in self._curves])
            else:
                maxrange = time.time()

            mini = Time.now().timestamp()
            for curve in self._curves:
                firstvalid = (curve.data_buffer[0] != 0).argmax()
                if curve.data_buffer[0, firstvalid] == 0:
                    continue
                mini = min(mini, curve.data_buffer[0, firstvalid])
            minrange = max(maxrange - self._time_span, mini)

            self.plotItem.setXRange(minrange,
                                    maxrange,
                                    padding=0.0,
                                    update=update_immediately)
        else:
            diff_time = self.starting_epoch_time - \
                max([curve.max_x() for curve in self._curves])
            if diff_time > DEFAULT_X_MIN:
                diff_time = DEFAULT_X_MIN
            self.getViewBox().setLimits(minXRange=diff_time)

    def _updateViews(self):
        self.vb2.setGeometry(self.plotItem.vb.sceneBoundingRect())
        self.vb2.linkedViewChanged(self.plotItem.vb, self.vb2.XAxis)

    def _get_value_from_arch(self, pvname, t_init, t_end, process_type,
                             process_bin_intvl):
        """Get values from archiver."""
        if self.carch is None:
            self.carch = ClientArchiver()
        self.carch.timeout = 120
        data = self.carch.getData(pvname, t_init, t_end, process_type,
                                  process_bin_intvl)
        if not data:
            return
        return data['timestamp'], data['value']

    def fill_curve_with_archdata(self,
                                 curve,
                                 pvname,
                                 t_init,
                                 t_end,
                                 factor=None,
                                 process_type='',
                                 process_bin_intvl=None):
        """Fill curve with archiver data."""
        data = self._get_value_from_arch(pvname, t_init, t_end, process_type,
                                         process_bin_intvl)
        if not data:
            return
        datax, datay = data
        self.fill_curve_buffer(curve, datax, datay, factor)

        self._filled_with_arch_data[pvname] = dict(
            curve=curve,
            factor=factor,
            process_type=process_type,
            process_bin_intvl=process_bin_intvl)

    def fill_curve_buffer(self, curve, datax, datay, factor=None):
        """Fill curve buffer."""
        nrpts = len(datax)
        if not nrpts:
            return
        buff = _np.zeros((2, self.bufferSize), order='f', dtype=float)
        if nrpts > self.bufferSize:
            smpls2discard = nrpts - self.bufferSize
            datax = datax[smpls2discard:]
            datay = datay[smpls2discard:]
            nrpts = len(datax)
        firstsmpl2fill = self.bufferSize - nrpts
        buff[0, firstsmpl2fill:] = datax
        buff[1, firstsmpl2fill:] = datay
        if factor:
            buff[1, :] /= factor
        curve.data_buffer = buff
        curve.points_accumulated = nrpts
        curve._min_y_value = min(datay)
        curve._max_y_value = max(datay)
        curve.latest_value = datay[-1]

    def _resetBuffers(self):
        for curve in self._curves:
            curve.initialize_buffer()
        self.bufferReset.emit()

    def _changeTimeSpan(self):
        new_time_span, ok = QInputDialog.getInt(
            self, 'Input', 'Set new time span value [s]: ')
        if not ok:
            return

        if new_time_span > self.timeSpan:
            t_end = Time.now()
            t_init = t_end - new_time_span
            for pvname, info in self._filled_with_arch_data.items():
                self.fill_curve_with_archdata(info['curve'], pvname,
                                              t_init.get_iso8601(),
                                              t_end.get_iso8601(),
                                              info['factor'],
                                              info['process_type'],
                                              info['process_bin_intvl'])

        self.timeSpan = new_time_span
        self.timeSpanChanged.emit()

    def _handle_mouse_moved(self, pos):
        """Show tooltip at mouse move."""
        if not self._show_tooltip:
            return

        # create label tooltip, if needed
        if not hasattr(self, 'label_tooltip'):
            self.label_tooltip = QLabel(self, Qt.ToolTip)
            self.timer_tooltip = QTimer(self)
            self.timer_tooltip.timeout.connect(self.label_tooltip.hide)
            self.timer_tooltip.setInterval(1000)

        # find nearest curve point
        nearest = (self._curves[0], _np.inf, None, None)
        for idx, curve in enumerate(self._curves):
            if not curve.isVisible():
                continue
            mappos = curve.mapFromScene(pos)
            posx, posy = mappos.x(), mappos.y()
            xData, yData = curve.curve.xData, curve.curve.yData
            if not xData.size:
                continue
            diffx = xData - posx
            idx = _np.argmin(_np.abs(diffx))
            if diffx[idx] < 0.5:
                valx, valy = xData[idx], yData[idx]
                diffy = abs(valy - posy)
                if diffy < nearest[1]:
                    nearest = (curve, diffy, valx, valy)

        # show tooltip
        curve, diffy, valx, valy = nearest
        ylimts = self.getViewBox().state['viewRange'][1]
        ydelta = ylimts[1] - ylimts[0]
        if diffy < 1e-2 * ydelta:
            txt = Time(timestamp=valx).get_iso8601() + '\n'
            txt += f'{curve.name()}: {valy:.3f}'
            font = QApplication.instance().font()
            font.setPointSize(font.pointSize() - 10)
            palette = QPalette()
            palette.setColor(QPalette.WindowText, curve.color)
            self.label_tooltip.setText(txt)
            self.label_tooltip.setFont(font)
            self.label_tooltip.setPalette(palette)
            self.label_tooltip.move(self.mapToGlobal(pos.toPoint()))
            self.label_tooltip.show()
            self.timer_tooltip.start()
            curve.scatter.setData(pos=[
                (valx, valy),
            ],
                                  symbol='o',
                                  size=15,
                                  brush=mkBrush(curve.color))
            curve.scatter.show()
Example #13
0
class SideView(GraphicsView):
    """SideView class

    This class contains a series of functions allowing the user to simultaneously view the 3D volume from all orientations. 

    Args:
        diff (int): Flag indicating the different brain view orientations. 
        parent (class): Base or parent class

    """
    def __init__(self, diff, parent=None):
        super(SideView, self).__init__(parent=parent)
        self.parent = parent
        self.brain = parent.brain
        self.diff = diff

        self.view1 = ViewBox()
        self.setCentralItem(self.view1)

        # Making Images out of data
        self.brain.section = (self.brain.section + self.diff) % 3
        self.i = int(self.brain.shape[self.brain.section] / 2)
        data_slice = self.brain.get_data_slice(self.i)
        self.brain_img1 = ImageItem(
            data_slice,
            autoDownsample=False,
            compositionMode=QtGui.QPainter.CompositionMode_SourceOver)
        self.brain.section = (self.brain.section - self.diff) % 3

        self.view1.addItem(self.brain_img1)
        self.view1.setAspectLocked(True)

        self.view1.setFixedHeight(250)
        self.view1.setFixedWidth(250)
        self.setMinimumHeight(250)

        self.vLine = InfiniteLine(angle=90, movable=False)
        self.hLine = InfiniteLine(angle=0, movable=False)
        self.vLine.setVisible(False)
        self.hLine.setVisible(False)
        self.view1.addItem(self.vLine, ignoreBounds=True)
        self.view1.addItem(self.hLine, ignoreBounds=True)

    def refresh_image(self):
        """Refresh Image

        This function refreshes the displayed volume orientation image.
        """
        self.brain.section = (self.brain.section + self.diff) % 3
        data_slice = self.brain.get_data_slice(self.i)
        self.brain_img1.setImage(data_slice)
        self.brain.section = (self.brain.section - self.diff) % 3

    def refresh_all_images(self):
        """Refresh all images

        This function refreshes both the side view images when triggered.
        """
        self.parent.main_widget._update_section_helper()
        self.parent.win1.refresh_image()
        self.parent.win1.view1.menu.actions()[0].trigger()
        self.parent.win2.refresh_image()
        self.parent.win2.view1.menu.actions()[0].trigger()

    def mouseDoubleClickEvent(self, event):
        """Click trigger

        Tracks a mouse double click event which triggers the refreshing of all the views

        Args
            event (event): Mouse double click events for the widget.
        """
        super(SideView, self).mouseDoubleClickEvent(event)
        self.brain.section = (self.brain.section + self.diff) % 3
        self.refresh_all_images()

    def set_i(self, position, out_of_box=False):
        """Set position

        This function sets the cursor position for each window.

        Args:
            position (tuple): Tuple containing the required coordinates.
            out_of_box (bool): Flag indicating if event possition is outisde of the considered volume.
        """
        section = (self.brain.section + self.diff) % 3
        if out_of_box:
            self.i = int(self.brain.shape[section] / 2)
            self.vLine.setVisible(False)
            self.hLine.setVisible(False)
        else:
            i = position[section]
            self.i = np.clip(i, 0, self.brain.shape[section] - 1)
            self.vLine.setVisible(True)
            self.hLine.setVisible(True)

            self.brain.section = (self.brain.section + self.diff) % 3
            x, y = self.brain.voxel_as_position(position[0], position[1],
                                                position[2])
            self.brain.section = (self.brain.section - self.diff) % 3
            self.vLine.setPos(x)
            self.hLine.setPos(y)
    def __init__(self, directory='.', **kwargs):
        super(astraPlotWidget, self).__init__(**kwargs)
        self.beam = raf.beam()
        self.twiss = rtf.twiss()
        self.directory = directory
        ''' twissPlotWidget '''
        self.twissPlotView = GraphicsView(useOpenGL=True)
        self.twissPlotWidget = GraphicsLayout()
        self.twissPlotView.setCentralItem(self.twissPlotWidget)

        self.latticePlotData = imageio.imread('lattice_plot.png')
        self.latticePlots = {}
        self.twissPlots = {}
        i = -1
        for entry in self.twissplotLayout:
            if entry == 'next_row':
                self.twissPlotWidget.nextRow()
            else:
                i += 1
                p = self.twissPlotWidget.addPlot(title=entry['name'])
                p.showGrid(x=True, y=True)
                vb = p.vb
                vb.setYRange(*entry['range'])
                latticePlot = ImageItem(self.latticePlotData)
                latticePlot.setOpts(axisOrder='row-major')
                vb.addItem(latticePlot)
                latticePlot.setZValue(-1)  # make sure this image is on top
                # latticePlot.setOpacity(0.5)
                self.twissPlots[entry['name']] = p.plot(
                    pen=mkPen('b', width=3))
                self.latticePlots[p.vb] = latticePlot
                p.vb.sigRangeChanged.connect(self.scaleLattice)
        ''' beamPlotWidget '''
        self.beamPlotWidget = QWidget()
        self.beamPlotLayout = QVBoxLayout()
        self.item = ImageItem()
        self.beamPlotWidget.setLayout(self.beamPlotLayout)
        self.beamPlotView = ImageView(imageItem=self.item)
        self.rainbow = rainbow()
        self.item.setLookupTable(self.rainbow)
        self.item.setLevels([0, 1])
        # self.beamPlotWidgetGraphicsLayout = GraphicsLayout()
        # p = self.beamPlotWidgetGraphicsLayout.addPlot(title='beam')
        # p.showGrid(x=True, y=True)
        # self.beamPlot = p.plot(pen=None, symbol='+')
        # self.beamPlotView.setCentralItem(self.beamPlotWidgetGraphicsLayout)
        self.beamPlotXAxisCombo = QComboBox()
        self.beamPlotXAxisCombo.addItems(
            ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma'])
        self.beamPlotYAxisCombo = QComboBox()
        self.beamPlotYAxisCombo.addItems(
            ['x', 'y', 'zn', 'cpx', 'cpy', 'BetaGamma'])
        self.beamPlotNumberBins = QSpinBox()
        self.beamPlotNumberBins.setRange(10, 500)
        self.beamPlotNumberBins.setSingleStep(10)
        self.histogramBins = 100
        self.beamPlotNumberBins.setValue(self.histogramBins)
        self.beamPlotAxisWidget = QWidget()
        self.beamPlotAxisLayout = QHBoxLayout()
        self.beamPlotAxisWidget.setLayout(self.beamPlotAxisLayout)
        self.beamPlotAxisLayout.addWidget(self.beamPlotXAxisCombo)
        self.beamPlotAxisLayout.addWidget(self.beamPlotYAxisCombo)
        self.beamPlotAxisLayout.addWidget(self.beamPlotNumberBins)
        self.beamPlotXAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.beamPlotYAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.beamPlotNumberBins.valueChanged.connect(self.plotDataBeam)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.beamPlotLayout.addWidget(self.beamPlotAxisWidget)
        self.beamPlotLayout.addWidget(self.beamPlotView)
        ''' slicePlotWidget '''
        self.sliceParams = [
            {
                'name': 'slice_normalized_horizontal_emittance',
                'units': 'm-rad',
                'text': 'enx'
            },
            {
                'name': 'slice_normalized_vertical_emittance',
                'units': 'm-rad',
                'text': 'eny'
            },
            {
                'name': 'slice_peak_current',
                'units': 'A',
                'text': 'PeakI'
            },
            {
                'name': 'slice_relative_momentum_spread',
                'units': '%',
                'text': 'sigma-p'
            },
        ]
        self.slicePlotWidget = QWidget()
        self.slicePlotLayout = QVBoxLayout()
        self.slicePlotWidget.setLayout(self.slicePlotLayout)
        # self.slicePlotView = GraphicsView(useOpenGL=True)
        self.slicePlotWidgetGraphicsLayout = GraphicsLayoutWidget()
        # self.slicePlots = {}
        self.slicePlotCheckbox = {}
        self.curve = {}
        self.sliceaxis = {}
        self.slicePlotCheckboxWidget = QWidget()
        self.slicePlotCheckboxLayout = QVBoxLayout()
        self.slicePlotCheckboxWidget.setLayout(self.slicePlotCheckboxLayout)
        self.slicePlot = self.slicePlotWidgetGraphicsLayout.addPlot(
            title='Slice', row=0, col=50)
        self.slicePlot.showAxis('left', False)
        self.slicePlot.showGrid(x=True, y=True)
        i = -1
        colors = ['b', 'r', 'g', 'k']
        for param in self.sliceParams:
            i += 1
            axis = AxisItem("left")
            labelStyle = {'color': '#' + colorStr(mkColor(colors[i]))[0:-2]}
            axis.setLabel(text=param['text'],
                          units=param['units'],
                          **labelStyle)
            viewbox = ViewBox()
            axis.linkToView(viewbox)
            viewbox.setXLink(self.slicePlot.vb)
            self.sliceaxis[param['name']] = [axis, viewbox]
            self.curve[param['name']] = PlotDataItem(pen=colors[i], symbol='+')
            viewbox.addItem(self.curve[param['name']])
            col = self.findFirstEmptyColumnInGraphicsLayout()
            self.slicePlotWidgetGraphicsLayout.ci.addItem(axis,
                                                          row=0,
                                                          col=col,
                                                          rowspan=1,
                                                          colspan=1)
            self.slicePlotWidgetGraphicsLayout.ci.addItem(viewbox,
                                                          row=0,
                                                          col=50)
            p.showGrid(x=True, y=True)
            # self.slicePlots[param] = self.slicePlot.plot(pen=colors[i], symbol='+')
            self.slicePlotCheckbox[param['name']] = QCheckBox(param['text'])
            self.slicePlotCheckboxLayout.addWidget(
                self.slicePlotCheckbox[param['name']])
            self.slicePlotCheckbox[param['name']].stateChanged.connect(
                self.plotDataSlice)
        # self.slicePlotView.setCentralItem(self.slicePlotWidgetGraphicsLayout)
        self.slicePlotSliceWidthWidget = QSpinBox()
        self.slicePlotSliceWidthWidget.setMaximum(1000)
        self.slicePlotSliceWidthWidget.setValue(100)
        self.slicePlotSliceWidthWidget.setSingleStep(10)
        self.slicePlotSliceWidthWidget.setSuffix("fs")
        self.slicePlotSliceWidthWidget.setSpecialValueText('Automatic')
        self.slicePlotAxisWidget = QWidget()
        self.slicePlotAxisLayout = QHBoxLayout()
        self.slicePlotAxisWidget.setLayout(self.slicePlotAxisLayout)
        self.slicePlotAxisLayout.addWidget(self.slicePlotCheckboxWidget)
        self.slicePlotAxisLayout.addWidget(self.slicePlotSliceWidthWidget)
        # self.slicePlotXAxisCombo.currentIndexChanged.connect(self.plotDataSlice)
        self.slicePlotSliceWidthWidget.valueChanged.connect(
            self.changeSliceLength)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.slicePlotLayout.addWidget(self.slicePlotAxisWidget)
        self.slicePlotLayout.addWidget(self.slicePlotWidgetGraphicsLayout)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        self.tabWidget = QTabWidget()

        self.folderButton = QPushButton('Select Directory')
        self.folderLineEdit = QLineEdit()
        self.folderLineEdit.setReadOnly(True)
        self.folderLineEdit.setText(self.directory)
        self.reloadButton = QPushButton()
        self.reloadButton.setIcon(qApp.style().standardIcon(
            QStyle.SP_BrowserReload))
        self.folderWidget = QGroupBox()
        self.folderLayout = QHBoxLayout()
        self.folderLayout.addWidget(self.folderButton)
        self.folderLayout.addWidget(self.folderLineEdit)
        self.folderLayout.addWidget(self.reloadButton)
        self.folderWidget.setLayout(self.folderLayout)
        self.folderWidget.setMaximumWidth(800)
        self.reloadButton.clicked.connect(
            lambda: self.changeDirectory(self.directory))
        self.folderButton.clicked.connect(self.changeDirectory)

        self.fileSelector = QComboBox()
        self.fileSelector.currentIndexChanged.connect(self.updateScreenCombo)
        self.screenSelector = QComboBox()
        self.screenSelector.currentIndexChanged.connect(self.changeScreen)
        self.beamWidget = QGroupBox()
        self.beamLayout = QHBoxLayout()
        self.beamLayout.addWidget(self.fileSelector)
        self.beamLayout.addWidget(self.screenSelector)
        self.beamWidget.setLayout(self.beamLayout)
        self.beamWidget.setMaximumWidth(800)
        self.beamWidget.setVisible(False)

        self.folderBeamWidget = QWidget()
        self.folderBeamLayout = QHBoxLayout()
        self.folderBeamLayout.setAlignment(Qt.AlignLeft)
        self.folderBeamWidget.setLayout(self.folderBeamLayout)
        self.folderBeamLayout.addWidget(self.folderWidget)
        self.folderBeamLayout.addWidget(self.beamWidget)

        self.tabWidget.addTab(self.twissPlotView, 'Twiss Plots')
        self.tabWidget.addTab(self.beamPlotWidget, 'Beam Plots')
        self.tabWidget.addTab(self.slicePlotWidget, 'Slice Beam Plots')
        self.tabWidget.currentChanged.connect(self.changeTab)
        self.layout.addWidget(self.folderBeamWidget)
        self.layout.addWidget(self.tabWidget)

        self.plotType = 'Twiss'
        self.changeDirectory(self.directory)
    def __init__(self, directory='.', **kwargs):
        super(astraPlotWidget, self).__init__(**kwargs)
        self.beam = raf.beam()
        self.twiss = rtf.twiss()
        self.directory = directory
        ''' twissPlotWidget '''
        self.twissPlotView = GraphicsView(useOpenGL=True)
        self.twissPlotWidget = GraphicsLayout()
        self.twissPlotView.setCentralItem(self.twissPlotWidget)

        self.latticePlotData = imageio.imread(
            os.path.dirname(os.path.abspath(__file__)) + '/lattice_plot.png')
        self.latticePlots = {}
        self.twissPlots = {}
        i = -1
        for entry in self.twissplotLayout:
            if entry == 'next_row':
                self.twissPlotWidget.nextRow()
            else:
                i += 1
                p = self.twissPlotWidget.addPlot(title=entry['name'])
                p.showGrid(x=True, y=True)
                vb = p.vb
                vb.setYRange(*entry['range'])
                latticePlot = ImageItem(self.latticePlotData)
                latticePlot.setOpts(axisOrder='row-major')
                vb.addItem(latticePlot)
                latticePlot.setZValue(-1)  # make sure this image is on top
                # latticePlot.setOpacity(0.5)
                self.twissPlots[entry['name']] = p.plot(
                    pen=mkPen('b', width=3))
                self.latticePlots[p.vb] = latticePlot
                p.vb.sigRangeChanged.connect(self.scaleLattice)
        ''' beamPlotWidget '''
        self.beamPlotWidget = QWidget()
        self.beamPlotLayout = QVBoxLayout()
        self.beamPlotWidget.setLayout(self.beamPlotLayout)

        #
        # self.beamPlotChoice =
        #

        self.beamPlotAxisWidget = QWidget()
        self.beamPlotAxisWidget.setMaximumHeight(100)
        Form = self.beamPlotAxisWidget

        self.beamPlotXAxisDict = OrderedDict()
        self.beamPlotXAxisDict['x'] = {'scale': 1e3, 'axis': 'x [mm]'}
        self.beamPlotXAxisDict['y'] = {'scale': 1e3, 'axis': 'y [mm]'}
        self.beamPlotXAxisDict['z'] = {
            'scale': 1e6,
            'axis': 'z [micron]',
            'norm': True
        }
        self.beamPlotXAxisDict['t'] = {
            'scale': 1e12,
            'axis': 't [ps]',
            'norm': True
        }
        self.beamPlotXAxisDict['cpx'] = {'scale': 1e3, 'axis': 'cpx [keV]'}
        self.beamPlotXAxisDict['cpy'] = {'scale': 1e3, 'axis': 'cpy [keV]'}
        self.beamPlotXAxisDict['BetaGamma'] = {
            'scale': 0.511,
            'axis': 'cp [MeV]'
        }

        # Form.setObjectName(("Form"))
        # Form.resize(874, 212)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth())
        Form.setSizePolicy(sizePolicy)
        self.horizontalLayout = QHBoxLayout(Form)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.beamPlotXAxisCombo = QComboBox(Form)
        self.beamPlotXAxisCombo.addItems(list(self.beamPlotXAxisDict.keys()))
        self.beamPlotXAxisCombo.setCurrentIndex(2)
        self.horizontalLayout.addWidget(self.beamPlotXAxisCombo)
        self.beamPlotYAxisCombo = QComboBox(Form)
        self.beamPlotYAxisCombo.addItems(list(self.beamPlotXAxisDict.keys()))
        self.beamPlotYAxisCombo.setCurrentIndex(6)
        self.horizontalLayout.addWidget(self.beamPlotYAxisCombo)
        self.groupBox = QGroupBox(Form)
        self.groupBox.setObjectName("groupBox")
        self.formLayout = QFormLayout(self.groupBox)
        self.formLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        self.formLayout.setObjectName("formLayout")
        self.chooseSliceWidth = QRadioButton(self.groupBox)
        self.chooseSliceWidth.setObjectName(("chooseSliceWidth"))
        self.formLayout.setWidget(0, QFormLayout.LabelRole,
                                  self.chooseSliceWidth)
        self.sliceWidth = QDoubleSpinBox(self.groupBox)
        self.sliceWidth.setDecimals(6)
        self.sliceWidth.setObjectName("sliceWidth")
        self.formLayout.setWidget(0, QFormLayout.FieldRole, self.sliceWidth)
        self.chooseSliceNumber = QRadioButton(self.groupBox)
        self.chooseSliceNumber.setChecked(True)
        self.chooseSliceNumber.setObjectName("chooseSliceNumber")
        self.formLayout.setWidget(1, QFormLayout.LabelRole,
                                  self.chooseSliceNumber)
        self.sliceNumber = QSpinBox(self.groupBox)
        self.sliceNumber.setObjectName(("sliceNumber"))
        self.formLayout.setWidget(1, QFormLayout.FieldRole, self.sliceNumber)
        self.horizontalLayout.addWidget(self.groupBox)
        self.chooseSliceWidth.setText(_translate("Form", "Slice Width", None))
        self.chooseSliceNumber.setText(
            _translate("Form", "Number of Slices", None))
        self.sliceWidth.setRange(1e-6, 1)
        self.sliceWidth.setSingleStep(0.00001)
        self.histogramWidth = 0.0005
        self.sliceWidth.setValue(self.histogramWidth)
        # self.sliceNumber = QSpinBox()
        self.sliceNumber.setRange(10, 10000)
        self.sliceNumber.setSingleStep(10)
        self.histogramBins = 100
        self.sliceNumber.setValue(self.histogramBins)

        # self.beamPlotXAxisCombo = QComboBox()

        # self.beamPlotAxisLayout = QHBoxLayout()
        # self.beamPlotAxisWidget.setLayout(self.beamPlotAxisLayout)

        # self.beamPlotAxisLayout.addWidget(self.beamPlotXAxisCombo)
        # self.beamPlotAxisLayout.addWidget(self.beamPlotYAxisCombo)
        # self.beamPlotAxisLayout.addWidget(self.beamPlotNumberBins)
        self.beamPlotXAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.beamPlotYAxisCombo.currentIndexChanged.connect(self.plotDataBeam)
        self.chooseSliceNumber.toggled.connect(self.plotDataBeam)
        self.sliceNumber.valueChanged.connect(self.plotDataBeam)
        self.sliceWidth.valueChanged.connect(self.plotDataBeam)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.canvasWidget = QWidget()
        l = QVBoxLayout(self.canvasWidget)
        self.sc = MyStaticMplCanvas(self.canvasWidget,
                                    width=1,
                                    height=1,
                                    dpi=150)
        l.addWidget(self.sc)
        self.beamPlotLayout.addWidget(self.beamPlotAxisWidget)
        self.beamPlotLayout.addWidget(self.canvasWidget)
        ''' slicePlotWidget '''
        self.sliceParams = [
            {
                'name': 'slice_normalized_horizontal_emittance',
                'units': 'm-rad',
                'text': 'enx'
            },
            {
                'name': 'slice_normalized_vertical_emittance',
                'units': 'm-rad',
                'text': 'eny'
            },
            {
                'name': 'slice_peak_current',
                'units': 'A',
                'text': 'PeakI'
            },
            {
                'name': 'slice_relative_momentum_spread',
                'units': '%',
                'text': 'sigma-p'
            },
            {
                'name': 'slice_beta_x',
                'units': 'm',
                'text': 'beta_x'
            },
            {
                'name': 'slice_beta_y',
                'units': 'm',
                'text': 'beta_y'
            },
        ]
        self.slicePlotWidget = QWidget()
        self.slicePlotLayout = QVBoxLayout()
        self.slicePlotWidget.setLayout(self.slicePlotLayout)
        # self.slicePlotView = GraphicsView(useOpenGL=True)
        self.slicePlotWidgetGraphicsLayout = GraphicsLayoutWidget()
        # self.slicePlots = {}
        self.slicePlotCheckbox = {}
        self.curve = {}
        self.sliceaxis = {}
        self.slicePlotCheckboxWidget = QWidget()
        self.slicePlotCheckboxLayout = QVBoxLayout()
        self.slicePlotCheckboxWidget.setLayout(self.slicePlotCheckboxLayout)
        self.slicePlot = self.slicePlotWidgetGraphicsLayout.addPlot(
            title='Slice', row=0, col=50)
        self.slicePlot.showAxis('left', False)
        self.slicePlot.showGrid(x=True, y=True)
        i = -1
        colors = ['b', 'r', 'g', 'k', 'y', 'm', 'c']
        for param in self.sliceParams:
            i += 1
            axis = AxisItem("left")
            labelStyle = {'color': '#' + colorStr(mkColor(colors[i]))[0:-2]}
            axis.setLabel(text=param['text'],
                          units=param['units'],
                          **labelStyle)
            viewbox = ViewBox()
            axis.linkToView(viewbox)
            viewbox.setXLink(self.slicePlot.vb)
            self.sliceaxis[param['name']] = [axis, viewbox]
            self.curve[param['name']] = PlotDataItem(pen=colors[i], symbol='+')
            viewbox.addItem(self.curve[param['name']])
            col = self.findFirstEmptyColumnInGraphicsLayout()
            self.slicePlotWidgetGraphicsLayout.ci.addItem(axis,
                                                          row=0,
                                                          col=col,
                                                          rowspan=1,
                                                          colspan=1)
            self.slicePlotWidgetGraphicsLayout.ci.addItem(viewbox,
                                                          row=0,
                                                          col=50)
            p.showGrid(x=True, y=True)
            # self.slicePlots[param] = self.slicePlot.plot(pen=colors[i], symbol='+')
            self.slicePlotCheckbox[param['name']] = QCheckBox(param['text'])
            self.slicePlotCheckboxLayout.addWidget(
                self.slicePlotCheckbox[param['name']])
            self.slicePlotCheckbox[param['name']].stateChanged.connect(
                self.plotDataSlice)
        # self.slicePlotView.setCentralItem(self.slicePlotWidgetGraphicsLayout)
        self.slicePlotSliceWidthWidget = QSpinBox()
        self.slicePlotSliceWidthWidget.setMaximum(500)
        self.slicePlotSliceWidthWidget.setValue(100)
        self.slicePlotSliceWidthWidget.setSingleStep(10)
        self.slicePlotSliceWidthWidget.setSuffix(" slices")
        self.slicePlotSliceWidthWidget.setSpecialValueText('Automatic')
        self.slicePlotAxisWidget = QWidget()
        self.slicePlotAxisLayout = QHBoxLayout()
        self.slicePlotAxisWidget.setLayout(self.slicePlotAxisLayout)
        self.slicePlotAxisLayout.addWidget(self.slicePlotCheckboxWidget)
        self.slicePlotAxisLayout.addWidget(self.slicePlotSliceWidthWidget)
        # self.slicePlotXAxisCombo.currentIndexChanged.connect(self.plotDataSlice)
        self.slicePlotSliceWidthWidget.valueChanged.connect(
            self.changeSliceLength)
        # self.beamPlotXAxisCombo.setCurrentIndex(2)
        # self.beamPlotYAxisCombo.setCurrentIndex(5)
        self.slicePlotLayout.addWidget(self.slicePlotAxisWidget)
        self.slicePlotLayout.addWidget(self.slicePlotWidgetGraphicsLayout)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        self.tabWidget = QTabWidget()

        self.folderButton = QPushButton('Select Directory')
        self.folderLineEdit = QLineEdit()
        self.folderLineEdit.setReadOnly(True)
        self.folderLineEdit.setText(self.directory)
        self.reloadButton = QPushButton()
        self.reloadButton.setIcon(qApp.style().standardIcon(
            QStyle.SP_BrowserReload))
        self.folderWidget = QGroupBox()
        self.folderLayout = QHBoxLayout()
        self.folderLayout.addWidget(self.folderButton)
        self.folderLayout.addWidget(self.folderLineEdit)
        self.folderLayout.addWidget(self.reloadButton)
        self.folderWidget.setLayout(self.folderLayout)
        self.folderWidget.setMaximumWidth(800)
        self.reloadButton.clicked.connect(
            lambda: self.changeDirectory(self.directory))
        self.folderButton.clicked.connect(self.changeDirectory)

        self.fileSelector = QComboBox()
        self.fileSelector.currentIndexChanged.connect(self.updateScreenCombo)
        self.screenSelector = QComboBox()
        self.screenSelector.currentIndexChanged.connect(self.changeScreen)
        self.beamWidget = QGroupBox()
        self.beamLayout = QHBoxLayout()
        self.beamLayout.addWidget(self.fileSelector)
        self.beamLayout.addWidget(self.screenSelector)
        self.beamWidget.setLayout(self.beamLayout)
        self.beamWidget.setMaximumWidth(800)
        self.beamWidget.setVisible(False)

        self.folderBeamWidget = QWidget()
        self.folderBeamLayout = QHBoxLayout()
        self.folderBeamLayout.setAlignment(Qt.AlignLeft)
        self.folderBeamWidget.setLayout(self.folderBeamLayout)
        self.folderBeamLayout.addWidget(self.folderWidget)
        self.folderBeamLayout.addWidget(self.beamWidget)

        self.tabWidget.addTab(self.twissPlotView, 'Twiss Plots')
        self.tabWidget.addTab(self.beamPlotWidget, 'Beam Plots')
        self.tabWidget.addTab(self.slicePlotWidget, 'Slice Beam Plots')
        # self.log = lw.loggerWidget()
        # self.log.addLogger(logger)
        # sys.stdout = lw.redirectLogger(self.log, 'stdout')
        # self.tabWidget.addTab(self.log,'Log')
        self.tabWidget.currentChanged.connect(self.changeTab)
        self.layout.addWidget(self.folderBeamWidget)
        self.layout.addWidget(self.tabWidget)

        self.plotType = 'Twiss'
        self.changeDirectory(self.directory)
Example #16
0
class BlendView(GraphicsView):
    def __init__(self, parent: QWidget):
        GraphicsView.__init__(self, parent)
        layout = GraphicsLayout()
        self.setCentralItem(layout)

        self._image_item = ImageItem()
        self.viewbox = ViewBox(layout, lockAspect=True, invertY=True)
        self.viewbox.addItem(self._image_item)
        layout.addItem(self.viewbox)

        self.scale = ScaleBar(size=10, suffix='μm')
        self.scale.setParentItem(self.viewbox)
        self.scale.anchor((1, 1), (1, 1), offset=(-20, -20))
        self.scale.hide()

        self._show_mask = False
        self.blend_mode = QPainter.CompositionMode_Screen
        self.items: List[ChannelImageItem] = None

    def clear(self):
        self.viewbox.clear()
        self.items = None

    def set_images(self, items: List[ChannelImageItem]):
        self.items = items
        if items is None or len(items) == 0:
            return
        images = []
        for item in items:
            image = scale_image(item.image, item.channel.settings.max, item.channel.settings.levels)
            images.append(array2qimage(image, normalize=False))

        self._draw_images(images)

    def _draw_images(self, images: List[QImage]):
        result = QImage(images[0].size(), QImage.Format_ARGB32_Premultiplied)
        painter = QPainter(result)
        painter.setCompositionMode(QPainter.CompositionMode_Source)
        painter.fillRect(images[0].rect(), Qt.transparent)
        painter.setCompositionMode(self.blend_mode)
        for i in images:
            painter.drawImage(0, 0, i)
        painter.end()

        self._image_item.setImage(rgb_view(result))

    def refresh_images(self):
        self.set_images(self.items)

    def set_blend_mode(self, modename: str):
        self.blend_mode = getattr(QPainter, 'CompositionMode_' + modename)
        self.set_images(self.items)

    def show_scale_bar(self, state: bool):
        if state:
            self.scale.show()
        else:
            self.scale.hide()

    def progress_fn(self, n):
        print("%d%% done" % n)

    def process_result(self, result):
        print("PROCESS RESULTS!")
        if self._show_mask:
            self._image_item.setImage(result)

    def thread_complete(self):
        print("THREAD COMPLETE!")

    def show_mask(self, state: bool):
        self._show_mask = state
        if not state:
            self.refresh_images()
            return

        if Manager.data.selected_mask is None or Manager.data.view_mode is ViewMode.GREYSCALE:
            return

        mask = Manager.data.selected_mask.image
        blend_image = self._image_item.image

        # Pass the function to execute
        worker = Worker(apply_mask, image=blend_image, mask=mask)  # Any other args, kwargs are passed to the run function
        worker.signals.result.connect(self.process_result)
        worker.signals.finished.connect(self.thread_complete)
        worker.signals.progress.connect(self.progress_fn)

        # Execute
        Manager.threadpool.start(worker)
Example #17
0
class HistogramItem(GraphicsWidget):
    """
    This is a graphicsWidget which provides controls for adjusting the display of an image.

    Includes:

    - Image histogram 
    - Movable region over histogram to select black/white levels

    Parameters
    ----------
    image : ImageItem or None
        If *image* is provided, then the control will be automatically linked to
        the image and changes to the control will be immediately reflected in
        the image's appearance.
    fillHistogram : bool
        By default, the histogram is rendered with a fill.
        For performance, set *fillHistogram* = False.
    """

    sigLevelsChanged = pyqtSignal(object)
    sigLevelChangeFinished = pyqtSignal(object)

    def __init__(self, image=None, fillHistogram=True, bounds: tuple = None):
        GraphicsWidget.__init__(self)
        self.imageItem = lambda: None  # fake a dead weakref

        self.layout = QGraphicsGridLayout()
        self.setLayout(self.layout)
        self.layout.setContentsMargins(1, 1, 1, 1)
        self.layout.setSpacing(0)
        self.vb = ViewBox(parent=self)
        # self.vb.setMaximumHeight(152)
        # self.vb.setMinimumWidth(45)
        self.vb.setMouseEnabled(x=True, y=False)

        self.region = LinearRegionItem([0, 1], 'vertical', swapMode='block', bounds=bounds)
        self.region.setZValue(1000)
        self.vb.addItem(self.region)
        self.region.lines[0].addMarker('<|', 0.5)
        self.region.lines[1].addMarker('|>', 0.5)
        self.region.sigRegionChanged.connect(self.regionChanging)
        self.region.sigRegionChangeFinished.connect(self.regionChanged)

        self.axis = AxisItem('bottom', linkView=self.vb, maxTickLength=-10, parent=self)
        self.layout.addItem(self.axis, 1, 0)
        self.layout.addItem(self.vb, 0, 0)
        self.range = None
        self.vb.sigRangeChanged.connect(self.viewRangeChanged)

        self.plot = PlotCurveItem(pen=(200, 200, 200, 100))
        # self.plot.rotate(90)
        self.vb.addItem(self.plot)

        self.fillHistogram(fillHistogram)
        self._showRegions()

        self.autoHistogramRange()

        if image is not None:
            self.setImageItem(image)

    def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
        if fill:
            self.plot.setFillLevel(level)
            self.plot.setBrush(color)
        else:
            self.plot.setFillLevel(None)


    def paint(self, p, *args):
        rgn = self.getLevels()
        self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
        self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))


    def setHistogramRange(self, mn, mx, padding=0.1):
        """Set the Y range on the histogram plot. This disables auto-scaling."""
        self.vb.enableAutoRange(self.vb.XAxis, False)
        self.vb.setYRange(mn, mx, padding)

    def autoHistogramRange(self):
        """Enable auto-scaling on the histogram plot."""
        self.vb.enableAutoRange(self.vb.XYAxes)

    def setImageItem(self, img):
        """Set an ImageItem to have its levels and LUT automatically controlled
        by this HistogramLUTItem.
        """
        self.imageItem = weakref.ref(img)
        img.sigImageChanged.connect(self.imageChanged)
        self.regionChanged()
        self.imageChanged(autoLevel=True)

    def viewRangeChanged(self):
        self.update()

    def regionChanged(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelChangeFinished.emit(self)

    def regionChanging(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelsChanged.emit(self)
        self.update()

    def imageChanged(self, autoLevel=False):
        if self.imageItem() is None:
            return

        self.plot.setVisible(True)
        # plot one histogram for all image data
        h = self.imageItem().getHistogram()
        if h[0] is None:
            return
        self.plot.setData(*h)
        if autoLevel:
            mn = h[0][0]
            mx = h[0][-1]
            self.region.setRegion([mn, mx])
        else:
            mn, mx = self.imageItem().levels
            self.region.setRegion([mn, mx])

    def getLevels(self):
        """
        Return the min and max levels.
        """
        return self.region.getRegion()

    def setLevels(self, min=None, max=None):
        """
        Set the min/max (bright and dark) levels.
        """
        assert None not in (min, max)
        self.region.setRegion((min, max))

    def _showRegions(self):
        self.region.setVisible(True)

    def saveState(self):
        return {
            'levels': self.getLevels(),
        }

    def restoreState(self, state):
        self.setLevels(*state['levels'])
class HistogramLUTItem_overlay(GraphicsWidget):
    """
    This is a graphicsWidget which provides controls for adjusting the display of an image.
    
    Includes:

    - Image histogram 
    - Movable region over histogram to select black/white levels
    - Gradient editor to define color lookup table for single-channel images
    
    Parameters
    ----------
    image : ImageItem or None
        If *image* is provided, then the control will be automatically linked to
        the image and changes to the control will be immediately reflected in
        the image's appearance.
    fillHistogram : bool
        By default, the histogram is rendered with a fill.
        For performance, set *fillHistogram* = False.    
    rgbHistogram : bool
        Sets whether the histogram is computed once over all channels of the
        image, or once per channel.
    levelMode : 'mono' or 'rgba'
        If 'mono', then only a single set of black/whilte level lines is drawn,
        and the levels apply to all channels in the image. If 'rgba', then one
        set of levels is drawn for each channel.
    """
    
    sigLookupTableChanged = QtCore.Signal(object)
    sigLevelsChanged = QtCore.Signal(object)
    sigLevelChangeFinished = QtCore.Signal(object)
    
    def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'):
        GraphicsWidget.__init__(self)
        self.overlay = False
        self.lut = None
        self.imageItem = lambda: None  # fake a dead weakref
        self.levelMode = levelMode
        self.rgbHistogram = rgbHistogram
        
        self.layout = QtGui.QGraphicsGridLayout()
        self.setLayout(self.layout)
        self.layout.setContentsMargins(1,1,1,1)
        self.layout.setSpacing(0)
        self.vb = ViewBox(parent=self)
        self.vb.setMaximumWidth(152)
        self.vb.setMinimumWidth(45)
        self.vb.setMouseEnabled(x=False, y=True)
        self.gradient = GradientEditorItem()
        self.gradient.setOrientation('right')
        self.gradient.loadPreset('grey')
        self.regions = [
            LinearRegionItem([0, 1], 'horizontal', swapMode='block'),
            LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r',
                             brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)),
            LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g',
                             brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)),
            LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b',
                             brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)),
            LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w',
                             brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))]
        for region in self.regions:
            region.setZValue(1000)
            self.vb.addItem(region)
            region.lines[0].addMarker('<|', 0.5)
            region.lines[1].addMarker('|>', 0.5)
            region.sigRegionChanged.connect(self.regionChanging)
            region.sigRegionChangeFinished.connect(self.regionChanged)
            
        self.region = self.regions[0]  # for backward compatibility.
        
        self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
        self.layout.addItem(self.axis, 0, 0)
        self.layout.addItem(self.vb, 0, 1)
        self.layout.addItem(self.gradient, 0, 2)
        self.range = None
        self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
        self.vb.setFlag(self.gradient.ItemStacksBehindParent)
        
        self.gradient.sigGradientChanged.connect(self.gradientChanged)
        self.vb.sigRangeChanged.connect(self.viewRangeChanged)
        add = QtGui.QPainter.CompositionMode_Plus
        self.plots = [
            PlotCurveItem(pen=(200, 200, 200, 100)),  # mono
            PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add),  # r
            PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add),  # g
            PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add),  # b
            PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add),  # a
            ]
        
        self.plot = self.plots[0]  # for backward compatibility.
        for plot in self.plots:
            plot.rotate(90)
            self.vb.addItem(plot)
        
        self.fillHistogram(fillHistogram)
        self._showRegions()
            
        self.vb.addItem(self.plot)
        self.autoHistogramRange()
        
        if image is not None:
            self.setImageItem(image)

        
    def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
        colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)]
        for i,plot in enumerate(self.plots):
            if fill:
                plot.setFillLevel(level)
                plot.setBrush(colors[i])
            else:
                plot.setFillLevel(None)
        
    def paint(self, p, *args):
        if self.levelMode != 'mono':
            return
        
        pen = self.region.lines[0].pen
        rgn = self.getLevels()
        p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
        p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))
        gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect())
        for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]:
            p.setPen(pen)
            p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft())
            p.drawLine(p2 - Point(0, 5), gradRect.topLeft())
            p.drawLine(gradRect.topLeft(), gradRect.topRight())
            p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
        
    def setHistogramRange(self, mn, mx, padding=0.1):
        """Set the Y range on the histogram plot. This disables auto-scaling."""
        self.vb.enableAutoRange(self.vb.YAxis, False)
        self.vb.setYRange(mn, mx, padding)

        
    def autoHistogramRange(self):
        """Enable auto-scaling on the histogram plot."""
        self.vb.enableAutoRange(self.vb.XYAxes)


    def setImageItem(self, img):
        """Set an ImageItem to have its levels and LUT automatically controlled
        by this HistogramLUTItem.
        """
        self.imageItem = weakref.ref(img)
        img.sigImageChanged.connect(self.imageChanged)
        img.setLookupTable(self.getLookupTable)  ## send function pointer, not the result
        self.regionChanged()
        self.imageChanged(autoLevel=True)

        
    def viewRangeChanged(self):
        self.update()
    
    def gradientChanged(self):
        if self.imageItem() is not None:
            if self.gradient.isLookupTrivial():
                self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8))
            else:
                self.imageItem().setLookupTable(self.getLookupTable)  ## send function pointer, not the result
            
        self.lut = None
        self.sigLookupTableChanged.emit(self)

    def getLookupTable(self, img=None, n=None, alpha=None):
        """Return a lookup table from the color gradient defined by this 
        HistogramLUTItem.
        """
        if self.levelMode is not 'mono':
            return None
        if n is None:
            if img.dtype == np.uint8:
                n = 256
            else:
                n = 512
        if self.lut is None:
            self.lut = self.gradient.getLookupTable(n, alpha=alpha)
        return self.lut


    def regionChanged(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelChangeFinished.emit(self)

    def regionChanging(self):
        if self.imageItem() is not None:
            self.imageItem().setLevels(self.getLevels())
        self.sigLevelsChanged.emit(self)
        self.update()

    def imageChanged(self, autoLevel=False, autoRange=False):
        if self.imageItem() is None:
            return
            
        if self.levelMode == 'mono':
            for plt in self.plots[1:]:
                plt.setVisible(False)
            self.plots[0].setVisible(True)
            # plot one histogram for all image data
            profiler = debug.Profiler()
            h = self.imageItem().getHistogram()
            profiler('get histogram')
            if h[0] is None:
                return
            self.plot.setData(*h)
            profiler('set plot')
            if autoLevel:
                mn = h[0][0]
                mx = h[0][-1]
                self.region.setRegion([mn, mx])
                profiler('set region')
            else:
                mn, mx = self.imageItem().levels
                self.region.setRegion([mn, mx])
        else:
            # plot one histogram for each channel
            self.plots[0].setVisible(False)
            ch = self.imageItem().getHistogram(perChannel=True)
            if ch[0] is None:
                return
            for i in range(1, 5):
                if len(ch) >= i:
                    h = ch[i-1]
                    self.plots[i].setVisible(True)
                    self.plots[i].setData(*h)
                    if autoLevel:
                        mn = h[0][0]
                        mx = h[0][-1]
                        self.region[i].setRegion([mn, mx])
                else:
                    # hide channels not present in image data
                    self.plots[i].setVisible(False)
            # make sure we are displaying the correct number of channels
            self._showRegions()
            
    def getLevels(self):
        """Return the min and max levels.
        
        For rgba mode, this returns a list of the levels for each channel.
        """
        if self.levelMode == 'mono':
            return self.region.getRegion()
        else:
            nch = self.imageItem().channels()
            if nch is None:
                nch = 3
            return [r.getRegion() for r in self.regions[1:nch+1]]

        
    def setLevels(self, min=None, max=None, rgba=None):
        """Set the min/max (bright and dark) levels.
        
        Arguments may be *min* and *max* for single-channel data, or 
        *rgba* = [(rmin, rmax), ...] for multi-channel data.
        """
        if self.levelMode == 'mono':
            if min is None:
                min, max = rgba[0]
            assert None not in (min, max)
            self.region.setRegion((min, max))
        else:
            if rgba is None:
                raise TypeError("Must specify rgba argument when levelMode != 'mono'.")
            for i, levels in enumerate(rgba):
                self.regions[i+1].setRegion(levels)

        
    def setLevelMode(self, mode):
        """ Set the method of controlling the image levels offered to the user. 
        Options are 'mono' or 'rgba'.
        """
        assert mode in ('mono', 'rgba')
        
        if mode == self.levelMode:
            return
        
        oldLevels = self.getLevels()
        self.levelMode = mode
        self._showRegions()
        
        # do our best to preserve old levels
        if mode == 'mono':
            levels = np.array(oldLevels).mean(axis=0)
            self.setLevels(*levels)
        else:
            levels = [oldLevels] * 4
            self.setLevels(rgba=levels)
            
        # force this because calling self.setLevels might not set the imageItem
        # levels if there was no change to the region item
        self.imageItem().setLevels(self.getLevels())
        
        self.imageChanged()
        self.update()


    def _showRegions(self):
        for i in range(len(self.regions)):
            self.regions[i].setVisible(False)
            
        if self.levelMode == 'rgba':
            imax = 4
            if self.imageItem() is not None:
                # Only show rgb channels if connected image lacks alpha.
                nch = self.imageItem().channels()
                if nch is None:
                    nch = 3
            xdif = 1.0 / nch
            for i in range(1, nch+1):
                self.regions[i].setVisible(True)
                self.regions[i].setSpan((i-1) * xdif, i * xdif)
            self.gradient.hide()
        elif self.levelMode == 'mono':
            self.regions[0].setVisible(True)
            self.gradient.show()
        else:
            raise ValueError("Unknown level mode %r" %  self.levelMode) 
    
    def saveState(self):
        return {
            'gradient': self.gradient.saveState(),
            'levels': self.getLevels(),
            'mode': self.levelMode,
        }
    
    def restoreState(self, state):
        self.setLevelMode(state['mode'])
        self.gradient.restoreState(state['gradient'])
        self.setLevels(*state['levels'])
        
    def setOverlay(self,state=False):
        self.overlay = state
Example #19
0
class FanDiagram(QtGui.QWidget):
    """
    Produces a fan diagram
    alphas/gammas should be the matrices saved from the FanCompiler, of form:

    arb     | niralpha1 | niralpha2 | niralpha3 | niralpha4 | ...
    1st sb  | 1sb alpha | 1sb alpha |     .
    2nd sb  | 2sb alpha | 2sb alpha |     .
    3rd sb  | 3sb alpha | 3sb alpha |     .
      .
      .
      .

    Assumes both alphas/gammas are the same shape

    Alternatively, pass nirAlpha and SBs as 1st/2nd args (as 1D arrays) to have it
    create the fan without any data

    Or, if alphaData and gammaData are strings, assumes they're paths to data files to plot
    :param alphas:
    :param gammas:
    :param kwargs:
    :return:
    """
    def __init__(self, alphaData, gammaData=None, view=None):
        super(FanDiagram, self).__init__()
        if gammaData is None and isinstance(alphaData, FanCompiler):
            alphaData, gammaData = alphaData.build(withErrors=False)[:2]

        self.layout = QtWidgets.QHBoxLayout()

        self.histAlpha = HistogramLUTWidget(self)
        self.centralView = GraphicsView()
        self.histGamma = HistogramLUTWidget(self)

        self.histAlpha.setMinimumWidth(150)
        self.histGamma.setMinimumWidth(150)

        self.layout.addWidget(self.histGamma)
        self.layout.addWidget(self.centralView)
        self.layout.addWidget(self.histAlpha)
        self.layout.setContentsMargins(0,0,0,0)
        self.layout.setSpacing(0)
        self.setLayout(self.layout)

        if view is None:
            self.view = ViewBox()
        else:
            self.view = view


        self.centralView.setCentralItem(self.view)
        self.view.setAspectLocked(True)
        self.view.invertY(True)

        if isinstance(alphaData, str) and isinstance(gammaData, str):
            alphaData = np.genfromtxt(alphaData, delimiter=',')
            gammaData = np.genfromtxt(gammaData, delimiter=',')

        if alphaData.ndim == gammaData.ndim == 1:
            # Assume you just want it to be created, and will later populate it
            nirAlphas = alphaData
            sbs = gammaData

            alphaData = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1
            alphaData[1:, 0] = sbs
            alphaData[0, 1:] = nirAlphas

            gammas = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1
            gammas[1:, 0] = sbs
            gammas[0, 1:] = nirAlphas

        sbs = alphaData[1:, 0]
        maxSB = sbs.max()
        nirAlphas = alphaData[0, 1:]


        self.alphaItem = PolarImageItem(r=sbs, theta=nirAlphas)
        self.alphaItem.setImage(alphaData[1:,1:])
        # nirAlphas+180 is what causes the gamma angles to appear on the left side of the
        # fan. This seemed easier than doing some sort of coordinate inversion/flipping
        # on the plot itself.
        self.gammaItem = PolarImageItem(sbs, nirAlphas+180, gammaData[1:,1:])

        self.view.addItem(self.alphaItem)
        self.view.addItem(self.gammaItem)

        self.histAlpha.setImageItem(self.alphaItem)
        self.histGamma.setImageItem(self.gammaItem)

        # manually set the default state to the black-gold-white-green-black. Not sure
        # if it's necessary to have this be a free parameter vs being hardcoded
        self.histAlpha.gradient.restoreState({
            "mode": "rgb",
            "ticks": [
                (0, (0, 0, 0, 255)),
                (.25, (128, 128, 0, 255)),
                (.5, (255, 255, 255, 255)),
                (.75, (0, 128, 0, 255)),
                (1, (0, 0, 0, 255))
            ]
        })

        # Set the default spacings for the alpha color axis. Again, not sure if it's
        # better to leave the 18pt font hard-coded or not, but I am
        self.histAlpha.axis.setTickFont(QtGui.QFont("Arial", 18))
        self.histAlpha.axis.setTickSpacing(30, 15)
        self.histAlpha.axis.setLabel("&alpha; (&deg;)", **{'font-family': 'Times',
                                                           "font-size":  "18pt"})

        # As with alpha, hard code the initial color space for gamma (blue-white-red)
        # and the font spacings and stuff
        self.histGamma.gradient.restoreState({
            "mode": "rgb",
            "ticks": [
                (0, (255, 0, 0, 255)),
                (.5, (255, 255, 255, 255)),
                (1, (0, 0, 255, 255))
            ]
        })
        self.histGamma.axis.setTickFont(QtGui.QFont("Arial", 18))
        self.histGamma.axis.setTickSpacing(15, 15)
        self.histGamma.axis.setLabel("&gamma; (&deg;)", **{'font-family': 'Times',
                                                           "font-size":  "18pt"})

        self.histAlpha.item.setLevels(-90, 90)
        self.histGamma.item.setLevels(-45, 45)

        self.histAlpha.autoHistogramRange()
        self.histGamma.autoHistogramRange()

        # Make it the right dimensions, making sure that the width is appropriate.
        # This makes it easier to automate plotting/saving fans and making sure
        # their dimensions are consistent.
        g = self.geometry()
        # I found these by eye, there's not very much important about them
        g.setWidth(773)
        g.setHeight(480)
        # Manually center it on the screen, since geometry isn't well defined at this point
        # before events are processed
        g.moveCenter(QtWidgets.QApplication.desktop().screenGeometry().center())
        self.setGeometry(g)

        # Add in the radial axes for it
        self.axes = {
            "radial": PolarAxis("radial"),
            "azimuthal": PolarAxis("azimuthal")
        }
        # Lighten the radial font to make it distinct from the other
        p = self.axes["radial"].pen()
        p.setColor(mkColor("#666666"))
        self.axes["radial"].setPen(p)

        for a in self.axes.values():
            # Make sure the axes sit on top of all other items
            a.setZValue(10000)
            # make sure that they scale appropriately, instead of just floating on top
            a.linkToView(self.view)
            # Ignore bounds prevents the window from resizing to try and fit in
            # the axes items
            self.addItem(a, ignoreBounds=True)

        # manually set the positions and string values for alpha angles. [-90, 90] work
        # well. The other half needs the +-180 to make sure the gamma angles have the
        # correctly labeled with respect to alpha_nir
        self.axes["azimuthal"].setTicks(
            [
                [(ii, str(ii)) for ii in np.arange(-90, 91, 30)] + # alpha side (Q1+Q4)
                [(ii, str(ii + 180)) for ii in np.arange(-180, -91, 30)] + #Q3
                [(ii, str(ii - 180)) for ii in np.arange(120, 151, 30)], #Q1
            ]
        )

        # add a title (without text)
        self.titleItem = TextItem()
        self.titleItem.setAnchor(Point(0.5, 1)) # anchor on bottom-center
        # Again, not sure if it's necessary to have the font color/size being
        # a free parameter
        self.titleItem.setColor("k")
        self.titleItem.setFont(QtGui.QFont("Arial", 15))
        # Ignore bounds so that the view won't try to account for it (which
        # causes a conflict because the title is placed with respect to the
        # view region)
        self.view.addItem(self.titleItem, ignoreBounds=True)
        
        self.show()
        # Arbitrary forcing updates to try and track down why some things don't
        # update correctly
        QtWidgets.QApplication.processEvents()
        self.view.updateViewRange(True, True)

    def setAlphaImage(self, img):
        self.alphaItem.setImage(img)

    def setGammaImage(self, img):
        self.gammaItem.setImage(img)

    def setImages(self, alpha, gamma):
        self.setAlphaImage(alpha)
        self.setGammaImage(gamma)

    def export(self, fname, hideHistograms=True,
               pngScale = 4):
        """
        Save fan diagrams to file, with the full image, and color bars on the alpha/gamma
        values
        :param fname: the fname to save as
        hideHistograms - (True) Prevent rendering the histograms, often ncier for
            figures/presentations

        If fname.endswith(".svg"), it outputs as an SVG. Howver, it's not the cleanest
            thing (the files are quite large/unoptimized, and I can't think of an
            easy way to correct that). Also, when the svg is converted to pdf via
            Inkscape, things get f****d up for some reasons (axes get thicker, fonts
            get borked, pixels get messed up). So, it kinda works, but there's
            stuff seriously wrong.

        One thing to make things cleaner is to use this site:
            https://jakearchibald.github.io/svgomg/
        which optimizies the svg and makes it a lot easier to work with
        :return:
        """

        # defaults = {
        #     "hideHistograms": False
        # }
        #
        # defaults.update(kwargs)

        doSvg = fname.endswith(".svg")

        if hideHistograms:
            # Hide the histogram data (and shrink the plot)
            # to avoid confusing people
            self.histAlpha.plot.hide()
            self.histAlpha.vb.setMaximumWidth(20)
            self.histGamma.plot.hide()
            self.histGamma.vb.setMaximumWidth(20)


        QtWidgets.QApplication.processEvents()

        self.histGamma.axis.setRange(-46.75, 46.75)
        self.histAlpha.axis.setRange(-94, 94)
        width, height = self.width(), self.height()


        if doSvg:
            from PyQt5 import QtSvg
            outputImage = QtSvg.QSvgGenerator()
            outputImage.setFileName(fname)
            outputImage.setSize(QtCore.QSize(int(width), int(height)))
            # I'm not sure why it has to be this, but the axis on the histogrm
            # were fuckingup without it
            outputImage.setResolution(96)
        else:
            outputImage = QtGui.QImage(width * pngScale, height * pngScale,
                                       QtGui.QImage.Format_ARGB32)
            # outputImage.setDotsPerMeterX(650 * 100 / 2.54)
            # outputImage.setDotsPerMeterY(650 * 100 / 2.54)
            # this gives a moderatly high quality image
            outputImage.setDevicePixelRatio(pngScale)
            outputImage.fill(QtGui.QColor("white"))

        outputPainter = QtGui.QPainter(outputImage)

        self.render(outputPainter)

        if not doSvg:
            ret = outputImage.save(fname)

        outputPainter.end()

    def addItem(self, item, ignoreBounds=False):
        self.view.addItem(item, ignoreBounds)

    def setViewRadius(self, r):
        # Set the view range of the fan diagram such that radius r is visible
        self.view.setRange(QtCore.QRect(-r, -r, 2*r, 2*r), padding=0)

    def hideHistogramAxes(self, hideTicks=True):
        # Hide the histogram region item and plots and all that for
        # less cluttered plots. Definitely useful if export is called with
        # hideHistograms=True, where the regions are useless.

        # Hide the linear regions
        self.histGamma.region.hide()
        self.histAlpha.region.hide()
        # Keep a reference to the old paint methods so you can reverse it if desired
        # This stops the painting of the bars which go from the linear region to the
        # gradient editor
        self.histGamma.item.oldPaint = self.histGamma.item.paint
        self.histAlpha.item.oldPaint = self.histAlpha.item.paint
        # Overwriting the functions to return None causes all the other rendering
        # things to abort
        self.histGamma.item.paint = lambda *x: None
        self.histAlpha.item.paint = lambda *x: None

        if hideTicks:
            # Hide the ticks which can be used for changing the stops/colors of
            # the gradients, which are rather ugly
            # Note: Since this only hides ticks which are present, I don't think
            [ii.hide() for ii in self.histAlpha.item.gradient.ticks.keys()]
            [ii.hide() for ii in self.histGamma.item.gradient.ticks.keys()]

        QtWidgets.QApplication.processEvents()
        # Hard coded numbers which make it look like the axes values line up with
        # the gradient item, which is more in-line with how color bars are interpreted
        self.histGamma.axis.setRange(-46.75, 46.75)
        self.histAlpha.axis.setRange(-94, 94)

    def showHistogramAxes(self, showTicks=True):
        try:
            self.histGamma.item.paint = self.histGamma.item.oldPaint
            self.histAlpha.item.paint = self.histAlpha.item.oldPaint
            del self.histAlpha.item.oldPaint
            del self.histGamma.item.oldPaint
        except AttributeError:
            # You didn't hide them first (or at least not here
            return
        self.histGamma.region.show()
        self.histAlpha.region.show()


        if showTicks:
            [ii.show() for ii in self.histAlpha.item.gradient.ticks.keys()]
            [ii.show() for ii in self.histGamma.item.gradient.ticks.keys()]

    @staticmethod
    def fromTMatrix(tMatrix, angle = 45, sbs=None):
        """
        Create a fan diagram from T matrices directly. The angle needs to be specified
        so the T matrix can be converted to a J matrix. The angle is relative to what's
        specified in the qwp.extractMatrices.makeU function.

        if you pass a string, it assumes it's a file name from a saved one. It'll load
        that and plot it. If you also pass values to sbs, it'll make sure only the passed
        values are plotted. Otherwise, it'll plot all the sbs in the file

        If you pass a tMatrix as returned from the fitting routines, you also need
        to pass the sbs directly in this case, since the tMatrices don't include them.
        :param tMatrix:
        :param angle:
        :param sbs:
        :return:
        """
        if isinstance(tMatrix, str):
            # a file is passed
            if sbs is not None:
                # Pass an array of sbs with a string, and this'll parse
                # out the sidebands which aren't included in  the passed array
                wantsbs = sbs
            else:
                wantsbs = None
            tMatrix, sbs = loadT(tMatrix)
            # Handle if only a select number of sidebands is specified
            if wantsbs is not None:
                try:
                    # Find the indices of the desired sidebands within the array of
                    # sidebands actually loaded
                    wantIdx = [sbs.tolist().index(ii) for ii in wantsbs]
                    # Cull it to only those specified
                    sbs = sbs[wantIdx]
                    # tMatrix is multidimensional (tMatrix.ndim>2), so ellipses cut
                    # out the other axes
                    tMatrix = tMatrix[..., wantIdx]
                    # Ensure that you got everything you want. Could happen if sidebands
                    # are requested (passed to the function) and not found
                    assert np.all(wantsbs == sbs)
                except ValueError as e:
                    raise IndexError("Invalid sideband requested ({} is not in loaded)".format(
                        e.args[0].split(' ')[0]
                    ))
                except AssertionError:
                    raise IndexError("Invalid sideband requested")


        jMatrix = makeJfromT(tMatrix, angle)
        if sbs is None:
            raise RuntimeWarning("Desired sidebands to plot should be specified as kwarg sbs")
            sbs = np.arange(8, 38, 2)

        alpha, gamma = jonesToFans(sbs = sbs, J=jMatrix)
        return FanDiagram(alpha, gamma)

    def setTitle(self, title="", adjustBounds=True):
        """
        Sets the title of the fan diagram, positioning the text right above the center
        of the fan
        :param title:
        :param adjustBounds:
        :return:
        """
        self.titleItem.setText(title)

        # Move the title so the bottom is at the top of the outer axis
        self.titleItem.setPos(0, self.axes["azimuthal"].fullBoundingRect.top())
        QtWidgets.QApplication.processEvents()
        # Double up because of some weird f*****g issue with Qt not appropriately
        # updating things when requested
        self.titleItem.setPos(0, self.axes["azimuthal"].fullBoundingRect.top())
        QtWidgets.QApplication.processEvents()
        # print(self.titleItem.mapRectToView(self.titleItem.boundingRect()))

        if adjustBounds:
            # Readjust the viewbox to frame the fan better

            # Find the top, based on the coordinates of the top of the title
            top = self.titleItem.mapRectToView(self.titleItem.boundingRect()).top()
            # Bottom is defiend by the bottom of the axes (includes the text)
            # Note: this assumes the
            bottom = self.axes["azimuthal"].fullBoundingRect.bottom()
            # print("bottom", bottom)
            w = abs(top-bottom)
            # print("new rect", QtCore.QRectF(-w/2, top, w, w))
            self.view.setRange(QtCore.QRectF(-w/2, top, w, w), padding=0)
            self.view.setRange(QtCore.QRectF(-w/2, top, w, w), padding=0)
            # self.view.update()

    def setMaxRadius(self, radius=40):
        # Set the maximum value for both of the axes to the value specified.
        # The 1e-6 is to prevent it from producing an "r=0" label and stuff
        self.axes["azimuthal"]._bounds["radial"] = [1e-6, radius]
        self.axes["radial"]._bounds["radial"] = [1e-6, radius]

        # Need to invalidate the cache for the axes, forcing it to redraw and update
        # the bounding rect and stuff
        self.axes["azimuthal"].picture = None
        self.axes["radial"].picture = None
class DataWidget(QWidget):
    def __init__(self, parent, shared_data):
        super(DataWidget, self).__init__(parent)
        self.__shared_data = shared_data
        self.__shared_data.update_sync.emit()

        # Add the file selection controls
        self.__dir_picker_button = QPushButton()
        self.__dir_picker_button.setEnabled(True)
        self.__dir_picker_button.setText("Load data")
        self.__dir_picker_button.setIcon(self.style().standardIcon(QStyle.SP_DirIcon))
        self.__dir_picker_button.setToolTip('Select the directory using the file explorer')
        self.__dir_picker_button.clicked.connect(self.__open_dir_picker)

        # Add the sync controls
        self.__sync_time_label = QLabel()
        self.__sync_time_label.setText('Enter the timecode (HH:mm:ss:zzz) : ')

        self.__sync_time_edit = QTimeEdit()
        self.__sync_time_edit.setDisplayFormat('HH:mm:ss:zzz')
        self.__sync_time_edit.setEnabled(False)

        self.__sync_time_button = QPushButton()
        self.__sync_time_button.setText('Sync data')
        self.__sync_time_button.setEnabled(False)
        self.__sync_time_button.clicked.connect(self.__sync_data)

        # Create the layout for the file controls
        dir_layout = QHBoxLayout()
        dir_layout.setContentsMargins(0, 0, 0, 0)
        dir_layout.addWidget(self.__dir_picker_button)
        dir_layout.addStretch(1)
        dir_layout.addWidget(self.__sync_time_label)
        dir_layout.addWidget(self.__sync_time_edit)
        dir_layout.addWidget(self.__sync_time_button)

        # Create the axis and their viewbox
        self.__x_axis_item = AxisItem('left')
        self.__y_axis_item = AxisItem('left')
        self.__z_axis_item = AxisItem('left')

        self.__x_axis_viewbox = ViewBox()
        self.__y_axis_viewbox = ViewBox()
        self.__z_axis_viewbox = ViewBox()

        # Create the widget which will display the data
        self.__graphic_view = GraphicsView(background="#ecf0f1")
        self.__graphic_layout = GraphicsLayout()
        self.__graphic_view.setCentralWidget(self.__graphic_layout)

        # Add the axis to the widget
        self.__graphic_layout.addItem(self.__x_axis_item, row=2, col=3, rowspan=1, colspan=1)
        self.__graphic_layout.addItem(self.__y_axis_item, row=2, col=2, rowspan=1, colspan=1)
        self.__graphic_layout.addItem(self.__z_axis_item, row=2, col=1, rowspan=1, colspan=1)

        self.__plot_item = PlotItem()
        self.__plot_item_viewbox = self.__plot_item.vb
        self.__graphic_layout.addItem(self.__plot_item, row=2, col=4, rowspan=1, colspan=1)

        self.__graphic_layout.scene().addItem(self.__x_axis_viewbox)
        self.__graphic_layout.scene().addItem(self.__y_axis_viewbox)
        self.__graphic_layout.scene().addItem(self.__z_axis_viewbox)

        self.__x_axis_item.linkToView(self.__x_axis_viewbox)
        self.__y_axis_item.linkToView(self.__y_axis_viewbox)
        self.__z_axis_item.linkToView(self.__z_axis_viewbox)

        self.__x_axis_viewbox.setXLink(self.__plot_item_viewbox)
        self.__y_axis_viewbox.setXLink(self.__plot_item_viewbox)
        self.__z_axis_viewbox.setXLink(self.__plot_item_viewbox)

        self.__plot_item_viewbox.sigResized.connect(self.__update_views)
        self.__x_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True)
        self.__y_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True)
        self.__z_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True)

        # Create the final layout
        self.__v_box = QVBoxLayout()
        self.__v_box.addLayout(dir_layout)
        self.__v_box.addWidget(self.__graphic_view)

        self.setLayout(self.__v_box)

        self.__restore_state()

    def __open_dir_picker(self):
        self.__shared_data.data_file_path = QFileDialog.getOpenFileUrl(self, 'Open the Hexoskin data directory',
                                                                       QDir.homePath())[0]
        if self.__shared_data.data_file_path is not None:
            try:
                self.__load_data()
                self.__add_selector_acc_gyr()
                self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z')
            except FileNotFoundError:
                pass
            except UnicodeDecodeError:
                pass

    def __load_data(self):
        if self.__shared_data.data_file_path is not None:
            self.__shared_data.import_parameter()

    def __show_data(self, field1, field2, field3):
        if self.__shared_data.parameter is not None:
            # Generate the timecodes if needed
            if len(self.__shared_data.parameter['TIMECODE']) == 0:
                if self.__shared_data.sampling_rate is None:
                    result = False
                    while not result and result == 0:
                        result = self.__show_sampling_rate_picker()

                self.__shared_data.add_timecode()

            self.__x_axis_viewbox.clear()
            self.__y_axis_viewbox.clear()
            self.__z_axis_viewbox.clear()

            # Show the 3 selected fields
            self.__x_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field1))),
                                                        pen='#34495e'))
            self.__y_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field2))),
                                                        pen='#9b59b6'))
            self.__z_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field3))),
                                                        pen='#3498db'))

            self.__x_axis_item.setLabel(field1, color="#34495e")
            self.__y_axis_item.setLabel(field2, color="#9b59b6")
            self.__z_axis_item.setLabel(field3, color="#3498db")

            # Add the middle line and the bottom timecodes
            timecodes = self.__shared_data.parameter['TIMECODE']
            middle = [0] * len(timecodes)
            self.__plot_item_viewbox.addItem(PlotCurveItem(middle, pen='#000000'))
            self.__plot_item.getAxis('bottom').setTicks(
                self.__generate_time_ticks(timecodes, self.__shared_data.sampling_rate))

            # Enable the controls
            self.__sync_time_edit.setEnabled(True)
            self.__sync_time_button.setEnabled(True)
            self.__dir_picker_button.setEnabled(False)
        self.__update_views()

    def __update_views(self):
        self.__x_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect())
        self.__y_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect())
        self.__z_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect())

    def __generate_time_ticks(self, timecodes, rate):
        ticks = list()

        steps = [rate * 30, rate * 15, rate * 5, rate]
        for step in steps:
            temp = list()
            i = step
            while i in range(len(timecodes)):
                temp.append((i, timecodes[i].strftime('%H:%M:%S:') + str(int(timecodes[i].microsecond / 1000))))
                i += step
            ticks.append(temp)

        return ticks

    def __sync_data(self):
        self.__shared_data.data_sync = self.__sync_time_edit.text()
        self.__shared_data.update_sync.emit()

    def __show_sampling_rate_picker(self) -> bool:
        self.__shared_data.sampling_rate, result = QInputDialog.getInt(self, 'Set sampling rate value', 'Sampling rate')
        return result

    def __add_selector_acc_gyr(self):
        if 'GYR_X' in self.__shared_data.parameter.keys() or 'GYR_Y' in self.__shared_data.parameter.keys() \
                or 'GYR_Z' in self.__shared_data.parameter.keys():
            show_acc = QPushButton()
            show_acc.setText('Show Accelerometer Axis')
            show_acc.clicked.connect(self.__show_acc)

            show_gyr = QPushButton()
            show_gyr.setText('Show Gyroscope Axis')
            show_gyr.clicked.connect(self.__show_gyr)

            layout = QHBoxLayout()
            layout.addWidget(show_acc)
            layout.addWidget(show_gyr)
            layout.addStretch(1)

            self.__v_box.addLayout(layout)

    def __show_acc(self):
        self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z')

    def __show_gyr(self):
        self.__show_data('GYR_X', 'GYR_Y', 'GYR_Z')

    def __restore_state(self):
        if self.__shared_data.parameter is not None:
            self.__add_selector_acc_gyr()
            self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z')
            print('trigger reimport')
        if self.__shared_data.data_sync is not None:
            text_time = self.__shared_data.data_sync.split(':')
            time = QTime()
            time.setHMS(int(text_time[0]), int(text_time[1]), int(text_time[2]), int(text_time[3]))
            self.__sync_time_edit.setTime(time)