Esempio n. 1
0
class ScanPlotWidget(PlotWidget):
    """
    Extend the PlotWidget Class with more functionality used for qudi scan images.
    Supported features:
     - draggable/static crosshair with optional range and size constraints.
     - zoom feature by rubberband selection
     - rubberband area selection

    This class depends on the ScanViewBox class defined further below.
    This class can be promoted in the Qt designer.
    """
    sigMouseAreaSelected = QtCore.Signal(
        QtCore.QRectF)  # mapped rectangle mouse cursor selection
    sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF)
    sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF)

    def __init__(self, *args, **kwargs):
        kwargs['viewBox'] = ScanViewBox()  # Use custom pg.ViewBox subclass
        super().__init__(*args, **kwargs)
        self.getViewBox().sigMouseAreaSelected.connect(
            self.sigMouseAreaSelected)

        self._min_crosshair_factor = 0.02
        self._crosshair_size = (0, 0)
        self._crosshair_range = None
        self.getViewBox().sigRangeChanged.connect(
            self._constraint_crosshair_size)

        self.crosshair = ROI((0, 0), (0, 0),
                             pen={
                                 'color': '#00ff00',
                                 'width': 1
                             })
        self.hline = InfiniteLine(pos=0,
                                  angle=0,
                                  movable=True,
                                  pen={
                                      'color': '#00ff00',
                                      'width': 1
                                  },
                                  hoverPen={
                                      'color': '#ffff00',
                                      'width': 1
                                  })
        self.vline = InfiniteLine(pos=0,
                                  angle=90,
                                  movable=True,
                                  pen={
                                      'color': '#00ff00',
                                      'width': 1
                                  },
                                  hoverPen={
                                      'color': '#ffff00',
                                      'width': 1
                                  })
        self.vline.sigDragged.connect(self._update_pos_from_line)
        self.hline.sigDragged.connect(self._update_pos_from_line)
        self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi)
        self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged)

    @property
    def crosshair_enabled(self):
        items = self.items()
        return (self.vline in items) and (self.hline
                                          in items) and (self.crosshair
                                                         in items)

    @property
    def crosshair_movable(self):
        return bool(self.crosshair.translatable)

    @property
    def crosshair_position(self):
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        return tuple(pos)

    @property
    def crosshair_size(self):
        return tuple(self._crosshair_size)

    @property
    def crosshair_min_size_factor(self):
        return float(self._min_crosshair_factor)

    @property
    def crosshair_range(self):
        if self._crosshair_range is None:
            return None
        return tuple(self._crosshair_range)

    @property
    def selection_enabled(self):
        return bool(self.getViewBox().rectangle_selection)

    @property
    def zoom_by_selection_enabled(self):
        return bool(self.getViewBox().zoom_by_selection)

    def toggle_selection(self, enable):
        """
        De-/Activate the rectangular rubber band selection tool.
        If active you can select a rectangular region within the ViewBox by dragging the mouse
        with the left button. Each selection rectangle in real-world data coordinates will be
        emitted by sigMouseAreaSelected.
        By using activate_zoom_by_selection you can optionally de-/activate zooming in on the
        selection.

        @param bool enable: Toggle selection on (True) or off (False)
        """
        return self.getViewBox().toggle_selection(enable)

    def toggle_zoom_by_selection(self, enable):
        """
        De-/Activate automatic zooming into a selection.
        See also: toggle_selection

        @param bool enable: Toggle zoom upon selection on (True) or off (False)
        """
        return self.getViewBox().toggle_zoom_by_selection(enable)

    def _update_pos_from_line(self, obj):
        """
        Called each time the position of the InfiniteLines has been changed by a user drag.
        Causes the crosshair rectangle to follow the lines.
        """
        if obj not in (self.hline, self.vline):
            return
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        size = self.crosshair.size()
        self.crosshair.blockSignals(True)
        self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2))
        self.crosshair.blockSignals(False)
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def _update_pos_from_roi(self, obj):
        """
        Called each time the position of the rectangular ROI has been changed by a user drag.
        Causes the InfiniteLines to follow the ROI.
        """
        if obj is not self.crosshair:
            return
        pos = self.crosshair.pos()
        size = self.crosshair.size()
        pos[0] += size[0] / 2
        pos[1] += size[1] / 2
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def toggle_crosshair(self, enable, movable=True):
        """
        Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be
        dragged by the user.

        @param bool enable: enable crosshair (True), disable crosshair (False)
        @param bool movable: enable user drag (True), disable user drag (False)
        """
        if not isinstance(enable, bool):
            raise TypeError('Positional argument "enable" must be bool type.')
        if not isinstance(movable, bool):
            raise TypeError('Optional argument "movable" must be bool type.')

        self.toggle_crosshair_movable(movable)

        is_enabled = self.crosshair_enabled
        if enable and not is_enabled:
            self.addItem(self.vline)
            self.addItem(self.hline)
            self.addItem(self.crosshair)
        elif not enable and is_enabled:
            self.removeItem(self.vline)
            self.removeItem(self.hline)
            self.removeItem(self.crosshair)
        return

    def toggle_crosshair_movable(self, enable):
        """
        Toggle if the crosshair can be dragged by the user.

        @param bool enable: enable (True), disable (False)
        """
        self.crosshair.translatable = bool(enable)
        self.vline.setMovable(enable)
        self.hline.setMovable(enable)
        return

    def set_crosshair_pos(self, pos):
        """
        Set the crosshair center to the given coordinates.

        @param QPointF|float[2] pos: (x,y) position of the crosshair
        """
        try:
            pos = tuple(pos)
        except TypeError:
            pos = (pos.x(), pos.y())
        size = self.crosshair.size()

        self.crosshair.blockSignals(True)
        self.vline.blockSignals(True)
        self.hline.blockSignals(True)
        self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.crosshair.blockSignals(False)
        self.vline.blockSignals(False)
        self.hline.blockSignals(False)
        self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos))
        return

    def set_crosshair_size(self, size, force_default=True):
        """
        Set the default size of the crosshair rectangle (x, y) and update the display.

        @param QSize|float[2] size: the (x,y) size of the crosshair rectangle
        @param bool force_default: Set default crosshair size and enforce minimal size (True).
                                   Enforce displayed crosshair size while keeping default size
                                   untouched (False).
        """
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        if force_default:
            if size[0] <= 0 and size[1] <= 0:
                self._crosshair_size = (0, 0)
            else:
                self._crosshair_size = size
                # Check if actually displayed size needs to be adjusted due to minimal size
                size = self._get_corrected_crosshair_size(size)

        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1] - size[1] / 2
        pos[0] -= size[0] / 2

        if self._crosshair_range:
            crange = self._crosshair_range
            self.crosshair.maxBounds = QtCore.QRectF(
                crange[0][0] - size[0] / 2, crange[1][0] - size[1] / 2,
                crange[0][1] - crange[0][0] + size[0],
                crange[1][1] - crange[1][0] + size[1])
        self.crosshair.blockSignals(True)
        self.crosshair.setSize(size)
        self.crosshair.setPos(pos)
        self.crosshair.blockSignals(False)
        return

    def set_crosshair_min_size_factor(self, factor):
        """
        Sets the minimum crosshair size factor. This will determine the minimum size of the
        smallest edge of the crosshair center rectangle.
        This minimum size is calculated by taking the smallest visible axis of the ViewBox and
        multiplying it with the scale factor set by this method.
        The crosshair rectangle will be then scaled accordingly if the set crosshair size is
        smaller than this minimal size.

        @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced.
        """
        if factor <= 0:
            self._min_crosshair_factor = 0
        elif factor <= 1:
            self._min_crosshair_factor = float(factor)
        else:
            raise ValueError('Crosshair min size factor must be a value <= 1.')
        return

    def set_crosshair_range(self, new_range):
        """
        Sets a range boundary for the crosshair position.

        @param float[2][2] new_range: two min-max range value tuples (for x and y axis).
                                      If None set unlimited ranges.
        """
        if new_range is None:
            self.vline.setBounds([None, None])
            self.hline.setBounds([None, None])
            self.crosshair.maxBounds = None
        else:
            self.vline.setBounds(new_range[0])
            self.hline.setBounds(new_range[1])
            size = self.crosshair.size()
            pos = self.crosshair_position
            self.crosshair.maxBounds = QtCore.QRectF(
                new_range[0][0] - size[0] / 2, new_range[1][0] - size[1] / 2,
                new_range[0][1] - new_range[0][0] + size[0],
                new_range[1][1] - new_range[1][0] + size[1])
            self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self._crosshair_range = new_range
        return

    def set_crosshair_pen(self, pen):
        """
        Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines.

        @param pen: pyqtgraph compatible pen to use
        """
        self.crosshair.setPen(pen)
        self.vline.setPen(pen)
        self.hline.setPen(pen)
        return

    def _constraint_crosshair_size(self):
        if self._min_crosshair_factor == 0:
            return
        if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0:
            return
        corr_size = self._get_corrected_crosshair_size(self._crosshair_size)
        if corr_size != tuple(self.crosshair.size()):
            self.set_crosshair_size(corr_size, force_default=False)
        return

    def _get_corrected_crosshair_size(self, size):
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        min_size = min(size)
        if min_size == 0:
            return size
        vb_size = self.getViewBox().viewRect().size()
        short_index = int(vb_size.width() > vb_size.height())
        min_vb_size = vb_size.width() if short_index == 0 else vb_size.height()
        min_vb_size *= self._min_crosshair_factor

        if min_size < min_vb_size:
            scale_factor = min_vb_size / min_size
            size = (size[0] * scale_factor, size[1] * scale_factor)
        return size
