summaryrefslogtreecommitdiff
path: root/silx/gui/plot/tools
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/tools')
-rw-r--r--silx/gui/plot/tools/CurveLegendsWidget.py247
-rw-r--r--silx/gui/plot/tools/LimitsToolBar.py131
-rw-r--r--silx/gui/plot/tools/PositionInfo.py376
-rw-r--r--silx/gui/plot/tools/RadarView.py361
-rw-r--r--silx/gui/plot/tools/__init__.py50
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py54
-rw-r--r--silx/gui/plot/tools/profile/__init__.py38
-rw-r--r--silx/gui/plot/tools/profile/core.py525
-rw-r--r--silx/gui/plot/tools/profile/editors.py307
-rw-r--r--silx/gui/plot/tools/profile/manager.py1076
-rw-r--r--silx/gui/plot/tools/profile/rois.py1156
-rw-r--r--silx/gui/plot/tools/profile/toolbar.py172
-rw-r--r--silx/gui/plot/tools/roi.py1417
-rw-r--r--silx/gui/plot/tools/test/__init__.py52
-rw-r--r--silx/gui/plot/tools/test/testCurveLegendsWidget.py125
-rw-r--r--silx/gui/plot/tools/test/testProfile.py673
-rw-r--r--silx/gui/plot/tools/test/testROI.py694
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py196
-rw-r--r--silx/gui/plot/tools/test/testTools.py147
-rw-r--r--silx/gui/plot/tools/toolbars.py362
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()