diff options
Diffstat (limited to 'silx/gui/plot/tools')
20 files changed, 0 insertions, 8159 deletions
diff --git a/silx/gui/plot/tools/CurveLegendsWidget.py b/silx/gui/plot/tools/CurveLegendsWidget.py deleted file mode 100644 index 4a517dd..0000000 --- a/silx/gui/plot/tools/CurveLegendsWidget.py +++ /dev/null @@ -1,247 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a widget to display :class:`PlotWidget` curve legends. -""" - -from __future__ import division - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "20/07/2018" - - -import logging -import weakref - - -from ... import qt -from ...widgets.FlowLayout import FlowLayout as _FlowLayout -from ..LegendSelector import LegendIcon as _LegendIcon -from .. import items - - -_logger = logging.getLogger(__name__) - - -class _LegendWidget(qt.QWidget): - """Widget displaying curve style and its legend - - :param QWidget parent: See :class:`QWidget` - :param ~silx.gui.plot.items.Curve curve: Associated curve - """ - - def __init__(self, parent, curve): - super(_LegendWidget, self).__init__(parent) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(10, 0, 10, 0) - - curve.sigItemChanged.connect(self._curveChanged) - - icon = _LegendIcon(curve=curve) - layout.addWidget(icon) - - label = qt.QLabel(curve.getName()) - label.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter) - layout.addWidget(label) - - self._update() - - def getCurve(self): - """Returns curve associated to this widget - - :rtype: Union[~silx.gui.plot.items.Curve,None] - """ - icon = self.findChild(_LegendIcon) - return icon.getCurve() - - def _update(self): - """Update widget according to current curve state. - """ - curve = self.getCurve() - if curve is None: - _logger.error('Curve no more exists') - self.setVisible(False) - return - - self.setEnabled(curve.isVisible()) - - label = self.findChild(qt.QLabel) - if curve.isHighlighted(): - label.setStyleSheet("border: 1px solid black") - else: - label.setStyleSheet("") - - def _curveChanged(self, event): - """Handle update of curve item - - :param event: Kind of change - """ - if event in (items.ItemChangedType.VISIBLE, - items.ItemChangedType.HIGHLIGHTED, - items.ItemChangedType.HIGHLIGHTED_STYLE): - self._update() - - -class CurveLegendsWidget(qt.QWidget): - """Widget displaying curves legends in a plot - - :param QWidget parent: See :class:`QWidget` - """ - - sigCurveClicked = qt.Signal(object) - """Signal emitted when the legend of a curve is clicked - - It provides the corresponding curve. - """ - - def __init__(self, parent=None): - super(CurveLegendsWidget, self).__init__(parent) - self._clicked = None - self._legends = {} - self._plotRef = None - - def layout(self): - layout = super(CurveLegendsWidget, self).layout() - if layout is None: - # Lazy layout initialization to allow overloading - layout = _FlowLayout() - layout.setHorizontalSpacing(0) - self.setLayout(layout) - return layout - - def getPlotWidget(self): - """Returns the associated :class:`PlotWidget` - - :rtype: Union[~silx.gui.plot.PlotWidget,None] - """ - return None if self._plotRef is None else self._plotRef() - - def setPlotWidget(self, plot): - """Set the associated :class:`PlotWidget` - - :param ~silx.gui.plot.PlotWidget plot: Plot widget to attach - """ - previousPlot = self.getPlotWidget() - if previousPlot is not None: - previousPlot.sigItemAdded.disconnect( self._itemAdded) - previousPlot.sigItemAboutToBeRemoved.disconnect(self._itemRemoved) - for legend in list(self._legends.keys()): - self._removeLegend(legend) - - self._plotRef = None if plot is None else weakref.ref(plot) - - if plot is not None: - plot.sigItemAdded.connect(self._itemAdded) - plot.sigItemAboutToBeRemoved.connect(self._itemRemoved) - - for legend in plot.getAllCurves(just_legend=True): - self._addLegend(legend) - - def curveAt(self, *args): - """Returns the curve object represented at the given position - - Either takes a QPoint or x and y as input in widget coordinates. - - :rtype: Union[~silx.gui.plot.items.Curve,None] - """ - if len(args) == 1: - point = args[0] - elif len(args) == 2: - point = qt.QPoint(*args) - else: - raise ValueError('Unsupported arguments') - assert isinstance(point, qt.QPoint) - - widget = self.childAt(point) - while widget not in (self, None): - if isinstance(widget, _LegendWidget): - return widget.getCurve() - widget = widget.parent() - return None # No widget or not in _LegendWidget - - def _itemAdded(self, item): - """Handle item added to the plot content""" - if isinstance(item, items.Curve): - self._addLegend(item.getName()) - - def _itemRemoved(self, item): - """Handle item removed from the plot content""" - if isinstance(item, items.Curve): - self._removeLegend(item.getName()) - - def _addLegend(self, legend): - """Add a curve to the legends - - :param str legend: Curve's legend - """ - if legend in self._legends: - return # Can happen when changing curve's y axis - - plot = self.getPlotWidget() - if plot is None: - return None - - curve = plot.getCurve(legend) - if curve is None: - _logger.error('Curve not found: %s' % legend) - return - - widget = _LegendWidget(parent=self, curve=curve) - self.layout().addWidget(widget) - self._legends[legend] = widget - - def _removeLegend(self, legend): - """Remove a curve from the legends if it exists - - :param str legend: The curve's legend - """ - widget = self._legends.pop(legend, None) - if widget is None: - _logger.warning('Unknown legend: %s' % legend) - else: - self.layout().removeWidget(widget) - widget.setParent(None) - - def mousePressEvent(self, event): - if event.button() == qt.Qt.LeftButton: - self._clicked = event.pos() - - _CLICK_THRESHOLD = 5 - """Threshold for clicks""" - - def mouseMoveEvent(self, event): - if self._clicked is not None: - dx = abs(self._clicked.x() - event.pos().x()) - dy = abs(self._clicked.y() - event.pos().y()) - if dx > self._CLICK_THRESHOLD or dy > self._CLICK_THRESHOLD: - self._clicked = None # Click is cancelled - - def mouseReleaseEvent(self, event): - if event.button() == qt.Qt.LeftButton and self._clicked is not None: - curve = self.curveAt(event.pos()) - if curve is not None: - self.sigCurveClicked.emit(curve) - - self._clicked = None diff --git a/silx/gui/plot/tools/LimitsToolBar.py b/silx/gui/plot/tools/LimitsToolBar.py deleted file mode 100644 index fc192a6..0000000 --- a/silx/gui/plot/tools/LimitsToolBar.py +++ /dev/null @@ -1,131 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""A toolbar to display and edit limits of a PlotWidget -""" - - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent"] -__license__ = "MIT" -__date__ = "16/10/2017" - - -from ... import qt -from ...widgets.FloatEdit import FloatEdit - - -class LimitsToolBar(qt.QToolBar): - """QToolBar displaying and controlling the limits of a :class:`PlotWidget`. - - To run the following sample code, a QApplication must be initialized. - First, create a PlotWindow: - - >>> from silx.gui.plot import PlotWindow - >>> plot = PlotWindow() # Create a PlotWindow to add the toolbar to - - Then, create the LimitsToolBar and add it to the PlotWindow. - - >>> from silx.gui import qt - >>> from silx.gui.plot.tools import LimitsToolBar - - >>> toolbar = LimitsToolBar(plot=plot) # Create the toolbar - >>> plot.addToolBar(qt.Qt.BottomToolBarArea, toolbar) # Add it to the plot - >>> plot.show() # To display the PlotWindow with the limits toolbar - - :param parent: See :class:`QToolBar`. - :param plot: :class:`PlotWidget` instance on which to operate. - :param str title: See :class:`QToolBar`. - """ - - def __init__(self, parent=None, plot=None, title='Limits'): - super(LimitsToolBar, self).__init__(title, parent) - assert plot is not None - self._plot = plot - self._plot.sigPlotSignal.connect(self._plotWidgetSlot) - - self._initWidgets() - - @property - def plot(self): - """The :class:`PlotWidget` the toolbar is attached to.""" - return self._plot - - def _initWidgets(self): - """Create and init Toolbar widgets.""" - xMin, xMax = self.plot.getXAxis().getLimits() - yMin, yMax = self.plot.getYAxis().getLimits() - - self.addWidget(qt.QLabel('Limits: ')) - self.addWidget(qt.QLabel(' X: ')) - self._xMinFloatEdit = FloatEdit(self, xMin) - self._xMinFloatEdit.editingFinished[()].connect( - self._xFloatEditChanged) - self.addWidget(self._xMinFloatEdit) - - self._xMaxFloatEdit = FloatEdit(self, xMax) - self._xMaxFloatEdit.editingFinished[()].connect( - self._xFloatEditChanged) - self.addWidget(self._xMaxFloatEdit) - - self.addWidget(qt.QLabel(' Y: ')) - self._yMinFloatEdit = FloatEdit(self, yMin) - self._yMinFloatEdit.editingFinished[()].connect( - self._yFloatEditChanged) - self.addWidget(self._yMinFloatEdit) - - self._yMaxFloatEdit = FloatEdit(self, yMax) - self._yMaxFloatEdit.editingFinished[()].connect( - self._yFloatEditChanged) - self.addWidget(self._yMaxFloatEdit) - - def _plotWidgetSlot(self, event): - """Listen to :class:`PlotWidget` events.""" - if event['event'] not in ('limitsChanged',): - return - - xMin, xMax = self.plot.getXAxis().getLimits() - yMin, yMax = self.plot.getYAxis().getLimits() - - self._xMinFloatEdit.setValue(xMin) - self._xMaxFloatEdit.setValue(xMax) - self._yMinFloatEdit.setValue(yMin) - self._yMaxFloatEdit.setValue(yMax) - - def _xFloatEditChanged(self): - """Handle X limits changed from the GUI.""" - xMin, xMax = self._xMinFloatEdit.value(), self._xMaxFloatEdit.value() - if xMax < xMin: - xMin, xMax = xMax, xMin - - self.plot.getXAxis().setLimits(xMin, xMax) - - def _yFloatEditChanged(self): - """Handle Y limits changed from the GUI.""" - yMin, yMax = self._yMinFloatEdit.value(), self._yMaxFloatEdit.value() - if yMax < yMin: - yMin, yMax = yMax, yMin - - self.plot.getYAxis().setLimits(yMin, yMax) diff --git a/silx/gui/plot/tools/PositionInfo.py b/silx/gui/plot/tools/PositionInfo.py deleted file mode 100644 index 81d312a..0000000 --- a/silx/gui/plot/tools/PositionInfo.py +++ /dev/null @@ -1,376 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a widget displaying mouse coordinates in a PlotWidget. - -It can be configured to provide more information. -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent"] -__license__ = "MIT" -__date__ = "16/10/2017" - - -import logging -import numbers -import traceback -import weakref - -import numpy - -from ....utils.deprecation import deprecated -from ... import qt -from .. import items -from ...widgets.ElidedLabel import ElidedLabel - - -_logger = logging.getLogger(__name__) - - -class _PositionInfoLabel(ElidedLabel): - """QLabel with a default size larger than what is displayed.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - - def sizeHint(self): - hint = super().sizeHint() - width = self.fontMetrics().boundingRect('##############').width() - return qt.QSize(max(hint.width(), width), hint.height()) - - -# PositionInfo ################################################################ - -class PositionInfo(qt.QWidget): - """QWidget displaying coords converted from data coords of the mouse. - - Provide this widget with a list of couple: - - - A name to display before the data - - A function that takes (x, y) as arguments and returns something that - gets converted to a string. - If the result is a float it is converted with '%.7g' format. - - To run the following sample code, a QApplication must be initialized. - First, create a PlotWindow and add a QToolBar where to place the - PositionInfo widget. - - >>> from silx.gui.plot import PlotWindow - >>> from silx.gui import qt - - >>> plot = PlotWindow() # Create a PlotWindow to add the widget to - >>> toolBar = qt.QToolBar() # Create a toolbar to place the widget in - >>> plot.addToolBar(qt.Qt.BottomToolBarArea, toolBar) # Add it to plot - - Then, create the PositionInfo widget and add it to the toolbar. - The PositionInfo widget is created with a list of converters, here - to display polar coordinates of the mouse position. - - >>> import numpy - >>> from silx.gui.plot.tools import PositionInfo - - >>> position = PositionInfo(plot=plot, converters=[ - ... ('Radius', lambda x, y: numpy.sqrt(x*x + y*y)), - ... ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x)))]) - >>> toolBar.addWidget(position) # Add the widget to the toolbar - <...> - >>> plot.show() # To display the PlotWindow with the position widget - - :param plot: The PlotWidget this widget is displaying data coords from. - :param converters: - List of 2-tuple: name to display and conversion function from (x, y) - in data coords to displayed value. - If None, the default, it displays X and Y. - :param parent: Parent widget - """ - - SNAP_THRESHOLD_DIST = 5 - - def __init__(self, parent=None, plot=None, converters=None): - assert plot is not None - self._plotRef = weakref.ref(plot) - self._snappingMode = self.SNAPPING_DISABLED - - super(PositionInfo, self).__init__(parent) - - if converters is None: - converters = (('X', lambda x, y: x), ('Y', lambda x, y: y)) - - self._fields = [] # To store (QLineEdit, name, function (x, y)->v) - - # Create a new layout with new widgets - layout = qt.QHBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - # layout.setSpacing(0) - - # Create all QLabel and store them with the corresponding converter - for name, func in converters: - layout.addWidget(qt.QLabel('<b>' + name + ':</b>')) - - contentWidget = _PositionInfoLabel(self) - contentWidget.setText('------') - layout.addWidget(contentWidget) - self._fields.append((contentWidget, name, func)) - - layout.addStretch(1) - self.setLayout(layout) - - # Connect to Plot events - plot.sigPlotSignal.connect(self._plotEvent) - - def getPlotWidget(self): - """Returns the PlotWidget this widget is attached to or None. - - :rtype: Union[~silx.gui.plot.PlotWidget,None] - """ - return self._plotRef() - - @property - @deprecated(replacement='getPlotWidget', since_version='0.8.0') - def plot(self): - return self.getPlotWidget() - - def getConverters(self): - """Return the list of converters as 2-tuple (name, function).""" - return [(name, func) for _label, name, func in self._fields] - - def _plotEvent(self, event): - """Handle events from the Plot. - - :param dict event: Plot event - """ - if event['event'] == 'mouseMoved': - x, y = event['x'], event['y'] - xPixel, yPixel = event['xpixel'], event['ypixel'] - self._updateStatusBar(x, y, xPixel, yPixel) - - def updateInfo(self): - """Update displayed information""" - plot = self.getPlotWidget() - if plot is None: - _logger.error("Trying to update PositionInfo " - "while PlotWidget no longer exists") - return - - widget = plot.getWidgetHandle() - position = widget.mapFromGlobal(qt.QCursor.pos()) - xPixel, yPixel = position.x(), position.y() - dataPos = plot.pixelToData(xPixel, yPixel, check=True) - if dataPos is not None: # Inside plot area - x, y = dataPos - self._updateStatusBar(x, y, xPixel, yPixel) - - def _updateStatusBar(self, x, y, xPixel, yPixel): - """Update information from the status bar using the definitions. - - :param float x: Position-x in data - :param float y: Position-y in data - :param float xPixel: Position-x in pixels - :param float yPixel: Position-y in pixels - """ - plot = self.getPlotWidget() - if plot is None: - return - - styleSheet = "color: rgb(0, 0, 0);" # Default style - xData, yData = x, y - - snappingMode = self.getSnappingMode() - - # Snapping when crosshair either not requested or active - if (snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and - (not (snappingMode & self.SNAPPING_CROSSHAIR) or - plot.getGraphCursor())): - styleSheet = "color: rgb(255, 0, 0);" # Style far from item - - if snappingMode & self.SNAPPING_ACTIVE_ONLY: - selectedItems = [] - - if snappingMode & self.SNAPPING_CURVE: - activeCurve = plot.getActiveCurve() - if activeCurve: - selectedItems.append(activeCurve) - - if snappingMode & self.SNAPPING_SCATTER: - activeScatter = plot._getActiveItem(kind='scatter') - if activeScatter: - selectedItems.append(activeScatter) - - else: - kinds = [] - if snappingMode & self.SNAPPING_CURVE: - kinds.append(items.Curve) - kinds.append(items.Histogram) - if snappingMode & self.SNAPPING_SCATTER: - kinds.append(items.Scatter) - selectedItems = [item for item in plot.getItems() - if isinstance(item, tuple(kinds)) and item.isVisible()] - - # Compute distance threshold - if qt.BINDING in ('PyQt5', 'PySide2'): - window = plot.window() - windowHandle = window.windowHandle() - if windowHandle is not None: - ratio = windowHandle.devicePixelRatio() - else: - ratio = qt.QGuiApplication.primaryScreen().devicePixelRatio() - else: - ratio = 1. - - # Baseline squared distance threshold - distInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2 - - for item in selectedItems: - if (snappingMode & self.SNAPPING_SYMBOLS_ONLY and ( - not isinstance(item, items.SymbolMixIn) or - not item.getSymbol())): - # Only handled if item symbols are visible - continue - - if isinstance(item, items.Histogram): - result = item.pick(xPixel, yPixel) - if result is not None: # Histogram picked - index = result.getIndices()[0] - edges = item.getBinEdgesData(copy=False) - - # Snap to bin center and value - xData = 0.5 * (edges[index] + edges[index + 1]) - yData = item.getValueData(copy=False)[index] - - # Update label style sheet - styleSheet = "color: rgb(0, 0, 0);" - break - - else: # Curve, Scatter - xArray = item.getXData(copy=False) - yArray = item.getYData(copy=False) - closestIndex = numpy.argmin( - pow(xArray - x, 2) + pow(yArray - y, 2)) - - xClosest = xArray[closestIndex] - yClosest = yArray[closestIndex] - - if isinstance(item, items.YAxisMixIn): - axis = item.getYAxis() - else: - axis = 'left' - - closestInPixels = plot.dataToPixel( - xClosest, yClosest, axis=axis) - if closestInPixels is not None: - curveDistInPixels = ( - (closestInPixels[0] - xPixel)**2 + - (closestInPixels[1] - yPixel)**2) - - if curveDistInPixels <= distInPixels: - # Update label style sheet - styleSheet = "color: rgb(0, 0, 0);" - - # if close enough, snap to data point coord - xData, yData = xClosest, yClosest - distInPixels = curveDistInPixels - - for label, name, func in self._fields: - label.setStyleSheet(styleSheet) - - try: - value = func(xData, yData) - text = self.valueToString(value) - label.setText(text) - except: - label.setText('Error') - _logger.error( - "Error while converting coordinates (%f, %f)" - "with converter '%s'" % (xPixel, yPixel, name)) - _logger.error(traceback.format_exc()) - - def valueToString(self, value): - if isinstance(value, (tuple, list)): - value = [self.valueToString(v) for v in value] - return ", ".join(value) - elif isinstance(value, numbers.Real): - # Use this for floats and int - return '%.7g' % value - else: - # Fallback for other types - return str(value) - - # Snapping mode - - SNAPPING_DISABLED = 0 - """No snapping occurs""" - - SNAPPING_CROSSHAIR = 1 << 0 - """Snapping only enabled when crosshair cursor is enabled""" - - SNAPPING_ACTIVE_ONLY = 1 << 1 - """Snapping only enabled for active item""" - - SNAPPING_SYMBOLS_ONLY = 1 << 2 - """Snapping only when symbols are visible""" - - SNAPPING_CURVE = 1 << 3 - """Snapping on curves""" - - SNAPPING_SCATTER = 1 << 4 - """Snapping on scatter""" - - def setSnappingMode(self, mode): - """Set the snapping mode. - - The mode is a mask. - - :param int mode: The mode to use - """ - if mode != self._snappingMode: - self._snappingMode = mode - self.updateInfo() - - def getSnappingMode(self): - """Returns the snapping mode as a mask - - :rtype: int - """ - return self._snappingMode - - _SNAPPING_LEGACY = (SNAPPING_CROSSHAIR | - SNAPPING_ACTIVE_ONLY | - SNAPPING_SYMBOLS_ONLY | - SNAPPING_CURVE | - SNAPPING_SCATTER) - """Legacy snapping mode""" - - @property - @deprecated(replacement="getSnappingMode", since_version="0.8") - def autoSnapToActiveCurve(self): - return self.getSnappingMode() == self._SNAPPING_LEGACY - - @autoSnapToActiveCurve.setter - @deprecated(replacement="setSnappingMode", since_version="0.8") - def autoSnapToActiveCurve(self, flag): - self.setSnappingMode( - self._SNAPPING_LEGACY if flag else self.SNAPPING_DISABLED) diff --git a/silx/gui/plot/tools/RadarView.py b/silx/gui/plot/tools/RadarView.py deleted file mode 100644 index 7076835..0000000 --- a/silx/gui/plot/tools/RadarView.py +++ /dev/null @@ -1,361 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""QWidget displaying an overview of a 2D plot. - -This shows the available range of the data, and the current location of the -plot view. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "22/02/2021" - -import logging -import weakref -from ... import qt -from ...utils import LockReentrant - -_logger = logging.getLogger(__name__) - - -class _DraggableRectItem(qt.QGraphicsRectItem): - """RectItem which signals its change through visibleRectDragged.""" - def __init__(self, *args, **kwargs): - super(_DraggableRectItem, self).__init__( - *args, **kwargs) - - self._previousCursor = None - self.setFlag(qt.QGraphicsItem.ItemIsMovable) - self.setFlag(qt.QGraphicsItem.ItemSendsGeometryChanges) - self.setAcceptHoverEvents(True) - self._ignoreChange = False - self._constraint = 0, 0, 0, 0 - - def setConstraintRect(self, left, top, width, height): - """Set the constraint rectangle for dragging. - - The coordinates are in the _DraggableRectItem coordinate system. - - This constraint only applies to modification through interaction - (i.e., this constraint is not applied to change through API). - - If the _DraggableRectItem is smaller than the constraint rectangle, - the _DraggableRectItem remains within the constraint rectangle. - If the _DraggableRectItem is wider than the constraint rectangle, - the constraint rectangle remains within the _DraggableRectItem. - """ - self._constraint = left, left + width, top, top + height - - def setPos(self, *args, **kwargs): - """Overridden to ignore changes from API in itemChange.""" - self._ignoreChange = True - super(_DraggableRectItem, self).setPos(*args, **kwargs) - self._ignoreChange = False - - def moveBy(self, *args, **kwargs): - """Overridden to ignore changes from API in itemChange.""" - self._ignoreChange = True - super(_DraggableRectItem, self).moveBy(*args, **kwargs) - self._ignoreChange = False - - def itemChange(self, change, value): - """Callback called before applying changes to the item.""" - if (change == qt.QGraphicsItem.ItemPositionChange and - not self._ignoreChange): - # Makes sure that the visible area is in the data - # or that data is in the visible area if area is too wide - x, y = value.x(), value.y() - xMin, xMax, yMin, yMax = self._constraint - - if self.rect().width() <= (xMax - xMin): - if x < xMin: - value.setX(xMin) - elif x > xMax - self.rect().width(): - value.setX(xMax - self.rect().width()) - else: - if x > xMin: - value.setX(xMin) - elif x < xMax - self.rect().width(): - value.setX(xMax - self.rect().width()) - - if self.rect().height() <= (yMax - yMin): - if y < yMin: - value.setY(yMin) - elif y > yMax - self.rect().height(): - value.setY(yMax - self.rect().height()) - else: - if y > yMin: - value.setY(yMin) - elif y < yMax - self.rect().height(): - value.setY(yMax - self.rect().height()) - - if self.pos() != value: - # Notify change through signal - views = self.scene().views() - assert len(views) == 1 - views[0].visibleRectDragged.emit( - value.x() + self.rect().left(), - value.y() + self.rect().top(), - self.rect().width(), - self.rect().height()) - - return value - - return super(_DraggableRectItem, self).itemChange( - change, value) - - def hoverEnterEvent(self, event): - """Called when the mouse enters the rectangle area""" - self._previousCursor = self.cursor() - self.setCursor(qt.Qt.OpenHandCursor) - - def hoverLeaveEvent(self, event): - """Called when the mouse leaves the rectangle area""" - if self._previousCursor is not None: - self.setCursor(self._previousCursor) - self._previousCursor = None - - -class RadarView(qt.QGraphicsView): - """Widget presenting a synthetic view of a 2D area and - the current visible area. - - Coordinates are as in QGraphicsView: - x goes from left to right and y goes from top to bottom. - This widget preserves the aspect ratio of the areas. - - The 2D area and the visible area can be set with :meth:`setDataRect` - and :meth:`setVisibleRect`. - When the visible area has been dragged by the user, its new position - is signaled by the *visibleRectDragged* signal. - - It is possible to invert the direction of the axes by using the - :meth:`scale` method of QGraphicsView. - """ - - visibleRectDragged = qt.Signal(float, float, float, float) - """Signals that the visible rectangle has been dragged. - - It provides: left, top, width, height in data coordinates. - """ - - _DATA_PEN = qt.QPen(qt.QColor('white')) - _DATA_BRUSH = qt.QBrush(qt.QColor('light gray')) - _ACTIVEDATA_PEN = qt.QPen(qt.QColor('black')) - _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor('transparent')) - _ACTIVEDATA_PEN.setWidth(2) - _ACTIVEDATA_PEN.setCosmetic(True) - _VISIBLE_PEN = qt.QPen(qt.QColor('blue')) - _VISIBLE_PEN.setWidth(2) - _VISIBLE_PEN.setCosmetic(True) - _VISIBLE_BRUSH = qt.QBrush(qt.QColor(0, 0, 0, 0)) - _TOOLTIP = 'Radar View:\nRed contour: Visible area\nGray area: The image' - - _PIXMAP_SIZE = 256 - - def __init__(self, parent=None): - self.__plotRef = None - self._scene = qt.QGraphicsScene() - self._dataRect = self._scene.addRect(0, 0, 1, 1, - self._DATA_PEN, - self._DATA_BRUSH) - self._imageRect = self._scene.addRect(0, 0, 1, 1, - self._ACTIVEDATA_PEN, - self._ACTIVEDATA_BRUSH) - self._imageRect.setVisible(False) - self._scatterRect = self._scene.addRect(0, 0, 1, 1, - self._ACTIVEDATA_PEN, - self._ACTIVEDATA_BRUSH) - self._scatterRect.setVisible(False) - self._curveRect = self._scene.addRect(0, 0, 1, 1, - self._ACTIVEDATA_PEN, - self._ACTIVEDATA_BRUSH) - self._curveRect.setVisible(False) - - self._visibleRect = _DraggableRectItem(0, 0, 1, 1) - self._visibleRect.setPen(self._VISIBLE_PEN) - self._visibleRect.setBrush(self._VISIBLE_BRUSH) - self._scene.addItem(self._visibleRect) - - super(RadarView, self).__init__(self._scene, parent) - self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff) - self.setFocusPolicy(qt.Qt.NoFocus) - self.setStyleSheet('border: 0px') - self.setToolTip(self._TOOLTIP) - - self.__reentrant = LockReentrant() - self.visibleRectDragged.connect(self._viewRectDragged) - - self.__timer = qt.QTimer(self) - self.__timer.timeout.connect(self._updateDataContent) - - def sizeHint(self): - # """Overridden to avoid sizeHint to depend on content size.""" - return self.minimumSizeHint() - - def wheelEvent(self, event): - # """Overridden to disable vertical scrolling with wheel.""" - event.ignore() - - def resizeEvent(self, event): - # """Overridden to fit current content to new size.""" - self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) - super(RadarView, self).resizeEvent(event) - - def setDataRect(self, left, top, width, height): - """Set the bounds of the data rectangular area. - - This sets the coordinate system. - """ - self._dataRect.setRect(left, top, width, height) - self._visibleRect.setConstraintRect(left, top, width, height) - self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) - - def setVisibleRect(self, left, top, width, height): - """Set the visible rectangular area. - - The coordinates are relative to the data rect. - """ - self.__visibleRect = left, top, width, height - self._visibleRect.setRect(0, 0, width, height) - self._visibleRect.setPos(left, top) - self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio) - - def __setVisibleRectFromPlot(self, plot): - """Update radar view visible area. - - Takes care of y coordinate conversion. - """ - xMin, xMax = plot.getXAxis().getLimits() - yMin, yMax = plot.getYAxis().getLimits() - self.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin) - - def getPlotWidget(self): - """Returns the connected plot - - :rtype: Union[None,PlotWidget] - """ - if self.__plotRef is None: - return None - plot = self.__plotRef() - if plot is None: - self.__plotRef = None - return plot - - def setPlotWidget(self, plot): - """Set the PlotWidget this radar view connects to. - - As result `setDataRect` and `setVisibleRect` will be called - automatically. - - :param Union[None,PlotWidget] plot: - """ - previousPlot = self.getPlotWidget() - if previousPlot is not None: # Disconnect previous plot - plot.getXAxis().sigLimitsChanged.disconnect(self._xLimitChanged) - plot.getYAxis().sigLimitsChanged.disconnect(self._yLimitChanged) - plot.getYAxis().sigInvertedChanged.disconnect(self._updateYAxisInverted) - - # Reset plot and timer - # FIXME: It would be good to clean up the display here - self.__plotRef = None - self.__timer.stop() - - if plot is not None: # Connect new plot - self.__plotRef = weakref.ref(plot) - plot.getXAxis().sigLimitsChanged.connect(self._xLimitChanged) - plot.getYAxis().sigLimitsChanged.connect(self._yLimitChanged) - plot.getYAxis().sigInvertedChanged.connect(self._updateYAxisInverted) - self.__setVisibleRectFromPlot(plot) - self._updateYAxisInverted() - self.__timer.start(500) - - def _xLimitChanged(self, vmin, vmax): - plot = self.getPlotWidget() - self.__setVisibleRectFromPlot(plot) - - def _yLimitChanged(self, vmin, vmax): - plot = self.getPlotWidget() - self.__setVisibleRectFromPlot(plot) - - def _updateYAxisInverted(self, inverted=None): - """Sync radar view axis orientation.""" - plot = self.getPlotWidget() - if inverted is None: - # Do not perform this when called from plot signal - inverted = plot.getYAxis().isInverted() - # Use scale to invert radarView - # RadarView default Y direction is from top to bottom - # As opposed to Plot. So invert RadarView when Plot is NOT inverted. - self.resetTransform() - if not inverted: - self.scale(1., -1.) - self.update() - - def _viewRectDragged(self, left, top, width, height): - """Slot for radar view visible rectangle changes.""" - plot = self.getPlotWidget() - if plot is None: - return - - if self.__reentrant.locked(): - return - - with self.__reentrant: - plot.setLimits(left, left + width, top, top + height) - - def _updateDataContent(self): - """Update the content to the current data content""" - plot = self.getPlotWidget() - if plot is None: - return - ranges = plot.getDataRange() - xmin, xmax = ranges.x if ranges.x is not None else (0, 0) - ymin, ymax = ranges.y if ranges.y is not None else (0, 0) - self.setDataRect(xmin, ymin, xmax - xmin, ymax - ymin) - - self.__updateItem(self._imageRect, plot.getActiveImage()) - self.__updateItem(self._scatterRect, plot.getActiveScatter()) - self.__updateItem(self._curveRect, plot.getActiveCurve()) - - def __updateItem(self, rect, item): - """Sync rect with item bounds - - :param QGraphicsRectItem rect: - :param Item item: - """ - if item is None: - rect.setVisible(False) - return - ranges = item._getBounds() - if ranges is None: - rect.setVisible(False) - return - xmin, xmax, ymin, ymax = ranges - width = xmax - xmin - height = ymax - ymin - rect.setRect(xmin, ymin, width, height) - rect.setVisible(True) diff --git a/silx/gui/plot/tools/__init__.py b/silx/gui/plot/tools/__init__.py deleted file mode 100644 index 09f468c..0000000 --- a/silx/gui/plot/tools/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a set of widgets working with :class:`PlotWidget`. - -It provides some QToolBar and QWidget: - -- :class:`InteractiveModeToolBar` -- :class:`OutputToolBar` -- :class:`ImageToolBar` -- :class:`CurveToolBar` -- :class:`LimitsToolBar` -- :class:`PositionInfo` - -It also provides a :mod:`~silx.gui.plot.tools.roi` module to handle -interactive region of interest on a :class:`~silx.gui.plot.PlotWidget`. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "01/03/2018" - - -from .toolbars import InteractiveModeToolBar # noqa -from .toolbars import OutputToolBar # noqa -from .toolbars import ImageToolBar, CurveToolBar, ScatterToolBar # noqa - -from .LimitsToolBar import LimitsToolBar # noqa -from .PositionInfo import PositionInfo # noqa diff --git a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py deleted file mode 100644 index 44187ef..0000000 --- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py +++ /dev/null @@ -1,54 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module profile tools for scatter plots. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -from silx.utils import deprecation -from . import toolbar - - -class ScatterProfileToolBar(toolbar.ProfileToolBar): - """QToolBar providing scatter plot profiling tools - - :param parent: See :class:`QToolBar`. - :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate. - :param str title: See :class:`QToolBar`. - """ - - def __init__(self, parent=None, plot=None, title=None): - super(ScatterProfileToolBar, self).__init__(parent, plot) - if title is not None: - deprecation.deprecated_warning("Attribute", - name="title", - reason="removed", - since_version="0.13.0", - only_once=True, - skip_backtrace_count=1) - self.setScheme("scatter") diff --git a/silx/gui/plot/tools/profile/__init__.py b/silx/gui/plot/tools/profile/__init__.py deleted file mode 100644 index d91191e..0000000 --- a/silx/gui/plot/tools/profile/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides tools to get profiles on plot data. - -It provides: - -- :class:`ScatterProfileToolBar`: a QToolBar to handle profile on scatter data - -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "07/06/2018" - - -from .ScatterProfileToolBar import ScatterProfileToolBar # noqa diff --git a/silx/gui/plot/tools/profile/core.py b/silx/gui/plot/tools/profile/core.py deleted file mode 100644 index 200f5cf..0000000 --- a/silx/gui/plot/tools/profile/core.py +++ /dev/null @@ -1,525 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module define core objects for profile tools. -""" - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno", "V. Valls"] -__license__ = "MIT" -__date__ = "17/04/2020" - -import collections -import numpy -import weakref - -from silx.image.bilinear import BilinearImage -from silx.gui import qt - - -CurveProfileData = collections.namedtuple( - 'CurveProfileData', [ - "coords", - "profile", - "title", - "xLabel", - "yLabel", - ]) - -RgbaProfileData = collections.namedtuple( - 'RgbaProfileData', [ - "coords", - "profile", - "profile_r", - "profile_g", - "profile_b", - "profile_a", - "title", - "xLabel", - "yLabel", - ]) - -ImageProfileData = collections.namedtuple( - 'ImageProfileData', [ - 'coords', - 'profile', - 'title', - 'xLabel', - 'yLabel', - 'colormap', - ]) - - -class ProfileRoiMixIn: - """Base mix-in for ROI which can be used to select a profile. - - This mix-in have to be applied to a :class:`~silx.gui.plot.items.roi.RegionOfInterest` - in order to be usable by a :class:`~silx.gui.plot.tools.profile.manager.ProfileManager`. - """ - - ITEM_KIND = None - """Define the plot item which can be used with this profile ROI""" - - sigProfilePropertyChanged = qt.Signal() - """Emitted when a property of this profile have changed""" - - sigPlotItemChanged = qt.Signal() - """Emitted when the plot item linked to this profile have changed""" - - def __init__(self, parent=None): - self.__profileWindow = None - self.__profileManager = None - self.__plotItem = None - self.setName("Profile") - self.setEditable(True) - self.setSelectable(True) - - def invalidateProfile(self): - """Must be called by the implementation when the profile have to be - recomputed.""" - profileManager = self.getProfileManager() - if profileManager is not None: - profileManager.requestUpdateProfile(self) - - def invalidateProperties(self): - """Must be called when a property of the profile have changed.""" - self.sigProfilePropertyChanged.emit() - - def _setPlotItem(self, plotItem): - """Specify the plot item to use with this profile - - :param `~silx.gui.plot.items.item.Item` plotItem: A plot item - """ - previousPlotItem = self.getPlotItem() - if previousPlotItem is plotItem: - return - self.__plotItem = weakref.ref(plotItem) - self.sigPlotItemChanged.emit() - - def getPlotItem(self): - """Returns the plot item used by this profile - - :rtype: `~silx.gui.plot.items.item.Item` - """ - if self.__plotItem is None: - return None - plotItem = self.__plotItem() - if plotItem is None: - self.__plotItem = None - return plotItem - - def _setProfileManager(self, profileManager): - self.__profileManager = profileManager - - def getProfileManager(self): - """ - Returns the profile manager connected to this ROI. - - :rtype: ~silx.gui.plot.tools.profile.manager.ProfileManager - """ - return self.__profileManager - - def getProfileWindow(self): - """ - Returns the windows associated to this ROI, else None. - - :rtype: ProfileWindow - """ - return self.__profileWindow - - def setProfileWindow(self, profileWindow): - """ - Associate a window to this ROI. Can be None. - - :param ProfileWindow profileWindow: A main window - to display the profile. - """ - if profileWindow is self.__profileWindow: - return - if self.__profileWindow is not None: - self.__profileWindow.sigClose.disconnect(self.__profileWindowAboutToClose) - self.__profileWindow.setRoiProfile(None) - self.__profileWindow = profileWindow - if self.__profileWindow is not None: - self.__profileWindow.sigClose.connect(self.__profileWindowAboutToClose) - self.__profileWindow.setRoiProfile(self) - - def __profileWindowAboutToClose(self): - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - try: - roiManager.removeRoi(self) - except ValueError: - pass - - def computeProfile(self, item): - """ - Compute the profile which will be displayed. - - This method is not called from the main Qt thread, but from a thread - pool. - - :param ~silx.gui.plot.items.Item item: A plot item - :rtype: Union[CurveProfileData,ImageProfileData] - """ - raise NotImplementedError() - - -def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method): - """Get a profile along one axis on a stack of images - - :param numpy.ndarray data: 3D volume (stack of 2D images) - The first dimension is the image index. - :param origin: Origin of image in plot (ox, oy) - :param scale: Scale of image in plot (sx, sy) - :param float position: Position of profile line in plot coords - on the axis orthogonal to the profile direction. - :param int roiWidth: Width of the profile in image pixels. - :param int axis: 0 for horizontal profile, 1 for vertical. - :param str method: method to compute the profile. Can be 'mean' or 'sum' or - 'none' - :return: profile image + effective ROI area corners in plot coords - """ - assert axis in (0, 1) - assert len(data.shape) == 3 - assert method in ('mean', 'sum', 'none') - - # Convert from plot to image coords - imgPos = int((position - origin[1 - axis]) / scale[1 - axis]) - - if axis == 1: # Vertical profile - # Transpose image to always do a horizontal profile - data = numpy.transpose(data, (0, 2, 1)) - - nimages, height, width = data.shape - - roiWidth = min(height, roiWidth) # Clip roi width to image size - - # Get [start, end[ coords of the roi in the data - start = int(int(imgPos) + 0.5 - roiWidth / 2.) - start = min(max(0, start), height - roiWidth) - end = start + roiWidth - - if method == 'none': - profile = None - else: - if start < height and end > 0: - if method == 'mean': - fct = numpy.mean - elif method == 'sum': - fct = numpy.sum - else: - raise ValueError('method not managed') - profile = fct(data[:, max(0, start):min(end, height), :], axis=1).astype(numpy.float32) - else: - profile = numpy.zeros((nimages, width), dtype=numpy.float32) - - # Compute effective ROI in plot coords - profileBounds = numpy.array( - (0, width, width, 0), - dtype=numpy.float32) * scale[axis] + origin[axis] - roiBounds = numpy.array( - (start, start, end, end), - dtype=numpy.float32) * scale[1 - axis] + origin[1 - axis] - - if axis == 0: # Horizontal profile - area = profileBounds, roiBounds - else: # vertical profile - area = roiBounds, profileBounds - - return profile, area - - -def _alignedPartialProfile(data, rowRange, colRange, axis, method): - """Mean of a rectangular region (ROI) of a stack of images - along a given axis. - - Returned values and all parameters are in image coordinates. - - :param numpy.ndarray data: 3D volume (stack of 2D images) - The first dimension is the image index. - :param rowRange: [min, max[ of ROI rows (upper bound excluded). - :type rowRange: 2-tuple of int (min, max) with min < max - :param colRange: [min, max[ of ROI columns (upper bound excluded). - :type colRange: 2-tuple of int (min, max) with min < max - :param int axis: The axis along which to take the profile of the ROI. - 0: Sum rows along columns. - 1: Sum columns along rows. - :param str method: method to compute the profile. Can be 'mean' or 'sum' - :return: Profile image along the ROI as the mean of the intersection - of the ROI and the image. - """ - assert axis in (0, 1) - assert len(data.shape) == 3 - assert rowRange[0] < rowRange[1] - assert colRange[0] < colRange[1] - assert method in ('mean', 'sum') - - nimages, height, width = data.shape - - # Range aligned with the integration direction - profileRange = colRange if axis == 0 else rowRange - - profileLength = abs(profileRange[1] - profileRange[0]) - - # Subset of the image to use as intersection of ROI and image - rowStart = min(max(0, rowRange[0]), height) - rowEnd = min(max(0, rowRange[1]), height) - colStart = min(max(0, colRange[0]), width) - colEnd = min(max(0, colRange[1]), width) - - if method == 'mean': - _fct = numpy.mean - elif method == 'sum': - _fct = numpy.sum - else: - raise ValueError('method not managed') - - imgProfile = _fct(data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1, - dtype=numpy.float32) - - # Profile including out of bound area - profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32) - - # Place imgProfile in full profile - offset = - min(0, profileRange[0]) - profile[:, offset:offset + imgProfile.shape[1]] = imgProfile - - return profile - - -def createProfile(roiInfo, currentData, origin, scale, lineWidth, method): - """Create the profile line for the the given image. - - :param roiInfo: information about the ROI: start point, end point and - type ("X", "Y", "D") - :param numpy.ndarray currentData: the 2D image or the 3D stack of images - on which we compute the profile. - :param origin: (ox, oy) the offset from origin - :type origin: 2-tuple of float - :param scale: (sx, sy) the scale to use - :type scale: 2-tuple of float - :param int lineWidth: width of the profile line - :param str method: method to compute the profile. Can be 'mean' or 'sum' - or 'none': to compute everything except the profile - :return: `coords, profile, area, profileName, xLabel`, where: - - coords is the X coordinate to use to display the profile - - profile is a 2D array of the profiles of the stack of images. - For a single image, the profile is a curve, so this parameter - has a shape *(1, len(curve))* - - area is a tuple of two 1D arrays with 4 values each. They represent - the effective ROI area corners in plot coords. - - profileName is a string describing the ROI, meant to be used as - title of the profile plot - - xLabel the label for X in the profile window - - :rtype: tuple(ndarray,ndarray,(ndarray,ndarray),str) - """ - if currentData is None or roiInfo is None or lineWidth is None: - raise ValueError("createProfile called with invalide arguments") - - # force 3D data (stack of images) - if len(currentData.shape) == 2: - currentData3D = currentData.reshape((1,) + currentData.shape) - elif len(currentData.shape) == 3: - currentData3D = currentData - - roiWidth = max(1, lineWidth) - roiStart, roiEnd, lineProjectionMode = roiInfo - - if lineProjectionMode == 'X': # Horizontal profile on the whole image - profile, area = _alignedFullProfile(currentData3D, - origin, scale, - roiStart[1], roiWidth, - axis=0, - method=method) - - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[0] + origin[0] - - yMin, yMax = min(area[1]), max(area[1]) - 1 - if roiWidth <= 1: - profileName = '{ylabel} = %g' % yMin - else: - profileName = '{ylabel} = [%g, %g]' % (yMin, yMax) - xLabel = '{xlabel}' - - elif lineProjectionMode == 'Y': # Vertical profile on the whole image - profile, area = _alignedFullProfile(currentData3D, - origin, scale, - roiStart[0], roiWidth, - axis=1, - method=method) - - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[1] + origin[1] - - xMin, xMax = min(area[0]), max(area[0]) - 1 - if roiWidth <= 1: - profileName = '{xlabel} = %g' % xMin - else: - profileName = '{xlabel} = [%g, %g]' % (xMin, xMax) - xLabel = '{ylabel}' - - else: # Free line profile - - # Convert start and end points in image coords as (row, col) - startPt = ((roiStart[1] - origin[1]) / scale[1], - (roiStart[0] - origin[0]) / scale[0]) - endPt = ((roiEnd[1] - origin[1]) / scale[1], - (roiEnd[0] - origin[0]) / scale[0]) - - if (int(startPt[0]) == int(endPt[0]) or - int(startPt[1]) == int(endPt[1])): - # Profile is aligned with one of the axes - - # Convert to int - startPt = int(startPt[0]), int(startPt[1]) - endPt = int(endPt[0]), int(endPt[1]) - - # Ensure startPt <= endPt - if startPt[0] > endPt[0] or startPt[1] > endPt[1]: - startPt, endPt = endPt, startPt - - if startPt[0] == endPt[0]: # Row aligned - rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth), - int(startPt[0] + 0.5 + 0.5 * roiWidth)) - colRange = startPt[1], endPt[1] + 1 - if method == 'none': - profile = None - else: - profile = _alignedPartialProfile(currentData3D, - rowRange, colRange, - axis=0, - method=method) - - else: # Column aligned - rowRange = startPt[0], endPt[0] + 1 - colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth), - int(startPt[1] + 0.5 + 0.5 * roiWidth)) - if method == 'none': - profile = None - else: - profile = _alignedPartialProfile(currentData3D, - rowRange, colRange, - axis=1, - method=method) - # Convert ranges to plot coords to draw ROI area - area = ( - numpy.array( - (colRange[0], colRange[1], colRange[1], colRange[0]), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array( - (rowRange[0], rowRange[0], rowRange[1], rowRange[1]), - dtype=numpy.float32) * scale[1] + origin[1]) - - else: # General case: use bilinear interpolation - - # Ensure startPt <= endPt - if (startPt[1] > endPt[1] or ( - startPt[1] == endPt[1] and startPt[0] > endPt[0])): - startPt, endPt = endPt, startPt - - if method == 'none': - profile = None - else: - profile = [] - for slice_idx in range(currentData3D.shape[0]): - bilinear = BilinearImage(currentData3D[slice_idx, :, :]) - - profile.append(bilinear.profile_line( - (startPt[0] - 0.5, startPt[1] - 0.5), - (endPt[0] - 0.5, endPt[1] - 0.5), - roiWidth, - method=method)) - profile = numpy.array(profile) - - # Extend ROI with half a pixel on each end, and - # Convert back to plot coords (x, y) - length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 + - (endPt[1] - startPt[1]) ** 2) - dRow = (endPt[0] - startPt[0]) / length - dCol = (endPt[1] - startPt[1]) / length - - # Extend ROI with half a pixel on each end - roiStartPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol - roiEndPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol - - # Rotate deltas by 90 degrees to apply line width - dRow, dCol = dCol, -dRow - - area = ( - numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol, - roiStartPt[1] + 0.5 * roiWidth * dCol, - roiEndPt[1] + 0.5 * roiWidth * dCol, - roiEndPt[1] - 0.5 * roiWidth * dCol), - dtype=numpy.float32) * scale[0] + origin[0], - numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow, - roiStartPt[0] + 0.5 * roiWidth * dRow, - roiEndPt[0] + 0.5 * roiWidth * dRow, - roiEndPt[0] - 0.5 * roiWidth * dRow), - dtype=numpy.float32) * scale[1] + origin[1]) - - # Convert start and end points back to plot coords - y0 = startPt[0] * scale[1] + origin[1] - x0 = startPt[1] * scale[0] + origin[0] - y1 = endPt[0] * scale[1] + origin[1] - x1 = endPt[1] * scale[0] + origin[0] - - if startPt[1] == endPt[1]: - profileName = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1) - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[1] + y0 - xLabel = '{ylabel}' - - elif startPt[0] == endPt[0]: - profileName = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1) - if method == 'none': - coords = None - else: - coords = numpy.arange(len(profile[0]), dtype=numpy.float32) - coords = coords * scale[0] + x0 - xLabel = '{xlabel}' - - else: - m = (y1 - y0) / (x1 - x0) - b = y0 - m * x0 - profileName = '{ylabel} = %g * {xlabel} %+g' % (m, b) - if method == 'none': - coords = None - else: - coords = numpy.linspace(x0, x1, len(profile[0]), - endpoint=True, - dtype=numpy.float32) - xLabel = '{xlabel}' - - return coords, profile, area, profileName, xLabel diff --git a/silx/gui/plot/tools/profile/editors.py b/silx/gui/plot/tools/profile/editors.py deleted file mode 100644 index 80e0452..0000000 --- a/silx/gui/plot/tools/profile/editors.py +++ /dev/null @@ -1,307 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides editors which are used to custom profile ROI properties. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - -import logging - -from silx.gui import qt - -from silx.gui.utils import blockSignals -from silx.gui.plot.PlotToolButtons import ProfileOptionToolButton -from silx.gui.plot.PlotToolButtons import ProfileToolButton -from . import rois -from . import core - - -_logger = logging.getLogger(__name__) - - -class _NoProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def setEditorData(self, roi): - pass - - def setRoiData(self, roi): - pass - - -class _DefaultImageProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent=parent) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self._initLayout(layout) - - def _initLayout(self, layout): - self._lineWidth = qt.QSpinBox(self) - self._lineWidth.setRange(1, 1000) - self._lineWidth.setValue(1) - self._lineWidth.valueChanged[int].connect(self._widgetChanged) - - self._methodsButton = ProfileOptionToolButton(parent=self, plot=None) - self._methodsButton.sigMethodChanged.connect(self._widgetChanged) - - label = qt.QLabel('W:') - label.setToolTip("Line width in pixels") - layout.addWidget(label) - layout.addWidget(self._lineWidth) - layout.addWidget(self._methodsButton) - - def _widgetChanged(self, value=None): - self.commitData() - - def commitData(self): - self.sigDataCommited.emit() - - def setEditorData(self, roi): - with blockSignals(self._lineWidth): - self._lineWidth.setValue(roi.getProfileLineWidth()) - with blockSignals(self._methodsButton): - method = roi.getProfileMethod() - self._methodsButton.setMethod(method) - - def setRoiData(self, roi): - lineWidth = self._lineWidth.value() - roi.setProfileLineWidth(lineWidth) - method = self._methodsButton.getMethod() - roi.setProfileMethod(method) - - -class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor): - - def _initLayout(self, layout): - super(_DefaultImageStackProfileRoiEditor, self)._initLayout(layout) - self._profileDim = ProfileToolButton(parent=self, plot=None) - self._profileDim.sigDimensionChanged.connect(self._widgetChanged) - layout.addWidget(self._profileDim) - - def setEditorData(self, roi): - super(_DefaultImageStackProfileRoiEditor, self).setEditorData(roi) - with blockSignals(self._profileDim): - kind = roi.getProfileType() - dim = {"1D": 1, "2D": 2}[kind] - self._profileDim.setDimension(dim) - - def setRoiData(self, roi): - super(_DefaultImageStackProfileRoiEditor, self).setRoiData(roi) - dim = self._profileDim.getDimension() - kind = {1: "1D", 2: "2D"}[dim] - roi.setProfileType(kind) - - -class _DefaultScatterProfileRoiEditor(qt.QWidget): - - sigDataCommited = qt.Signal() - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent=parent) - - self._nPoints = qt.QSpinBox(self) - self._nPoints.setRange(1, 9999) - self._nPoints.setValue(1024) - self._nPoints.valueChanged[int].connect(self.__widgetChanged) - - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - label = qt.QLabel('Samples:') - label.setToolTip("Number of sample points of the profile") - layout.addWidget(label) - layout.addWidget(self._nPoints) - - def __widgetChanged(self, value=None): - self.commitData() - - def commitData(self): - self.sigDataCommited.emit() - - def setEditorData(self, roi): - with blockSignals(self._nPoints): - self._nPoints.setValue(roi.getNPoints()) - - def setRoiData(self, roi): - nPoints = self._nPoints.value() - roi.setNPoints(nPoints) - - -class ProfileRoiEditorAction(qt.QWidgetAction): - """ - Action displaying GUI to edit the selected ROI. - - :param qt.QWidget parent: Parent widget - """ - def __init__(self, parent=None): - super(ProfileRoiEditorAction, self).__init__(parent) - self.__roiManager = None - self.__roi = None - self.__inhibiteReentance = None - - def createWidget(self, parent): - """Inherit the method to create a new editor""" - widget = qt.QWidget(parent) - layout = qt.QHBoxLayout(widget) - if isinstance(parent, qt.QMenu): - margins = layout.contentsMargins() - layout.setContentsMargins(margins.left(), 0, margins.right(), 0) - else: - layout.setContentsMargins(0, 0, 0, 0) - - editorClass = self.getEditorClass(self.__roi) - editor = editorClass(parent) - editor.setEditorData(self.__roi) - self.__setEditor(widget, editor) - return widget - - def deleteWidget(self, widget): - """Inherit the method to delete an editor""" - self.__setEditor(widget, None) - return qt.QWidgetAction.deleteWidget(self, widget) - - def _getEditor(self, widget): - """Returns the editor contained in the widget holder""" - layout = widget.layout() - if layout.count() == 0: - return None - return layout.itemAt(0).widget() - - def setRoiManager(self, roiManager): - """ - Connect this action to a ROI manager. - - :param RegionOfInterestManager roiManager: A ROI manager - """ - if self.__roiManager is roiManager: - return - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged) - self.__roiManager = roiManager - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged) - self.__currentRoiChanged(roiManager.getCurrentRoi()) - - def __currentRoiChanged(self, roi): - """Handle changes of the selected ROI""" - if roi is not None and not isinstance(roi, core.ProfileRoiMixIn): - return - self.setProfileRoi(roi) - - def setProfileRoi(self, roi): - """Set a profile ROI to edit. - - :param ProfileRoiMixIn roi: A profile ROI - """ - if self.__roi is roi: - return - if self.__roi is not None: - self.__roi.sigProfilePropertyChanged.disconnect(self.__roiPropertyChanged) - self.__roi = roi - if self.__roi is not None: - self.__roi.sigProfilePropertyChanged.connect(self.__roiPropertyChanged) - self._updateWidgets() - - def getRoiProfile(self): - """Returns the edited profile ROI. - - :rtype: ProfileRoiMixIn - """ - return self.__roi - - def __roiPropertyChanged(self): - """Handle changes on the property defining the ROI. - """ - self._updateWidgetValues() - - def __setEditor(self, widget, editor): - """Set the editor to display. - - :param qt.QWidget editor: The editor to display - """ - previousEditor = self._getEditor(widget) - if previousEditor is editor: - return - layout = widget.layout() - if previousEditor is not None: - previousEditor.sigDataCommited.disconnect(self._editorDataCommited) - layout.removeWidget(previousEditor) - previousEditor.deleteLater() - if editor is not None: - editor.sigDataCommited.connect(self._editorDataCommited) - layout.addWidget(editor) - - def getEditorClass(self, roi): - """Returns the editor class to use according to the ROI.""" - if roi is None: - editorClass = _NoProfileRoiEditor - elif isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - # Must be done before the default image ROI - # Cause ImageStack ROIs inherit from Image ROIs - editorClass = _DefaultImageStackProfileRoiEditor - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - editorClass = _DefaultImageProfileRoiEditor - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - editorClass = _DefaultScatterProfileRoiEditor - else: - # Unsupported - editorClass = _NoProfileRoiEditor - return editorClass - - def _updateWidgets(self): - """Update the kind of editor to display, according to the selected - profile ROI.""" - parent = self.parent() - editorClass = self.getEditorClass(self.__roi) - for widget in self.createdWidgets(): - editor = editorClass(parent) - editor.setEditorData(self.__roi) - self.__setEditor(widget, editor) - - def _updateWidgetValues(self): - """Update the content of the displayed editor, according to the - selected profile ROI.""" - for widget in self.createdWidgets(): - editor = self._getEditor(widget) - if self.__inhibiteReentance is editor: - continue - editor.setEditorData(self.__roi) - - def _editorDataCommited(self): - """Handle changes from the editor.""" - editor = self.sender() - if self.__roi is not None: - self.__inhibiteReentance = editor - editor.setRoiData(self.__roi) - self.__inhibiteReentance = None diff --git a/silx/gui/plot/tools/profile/manager.py b/silx/gui/plot/tools/profile/manager.py deleted file mode 100644 index 68db9a6..0000000 --- a/silx/gui/plot/tools/profile/manager.py +++ /dev/null @@ -1,1076 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a manager to compute and display profiles. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - -import logging -import weakref - -from silx.gui import qt -from silx.gui import colors -from silx.gui import utils - -from silx.utils.weakref import WeakMethodProxy -from silx.gui import icons -from silx.gui.plot import PlotWidget -from silx.gui.plot.tools.roi import RegionOfInterestManager -from silx.gui.plot.tools.roi import CreateRoiModeAction -from silx.gui.plot import items -from silx.gui.qt import silxGlobalThreadPool -from silx.gui.qt import inspect -from . import rois -from . import core -from . import editors - - -_logger = logging.getLogger(__name__) - - -class _RunnableComputeProfile(qt.QRunnable): - """Runner to process profiles - - :param qt.QThreadPool threadPool: The thread which will be used to - execute this runner. It is used to update the used signals - :param ~silx.gui.plot.items.Item item: Item in which the profile is - computed - :param ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn roi: ROI - defining the profile shape and other characteristics - """ - - class _Signals(qt.QObject): - """Signal holder""" - resultReady = qt.Signal(object, object) - runnerFinished = qt.Signal(object) - - def __init__(self, threadPool, item, roi): - """Constructor - """ - super(_RunnableComputeProfile, self).__init__() - self._signals = self._Signals() - self._signals.moveToThread(threadPool.thread()) - self._item = item - self._roi = roi - self._cancelled = False - - def _lazyCancel(self): - """Cancel the runner if it is not yet started. - - The threadpool will still execute the runner, but this will process - nothing. - - This is only used with Qt<5.9 where QThreadPool.tryTake is not available. - """ - self._cancelled = True - - def autoDelete(self): - return False - - def getRoi(self): - """Returns the ROI in which the runner will compute a profile. - - :rtype: ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn - """ - return self._roi - - @property - def resultReady(self): - """Signal emitted when the result of the computation is available. - - This signal provides 2 values: The ROI, and the computation result. - """ - return self._signals.resultReady - - @property - def runnerFinished(self): - """Signal emitted when runner have finished. - - This signal provides a single value: the runner itself. - """ - return self._signals.runnerFinished - - def run(self): - """Process the profile computation. - """ - if not self._cancelled: - try: - profileData = self._roi.computeProfile(self._item) - except Exception: - _logger.error("Error while computing profile", exc_info=True) - else: - self.resultReady.emit(self._roi, profileData) - self.runnerFinished.emit(self) - - -class ProfileWindow(qt.QMainWindow): - """ - Display a computed profile. - - The content can be described using :meth:`setRoiProfile` if the source of - the profile is a profile ROI, and :meth:`setProfile` for the data content. - """ - - sigClose = qt.Signal() - """Emitted by :meth:`closeEvent` (e.g. when the window is closed - through the window manager's close icon).""" - - def __init__(self, parent=None, backend=None): - qt.QMainWindow.__init__(self, parent=parent, flags=qt.Qt.Dialog) - - self.setWindowTitle('Profile window') - self._plot1D = None - self._plot2D = None - self._backend = backend - self._data = None - - widget = qt.QWidget() - self._layout = qt.QStackedLayout(widget) - self._layout.setContentsMargins(0, 0, 0, 0) - self.setCentralWidget(widget) - - def prepareWidget(self, roi): - """Called before the show to prepare the window to use with - a specific ROI.""" - if isinstance(roi, rois._DefaultImageStackProfileRoiMixIn): - profileType = roi.getProfileType() - else: - profileType = "1D" - if profileType == "1D": - self.getPlot1D() - elif profileType == "2D": - self.getPlot2D() - - def createPlot1D(self, parent, backend): - """Inherit this function to create your own plot to render 1D - profiles. The default value is a `Plot1D`. - - :param parent: The parent of this widget or None. - :param backend: The backend to use for the plot. - See :class:`PlotWidget` for the list of supported backend. - :rtype: PlotWidget - """ - # import here to avoid circular import - from ...PlotWindow import Plot1D - plot = Plot1D(parent=parent, backend=backend) - plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1) - plot.setGraphYLabel('Profile') - plot.setGraphXLabel('') - return plot - - def createPlot2D(self, parent, backend): - """Inherit this function to create your own plot to render 2D - profiles. The default value is a `Plot2D`. - - :param parent: The parent of this widget or None. - :param backend: The backend to use for the plot. - See :class:`PlotWidget` for the list of supported backend. - :rtype: PlotWidget - """ - # import here to avoid circular import - from ...PlotWindow import Plot2D - return Plot2D(parent=parent, backend=backend) - - def getPlot1D(self, init=True): - """Return the current plot used to display curves and create it if it - does not yet exists and `init` is True. Else returns None.""" - if not init: - return self._plot1D - if self._plot1D is None: - self._plot1D = self.createPlot1D(self, self._backend) - self._layout.addWidget(self._plot1D) - return self._plot1D - - def _showPlot1D(self): - plot = self.getPlot1D() - self._layout.setCurrentWidget(plot) - - def getPlot2D(self, init=True): - """Return the current plot used to display image and create it if it - does not yet exists and `init` is True. Else returns None.""" - if not init: - return self._plot2D - if self._plot2D is None: - self._plot2D = self.createPlot2D(parent=self, backend=self._backend) - self._layout.addWidget(self._plot2D) - return self._plot2D - - def _showPlot2D(self): - plot = self.getPlot2D() - self._layout.setCurrentWidget(plot) - - def getCurrentPlotWidget(self): - return self._layout.currentWidget() - - def closeEvent(self, qCloseEvent): - self.sigClose.emit() - qCloseEvent.accept() - - def setRoiProfile(self, roi): - """Set the profile ROI which it the source of the following data - to display. - - :param ProfileRoiMixIn roi: The profile ROI data source - """ - if roi is None: - return - self.__color = colors.rgba(roi.getColor()) - - def _setImageProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by an image. - - :param core.ImageProfileData data: Computed data profile - """ - plot = self.getPlot2D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - - - coords = data.coords - colormap = data.colormap - profileScale = (coords[-1] - coords[0]) / data.profile.shape[1], 1 - plot.addImage(data.profile, - legend="profile", - colormap=colormap, - origin=(coords[0], 0), - scale=profileScale) - plot.getYAxis().setLabel("Frame index (depth)") - - self._showPlot2D() - - def _setCurveProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by a curve. - - :param core.CurveProfileData data: Computed data profile - """ - plot = self.getPlot1D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - plot.getYAxis().setLabel(data.yLabel) - - plot.addCurve(data.coords, - data.profile, - legend="level", - color=self.__color) - - self._showPlot1D() - - def _setRgbaProfile(self, data): - """ - Setup the window to display a new profile data which is represented - by a curve. - - :param core.RgbaProfileData data: Computed data profile - """ - plot = self.getPlot1D() - - plot.clear() - plot.setGraphTitle(data.title) - plot.getXAxis().setLabel(data.xLabel) - plot.getYAxis().setLabel(data.yLabel) - - self._showPlot1D() - - plot.addCurve(data.coords, data.profile, - legend="level", color="black") - plot.addCurve(data.coords, data.profile_r, - legend="red", color="red") - plot.addCurve(data.coords, data.profile_g, - legend="green", color="green") - plot.addCurve(data.coords, data.profile_b, - legend="blue", color="blue") - if data.profile_a is not None: - plot.addCurve(data.coords, data.profile_a, legend="alpha", color="gray") - - def clear(self): - """Clear the window profile""" - plot = self.getPlot1D(init=False) - if plot is not None: - plot.clear() - plot = self.getPlot2D(init=False) - if plot is not None: - plot.clear() - - def getProfile(self): - """Returns the profile data which is displayed""" - return self.__data - - def setProfile(self, data): - """ - Setup the window to display a new profile data. - - This method dispatch the result to a specific method according to the - data type. - - :param data: Computed data profile - """ - self.__data = data - if data is None: - self.clear() - elif isinstance(data, core.ImageProfileData): - self._setImageProfile(data) - elif isinstance(data, core.RgbaProfileData): - self._setRgbaProfile(data) - elif isinstance(data, core.CurveProfileData): - self._setCurveProfile(data) - else: - raise TypeError("Unsupported type %s" % type(data)) - - -class _ClearAction(qt.QAction): - """Action to clear the profile manager - - The action is only enabled if something can be cleaned up. - """ - - def __init__(self, parent, profileManager): - super(_ClearAction, self).__init__(parent) - self.__profileManager = weakref.ref(profileManager) - icon = icons.getQIcon('profile-clear') - self.setIcon(icon) - self.setText('Clear profile') - self.setToolTip('Clear the profiles') - self.setCheckable(False) - self.setEnabled(False) - self.triggered.connect(profileManager.clearProfile) - plot = profileManager.getPlotWidget() - roiManager = profileManager.getRoiManager() - plot.sigInteractiveModeChanged.connect(self.__modeUpdated) - roiManager.sigRoiChanged.connect(self.__roiListUpdated) - - def getProfileManager(self): - return self.__profileManager() - - def __roiListUpdated(self): - self.__update() - - def __modeUpdated(self, source): - self.__update() - - def __update(self): - profileManager = self.getProfileManager() - if profileManager is None: - return - roiManager = profileManager.getRoiManager() - if roiManager is None: - return - enabled = roiManager.isStarted() or len(roiManager.getRois()) > 0 - self.setEnabled(enabled) - - -class _StoreLastParamBehavior(qt.QObject): - """This object allow to store and restore the properties of the ROI - profiles""" - - def __init__(self, parent): - assert isinstance(parent, ProfileManager) - super(_StoreLastParamBehavior, self).__init__(parent=parent) - self.__properties = {} - self.__profileRoi = None - self.__filter = utils.LockReentrant() - - def _roi(self): - """Return the spied ROI""" - if self.__profileRoi is None: - return None - roi = self.__profileRoi() - if roi is None: - self.__profileRoi = None - return roi - - def setProfileRoi(self, roi): - """Set a profile ROI to spy. - - :param ProfileRoiMixIn roi: A profile ROI - """ - previousRoi = self._roi() - if previousRoi is roi: - return - if previousRoi is not None: - previousRoi.sigProfilePropertyChanged.disconnect(self._profilePropertyChanged) - self.__profileRoi = None if roi is None else weakref.ref(roi) - if roi is not None: - roi.sigProfilePropertyChanged.connect(self._profilePropertyChanged) - - def _profilePropertyChanged(self): - """Handle changes on the properties defining the profile ROI. - """ - if self.__filter.locked(): - return - roi = self.sender() - self.storeProperties(roi) - - def storeProperties(self, roi): - if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - self.__properties["method"] = roi.getProfileMethod() - self.__properties["line-width"] = roi.getProfileLineWidth() - self.__properties["type"] = roi.getProfileType() - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - self.__properties["method"] = roi.getProfileMethod() - self.__properties["line-width"] = roi.getProfileLineWidth() - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - self.__properties["npoints"] = roi.getNPoints() - - def restoreProperties(self, roi): - with self.__filter: - if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn, - rois.ProfileImageStackCrossROI)): - value = self.__properties.get("method", None) - if value is not None: - roi.setProfileMethod(value) - value = self.__properties.get("line-width", None) - if value is not None: - roi.setProfileLineWidth(value) - value = self.__properties.get("type", None) - if value is not None: - roi.setProfileType(value) - elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn, - rois.ProfileImageCrossROI)): - value = self.__properties.get("method", None) - if value is not None: - roi.setProfileMethod(value) - value = self.__properties.get("line-width", None) - if value is not None: - roi.setProfileLineWidth(value) - elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn, - rois.ProfileScatterCrossROI)): - value = self.__properties.get("npoints", None) - if value is not None: - roi.setNPoints(value) - - -class ProfileManager(qt.QObject): - """Base class for profile management tools - - :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate. - :param plot: :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` - on which to operate. - """ - def __init__(self, parent=None, plot=None, roiManager=None): - super(ProfileManager, self).__init__(parent) - - assert isinstance(plot, PlotWidget) - self._plotRef = weakref.ref( - plot, WeakMethodProxy(self.__plotDestroyed)) - - # Set-up interaction manager - if roiManager is None: - roiManager = RegionOfInterestManager(plot) - - self._roiManagerRef = weakref.ref(roiManager) - self._rois = [] - self._pendingRunners = [] - """List of ROIs which have to be updated""" - - self.__reentrantResults = {} - """Store reentrant result to avoid to skip some of them - cause the implementation uses a QEventLoop.""" - - self._profileWindowClass = ProfileWindow - """Class used to display the profile results""" - - self._computedProfiles = 0 - """Statistics for tests""" - - self.__itemTypes = [] - """Kind of items to use""" - - self.__tracking = False - """Is the plot active items are tracked""" - - self.__useColorFromCursor = True - """If true, force the ROI color with the colormap marker color""" - - self._item = None - """The selected item""" - - self.__singleProfileAtATime = True - """When it's true, only a single profile is displayed at a time.""" - - self._previousWindowGeometry = [] - - self._storeProperties = _StoreLastParamBehavior(self) - """If defined the profile properties of the last ROI are reused to the - new created ones""" - - # Listen to plot limits changed - plot.getXAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile) - plot.getYAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile) - - roiManager.sigInteractiveModeFinished.connect(self.__interactionFinished) - roiManager.sigInteractiveRoiCreated.connect(self.__roiCreated) - roiManager.sigRoiAdded.connect(self.__roiAdded) - roiManager.sigRoiAboutToBeRemoved.connect(self.__roiRemoved) - - def setSingleProfile(self, enable): - """ - Enable or disable the single profile mode. - - In single mode, the manager enforce a single ROI at the same - time. A new one will remove the previous one. - - If this mode is not enabled, many ROIs can be created, and many - profile windows will be displayed. - """ - self.__singleProfileAtATime = enable - - def isSingleProfile(self): - """ - Returns true if the manager is in a single profile mode. - - :rtype: bool - """ - return self.__singleProfileAtATime - - def __interactionFinished(self): - """Handle end of interactive mode""" - pass - - def __roiAdded(self, roi): - """Handle new ROI""" - # Filter out non profile ROIs - if not isinstance(roi, core.ProfileRoiMixIn): - return - self.__addProfile(roi) - - def __roiRemoved(self, roi): - """Handle removed ROI""" - # Filter out non profile ROIs - if not isinstance(roi, core.ProfileRoiMixIn): - return - self.__removeProfile(roi) - - def createProfileAction(self, profileRoiClass, parent=None): - """Create an action from a class of ProfileRoi - - :param core.ProfileRoiMixIn profileRoiClass: A class of a profile ROI - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - if not issubclass(profileRoiClass, core.ProfileRoiMixIn): - raise TypeError("Type %s not expected" % type(profileRoiClass)) - roiManager = self.getRoiManager() - action = CreateRoiModeAction(parent, roiManager, profileRoiClass) - if hasattr(profileRoiClass, "ICON"): - action.setIcon(icons.getQIcon(profileRoiClass.ICON)) - if hasattr(profileRoiClass, "NAME"): - def articulify(word): - """Add an an/a article in the front of the word""" - first = word[1] if word[0] == 'h' else word[0] - if first in "aeiou": - return "an " + word - return "a " + word - action.setText('Define %s' % articulify(profileRoiClass.NAME)) - action.setToolTip('Enables %s selection mode' % profileRoiClass.NAME) - action.setSingleShot(True) - return action - - def createClearAction(self, parent): - """Create an action to clean up the plot from the profile ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - action = _ClearAction(parent, self) - return action - - def createImageActions(self, parent): - """Create actions designed for image items. This actions created - new ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileImageHorizontalLineROI, - rois.ProfileImageVerticalLineROI, - rois.ProfileImageLineROI, - rois.ProfileImageDirectedLineROI, - rois.ProfileImageCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createScatterActions(self, parent): - """Create actions designed for scatter items. This actions created - new ROIs. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileScatterHorizontalLineROI, - rois.ProfileScatterVerticalLineROI, - rois.ProfileScatterLineROI, - rois.ProfileScatterCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createScatterSliceActions(self, parent): - """Create actions designed for regular scatter items. This actions - created new ROIs. - - This ROIs was designed to use the input data without interpolation, - like you could do with an image. - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileScatterHorizontalSliceROI, - rois.ProfileScatterVerticalSliceROI, - rois.ProfileScatterCrossSliceROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createImageStackActions(self, parent): - """Create actions designed for stack image items. This actions - created new ROIs. - - This ROIs was designed to create both profile on the displayed image - and profile on the full stack (2D result). - - :param qt.QObject parent: The parent of the created action. - :rtype: List[qt.QAction] - """ - profileClasses = [ - rois.ProfileImageStackHorizontalLineROI, - rois.ProfileImageStackVerticalLineROI, - rois.ProfileImageStackLineROI, - rois.ProfileImageStackCrossROI, - ] - return [self.createProfileAction(pc, parent=parent) for pc in profileClasses] - - def createEditorAction(self, parent): - """Create an action containing GUI to edit the selected profile ROI. - - :param qt.QObject parent: The parent of the created action. - :rtype: qt.QAction - """ - action = editors.ProfileRoiEditorAction(parent) - action.setRoiManager(self.getRoiManager()) - return action - - def setItemType(self, image=False, scatter=False): - """Set the item type to use and select the active one. - - :param bool image: Image item are allowed - :param bool scatter: Scatter item are allowed - """ - self.__itemTypes = [] - plot = self.getPlotWidget() - item = None - if image: - self.__itemTypes.append("image") - item = plot.getActiveImage() - if scatter: - self.__itemTypes.append("scatter") - if item is None: - item = plot.getActiveScatter() - self.setPlotItem(item) - - def setProfileWindowClass(self, profileWindowClass): - """Set the class which will be instantiated to display profile result. - """ - self._profileWindowClass = profileWindowClass - - def setActiveItemTracking(self, tracking): - """Enable/disable the tracking of the active item of the plot. - - :param bool tracking: Tracking mode - """ - if self.__tracking == tracking: - return - plot = self.getPlotWidget() - if self.__tracking: - plot.sigActiveImageChanged.disconnect(self._activeImageChanged) - plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged) - self.__tracking = tracking - if self.__tracking: - plot.sigActiveImageChanged.connect(self.__activeImageChanged) - plot.sigActiveScatterChanged.connect(self.__activeScatterChanged) - - def setDefaultColorFromCursorColor(self, enabled): - """Enabled/disable the use of the colormap cursor color to display the - ROIs. - - If set, the manager will update the color of the profile ROIs using the - current colormap cursor color from the selected item. - """ - self.__useColorFromCursor = enabled - - def __activeImageChanged(self, previous, legend): - """Handle plot item selection""" - if "image" in self.__itemTypes: - plot = self.getPlotWidget() - item = plot.getImage(legend) - self.setPlotItem(item) - - def __activeScatterChanged(self, previous, legend): - """Handle plot item selection""" - if "scatter" in self.__itemTypes: - plot = self.getPlotWidget() - item = plot.getScatter(legend) - self.setPlotItem(item) - - def __roiCreated(self, roi): - """Handle ROI creation""" - # Filter out non profile ROIs - if isinstance(roi, core.ProfileRoiMixIn): - if self._storeProperties is not None: - # Initialize the properties with the previous ones - self._storeProperties.restoreProperties(roi) - - def __addProfile(self, profileRoi): - """Add a new ROI to the manager.""" - if profileRoi.getFocusProxy() is None: - if self._storeProperties is not None: - # Follow changes on properties - self._storeProperties.setProfileRoi(profileRoi) - if self.__singleProfileAtATime: - # FIXME: It would be good to reuse the windows to avoid blinking - self.clearProfile() - - profileRoi._setProfileManager(self) - self._updateRoiColor(profileRoi) - self._rois.append(profileRoi) - self.requestUpdateProfile(profileRoi) - - def __removeProfile(self, profileRoi): - """Remove a ROI from the manager.""" - window = self._disconnectProfileWindow(profileRoi) - if window is not None: - geometry = window.geometry() - if not geometry.isEmpty(): - self._previousWindowGeometry.append(geometry) - self.clearProfileWindow(window) - if profileRoi in self._rois: - self._rois.remove(profileRoi) - - def _disconnectProfileWindow(self, profileRoi): - """Handle profile window close.""" - window = profileRoi.getProfileWindow() - profileRoi.setProfileWindow(None) - return window - - def clearProfile(self): - """Clear the associated ROI profile""" - roiManager = self.getRoiManager() - for roi in list(self._rois): - if roi.getFocusProxy() is not None: - # Skip sub ROIs, it will be removed by their parents - continue - roiManager.removeRoi(roi) - - if not roiManager.isDrawing(): - # Clean the selected mode - roiManager.stop() - - def hasPendingOperations(self): - """Returns true if a thread is still computing or displaying a profile. - - :rtype: bool - """ - return len(self.__reentrantResults) > 0 or len(self._pendingRunners) > 0 - - def requestUpdateAllProfile(self): - """Request to update the profile of all the managed ROIs. - """ - for roi in self._rois: - self.requestUpdateProfile(roi) - - def requestUpdateProfile(self, profileRoi): - """Request to update a specific profile ROI. - - :param ~core.ProfileRoiMixIn profileRoi: - """ - if profileRoi.computeProfile is None: - return - threadPool = silxGlobalThreadPool() - - # Clean up deprecated runners - for runner in list(self._pendingRunners): - if not inspect.isValid(runner): - self._pendingRunners.remove(runner) - continue - if runner.getRoi() is profileRoi: - if hasattr(threadPool, "tryTake"): - if threadPool.tryTake(runner): - self._pendingRunners.remove(runner) - else: # Support Qt<5.9 - runner._lazyCancel() - - item = self.getPlotItem() - if item is None or not isinstance(item, profileRoi.ITEM_KIND): - # This item is not compatible with this profile - profileRoi._setPlotItem(None) - profileWindow = profileRoi.getProfileWindow() - if profileWindow is not None: - profileWindow.setProfile(None) - return - - profileRoi._setPlotItem(item) - runner = _RunnableComputeProfile(threadPool, item, profileRoi) - runner.runnerFinished.connect(self.__cleanUpRunner) - runner.resultReady.connect(self.__displayResult) - self._pendingRunners.append(runner) - threadPool.start(runner) - - def __cleanUpRunner(self, runner): - """Remove a thread pool runner from the list of hold tasks. - - Called at the termination of the runner. - """ - if runner in self._pendingRunners: - self._pendingRunners.remove(runner) - - def __displayResult(self, roi, profileData): - """Display the result of a ROI. - - :param ~core.ProfileRoiMixIn profileRoi: A managed ROI - :param ~core.CurveProfileData profileData: Computed data profile - """ - if roi in self.__reentrantResults: - # Store the data to process it in the main loop - # And not a sub loop created by initProfileWindow - # This also remove the duplicated requested - self.__reentrantResults[roi] = profileData - return - - self.__reentrantResults[roi] = profileData - self._computedProfiles = self._computedProfiles + 1 - window = roi.getProfileWindow() - if window is None: - plot = self.getPlotWidget() - window = self.createProfileWindow(plot, roi) - # roi.profileWindow have to be set before initializing the window - # Cause the initialization is using QEventLoop - roi.setProfileWindow(window) - self.initProfileWindow(window, roi) - window.show() - - lastData = self.__reentrantResults.pop(roi) - window.setProfile(lastData) - - def __plotDestroyed(self, ref): - """Handle finalization of PlotWidget - - :param ref: weakref to the plot - """ - self._plotRef = None - self._roiManagerRef = None - self._pendingRunners = [] - - def setPlotItem(self, item): - """Set the plot item focused by the profile manager. - - :param ~silx.gui.plot.items.Item item: A plot item - """ - previous = self.getPlotItem() - if previous is item: - return - if item is None: - self._item = None - else: - item.sigItemChanged.connect(self.__itemChanged) - self._item = weakref.ref(item) - self._updateRoiColors() - self.requestUpdateAllProfile() - - def getDefaultColor(self, item): - """Returns the default ROI color to use according to the given item. - - :param ~silx.gui.plot.items.item.Item item: AN item - :rtype: qt.QColor - """ - color = 'pink' - if isinstance(item, items.ColormapMixIn): - colormap = item.getColormap() - name = colormap.getName() - if name is not None: - color = colors.cursorColorForColormap(name) - color = colors.asQColor(color) - return color - - def _updateRoiColors(self): - """Update ROI color according to the item selection""" - if not self.__useColorFromCursor: - return - item = self.getPlotItem() - color = self.getDefaultColor(item) - for roi in self._rois: - roi.setColor(color) - - def _updateRoiColor(self, roi): - """Update a specific ROI according to the current selected item. - - :param RegionOfInterest roi: The ROI to update - """ - if not self.__useColorFromCursor: - return - item = self.getPlotItem() - color = self.getDefaultColor(item) - roi.setColor(color) - - def __itemChanged(self, changeType): - """Handle item changes. - """ - if changeType in (items.ItemChangedType.DATA, - items.ItemChangedType.MASK, - items.ItemChangedType.POSITION, - items.ItemChangedType.SCALE): - self.requestUpdateAllProfile() - elif changeType == (items.ItemChangedType.COLORMAP): - self._updateRoiColors() - - def getPlotItem(self): - """Returns the item focused by the profile manager. - - :rtype: ~silx.gui.plot.items.Item - """ - if self._item is None: - return None - item = self._item() - if item is None: - self._item = None - return item - - def getPlotWidget(self): - """The plot associated to the profile manager. - - :rtype: ~silx.gui.plot.PlotWidget - """ - if self._plotRef is None: - return None - plot = self._plotRef() - if plot is None: - self._plotRef = None - return plot - - def getCurrentRoi(self): - """Returns the currently selected ROI, else None. - - :rtype: core.ProfileRoiMixIn - """ - roiManager = self.getRoiManager() - if roiManager is None: - return None - roi = roiManager.getCurrentRoi() - if not isinstance(roi, core.ProfileRoiMixIn): - return None - return roi - - def getRoiManager(self): - """Returns the used ROI manager - - :rtype: RegionOfInterestManager - """ - return self._roiManagerRef() - - def createProfileWindow(self, plot, roi): - """Create a new profile window. - - :param ~core.ProfileRoiMixIn roi: The plot containing the raw data - :param ~core.ProfileRoiMixIn roi: A managed ROI - :rtype: ~ProfileWindow - """ - return self._profileWindowClass(plot) - - def initProfileWindow(self, profileWindow, roi): - """This function is called just after the profile window creation in - order to initialize the window location. - - :param ~ProfileWindow profileWindow: - The profile window to initialize. - """ - # Enforce the use of one of the widgets - # To have the correct window size - profileWindow.prepareWidget(roi) - profileWindow.adjustSize() - - # Trick to avoid blinking while retrieving the right window size - # Display the window, hide it and wait for some event loops - profileWindow.show() - profileWindow.hide() - eventLoop = qt.QEventLoop(self) - for _ in range(10): - if not eventLoop.processEvents(): - break - - profileWindow.show() - if len(self._previousWindowGeometry) > 0: - geometry = self._previousWindowGeometry.pop() - profileWindow.setGeometry(geometry) - return - - window = self.getPlotWidget().window() - winGeom = window.frameGeometry() - qapp = qt.QApplication.instance() - desktop = qapp.desktop() - screenGeom = desktop.availableGeometry(window) - spaceOnLeftSide = winGeom.left() - spaceOnRightSide = screenGeom.width() - winGeom.right() - - profileGeom = profileWindow.frameGeometry() - profileWidth = profileGeom.width() - - # Align vertically to the center of the window - top = winGeom.top() + (winGeom.height() - profileGeom.height()) // 2 - - margin = 5 - if profileWidth < spaceOnRightSide: - # Place profile on the right - left = winGeom.right() + margin - elif profileWidth < spaceOnLeftSide: - # Place profile on the left - left = max(0, winGeom.left() - profileWidth - margin) - else: - # Move it as much as possible where there is more space - if spaceOnLeftSide > spaceOnRightSide: - left = 0 - else: - left = screenGeom.width() - profileGeom.width() - profileWindow.move(left, top) - - - def clearProfileWindow(self, profileWindow): - """Called when a profile window is not anymore needed. - - By default the window will be closed. But it can be - inherited to change this behavior. - """ - profileWindow.deleteLater() diff --git a/silx/gui/plot/tools/profile/rois.py b/silx/gui/plot/tools/profile/rois.py deleted file mode 100644 index eb7e975..0000000 --- a/silx/gui/plot/tools/profile/rois.py +++ /dev/null @@ -1,1156 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module define ROIs for profile tools. - -.. inheritance-diagram:: - silx.gui.plot.tools.profile.rois - :top-classes: silx.gui.plot.tools.profile.core.ProfileRoiMixIn, silx.gui.plot.items.roi.RegionOfInterest - :parts: 1 - :private-bases: -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "01/12/2020" - -import numpy -import weakref -from concurrent.futures import CancelledError - -from silx.gui import colors - -from silx.gui.plot import items -from silx.gui.plot.items import roi as roi_items -from . import core -from silx.gui import utils -from .....utils.proxy import docstring - - -def _relabelAxes(plot, text): - """Relabel {xlabel} and {ylabel} from this text using the corresponding - plot axis label. If the axis label is empty, label it with "X" and "Y". - - :rtype: str - """ - xLabel = plot.getXAxis().getLabel() - if not xLabel: - xLabel = "X" - yLabel = plot.getYAxis().getLabel() - if not yLabel: - yLabel = "Y" - return text.format(xlabel=xLabel, ylabel=yLabel) - - -def _lineProfileTitle(x0, y0, x1, y1): - """Compute corresponding plot title - - This can be overridden to change title behavior. - - :param float x0: Profile start point X coord - :param float y0: Profile start point Y coord - :param float x1: Profile end point X coord - :param float y1: Profile end point Y coord - :return: Title to use - :rtype: str - """ - if x0 == x1: - title = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1) - elif y0 == y1: - title = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1) - else: - m = (y1 - y0) / (x1 - x0) - b = y0 - m * x0 - title = '{ylabel} = %g * {xlabel} %+g' % (m, b) - - return title - - -class _ImageProfileArea(items.Shape): - """This shape displays the location of pixels used to compute the - profile.""" - - def __init__(self, parentRoi): - items.Shape.__init__(self, "polygon") - color = colors.rgba(parentRoi.getColor()) - self.setColor(color) - self.setFill(True) - self.setOverlay(True) - self.setPoints([[0, 0], [0, 0]]) # Else it segfault - - self.__parentRoi = weakref.ref(parentRoi) - parentRoi.sigItemChanged.connect(self._updateAreaProperty) - parentRoi.sigRegionChanged.connect(self._updateArea) - parentRoi.sigProfilePropertyChanged.connect(self._updateArea) - parentRoi.sigPlotItemChanged.connect(self._updateArea) - - def getParentRoi(self): - if self.__parentRoi is None: - return None - parentRoi = self.__parentRoi() - if parentRoi is None: - self.__parentRoi = None - return parentRoi - - def _updateAreaProperty(self, event=None, checkVisibility=True): - parentRoi = self.sender() - if event == items.ItemChangedType.COLOR: - parentRoi._updateItemProperty(event, parentRoi, self) - elif event == items.ItemChangedType.VISIBLE: - if self.getPlotItem() is not None: - parentRoi._updateItemProperty(event, parentRoi, self) - - def _updateArea(self): - roi = self.getParentRoi() - item = roi.getPlotItem() - if item is None: - self.setVisible(False) - return - polygon = self._computePolygon(item) - self.setVisible(True) - polygon = numpy.array(polygon).T - self.setLineStyle("--") - self.setPoints(polygon, copy=False) - - def _computePolygon(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - currentData = item.getValueData(copy=False) - - roi = self.getParentRoi() - origin = item.getOrigin() - scale = item.getScale() - _coords, _profile, area, _profileName, _xLabel = core.createProfile( - roiInfo=roi._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=roi.getProfileLineWidth(), - method="none") - return area - - -class _SliceProfileArea(items.Shape): - """This shape displays the location a profile in a scatter. - - Each point used to compute the slice are linked together. - """ - - def __init__(self, parentRoi): - items.Shape.__init__(self, "polygon") - color = colors.rgba(parentRoi.getColor()) - self.setColor(color) - self.setFill(True) - self.setOverlay(True) - self.setPoints([[0, 0], [0, 0]]) # Else it segfault - - self.__parentRoi = weakref.ref(parentRoi) - parentRoi.sigItemChanged.connect(self._updateAreaProperty) - parentRoi.sigRegionChanged.connect(self._updateArea) - parentRoi.sigProfilePropertyChanged.connect(self._updateArea) - parentRoi.sigPlotItemChanged.connect(self._updateArea) - - def getParentRoi(self): - if self.__parentRoi is None: - return None - parentRoi = self.__parentRoi() - if parentRoi is None: - self.__parentRoi = None - return parentRoi - - def _updateAreaProperty(self, event=None, checkVisibility=True): - parentRoi = self.sender() - if event == items.ItemChangedType.COLOR: - parentRoi._updateItemProperty(event, parentRoi, self) - elif event == items.ItemChangedType.VISIBLE: - if self.getPlotItem() is not None: - parentRoi._updateItemProperty(event, parentRoi, self) - - def _updateArea(self): - roi = self.getParentRoi() - item = roi.getPlotItem() - if item is None: - self.setVisible(False) - return - polylines = self._computePolylines(roi, item) - if polylines is None: - self.setVisible(False) - return - self.setVisible(True) - self.setLineStyle("--") - self.setPoints(polylines, copy=False) - - def _computePolylines(self, roi, item): - slicing = roi._getSlice(item) - if slicing is None: - return None - xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False) - xx, yy = xx[slicing], yy[slicing] - polylines = numpy.array((xx, yy)).T - if len(polylines) == 0: - return None - return polylines - - -class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn): - """Provide common behavior for silx default image profile ROI. - """ - - ITEM_KIND = items.ImageBase - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__method = "mean" - self.__width = 1 - self.sigRegionChanged.connect(self.__regionChanged) - self.sigPlotItemChanged.connect(self.__updateArea) - self.__area = _ImageProfileArea(self) - self.addItem(self.__area) - - def __regionChanged(self): - self.invalidateProfile() - self.__updateArea() - - def setProfileMethod(self, method): - """ - :param str method: method to compute the profile. Can be 'mean' or 'sum' - """ - if self.__method == method: - return - self.__method = method - self.invalidateProperties() - self.invalidateProfile() - - def getProfileMethod(self): - return self.__method - - def setProfileLineWidth(self, width): - if self.__width == width: - return - self.__width = width - self.__updateArea() - self.invalidateProperties() - self.invalidateProfile() - - def getProfileLineWidth(self): - return self.__width - - def __updateArea(self): - plotItem = self.getPlotItem() - if plotItem is None: - self.setLineStyle("-") - else: - self.setLineStyle("--") - - def _getRoiInfo(self): - """Wrapper to allow to reuse the previous Profile code. - - It would be good to remove it at one point. - """ - if isinstance(self, roi_items.HorizontalLineROI): - lineProjectionMode = 'X' - y = self.getPosition() - roiStart = (0, y) - roiEnd = (1, y) - elif isinstance(self, roi_items.VerticalLineROI): - lineProjectionMode = 'Y' - x = self.getPosition() - roiStart = (x, 0) - roiEnd = (x, 1) - elif isinstance(self, roi_items.LineROI): - lineProjectionMode = 'D' - roiStart, roiEnd = self.getEndPoints() - else: - assert False - - return roiStart, roiEnd, lineProjectionMode - - def computeProfile(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - origin = item.getOrigin() - scale = item.getScale() - method = self.getProfileMethod() - lineWidth = self.getProfileLineWidth() - - def createProfile2(currentData): - coords, profile, _area, profileName, xLabel = core.createProfile( - roiInfo=self._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=lineWidth, - method=method) - return coords, profile, profileName, xLabel - - currentData = item.getValueData(copy=False) - - yLabel = "%s" % str(method).capitalize() - coords, profile, title, xLabel = createProfile2(currentData) - title = title + "; width = %d" % lineWidth - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - title = _relabelAxes(plot, title) - xLabel = _relabelAxes(plot, xLabel) - - if isinstance(item, items.ImageRgba): - rgba = item.getData(copy=False) - _coords, r, _profileName, _xLabel = createProfile2(rgba[..., 0]) - _coords, g, _profileName, _xLabel = createProfile2(rgba[..., 1]) - _coords, b, _profileName, _xLabel = createProfile2(rgba[..., 2]) - if rgba.shape[-1] == 4: - _coords, a, _profileName, _xLabel = createProfile2(rgba[..., 3]) - else: - a = [None] - data = core.RgbaProfileData( - coords=coords, - profile=profile[0], - profile_r=r[0], - profile_g=g[0], - profile_b=b[0], - profile_a=a[0], - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - else: - data = core.CurveProfileData( - coords=coords, - profile=profile[0], - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - return data - - -class ProfileImageHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an horizontal profile at a location of an image""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageVerticalLineROI(roi_items.VerticalLineROI, - _DefaultImageProfileRoiMixIn): - """ROI for a vertical profile at a location of an image""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageLineROI(roi_items.LineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an image profile between 2 points. - - The X profile of this ROI is the projecting into one of the x/y axes, - using its scale and its orientation. - """ - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageDirectedLineROI(roi_items.LineROI, - _DefaultImageProfileRoiMixIn): - """ROI for an image profile between 2 points. - - The X profile of the line is displayed projected into the line itself, - using its scale and its orientation. It's the distance from the origin. - """ - - ICON = 'shape-diagonal-directed' - NAME = 'directed line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageProfileRoiMixIn.__init__(self, parent=parent) - self._handleStart.setSymbol('o') - - def computeProfile(self, item): - if not isinstance(item, items.ImageBase): - raise TypeError("Unexpected class %s" % type(item)) - - from silx.image.bilinear import BilinearImage - - origin = item.getOrigin() - scale = item.getScale() - method = self.getProfileMethod() - lineWidth = self.getProfileLineWidth() - currentData = item.getValueData(copy=False) - - roiInfo = self._getRoiInfo() - roiStart, roiEnd, _lineProjectionMode = roiInfo - - startPt = ((roiStart[1] - origin[1]) / scale[1], - (roiStart[0] - origin[0]) / scale[0]) - endPt = ((roiEnd[1] - origin[1]) / scale[1], - (roiEnd[0] - origin[0]) / scale[0]) - - if numpy.array_equal(startPt, endPt): - return None - - bilinear = BilinearImage(currentData) - profile = bilinear.profile_line( - (startPt[0] - 0.5, startPt[1] - 0.5), - (endPt[0] - 0.5, endPt[1] - 0.5), - lineWidth, - method=method) - - # Compute the line size - lineSize = numpy.sqrt((roiEnd[1] - roiStart[1]) ** 2 + - (roiEnd[0] - roiStart[0]) ** 2) - coords = numpy.linspace(0, lineSize, len(profile), - endpoint=True, - dtype=numpy.float32) - - title = _lineProfileTitle(*roiStart, *roiEnd) - title = title + "; width = %d" % lineWidth - xLabel = "√({xlabel}²+{ylabel}²)" - yLabel = str(method).capitalize() - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - xLabel = _relabelAxes(plot, xLabel) - title = _relabelAxes(plot, title) - - data = core.CurveProfileData( - coords=coords, - profile=profile, - title=title, - xLabel=xLabel, - yLabel=yLabel, - ) - return data - - -class _ProfileCrossROI(roi_items.HandleBasedROI, core.ProfileRoiMixIn): - - """ROI to manage a cross of profiles - - It is managed using 2 sub ROIs for vertical and horizontal. - """ - - _kind = "Cross" - """Label for this kind of ROI""" - - _plotShape = "point" - """Plot shape which is used for the first interaction""" - - def __init__(self, parent=None): - roi_items.HandleBasedROI.__init__(self, parent=parent) - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.sigRegionChanged.connect(self.__regionChanged) - self.sigAboutToBeRemoved.connect(self.__aboutToBeRemoved) - self.__position = 0, 0 - self.__vline = None - self.__hline = None - self.__handle = self.addHandle() - self.__handleLabel = self.addLabelHandle() - self.__handleLabel.setText(self.getName()) - self.__inhibitReentance = utils.LockReentrant() - self.computeProfile = None - self.sigItemChanged.connect(self.__updateLineProperty) - - # Make sure the marker is over the ROIs - self.__handle.setZValue(1) - # Create the vline and the hline - self._createSubRois() - - @docstring(roi_items.HandleBasedROI) - def contains(self, position): - roiPos = self.getPosition() - return position[0] == roiPos[0] or position[1] == roiPos[1] - - def setFirstShapePoints(self, points): - pos = points[0] - self.setPosition(pos) - - def getPosition(self): - """Returns the position of this ROI - - :rtype: numpy.ndarray - """ - return self.__position - - def setPosition(self, pos): - """Set the position of this ROI - - :param numpy.ndarray pos: 2d-coordinate of this point - """ - self.__position = pos - with utils.blockSignals(self.__handle): - self.__handle.setPosition(*pos) - with utils.blockSignals(self.__handleLabel): - self.__handleLabel.setPosition(*pos) - self.sigRegionChanged.emit() - - def handleDragUpdated(self, handle, origin, previous, current): - if handle is self.__handle: - self.setPosition(current) - - def __updateLineProperty(self, event=None, checkVisibility=True): - if event == items.ItemChangedType.NAME: - self.__handleLabel.setText(self.getName()) - elif event in [items.ItemChangedType.COLOR, - items.ItemChangedType.VISIBLE]: - lines = [] - if self.__vline: - lines.append(self.__vline) - if self.__hline: - lines.append(self.__hline) - self._updateItemProperty(event, self, lines) - - def _createLines(self, parent): - """Inherit this function to return 2 ROI objects for respectivly - the horizontal, and the vertical lines.""" - raise NotImplementedError() - - def _setProfileManager(self, profileManager): - core.ProfileRoiMixIn._setProfileManager(self, profileManager) - # Connecting the vline and the hline - roiManager = profileManager.getRoiManager() - roiManager.addRoi(self.__vline) - roiManager.addRoi(self.__hline) - - def _createSubRois(self): - hline, vline = self._createLines(parent=None) - for i, line in enumerate([vline, hline]): - line.setPosition(self.__position[i]) - line.setEditable(True) - line.setSelectable(True) - line.setFocusProxy(self) - line.setName("") - self.__vline = vline - self.__hline = hline - vline.sigAboutToBeRemoved.connect(self.__vlineRemoved) - vline.sigRegionChanged.connect(self.__vlineRegionChanged) - hline.sigAboutToBeRemoved.connect(self.__hlineRemoved) - hline.sigRegionChanged.connect(self.__hlineRegionChanged) - - def _getLines(self): - return self.__hline, self.__vline - - def __regionChanged(self): - if self.__inhibitReentance.locked(): - return - x, y = self.getPosition() - hline, vline = self._getLines() - if hline is None: - return - with self.__inhibitReentance: - hline.setPosition(y) - vline.setPosition(x) - - def __vlineRegionChanged(self): - if self.__inhibitReentance.locked(): - return - pos = self.getPosition() - vline = self.__vline - pos = vline.getPosition(), pos[1] - with self.__inhibitReentance: - self.setPosition(pos) - - def __hlineRegionChanged(self): - if self.__inhibitReentance.locked(): - return - pos = self.getPosition() - hline = self.__hline - pos = pos[0], hline.getPosition() - with self.__inhibitReentance: - self.setPosition(pos) - - def __aboutToBeRemoved(self): - vline = self.__vline - hline = self.__hline - # Avoid side remove signals - if hline is not None: - hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved) - hline.sigRegionChanged.disconnect(self.__hlineRegionChanged) - if vline is not None: - vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved) - vline.sigRegionChanged.disconnect(self.__vlineRegionChanged) - # Clean up the child - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - if hline is not None: - roiManager.removeRoi(hline) - self.__hline = None - if vline is not None: - roiManager.removeRoi(vline) - self.__vline = None - - def __hlineRemoved(self): - self.__lineRemoved(isHline=True) - - def __vlineRemoved(self): - self.__lineRemoved(isHline=False) - - def __lineRemoved(self, isHline): - """If any of the lines is removed: disconnect this objects, and let the - other one persist""" - hline, vline = self._getLines() - - hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved) - vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved) - hline.sigRegionChanged.disconnect(self.__hlineRegionChanged) - vline.sigRegionChanged.disconnect(self.__vlineRegionChanged) - - self.__hline = None - self.__vline = None - profileManager = self.getProfileManager() - roiManager = profileManager.getRoiManager() - if isHline: - self.__releaseLine(vline) - else: - self.__releaseLine(hline) - roiManager.removeRoi(self) - - def __releaseLine(self, line): - """Release the line in order to make it independent""" - line.setFocusProxy(None) - line.setName(self.getName()) - line.setEditable(self.isEditable()) - line.setSelectable(self.isSelectable()) - - -class ProfileImageCrossROI(_ProfileCrossROI): - """ROI to manage a cross of profiles - - It is managed using 2 sub ROIs for vertical and horizontal. - """ - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.ImageBase - - def _createLines(self, parent): - vline = ProfileImageVerticalLineROI(parent=parent) - hline = ProfileImageHorizontalLineROI(parent=parent) - return hline, vline - - def setProfileMethod(self, method): - """ - :param str method: method to compute the profile. Can be 'mean' or 'sum' - """ - hline, vline = self._getLines() - hline.setProfileMethod(method) - vline.setProfileMethod(method) - self.invalidateProperties() - - def getProfileMethod(self): - hline, _vline = self._getLines() - return hline.getProfileMethod() - - def setProfileLineWidth(self, width): - hline, vline = self._getLines() - hline.setProfileLineWidth(width) - vline.setProfileLineWidth(width) - self.invalidateProperties() - - def getProfileLineWidth(self): - hline, _vline = self._getLines() - return hline.getProfileLineWidth() - - -class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn): - """Provide common behavior for silx default scatter profile ROI. - """ - - ITEM_KIND = items.Scatter - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__nPoints = 1024 - self.sigRegionChanged.connect(self.__regionChanged) - - def __regionChanged(self): - self.invalidateProfile() - - # Number of points - - def getNPoints(self): - """Returns the number of points of the profiles - - :rtype: int - """ - return self.__nPoints - - def setNPoints(self, npoints): - """Set the number of points of the profiles - - :param int npoints: - """ - npoints = int(npoints) - if npoints < 1: - raise ValueError("Unsupported number of points: %d" % npoints) - elif npoints != self.__nPoints: - self.__nPoints = npoints - self.invalidateProperties() - self.invalidateProfile() - - def _computeProfile(self, scatter, x0, y0, x1, y1): - """Compute corresponding profile - - :param float x0: Profile start point X coord - :param float y0: Profile start point Y coord - :param float x1: Profile end point X coord - :param float y1: Profile end point Y coord - :return: (points, values) profile data or None - """ - future = scatter._getInterpolator() - try: - interpolator = future.result() - except CancelledError: - return None - if interpolator is None: - return None # Cannot init an interpolator - - nPoints = self.getNPoints() - points = numpy.transpose(( - numpy.linspace(x0, x1, nPoints, endpoint=True), - numpy.linspace(y0, y1, nPoints, endpoint=True))) - - values = interpolator(points) - - if not numpy.any(numpy.isfinite(values)): - return None # Profile outside convex hull - - return points, values - - def computeProfile(self, item): - """Update profile according to current ROI""" - if not isinstance(item, items.Scatter): - raise TypeError("Unexpected class %s" % type(item)) - - # Get end points - if isinstance(self, roi_items.LineROI): - points = self.getEndPoints() - x0, y0 = points[0] - x1, y1 = points[1] - elif isinstance(self, (roi_items.VerticalLineROI, roi_items.HorizontalLineROI)): - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - - if isinstance(self, roi_items.HorizontalLineROI): - x0, x1 = plot.getXAxis().getLimits() - y0 = y1 = self.getPosition() - - elif isinstance(self, roi_items.VerticalLineROI): - x0 = x1 = self.getPosition() - y0, y1 = plot.getYAxis().getLimits() - else: - raise RuntimeError('Unsupported ROI for profile: {}'.format(self.__class__)) - - if x1 < x0 or (x1 == x0 and y1 < y0): - # Invert points - x0, y0, x1, y1 = x1, y1, x0, y0 - - profile = self._computeProfile(item, x0, y0, x1, y1) - if profile is None: - return None - - title = _lineProfileTitle(x0, y0, x1, y1) - points = profile[0] - values = profile[1] - - if (numpy.abs(points[-1, 0] - points[0, 0]) > - numpy.abs(points[-1, 1] - points[0, 1])): - xProfile = points[:, 0] - xLabel = '{xlabel}' - else: - xProfile = points[:, 1] - xLabel = '{ylabel}' - - # Use the axis names from the original - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - title = _relabelAxes(plot, title) - xLabel = _relabelAxes(plot, xLabel) - - data = core.CurveProfileData( - coords=xProfile, - profile=values, - title=title, - xLabel=xLabel, - yLabel='Profile', - ) - return data - - -class ProfileScatterHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterVerticalLineROI(roi_items.VerticalLineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterLineROI(roi_items.LineROI, - _DefaultScatterProfileRoiMixIn): - """ROI for an horizontal profile at a location of a scatter""" - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterCrossROI(_ProfileCrossROI): - """ROI to manage a cross of profiles for scatters. - """ - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.Scatter - - def _createLines(self, parent): - vline = ProfileScatterVerticalLineROI(parent=parent) - hline = ProfileScatterHorizontalLineROI(parent=parent) - return hline, vline - - def getNPoints(self): - """Returns the number of points of the profiles - - :rtype: int - """ - hline, _vline = self._getLines() - return hline.getNPoints() - - def setNPoints(self, npoints): - """Set the number of points of the profiles - - :param int npoints: - """ - hline, vline = self._getLines() - hline.setNPoints(npoints) - vline.setNPoints(npoints) - self.invalidateProperties() - - -class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn): - """Default ROI to allow to slice in the scatter data.""" - - ITEM_KIND = items.Scatter - - def __init__(self, parent=None): - core.ProfileRoiMixIn.__init__(self, parent=parent) - self.__area = _SliceProfileArea(self) - self.addItem(self.__area) - self.sigRegionChanged.connect(self._regionChanged) - self.sigPlotItemChanged.connect(self._updateArea) - - def _regionChanged(self): - self.invalidateProfile() - self._updateArea() - - def _updateArea(self): - plotItem = self.getPlotItem() - if plotItem is None: - self.setLineStyle("-") - else: - self.setLineStyle("--") - - def _getSlice(self, item): - position = self.getPosition() - bounds = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_BOUNDS) - if isinstance(self, roi_items.HorizontalLineROI): - axis = 1 - elif isinstance(self, roi_items.VerticalLineROI): - axis = 0 - else: - assert False - if position < bounds[0][axis] or position > bounds[1][axis]: - # ROI outside of the scatter bound - return None - - major_order = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER) - assert major_order == 'row' - max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_SHAPE) - - xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False) - if isinstance(self, roi_items.HorizontalLineROI): - axis = yy - max_grid_first = max_grid_yy - max_grid_second = max_grid_xx - major_axis = major_order == 'column' - elif isinstance(self, roi_items.VerticalLineROI): - axis = xx - max_grid_first = max_grid_xx - max_grid_second = max_grid_yy - major_axis = major_order == 'row' - else: - assert False - - def argnearest(array, value): - array = numpy.abs(array - value) - return numpy.argmin(array) - - if major_axis: - # slice in the middle of the scatter - start = max_grid_second // 2 * max_grid_first - vslice = axis[start:start + max_grid_second] - index = argnearest(vslice, position) - slicing = slice(index, None, max_grid_first) - else: - # slice in the middle of the scatter - vslice = axis[max_grid_second // 2::max_grid_second] - index = argnearest(vslice, position) - start = index * max_grid_second - slicing = slice(start, start + max_grid_second) - - return slicing - - def computeProfile(self, item): - if not isinstance(item, items.Scatter): - raise TypeError("Unsupported %s item" % type(item)) - - slicing = self._getSlice(item) - if slicing is None: - # ROI out of bounds - return None - - _xx, _yy, values, _xx_error, _yy_error = item.getData(copy=False) - profile = values[slicing] - - if isinstance(self, roi_items.HorizontalLineROI): - title = "Horizontal slice" - xLabel = "{xlabel} index" - elif isinstance(self, roi_items.VerticalLineROI): - title = "Vertical slice" - xLabel = "{ylabel} index" - else: - assert False - - # Use the axis names from the original plot - profileManager = self.getProfileManager() - plot = profileManager.getPlotWidget() - xLabel = _relabelAxes(plot, xLabel) - - data = core.CurveProfileData( - coords=numpy.arange(len(profile)), - profile=profile, - title=title, - xLabel=xLabel, - yLabel="Profile", - ) - return data - - -class ProfileScatterHorizontalSliceROI(roi_items.HorizontalLineROI, - _DefaultScatterProfileSliceRoiMixIn): - """ROI for an horizontal profile at a location of a scatter - using data slicing. - """ - - ICON = 'slice-horizontal' - NAME = 'horizontal data slice profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI, - _DefaultScatterProfileSliceRoiMixIn): - """ROI for a vertical profile at a location of a scatter - using data slicing. - """ - - ICON = 'slice-vertical' - NAME = 'vertical data slice profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent) - - -class ProfileScatterCrossSliceROI(_ProfileCrossROI): - """ROI to manage a cross of slicing profiles on scatters. - """ - - ICON = 'slice-cross' - NAME = 'cross data slice profile' - ITEM_KIND = items.Scatter - - def _createLines(self, parent): - vline = ProfileScatterVerticalSliceROI(parent=parent) - hline = ProfileScatterHorizontalSliceROI(parent=parent) - return hline, vline - - -class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn): - - ITEM_KIND = items.ImageStack - - def __init__(self, parent=None): - super(_DefaultImageStackProfileRoiMixIn, self).__init__(parent=parent) - self.__profileType = "1D" - """Kind of profile""" - - def getProfileType(self): - return self.__profileType - - def setProfileType(self, kind): - assert kind in ["1D", "2D"] - if self.__profileType == kind: - return - self.__profileType = kind - self.invalidateProperties() - self.invalidateProfile() - - def computeProfile(self, item): - if not isinstance(item, items.ImageStack): - raise TypeError("Unexpected class %s" % type(item)) - - kind = self.getProfileType() - if kind == "1D": - result = _DefaultImageProfileRoiMixIn.computeProfile(self, item) - # z = item.getStackPosition() - return result - - assert kind == "2D" - - def createProfile2(currentData): - coords, profile, _area, profileName, xLabel = core.createProfile( - roiInfo=self._getRoiInfo(), - currentData=currentData, - origin=origin, - scale=scale, - lineWidth=self.getProfileLineWidth(), - method=method) - return coords, profile, profileName, xLabel - - currentData = numpy.array(item.getStackData(copy=False)) - origin = item.getOrigin() - scale = item.getScale() - colormap = item.getColormap() - method = self.getProfileMethod() - - coords, profile, profileName, xLabel = createProfile2(currentData) - - data = core.ImageProfileData( - coords=coords, - profile=profile, - title=profileName, - xLabel=xLabel, - yLabel="Profile", - colormap=colormap, - ) - return data - - -class ProfileImageStackHorizontalLineROI(roi_items.HorizontalLineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an horizontal profile at a location of a stack of images""" - - ICON = 'shape-horizontal' - NAME = 'horizontal line profile' - - def __init__(self, parent=None): - roi_items.HorizontalLineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackVerticalLineROI(roi_items.VerticalLineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-vertical' - NAME = 'vertical line profile' - - def __init__(self, parent=None): - roi_items.VerticalLineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackLineROI(roi_items.LineROI, - _DefaultImageStackProfileRoiMixIn): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-diagonal' - NAME = 'line profile' - - def __init__(self, parent=None): - roi_items.LineROI.__init__(self, parent=parent) - _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent) - - -class ProfileImageStackCrossROI(ProfileImageCrossROI): - """ROI for an vertical profile at a location of a stack of images""" - - ICON = 'shape-cross' - NAME = 'cross profile' - ITEM_KIND = items.ImageStack - - def _createLines(self, parent): - vline = ProfileImageStackVerticalLineROI(parent=parent) - hline = ProfileImageStackHorizontalLineROI(parent=parent) - return hline, vline - - def getProfileType(self): - hline, _vline = self._getLines() - return hline.getProfileType() - - def setProfileType(self, kind): - hline, vline = self._getLines() - hline.setProfileType(kind) - vline.setProfileType(kind) - self.invalidateProperties() diff --git a/silx/gui/plot/tools/profile/toolbar.py b/silx/gui/plot/tools/profile/toolbar.py deleted file mode 100644 index 4a9a195..0000000 --- a/silx/gui/plot/tools/profile/toolbar.py +++ /dev/null @@ -1,172 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides tool bar helper. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import logging -import weakref - -from silx.gui import qt -from silx.gui.widgets.MultiModeAction import MultiModeAction -from . import manager -from .. import roi as roi_mdl -from silx.gui.plot import items - - -_logger = logging.getLogger(__name__) - - -class ProfileToolBar(qt.QToolBar): - """Tool bar to provide profile for a plot. - - It is an helper class. For a dedicated application it would be better to - use an own tool bar in order in order have more flexibility. - """ - def __init__(self, parent=None, plot=None): - super(ProfileToolBar, self).__init__(parent=parent) - self.__scheme = None - self.__manager = None - self.__plot = weakref.ref(plot) - self.__multiAction = None - - def getPlotWidget(self): - """The :class:`~silx.gui.plot.PlotWidget` associated to the toolbar. - - :rtype: Union[~silx.gui.plot.PlotWidget,None] - """ - if self.__plot is None: - return None - plot = self.__plot() - if self.__plot is None: - self.__plot = None - return plot - - def setScheme(self, scheme): - """Initialize the tool bar using a configuration scheme. - - It have to be done once and only once. - - :param str scheme: One of "scatter", "image", "imagestack" - """ - assert self.__scheme is None - self.__scheme = scheme - - plot = self.getPlotWidget() - self.__manager = manager.ProfileManager(self, plot) - - if scheme == "image": - self.__manager.setItemType(image=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createImageActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveImageChanged.connect(self._activeImageChanged) - self._activeImageChanged() - - elif scheme == "scatter": - self.__manager.setItemType(scatter=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createScatterActions(self): - multiAction.addAction(action) - for action in self.__manager.createScatterSliceActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveScatterChanged.connect(self._activeScatterChanged) - self._activeScatterChanged() - - elif scheme == "imagestack": - self.__manager.setItemType(image=True) - self.__manager.setActiveItemTracking(True) - - multiAction = MultiModeAction(self) - self.addAction(multiAction) - for action in self.__manager.createImageStackActions(self): - multiAction.addAction(action) - self.__multiAction = multiAction - - cleanAction = self.__manager.createClearAction(self) - self.addAction(cleanAction) - editorAction = self.__manager.createEditorAction(self) - self.addAction(editorAction) - - plot.sigActiveImageChanged.connect(self._activeImageChanged) - self._activeImageChanged() - - else: - raise ValueError("Toolbar scheme %s unsupported" % scheme) - - def _setRoiActionEnabled(self, itemKind, enabled): - for action in self.__multiAction.getMenu().actions(): - if not isinstance(action, roi_mdl.CreateRoiModeAction): - continue - roiClass = action.getRoiClass() - if issubclass(itemKind, roiClass.ITEM_KIND): - action.setEnabled(enabled) - - def _activeImageChanged(self, previous=None, legend=None): - """Handle active image change to toggle actions""" - if legend is None: - self._setRoiActionEnabled(items.ImageStack, False) - self._setRoiActionEnabled(items.ImageBase, False) - else: - plot = self.getPlotWidget() - image = plot.getActiveImage() - # Disable for empty image - enabled = image.getData(copy=False).size > 0 - self._setRoiActionEnabled(type(image), enabled) - - def _activeScatterChanged(self, previous=None, legend=None): - """Handle active scatter change to toggle actions""" - if legend is None: - self._setRoiActionEnabled(items.Scatter, False) - else: - plot = self.getPlotWidget() - scatter = plot.getActiveScatter() - # Disable for empty image - enabled = scatter.getValueData(copy=False).size > 0 - self._setRoiActionEnabled(type(scatter), enabled) diff --git a/silx/gui/plot/tools/roi.py b/silx/gui/plot/tools/roi.py deleted file mode 100644 index 4e2d6db..0000000 --- a/silx/gui/plot/tools/roi.py +++ /dev/null @@ -1,1417 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides ROI interaction for :class:`~silx.gui.plot.PlotWidget`. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import enum -import logging -import time -import weakref -import functools - -import numpy - -from ... import qt, icons -from ...utils import blockSignals -from ...utils import LockReentrant -from .. import PlotWidget -from ..items import roi as roi_items - -from ...colors import rgba - - -logger = logging.getLogger(__name__) - - -class CreateRoiModeAction(qt.QAction): - """ - This action is a plot mode which allows to create new ROIs using a ROI - manager. - - A ROI is created using a specific `roiClass`. `initRoi` and `finalizeRoi` - can be inherited to custom the ROI initialization. - - :param class roiClass: The ROI class which will be created by this action. - :param qt.QObject parent: The action parent - :param RegionOfInterestManager roiManager: The ROI manager - """ - - def __init__(self, parent, roiManager, roiClass): - assert roiManager is not None - assert roiClass is not None - qt.QAction.__init__(self, parent=parent) - self._roiManager = weakref.ref(roiManager) - self._roiClass = roiClass - self._singleShot = False - self._initAction() - self.triggered[bool].connect(self._actionTriggered) - - def _initAction(self): - """Default initialization of the action""" - roiClass = self._roiClass - - name = None - iconName = None - if hasattr(roiClass, "NAME"): - name = roiClass.NAME - if hasattr(roiClass, "ICON"): - iconName = roiClass.ICON - - if iconName is None: - iconName = "add-shape-unknown" - if name is None: - name = roiClass.__name__ - text = 'Add %s' % name - self.setIcon(icons.getQIcon(iconName)) - self.setText(text) - self.setCheckable(True) - self.setToolTip(text) - - def getRoiClass(self): - """Return the ROI class used by this action to create ROIs""" - return self._roiClass - - def getRoiManager(self): - return self._roiManager() - - def setSingleShot(self, singleShot): - """Set it to True to deactivate the action after the first creation - of a ROI. - - :param bool singleShot: New single short state - """ - self._singleShot = singleShot - - def getSingleShot(self): - """If True, after the first creation of a ROI with this mode, - the mode is deactivated. - - :rtype: bool - """ - return self._singleShot - - def _actionTriggered(self, checked): - """Handle mode actions being checked by the user - - :param bool checked: - :param str kind: Corresponding shape kind - """ - roiManager = self.getRoiManager() - if roiManager is None: - return - - if checked: - roiManager.start(self._roiClass, self) - self.__interactiveModeStarted(roiManager) - else: - source = roiManager.getInteractionSource() - if source is self: - roiManager.stop() - - def __interactiveModeStarted(self, roiManager): - roiManager.sigInteractiveRoiCreated.connect(self.initRoi) - roiManager.sigInteractiveRoiFinalized.connect(self.__finalizeRoi) - roiManager.sigInteractiveModeFinished.connect(self.__interactiveModeFinished) - - def __interactiveModeFinished(self): - roiManager = self.getRoiManager() - if roiManager is not None: - roiManager.sigInteractiveRoiCreated.disconnect(self.initRoi) - roiManager.sigInteractiveRoiFinalized.disconnect(self.__finalizeRoi) - roiManager.sigInteractiveModeFinished.disconnect(self.__interactiveModeFinished) - self.setChecked(False) - - def initRoi(self, roi): - """Inherit it to custom the new ROI at it's creation during the - interaction.""" - pass - - def __finalizeRoi(self, roi): - self.finalizeRoi(roi) - if self._singleShot: - roiManager = self.getRoiManager() - if roiManager is not None: - roiManager.stop() - - def finalizeRoi(self, roi): - """Inherit it to custom the new ROI after it's creation when the - interaction is finalized.""" - pass - - -class RoiModeSelector(qt.QWidget): - def __init__(self, parent=None): - super(RoiModeSelector, self).__init__(parent=parent) - self.__roi = None - self.__reentrant = LockReentrant() - - layout = qt.QHBoxLayout(self) - if isinstance(parent, qt.QMenu): - margins = layout.contentsMargins() - layout.setContentsMargins(margins.left(), 0, margins.right(), 0) - else: - layout.setContentsMargins(0, 0, 0, 0) - - self._label = qt.QLabel(self) - self._label.setText("Mode:") - self._label.setToolTip("Select a specific interaction to edit the ROI") - self._combo = qt.QComboBox(self) - self._combo.currentIndexChanged.connect(self._modeSelected) - layout.addWidget(self._label) - layout.addWidget(self._combo) - self._updateAvailableModes() - - def getRoi(self): - """Returns the edited ROI. - - :rtype: roi_items.RegionOfInterest - """ - return self.__roi - - def setRoi(self, roi): - """Returns the edited ROI. - - :rtype: roi_items.RegionOfInterest - """ - if self.__roi is roi: - return - if not isinstance(roi, roi_items.InteractionModeMixIn): - self.__roi = None - self._updateAvailableModes() - return - - if self.__roi is not None: - self.__roi.sigInteractionModeChanged.disconnect(self._modeChanged) - self.__roi = roi - if self.__roi is not None: - self.__roi.sigInteractionModeChanged.connect(self._modeChanged) - self._updateAvailableModes() - - def isEmpty(self): - return not self._label.isVisibleTo(self) - - def _updateAvailableModes(self): - roi = self.getRoi() - if isinstance(roi, roi_items.InteractionModeMixIn): - modes = roi.availableInteractionModes() - else: - modes = [] - if len(modes) <= 1: - self._label.setVisible(False) - self._combo.setVisible(False) - else: - self._label.setVisible(True) - self._combo.setVisible(True) - with blockSignals(self._combo): - self._combo.clear() - for im, m in enumerate(modes): - self._combo.addItem(m.label, m) - self._combo.setItemData(im, m.description, qt.Qt.ToolTipRole) - mode = roi.getInteractionMode() - self._modeChanged(mode) - index = modes.index(mode) - self._combo.setCurrentIndex(index) - - def _modeChanged(self, mode): - """Triggered when the ROI interaction mode was changed externally""" - if self.__reentrant.locked(): - # This event was initialised by the widget - return - roi = self.__roi - modes = roi.availableInteractionModes() - index = modes.index(mode) - with blockSignals(self._combo): - self._combo.setCurrentIndex(index) - - def _modeSelected(self): - """Triggered when the ROI interaction mode was selected in the widget""" - index = self._combo.currentIndex() - if index == -1: - return - roi = self.getRoi() - if roi is not None: - mode = self._combo.itemData(index, qt.Qt.UserRole) - with self.__reentrant: - roi.setInteractionMode(mode) - - -class RoiModeSelectorAction(qt.QWidgetAction): - """Display the selected mode of a ROI and allow to change it""" - - def __init__(self, parent=None): - super(RoiModeSelectorAction, self).__init__(parent) - self.__roiManager = None - - def createWidget(self, parent): - """Inherit the method to create a new widget""" - widget = RoiModeSelector(parent) - manager = self.__roiManager - if manager is not None: - roi = manager.getCurrentRoi() - widget.setRoi(roi) - self.setVisible(not widget.isEmpty()) - return widget - - def deleteWidget(self, widget): - """Inherit the method to delete a widget""" - widget.setRoi(None) - return qt.QWidgetAction.deleteWidget(self, widget) - - def setRoiManager(self, roiManager): - """ - Connect this action to a ROI manager. - - :param RegionOfInterestManager roiManager: A ROI manager - """ - if self.__roiManager is roiManager: - return - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged) - self.__roiManager = roiManager - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged) - self.__currentRoiChanged(roiManager.getCurrentRoi()) - - def __currentRoiChanged(self, roi): - """Handle changes of the selected ROI""" - self.setRoi(roi) - - def setRoi(self, roi): - """Set a profile ROI to edit. - - :param ProfileRoiMixIn roi: A profile ROI - """ - widget = None - for widget in self.createdWidgets(): - widget.setRoi(roi) - if widget is not None: - self.setVisible(not widget.isEmpty()) - - -class RegionOfInterestManager(qt.QObject): - """Class handling ROI interaction on a PlotWidget. - - It supports the multiple ROIs: points, rectangles, polygons, - lines, horizontal and vertical lines. - - See ``plotInteractiveImageROI.py`` sample code (:ref:`sample-code`). - - :param silx.gui.plot.PlotWidget parent: - The plot widget in which to control the ROIs. - """ - - sigRoiAdded = qt.Signal(roi_items.RegionOfInterest) - """Signal emitted when a new ROI has been added. - - It provides the newly add :class:`RegionOfInterest` object. - """ - - sigRoiAboutToBeRemoved = qt.Signal(roi_items.RegionOfInterest) - """Signal emitted just before a ROI is removed. - - It provides the :class:`RegionOfInterest` object that is about to be removed. - """ - - sigRoiChanged = qt.Signal() - """Signal emitted whenever the ROIs have changed.""" - - sigCurrentRoiChanged = qt.Signal(object) - """Signal emitted whenever a ROI is selected.""" - - sigInteractiveModeStarted = qt.Signal(object) - """Signal emitted when switching to ROI drawing interactive mode. - - It provides the class of the ROI which will be created by the interactive - mode. - """ - - sigInteractiveRoiCreated = qt.Signal(object) - """Signal emitted when a ROI is created during the interaction. - The interaction is still incomplete and can be aborted. - - It provides the ROI object which was just been created. - """ - - sigInteractiveRoiFinalized = qt.Signal(object) - """Signal emitted when a ROI creation is complet. - - It provides the ROI object which was just been created. - """ - - sigInteractiveModeFinished = qt.Signal() - """Signal emitted when leaving interactive ROI drawing mode. - """ - - ROI_CLASSES = ( - roi_items.PointROI, - roi_items.CrossROI, - roi_items.RectangleROI, - roi_items.CircleROI, - roi_items.EllipseROI, - roi_items.PolygonROI, - roi_items.LineROI, - roi_items.HorizontalLineROI, - roi_items.VerticalLineROI, - roi_items.ArcROI, - roi_items.HorizontalRangeROI, - ) - - def __init__(self, parent): - assert isinstance(parent, PlotWidget) - super(RegionOfInterestManager, self).__init__(parent) - self._rois = [] # List of ROIs - self._drawnROI = None # New ROI being currently drawn - - self._roiClass = None - self._source = None - self._color = rgba('red') - - self._label = "__RegionOfInterestManager__%d" % id(self) - - self._currentRoi = None - """Hold currently selected ROI""" - - self._eventLoop = None - - self._modeActions = {} - - parent.sigPlotSignal.connect(self._plotSignals) - - parent.sigInteractiveModeChanged.connect( - self._plotInteractiveModeChanged) - - parent.sigItemRemoved.connect(self._itemRemoved) - - parent._sigDefaultContextMenu.connect(self._feedContextMenu) - - @classmethod - def getSupportedRoiClasses(cls): - """Returns the default available ROI classes - - :rtype: List[class] - """ - return tuple(cls.ROI_CLASSES) - - # Associated QActions - - def getInteractionModeAction(self, roiClass): - """Returns the QAction corresponding to a kind of ROI - - The QAction allows to enable the corresponding drawing - interactive mode. - - :param class roiClass: The ROI class which will be created by this action. - :rtype: QAction - :raise ValueError: If kind is not supported - """ - if not issubclass(roiClass, roi_items.RegionOfInterest): - raise ValueError('Unsupported ROI class %s' % roiClass) - - action = self._modeActions.get(roiClass, None) - if action is None: # Lazy-loading - action = CreateRoiModeAction(self, self, roiClass) - self._modeActions[roiClass] = action - return action - - # PlotWidget eventFilter and listeners - - def _plotInteractiveModeChanged(self, source): - """Handle change of interactive mode in the plot""" - if source is not self: - self.__roiInteractiveModeEnded() - - def _getRoiFromItem(self, item): - """Returns the ROI which own this item, else None - if this manager do not have knowledge of this ROI.""" - for roi in self._rois: - if isinstance(roi, roi_items.RegionOfInterest): - for child in roi.getItems(): - if child is item: - return roi - return None - - def _itemRemoved(self, item): - """Called after an item was removed from the plot.""" - if not hasattr(item, "_roiGroup"): - # Early break to avoid to use _getRoiFromItem - # And to avoid reentrant signal when the ROI remove the item itself - return - roi = self._getRoiFromItem(item) - if roi is not None: - self.removeRoi(roi) - - # Handle ROI interaction - - def _handleInteraction(self, event): - """Handle mouse interaction for ROI addition""" - roiClass = self.getCurrentInteractionModeRoiClass() - if roiClass is None: - return # Should not happen - - kind = roiClass.getFirstInteractionShape() - if kind == 'point': - if event['event'] == 'mouseClicked' and event['button'] == 'left': - points = numpy.array([(event['x'], event['y'])], - dtype=numpy.float64) - # Not an interactive creation - roi = self._createInteractiveRoi(roiClass, points=points) - roi.creationFinalized() - self.sigInteractiveRoiFinalized.emit(roi) - else: # other shapes - if (event['event'] in ('drawingProgress', 'drawingFinished') and - event['parameters']['label'] == self._label): - points = numpy.array((event['xdata'], event['ydata']), - dtype=numpy.float64).T - - if self._drawnROI is None: # Create new ROI - # NOTE: Set something before createRoi, so isDrawing is True - self._drawnROI = object() - self._drawnROI = self._createInteractiveRoi(roiClass, points=points) - else: - self._drawnROI.setFirstShapePoints(points) - - if event['event'] == 'drawingFinished': - if kind == 'polygon' and len(points) > 1: - self._drawnROI.setFirstShapePoints(points[:-1]) - roi = self._drawnROI - self._drawnROI = None # Stop drawing - roi.creationFinalized() - self.sigInteractiveRoiFinalized.emit(roi) - - # RegionOfInterest selection - - def __getRoiFromMarker(self, marker): - """Returns a ROI from a marker, else None""" - # This should be speed up - for roi in self._rois: - if isinstance(roi, roi_items.HandleBasedROI): - for m in roi.getHandles(): - if m is marker: - return roi - else: - for m in roi.getItems(): - if m is marker: - return roi - return None - - def setCurrentRoi(self, roi): - """Set the currently selected ROI, and emit a signal. - - :param Union[RegionOfInterest,None] roi: The ROI to select - """ - if self._currentRoi is roi: - return - if roi is not None: - # Note: Fixed range to avoid infinite loops - for _ in range(10): - target = roi.getFocusProxy() - if target is None: - break - roi = target - else: - raise RuntimeError("Max selection proxy depth (10) reached.") - - if self._currentRoi is not None: - self._currentRoi.setHighlighted(False) - self._currentRoi = roi - if self._currentRoi is not None: - self._currentRoi.setHighlighted(True) - self.sigCurrentRoiChanged.emit(roi) - - def getCurrentRoi(self): - """Returns the currently selected ROI, else None. - - :rtype: Union[RegionOfInterest,None] - """ - return self._currentRoi - - def _plotSignals(self, event): - """Handle mouse interaction for ROI addition""" - clicked = False - roi = None - if event["event"] in ("markerClicked", "markerMoving"): - plot = self.parent() - legend = event["label"] - marker = plot._getMarker(legend=legend) - roi = self.__getRoiFromMarker(marker) - elif event["event"] == "mouseClicked" and event["button"] == "left": - # Marker click is only for dnd - # This also can click on a marker - clicked = True - plot = self.parent() - marker = plot._getMarkerAt(event["xpixel"], event["ypixel"]) - roi = self.__getRoiFromMarker(marker) - else: - return - - if roi not in self._rois: - # The ROI is not own by this manager - return - - if roi is not None: - currentRoi = self.getCurrentRoi() - if currentRoi is roi: - if clicked: - self.__updateMode(roi) - elif roi.isSelectable(): - self.setCurrentRoi(roi) - else: - self.setCurrentRoi(None) - - def __updateMode(self, roi): - if isinstance(roi, roi_items.InteractionModeMixIn): - available = roi.availableInteractionModes() - mode = roi.getInteractionMode() - imode = available.index(mode) - mode = available[(imode + 1) % len(available)] - roi.setInteractionMode(mode) - - def _feedContextMenu(self, menu): - """Called wen the default plot context menu is about to be displayed""" - roi = self.getCurrentRoi() - if roi is not None: - if roi.isEditable(): - # Filter by data position - # FIXME: It would be better to use GUI coords for it - plot = self.parent() - pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos()) - data = plot.pixelToData(pos.x(), pos.y()) - if roi.contains(data): - if isinstance(roi, roi_items.InteractionModeMixIn): - self._contextMenuForInteractionMode(menu, roi) - - removeAction = qt.QAction(menu) - removeAction.setText("Remove %s" % roi.getName()) - callback = functools.partial(self.removeRoi, roi) - removeAction.triggered.connect(callback) - menu.addAction(removeAction) - - def _contextMenuForInteractionMode(self, menu, roi): - availableModes = roi.availableInteractionModes() - currentMode = roi.getInteractionMode() - submenu = qt.QMenu(menu) - modeGroup = qt.QActionGroup(menu) - modeGroup.setExclusive(True) - for mode in availableModes: - action = qt.QAction(menu) - action.setText(mode.label) - action.setToolTip(mode.description) - action.setCheckable(True) - if mode is currentMode: - action.setChecked(True) - else: - callback = functools.partial(roi.setInteractionMode, mode) - action.triggered.connect(callback) - modeGroup.addAction(action) - submenu.addAction(action) - action = qt.QAction(menu) - action.setMenu(submenu) - action.setText("%s interaction mode" % roi.getName()) - menu.addAction(action) - - # RegionOfInterest API - - def getRois(self): - """Returns the list of ROIs. - - It returns an empty tuple if there is currently no ROI. - - :return: Tuple of arrays of objects describing the ROIs - :rtype: List[RegionOfInterest] - """ - return tuple(self._rois) - - def clear(self): - """Reset current ROIs - - :return: True if ROIs were reset. - :rtype: bool - """ - if self.getRois(): # Something to reset - for roi in self._rois: - roi.sigRegionChanged.disconnect( - self._regionOfInterestChanged) - roi.setParent(None) - self._rois = [] - self._roisUpdated() - return True - - else: - return False - - def _regionOfInterestChanged(self, event=None): - """Handle ROI object changed""" - self.sigRoiChanged.emit() - - def _createInteractiveRoi(self, roiClass, points, label=None, index=None): - """Create a new ROI with interactive creation. - - :param class roiClass: The class of the ROI to create - :param numpy.ndarray points: The first shape used to create the ROI - :param str label: The label to display along with the ROI. - :param int index: The position where to insert the ROI. - By default it is appended to the end of the list. - :return: The created ROI object - :rtype: roi_items.RegionOfInterest - :raise RuntimeError: When ROI cannot be added because the maximum - number of ROIs has been reached. - """ - roi = roiClass(parent=None) - if label is not None: - roi.setName(str(label)) - roi.creationStarted() - roi.setFirstShapePoints(points) - - self.addRoi(roi, index) - if roi.isSelectable(): - self.setCurrentRoi(roi) - self.sigInteractiveRoiCreated.emit(roi) - return roi - - def containsRoi(self, roi): - """Returns true if the ROI is part of this manager. - - :param roi_items.RegionOfInterest roi: The ROI to add - :rtype: bool - """ - return roi in self._rois - - def addRoi(self, roi, index=None, useManagerColor=True): - """Add the ROI to the list of ROIs. - - :param roi_items.RegionOfInterest roi: The ROI to add - :param int index: The position where to insert the ROI, - By default it is appended to the end of the list of ROIs - :param bool useManagerColor: - Whether to set the ROI color to the default one of the manager or not. - (Default: True). - :raise RuntimeError: When ROI cannot be added because the maximum - number of ROIs has been reached. - """ - plot = self.parent() - if plot is None: - raise RuntimeError( - 'Cannot add ROI: PlotWidget no more available') - - roi.setParent(self) - - if useManagerColor: - roi.setColor(self.getColor()) - - roi.sigRegionChanged.connect(self._regionOfInterestChanged) - roi.sigItemChanged.connect(self._regionOfInterestChanged) - - if index is None: - self._rois.append(roi) - else: - self._rois.insert(index, roi) - self.sigRoiAdded.emit(roi) - self._roisUpdated() - - def removeRoi(self, roi): - """Remove a ROI from the list of ROIs. - - :param roi_items.RegionOfInterest roi: The ROI to remove - :raise ValueError: When ROI does not belong to this object - """ - if not (isinstance(roi, roi_items.RegionOfInterest) and - roi.parent() is self and - roi in self._rois): - raise ValueError( - 'RegionOfInterest does not belong to this instance') - - roi.sigAboutToBeRemoved.emit() - self.sigRoiAboutToBeRemoved.emit(roi) - - if roi is self._currentRoi: - self.setCurrentRoi(None) - - mustRestart = False - if roi is self._drawnROI: - self._drawnROI = None - mustRestart = True - self._rois.remove(roi) - roi.sigRegionChanged.disconnect(self._regionOfInterestChanged) - roi.sigItemChanged.disconnect(self._regionOfInterestChanged) - roi.setParent(None) - self._roisUpdated() - - if mustRestart: - self._restart() - - def _roisUpdated(self): - """Handle update of the ROI list""" - self.sigRoiChanged.emit() - - # RegionOfInterest parameters - - def getColor(self): - """Return the default color of created ROIs - - :rtype: QColor - """ - return qt.QColor.fromRgbF(*self._color) - - def setColor(self, color): - """Set the default color to use when creating ROIs. - - Existing ROIs are not affected. - - :param color: The color to use for displaying ROIs as - either a color name, a QColor, a list of uint8 or float in [0, 1]. - """ - self._color = rgba(color) - - # Control ROI - - def getCurrentInteractionModeRoiClass(self): - """Returns the current ROI class used by the interactive drawing mode. - - Returns None if the ROI manager is not in an interactive mode. - - :rtype: Union[class,None] - """ - return self._roiClass - - def getInteractionSource(self): - """Returns the object which have requested the ROI creation. - - Returns None if the ROI manager is not in an interactive mode. - - :rtype: Union[object,None] - """ - return self._source - - def isStarted(self): - """Returns True if an interactive ROI drawing mode is active. - - :rtype: bool - """ - return self._roiClass is not None - - def isDrawing(self): - """Returns True if an interactive ROI is drawing. - - :rtype: bool - """ - return self._drawnROI is not None - - def start(self, roiClass, source=None): - """Start an interactive ROI drawing mode. - - :param class roiClass: The ROI class to create. It have to inherite from - `roi_items.RegionOfInterest`. - :param object source: SOurce of the ROI interaction. - :return: True if interactive ROI drawing was started, False otherwise - :rtype: bool - :raise ValueError: If roiClass is not supported - """ - self.stop() - - if not issubclass(roiClass, roi_items.RegionOfInterest): - raise ValueError('Unsupported ROI class %s' % roiClass) - - plot = self.parent() - if plot is None: - return False - - self._roiClass = roiClass - self._source = source - - self._restart() - - plot.sigPlotSignal.connect(self._handleInteraction) - - self.sigInteractiveModeStarted.emit(roiClass) - - return True - - def _restart(self): - """Restart the plot interaction without changing the - source or the ROI class. - """ - roiClass = self._roiClass - plot = self.parent() - firstInteractionShapeKind = roiClass.getFirstInteractionShape() - - if firstInteractionShapeKind == 'point': - plot.setInteractiveMode(mode='select', source=self) - else: - if roiClass.showFirstInteractionShape(): - color = rgba(self.getColor()) - else: - color = None - plot.setInteractiveMode(mode='select-draw', - source=self, - shape=firstInteractionShapeKind, - color=color, - label=self._label) - - def __roiInteractiveModeEnded(self): - """Handle end of ROI draw interactive mode""" - if self.isStarted(): - self._roiClass = None - self._source = None - - if self._drawnROI is not None: - # Cancel ROI create - roi = self._drawnROI - self._drawnROI = None - self.removeRoi(roi) - - plot = self.parent() - if plot is not None: - plot.sigPlotSignal.disconnect(self._handleInteraction) - - self.sigInteractiveModeFinished.emit() - - def stop(self): - """Stop interactive ROI drawing mode. - - :return: True if an interactive ROI drawing mode was actually stopped - :rtype: bool - """ - if not self.isStarted(): - return False - - plot = self.parent() - if plot is not None: - # This leads to call __roiInteractiveModeEnded through - # interactive mode changed signal - plot.resetInteractiveMode() - else: # Fallback - self.__roiInteractiveModeEnded() - - return True - - def exec_(self, roiClass): - """Block until :meth:`quit` is called. - - :param class kind: The class of the ROI which have to be created. - See `silx.gui.plot.items.roi`. - :return: The list of ROIs - :rtype: tuple - """ - self.start(roiClass) - - plot = self.parent() - plot.show() - plot.raise_() - - self._eventLoop = qt.QEventLoop() - self._eventLoop.exec_() - self._eventLoop = None - - self.stop() - - rois = self.getRois() - self.clear() - return rois - - def quit(self): - """Stop a blocking :meth:`exec_` and call :meth:`stop`""" - if self._eventLoop is not None: - self._eventLoop.quit() - self._eventLoop = None - self.stop() - - -class InteractiveRegionOfInterestManager(RegionOfInterestManager): - """RegionOfInterestManager with features for use from interpreter. - - It is meant to be used through the :meth:`exec_`. - It provides some messages to display in a status bar and - different modes to end blocking calls to :meth:`exec_`. - - :param parent: See QObject - """ - - sigMessageChanged = qt.Signal(str) - """Signal emitted when a new message should be displayed to the user - - It provides the message as a str. - """ - - def __init__(self, parent): - super(InteractiveRegionOfInterestManager, self).__init__(parent) - self._maxROI = None - self.__timeoutEndTime = None - self.__message = '' - self.__validationMode = self.ValidationMode.ENTER - self.__execClass = None - - self.sigRoiAdded.connect(self.__added) - self.sigRoiAboutToBeRemoved.connect(self.__aboutToBeRemoved) - self.sigInteractiveModeStarted.connect(self.__started) - self.sigInteractiveModeFinished.connect(self.__finished) - - # Max ROI - - def getMaxRois(self): - """Returns the maximum number of ROIs or None if no limit. - - :rtype: Union[int,None] - """ - return self._maxROI - - def setMaxRois(self, max_): - """Set the maximum number of ROIs. - - :param Union[int,None] max_: The max limit or None for no limit. - :raise ValueError: If there is more ROIs than max value - """ - if max_ is not None: - max_ = int(max_) - if max_ <= 0: - raise ValueError('Max limit must be strictly positive') - - if len(self.getRois()) > max_: - raise ValueError( - 'Cannot set max limit: Already too many ROIs') - - self._maxROI = max_ - - def isMaxRois(self): - """Returns True if the maximum number of ROIs is reached. - - :rtype: bool - """ - max_ = self.getMaxRois() - return max_ is not None and len(self.getRois()) >= max_ - - # Validation mode - - @enum.unique - class ValidationMode(enum.Enum): - """Mode of validation to leave blocking :meth:`exec_`""" - - AUTO = 'auto' - """Automatically ends the interactive mode once - the user terminates the last ROI shape.""" - - ENTER = 'enter' - """Ends the interactive mode when the *Enter* key is pressed.""" - - AUTO_ENTER = 'auto_enter' - """Ends the interactive mode when reaching max ROIs or - when the *Enter* key is pressed. - """ - - NONE = 'none' - """Do not provide the user a way to end the interactive mode. - - The end of :meth:`exec_` is done through :meth:`quit` or timeout. - """ - - def getValidationMode(self): - """Returns the interactive mode validation in use. - - :rtype: ValidationMode - """ - return self.__validationMode - - def setValidationMode(self, mode): - """Set the way to perform interactive mode validation. - - See :class:`ValidationMode` enumeration for the supported - validation modes. - - :param ValidationMode mode: The interactive mode validation to use. - """ - assert isinstance(mode, self.ValidationMode) - if mode != self.__validationMode: - self.__validationMode = mode - - if self.isExec(): - if (self.isMaxRois() and self.getValidationMode() in - (self.ValidationMode.AUTO, - self.ValidationMode.AUTO_ENTER)): - self.quit() - - self.__updateMessage() - - def eventFilter(self, obj, event): - if event.type() == qt.QEvent.Hide: - self.quit() - - if event.type() == qt.QEvent.KeyPress: - key = event.key() - if (key in (qt.Qt.Key_Return, qt.Qt.Key_Enter) and - self.getValidationMode() in ( - self.ValidationMode.ENTER, - self.ValidationMode.AUTO_ENTER)): - # Stop on return key pressed - self.quit() - return True # Stop further handling of this keys - - if (key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or ( - key == qt.Qt.Key_Z and - event.modifiers() & qt.Qt.ControlModifier)): - rois = self.getRois() - if rois: # Something to undo - self.removeRoi(rois[-1]) - # Stop further handling of keys if something was undone - return True - - return super(InteractiveRegionOfInterestManager, self).eventFilter(obj, event) - - # Message API - - def getMessage(self): - """Returns the current status message. - - This message is meant to be displayed in a status bar. - - :rtype: str - """ - if self.__timeoutEndTime is None: - return self.__message - else: - remaining = self.__timeoutEndTime - time.time() - return self.__message + (' - %d seconds remaining' % - max(1, int(remaining))) - - # Listen to ROI updates - - def __added(self, *args, **kwargs): - """Handle new ROI added""" - max_ = self.getMaxRois() - if max_ is not None: - # When reaching max number of ROIs, redo last one - while len(self.getRois()) > max_: - self.removeRoi(self.getRois()[-2]) - - self.__updateMessage() - if (self.isMaxRois() and - self.getValidationMode() in (self.ValidationMode.AUTO, - self.ValidationMode.AUTO_ENTER)): - self.quit() - - def __aboutToBeRemoved(self, *args, **kwargs): - """Handle removal of a ROI""" - # RegionOfInterest not removed yet - self.__updateMessage(nbrois=len(self.getRois()) - 1) - - def __started(self, roiKind): - """Handle interactive mode started""" - self.__updateMessage() - - def __finished(self): - """Handle interactive mode finished""" - self.__updateMessage() - - def __updateMessage(self, nbrois=None): - """Update message""" - if not self.isExec(): - message = 'Done' - - elif not self.isStarted(): - message = 'Use %s ROI edition mode' % self.__execClass - - else: - if nbrois is None: - nbrois = len(self.getRois()) - - name = self.__execClass._getShortName() - - max_ = self.getMaxRois() - if max_ is None: - message = 'Select %ss (%d selected)' % (name, nbrois) - - elif max_ <= 1: - message = 'Select a %s' % name - else: - message = 'Select %d/%d %ss' % (nbrois, max_, name) - - if (self.getValidationMode() == self.ValidationMode.ENTER and - self.isMaxRois()): - message += ' - Press Enter to confirm' - - if message != self.__message: - self.__message = message - # Use getMessage to add timeout message - self.sigMessageChanged.emit(self.getMessage()) - - # Handle blocking call - - def __timeoutUpdate(self): - """Handle update of timeout""" - if (self.__timeoutEndTime is not None and - (self.__timeoutEndTime - time.time()) > 0): - self.sigMessageChanged.emit(self.getMessage()) - else: # Stop interactive mode and message timer - timer = self.sender() - if timer is not None: - timer.stop() - self.__timeoutEndTime = None - self.quit() - - def isExec(self): - """Returns True if :meth:`exec_` is currently running. - - :rtype: bool""" - return self.__execClass is not None - - def exec_(self, roiClass, timeout=0): - """Block until ROI selection is done or timeout is elapsed. - - :meth:`quit` also ends this blocking call. - - :param class roiClass: The class of the ROI which have to be created. - See `silx.gui.plot.items.roi`. - :param int timeout: Maximum duration in seconds to block. - Default: No timeout - :return: The list of ROIs - :rtype: List[RegionOfInterest] - """ - plot = self.parent() - if plot is None: - return - - self.__execClass = roiClass - - plot.installEventFilter(self) - - if timeout > 0: - self.__timeoutEndTime = time.time() + timeout - timer = qt.QTimer(self) - timer.timeout.connect(self.__timeoutUpdate) - timer.start(1000) - - rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass) - - timer.stop() - self.__timeoutEndTime = None - - else: - rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass) - - plot.removeEventFilter(self) - - self.__execClass = None - self.__updateMessage() - - return rois - - -class _DeleteRegionOfInterestToolButton(qt.QToolButton): - """Tool button deleting a ROI object - - :param parent: See QWidget - :param RegionOfInterest roi: The ROI to delete - """ - - def __init__(self, parent, roi): - super(_DeleteRegionOfInterestToolButton, self).__init__(parent) - self.setIcon(icons.getQIcon('remove')) - self.setToolTip("Remove this ROI") - self.__roiRef = roi if roi is None else weakref.ref(roi) - self.clicked.connect(self.__clicked) - - def __clicked(self, checked): - """Handle button clicked""" - roi = None if self.__roiRef is None else self.__roiRef() - if roi is not None: - manager = roi.parent() - if manager is not None: - manager.removeRoi(roi) - self.__roiRef = None - - -class RegionOfInterestTableWidget(qt.QTableWidget): - """Widget displaying the ROIs of a :class:`RegionOfInterestManager`""" - - def __init__(self, parent=None): - super(RegionOfInterestTableWidget, self).__init__(parent) - self._roiManagerRef = None - - headers = ['Label', 'Edit', 'Kind', 'Coordinates', ''] - self.setColumnCount(len(headers)) - self.setHorizontalHeaderLabels(headers) - - horizontalHeader = self.horizontalHeader() - horizontalHeader.setDefaultAlignment(qt.Qt.AlignLeft) - if hasattr(horizontalHeader, 'setResizeMode'): # Qt 4 - setSectionResizeMode = horizontalHeader.setResizeMode - else: # Qt5 - setSectionResizeMode = horizontalHeader.setSectionResizeMode - - setSectionResizeMode(0, qt.QHeaderView.Interactive) - setSectionResizeMode(1, qt.QHeaderView.ResizeToContents) - setSectionResizeMode(2, qt.QHeaderView.ResizeToContents) - setSectionResizeMode(3, qt.QHeaderView.Stretch) - setSectionResizeMode(4, qt.QHeaderView.ResizeToContents) - - verticalHeader = self.verticalHeader() - verticalHeader.setVisible(False) - - self.setSelectionMode(qt.QAbstractItemView.NoSelection) - self.setFocusPolicy(qt.Qt.NoFocus) - - self.itemChanged.connect(self.__itemChanged) - - def __itemChanged(self, item): - """Handle item updates""" - column = item.column() - index = item.data(qt.Qt.UserRole) - - if index is not None: - manager = self.getRegionOfInterestManager() - roi = manager.getRois()[index] - else: - return - - if column == 0: - # First collect information from item, then update ROI - # Otherwise, this causes issues issues - checked = item.checkState() == qt.Qt.Checked - text= item.text() - roi.setVisible(checked) - roi.setName(text) - elif column == 1: - roi.setEditable(item.checkState() == qt.Qt.Checked) - elif column in (2, 3, 4): - pass # TODO - else: - logger.error('Unhandled column %d', column) - - def setRegionOfInterestManager(self, manager): - """Set the :class:`RegionOfInterestManager` object to sync with - - :param RegionOfInterestManager manager: - """ - assert manager is None or isinstance(manager, RegionOfInterestManager) - - previousManager = self.getRegionOfInterestManager() - - if previousManager is not None: - previousManager.sigRoiChanged.disconnect(self._sync) - self.setRowCount(0) - - self._roiManagerRef = weakref.ref(manager) - - self._sync() - - if manager is not None: - manager.sigRoiChanged.connect(self._sync) - - def _getReadableRoiDescription(self, roi): - """Returns modelisation of a ROI as a readable sequence of values. - - :rtype: str - """ - text = str(roi) - try: - # Extract the params from syntax "CLASSNAME(PARAMS)" - elements = text.split("(", 1) - if len(elements) != 2: - return text - result = elements[1] - result = result.strip() - if not result.endswith(")"): - return text - result = result[0:-1] - # Capitalize each words - result = result.title() - return result - except Exception: - logger.debug("Backtrace", exc_info=True) - return text - - def _sync(self): - """Update widget content according to ROI manger""" - manager = self.getRegionOfInterestManager() - - if manager is None: - self.setRowCount(0) - return - - rois = manager.getRois() - - self.setRowCount(len(rois)) - for index, roi in enumerate(rois): - baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled - - # Label and visible - label = roi.getName() - item = qt.QTableWidgetItem(label) - item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable) - item.setData(qt.Qt.UserRole, index) - item.setCheckState( - qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked) - self.setItem(index, 0, item) - - # Editable - item = qt.QTableWidgetItem() - item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable) - item.setData(qt.Qt.UserRole, index) - item.setCheckState( - qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked) - self.setItem(index, 1, item) - item.setTextAlignment(qt.Qt.AlignCenter) - item.setText(None) - - # Kind - label = roi._getShortName() - if label is None: - # Default value if kind is not overrided - label = roi.__class__.__name__ - item = qt.QTableWidgetItem(label.capitalize()) - item.setFlags(baseFlags) - self.setItem(index, 2, item) - - item = qt.QTableWidgetItem() - item.setFlags(baseFlags) - - # Coordinates - text = self._getReadableRoiDescription(roi) - item.setText(text) - self.setItem(index, 3, item) - - # Delete - delBtn = _DeleteRegionOfInterestToolButton(None, roi) - widget = qt.QWidget(self) - layout = qt.QHBoxLayout() - layout.setContentsMargins(2, 2, 2, 2) - layout.setSpacing(0) - widget.setLayout(layout) - layout.addStretch(1) - layout.addWidget(delBtn) - layout.addStretch(1) - self.setCellWidget(index, 4, widget) - - def getRegionOfInterestManager(self): - """Returns the :class:`RegionOfInterestManager` this widget supervise. - - It returns None if not sync with an :class:`RegionOfInterestManager`. - - :rtype: RegionOfInterestManager - """ - return None if self._roiManagerRef is None else self._roiManagerRef() diff --git a/silx/gui/plot/tools/test/__init__.py b/silx/gui/plot/tools/test/__init__.py deleted file mode 100644 index 1429545..0000000 --- a/silx/gui/plot/tools/test/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "26/03/2018" - - -import unittest - -from . import testROI -from . import testTools -from . import testScatterProfileToolBar -from . import testCurveLegendsWidget -from . import testProfile - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTests( - [testROI.suite(), - testTools.suite(), - testScatterProfileToolBar.suite(), - testCurveLegendsWidget.suite(), - testProfile.suite(), - ]) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/silx/gui/plot/tools/test/testCurveLegendsWidget.py deleted file mode 100644 index 4824dd7..0000000 --- a/silx/gui/plot/tools/test/testCurveLegendsWidget.py +++ /dev/null @@ -1,125 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "02/08/2018" - - -import unittest - -from silx.gui import qt -from silx.utils.testutils import ParametricTestCase -from silx.gui.utils.testutils import TestCaseQt -from silx.gui.plot import PlotWindow -from silx.gui.plot.tools import CurveLegendsWidget - - -class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase): - """Tests for CurveLegendsWidget class""" - - def setUp(self): - super(TestCurveLegendsWidget, self).setUp() - self.plot = PlotWindow() - - self.legends = CurveLegendsWidget.CurveLegendsWidget() - self.legends.setPlotWidget(self.plot) - - dock = qt.QDockWidget() - dock.setWindowTitle('Curve Legends') - dock.setWidget(self.legends) - self.plot.addTabbedDockWidget(dock) - - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - def tearDown(self): - del self.legends - self.qapp.processEvents() - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - del self.plot - super(TestCurveLegendsWidget, self).tearDown() - - def _assertNbLegends(self, count): - """Check the number of legends in the CurveLegendsWidget""" - children = self.legends.findChildren(CurveLegendsWidget._LegendWidget) - self.assertEqual(len(children), count) - - def testAddRemoveCurves(self): - """Test CurveLegendsWidget while adding/removing curves""" - self.plot.addCurve((0, 1), (1, 2), legend='a') - self._assertNbLegends(1) - self.plot.addCurve((0, 1), (2, 3), legend='b') - self._assertNbLegends(2) - - # Detached/attach - self.legends.setPlotWidget(None) - self._assertNbLegends(0) - - self.legends.setPlotWidget(self.plot) - self._assertNbLegends(2) - - self.plot.clear() - self._assertNbLegends(0) - - def testUpdateCurves(self): - """Test CurveLegendsWidget while updating curves """ - self.plot.addCurve((0, 1), (1, 2), legend='a') - self._assertNbLegends(1) - self.plot.addCurve((0, 1), (2, 3), legend='b') - self._assertNbLegends(2) - - # Activate curve - self.plot.setActiveCurve('a') - self.qapp.processEvents() - self.plot.setActiveCurve('b') - self.qapp.processEvents() - - # Change curve style - curve = self.plot.getCurve('a') - curve.setLineWidth(2) - for linestyle in (':', '', '--', '-'): - with self.subTest(linestyle=linestyle): - curve.setLineStyle(linestyle) - self.qapp.processEvents() - self.qWait(1000) - - for symbol in ('o', 'd', '', 's'): - with self.subTest(symbol=symbol): - curve.setSymbol(symbol) - self.qapp.processEvents() - self.qWait(1000) - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase( - TestCurveLegendsWidget)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testProfile.py b/silx/gui/plot/tools/test/testProfile.py deleted file mode 100644 index 444cfe0..0000000 --- a/silx/gui/plot/tools/test/testProfile.py +++ /dev/null @@ -1,673 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import unittest -import contextlib -import numpy -import logging - -from silx.gui import qt -from silx.utils import deprecation -from silx.utils import testutils - -from silx.gui.utils.testutils import TestCaseQt -from silx.utils.testutils import ParametricTestCase -from silx.gui.plot import PlotWindow, Plot1D, Plot2D, Profile -from silx.gui.plot.StackView import StackView -from silx.gui.plot.tools.profile import rois -from silx.gui.plot.tools.profile import editors -from silx.gui.plot.items import roi as roi_items -from silx.gui.plot.tools.profile import manager -from silx.gui import plot as silx_plot - -_logger = logging.getLogger(__name__) - - -class TestRois(TestCaseQt): - - def test_init(self): - """Check that the constructor is not called twice""" - roi = rois.ProfileImageVerticalLineROI() - if qt.BINDING not in ["PySide", "PySide2"]: - # the profile ROI + the shape - self.assertEqual(roi.receivers(roi.sigRegionChanged), 2) - - -class TestInteractions(TestCaseQt): - - @contextlib.contextmanager - def defaultPlot(self): - try: - widget = silx_plot.PlotWidget() - widget.show() - self.qWaitForWindowExposed(widget) - yield widget - finally: - widget.close() - widget = None - self.qWait() - - @contextlib.contextmanager - def imagePlot(self): - try: - widget = silx_plot.Plot2D() - image = numpy.arange(10 * 10).reshape(10, -1) - widget.addImage(image) - widget.show() - self.qWaitForWindowExposed(widget) - yield widget - finally: - widget.close() - widget = None - self.qWait() - - @contextlib.contextmanager - def scatterPlot(self): - try: - widget = silx_plot.ScatterView() - - nbX, nbY = 7, 5 - yy = numpy.atleast_2d(numpy.ones(nbY)).T - xx = numpy.atleast_2d(numpy.ones(nbX)) - positionX = numpy.linspace(10, 50, nbX) * yy - positionX = positionX.reshape(nbX * nbY) - positionY = numpy.atleast_2d(numpy.linspace(20, 60, nbY)).T * xx - positionY = positionY.reshape(nbX * nbY) - values = numpy.arange(nbX * nbY) - - widget.setData(positionX, positionY, values) - widget.resetZoom() - widget.show() - self.qWaitForWindowExposed(widget) - yield widget.getPlotWidget() - finally: - widget.close() - widget = None - self.qWait() - - @contextlib.contextmanager - def stackPlot(self): - try: - widget = silx_plot.StackView() - image = numpy.arange(10 * 10).reshape(10, -1) - cube = numpy.array([image, image, image]) - widget.setStack(cube) - widget.resetZoom() - widget.show() - self.qWaitForWindowExposed(widget) - yield widget.getPlotWidget() - finally: - widget.close() - widget = None - self.qWait() - - def waitPendingOperations(self, proflie): - for _ in range(10): - if not proflie.hasPendingOperations(): - return - self.qWait(100) - _logger.error("The profile manager still have pending operations") - - def genericRoiTest(self, plot, roiClass): - profileManager = manager.ProfileManager(plot, plot) - profileManager.setItemType(image=True, scatter=True) - - try: - action = profileManager.createProfileAction(roiClass, plot) - action.triggered[bool].emit(True) - widget = plot.getWidgetHandle() - - # Do the mouse interaction - pos1 = widget.width() * 0.4, widget.height() * 0.4 - self.mouseMove(widget, pos=pos1) - self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1) - - if issubclass(roiClass, roi_items.LineROI): - pos2 = widget.width() * 0.6, widget.height() * 0.6 - self.mouseMove(widget, pos=pos2) - self.mouseClick(widget, qt.Qt.LeftButton, pos=pos2) - - self.waitPendingOperations(profileManager) - - # Test that something was computed - if issubclass(roiClass, rois._ProfileCrossROI): - self.assertEqual(profileManager._computedProfiles, 2) - elif issubclass(roiClass, roi_items.LineROI): - self.assertGreaterEqual(profileManager._computedProfiles, 1) - else: - self.assertEqual(profileManager._computedProfiles, 1) - - # Test the created ROIs - profileRois = profileManager.getRoiManager().getRois() - if issubclass(roiClass, rois._ProfileCrossROI): - self.assertEqual(len(profileRois), 3) - else: - self.assertEqual(len(profileRois), 1) - # The first one should be the expected one - roi = profileRois[0] - - # Test that something was displayed - if issubclass(roiClass, rois._ProfileCrossROI): - profiles = roi._getLines() - window = profiles[0].getProfileWindow() - self.assertIsNotNone(window) - window = profiles[1].getProfileWindow() - self.assertIsNotNone(window) - else: - window = roi.getProfileWindow() - self.assertIsNotNone(window) - finally: - profileManager.clearProfile() - - def testImageActions(self): - roiClasses = [ - rois.ProfileImageHorizontalLineROI, - rois.ProfileImageVerticalLineROI, - rois.ProfileImageLineROI, - rois.ProfileImageCrossROI, - ] - with self.imagePlot() as plot: - for roiClass in roiClasses: - with self.subTest(roiClass=roiClass): - self.genericRoiTest(plot, roiClass) - - def testScatterActions(self): - roiClasses = [ - rois.ProfileScatterHorizontalLineROI, - rois.ProfileScatterVerticalLineROI, - rois.ProfileScatterLineROI, - rois.ProfileScatterCrossROI, - rois.ProfileScatterHorizontalSliceROI, - rois.ProfileScatterVerticalSliceROI, - rois.ProfileScatterCrossSliceROI, - ] - with self.scatterPlot() as plot: - for roiClass in roiClasses: - with self.subTest(roiClass=roiClass): - self.genericRoiTest(plot, roiClass) - - def testStackActions(self): - roiClasses = [ - rois.ProfileImageStackHorizontalLineROI, - rois.ProfileImageStackVerticalLineROI, - rois.ProfileImageStackLineROI, - rois.ProfileImageStackCrossROI, - ] - with self.stackPlot() as plot: - for roiClass in roiClasses: - with self.subTest(roiClass=roiClass): - self.genericRoiTest(plot, roiClass) - - def genericEditorTest(self, plot, roi, editor): - if isinstance(editor, editors._NoProfileRoiEditor): - pass - elif isinstance(editor, editors._DefaultImageStackProfileRoiEditor): - # GUI to ROI - editor._lineWidth.setValue(2) - self.assertEqual(roi.getProfileLineWidth(), 2) - editor._methodsButton.setMethod("sum") - self.assertEqual(roi.getProfileMethod(), "sum") - editor._profileDim.setDimension(1) - self.assertEqual(roi.getProfileType(), "1D") - # ROI to GUI - roi.setProfileLineWidth(3) - self.assertEqual(editor._lineWidth.value(), 3) - roi.setProfileMethod("mean") - self.assertEqual(editor._methodsButton.getMethod(), "mean") - roi.setProfileType("2D") - self.assertEqual(editor._profileDim.getDimension(), 2) - elif isinstance(editor, editors._DefaultImageProfileRoiEditor): - # GUI to ROI - editor._lineWidth.setValue(2) - self.assertEqual(roi.getProfileLineWidth(), 2) - editor._methodsButton.setMethod("sum") - self.assertEqual(roi.getProfileMethod(), "sum") - # ROI to GUI - roi.setProfileLineWidth(3) - self.assertEqual(editor._lineWidth.value(), 3) - roi.setProfileMethod("mean") - self.assertEqual(editor._methodsButton.getMethod(), "mean") - elif isinstance(editor, editors._DefaultScatterProfileRoiEditor): - # GUI to ROI - editor._nPoints.setValue(100) - self.assertEqual(roi.getNPoints(), 100) - # ROI to GUI - roi.setNPoints(200) - self.assertEqual(editor._nPoints.value(), 200) - else: - assert False - - def testEditors(self): - roiClasses = [ - (rois.ProfileImageHorizontalLineROI, editors._DefaultImageProfileRoiEditor), - (rois.ProfileImageVerticalLineROI, editors._DefaultImageProfileRoiEditor), - (rois.ProfileImageLineROI, editors._DefaultImageProfileRoiEditor), - (rois.ProfileImageCrossROI, editors._DefaultImageProfileRoiEditor), - (rois.ProfileScatterHorizontalLineROI, editors._DefaultScatterProfileRoiEditor), - (rois.ProfileScatterVerticalLineROI, editors._DefaultScatterProfileRoiEditor), - (rois.ProfileScatterLineROI, editors._DefaultScatterProfileRoiEditor), - (rois.ProfileScatterCrossROI, editors._DefaultScatterProfileRoiEditor), - (rois.ProfileScatterHorizontalSliceROI, editors._NoProfileRoiEditor), - (rois.ProfileScatterVerticalSliceROI, editors._NoProfileRoiEditor), - (rois.ProfileScatterCrossSliceROI, editors._NoProfileRoiEditor), - (rois.ProfileImageStackHorizontalLineROI, editors._DefaultImageStackProfileRoiEditor), - (rois.ProfileImageStackVerticalLineROI, editors._DefaultImageStackProfileRoiEditor), - (rois.ProfileImageStackLineROI, editors._DefaultImageStackProfileRoiEditor), - (rois.ProfileImageStackCrossROI, editors._DefaultImageStackProfileRoiEditor), - ] - with self.defaultPlot() as plot: - profileManager = manager.ProfileManager(plot, plot) - editorAction = profileManager.createEditorAction(parent=plot) - for roiClass, editorClass in roiClasses: - with self.subTest(roiClass=roiClass): - roi = roiClass() - roi._setProfileManager(profileManager) - try: - # Force widget creation - menu = qt.QMenu(plot) - menu.addAction(editorAction) - widgets = editorAction.createdWidgets() - self.assertGreater(len(widgets), 0) - - editorAction.setProfileRoi(roi) - editorWidget = editorAction._getEditor(widgets[0]) - self.assertIsInstance(editorWidget, editorClass) - self.genericEditorTest(plot, roi, editorWidget) - finally: - editorAction.setProfileRoi(None) - menu.deleteLater() - menu = None - self.qapp.processEvents() - - -class TestProfileToolBar(TestCaseQt, ParametricTestCase): - """Tests for ProfileToolBar widget.""" - - def setUp(self): - super(TestProfileToolBar, self).setUp() - self.plot = PlotWindow() - self.toolBar = Profile.ProfileToolBar(plot=self.plot) - self.plot.addToolBar(self.toolBar) - - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - self.mouseMove(self.plot) # Move to center - self.qapp.processEvents() - deprecation.FORCE = True - - def tearDown(self): - deprecation.FORCE = False - self.qapp.processEvents() - profileManager = self.toolBar.getProfileManager() - profileManager.clearProfile() - profileManager = None - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - del self.plot - del self.toolBar - - super(TestProfileToolBar, self).tearDown() - - def testAlignedProfile(self): - """Test horizontal and vertical profile, without and with image""" - # Use Plot backend widget to submit mouse events - widget = self.plot.getWidgetHandle() - for method in ('sum', 'mean'): - with self.subTest(method=method): - # 2 positions to use for mouse events - pos1 = widget.width() * 0.4, widget.height() * 0.4 - pos2 = widget.width() * 0.6, widget.height() * 0.6 - - for action in (self.toolBar.hLineAction, self.toolBar.vLineAction): - with self.subTest(mode=action.text()): - # Trigger tool button for mode - action.trigger() - # Without image - self.mouseMove(widget, pos=pos1) - self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1) - - # with image - self.plot.addImage( - numpy.arange(100 * 100).reshape(100, -1)) - self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) - self.mouseMove(widget, pos=pos2) - self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) - - self.mouseMove(widget) - self.mouseClick(widget, qt.Qt.LeftButton) - - manager = self.toolBar.getProfileManager() - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - @testutils.test_logging(deprecation.depreclog.name, warning=4) - def testDiagonalProfile(self): - """Test diagonal profile, without and with image""" - # Use Plot backend widget to submit mouse events - widget = self.plot.getWidgetHandle() - - for method in ('sum', 'mean'): - for image in (False, True): - with self.subTest(method=method, image=image): - # 2 positions to use for mouse events - pos1 = widget.width() * 0.4, widget.height() * 0.4 - pos2 = widget.width() * 0.6, widget.height() * 0.6 - - if image: - self.plot.addImage( - numpy.arange(100 * 100).reshape(100, -1)) - - # Trigger tool button for diagonal profile mode - self.toolBar.lineAction.trigger() - - # draw profile line - widget.setFocus(qt.Qt.OtherFocusReason) - self.mouseMove(widget, pos=pos1) - self.qWait(100) - self.mousePress(widget, qt.Qt.LeftButton, pos=pos1) - self.qWait(100) - self.mouseMove(widget, pos=pos2) - self.qWait(100) - self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2) - self.qWait(100) - - manager = self.toolBar.getProfileManager() - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - roi = manager.getCurrentRoi() - self.assertIsNotNone(roi) - roi.setProfileLineWidth(3) - roi.setProfileMethod(method) - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - if image is True: - curveItem = self.toolBar.getProfilePlot().getAllCurves()[0] - if method == 'sum': - self.assertTrue(curveItem.getData()[1].max() > 10000) - elif method == 'mean': - self.assertTrue(curveItem.getData()[1].max() < 10000) - - # Remove the ROI so the profile window is also removed - roiManager = manager.getRoiManager() - roiManager.removeRoi(roi) - self.qWait(100) - - -class TestDeprecatedProfileToolBar(TestCaseQt): - """Tests old features of the ProfileToolBar widget.""" - - def setUp(self): - self.plot = None - super(TestDeprecatedProfileToolBar, self).setUp() - - def tearDown(self): - if self.plot is not None: - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - self.plot = None - self.qWait() - - super(TestDeprecatedProfileToolBar, self).tearDown() - - @testutils.test_logging(deprecation.depreclog.name, warning=2) - def testCustomProfileWindow(self): - from silx.gui.plot import ProfileMainWindow - - self.plot = PlotWindow() - profileWindow = ProfileMainWindow.ProfileMainWindow(self.plot) - toolBar = Profile.ProfileToolBar(parent=self.plot, - plot=self.plot, - profileWindow=profileWindow) - - self.plot.show() - self.qWaitForWindowExposed(self.plot) - profileWindow.show() - self.qWaitForWindowExposed(profileWindow) - self.qapp.processEvents() - - self.plot.addImage(numpy.arange(10 * 10).reshape(10, -1)) - profile = rois.ProfileImageHorizontalLineROI() - profile.setPosition(5) - toolBar.getProfileManager().getRoiManager().addRoi(profile) - toolBar.getProfileManager().getRoiManager().setCurrentRoi(profile) - - for _ in range(20): - self.qWait(200) - if not toolBar.getProfileManager().hasPendingOperations(): - break - - # There is a displayed profile - self.assertIsNotNone(profileWindow.getProfile()) - self.assertIs(toolBar.getProfileMainWindow(), profileWindow) - - # There is nothing anymore but the window is still there - toolBar.getProfileManager().clearProfile() - self.qapp.processEvents() - self.assertIsNone(profileWindow.getProfile()) - - -class TestProfile3DToolBar(TestCaseQt): - """Tests for Profile3DToolBar widget. - """ - def setUp(self): - super(TestProfile3DToolBar, self).setUp() - self.plot = StackView() - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - self.plot.setStack(numpy.array([ - [[0, 1, 2], [3, 4, 5]], - [[6, 7, 8], [9, 10, 11]], - [[12, 13, 14], [15, 16, 17]] - ])) - deprecation.FORCE = True - - def tearDown(self): - deprecation.FORCE = False - profileManager = self.plot.getProfileToolbar().getProfileManager() - profileManager.clearProfile() - profileManager = None - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - self.plot = None - - super(TestProfile3DToolBar, self).tearDown() - - @testutils.test_logging(deprecation.depreclog.name, warning=2) - def testMethodProfile2D(self): - """Test that the profile can have a different method if we want to - compute then in 1D or in 2D""" - - toolBar = self.plot.getProfileToolbar() - - toolBar.vLineAction.trigger() - plot2D = self.plot.getPlotWidget().getWidgetHandle() - pos1 = plot2D.width() * 0.5, plot2D.height() * 0.5 - self.mouseClick(plot2D, qt.Qt.LeftButton, pos=pos1) - - manager = toolBar.getProfileManager() - roi = manager.getCurrentRoi() - roi.setProfileMethod("mean") - roi.setProfileType("2D") - roi.setProfileLineWidth(3) - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - # check 2D 'mean' profile - profilePlot = toolBar.getProfilePlot() - data = profilePlot.getAllImages()[0].getData() - expected = numpy.array([[1, 4], [7, 10], [13, 16]]) - numpy.testing.assert_almost_equal(data, expected) - - @testutils.test_logging(deprecation.depreclog.name, warning=2) - def testMethodSumLine(self): - """Simple interaction test to make sure the sum is correctly computed - """ - toolBar = self.plot.getProfileToolbar() - - toolBar.lineAction.trigger() - plot2D = self.plot.getPlotWidget().getWidgetHandle() - pos1 = plot2D.width() * 0.5, plot2D.height() * 0.2 - pos2 = plot2D.width() * 0.5, plot2D.height() * 0.8 - - self.mouseMove(plot2D, pos=pos1) - self.mousePress(plot2D, qt.Qt.LeftButton, pos=pos1) - self.mouseMove(plot2D, pos=pos2) - self.mouseRelease(plot2D, qt.Qt.LeftButton, pos=pos2) - - manager = toolBar.getProfileManager() - roi = manager.getCurrentRoi() - roi.setProfileMethod("sum") - roi.setProfileType("2D") - roi.setProfileLineWidth(3) - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - # check 2D 'sum' profile - profilePlot = toolBar.getProfilePlot() - data = profilePlot.getAllImages()[0].getData() - expected = numpy.array([[3, 12], [21, 30], [39, 48]]) - numpy.testing.assert_almost_equal(data, expected) - - -class TestGetProfilePlot(TestCaseQt): - - def setUp(self): - self.plot = None - super(TestGetProfilePlot, self).setUp() - - def tearDown(self): - if self.plot is not None: - manager = self.plot.getProfileToolbar().getProfileManager() - manager.clearProfile() - manager = None - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - self.plot = None - - super(TestGetProfilePlot, self).tearDown() - - def testProfile1D(self): - self.plot = Plot2D() - self.plot.show() - self.qWaitForWindowExposed(self.plot) - self.plot.addImage([[0, 1], [2, 3]]) - - toolBar = self.plot.getProfileToolbar() - - manager = toolBar.getProfileManager() - roiManager = manager.getRoiManager() - - roi = rois.ProfileImageHorizontalLineROI() - roi.setPosition(0.5) - roiManager.addRoi(roi) - roiManager.setCurrentRoi(roi) - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - profileWindow = roi.getProfileWindow() - self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) - self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D) - - def testProfile2D(self): - """Test that the profile plot associated to a stack view is either a - Plot1D or a plot 2D instance.""" - self.plot = StackView() - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - self.plot.setStack(numpy.array([[[0, 1], [2, 3]], - [[4, 5], [6, 7]]])) - - toolBar = self.plot.getProfileToolbar() - - manager = toolBar.getProfileManager() - roiManager = manager.getRoiManager() - - roi = rois.ProfileImageStackHorizontalLineROI() - roi.setPosition(0.5) - roi.setProfileType("2D") - roiManager.addRoi(roi) - roiManager.setCurrentRoi(roi) - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - profileWindow = roi.getProfileWindow() - self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) - self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot2D) - - roi.setProfileType("1D") - - for _ in range(20): - self.qWait(200) - if not manager.hasPendingOperations(): - break - - profileWindow = roi.getProfileWindow() - self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow) - self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D) - - -def suite(): - test_suite = unittest.TestSuite() - loadTests = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loadTests(TestRois)) - test_suite.addTest(loadTests(TestInteractions)) - test_suite.addTest(loadTests(TestProfileToolBar)) - test_suite.addTest(loadTests(TestGetProfilePlot)) - test_suite.addTest(loadTests(TestProfile3DToolBar)) - test_suite.addTest(loadTests(TestDeprecatedProfileToolBar)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testROI.py b/silx/gui/plot/tools/test/testROI.py deleted file mode 100644 index 8a00073..0000000 --- a/silx/gui/plot/tools/test/testROI.py +++ /dev/null @@ -1,694 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import unittest -import numpy.testing - -from silx.gui import qt -from silx.utils.testutils import ParametricTestCase -from silx.gui.utils.testutils import TestCaseQt, SignalListener -from silx.gui.plot import PlotWindow -import silx.gui.plot.items.roi as roi_items -from silx.gui.plot.tools import roi - - -class TestRoiItems(TestCaseQt): - - def testLine_geometry(self): - item = roi_items.LineROI() - startPoint = numpy.array([1, 2]) - endPoint = numpy.array([3, 4]) - item.setEndPoints(startPoint, endPoint) - numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint) - numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint) - - def testHLine_geometry(self): - item = roi_items.HorizontalLineROI() - item.setPosition(15) - self.assertEqual(item.getPosition(), 15) - - def testVLine_geometry(self): - item = roi_items.VerticalLineROI() - item.setPosition(15) - self.assertEqual(item.getPosition(), 15) - - def testPoint_geometry(self): - point = numpy.array([1, 2]) - item = roi_items.PointROI() - item.setPosition(point) - numpy.testing.assert_allclose(item.getPosition(), point) - - def testRectangle_originGeometry(self): - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - center = numpy.array([5, 10]) - item = roi_items.RectangleROI() - item.setGeometry(origin=origin, size=size) - numpy.testing.assert_allclose(item.getOrigin(), origin) - numpy.testing.assert_allclose(item.getSize(), size) - numpy.testing.assert_allclose(item.getCenter(), center) - - def testRectangle_centerGeometry(self): - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - center = numpy.array([5, 10]) - item = roi_items.RectangleROI() - item.setGeometry(center=center, size=size) - numpy.testing.assert_allclose(item.getOrigin(), origin) - numpy.testing.assert_allclose(item.getSize(), size) - numpy.testing.assert_allclose(item.getCenter(), center) - - def testRectangle_setCenterGeometry(self): - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - item = roi_items.RectangleROI() - item.setGeometry(origin=origin, size=size) - newCenter = numpy.array([0, 0]) - item.setCenter(newCenter) - expectedOrigin = numpy.array([-5, -10]) - numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin) - numpy.testing.assert_allclose(item.getCenter(), newCenter) - numpy.testing.assert_allclose(item.getSize(), size) - - def testRectangle_setOriginGeometry(self): - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - item = roi_items.RectangleROI() - item.setGeometry(origin=origin, size=size) - newOrigin = numpy.array([10, 10]) - item.setOrigin(newOrigin) - expectedCenter = numpy.array([15, 20]) - numpy.testing.assert_allclose(item.getOrigin(), newOrigin) - numpy.testing.assert_allclose(item.getCenter(), expectedCenter) - numpy.testing.assert_allclose(item.getSize(), size) - - def testCircle_geometry(self): - center = numpy.array([0, 0]) - radius = 10. - item = roi_items.CircleROI() - item.setGeometry(center=center, radius=radius) - numpy.testing.assert_allclose(item.getCenter(), center) - numpy.testing.assert_allclose(item.getRadius(), radius) - - def testCircle_setCenter(self): - center = numpy.array([0, 0]) - radius = 10. - item = roi_items.CircleROI() - item.setGeometry(center=center, radius=radius) - newCenter = numpy.array([-10, 0]) - item.setCenter(newCenter) - numpy.testing.assert_allclose(item.getCenter(), newCenter) - numpy.testing.assert_allclose(item.getRadius(), radius) - - def testCircle_setRadius(self): - center = numpy.array([0, 0]) - radius = 10. - item = roi_items.CircleROI() - item.setGeometry(center=center, radius=radius) - newRadius = 5.1 - item.setRadius(newRadius) - numpy.testing.assert_allclose(item.getCenter(), center) - numpy.testing.assert_allclose(item.getRadius(), newRadius) - - def testCircle_contains(self): - center = numpy.array([2, -1]) - radius = 1. - item = roi_items.CircleROI() - item.setGeometry(center=center, radius=radius) - self.assertTrue(item.contains([1, -1])) - self.assertFalse(item.contains([0, 0])) - self.assertTrue(item.contains([2, 0])) - self.assertFalse(item.contains([3.01, -1])) - - def testEllipse_contains(self): - center = numpy.array([-2, 0]) - item = roi_items.EllipseROI() - item.setCenter(center) - item.setOrientation(numpy.pi / 4.0) - item.setMajorRadius(2) - item.setMinorRadius(1) - print(item.getMinorRadius(), item.getMajorRadius()) - self.assertFalse(item.contains([0, 0])) - self.assertTrue(item.contains([-1, 1])) - self.assertTrue(item.contains([-3, 0])) - self.assertTrue(item.contains([-2, 0])) - self.assertTrue(item.contains([-2, 1])) - self.assertFalse(item.contains([-4, 1])) - - def testRectangle_isIn(self): - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - item = roi_items.RectangleROI() - item.setGeometry(origin=origin, size=size) - self.assertTrue(item.contains(position=(0, 0))) - self.assertTrue(item.contains(position=(2, 14))) - self.assertFalse(item.contains(position=(14, 12))) - - def testPolygon_emptyGeometry(self): - points = numpy.empty((0, 2)) - item = roi_items.PolygonROI() - item.setPoints(points) - numpy.testing.assert_allclose(item.getPoints(), points) - - def testPolygon_geometry(self): - points = numpy.array([[10, 10], [12, 10], [50, 1]]) - item = roi_items.PolygonROI() - item.setPoints(points) - numpy.testing.assert_allclose(item.getPoints(), points) - - def testPolygon_isIn(self): - points = numpy.array([[0, 0], [0, 10], [5, 10]]) - item = roi_items.PolygonROI() - item.setPoints(points) - self.assertTrue(item.contains((0, 0))) - self.assertFalse(item.contains((6, 2))) - self.assertFalse(item.contains((-2, 5))) - self.assertFalse(item.contains((2, -1))) - self.assertFalse(item.contains((8, 1))) - self.assertTrue(item.contains((1, 8))) - - def testArc_getToSetGeometry(self): - """Test that we can use getGeometry as input to setGeometry""" - item = roi_items.ArcROI() - item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]])) - item.setGeometry(*item.getGeometry()) - - def testArc_degenerated_point(self): - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0 - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - - def testArc_degenerated_line(self): - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - - def testArc_special_circle(self): - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - numpy.testing.assert_allclose(item.getCenter(), center) - self.assertAlmostEqual(item.getInnerRadius(), innerRadius) - self.assertAlmostEqual(item.getOuterRadius(), outerRadius) - self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0) - self.assertTrue(item.isClosed()) - - def testArc_special_donut(self): - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - numpy.testing.assert_allclose(item.getCenter(), center) - self.assertAlmostEqual(item.getInnerRadius(), innerRadius) - self.assertAlmostEqual(item.getOuterRadius(), outerRadius) - self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0) - self.assertTrue(item.isClosed()) - - def testArc_clockwiseGeometry(self): - """Test that we can use getGeometry as input to setGeometry""" - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - numpy.testing.assert_allclose(item.getCenter(), center) - self.assertAlmostEqual(item.getInnerRadius(), innerRadius) - self.assertAlmostEqual(item.getOuterRadius(), outerRadius) - self.assertAlmostEqual(item.getStartAngle(), startAngle) - self.assertAlmostEqual(item.getEndAngle(), endAngle) - self.assertAlmostEqual(item.isClosed(), False) - - def testArc_anticlockwiseGeometry(self): - """Test that we can use getGeometry as input to setGeometry""" - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, -numpy.pi * 0.5 - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - numpy.testing.assert_allclose(item.getCenter(), center) - self.assertAlmostEqual(item.getInnerRadius(), innerRadius) - self.assertAlmostEqual(item.getOuterRadius(), outerRadius) - self.assertAlmostEqual(item.getStartAngle(), startAngle) - self.assertAlmostEqual(item.getEndAngle(), endAngle) - self.assertAlmostEqual(item.isClosed(), False) - - def testHRange_geometry(self): - item = roi_items.HorizontalRangeROI() - vmin = 1 - vmax = 3 - item.setRange(vmin, vmax) - self.assertAlmostEqual(item.getMin(), vmin) - self.assertAlmostEqual(item.getMax(), vmax) - self.assertAlmostEqual(item.getCenter(), 2) - - -class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): - """Tests for RegionOfInterestManager class""" - - def setUp(self): - super(TestRegionOfInterestManager, self).setUp() - self.plot = PlotWindow() - - self.roiTableWidget = roi.RegionOfInterestTableWidget() - dock = qt.QDockWidget() - dock.setWidget(self.roiTableWidget) - self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, dock) - - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - def tearDown(self): - del self.roiTableWidget - self.qapp.processEvents() - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - del self.plot - super(TestRegionOfInterestManager, self).tearDown() - - def test(self): - """Test ROI of different shapes""" - tests = ( # shape, points=[list of (x, y), list of (x, y)] - (roi_items.PointROI, numpy.array(([(10., 15.)], [(20., 25.)]))), - (roi_items.RectangleROI, - numpy.array((((1., 10.), (11., 20.)), - ((2., 3.), (12., 13.))))), - (roi_items.PolygonROI, - numpy.array((((0., 1.), (0., 10.), (10., 0.)), - ((5., 6.), (5., 16.), (15., 6.))))), - (roi_items.LineROI, - numpy.array((((10., 20.), (10., 30.)), - ((30., 40.), (30., 50.))))), - (roi_items.HorizontalLineROI, - numpy.array((((10., 20.), (10., 30.)), - ((30., 40.), (30., 50.))))), - (roi_items.VerticalLineROI, - numpy.array((((10., 20.), (10., 30.)), - ((30., 40.), (30., 50.))))), - (roi_items.HorizontalLineROI, - numpy.array((((10., 20.), (10., 30.)), - ((30., 40.), (30., 50.))))), - ) - - for roiClass, points in tests: - with self.subTest(roiClass=roiClass): - manager = roi.RegionOfInterestManager(self.plot) - self.roiTableWidget.setRegionOfInterestManager(manager) - manager.start(roiClass) - - self.assertEqual(manager.getRois(), ()) - - finishListener = SignalListener() - manager.sigInteractiveModeFinished.connect(finishListener) - - changedListener = SignalListener() - manager.sigRoiChanged.connect(changedListener) - - # Add a point - r = roiClass() - r.setFirstShapePoints(points[0]) - manager.addRoi(r) - self.qapp.processEvents() - self.assertTrue(len(manager.getRois()), 1) - self.assertEqual(changedListener.callCount(), 1) - - # Remove it - manager.removeRoi(manager.getRois()[0]) - self.assertEqual(manager.getRois(), ()) - self.assertEqual(changedListener.callCount(), 2) - - # Add two point - r = roiClass() - r.setFirstShapePoints(points[0]) - manager.addRoi(r) - self.qapp.processEvents() - r = roiClass() - r.setFirstShapePoints(points[1]) - manager.addRoi(r) - self.qapp.processEvents() - self.assertTrue(len(manager.getRois()), 2) - self.assertEqual(changedListener.callCount(), 4) - - # Reset it - result = manager.clear() - self.assertTrue(result) - self.assertEqual(manager.getRois(), ()) - self.assertEqual(changedListener.callCount(), 5) - - changedListener.clear() - - # Add two point - r = roiClass() - r.setFirstShapePoints(points[0]) - manager.addRoi(r) - self.qapp.processEvents() - r = roiClass() - r.setFirstShapePoints(points[1]) - manager.addRoi(r) - self.qapp.processEvents() - self.assertTrue(len(manager.getRois()), 2) - self.assertEqual(changedListener.callCount(), 2) - - # stop - result = manager.stop() - self.assertTrue(result) - self.assertTrue(len(manager.getRois()), 1) - self.qapp.processEvents() - self.assertEqual(finishListener.callCount(), 1) - - manager.clear() - - def testRoiDisplay(self): - rois = [] - - # Line - item = roi_items.LineROI() - startPoint = numpy.array([1, 2]) - endPoint = numpy.array([3, 4]) - item.setEndPoints(startPoint, endPoint) - rois.append(item) - # Horizontal line - item = roi_items.HorizontalLineROI() - item.setPosition(15) - rois.append(item) - # Vertical line - item = roi_items.VerticalLineROI() - item.setPosition(15) - rois.append(item) - # Point - item = roi_items.PointROI() - point = numpy.array([1, 2]) - item.setPosition(point) - rois.append(item) - # Rectangle - item = roi_items.RectangleROI() - origin = numpy.array([0, 0]) - size = numpy.array([10, 20]) - item.setGeometry(origin=origin, size=size) - rois.append(item) - # Polygon - item = roi_items.PolygonROI() - points = numpy.array([[10, 10], [12, 10], [50, 1]]) - item.setPoints(points) - rois.append(item) - # Degenerated polygon: No points - item = roi_items.PolygonROI() - points = numpy.empty((0, 2)) - item.setPoints(points) - rois.append(item) - # Degenerated polygon: A single point - item = roi_items.PolygonROI() - points = numpy.array([[5, 10]]) - item.setPoints(points) - rois.append(item) - # Degenerated arc: it's a point - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0 - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - rois.append(item) - # Degenerated arc: it's a line - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - rois.append(item) - # Special arc: it's a donut - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - rois.append(item) - # Arc - item = roi_items.ArcROI() - center = numpy.array([10, 20]) - innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi - item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle) - rois.append(item) - # Horizontal Range - item = roi_items.HorizontalRangeROI() - item.setRange(-1, 3) - rois.append(item) - - manager = roi.RegionOfInterestManager(self.plot) - self.roiTableWidget.setRegionOfInterestManager(manager) - for item in rois: - with self.subTest(roi=str(item)): - manager.addRoi(item) - self.qapp.processEvents() - item.setEditable(True) - self.qapp.processEvents() - item.setEditable(False) - self.qapp.processEvents() - manager.removeRoi(item) - self.qapp.processEvents() - - def testSelectionProxy(self): - item1 = roi_items.PointROI() - item1.setSelectable(True) - item2 = roi_items.PointROI() - item2.setSelectable(True) - item1.setFocusProxy(item2) - manager = roi.RegionOfInterestManager(self.plot) - manager.setCurrentRoi(item1) - self.assertIs(manager.getCurrentRoi(), item2) - - def testRemovedSelection(self): - item1 = roi_items.PointROI() - item1.setSelectable(True) - manager = roi.RegionOfInterestManager(self.plot) - manager.addRoi(item1) - manager.setCurrentRoi(item1) - manager.removeRoi(item1) - self.assertIs(manager.getCurrentRoi(), None) - - def testMaxROI(self): - """Test Max ROI""" - origin1 = numpy.array([1., 10.]) - size1 = numpy.array([10., 10.]) - origin2 = numpy.array([2., 3.]) - size2 = numpy.array([10., 10.]) - - manager = roi.InteractiveRegionOfInterestManager(self.plot) - self.roiTableWidget.setRegionOfInterestManager(manager) - self.assertEqual(manager.getRois(), ()) - - changedListener = SignalListener() - manager.sigRoiChanged.connect(changedListener) - - # Add two point - item = roi_items.RectangleROI() - item.setGeometry(origin=origin1, size=size1) - manager.addRoi(item) - item = roi_items.RectangleROI() - item.setGeometry(origin=origin2, size=size2) - manager.addRoi(item) - self.qapp.processEvents() - self.assertEqual(changedListener.callCount(), 2) - self.assertEqual(len(manager.getRois()), 2) - - # Try to set max ROI to 1 while there is 2 ROIs - with self.assertRaises(ValueError): - manager.setMaxRois(1) - - manager.clear() - self.assertEqual(len(manager.getRois()), 0) - self.assertEqual(changedListener.callCount(), 3) - - # Set max limit to 1 - manager.setMaxRois(1) - - # Add a point - item = roi_items.RectangleROI() - item.setGeometry(origin=origin1, size=size1) - manager.addRoi(item) - self.qapp.processEvents() - self.assertEqual(changedListener.callCount(), 4) - - # Add a 2nd point while max ROI is 1 - item = roi_items.RectangleROI() - item.setGeometry(origin=origin1, size=size1) - manager.addRoi(item) - self.qapp.processEvents() - self.assertEqual(changedListener.callCount(), 6) - self.assertEqual(len(manager.getRois()), 1) - - def testChangeInteractionMode(self): - """Test change of interaction mode""" - manager = roi.RegionOfInterestManager(self.plot) - self.roiTableWidget.setRegionOfInterestManager(manager) - manager.start(roi_items.PointROI) - - interactiveModeToolBar = self.plot.getInteractiveModeToolBar() - panAction = interactiveModeToolBar.getPanModeAction() - - for roiClass in manager.getSupportedRoiClasses(): - with self.subTest(roiClass=roiClass): - # Change to pan mode - panAction.trigger() - - # Change to interactive ROI mode - action = manager.getInteractionModeAction(roiClass) - action.trigger() - - self.assertEqual(roiClass, manager.getCurrentInteractionModeRoiClass()) - - manager.clear() - - def testLineInteraction(self): - """This test make sure that a ROI based on handles can be edited with - the mouse.""" - xlimit = self.plot.getXAxis().getLimits() - ylimit = self.plot.getYAxis().getLimits() - points = numpy.array([xlimit, ylimit]).T - center = numpy.mean(points, axis=0) - - # Create the line - manager = roi.RegionOfInterestManager(self.plot) - item = roi_items.LineROI() - item.setEndPoints(points[0], points[1]) - item.setEditable(True) - manager.addRoi(item) - self.qapp.processEvents() - - # Drag the center - widget = self.plot.getWidgetHandle() - mx, my = self.plot.dataToPixel(*center) - self.mouseMove(widget, pos=(mx, my)) - self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my)) - self.mouseMove(widget, pos=(mx, my+25)) - self.mouseMove(widget, pos=(mx, my+50)) - self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my+50)) - - result = numpy.array(item.getEndPoints()) - # x location is still the same - numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5) - # size is still the same - numpy.testing.assert_allclose(points[1] - points[0], - result[1] - result[0], atol=0.5) - # But Y is not the same - self.assertNotEqual(points[0, 1], result[0, 1]) - self.assertNotEqual(points[1, 1], result[1, 1]) - item = None - manager.clear() - self.qapp.processEvents() - - def testPlotWhenCleared(self): - """PlotWidget.clear should clean up the available ROIs""" - manager = roi.RegionOfInterestManager(self.plot) - item = roi_items.LineROI() - item.setEndPoints((0, 0), (1, 1)) - item.setEditable(True) - manager.addRoi(item) - self.qWait() - try: - # Make sure the test setup is fine - self.assertNotEqual(len(manager.getRois()), 0) - self.assertNotEqual(len(self.plot.getItems()), 0) - - # Call clear and test the expected state - self.plot.clear() - self.assertEqual(len(manager.getRois()), 0) - self.assertEqual(len(self.plot.getItems()), 0) - finally: - # Clean up - manager.clear() - - def testPlotWhenRoiRemoved(self): - """Make sure there is no remaining items in the plot when a ROI is removed""" - manager = roi.RegionOfInterestManager(self.plot) - item = roi_items.LineROI() - item.setEndPoints((0, 0), (1, 1)) - item.setEditable(True) - manager.addRoi(item) - self.qWait() - try: - # Make sure the test setup is fine - self.assertNotEqual(len(manager.getRois()), 0) - self.assertNotEqual(len(self.plot.getItems()), 0) - - # Call clear and test the expected state - manager.removeRoi(item) - self.assertEqual(len(manager.getRois()), 0) - self.assertEqual(len(self.plot.getItems()), 0) - finally: - # Clean up - manager.clear() - - def testArcRoiSwitchMode(self): - """Make sure we can switch mode by clicking on the ROI""" - xlimit = self.plot.getXAxis().getLimits() - ylimit = self.plot.getYAxis().getLimits() - points = numpy.array([xlimit, ylimit]).T - center = numpy.mean(points, axis=0) - size = numpy.abs(points[1] - points[0]) - - # Create the line - manager = roi.RegionOfInterestManager(self.plot) - item = roi_items.ArcROI() - item.setGeometry(center, size[1] / 10, size[1] / 2, 0, 3) - item.setEditable(True) - item.setSelectable(True) - manager.addRoi(item) - self.qapp.processEvents() - - # Initial state - self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode) - self.qWait(500) - - # Click on the center - widget = self.plot.getWidgetHandle() - mx, my = self.plot.dataToPixel(*center) - - # Select the ROI - self.mouseMove(widget, pos=(mx, my)) - self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my)) - self.qWait(500) - self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode) - - # Change the mode - self.mouseMove(widget, pos=(mx, my)) - self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my)) - self.qWait(500) - self.assertIs(item.getInteractionMode(), roi_items.ArcROI.PolarMode) - - manager.clear() - self.qapp.processEvents() - - -def suite(): - test_suite = unittest.TestSuite() - loadTests = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loadTests(TestRoiItems)) - test_suite.addTest(loadTests(TestRegionOfInterestManager)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/silx/gui/plot/tools/test/testScatterProfileToolBar.py deleted file mode 100644 index b9f4885..0000000 --- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py +++ /dev/null @@ -1,196 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import unittest -import numpy - -from silx.gui import qt -from silx.utils.testutils import ParametricTestCase -from silx.gui.utils.testutils import TestCaseQt -from silx.gui.plot import PlotWindow -from silx.gui.plot.tools.profile import manager -from silx.gui.plot.tools.profile import core -from silx.gui.plot.tools.profile import rois - - -class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase): - """Tests for ScatterProfileToolBar class""" - - def setUp(self): - super(TestScatterProfileToolBar, self).setUp() - self.plot = PlotWindow() - - self.manager = manager.ProfileManager(plot=self.plot) - self.manager.setItemType(scatter=True) - self.manager.setActiveItemTracking(True) - - self.plot.show() - self.qWaitForWindowExposed(self.plot) - - def tearDown(self): - del self.manager - self.qapp.processEvents() - self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) - self.plot.close() - del self.plot - super(TestScatterProfileToolBar, self).tearDown() - - def testHorizontalProfile(self): - """Test ScatterProfileToolBar horizontal profile""" - - roiManager = self.manager.getRoiManager() - - # Add a scatter plot - self.plot.addScatter( - x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.)) - self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) - self.qapp.processEvents() - - # Set a ROI profile - roi = rois.ProfileScatterHorizontalLineROI() - roi.setPosition(0.5) - roi.setNPoints(8) - roiManager.addRoi(roi) - - # Wait for async interpolator init - for _ in range(20): - self.qWait(200) - if not self.manager.hasPendingOperations(): - break - self.qapp.processEvents() - - window = roi.getProfileWindow() - self.assertIsNotNone(window) - data = window.getProfile() - self.assertIsInstance(data, core.CurveProfileData) - self.assertEqual(len(data.coords), 8) - - # Check that profile has same limits than Plot - xLimits = self.plot.getXAxis().getLimits() - self.assertEqual(data.coords[0], xLimits[0]) - self.assertEqual(data.coords[-1], xLimits[1]) - - # Clear the profile - self.manager.clearProfile() - self.qapp.processEvents() - self.assertIsNone(roi.getProfileWindow()) - - def testVerticalProfile(self): - """Test ScatterProfileToolBar vertical profile""" - - roiManager = self.manager.getRoiManager() - - # Add a scatter plot - self.plot.addScatter( - x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.)) - self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) - self.qapp.processEvents() - - # Set a ROI profile - roi = rois.ProfileScatterVerticalLineROI() - roi.setPosition(0.5) - roi.setNPoints(8) - roiManager.addRoi(roi) - - # Wait for async interpolator init - for _ in range(10): - self.qWait(200) - if not self.manager.hasPendingOperations(): - break - - window = roi.getProfileWindow() - self.assertIsNotNone(window) - data = window.getProfile() - self.assertIsInstance(data, core.CurveProfileData) - self.assertEqual(len(data.coords), 8) - - # Check that profile has same limits than Plot - yLimits = self.plot.getYAxis().getLimits() - self.assertEqual(data.coords[0], yLimits[0]) - self.assertEqual(data.coords[-1], yLimits[1]) - - # Check that profile limits are updated when changing limits - self.plot.getYAxis().setLimits(yLimits[0] + 1, yLimits[1] + 10) - - # Wait for async interpolator init - for _ in range(10): - self.qWait(200) - if not self.manager.hasPendingOperations(): - break - - yLimits = self.plot.getYAxis().getLimits() - data = window.getProfile() - self.assertEqual(data.coords[0], yLimits[0]) - self.assertEqual(data.coords[-1], yLimits[1]) - - # Clear the profile - self.manager.clearProfile() - self.qapp.processEvents() - self.assertIsNone(roi.getProfileWindow()) - - def testLineProfile(self): - """Test ScatterProfileToolBar line profile""" - - roiManager = self.manager.getRoiManager() - - # Add a scatter plot - self.plot.addScatter( - x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.)) - self.plot.resetZoom(dataMargins=(.1, .1, .1, .1)) - self.qapp.processEvents() - - # Set a ROI profile - roi = rois.ProfileScatterLineROI() - roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.])) - roi.setNPoints(8) - roiManager.addRoi(roi) - - # Wait for async interpolator init - for _ in range(10): - self.qWait(200) - if not self.manager.hasPendingOperations(): - break - - window = roi.getProfileWindow() - self.assertIsNotNone(window) - data = window.getProfile() - self.assertIsInstance(data, core.CurveProfileData) - self.assertEqual(len(data.coords), 8) - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase( - TestScatterProfileToolBar)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/test/testTools.py b/silx/gui/plot/tools/test/testTools.py deleted file mode 100644 index 70c8105..0000000 --- a/silx/gui/plot/tools/test/testTools.py +++ /dev/null @@ -1,147 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Basic tests for silx.gui.plot.tools package""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "02/03/2018" - - -import functools -import unittest -import numpy - -from silx.utils.testutils import TestLogging -from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate -from silx.gui import qt -from silx.gui.plot import PlotWindow -from silx.gui.plot import tools -from silx.gui.plot.test.utils import PlotWidgetTestCase - - -class TestPositionInfo(PlotWidgetTestCase): - """Tests for PositionInfo widget.""" - - def _createPlot(self): - return PlotWindow() - - def setUp(self): - super(TestPositionInfo, self).setUp() - self.mouseMove(self.plot, pos=(0, 0)) - self.qapp.processEvents() - self.qWait(100) - - def tearDown(self): - super(TestPositionInfo, self).tearDown() - - def _test(self, positionWidget, converterNames, **kwargs): - """General test of PositionInfo. - - - Add it to a toolbar and - - Move mouse around the center of the PlotWindow. - """ - toolBar = qt.QToolBar() - self.plot.addToolBar(qt.Qt.BottomToolBarArea, toolBar) - - toolBar.addWidget(positionWidget) - - converters = positionWidget.getConverters() - self.assertEqual(len(converters), len(converterNames)) - for index, name in enumerate(converterNames): - self.assertEqual(converters[index][0], name) - - with TestLogging(tools.__name__, **kwargs): - # Move mouse to center - center = self.plot.size() / 2 - self.mouseMove(self.plot, pos=(center.width(), center.height())) - # Move out - self.mouseMove(self.plot, pos=(1, 1)) - - def testDefaultConverters(self): - """Test PositionInfo with default converters""" - positionWidget = tools.PositionInfo(plot=self.plot) - self._test(positionWidget, ('X', 'Y')) - - def testCustomConverters(self): - """Test PositionInfo with custom converters""" - converters = [ - ('Coords', lambda x, y: (int(x), int(y))), - ('Radius', lambda x, y: numpy.sqrt(x * x + y * y)), - ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x))) - ] - positionWidget = tools.PositionInfo(plot=self.plot, - converters=converters) - self._test(positionWidget, ('Coords', 'Radius', 'Angle')) - - def testFailingConverters(self): - """Test PositionInfo with failing custom converters""" - def raiseException(x, y): - raise RuntimeError() - - positionWidget = tools.PositionInfo( - plot=self.plot, - converters=[('Exception', raiseException)]) - self._test(positionWidget, ['Exception'], error=2) - - def testUpdate(self): - """Test :meth:`PositionInfo.updateInfo`""" - calls = [] - - def update(calls, x, y): # Get number of calls - calls.append((x, y)) - return len(calls) - - positionWidget = tools.PositionInfo( - plot=self.plot, - converters=[('Call count', functools.partial(update, calls))]) - - positionWidget.updateInfo() - self.assertEqual(len(calls), 1) - - -class TestPlotToolsToolbars(PlotWidgetTestCase): - """Tests toolbars from silx.gui.plot.tools""" - - def test(self): - """"Add all toolbars""" - for tbClass in (tools.InteractiveModeToolBar, - tools.ImageToolBar, - tools.CurveToolBar, - tools.OutputToolBar): - tb = tbClass(parent=self.plot, plot=self.plot) - self.plot.addToolBar(tb) - - -def suite(): - test_suite = unittest.TestSuite() - # test_suite.addTest(positionInfoTestSuite) - for testClass in (TestPositionInfo, TestPlotToolsToolbars): - test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( - testClass)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/plot/tools/toolbars.py b/silx/gui/plot/tools/toolbars.py deleted file mode 100644 index 3df7d06..0000000 --- a/silx/gui/plot/tools/toolbars.py +++ /dev/null @@ -1,362 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides toolbars that work with :class:`PlotWidget`. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "01/03/2018" - - -from ... import qt -from .. import actions -from ..PlotWidget import PlotWidget -from .. import PlotToolButtons -from ....utils.deprecation import deprecated - - -class InteractiveModeToolBar(qt.QToolBar): - """Toolbar with interactive mode actions - - :param parent: See :class:`QWidget` - :param silx.gui.plot.PlotWidget plot: PlotWidget to control - :param str title: Title of the toolbar. - """ - - def __init__(self, parent=None, plot=None, title='Plot Interaction'): - super(InteractiveModeToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self._zoomModeAction = actions.mode.ZoomModeAction( - parent=self, plot=plot) - self.addAction(self._zoomModeAction) - - self._panModeAction = actions.mode.PanModeAction( - parent=self, plot=plot) - self.addAction(self._panModeAction) - - def getZoomModeAction(self): - """Returns the zoom mode QAction. - - :rtype: PlotAction - """ - return self._zoomModeAction - - def getPanModeAction(self): - """Returns the pan mode QAction - - :rtype: PlotAction - """ - return self._panModeAction - - -class OutputToolBar(qt.QToolBar): - """Toolbar providing icons to copy, save and print a PlotWidget - - :param parent: See :class:`QWidget` - :param silx.gui.plot.PlotWidget plot: PlotWidget to control - :param str title: Title of the toolbar. - """ - - def __init__(self, parent=None, plot=None, title='Plot Output'): - super(OutputToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self._copyAction = actions.io.CopyAction(parent=self, plot=plot) - self.addAction(self._copyAction) - - self._saveAction = actions.io.SaveAction(parent=self, plot=plot) - self.addAction(self._saveAction) - - self._printAction = actions.io.PrintAction(parent=self, plot=plot) - self.addAction(self._printAction) - - def getCopyAction(self): - """Returns the QAction performing copy to clipboard of the PlotWidget - - :rtype: PlotAction - """ - return self._copyAction - - def getSaveAction(self): - """Returns the QAction performing save to file of the PlotWidget - - :rtype: PlotAction - """ - return self._saveAction - - def getPrintAction(self): - """Returns the QAction performing printing of the PlotWidget - - :rtype: PlotAction - """ - return self._printAction - - -class ImageToolBar(qt.QToolBar): - """Toolbar providing PlotAction suited when displaying images - - :param parent: See :class:`QWidget` - :param silx.gui.plot.PlotWidget plot: PlotWidget to control - :param str title: Title of the toolbar. - """ - - def __init__(self, parent=None, plot=None, title='Image'): - super(ImageToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self._resetZoomAction = actions.control.ResetZoomAction( - parent=self, plot=plot) - self.addAction(self._resetZoomAction) - - self._colormapAction = actions.control.ColormapAction( - parent=self, plot=plot) - self.addAction(self._colormapAction) - - self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton( - parent=self, plot=plot) - self.addWidget(self._keepDataAspectRatioButton) - - self._yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton( - parent=self, plot=plot) - self.addWidget(self._yAxisInvertedButton) - - def getResetZoomAction(self): - """Returns the QAction to reset the zoom. - - :rtype: PlotAction - """ - return self._resetZoomAction - - def getColormapAction(self): - """Returns the QAction to control the colormap. - - :rtype: PlotAction - """ - return self._colormapAction - - def getKeepDataAspectRatioButton(self): - """Returns the QToolButton controlling data aspect ratio. - - :rtype: QToolButton - """ - return self._keepDataAspectRatioButton - - def getYAxisInvertedButton(self): - """Returns the QToolButton controlling Y axis orientation. - - :rtype: QToolButton - """ - return self._yAxisInvertedButton - - -class CurveToolBar(qt.QToolBar): - """Toolbar providing PlotAction suited when displaying curves - - :param parent: See :class:`QWidget` - :param silx.gui.plot.PlotWidget plot: PlotWidget to control - :param str title: Title of the toolbar. - """ - - def __init__(self, parent=None, plot=None, title='Image'): - super(CurveToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self._resetZoomAction = actions.control.ResetZoomAction( - parent=self, plot=plot) - self.addAction(self._resetZoomAction) - - self._xAxisAutoScaleAction = actions.control.XAxisAutoScaleAction( - parent=self, plot=plot) - self.addAction(self._xAxisAutoScaleAction) - - self._yAxisAutoScaleAction = actions.control.YAxisAutoScaleAction( - parent=self, plot=plot) - self.addAction(self._yAxisAutoScaleAction) - - self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction( - parent=self, plot=plot) - self.addAction(self._xAxisLogarithmicAction) - - self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction( - parent=self, plot=plot) - self.addAction(self._yAxisLogarithmicAction) - - self._gridAction = actions.control.GridAction( - parent=self, plot=plot) - self.addAction(self._gridAction) - - self._curveStyleAction = actions.control.CurveStyleAction( - parent=self, plot=plot) - self.addAction(self._curveStyleAction) - - def getResetZoomAction(self): - """Returns the QAction to reset the zoom. - - :rtype: PlotAction - """ - return self._resetZoomAction - - def getXAxisAutoScaleAction(self): - """Returns the QAction to toggle X axis autoscale. - - :rtype: PlotAction - """ - return self._xAxisAutoScaleAction - - def getYAxisAutoScaleAction(self): - """Returns the QAction to toggle Y axis autoscale. - - :rtype: PlotAction - """ - return self._yAxisAutoScaleAction - - def getXAxisLogarithmicAction(self): - """Returns the QAction to toggle X axis log/linear scale. - - :rtype: PlotAction - """ - return self._xAxisLogarithmicAction - - def getYAxisLogarithmicAction(self): - """Returns the QAction to toggle Y axis log/linear scale. - - :rtype: PlotAction - """ - return self._yAxisLogarithmicAction - - def getGridAction(self): - """Returns the action to toggle the plot grid. - - :rtype: PlotAction - """ - return self._gridAction - - def getCurveStyleAction(self): - """Returns the QAction to change the style of all curves. - - :rtype: PlotAction - """ - return self._curveStyleAction - - -class ScatterToolBar(qt.QToolBar): - """Toolbar providing PlotAction suited when displaying scatter plot - - :param parent: See :class:`QWidget` - :param silx.gui.plot.PlotWidget plot: PlotWidget to control - :param str title: Title of the toolbar. - """ - - def __init__(self, parent=None, plot=None, title='Scatter Tools'): - super(ScatterToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self._resetZoomAction = actions.control.ResetZoomAction( - parent=self, plot=plot) - self.addAction(self._resetZoomAction) - - self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction( - parent=self, plot=plot) - self.addAction(self._xAxisLogarithmicAction) - - self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction( - parent=self, plot=plot) - self.addAction(self._yAxisLogarithmicAction) - - self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton( - parent=self, plot=plot) - self.addWidget(self._keepDataAspectRatioButton) - - self._gridAction = actions.control.GridAction( - parent=self, plot=plot) - self.addAction(self._gridAction) - - self._colormapAction = actions.control.ColormapAction( - parent=self, plot=plot) - self.addAction(self._colormapAction) - - self._visualizationToolButton = \ - PlotToolButtons.ScatterVisualizationToolButton(parent=self, plot=plot) - self.addWidget(self._visualizationToolButton) - - def getResetZoomAction(self): - """Returns the QAction to reset the zoom. - - :rtype: PlotAction - """ - return self._resetZoomAction - - def getXAxisLogarithmicAction(self): - """Returns the QAction to toggle X axis log/linear scale. - - :rtype: PlotAction - """ - return self._xAxisLogarithmicAction - - def getYAxisLogarithmicAction(self): - """Returns the QAction to toggle Y axis log/linear scale. - - :rtype: PlotAction - """ - return self._yAxisLogarithmicAction - - def getGridAction(self): - """Returns the action to toggle the plot grid. - - :rtype: PlotAction - """ - return self._gridAction - - def getColormapAction(self): - """Returns the QAction to control the colormap. - - :rtype: PlotAction - """ - return self._colormapAction - - def getKeepDataAspectRatioButton(self): - """Returns the QToolButton controlling data aspect ratio. - - :rtype: QToolButton - """ - return self._keepDataAspectRatioButton - - def getScatterVisualizationToolButton(self): - """Returns the QToolButton controlling the visualization mode. - - :rtype: ScatterVisualizationToolButton - """ - return self._visualizationToolButton - - @deprecated(replacement='getScatterVisualizationToolButton', - since_version='0.11.0') - def getSymbolToolButton(self): - return self.getScatterVisualizationToolButton() |