Esempio n. 2
0
class SpecvizDataViewer(DataViewer):
    """

    """
    LABEL = 'SpecViz viewer'
    _state_cls = SpecvizViewerState
    _options_cls = SpecvizViewerStateWidget
    _layer_style_widget_cls = SpecvizLayerStateWidget
    _data_artist_cls = SpecvizLayerArtist
    _subset_artist_cls = SpecvizLayerArtist
    _inherit_tools = False
    tools = []
    subtools = {}

    def __init__(self, *args, layout=None, include_line=False, **kwargs):
        # Load specviz plugins
        Application.load_local_plugins()

        super(SpecvizDataViewer, self).__init__(*args, **kwargs)
        self.statusBar().hide()

        # Instantiate workspace widget
        self.current_workspace = Workspace()
        self.hub = Hub(self.current_workspace)

        # Store a reference to the cubeviz layout instance
        self._layout = layout

        # Add an initially empty plot window
        self.current_workspace.add_plot_window()

        self.setCentralWidget(self.current_workspace)

        self.options.gridLayout.addWidget(self.current_workspace.list_view)

        # When a new data item is added to the specviz model, create a new
        # glue data component and add it to the glue data list
        # self.current_workspace.model.data_added.connect(self.reverse_add_data)

        self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView)
        self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint)
        self.current_workspace.current_plot_window.showMaximized()

        # Create and attach a movable vertical line indicating the current
        # slice position in the cube
        if include_line:
            self._slice_indicator = InfiniteLine(0, movable=True,
                                                 pen={'color': 'g', 'width': 3})
            self.current_workspace.current_plot_window.plot_widget.addItem(
                self._slice_indicator)

    def reverse_add_data(self, data_item):
        """
        Adds data from specviz to glue.

        Parameters
        ----------
        data_item : :class:`specviz.core.items.DataItem`
            The data item recently added to model.
        """
        new_data = Data(label=data_item.name)
        new_data.coords = coordinates_from_header(data_item.spectrum.wcs)

        flux_component = Component(data_item.spectrum.flux,
                                   data_item.spectrum.flux.unit)
        new_data.add_component(flux_component, "Flux")

        disp_component = Component(data_item.spectrum.spectral_axis,
                                   data_item.spectrum.spectral_axis.unit)
        new_data.add_component(disp_component, "Dispersion")

        if data_item.spectrum.uncertainty is not None:
            uncert_component = Component(data_item.spectrum.uncertainty.array,
                                         data_item.spectrum.uncertainty.unit)
            new_data.add_component(uncert_component, "Uncertainty")

        self._session.data_collection.append(new_data)

    def add_data(self, data):
        """

        Parameters
        ----------
        data

        Returns
        -------

        """
        if not glue_data_has_spectral_axis(data):
            QMessageBox.critical(self, "Error", "Data is not a 1D spectrum",
                                 buttons=QMessageBox.Ok)
            return False
        return super(SpecvizDataViewer, self).add_data(data)

    def add_subset(self, subset):
        """

        Parameters
        ----------
        subset

        Returns
        -------

        """
        if not glue_data_has_spectral_axis(subset):
            QMessageBox.critical(self, "Error", "Subset is not a 1D spectrum",
                                 buttons=QMessageBox.Ok)
            return False
        return super(SpecvizDataViewer, self).add_subset(subset)

    def get_layer_artist(self, cls, layer=None, layer_state=None):
        """

        Parameters
        ----------
        cls
        layer
        layer_state

        Returns
        -------

        """
        return cls(self.current_workspace, self.state, layer=layer, layer_state=layer_state)

    def initialize_toolbar(self):
        """

        """
        # Merge the main tool bar and the plot tool bar to get back some
        # real estate
        self.current_workspace.addToolBar(
            self.current_workspace.current_plot_window.tool_bar)
        self.current_workspace.main_tool_bar.setIconSize(QSize(15, 15))

        # Hide the first five actions in the default specviz tool bar
        for act in self.current_workspace.main_tool_bar.actions()[:6]:
            act.setVisible(False)

        # Hide the tabs of the mdiarea in specviz.
        self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView)
        self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint)
        self.current_workspace.current_plot_window.showMaximized()

        if self._layout is not None:
            cube_ops = QAction(QIcon(":/icons/cube.svg"), "Cube Operations",
                               self.current_workspace.main_tool_bar)
            self.current_workspace.main_tool_bar.addAction(cube_ops)
            self.current_workspace.main_tool_bar.addSeparator()

            button = self.current_workspace.main_tool_bar.widgetForAction(cube_ops)
            button.setPopupMode(QToolButton.InstantPopup)
            menu = QMenu(self.current_workspace.main_tool_bar)
            button.setMenu(menu)

            # Create operation actions
            menu.addSection("2D Operations")

            act = QAction("Simple Linemap", self)
            act.triggered.connect(self._create_simple_linemap)
            menu.addAction(act)

            act = QAction("Fitted Linemap", self)
            act.triggered.connect(self._create_fitted_linemap)
            menu.addAction(act)

            menu.addSection("3D Operations")

            act = QAction("Fit Spaxels", self)
            act.triggered.connect(self._fit_spaxels)
            menu.addAction(act)

            act = QAction("Spectral Smoothing", self)
            act.triggered.connect(self._spectral_smoothing)
            menu.addAction(act)

    def update_units(self, spectral_axis_unit=None, data_unit=None):
        """
        Interface for data viewers to update the plotted axis units in specviz.

        Parameters
        ----------
        spectral_axis_unit : str or :class:`~astropy.unit.Quantity`
            The spectral axis unit to convert to.
        data_unit : str or :class:`~astropy.unit.Quantity`
            The data axis unit to convert to.
        """
        if spectral_axis_unit is not None:
            self.hub.plot_widget.spectral_axis_unit = spectral_axis_unit

        if data_unit is not None:
            self.hub.plot_widget.data_unit = data_unit

    def update_slice_indicator_position(self, pos):
        """
        Updates the current position of the movable vertical slice indicator.

        Parameters
        ----------
        pos : float
            The new position of the vertical line.
        """
        if self._slice_indicator is not None:
            self._slice_indicator.blockSignals(True)
            self._slice_indicator.setPos(u.Quantity(pos).value)
            self._slice_indicator.blockSignals(False)

    def _create_simple_linemap(self):
        def threadable_function(data, tracker):
            out = np.empty(shape=data.shape[1:])
            mask = self.hub.region_mask

            for x in range(data.shape[1]):
                for y in range(data.shape[2]):
                    out[x, y] = np.sum(data[:, x, y][mask])
                    tracker()

            return out, data.meta.get('unit')

        spectral_operation = SpectralOperationHandler(
            data=self.layers[0].state.layer,
            function=threadable_function,
            operation_name="Simple Linemap",
            component_id=self.layers[0].state.attribute,
            layout=self._layout,
            ui_settings={
                'title': "Simple Linemap",
                'group_box_title': "Choose the component to use for linemap "
                                   "generation",
                'description': "Sums the values of the chosen component in the "
                               "range of the current ROI in the spectral view "
                               "for each spectrum in the data cube."})

        spectral_operation.exec_()

    def _create_fitted_linemap(self):
        # Check to see if the model fitting plugin is loaded
        model_editor_plugin = self.current_workspace._plugin_bars.get("Model Editor")

        if model_editor_plugin is None:
            logging.error("Model editor plugin is not loaded.")
            return

        if (model_editor_plugin.model_tree_view.model() is None or
                model_editor_plugin.model_tree_view.model().evaluate() is None):
            QMessageBox.warning(self,
                                "No evaluable model.",
                                "There is currently no model or the created "
                                "model is empty. Unable to perform fitted "
                                "linemap operation.")
            return

        def threadable_function(data, tracker):
            out = np.empty(shape=data.shape[1:])
            mask = self.hub.region_mask

            spectral_axis = self.hub.plot_item.spectral_axis
            model = model_editor_plugin.model_tree_view.model().evaluate()

            for x in range(data.shape[1]):
                for y in range(data.shape[2]):
                    flux = data[:, x, y].value

                    fitter = LevMarLSQFitter()
                    fit_model = fitter(model,
                                       spectral_axis[mask],
                                       flux[mask])

                    new_data = fit_model(spectral_axis)

                    out[x, y] = np.sum(new_data[mask])

                    tracker()

            return out, data.meta.get('unit')

        spectral_operation = SpectralOperationHandler(
            data=self.layers[0].state.layer,
            function=threadable_function,
            operation_name="Fitted Linemap",
            component_id=self.layers[0].state.attribute,
            layout=self._layout,
            ui_settings={
                'title': "Fitted Linemap",
                'group_box_title': "Choose the component to use for linemap "
                                   "generation",
                'description': "Fits the current model to the values of the "
                               "chosen component in the range of the current "
                               "ROI in the spectral view for each spectrum in "
                               "the data cube."})

        spectral_operation.exec_()

    def _fit_spaxels(self):
        # Check to see if the model fitting plugin is loaded
        model_editor_plugin = self.current_workspace._plugin_bars.get("Model Editor")

        if model_editor_plugin is None:
            logging.error("Model editor plugin is not loaded.")
            return

        if (model_editor_plugin.model_tree_view.model() is None or
                model_editor_plugin.model_tree_view.model().evaluate() is None):
            QMessageBox.warning(self,
                                "No evaluable model.",
                                "There is currently no model or the created "
                                "model is empty. Unable to perform fitted "
                                "linemap operation.")
            return

        def threadable_function(data, tracker):
            out = np.empty(shape=data.shape)
            mask = self.hub.region_mask

            spectral_axis = self.hub.plot_item.spectral_axis
            model = model_editor_plugin.model_tree_view.model().evaluate()

            for x in range(data.shape[1]):
                for y in range(data.shape[2]):
                    flux = data[:, x, y].value

                    fitter = LevMarLSQFitter()
                    fit_model = fitter(model,
                                       spectral_axis[mask],
                                       flux[mask])

                    new_data = fit_model(spectral_axis)

                    out[:, x, y] = np.sum(new_data[mask])

                    tracker()

            return out, data.meta.get('unit')

        spectral_operation = SpectralOperationHandler(
            data=self.layers[0].state.layer,
            function=threadable_function,
            operation_name="Fit Spaxels",
            component_id=self.layers[0].state.attribute,
            layout=self._layout,
            ui_settings={
                'title': "Fit Spaxel",
                'group_box_title': "Choose the component to use for spaxel "
                                   "fitting",
                'description': "Fits the current model to the values of the "
                               "chosen component in the range of the current "
                               "ROI in the spectral view for each spectrum in "
                               "the data cube."})

        spectral_operation.exec_()

    def _spectral_smoothing(self):
        def threadable_function(func, data, tracker, **kwargs):
            out = np.empty(shape=data.shape)

            for x in range(data.shape[1]):
                for y in range(data.shape[2]):
                    out[:, x, y] = func(data[:, x, y],
                                        data.spectral_axis)
                    tracker()

            return out, data.meta.get('unit')

        stack = FunctionalOperation.operations()[::-1]

        if len(stack) == 0:
            QMessageBox.warning(self,
                                "No smoothing in history.",
                                "To apply a smoothing operation to the entire "
                                "cube, first do a local smoothing operation "
                                "(Operations > Smoothing). Once done, the "
                                "operation can then be performed over the "
                                "entire cube.")
            return

        spectral_operation = SpectralOperationHandler(
            data=self.layers[0].state.layer,
            func_proxy=threadable_function,
            stack=stack,
            component_id=self.layers[0].state.attribute,
            layout=self._layout,
            ui_settings={
                'title': "Spectral Smoothing",
                'group_box_title': "Choose the component to smooth.",
                'description': "Performs a previous smoothing operation over "
                               "the selected component for the entire cube."})

        spectral_operation.exec_()
Esempio n. 3
0
class SpecvizDataViewer(DataViewer):
    """

    """
    LABEL = 'SpecViz viewer'
    _state_cls = SpecvizViewerState
    _options_cls = SpecvizViewerStateWidget
    _layer_style_widget_cls = SpecvizLayerStateWidget
    _data_artist_cls = SpecvizLayerArtist
    _subset_artist_cls = SpecvizLayerArtist
    _inherit_tools = False
    tools = []
    subtools = {}

    def __init__(self, *args, layout=None, include_line=False, **kwargs):
        # Load specviz plugins
        Application.load_local_plugins()

        super(SpecvizDataViewer, self).__init__(*args, **kwargs)
        self.statusBar().hide()

        # Instantiate workspace widget
        self.current_workspace = Workspace()
        self.hub = Hub(self.current_workspace)

        # Store a reference to the cubeviz layout instance
        self._layout = layout

        # Add an initially empty plot window
        self.current_workspace.add_plot_window()

        self.setCentralWidget(self.current_workspace)

        self.options.gridLayout.addWidget(self.current_workspace.list_view)

        # When a new data item is added to the specviz model, create a new
        # glue data component and add it to the glue data list
        # self.current_workspace.model.data_added.connect(self.reverse_add_data)

        self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView)
        self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint)
        self.current_workspace.current_plot_window.showMaximized()

        # Create and attach a movable vertical line indicating the current
        # slice position in the cube
        if include_line:
            self._slice_indicator = InfiniteLine(0, movable=True,
                                                 pen={'color': 'g', 'width': 3})
            self.current_workspace.current_plot_window.plot_widget.addItem(
                self._slice_indicator)

    def reverse_add_data(self, data_item):
        """
        Adds data from specviz to glue.

        Parameters
        ----------
        data_item : :class:`specviz.core.items.DataItem`
            The data item recently added to model.
        """
        new_data = Data(label=data_item.name)
        new_data.coords = coordinates_from_header(data_item.spectrum.wcs)

        flux_component = Component(data_item.spectrum.flux,
                                   data_item.spectrum.flux.unit)
        new_data.add_component(flux_component, "Flux")

        disp_component = Component(data_item.spectrum.spectral_axis,
                                   data_item.spectrum.spectral_axis.unit)
        new_data.add_component(disp_component, "Dispersion")

        if data_item.spectrum.uncertainty is not None:
            uncert_component = Component(data_item.spectrum.uncertainty.array,
                                         data_item.spectrum.uncertainty.unit)
            new_data.add_component(uncert_component, "Uncertainty")

        self._session.data_collection.append(new_data)

    def add_data(self, data):
        """

        Parameters
        ----------
        data

        Returns
        -------

        """
        if not glue_data_has_spectral_axis(data):
            QMessageBox.critical(self, "Error", "Data is not a 1D spectrum",
                                 buttons=QMessageBox.Ok)
            return False
        return super(SpecvizDataViewer, self).add_data(data)

    def add_subset(self, subset):
        """

        Parameters
        ----------
        subset

        Returns
        -------

        """
        if not glue_data_has_spectral_axis(subset):
            QMessageBox.critical(self, "Error", "Subset is not a 1D spectrum",
                                 buttons=QMessageBox.Ok)
            return False
        return super(SpecvizDataViewer, self).add_subset(subset)

    def get_layer_artist(self, cls, layer=None, layer_state=None):
        """

        Parameters
        ----------
        cls
        layer
        layer_state

        Returns
        -------

        """
        return cls(self.current_workspace, self.state, layer=layer, layer_state=layer_state)

    def initialize_toolbar(self):
        """

        """
        # Merge the main tool bar and the plot tool bar to get back some
        # real estate
        self.current_workspace.addToolBar(
            self.current_workspace.current_plot_window.tool_bar)
        self.current_workspace.main_tool_bar.setIconSize(QSize(15, 15))

        # Hide the first five actions in the default specviz tool bar
        for act in self.current_workspace.main_tool_bar.actions()[:6]:
            act.setVisible(False)

        # Hide the tabs of the mdiarea in specviz.
        self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView)
        self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint)
        self.current_workspace.current_plot_window.showMaximized()

        if self._layout is not None:
            cube_ops = QAction(QIcon(":/icons/cube.svg"), "Cube Operations",
                               self.current_workspace.main_tool_bar)
            self.current_workspace.main_tool_bar.addAction(cube_ops)
            self.current_workspace.main_tool_bar.addSeparator()

            button = self.current_workspace.main_tool_bar.widgetForAction(cube_ops)
            button.setPopupMode(QToolButton.InstantPopup)
            menu = QMenu(self.current_workspace.main_tool_bar)
            button.setMenu(menu)

            # Create operation actions
            menu.addSection("2D Operations")

            act = QAction("Simple Linemap", self)
            act.triggered.connect(lambda: simple_linemap(self))
            menu.addAction(act)

            act = QAction("Fitted Linemap", self)
            act.triggered.connect(lambda: fitted_linemap(self))
            menu.addAction(act)

            menu.addSection("3D Operations")

            act = QAction("Fit Spaxels", self)
            act.triggered.connect(lambda: fit_spaxels(self))
            menu.addAction(act)

            act = QAction("Spectral Smoothing", self)
            act.triggered.connect(lambda: spectral_smoothing(self))
            menu.addAction(act)

    def update_units(self, spectral_axis_unit=None, data_unit=None):
        """
        Interface for data viewers to update the plotted axis units in specviz.

        Parameters
        ----------
        spectral_axis_unit : str or :class:`~astropy.unit.Quantity`
            The spectral axis unit to convert to.
        data_unit : str or :class:`~astropy.unit.Quantity`
            The data axis unit to convert to.
        """
        if spectral_axis_unit is not None:
            self.hub.plot_widget.spectral_axis_unit = spectral_axis_unit

        if data_unit is not None:
            self.hub.plot_widget.data_unit = data_unit

    def update_slice_indicator_position(self, pos):
        """
        Updates the current position of the movable vertical slice indicator.

        Parameters
        ----------
        pos : float
            The new position of the vertical line.
        """
        if self._slice_indicator is not None:
            self._slice_indicator.blockSignals(True)
            self._slice_indicator.setPos(u.Quantity(pos).value)
            self._slice_indicator.blockSignals(False)
Esempio n. 4
0
class ScanPlotWidget(PlotWidget):
    """
    Extend the PlotWidget Class with more functionality used for qudi scan images.
    Supported features:
     - draggable/static crosshair with optional range and size constraints.
     - zoom feature by rubberband selection
     - rubberband area selection

    This class depends on the ScanViewBox class defined further below.
    This class can be promoted in the Qt designer.
    """
    sigMouseAreaSelected = QtCore.Signal(QtCore.QRectF)  # mapped rectangle mouse cursor selection
    sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF)
    sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF)

    def __init__(self, *args, **kwargs):
        kwargs['viewBox'] = ScanViewBox()  # Use custom pg.ViewBox subclass
        super().__init__(*args, **kwargs)
        self.getViewBox().sigMouseAreaSelected.connect(self.sigMouseAreaSelected)

        self._min_crosshair_factor = 0.02
        self._crosshair_size = (0, 0)
        self._crosshair_range = None
        self.getViewBox().sigRangeChanged.connect(self._constraint_crosshair_size)

        self.crosshair = ROI((0, 0), (0, 0), pen={'color': '#00ff00', 'width': 1})
        self.hline = InfiniteLine(pos=0,
                                  angle=0,
                                  movable=True,
                                  pen={'color': '#00ff00', 'width': 1},
                                  hoverPen={'color': '#ffff00', 'width': 1})
        self.vline = InfiniteLine(pos=0,
                                  angle=90,
                                  movable=True,
                                  pen={'color': '#00ff00', 'width': 1},
                                  hoverPen={'color': '#ffff00', 'width': 1})
        self.vline.sigDragged.connect(self._update_pos_from_line)
        self.hline.sigDragged.connect(self._update_pos_from_line)
        self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi)
        self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged)

    @property
    def crosshair_enabled(self):
        items = self.items()
        return (self.vline in items) and (self.hline in items) and (self.crosshair in items)

    @property
    def crosshair_movable(self):
        return bool(self.crosshair.translatable)

    @property
    def crosshair_position(self):
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        return tuple(pos)

    @property
    def crosshair_size(self):
        return tuple(self._crosshair_size)

    @property
    def crosshair_min_size_factor(self):
        return float(self._min_crosshair_factor)

    @property
    def crosshair_range(self):
        if self._crosshair_range is None:
            return None
        return tuple(self._crosshair_range)

    @property
    def selection_enabled(self):
        return bool(self.getViewBox().rectangle_selection)

    @property
    def zoom_by_selection_enabled(self):
        return bool(self.getViewBox().zoom_by_selection)

    def toggle_selection(self, enable):
        """
        De-/Activate the rectangular rubber band selection tool.
        If active you can select a rectangular region within the ViewBox by dragging the mouse
        with the left button. Each selection rectangle in real-world data coordinates will be
        emitted by sigMouseAreaSelected.
        By using activate_zoom_by_selection you can optionally de-/activate zooming in on the
        selection.

        @param bool enable: Toggle selection on (True) or off (False)
        """
        return self.getViewBox().toggle_selection(enable)

    def toggle_zoom_by_selection(self, enable):
        """
        De-/Activate automatic zooming into a selection.
        See also: toggle_selection

        @param bool enable: Toggle zoom upon selection on (True) or off (False)
        """
        return self.getViewBox().toggle_zoom_by_selection(enable)

    def _update_pos_from_line(self, obj):
        """
        Called each time the position of the InfiniteLines has been changed by a user drag.
        Causes the crosshair rectangle to follow the lines.
        """
        if obj not in (self.hline, self.vline):
            return
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        size = self.crosshair.size()
        self.crosshair.blockSignals(True)
        self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2))
        self.crosshair.blockSignals(False)
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def _update_pos_from_roi(self, obj):
        """
        Called each time the position of the rectangular ROI has been changed by a user drag.
        Causes the InfiniteLines to follow the ROI.
        """
        if obj is not self.crosshair:
            return
        pos = self.crosshair.pos()
        size = self.crosshair.size()
        pos[0] += size[0] / 2
        pos[1] += size[1] / 2
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def toggle_crosshair(self, enable, movable=True):
        """
        Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be
        dragged by the user.

        @param bool enable: enable crosshair (True), disable crosshair (False)
        @param bool movable: enable user drag (True), disable user drag (False)
        """
        if not isinstance(enable, bool):
            raise TypeError('Positional argument "enable" must be bool type.')
        if not isinstance(movable, bool):
            raise TypeError('Optional argument "movable" must be bool type.')

        self.toggle_crosshair_movable(movable)

        is_enabled = self.crosshair_enabled
        if enable and not is_enabled:
            self.addItem(self.vline)
            self.addItem(self.hline)
            self.addItem(self.crosshair)
        elif not enable and is_enabled:
            self.removeItem(self.vline)
            self.removeItem(self.hline)
            self.removeItem(self.crosshair)
        return

    def toggle_crosshair_movable(self, enable):
        """
        Toggle if the crosshair can be dragged by the user.

        @param bool enable: enable (True), disable (False)
        """
        self.crosshair.translatable = bool(enable)
        self.vline.setMovable(enable)
        self.hline.setMovable(enable)
        return

    def set_crosshair_pos(self, pos):
        """
        Set the crosshair center to the given coordinates.

        @param QPointF|float[2] pos: (x,y) position of the crosshair
        """
        try:
            pos = tuple(pos)
        except TypeError:
            pos = (pos.x(), pos.y())
        size = self.crosshair.size()

        self.crosshair.blockSignals(True)
        self.vline.blockSignals(True)
        self.hline.blockSignals(True)
        self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.crosshair.blockSignals(False)
        self.vline.blockSignals(False)
        self.hline.blockSignals(False)
        self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos))
        return

    def set_crosshair_size(self, size, force_default=True):
        """
        Set the default size of the crosshair rectangle (x, y) and update the display.

        @param QSize|float[2] size: the (x,y) size of the crosshair rectangle
        @param bool force_default: Set default crosshair size and enforce minimal size (True).
                                   Enforce displayed crosshair size while keeping default size
                                   untouched (False).
        """
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        if force_default:
            if size[0] <= 0 and size[1] <= 0:
                self._crosshair_size = (0, 0)
            else:
                self._crosshair_size = size
                # Check if actually displayed size needs to be adjusted due to minimal size
                size = self._get_corrected_crosshair_size(size)

        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1] - size[1] / 2
        pos[0] -= size[0] / 2

        if self._crosshair_range:
            crange = self._crosshair_range
            self.crosshair.maxBounds = QtCore.QRectF(crange[0][0] - size[0] / 2,
                                                     crange[1][0] - size[1] / 2,
                                                     crange[0][1] - crange[0][0] + size[0],
                                                     crange[1][1] - crange[1][0] + size[1])
        self.crosshair.blockSignals(True)
        self.crosshair.setSize(size)
        self.crosshair.setPos(pos)
        self.crosshair.blockSignals(False)
        return

    def set_crosshair_min_size_factor(self, factor):
        """
        Sets the minimum crosshair size factor. This will determine the minimum size of the
        smallest edge of the crosshair center rectangle.
        This minimum size is calculated by taking the smallest visible axis of the ViewBox and
        multiplying it with the scale factor set by this method.
        The crosshair rectangle will be then scaled accordingly if the set crosshair size is
        smaller than this minimal size.

        @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced.
        """
        if factor <= 0:
            self._min_crosshair_factor = 0
        elif factor <= 1:
            self._min_crosshair_factor = float(factor)
        else:
            raise ValueError('Crosshair min size factor must be a value <= 1.')
        return

    def set_crosshair_range(self, new_range):
        """
        Sets a range boundary for the crosshair position.

        @param float[2][2] new_range: two min-max range value tuples (for x and y axis).
                                      If None set unlimited ranges.
        """
        if new_range is None:
            self.vline.setBounds([None, None])
            self.hline.setBounds([None, None])
            self.crosshair.maxBounds = None
        else:
            self.vline.setBounds(new_range[0])
            self.hline.setBounds(new_range[1])
            size = self.crosshair.size()
            pos = self.crosshair_position
            self.crosshair.maxBounds = QtCore.QRectF(new_range[0][0] - size[0] / 2,
                                                     new_range[1][0] - size[1] / 2,
                                                     new_range[0][1] - new_range[0][0] + size[0],
                                                     new_range[1][1] - new_range[1][0] + size[1])
            self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self._crosshair_range = new_range
        return

    def set_crosshair_pen(self, pen):
        """
        Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines.

        @param pen: pyqtgraph compatible pen to use
        """
        self.crosshair.setPen(pen)
        self.vline.setPen(pen)
        self.hline.setPen(pen)
        return

    def _constraint_crosshair_size(self):
        if self._min_crosshair_factor == 0:
            return
        if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0:
            return
        corr_size = self._get_corrected_crosshair_size(self._crosshair_size)
        if corr_size != tuple(self.crosshair.size()):
            self.set_crosshair_size(corr_size, force_default=False)
        return

    def _get_corrected_crosshair_size(self, size):
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        min_size = min(size)
        if min_size == 0:
            return size
        vb_size = self.getViewBox().viewRect().size()
        short_index = int(vb_size.width() > vb_size.height())
        min_vb_size = vb_size.width() if short_index == 0 else vb_size.height()
        min_vb_size *= self._min_crosshair_factor

        if min_size < min_vb_size:
            scale_factor = min_vb_size / min_size
            size = (size[0] * scale_factor, size[1] * scale_factor)
        return size