summaryrefslogtreecommitdiff
path: root/silx/gui/plot/items/roi.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/items/roi.py')
-rw-r--r--silx/gui/plot/items/roi.py3025
1 files changed, 2182 insertions, 843 deletions
diff --git a/silx/gui/plot/items/roi.py b/silx/gui/plot/items/roi.py
index dcad943..ff73fe6 100644
--- a/silx/gui/plot/items/roi.py
+++ b/silx/gui/plot/items/roi.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+# 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
@@ -23,6 +23,10 @@
#
# ###########################################################################*/
"""This module provides ROI item for the :class:`~silx.gui.plot.PlotWidget`.
+
+.. inheritance-diagram::
+ silx.gui.plot.items.roi
+ :parts: 1
"""
__authors__ = ["T. Vincent"]
@@ -30,18 +34,21 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import functools
-import itertools
import logging
-import collections
import numpy
+import weakref
+from silx.image.shapes import Polygon
from ....utils.weakref import WeakList
from ... import qt
+from ... import utils
from .. import items
+from ..items import core
from ...colors import rgba
import silx.utils.deprecation
-from silx.utils.proxy import docstring
+from silx.image._boundingbox import _BoundingBox
+from ....utils.proxy import docstring
+from ..utils.intersections import segments_intersection
logger = logging.getLogger(__name__)
@@ -54,6 +61,9 @@ class _RegionOfInterestBase(qt.QObject):
:param str name: The name of the ROI
"""
+ sigAboutToBeRemoved = qt.Signal()
+ """Signal emitted just before this ROI is removed from its manager."""
+
sigItemChanged = qt.Signal(object)
"""Signal emitted when item has changed.
@@ -61,9 +71,9 @@ class _RegionOfInterestBase(qt.QObject):
See :class:`ItemChangedType` for flags description.
"""
- def __init__(self, parent=None, name=''):
- qt.QObject.__init__(self)
- self.__name = str(name)
+ def __init__(self, parent=None):
+ qt.QObject.__init__(self, parent=parent)
+ self.__name = ''
def getName(self):
"""Returns the name of the ROI
@@ -81,18 +91,44 @@ class _RegionOfInterestBase(qt.QObject):
name = str(name)
if self.__name != name:
self.__name = name
- self.sigItemChanged.emit(items.ItemChangedType.NAME)
+ self._updated(items.ItemChangedType.NAME)
+
+ def _updated(self, event=None, checkVisibility=True):
+ """Implement Item mix-in update method by updating the plot items
+
+ See :class:`~silx.gui.plot.items.Item._updated`
+ """
+ self.sigItemChanged.emit(event)
+ def contains(self, position):
+ """Returns True if the `position` is in this ROI.
-class RegionOfInterest(_RegionOfInterestBase):
+ :param tuple[float,float] position: position to check
+ :return: True if the value / point is consider to be in the region of
+ interest.
+ :rtype: bool
+ """
+ raise NotImplementedError("Base class")
+
+
+class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""Object describing a region of interest in a plot.
:param QObject parent:
The RegionOfInterestManager that created this object
"""
- _kind = None
- """Label for this kind of ROI.
+ _DEFAULT_LINEWIDTH = 1.
+ """Default line width of the curve"""
+
+ _DEFAULT_LINESTYLE = '-'
+ """Default line style of the curve"""
+
+ _DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
+ """Default highlight style of the item"""
+
+ ICON, NAME, SHORT_NAME = None, None, None
+ """Metadata to describe the ROI in labels, tooltips and widgets
Should be set by inherited classes to custom the ROI manager widget.
"""
@@ -100,50 +136,125 @@ class RegionOfInterest(_RegionOfInterestBase):
sigRegionChanged = qt.Signal()
"""Signal emitted everytime the shape or position of the ROI changes"""
+ sigEditingStarted = qt.Signal()
+ """Signal emitted when the user start editing the roi"""
+
+ sigEditingFinished = qt.Signal()
+ """Signal emitted when the region edition is finished. During edition
+ sigEditionChanged will be emitted several times and
+ sigRegionEditionFinished only at end"""
+
def __init__(self, parent=None):
- # Avoid circular dependancy
+ # Avoid circular dependency
from ..tools import roi as roi_tools
assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
- _RegionOfInterestBase.__init__(self, parent, '')
+ _RegionOfInterestBase.__init__(self, parent)
+ core.HighlightedMixIn.__init__(self)
self._color = rgba('red')
- self._items = WeakList()
- self._editAnchors = WeakList()
- self._points = None
- self._labelItem = None
self._editable = False
+ self._selectable = False
+ self._focusProxy = None
self._visible = True
- self.sigItemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, event):
- """Handle name change"""
- if event == items.ItemChangedType.NAME:
- self._updateLabelItem(self.getName())
-
- def __del__(self):
- # Clean-up plot items
- self._removePlotItems()
+ self._child = WeakList()
+
+ def _connectToPlot(self, plot):
+ """Called after connection to a plot"""
+ for item in self.getItems():
+ # This hack is needed to avoid reentrant call from _disconnectFromPlot
+ # to the ROI manager. It also speed up the item tests in _itemRemoved
+ item._roiGroup = True
+ plot.addItem(item)
+
+ def _disconnectFromPlot(self, plot):
+ """Called before disconnection from a plot"""
+ for item in self.getItems():
+ # The item could be already be removed by the plot
+ if item.getPlot() is not None:
+ del item._roiGroup
+ plot.removeItem(item)
+
+ def _setItemName(self, item):
+ """Helper to generate a unique id to a plot item"""
+ legend = "__ROI-%d__%d" % (id(self), id(item))
+ item.setName(legend)
def setParent(self, parent):
"""Set the parent of the RegionOfInterest
- :param Union[None,RegionOfInterestManager] parent:
+ :param Union[None,RegionOfInterestManager] parent: The new parent
"""
- # Avoid circular dependancy
+ # Avoid circular dependency
from ..tools import roi as roi_tools
if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
raise ValueError('Unsupported parent')
- self._removePlotItems()
+ previousParent = self.parent()
+ if previousParent is not None:
+ previousPlot = previousParent.parent()
+ if previousPlot is not None:
+ self._disconnectFromPlot(previousPlot)
super(RegionOfInterest, self).setParent(parent)
- self._createPlotItems()
+ if parent is not None:
+ plot = parent.parent()
+ if plot is not None:
+ self._connectToPlot(plot)
+
+ def addItem(self, item):
+ """Add an item to the set of this ROI children.
+
+ This item will be added and removed to the plot used by the ROI.
+
+ If the ROI is already part of a plot, the item will also be added to
+ the plot.
+
+ It the item do not have a name already, a unique one is generated to
+ avoid item collision in the plot.
+
+ :param silx.gui.plot.items.Item item: A plot item
+ """
+ assert item is not None
+ self._child.append(item)
+ if item.getName() == '':
+ self._setItemName(item)
+ manager = self.parent()
+ if manager is not None:
+ plot = manager.parent()
+ if plot is not None:
+ item._roiGroup = True
+ plot.addItem(item)
+
+ def removeItem(self, item):
+ """Remove an item from this ROI children.
+
+ If the item is part of a plot it will be removed too.
+
+ :param silx.gui.plot.items.Item item: A plot item
+ """
+ assert item is not None
+ self._child.remove(item)
+ plot = item.getPlot()
+ if plot is not None:
+ del item._roiGroup
+ plot.removeItem(item)
+
+ def getItems(self):
+ """Returns the list of PlotWidget items of this RegionOfInterest.
+
+ :rtype: List[~silx.gui.plot.items.Item]
+ """
+ return tuple(self._child)
@classmethod
- def _getKind(cls):
+ def _getShortName(cls):
"""Return an human readable kind of ROI
:rtype: str
"""
- return cls._kind
+ if hasattr(cls, "SHORT_NAME"):
+ name = cls.SHORT_NAME
+ if name is None:
+ name = cls.__name__
+ return name
def getColor(self):
"""Returns the color of this ROI
@@ -152,14 +263,6 @@ class RegionOfInterest(_RegionOfInterestBase):
"""
return qt.QColor.fromRgbF(*self._color)
- def _getAnchorColor(self, color):
- """Returns the anchor color from the base ROI color
-
- :param Union[numpy.array,Tuple,List]: color
- :rtype: Union[numpy.array,Tuple,List]
- """
- return color[:3] + (0.5,)
-
def setColor(self, color):
"""Set the color used for this ROI.
@@ -169,22 +272,7 @@ class RegionOfInterest(_RegionOfInterestBase):
color = rgba(color)
if color != self._color:
self._color = color
-
- # Update color of shape items in the plot
- rgbaColor = rgba(color)
- for item in list(self._items):
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
- item = self._getLabelItem()
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
-
- rgbaColor = self._getAnchorColor(rgbaColor)
- for item in list(self._editAnchors):
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
-
- self.sigItemChanged.emit(items.ItemChangedType.COLOR)
+ self._updated(items.ItemChangedType.COLOR)
@silx.utils.deprecation.deprecated(reason='API modification',
replacement='getName()',
@@ -222,10 +310,50 @@ class RegionOfInterest(_RegionOfInterestBase):
editable = bool(editable)
if self._editable != editable:
self._editable = editable
- # Recreate plot items
- # This can be avoided once marker.setDraggable is public
- self._createPlotItems()
- self.sigItemChanged.emit(items.ItemChangedType.EDITABLE)
+ self._updated(items.ItemChangedType.EDITABLE)
+
+ def isSelectable(self):
+ """Returns whether the ROI is selectable by the user or not.
+
+ :rtype: bool
+ """
+ return self._selectable
+
+ def setSelectable(self, selectable):
+ """Set whether the ROI can be selected interactively.
+
+ :param bool selectable: True to allow selection by the user,
+ False to disable.
+ """
+ selectable = bool(selectable)
+ if self._selectable != selectable:
+ self._selectable = selectable
+ self._updated(items.ItemChangedType.SELECTABLE)
+
+ def getFocusProxy(self):
+ """Returns the ROI which have to be selected when this ROI is selected,
+ else None if no proxy specified.
+
+ :rtype: RegionOfInterest
+ """
+ proxy = self._focusProxy
+ if proxy is None:
+ return None
+ proxy = proxy()
+ if proxy is None:
+ self._focusProxy = None
+ return proxy
+
+ def setFocusProxy(self, roi):
+ """Set the real ROI which will be selected when this ROI is selected,
+ else None to remove the proxy already specified.
+
+ :param RegionOfInterest roi: A ROI
+ """
+ if roi is not None:
+ self._focusProxy = weakref.ref(roi)
+ else:
+ self._focusProxy = None
def isVisible(self):
"""Returns whether the ROI is visible in the plot.
@@ -249,21 +377,7 @@ class RegionOfInterest(_RegionOfInterestBase):
visible = bool(visible)
if self._visible != visible:
self._visible = visible
- if self._labelItem is not None:
- self._labelItem.setVisible(visible)
- for item in self._items + self._editAnchors:
- item.setVisible(visible)
- self.sigItemChanged.emit(items.ItemChangedType.VISIBLE)
-
- def _getControlPoints(self):
- """Returns the current ROI control points.
-
- It returns an empty tuple if there is currently no ROI.
-
- :return: Array of (x, y) position in plot coordinates
- :rtype: numpy.ndarray
- """
- return None if self._points is None else numpy.array(self._points)
+ self._updated(items.ItemChangedType.VISIBLE)
@classmethod
def showFirstInteractionShape(cls):
@@ -272,7 +386,7 @@ class RegionOfInterest(_RegionOfInterestBase):
:rtype: bool
"""
- return True
+ return False
@classmethod
def getFirstInteractionShape(cls):
@@ -291,226 +405,369 @@ class RegionOfInterest(_RegionOfInterestBase):
This interaction is constrained by the plot API and only supports few
shapes.
"""
- points = self._createControlPointsFromFirstShape(points)
- self._setControlPoints(points)
-
- def _createControlPointsFromFirstShape(self, points):
- """Returns the list of control points from the very first shape
- provided.
+ raise NotImplementedError()
- This shape is provided by the plot interaction and constained by the
- class of the ROI itself.
+ def creationStarted(self):
+ """"Called when the ROI creation interaction was started.
"""
- return points
+ pass
- def _setControlPoints(self, points):
- """Set this ROI control points.
+ @docstring(_RegionOfInterestBase)
+ def contains(self, position):
+ raise NotImplementedError("Base class")
- :param points: Iterable of (x, y) control points
+ def creationFinalized(self):
+ """"Called when the ROI creation interaction was finalized.
"""
- points = numpy.array(points)
+ pass
- nbPointsChanged = (self._points is None or
- points.shape != self._points.shape)
+ def _updateItemProperty(self, event, source, destination):
+ """Update the item property of a destination from an item source.
- if nbPointsChanged or not numpy.all(numpy.equal(points, self._points)):
- self._points = points
-
- self._updateShape()
- if self._items and not nbPointsChanged: # Update plot items
- item = self._getLabelItem()
- if item is not None:
- markerPos = self._getLabelPosition()
- item.setPosition(*markerPos)
-
- if self._editAnchors: # Update anchors
- for anchor, point in zip(self._editAnchors, points):
- old = anchor.blockSignals(True)
- anchor.setPosition(*point)
- anchor.blockSignals(old)
-
- else: # No items or new point added
- # re-create plot items
- self._createPlotItems()
-
- self.sigRegionChanged.emit()
-
- def _updateShape(self):
- """Called when shape must be updated.
-
- Must be reimplemented if a shape item have to be updated.
+ :param items.ItemChangedType event: Property type to update
+ :param silx.gui.plot.items.Item source: The reference for the data
+ :param event Union[Item,List[Item]] destination: The item(s) to update
"""
- return
-
- def _getLabelPosition(self):
- """Compute position of the label
+ if not isinstance(destination, (list, tuple)):
+ destination = [destination]
+ if event == items.ItemChangedType.NAME:
+ value = source.getName()
+ for d in destination:
+ d.setName(value)
+ elif event == items.ItemChangedType.EDITABLE:
+ value = source.isEditable()
+ for d in destination:
+ d.setEditable(value)
+ elif event == items.ItemChangedType.SELECTABLE:
+ value = source.isSelectable()
+ for d in destination:
+ d._setSelectable(value)
+ elif event == items.ItemChangedType.COLOR:
+ value = rgba(source.getColor())
+ for d in destination:
+ d.setColor(value)
+ elif event == items.ItemChangedType.LINE_STYLE:
+ value = self.getLineStyle()
+ for d in destination:
+ d.setLineStyle(value)
+ elif event == items.ItemChangedType.LINE_WIDTH:
+ value = self.getLineWidth()
+ for d in destination:
+ d.setLineWidth(value)
+ elif event == items.ItemChangedType.SYMBOL:
+ value = self.getSymbol()
+ for d in destination:
+ d.setSymbol(value)
+ elif event == items.ItemChangedType.SYMBOL_SIZE:
+ value = self.getSymbolSize()
+ for d in destination:
+ d.setSymbolSize(value)
+ elif event == items.ItemChangedType.VISIBLE:
+ value = self.isVisible()
+ for d in destination:
+ d.setVisible(value)
+ else:
+ assert False
- :return: (x, y) position of the marker
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.HIGHLIGHTED:
+ style = self.getCurrentStyle()
+ self._updatedStyle(event, style)
+ else:
+ hilighted = self.isHighlighted()
+ if hilighted:
+ if event == items.ItemChangedType.HIGHLIGHTED_STYLE:
+ style = self.getCurrentStyle()
+ self._updatedStyle(event, style)
+ else:
+ if event in [items.ItemChangedType.COLOR,
+ items.ItemChangedType.LINE_STYLE,
+ items.ItemChangedType.LINE_WIDTH,
+ items.ItemChangedType.SYMBOL,
+ items.ItemChangedType.SYMBOL_SIZE]:
+ style = self.getCurrentStyle()
+ self._updatedStyle(event, style)
+ super(RegionOfInterest, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ """Called when the current displayed style of the ROI was changed.
+
+ :param event: The event responsible of the change of the style
+ :param items.CurveStyle style: The current style
"""
- return None
+ pass
+
+ def getCurrentStyle(self):
+ """Returns the current curve style.
- def _createPlotItems(self):
- """Create items displaying the ROI in the plot.
+ Curve style depends on curve highlighting
- It first removes any existing plot items.
+ :rtype: CurveStyle
"""
- roiManager = self.parent()
- if roiManager is None:
- return
- plot = roiManager.parent()
+ baseColor = rgba(self.getColor())
+ if isinstance(self, core.LineMixIn):
+ baseLinestyle = self.getLineStyle()
+ baseLinewidth = self.getLineWidth()
+ else:
+ baseLinestyle = self._DEFAULT_LINESTYLE
+ baseLinewidth = self._DEFAULT_LINEWIDTH
+ if isinstance(self, core.SymbolMixIn):
+ baseSymbol = self.getSymbol()
+ baseSymbolsize = self.getSymbolSize()
+ else:
+ baseSymbol = 'o'
+ baseSymbolsize = 1
+
+ if self.isHighlighted():
+ style = self.getHighlightedStyle()
+ color = style.getColor()
+ linestyle = style.getLineStyle()
+ linewidth = style.getLineWidth()
+ symbol = style.getSymbol()
+ symbolsize = style.getSymbolSize()
+
+ return items.CurveStyle(
+ color=baseColor if color is None else color,
+ linestyle=baseLinestyle if linestyle is None else linestyle,
+ linewidth=baseLinewidth if linewidth is None else linewidth,
+ symbol=baseSymbol if symbol is None else symbol,
+ symbolsize=baseSymbolsize if symbolsize is None else symbolsize)
+ else:
+ return items.CurveStyle(color=baseColor,
+ linestyle=baseLinestyle,
+ linewidth=baseLinewidth,
+ symbol=baseSymbol,
+ symbolsize=baseSymbolsize)
- self._removePlotItems()
+ def _editingStarted(self):
+ assert self._editable is True
+ self.sigEditingStarted.emit()
- legendPrefix = "__RegionOfInterest-%d__" % id(self)
- itemIndex = 0
+ def _editingFinished(self):
+ self.sigEditingFinished.emit()
- controlPoints = self._getControlPoints()
- if self._labelItem is None:
- self._labelItem = self._createLabelItem()
- if self._labelItem is not None:
- self._labelItem._setLegend(legendPrefix + "label")
- plot._add(self._labelItem)
- self._labelItem.setVisible(self.isVisible())
+class HandleBasedROI(RegionOfInterest):
+ """Manage a ROI based on a set of handles"""
- self._items = WeakList()
- plotItems = self._createShapeItems(controlPoints)
- for item in plotItems:
- item._setLegend(legendPrefix + str(itemIndex))
- plot._add(item)
- item.setVisible(self.isVisible())
- self._items.append(item)
- itemIndex += 1
+ def __init__(self, parent=None):
+ RegionOfInterest.__init__(self, parent=parent)
+ self._handles = []
+ self._posOrigin = None
+ self._posPrevious = None
- self._editAnchors = WeakList()
- if self.isEditable():
- plotItems = self._createAnchorItems(controlPoints)
- color = rgba(self.getColor())
- color = self._getAnchorColor(color)
- for index, item in enumerate(plotItems):
- item._setLegend(legendPrefix + str(itemIndex))
- item.setColor(color)
- item.setVisible(self.isVisible())
- plot._add(item)
- item.sigItemChanged.connect(functools.partial(
- self._controlPointAnchorChanged, index))
- self._editAnchors.append(item)
- itemIndex += 1
+ def addUserHandle(self, item=None):
+ """
+ Add a new free handle to the ROI.
- def _updateLabelItem(self, label):
- """Update the marker displaying the label.
+ This handle do nothing. It have to be managed by the ROI
+ implementing this class.
- Inherite this method to custom the way the ROI display the label.
+ :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
+ add, else None to create a default marker.
+ :rtype: silx.gui.plot.items.Marker
+ """
+ return self.addHandle(item, role="user")
- :param str label: The new label to use
+ def addLabelHandle(self, item=None):
"""
- item = self._getLabelItem()
- if item is not None:
- item.setText(label)
+ Add a new label handle to the ROI.
- def _createLabelItem(self):
- """Returns a created marker which will be used to dipslay the label of
- this ROI.
+ This handle is not draggable nor selectable.
- Inherite this method to return nothing if no new items have to be
- created, or your own marker.
+ It is displayed without symbol, but it is always visible anyway
+ the ROI is editable, in order to display text.
- :rtype: Union[None,Marker]
+ :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
+ add, else None to create a default marker.
+ :rtype: silx.gui.plot.items.Marker
"""
- # Add label marker
- markerPos = self._getLabelPosition()
- marker = items.Marker()
- marker.setPosition(*markerPos)
- marker.setText(self.getName())
- marker.setColor(rgba(self.getColor()))
- marker.setSymbol('')
- marker._setDraggable(False)
- return marker
+ return self.addHandle(item, role="label")
- def _getLabelItem(self):
- """Returns the marker displaying the label of this ROI.
-
- Inherite this method to choose your own item. In case this item is also
- a control point.
+ def addTranslateHandle(self, item=None):
"""
- return self._labelItem
+ Add a new translate handle to the ROI.
- def _createShapeItems(self, points):
- """Create shape items from the current control points.
+ Dragging translate handles affect the position position of the ROI
+ but not the shape itself.
- :rtype: List[PlotItem]
+ :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
+ add, else None to create a default marker.
+ :rtype: silx.gui.plot.items.Marker
"""
- return []
-
- def _createAnchorItems(self, points):
- """Create anchor items from the current control points.
+ return self.addHandle(item, role="translate")
- :rtype: List[Marker]
+ def addHandle(self, item=None, role="default"):
"""
- return []
+ Add a new handle to the ROI.
- def _controlPointAnchorChanged(self, index, event):
- """Handle update of position of an edition anchor
+ Dragging handles while affect the position or the shape of the
+ ROI.
- :param int index: Index of the anchor
- :param ItemChangedType event: Event type
+ :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
+ add, else None to create a default marker.
+ :rtype: silx.gui.plot.items.Marker
+ """
+ if item is None:
+ item = items.Marker()
+ color = rgba(self.getColor())
+ color = self._computeHandleColor(color)
+ item.setColor(color)
+ if role == "default":
+ item.setSymbol("s")
+ elif role == "user":
+ pass
+ elif role == "translate":
+ item.setSymbol("+")
+ elif role == "label":
+ item.setSymbol("")
+
+ if role == "user":
+ pass
+ elif role == "label":
+ item._setSelectable(False)
+ item._setDraggable(False)
+ item.setVisible(True)
+ else:
+ self.__updateEditable(item, self.isEditable(), remove=False)
+ item._setSelectable(False)
+
+ self._handles.append((item, role))
+ self.addItem(item)
+ return item
+
+ def removeHandle(self, handle):
+ data = [d for d in self._handles if d[0] is handle][0]
+ self._handles.remove(data)
+ role = data[1]
+ if role not in ["user", "label"]:
+ if self.isEditable():
+ self.__updateEditable(handle, False)
+ self.removeItem(handle)
+
+ def getHandles(self):
+ """Returns the list of handles of this HandleBasedROI.
+
+ :rtype: List[~silx.gui.plot.items.Marker]
"""
- if event == items.ItemChangedType.POSITION:
- anchor = self._editAnchors[index]
- previous = self._points[index].copy()
- current = anchor.getPosition()
- self._controlPointAnchorPositionChanged(index, current, previous)
+ return tuple(data[0] for data in self._handles)
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- """Called when an anchor is manually edited.
+ def _updated(self, event=None, checkVisibility=True):
+ """Implement Item mix-in update method by updating the plot items
- This function have to be inherited to change the behaviours of the
- control points. This function have to call :meth:`_getControlPoints` to
- reach the previous state of the control points. Updated the positions
- of the changed control points. Then call :meth:`_setControlPoints` to
- update the anchors and send signals.
+ See :class:`~silx.gui.plot.items.Item._updated`
"""
- points = self._getControlPoints()
- points[index] = current
- self._setControlPoints(points)
+ if event == items.ItemChangedType.NAME:
+ self._updateText(self.getName())
+ elif event == items.ItemChangedType.VISIBLE:
+ for item, role in self._handles:
+ visible = self.isVisible()
+ editionVisible = visible and self.isEditable()
+ if role not in ["user", "label"]:
+ item.setVisible(editionVisible)
+ else:
+ item.setVisible(visible)
+ elif event == items.ItemChangedType.EDITABLE:
+ for item, role in self._handles:
+ editable = self.isEditable()
+ if role not in ["user", "label"]:
+ self.__updateEditable(item, editable)
+ super(HandleBasedROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super(HandleBasedROI, self)._updatedStyle(event, style)
+
+ # Update color of shape items in the plot
+ color = rgba(self.getColor())
+ handleColor = self._computeHandleColor(color)
+ for item, role in self._handles:
+ if role == 'user':
+ pass
+ elif role == 'label':
+ item.setColor(color)
+ else:
+ item.setColor(handleColor)
+
+ def __updateEditable(self, handle, editable, remove=True):
+ # NOTE: visibility change emit a position update event
+ handle.setVisible(editable and self.isVisible())
+ handle._setDraggable(editable)
+ if editable:
+ handle.sigDragStarted.connect(self._handleEditingStarted)
+ handle.sigItemChanged.connect(self._handleEditingUpdated)
+ handle.sigDragFinished.connect(self._handleEditingFinished)
+ else:
+ if remove:
+ handle.sigDragStarted.disconnect(self._handleEditingStarted)
+ handle.sigItemChanged.disconnect(self._handleEditingUpdated)
+ handle.sigDragFinished.disconnect(self._handleEditingFinished)
+
+ def _handleEditingStarted(self):
+ super(HandleBasedROI, self)._editingStarted()
+ handle = self.sender()
+ self._posOrigin = numpy.array(handle.getPosition())
+ self._posPrevious = numpy.array(self._posOrigin)
+ self.handleDragStarted(handle, self._posOrigin)
+
+ def _handleEditingUpdated(self):
+ if self._posOrigin is None:
+ # Avoid to handle events when visibility change
+ return
+ handle = self.sender()
+ current = numpy.array(handle.getPosition())
+ self.handleDragUpdated(handle, self._posOrigin, self._posPrevious, current)
+ self._posPrevious = current
+
+ def _handleEditingFinished(self):
+ handle = self.sender()
+ current = numpy.array(handle.getPosition())
+ self.handleDragFinished(handle, self._posOrigin, current)
+ self._posPrevious = None
+ self._posOrigin = None
+ super(HandleBasedROI, self)._editingFinished()
+
+ def isHandleBeingDragged(self):
+ """Returns True if one of the handles is currently being dragged.
- def _removePlotItems(self):
- """Remove items from their plot."""
- for item in itertools.chain(list(self._items),
- list(self._editAnchors)):
+ :rtype: bool
+ """
+ return self._posOrigin is not None
- plot = item.getPlot()
- if plot is not None:
- plot._remove(item)
- self._items = WeakList()
- self._editAnchors = WeakList()
+ def handleDragStarted(self, handle, origin):
+ """Called when an handler drag started"""
+ pass
- if self._labelItem is not None:
- item = self._labelItem
- plot = item.getPlot()
- if plot is not None:
- plot._remove(item)
- self._labelItem = None
+ def handleDragUpdated(self, handle, origin, previous, current):
+ """Called when an handle drag position changed"""
+ pass
- def _updated(self, event=None, checkVisibility=True):
- """Implement Item mix-in update method by updating the plot items
+ def handleDragFinished(self, handle, origin, current):
+ """Called when an handle drag finished"""
+ pass
- See :class:`~silx.gui.plot.items.Item._updated`
+ def _computeHandleColor(self, color):
+ """Returns the anchor color from the base ROI color
+
+ :param Union[numpy.array,Tuple,List]: color
+ :rtype: Union[numpy.array,Tuple,List]
"""
- self._createPlotItems()
+ return color[:3] + (0.5,)
- def __str__(self):
- """Returns parameters of the ROI as a string."""
- points = self._getControlPoints()
- params = '; '.join('(%f; %f)' % (pt[0], pt[1]) for pt in points)
- return "%s(%s)" % (self.__class__.__name__, params)
+ def _updateText(self, text):
+ """Update the text displayed by this ROI
+
+ :param str text: A text
+ """
+ pass
class PointROI(RegionOfInterest, items.SymbolMixIn):
"""A ROI identifying a point in a 2D plot."""
- _kind = "Point"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-point'
+ NAME = 'point markers'
+ SHORT_NAME = "point"
+ """Metadata for this kind of ROI"""
_plotShape = "point"
"""Plot shape which is used for the first interaction"""
@@ -522,82 +779,186 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
"""
def __init__(self, parent=None):
- items.SymbolMixIn.__init__(self)
RegionOfInterest.__init__(self, parent=parent)
+ items.SymbolMixIn.__init__(self)
+ self._marker = items.Marker()
+ self._marker.sigItemChanged.connect(self._pointPositionChanged)
+ self._marker.setSymbol(self._DEFAULT_SYMBOL)
+ self._marker.sigDragStarted.connect(self._editingStarted)
+ self._marker.sigDragFinished.connect(self._editingFinished)
+ self.addItem(self._marker)
+
+ def setFirstShapePoints(self, points):
+ self.setPosition(points[0])
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.NAME:
+ label = self.getName()
+ self._marker.setText(label)
+ elif event == items.ItemChangedType.EDITABLE:
+ self._marker._setDraggable(self.isEditable())
+ elif event in [items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SELECTABLE]:
+ self._updateItemProperty(event, self, self._marker)
+ super(PointROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ self._marker.setColor(style.getColor())
def getPosition(self):
"""Returns the position of this ROI
:rtype: numpy.ndarray
"""
- return self._points[0].copy()
+ return self._marker.getPosition()
def setPosition(self, pos):
"""Set the position of this ROI
:param numpy.ndarray pos: 2d-coordinate of this point
"""
- controlPoints = numpy.array([pos])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
+ self._marker.setPosition(*pos)
- def _updateLabelItem(self, label):
- self._items[0].setText(label)
-
- def _updateShape(self):
- if len(self._items) > 0:
- controlPoints = self._getControlPoints()
- item = self._items[0]
- item.setPosition(*controlPoints[0])
+ @docstring(_RegionOfInterestBase)
+ def contains(self, position):
+ raise NotImplementedError('Base class')
- def __positionChanged(self, event):
+ def _pointPositionChanged(self, event):
"""Handle position changed events of the marker"""
if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- if isinstance(marker, items.Marker):
- self.setPosition(marker.getPosition())
-
- def _createShapeItems(self, points):
- marker = items.Marker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getName())
- marker.setSymbol(self.getSymbol())
- marker.setSymbolSize(self.getSymbolSize())
- marker.setColor(rgba(self.getColor()))
- marker._setDraggable(self.isEditable())
- if self.isEditable():
- marker.sigItemChanged.connect(self.__positionChanged)
- return [marker]
+ self.sigRegionChanged.emit()
def __str__(self):
- points = self._getControlPoints()
- params = '%f %f' % (points[0, 0], points[0, 1])
+ params = '%f %f' % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
-class LineROI(RegionOfInterest, items.LineMixIn):
+class CrossROI(HandleBasedROI, items.LineMixIn):
+ """A ROI identifying a point in a 2D plot and displayed as a cross
+ """
+
+ ICON = 'add-shape-cross'
+ NAME = 'cross marker'
+ SHORT_NAME = "cross"
+ """Metadata for this kind of ROI"""
+
+ _plotShape = "point"
+ """Plot shape which is used for the first interaction"""
+
+ def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
+ items.LineMixIn.__init__(self)
+ self._handle = self.addHandle()
+ self._handle.sigItemChanged.connect(self._handlePositionChanged)
+ self._handleLabel = self.addLabelHandle()
+ self._vmarker = self.addUserHandle(items.YMarker())
+ self._vmarker._setSelectable(False)
+ self._vmarker._setDraggable(False)
+ self._vmarker.setPosition(*self.getPosition())
+ self._hmarker = self.addUserHandle(items.XMarker())
+ self._hmarker._setSelectable(False)
+ self._hmarker._setDraggable(False)
+ self._hmarker.setPosition(*self.getPosition())
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event in [items.ItemChangedType.VISIBLE]:
+ markers = (self._vmarker, self._hmarker)
+ self._updateItemProperty(event, self, markers)
+ super(CrossROI, self)._updated(event, checkVisibility)
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
+
+ def _updatedStyle(self, event, style):
+ super(CrossROI, self)._updatedStyle(event, style)
+ for marker in [self._vmarker, self._hmarker]:
+ marker.setColor(style.getColor())
+ marker.setLineStyle(style.getLineStyle())
+ marker.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ pos = points[0]
+ self.setPosition(pos)
+
+ def getPosition(self):
+ """Returns the position of this ROI
+
+ :rtype: numpy.ndarray
+ """
+ return self._handle.getPosition()
+
+ def setPosition(self, pos):
+ """Set the position of this ROI
+
+ :param numpy.ndarray pos: 2d-coordinate of this point
+ """
+ self._handle.setPosition(*pos)
+
+ def _handlePositionChanged(self, event):
+ """Handle center marker position updates"""
+ if event is items.ItemChangedType.POSITION:
+ position = self.getPosition()
+ self._handleLabel.setPosition(*position)
+ self._vmarker.setPosition(*position)
+ self._hmarker.setPosition(*position)
+ self.sigRegionChanged.emit()
+
+ @docstring(HandleBasedROI)
+ def contains(self, position):
+ roiPos = self.getPosition()
+ return position[0] == roiPos[0] or position[1] == roiPos[1]
+
+
+class LineROI(HandleBasedROI, items.LineMixIn):
"""A ROI identifying a line in a 2D plot.
This ROI provides 1 anchor for each boundary of the line, plus an center
in the center to translate the full ROI.
"""
- _kind = "Line"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-diagonal'
+ NAME = 'line ROI'
+ SHORT_NAME = "line"
+ """Metadata for this kind of ROI"""
_plotShape = "line"
"""Plot shape which is used for the first interaction"""
def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
items.LineMixIn.__init__(self)
- RegionOfInterest.__init__(self, parent=parent)
+ self._handleStart = self.addHandle()
+ self._handleEnd = self.addHandle()
+ self._handleCenter = self.addTranslateHandle()
+ self._handleLabel = self.addLabelHandle()
+
+ shape = items.Shape("polylines")
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setColor(rgba(self.getColor()))
+ shape.setFill(False)
+ shape.setOverlay(True)
+ shape.setLineStyle(self.getLineStyle())
+ shape.setLineWidth(self.getLineWidth())
+ self.__shape = shape
+ self.addItem(shape)
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.VISIBLE:
+ self._updateItemProperty(event, self, self.__shape)
+ super(LineROI, self)._updated(event, checkVisibility)
- def _createControlPointsFromFirstShape(self, points):
- center = numpy.mean(points, axis=0)
- controlPoints = numpy.array([points[0], points[1], center])
- return controlPoints
+ def _updatedStyle(self, event, style):
+ super(LineROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ assert len(points) == 2
+ self.setEndPoints(points[0], points[1])
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
def setEndPoints(self, startPoint, endPoint):
"""Set this line location using the ending points
@@ -605,88 +966,83 @@ class LineROI(RegionOfInterest, items.LineMixIn):
:param numpy.ndarray startPoint: Staring bounding point of the line
:param numpy.ndarray endPoint: Ending bounding point of the line
"""
- assert(startPoint.shape == (2,) and endPoint.shape == (2,))
- shapePoints = numpy.array([startPoint, endPoint])
- controlPoints = self._createControlPointsFromFirstShape(shapePoints)
- self._setControlPoints(controlPoints)
+ if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
+ self.__updateEndPoints(startPoint, endPoint)
+
+ def __updateEndPoints(self, startPoint, endPoint):
+ """Update marker and shape to match given end points
+
+ :param numpy.ndarray startPoint: Staring bounding point of the line
+ :param numpy.ndarray endPoint: Ending bounding point of the line
+ """
+ startPoint = numpy.array(startPoint)
+ endPoint = numpy.array(endPoint)
+ center = (startPoint + endPoint) * 0.5
+
+ with utils.blockSignals(self._handleStart):
+ self._handleStart.setPosition(startPoint[0], startPoint[1])
+ with utils.blockSignals(self._handleEnd):
+ self._handleEnd.setPosition(endPoint[0], endPoint[1])
+ with utils.blockSignals(self._handleCenter):
+ self._handleCenter.setPosition(center[0], center[1])
+ with utils.blockSignals(self._handleLabel):
+ self._handleLabel.setPosition(center[0], center[1])
+
+ line = numpy.array((startPoint, endPoint))
+ self.__shape.setPoints(line)
+ self.sigRegionChanged.emit()
def getEndPoints(self):
"""Returns bounding points of this ROI.
:rtype: Tuple(numpy.ndarray,numpy.ndarray)
"""
- startPoint = self._points[0].copy()
- endPoint = self._points[1].copy()
+ startPoint = numpy.array(self._handleStart.getPosition())
+ endPoint = numpy.array(self._handleEnd.getPosition())
return (startPoint, endPoint)
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points[-1]
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _getShapeFromControlPoints(self, points):
- # Remove the center from the control points
- return points[0:2]
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("polylines")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- item.setLineStyle(self.getLineStyle())
- item.setLineWidth(self.getLineWidth())
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- for point in points[0:-1]:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- # Add an anchor to the center of the rectangle
- center = numpy.mean(points, axis=0)
- anchor = items.Marker()
- anchor.setPosition(*center)
- anchor.setText('')
- anchor.setSymbol('+')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- return anchors
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- if index == len(self._editAnchors) - 1:
- # It is the center anchor
- points = self._getControlPoints()
- center = numpy.mean(points[0:-1], axis=0)
- offset = current - previous
- points[-1] = current
- points[0:-1] = points[0:-1] + offset
- self._setControlPoints(points)
- else:
- # Update the center
- points = self._getControlPoints()
- points[index] = current
- center = numpy.mean(points[0:-1], axis=0)
- points[-1] = center
- self._setControlPoints(points)
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle is self._handleStart:
+ _start, end = self.getEndPoints()
+ self.__updateEndPoints(current, end)
+ elif handle is self._handleEnd:
+ start, _end = self.getEndPoints()
+ self.__updateEndPoints(start, current)
+ elif handle is self._handleCenter:
+ start, end = self.getEndPoints()
+ delta = current - previous
+ start += delta
+ end += delta
+ self.setEndPoints(start, end)
+
+ @docstring(_RegionOfInterestBase)
+ def contains(self, position):
+ bottom_left = position[0], position[1]
+ bottom_right = position[0] + 1, position[1]
+ top_left = position[0], position[1] + 1
+ top_right = position[0] + 1, position[1] + 1
+
+ line_pt1 = self._points[0]
+ line_pt2 = self._points[1]
+
+ bb1 = _BoundingBox.from_points(self._points)
+ if bb1.contains(position) is False:
+ return False
+
+ return (
+ segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_left, seg2_end_pt=bottom_right) or
+ segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_right, seg2_end_pt=top_right) or
+ segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
+ seg2_start_pt=top_right, seg2_end_pt=top_left) or
+ segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
+ seg2_start_pt=top_left, seg2_end_pt=bottom_left)
+ )
def __str__(self):
- points = self._getControlPoints()
- params = points[0][0], points[0][1], points[1][0], points[1][1]
+ start, end = self.getEndPoints()
+ params = start[0], start[1], end[0], end[1]
params = 'start: %f %f; end: %f %f' % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -694,199 +1050,230 @@ class LineROI(RegionOfInterest, items.LineMixIn):
class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal line in a 2D plot."""
- _kind = "HLine"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-horizontal'
+ NAME = 'horizontal line ROI'
+ SHORT_NAME = "hline"
+ """Metadata for this kind of ROI"""
_plotShape = "hline"
"""Plot shape which is used for the first interaction"""
def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
RegionOfInterest.__init__(self, parent=parent)
+ items.LineMixIn.__init__(self)
+ self._marker = items.YMarker()
+ self._marker.sigItemChanged.connect(self._linePositionChanged)
+ self._marker.sigDragStarted.connect(self._editingStarted)
+ self._marker.sigDragFinished.connect(self._editingFinished)
+ self.addItem(self._marker)
- def _createControlPointsFromFirstShape(self, points):
- points = numpy.array([(float('nan'), points[0, 1])],
- dtype=numpy.float64)
- return points
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.NAME:
+ label = self.getName()
+ self._marker.setText(label)
+ elif event == items.ItemChangedType.EDITABLE:
+ self._marker._setDraggable(self.isEditable())
+ elif event in [items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SELECTABLE]:
+ self._updateItemProperty(event, self, self._marker)
+ super(HorizontalLineROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ self._marker.setColor(style.getColor())
+ self._marker.setLineStyle(style.getLineStyle())
+ self._marker.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ pos = points[0, 1]
+ if pos == self.getPosition():
+ return
+ self.setPosition(pos)
def getPosition(self):
"""Returns the position of this line if the horizontal axis
:rtype: float
"""
- return self._points[0, 1]
+ pos = self._marker.getPosition()
+ return pos[1]
def setPosition(self, pos):
"""Set the position of this ROI
:param float pos: Horizontal position of this line
"""
- controlPoints = numpy.array([[float('nan'), pos]])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
-
- def _updateLabelItem(self, label):
- self._items[0].setText(label)
+ self._marker.setPosition(0, pos)
- def _updateShape(self):
- if len(self._items) > 0:
- controlPoints = self._getControlPoints()
- item = self._items[0]
- item.setPosition(*controlPoints[0])
+ @docstring(_RegionOfInterestBase)
+ def contains(self, position):
+ return position[1] == self.getPosition()[1]
- def __positionChanged(self, event):
+ def _linePositionChanged(self, event):
"""Handle position changed events of the marker"""
if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- if isinstance(marker, items.YMarker):
- self.setPosition(marker.getYPosition())
-
- def _createShapeItems(self, points):
- marker = items.YMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getName())
- marker.setColor(rgba(self.getColor()))
- marker.setLineWidth(self.getLineWidth())
- marker.setLineStyle(self.getLineStyle())
- marker._setDraggable(self.isEditable())
- if self.isEditable():
- marker.sigItemChanged.connect(self.__positionChanged)
- return [marker]
+ self.sigRegionChanged.emit()
def __str__(self):
- points = self._getControlPoints()
- params = 'y: %f' % points[0, 1]
+ params = 'y: %f' % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
class VerticalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying a vertical line in a 2D plot."""
- _kind = "VLine"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-vertical'
+ NAME = 'vertical line ROI'
+ SHORT_NAME = "vline"
+ """Metadata for this kind of ROI"""
_plotShape = "vline"
"""Plot shape which is used for the first interaction"""
def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
RegionOfInterest.__init__(self, parent=parent)
+ items.LineMixIn.__init__(self)
+ self._marker = items.XMarker()
+ self._marker.sigItemChanged.connect(self._linePositionChanged)
+ self._marker.sigDragStarted.connect(self._editingStarted)
+ self._marker.sigDragFinished.connect(self._editingFinished)
+ self.addItem(self._marker)
- def _createControlPointsFromFirstShape(self, points):
- points = numpy.array([(points[0, 0], float('nan'))],
- dtype=numpy.float64)
- return points
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.NAME:
+ label = self.getName()
+ self._marker.setText(label)
+ elif event == items.ItemChangedType.EDITABLE:
+ self._marker._setDraggable(self.isEditable())
+ elif event in [items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SELECTABLE]:
+ self._updateItemProperty(event, self, self._marker)
+ super(VerticalLineROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ self._marker.setColor(style.getColor())
+ self._marker.setLineStyle(style.getLineStyle())
+ self._marker.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ pos = points[0, 0]
+ self.setPosition(pos)
def getPosition(self):
"""Returns the position of this line if the horizontal axis
:rtype: float
"""
- return self._points[0, 0]
+ pos = self._marker.getPosition()
+ return pos[0]
def setPosition(self, pos):
"""Set the position of this ROI
:param float pos: Horizontal position of this line
"""
- controlPoints = numpy.array([[pos, float('nan')]])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
+ self._marker.setPosition(pos, 0)
- def _updateLabelItem(self, label):
- self._items[0].setText(label)
+ @docstring(RegionOfInterest)
+ def contains(self, position):
+ return position[0] == self.getPosition()[0]
- def _updateShape(self):
- if len(self._items) > 0:
- controlPoints = self._getControlPoints()
- item = self._items[0]
- item.setPosition(*controlPoints[0])
-
- def __positionChanged(self, event):
+ def _linePositionChanged(self, event):
"""Handle position changed events of the marker"""
if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- if isinstance(marker, items.XMarker):
- self.setPosition(marker.getXPosition())
-
- def _createShapeItems(self, points):
- marker = items.XMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getName())
- marker.setColor(rgba(self.getColor()))
- marker.setLineWidth(self.getLineWidth())
- marker.setLineStyle(self.getLineStyle())
- marker._setDraggable(self.isEditable())
- if self.isEditable():
- marker.sigItemChanged.connect(self.__positionChanged)
- return [marker]
+ self.sigRegionChanged.emit()
def __str__(self):
- points = self._getControlPoints()
- params = 'x: %f' % points[0, 0]
+ params = 'x: %f' % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
-class RectangleROI(RegionOfInterest, items.LineMixIn):
+class RectangleROI(HandleBasedROI, items.LineMixIn):
"""A ROI identifying a rectangle in a 2D plot.
This ROI provides 1 anchor for each corner, plus an anchor in the
center to translate the full ROI.
"""
- _kind = "Rectangle"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-rectangle'
+ NAME = 'rectangle ROI'
+ SHORT_NAME = "rectangle"
+ """Metadata for this kind of ROI"""
_plotShape = "rectangle"
"""Plot shape which is used for the first interaction"""
def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
items.LineMixIn.__init__(self)
- RegionOfInterest.__init__(self, parent=parent)
+ self._handleTopLeft = self.addHandle()
+ self._handleTopRight = self.addHandle()
+ self._handleBottomLeft = self.addHandle()
+ self._handleBottomRight = self.addHandle()
+ self._handleCenter = self.addTranslateHandle()
+ self._handleLabel = self.addLabelHandle()
+
+ shape = items.Shape("rectangle")
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setFill(False)
+ shape.setOverlay(True)
+ shape.setLineStyle(self.getLineStyle())
+ shape.setLineWidth(self.getLineWidth())
+ shape.setColor(rgba(self.getColor()))
+ self.__shape = shape
+ self.addItem(shape)
- def _createControlPointsFromFirstShape(self, points):
- point0 = points[0]
- point1 = points[1]
+ def _updated(self, event=None, checkVisibility=True):
+ if event in [items.ItemChangedType.VISIBLE]:
+ self._updateItemProperty(event, self, self.__shape)
+ super(RectangleROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super(RectangleROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ assert len(points) == 2
+ self._setBound(points)
- # 4 corners
- controlPoints = numpy.array([
- point0[0], point0[1],
- point0[0], point1[1],
- point1[0], point1[1],
- point1[0], point0[1],
- ])
- # Central
- center = numpy.mean(points, axis=0)
- controlPoints = numpy.append(controlPoints, center)
- controlPoints.shape = -1, 2
- return controlPoints
+ def _setBound(self, points):
+ """Initialize the rectangle from a bunch of points"""
+ top = max(points[:, 1])
+ bottom = min(points[:, 1])
+ left = min(points[:, 0])
+ right = max(points[:, 0])
+ size = right - left, top - bottom
+ self._updateGeometry(origin=(left, bottom), size=size)
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
def getCenter(self):
"""Returns the central point of this rectangle
:rtype: numpy.ndarray([float,float])
"""
- return numpy.mean(self._points, axis=0)
+ pos = self._handleCenter.getPosition()
+ return numpy.array(pos)
def getOrigin(self):
"""Returns the corner point with the smaller coordinates
:rtype: numpy.ndarray([float,float])
"""
- return numpy.min(self._points, axis=0)
+ pos = self._handleBottomLeft.getPosition()
+ return numpy.array(pos)
def getSize(self):
"""Returns the size of this rectangle
:rtype: numpy.ndarray([float,float])
"""
- minPoint = numpy.min(self._points, axis=0)
- maxPoint = numpy.max(self._points, axis=0)
- return maxPoint - minPoint
+ vmin = self._handleBottomLeft.getPosition()
+ vmax = self._handleTopRight.getPosition()
+ vmin, vmax = numpy.array(vmin), numpy.array(vmax)
+ return vmax - vmin
def setOrigin(self, position):
"""Set the origin position of this ROI
@@ -915,93 +1302,80 @@ class RectangleROI(RegionOfInterest, items.LineMixIn):
def setGeometry(self, origin=None, size=None, center=None):
"""Set the geometry of the ROI
"""
+ if ((origin is None or numpy.array_equal(origin, self.getOrigin())) and
+ (center is None or numpy.array_equal(center, self.getCenter())) and
+ numpy.array_equal(size, self.getSize())):
+ return # Nothing has changed
+
+ self._updateGeometry(origin, size, center)
+
+ def _updateGeometry(self, origin=None, size=None, center=None):
+ """Forced update of the geometry of the ROI"""
if origin is not None:
origin = numpy.array(origin)
size = numpy.array(size)
points = numpy.array([origin, origin + size])
- controlPoints = self._createControlPointsFromFirstShape(points)
+ center = origin + size * 0.5
elif center is not None:
center = numpy.array(center)
size = numpy.array(size)
points = numpy.array([center - size * 0.5, center + size * 0.5])
- controlPoints = self._createControlPointsFromFirstShape(points)
else:
- raise ValueError("Origin or cengter expected")
- self._setControlPoints(controlPoints)
-
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points.min(axis=0)
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _getShapeFromControlPoints(self, points):
- minPoint = points.min(axis=0)
- maxPoint = points.max(axis=0)
- return numpy.array([minPoint, maxPoint])
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("rectangle")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- item.setLineStyle(self.getLineStyle())
- item.setLineWidth(self.getLineWidth())
- return [item]
-
- def _createAnchorItems(self, points):
- # Remove the center control point
- points = points[0:-1]
-
- anchors = []
- for point in points:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- # Add an anchor to the center of the rectangle
- center = numpy.mean(points, axis=0)
- anchor = items.Marker()
- anchor.setPosition(*center)
- anchor.setText('')
- anchor.setSymbol('+')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- return anchors
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- if index == len(self._editAnchors) - 1:
+ raise ValueError("Origin or center expected")
+
+ with utils.blockSignals(self._handleBottomLeft):
+ self._handleBottomLeft.setPosition(points[0, 0], points[0, 1])
+ with utils.blockSignals(self._handleBottomRight):
+ self._handleBottomRight.setPosition(points[1, 0], points[0, 1])
+ with utils.blockSignals(self._handleTopLeft):
+ self._handleTopLeft.setPosition(points[0, 0], points[1, 1])
+ with utils.blockSignals(self._handleTopRight):
+ self._handleTopRight.setPosition(points[1, 0], points[1, 1])
+ with utils.blockSignals(self._handleCenter):
+ self._handleCenter.setPosition(center[0], center[1])
+ with utils.blockSignals(self._handleLabel):
+ self._handleLabel.setPosition(points[0, 0], points[0, 1])
+
+ self.__shape.setPoints(points)
+ self.sigRegionChanged.emit()
+
+ @docstring(HandleBasedROI)
+ def contains(self, position):
+ assert isinstance(position, (tuple, list, numpy.array))
+ points = self.__shape.getPoints()
+ bb1 = _BoundingBox.from_points(points)
+ return bb1.contains(position)
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle is self._handleCenter:
# It is the center anchor
- points = self._getControlPoints()
- center = numpy.mean(points[0:-1], axis=0)
- offset = current - previous
- points[-1] = current
- points[0:-1] = points[0:-1] + offset
- self._setControlPoints(points)
+ size = self.getSize()
+ self._updateGeometry(center=current, size=size)
else:
- # Fix other corners
- constrains = [(1, 3), (0, 2), (3, 1), (2, 0)]
- constrains = constrains[index]
- points = self._getControlPoints()
- points[index] = current
- points[constrains[0]][0] = current[0]
- points[constrains[1]][1] = current[1]
- # Update the center
- center = numpy.mean(points[0:-1], axis=0)
- points[-1] = center
- self._setControlPoints(points)
+ opposed = {
+ self._handleBottomLeft: self._handleTopRight,
+ self._handleTopRight: self._handleBottomLeft,
+ self._handleBottomRight: self._handleTopLeft,
+ self._handleTopLeft: self._handleBottomRight,
+ }
+ handle2 = opposed[handle]
+ current2 = handle2.getPosition()
+ points = numpy.array([current, current2])
+
+ # Switch handles if they were crossed by interaction
+ if self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition():
+ self._handleBottomLeft, self._handleBottomRight = self._handleBottomRight, self._handleBottomLeft
+
+ if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition():
+ self._handleTopLeft, self._handleTopRight = self._handleTopRight, self._handleTopLeft
+
+ if self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition():
+ self._handleBottomLeft, self._handleTopLeft = self._handleTopLeft, self._handleBottomLeft
+
+ if self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition():
+ self._handleBottomRight, self._handleTopRight = self._handleTopRight, self._handleBottomRight
+
+ self._setBound(points)
def __str__(self):
origin = self.getOrigin()
@@ -1011,21 +1385,504 @@ class RectangleROI(RegionOfInterest, items.LineMixIn):
return "%s(%s)" % (self.__class__.__name__, params)
-class PolygonROI(RegionOfInterest, items.LineMixIn):
+class CircleROI(HandleBasedROI, items.LineMixIn):
+ """A ROI identifying a circle in a 2D plot.
+
+ This ROI provides 1 anchor at the center to translate the circle,
+ and one anchor on the perimeter to change the radius.
+ """
+
+ ICON = 'add-shape-circle'
+ NAME = 'circle ROI'
+ SHORT_NAME = "circle"
+ """Metadata for this kind of ROI"""
+
+ _kind = "Circle"
+ """Label for this kind of ROI"""
+
+ _plotShape = "line"
+ """Plot shape which is used for the first interaction"""
+
+ def __init__(self, parent=None):
+ items.LineMixIn.__init__(self)
+ HandleBasedROI.__init__(self, parent=parent)
+ self._handlePerimeter = self.addHandle()
+ self._handleCenter = self.addTranslateHandle()
+ self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
+ self._handleLabel = self.addLabelHandle()
+
+ shape = items.Shape("polygon")
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setColor(rgba(self.getColor()))
+ shape.setFill(False)
+ shape.setOverlay(True)
+ shape.setLineStyle(self.getLineStyle())
+ shape.setLineWidth(self.getLineWidth())
+ self.__shape = shape
+ self.addItem(shape)
+
+ self.__radius = 0
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.VISIBLE:
+ self._updateItemProperty(event, self, self.__shape)
+ super(CircleROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super(CircleROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ assert len(points) == 2
+ self._setRay(points)
+
+ def _setRay(self, points):
+ """Initialize the circle from the center point and a
+ perimeter point."""
+ center = points[0]
+ radius = numpy.linalg.norm(points[0] - points[1])
+ self.setGeometry(center=center, radius=radius)
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
+
+ def getCenter(self):
+ """Returns the central point of this rectangle
+
+ :rtype: numpy.ndarray([float,float])
+ """
+ pos = self._handleCenter.getPosition()
+ return numpy.array(pos)
+
+ def getRadius(self):
+ """Returns the radius of this circle
+
+ :rtype: float
+ """
+ return self.__radius
+
+ def setCenter(self, position):
+ """Set the center point of this ROI
+
+ :param numpy.ndarray position: Location of the center of the circle
+ """
+ self._handleCenter.setPosition(*position)
+
+ def setRadius(self, radius):
+ """Set the size of this ROI
+
+ :param float size: Radius of the circle
+ """
+ radius = float(radius)
+ if radius != self.__radius:
+ self.__radius = radius
+ self._updateGeometry()
+
+ def setGeometry(self, center, radius):
+ """Set the geometry of the ROI
+ """
+ if numpy.array_equal(center, self.getCenter()):
+ self.setRadius(radius)
+ else:
+ self.__radius = float(radius) # Update radius directly
+ self.setCenter(center) # Calls _updateGeometry
+
+ def _updateGeometry(self):
+ """Update the handles and shape according to given parameters"""
+ center = self.getCenter()
+ perimeter_point = numpy.array([center[0] + self.__radius, center[1]])
+
+ self._handlePerimeter.setPosition(perimeter_point[0], perimeter_point[1])
+ self._handleLabel.setPosition(center[0], center[1])
+
+ nbpoints = 27
+ angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
+ circleShape = numpy.array((numpy.cos(angles) * self.__radius,
+ numpy.sin(angles) * self.__radius)).T
+ circleShape += center
+ self.__shape.setPoints(circleShape)
+ self.sigRegionChanged.emit()
+
+ def _centerPositionChanged(self, event):
+ """Handle position changed events of the center marker"""
+ if event is items.ItemChangedType.POSITION:
+ self._updateGeometry()
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle is self._handlePerimeter:
+ center = self.getCenter()
+ self.setRadius(numpy.linalg.norm(center - current))
+
+ def __str__(self):
+ center = self.getCenter()
+ radius = self.getRadius()
+ params = center[0], center[1], radius
+ params = 'center: %f %f; radius: %f;' % params
+ return "%s(%s)" % (self.__class__.__name__, params)
+
+
+class EllipseROI(HandleBasedROI, items.LineMixIn):
+ """A ROI identifying an oriented ellipse in a 2D plot.
+
+ This ROI provides 1 anchor at the center to translate the circle,
+ and two anchors on the perimeter to modify the major-radius and
+ minor-radius. These two anchors also allow to change the orientation.
+ """
+
+ ICON = 'add-shape-ellipse'
+ NAME = 'ellipse ROI'
+ SHORT_NAME = "ellipse"
+ """Metadata for this kind of ROI"""
+
+ _plotShape = "line"
+ """Plot shape which is used for the first interaction"""
+
+ def __init__(self, parent=None):
+ items.LineMixIn.__init__(self)
+ HandleBasedROI.__init__(self, parent=parent)
+ self._handleAxis0 = self.addHandle()
+ self._handleAxis1 = self.addHandle()
+ self._handleCenter = self.addTranslateHandle()
+ self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
+ self._handleLabel = self.addLabelHandle()
+
+ shape = items.Shape("polygon")
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setColor(rgba(self.getColor()))
+ shape.setFill(False)
+ shape.setOverlay(True)
+ shape.setLineStyle(self.getLineStyle())
+ shape.setLineWidth(self.getLineWidth())
+ self.__shape = shape
+ self.addItem(shape)
+
+ self._radius = 0., 0.
+ self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.VISIBLE:
+ self._updateItemProperty(event, self, self.__shape)
+ super(EllipseROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super(EllipseROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ assert len(points) == 2
+ self._setRay(points)
+
+ @staticmethod
+ def _calculateOrientation(p0, p1):
+ """return angle in radians between the vector p0-p1
+ and the X axis
+
+ :param p0: first point coordinates (x, y)
+ :param p1: second point coordinates
+ :return:
+ """
+ vector = (p1[0] - p0[0], p1[1] - p0[1])
+ x_unit_vector = (1, 0)
+ norm = numpy.linalg.norm(vector)
+ if norm != 0:
+ theta = numpy.arccos(numpy.dot(vector, x_unit_vector) / norm)
+ else:
+ theta = 0
+ if vector[1] < 0:
+ # arccos always returns values in range [0, pi]
+ theta = 2 * numpy.pi - theta
+ return theta
+
+ def _setRay(self, points):
+ """Initialize the circle from the center point and a
+ perimeter point."""
+ center = points[0]
+ radius = numpy.linalg.norm(points[0] - points[1])
+ orientation = self._calculateOrientation(points[0], points[1])
+ self.setGeometry(center=center,
+ radius=(radius, radius),
+ orientation=orientation)
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
+
+ def getCenter(self):
+ """Returns the central point of this rectangle
+
+ :rtype: numpy.ndarray([float,float])
+ """
+ pos = self._handleCenter.getPosition()
+ return numpy.array(pos)
+
+ def getMajorRadius(self):
+ """Returns the half-diameter of the major axis.
+
+ :rtype: float
+ """
+ return max(self._radius)
+
+ def getMinorRadius(self):
+ """Returns the half-diameter of the minor axis.
+
+ :rtype: float
+ """
+ return min(self._radius)
+
+ def getOrientation(self):
+ """Return angle in radians between the horizontal (X) axis
+ and the major axis of the ellipse in [0, 2*pi[
+
+ :rtype: float:
+ """
+ return self._orientation
+
+ def setCenter(self, center):
+ """Set the center point of this ROI
+
+ :param numpy.ndarray position: Coordinates (X, Y) of the center
+ of the ellipse
+ """
+ self._handleCenter.setPosition(*center)
+
+ def setMajorRadius(self, radius):
+ """Set the half-diameter of the major axis of the ellipse.
+
+ :param float radius:
+ Major radius of the ellipsis. Must be a positive value.
+ """
+ if self._radius[0] > self._radius[1]:
+ newRadius = radius, self._radius[1]
+ else:
+ newRadius = self._radius[0], radius
+ self.setGeometry(radius=newRadius)
+
+ def setMinorRadius(self, radius):
+ """Set the half-diameter of the minor axis of the ellipse.
+
+ :param float radius:
+ Minor radius of the ellipsis. Must be a positive value.
+ """
+ if self._radius[0] > self._radius[1]:
+ newRadius = self._radius[0], radius
+ else:
+ newRadius = radius, self._radius[1]
+ self.setGeometry(radius=newRadius)
+
+ def setOrientation(self, orientation):
+ """Rotate the ellipse
+
+ :param float orientation: Angle in radians between the horizontal and
+ the major axis.
+ :return:
+ """
+ self.setGeometry(orientation=orientation)
+
+ def setGeometry(self, center=None, radius=None, orientation=None):
+ """
+
+ :param center: (X, Y) coordinates
+ :param float majorRadius:
+ :param float minorRadius:
+ :param float orientation: angle in radians between the major axis and the
+ horizontal
+ :return:
+ """
+ if center is None:
+ center = self.getCenter()
+
+ if radius is None:
+ radius = self._radius
+ else:
+ radius = float(radius[0]), float(radius[1])
+
+ if orientation is None:
+ orientation = self._orientation
+ else:
+ # ensure that we store the orientation in range [0, 2*pi
+ orientation = numpy.mod(orientation, 2 * numpy.pi)
+
+ if (numpy.array_equal(center, self.getCenter()) or
+ radius != self._radius or
+ orientation != self._orientation):
+
+ # Update parameters directly
+ self._radius = radius
+ self._orientation = orientation
+
+ if numpy.array_equal(center, self.getCenter()):
+ self._updateGeometry()
+ else:
+ # This will call _updateGeometry
+ self.setCenter(center)
+
+ def _updateGeometry(self):
+ """Update shape and markers"""
+ center = self.getCenter()
+
+ orientation = self.getOrientation()
+ if self._radius[1] > self._radius[0]:
+ # _handleAxis1 is the major axis
+ orientation -= numpy.pi/2
+
+ point0 = numpy.array([center[0] + self._radius[0] * numpy.cos(orientation),
+ center[1] + self._radius[0] * numpy.sin(orientation)])
+ point1 = numpy.array([center[0] - self._radius[1] * numpy.sin(orientation),
+ center[1] + self._radius[1] * numpy.cos(orientation)])
+ with utils.blockSignals(self._handleAxis0):
+ self._handleAxis0.setPosition(*point0)
+ with utils.blockSignals(self._handleAxis1):
+ self._handleAxis1.setPosition(*point1)
+ with utils.blockSignals(self._handleLabel):
+ self._handleLabel.setPosition(*center)
+
+ nbpoints = 27
+ angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
+ X = (self._radius[0] * numpy.cos(angles) * numpy.cos(orientation)
+ - self._radius[1] * numpy.sin(angles) * numpy.sin(orientation))
+ Y = (self._radius[0] * numpy.cos(angles) * numpy.sin(orientation)
+ + self._radius[1] * numpy.sin(angles) * numpy.cos(orientation))
+
+ ellipseShape = numpy.array((X, Y)).T
+ ellipseShape += center
+ self.__shape.setPoints(ellipseShape)
+ self.sigRegionChanged.emit()
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle in (self._handleAxis0, self._handleAxis1):
+ center = self.getCenter()
+ orientation = self._calculateOrientation(center, current)
+ distance = numpy.linalg.norm(center - current)
+
+ if handle is self._handleAxis1:
+ if self._radius[0] > distance:
+ # _handleAxis1 is not the major axis, rotate -90 degrees
+ orientation -= numpy.pi/2
+ radius = self._radius[0], distance
+
+ else: # _handleAxis0
+ if self._radius[1] > distance:
+ # _handleAxis0 is not the major axis, rotate +90 degrees
+ orientation += numpy.pi/2
+ radius = distance, self._radius[1]
+
+ self.setGeometry(radius=radius, orientation=orientation)
+
+ def _centerPositionChanged(self, event):
+ """Handle position changed events of the center marker"""
+ if event is items.ItemChangedType.POSITION:
+ self._updateGeometry()
+
+ def __str__(self):
+ center = self.getCenter()
+ major = self.getMajorRadius()
+ minor = self.getMinorRadius()
+ orientation = self.getOrientation()
+ params = center[0], center[1], major, minor, orientation
+ params = 'center: %f %f; major radius: %f: minor radius: %f; orientation: %f' % params
+ return "%s(%s)" % (self.__class__.__name__, params)
+
+
+class PolygonROI(HandleBasedROI, items.LineMixIn):
"""A ROI identifying a closed polygon in a 2D plot.
This ROI provides 1 anchor for each point of the polygon.
"""
- _kind = "Polygon"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-polygon'
+ NAME = 'polygon ROI'
+ SHORT_NAME = "polygon"
+ """Metadata for this kind of ROI"""
_plotShape = "polygon"
"""Plot shape which is used for the first interaction"""
def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
items.LineMixIn.__init__(self)
- RegionOfInterest.__init__(self, parent=parent)
+ self._handleLabel = self.addLabelHandle()
+ self._handleCenter = self.addTranslateHandle()
+ self._handlePoints = []
+ self._points = numpy.empty((0, 2))
+ self._handleClose = None
+
+ self._polygon_shape = None
+ shape = self.__createShape()
+ self.__shape = shape
+ self.addItem(shape)
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event in [items.ItemChangedType.VISIBLE]:
+ self._updateItemProperty(event, self, self.__shape)
+ super(PolygonROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super(PolygonROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
+ if self._handleClose is not None:
+ color = self._computeHandleColor(style.getColor())
+ self._handleClose.setColor(color)
+
+ def __createShape(self, interaction=False):
+ kind = "polygon" if not interaction else "polylines"
+ shape = items.Shape(kind)
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setFill(False)
+ shape.setOverlay(True)
+ style = self.getCurrentStyle()
+ shape.setLineStyle(style.getLineStyle())
+ shape.setLineWidth(style.getLineWidth())
+ shape.setColor(rgba(style.getColor()))
+ return shape
+
+ def setFirstShapePoints(self, points):
+ if self._handleClose is not None:
+ self._handleClose.setPosition(*points[0])
+ self.setPoints(points)
+
+ def creationStarted(self):
+ """"Called when the ROI creation interaction was started.
+ """
+ # Handle to see where to close the polygon
+ self._handleClose = self.addUserHandle()
+ self._handleClose.setSymbol("o")
+ color = self._computeHandleColor(rgba(self.getColor()))
+ self._handleClose.setColor(color)
+
+ # Hide the center while creating the first shape
+ self._handleCenter.setSymbol("")
+
+ # In interaction replace the polygon by a line, to display something unclosed
+ self.removeItem(self.__shape)
+ self.__shape = self.__createShape(interaction=True)
+ self.__shape.setPoints(self._points)
+ self.addItem(self.__shape)
+
+ def isBeingCreated(self):
+ """Returns true if the ROI is in creation step"""
+ return self._handleClose is not None
+
+ def creationFinalized(self):
+ """"Called when the ROI creation interaction was finalized.
+ """
+ self.removeHandle(self._handleClose)
+ self._handleClose = None
+ self.removeItem(self.__shape)
+ self.__shape = self.__createShape()
+ self.__shape.setPoints(self._points)
+ self.addItem(self.__shape)
+ # Hide the center while creating the first shape
+ self._handleCenter.setSymbol("+")
+ for handle in self._handlePoints:
+ handle.setSymbol("s")
+
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
def getPoints(self):
"""Returns the list of the points of this polygon.
@@ -1040,213 +1897,484 @@ class PolygonROI(RegionOfInterest, items.LineMixIn):
:param numpy.ndarray pos: 2d-coordinate of this point
"""
assert(len(points.shape) == 2 and points.shape[1] == 2)
- if len(points) > 0:
- controlPoints = numpy.array(points)
- else:
- controlPoints = numpy.empty((0, 2))
- self._setControlPoints(controlPoints)
- def _getLabelPosition(self):
- points = self._getControlPoints()
- if len(points) == 0:
- # FIXME: we should return none, this polygon have no location
- return numpy.array([0, 0])
- return points[numpy.argmin(points[:, 1])]
+ if numpy.array_equal(points, self._points):
+ return # Nothing has changed
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- shape.setPoints(points)
+ self._polygon_shape = None
+
+ # Update the needed handles
+ while len(self._handlePoints) != len(points):
+ if len(self._handlePoints) < len(points):
+ handle = self.addHandle()
+ self._handlePoints.append(handle)
+ if self.isBeingCreated():
+ handle.setSymbol("")
+ else:
+ handle = self._handlePoints.pop(-1)
+ self.removeHandle(handle)
+
+ for handle, position in zip(self._handlePoints, points):
+ with utils.blockSignals(handle):
+ handle.setPosition(position[0], position[1])
+
+ if len(points) > 0:
+ if not self.isHandleBeingDragged():
+ vmin = numpy.min(points, axis=0)
+ vmax = numpy.max(points, axis=0)
+ center = (vmax + vmin) * 0.5
+ with utils.blockSignals(self._handleCenter):
+ self._handleCenter.setPosition(center[0], center[1])
+
+ num = numpy.argmin(points[:, 1])
+ pos = points[num]
+ with utils.blockSignals(self._handleLabel):
+ self._handleLabel.setPosition(pos[0], pos[1])
- def _createShapeItems(self, points):
if len(points) == 0:
- return []
+ self._points = numpy.empty((0, 2))
+ else:
+ self._points = points
+ self.__shape.setPoints(self._points)
+ self.sigRegionChanged.emit()
+
+ def translate(self, x, y):
+ points = self.getPoints()
+ delta = numpy.array([x, y])
+ self.setPoints(points)
+ self.setPoints(points + delta)
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle is self._handleCenter:
+ delta = current - previous
+ self.translate(delta[0], delta[1])
else:
- item = items.Shape("polygon")
- item.setPoints(points)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- item.setLineStyle(self.getLineStyle())
- item.setLineWidth(self.getLineWidth())
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- for point in points:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
- return anchors
+ points = self.getPoints()
+ num = self._handlePoints.index(handle)
+ points[num] = current
+ self.setPoints(points)
+
+ def handleDragFinished(self, handle, origin, current):
+ points = self._points
+ if len(points) > 0:
+ # Only update the center at the end
+ # To avoid to disturb the interaction
+ vmin = numpy.min(points, axis=0)
+ vmax = numpy.max(points, axis=0)
+ center = (vmax + vmin) * 0.5
+ with utils.blockSignals(self._handleCenter):
+ self._handleCenter.setPosition(center[0], center[1])
def __str__(self):
- points = self._getControlPoints()
+ points = self._points
params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
return "%s(%s)" % (self.__class__.__name__, params)
+ @docstring(HandleBasedROI)
+ def contains(self, position):
+ bb1 = _BoundingBox.from_points(self.getPoints())
+ if bb1.contains(position) is False:
+ return False
+
+ if self._polygon_shape is None:
+ self._polygon_shape = Polygon(vertices=self.getPoints())
+
+ # warning: both the polygon and the value are inverted
+ return self._polygon_shape.is_inside(row=position[0], col=position[1])
+
+ def _setControlPoints(self, points):
+ RegionOfInterest._setControlPoints(self, points=points)
+ self._polygon_shape = None
-class ArcROI(RegionOfInterest, items.LineMixIn):
+
+class ArcROI(HandleBasedROI, items.LineMixIn):
"""A ROI identifying an arc of a circle with a width.
- This ROI provides 3 anchors to control the curvature, 1 anchor to control
- the weigth, and 1 anchor to translate the shape.
+ This ROI provides
+ - 3 handle to control the curvature
+ - 1 handle to control the weight
+ - 1 anchor to translate the shape.
"""
- _kind = "Arc"
- """Label for this kind of ROI"""
+ ICON = 'add-shape-arc'
+ NAME = 'arc ROI'
+ SHORT_NAME = "arc"
+ """Metadata for this kind of ROI"""
_plotShape = "line"
"""Plot shape which is used for the first interaction"""
- _ArcGeometry = collections.namedtuple('ArcGeometry', ['center',
- 'startPoint', 'endPoint',
- 'radius', 'weight',
- 'startAngle', 'endAngle'])
+ class _Geometry:
+ def __init__(self):
+ self.center = None
+ self.startPoint = None
+ self.endPoint = None
+ self.radius = None
+ self.weight = None
+ self.startAngle = None
+ self.endAngle = None
+ self._closed = None
+
+ @classmethod
+ def createEmpty(cls):
+ zero = numpy.array([0, 0])
+ return cls.create(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)
+
+ @classmethod
+ def createRect(cls, startPoint, endPoint, weight):
+ return cls.create(None, startPoint, endPoint, None, weight, None, None, False)
+
+ @classmethod
+ def createCircle(cls, center, startPoint, endPoint, radius,
+ weight, startAngle, endAngle):
+ return cls.create(center, startPoint, endPoint, radius,
+ weight, startAngle, endAngle, True)
+
+ @classmethod
+ def create(cls, center, startPoint, endPoint, radius,
+ weight, startAngle, endAngle, closed=False):
+ g = cls()
+ g.center = center
+ g.startPoint = startPoint
+ g.endPoint = endPoint
+ g.radius = radius
+ g.weight = weight
+ g.startAngle = startAngle
+ g.endAngle = endAngle
+ g._closed = closed
+ return g
+
+ def withWeight(self, weight):
+ """Create a new geometry with another weight
+ """
+ return self.create(self.center, self.startPoint, self.endPoint,
+ self.radius, weight,
+ self.startAngle, self.endAngle, self._closed)
+
+ def withRadius(self, radius):
+ """Create a new geometry with another radius.
+
+ The weight and the center is conserved.
+ """
+ startPoint = self.center + (self.startPoint - self.center) / self.radius * radius
+ endPoint = self.center + (self.endPoint - self.center) / self.radius * radius
+ return self.create(self.center, startPoint, endPoint,
+ radius, self.weight,
+ self.startAngle, self.endAngle, self._closed)
+
+ def translated(self, x, y):
+ delta = numpy.array([x, y])
+ center = None if self.center is None else self.center + delta
+ startPoint = None if self.startPoint is None else self.startPoint + delta
+ endPoint = None if self.endPoint is None else self.endPoint + delta
+ return self.create(center, startPoint, endPoint,
+ self.radius, self.weight,
+ self.startAngle, self.endAngle, self._closed)
+
+ def getKind(self):
+ """Returns the kind of shape defined"""
+ if self.center is None:
+ return "rect"
+ elif numpy.isnan(self.startAngle):
+ return "point"
+ elif self.isClosed():
+ if self.weight <= 0 or self.weight * 0.5 >= self.radius:
+ return "circle"
+ else:
+ return "donut"
+ else:
+ if self.weight * 0.5 < self.radius:
+ return "arc"
+ else:
+ return "camembert"
+
+ def isClosed(self):
+ """Returns True if the geometry is a circle like"""
+ if self._closed is not None:
+ return self._closed
+ delta = numpy.abs(self.endAngle - self.startAngle)
+ self._closed = numpy.isclose(delta, numpy.pi * 2)
+ return self._closed
+
+ def __str__(self):
+ return str((self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed))
def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
items.LineMixIn.__init__(self)
- RegionOfInterest.__init__(self, parent=parent)
- self._geometry = None
+ self._geometry = self._Geometry.createEmpty()
+ self._handleLabel = self.addLabelHandle()
+
+ self._handleStart = self.addHandle()
+ self._handleStart.setSymbol("o")
+ self._handleMid = self.addHandle()
+ self._handleMid.setSymbol("o")
+ self._handleEnd = self.addHandle()
+ self._handleEnd.setSymbol("o")
+ self._handleWeight = self.addHandle()
+ self._handleWeight._setConstraint(self._arcCurvatureMarkerConstraint)
+ self._handleMove = self.addTranslateHandle()
+
+ shape = items.Shape("polygon")
+ shape.setPoints([[0, 0], [0, 0]])
+ shape.setColor(rgba(self.getColor()))
+ shape.setFill(False)
+ shape.setOverlay(True)
+ shape.setLineStyle(self.getLineStyle())
+ shape.setLineWidth(self.getLineWidth())
+ self.__shape = shape
+ self.addItem(shape)
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.VISIBLE:
+ self._updateItemProperty(event, self, self.__shape)
+ super(ArcROI, self)._updated(event, checkVisibility)
- def _getInternalGeometry(self):
- """Returns the object storing the internal geometry of this ROI.
+ def _updatedStyle(self, event, style):
+ super(ArcROI, self)._updatedStyle(event, style)
+ self.__shape.setColor(style.getColor())
+ self.__shape.setLineStyle(style.getLineStyle())
+ self.__shape.setLineWidth(style.getLineWidth())
- This geometry is derived from the control points and cached for
- efficiency. Calling :meth:`_setControlPoints` invalidate the cache.
+ def setFirstShapePoints(self, points):
+ """"Initialize the ROI using the points from the first interaction.
+
+ This interaction is constrained by the plot API and only supports few
+ shapes.
"""
- if self._geometry is None:
- controlPoints = self._getControlPoints()
- self._geometry = self._createGeometryFromControlPoint(controlPoints)
- return self._geometry
+ # The first shape is a line
+ point0 = points[0]
+ point1 = points[1]
- @classmethod
- def showFirstInteractionShape(cls):
- return False
+ # Compute a non collinear point for the curvature
+ center = (point1 + point0) * 0.5
+ normal = point1 - center
+ normal = numpy.array((normal[1], -normal[0]))
+ defaultCurvature = numpy.pi / 5.0
+ weightCoef = 0.20
+ mid = center - normal * defaultCurvature
+ distance = numpy.linalg.norm(point0 - point1)
+ weight = distance * weightCoef
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points.min(axis=0)
+ geometry = self._createGeometryFromControlPoints(point0, mid, point1, weight)
+ self._geometry = geometry
+ self._updateHandles()
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- controlPoints = self._getControlPoints()
- currentWeigth = numpy.linalg.norm(controlPoints[3] - controlPoints[1]) * 2
-
- if index in [0, 2]:
- # Moving start or end will maintain the same curvature
- # Then we have to custom the curvature control point
- startPoint = controlPoints[0]
- endPoint = controlPoints[2]
- center = (startPoint + endPoint) * 0.5
- normal = (endPoint - startPoint)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- # Compute the coeficient which have to be constrained
- if distance != 0:
- normal /= distance
- midVector = controlPoints[1] - center
- constainedCoef = numpy.dot(midVector, normal) / distance
+ def _updateText(self, text):
+ self._handleLabel.setText(text)
+
+ def _updateMidHandle(self):
+ """Keep the same geometry, but update the location of the control
+ points.
+
+ So calling this function do not trigger sigRegionChanged.
+ """
+ geometry = self._geometry
+
+ if geometry.isClosed():
+ start = numpy.array(self._handleStart.getPosition())
+ geometry.endPoint = start
+ with utils.blockSignals(self._handleEnd):
+ self._handleEnd.setPosition(*start)
+ midPos = geometry.center + geometry.center - start
+ else:
+ if geometry.center is None:
+ midPos = geometry.startPoint * 0.66 + geometry.endPoint * 0.34
else:
- constainedCoef = 1.0
-
- # Compute the location of the curvature point
- controlPoints[index] = current
- startPoint = controlPoints[0]
- endPoint = controlPoints[2]
- center = (startPoint + endPoint) * 0.5
- normal = (endPoint - startPoint)
+ midAngle = geometry.startAngle * 0.66 + geometry.endAngle * 0.34
+ vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
+ midPos = geometry.center + geometry.radius * vector
+
+ with utils.blockSignals(self._handleMid):
+ self._handleMid.setPosition(*midPos)
+
+ def _updateWeightHandle(self):
+ geometry = self._geometry
+ if geometry.center is None:
+ # rectangle
+ center = (geometry.startPoint + geometry.endPoint) * 0.5
+ normal = geometry.endPoint - geometry.startPoint
normal = numpy.array((normal[1], -normal[0]))
distance = numpy.linalg.norm(normal)
if distance != 0:
- # BTW we dont need to divide by the distance here
- # Cause we compute normal * distance after all
- normal /= distance
- midPoint = center + normal * constainedCoef * distance
- controlPoints[1] = midPoint
-
- # The weight have to be fixed
- self._updateWeightControlPoint(controlPoints, currentWeigth)
- self._setControlPoints(controlPoints)
-
- elif index == 1:
- # The weight have to be fixed
- controlPoints[index] = current
- self._updateWeightControlPoint(controlPoints, currentWeigth)
- self._setControlPoints(controlPoints)
+ normal = normal / distance
+ weightPos = center + normal * geometry.weight * 0.5
else:
- super(ArcROI, self)._controlPointAnchorPositionChanged(index, current, previous)
+ if geometry.isClosed():
+ midAngle = geometry.startAngle + numpy.pi * 0.5
+ elif geometry.center is not None:
+ midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
+ vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
+ weightPos = geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+
+ with utils.blockSignals(self._handleWeight):
+ self._handleWeight.setPosition(*weightPos)
+
+ def _getWeightFromHandle(self, weightPos):
+ geometry = self._geometry
+ if geometry.center is None:
+ # rectangle
+ center = (geometry.startPoint + geometry.endPoint) * 0.5
+ return numpy.linalg.norm(center - weightPos) * 2
+ else:
+ distance = numpy.linalg.norm(geometry.center - weightPos)
+ return abs(distance - geometry.radius) * 2
- def _updateWeightControlPoint(self, controlPoints, weigth):
- startPoint = controlPoints[0]
- midPoint = controlPoints[1]
- endPoint = controlPoints[2]
- normal = (endPoint - startPoint)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- controlPoints[3] = midPoint + normal * weigth * 0.5
+ def _updateHandles(self):
+ geometry = self._geometry
+ with utils.blockSignals(self._handleStart):
+ self._handleStart.setPosition(*geometry.startPoint)
+ with utils.blockSignals(self._handleEnd):
+ self._handleEnd.setPosition(*geometry.endPoint)
+
+ self._updateMidHandle()
+ self._updateWeightHandle()
- def _createGeometryFromControlPoint(self, controlPoints):
+ self._updateShape()
+
+ def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False):
+ """Update the curvature using 3 control points in the curve
+
+ :param bool updateCurveHandles: If False curve handles are already at
+ the right location
+ """
+ if updateCurveHandles:
+ with utils.blockSignals(self._handleStart):
+ self._handleStart.setPosition(*start)
+ with utils.blockSignals(self._handleMid):
+ self._handleMid.setPosition(*mid)
+ with utils.blockSignals(self._handleEnd):
+ self._handleEnd.setPosition(*end)
+
+ if checkClosed:
+ closed = self._isCloseInPixel(start, end)
+ else:
+ closed = self._geometry.isClosed()
+
+ weight = self._geometry.weight
+ geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
+ self._geometry = geometry
+
+ self._updateWeightHandle()
+ self._updateShape()
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ if handle is self._handleStart:
+ mid = numpy.array(self._handleMid.getPosition())
+ end = numpy.array(self._handleEnd.getPosition())
+ self._updateCurvature(current, mid, end,
+ checkClosed=True, updateCurveHandles=False)
+ elif handle is self._handleMid:
+ if self._geometry.isClosed():
+ radius = numpy.linalg.norm(self._geometry.center - current)
+ self._geometry = self._geometry.withRadius(radius)
+ self._updateHandles()
+ else:
+ start = numpy.array(self._handleStart.getPosition())
+ end = numpy.array(self._handleEnd.getPosition())
+ self._updateCurvature(start, current, end, updateCurveHandles=False)
+ elif handle is self._handleEnd:
+ start = numpy.array(self._handleStart.getPosition())
+ mid = numpy.array(self._handleMid.getPosition())
+ self._updateCurvature(start, mid, current,
+ checkClosed=True, updateCurveHandles=False)
+ elif handle is self._handleWeight:
+ weight = self._getWeightFromHandle(current)
+ self._geometry = self._geometry.withWeight(weight)
+ self._updateShape()
+ elif handle is self._handleMove:
+ delta = current - previous
+ self.translate(*delta)
+
+ def _isCloseInPixel(self, point1, point2):
+ manager = self.parent()
+ if manager is None:
+ return False
+ plot = manager.parent()
+ if plot is None:
+ return False
+ point1 = plot.dataToPixel(*point1)
+ if point1 is None:
+ return False
+ point2 = plot.dataToPixel(*point2)
+ if point2 is None:
+ return False
+ return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
+
+ def _normalizeGeometry(self):
+ """Keep the same phisical geometry, but with normalized parameters.
+ """
+ geometry = self._geometry
+ if geometry.weight * 0.5 >= geometry.radius:
+ radius = (geometry.weight * 0.5 + geometry.radius) * 0.5
+ geometry = geometry.withRadius(radius)
+ geometry = geometry.withWeight(radius * 2)
+ self._geometry = geometry
+ return True
+ return False
+
+ def handleDragFinished(self, handle, origin, current):
+ if handle in [self._handleStart, self._handleMid, self._handleEnd]:
+ if self._normalizeGeometry():
+ self._updateHandles()
+ else:
+ self._updateMidHandle()
+ if self._geometry.isClosed():
+ self._handleStart.setSymbol("x")
+ self._handleEnd.setSymbol("x")
+ else:
+ self._handleStart.setSymbol("o")
+ self._handleEnd.setSymbol("o")
+
+ def _createGeometryFromControlPoints(self, start, mid, end, weight, closed=None):
"""Returns the geometry of the object"""
- weigth = numpy.linalg.norm(controlPoints[3] - controlPoints[1]) * 2
- if numpy.allclose(controlPoints[0], controlPoints[2]):
+ if closed or (closed is None and numpy.allclose(start, end)):
# Special arc: It's a closed circle
- center = (controlPoints[0] + controlPoints[1]) * 0.5
- radius = numpy.linalg.norm(controlPoints[0] - center)
- v = controlPoints[0] - center
+ center = (start + mid) * 0.5
+ radius = numpy.linalg.norm(start - center)
+ v = start - center
startAngle = numpy.angle(complex(v[0], v[1]))
endAngle = startAngle + numpy.pi * 2.0
- return self._ArcGeometry(center, controlPoints[0], controlPoints[2],
- radius, weigth, startAngle, endAngle)
+ return self._Geometry.createCircle(center, start, end, radius,
+ weight, startAngle, endAngle)
- elif numpy.linalg.norm(
- numpy.cross(controlPoints[1] - controlPoints[0],
- controlPoints[2] - controlPoints[0])) < 1e-5:
+ elif numpy.linalg.norm(numpy.cross(mid - start, end - start)) < 1e-5:
# Degenerated arc, it's a rectangle
- return self._ArcGeometry(None, controlPoints[0], controlPoints[2],
- None, weigth, None, None)
+ return self._Geometry.createRect(start, end, weight)
else:
- center, radius = self._circleEquation(*controlPoints[:3])
- v = controlPoints[0] - center
+ center, radius = self._circleEquation(start, mid, end)
+ v = start - center
startAngle = numpy.angle(complex(v[0], v[1]))
- v = controlPoints[1] - center
+ v = mid - center
midAngle = numpy.angle(complex(v[0], v[1]))
- v = controlPoints[2] - center
+ v = end - center
endAngle = numpy.angle(complex(v[0], v[1]))
+
# Is it clockwise or anticlockwise
- if (midAngle - startAngle + 2 * numpy.pi) % (2 * numpy.pi) <= numpy.pi:
+ relativeMid = (endAngle - midAngle + 2 * numpy.pi) % (2 * numpy.pi)
+ relativeEnd = (endAngle - startAngle + 2 * numpy.pi) % (2 * numpy.pi)
+ if relativeMid < relativeEnd:
if endAngle < startAngle:
endAngle += 2 * numpy.pi
else:
if endAngle > startAngle:
endAngle -= 2 * numpy.pi
- return self._ArcGeometry(center, controlPoints[0], controlPoints[2],
- radius, weigth, startAngle, endAngle)
+ return self._Geometry.create(center, start, end,
+ radius, weight, startAngle, endAngle)
- def _isCircle(self, geometry):
- """Returns True if the geometry is a closed circle"""
- delta = numpy.abs(geometry.endAngle - geometry.startAngle)
- return numpy.isclose(delta, numpy.pi * 2)
-
- def _getShapeFromControlPoints(self, controlPoints):
- geometry = self._createGeometryFromControlPoint(controlPoints)
- if geometry.center is None:
+ def _createShapeFromGeometry(self, geometry):
+ kind = geometry.getKind()
+ if kind == "rect":
# It is not an arc
- # but we can display it as an the intermediat shape
+ # but we can display it as an intermediate shape
normal = (geometry.endPoint - geometry.startPoint)
normal = numpy.array((normal[1], -normal[0]))
distance = numpy.linalg.norm(normal)
@@ -1257,15 +2385,40 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
geometry.endPoint + normal * geometry.weight * 0.5,
geometry.endPoint - normal * geometry.weight * 0.5,
geometry.startPoint - normal * geometry.weight * 0.5])
+ elif kind == "point":
+ # It is not an arc
+ # but we can display it as an intermediate shape
+ # NOTE: At least 2 points are expected
+ points = numpy.array([geometry.startPoint, geometry.startPoint])
+ elif kind == "circle":
+ outerRadius = geometry.radius + geometry.weight * 0.5
+ angles = numpy.arange(0, 2 * numpy.pi, 0.1)
+ # It's a circle
+ points = []
+ numpy.append(angles, angles[-1])
+ for angle in angles:
+ direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
+ points.append(geometry.center + direction * outerRadius)
+ points = numpy.array(points)
+ elif kind == "donut":
+ innerRadius = geometry.radius - geometry.weight * 0.5
+ outerRadius = geometry.radius + geometry.weight * 0.5
+ angles = numpy.arange(0, 2 * numpy.pi, 0.1)
+ # It's a donut
+ points = []
+ # NOTE: NaN value allow to create 2 separated circle shapes
+ # using a single plot item. It's a kind of cheat
+ points.append(numpy.array([float("nan"), float("nan")]))
+ for angle in angles:
+ direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
+ points.insert(0, geometry.center + direction * innerRadius)
+ points.append(geometry.center + direction * outerRadius)
+ points.append(numpy.array([float("nan"), float("nan")]))
+ points = numpy.array(points)
else:
innerRadius = geometry.radius - geometry.weight * 0.5
outerRadius = geometry.radius + geometry.weight * 0.5
- if numpy.isnan(geometry.startAngle):
- # Degenerated, it's a point
- # At least 2 points are expected
- return numpy.array([geometry.startPoint, geometry.startPoint])
-
delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
if geometry.startAngle == geometry.endAngle:
# Degenerated, it's a line (single radius)
@@ -1280,57 +2433,58 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
if angles[-1] != geometry.endAngle:
angles = numpy.append(angles, geometry.endAngle)
- isCircle = self._isCircle(geometry)
-
- if isCircle:
- if innerRadius <= 0:
- # It's a circle
- points = []
- numpy.append(angles, angles[-1])
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- else:
- # It's a donut
- points = []
- # NOTE: NaN value allow to create 2 separated circle shapes
- # using a single plot item. It's a kind of cheat
- points.append(numpy.array([float("nan"), float("nan")]))
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.append(numpy.array([float("nan"), float("nan")]))
+ if kind == "camembert":
+ # It's a part of camembert
+ points = []
+ points.append(geometry.center)
+ points.append(geometry.startPoint)
+ delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
+ for angle in angles:
+ direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
+ points.append(geometry.center + direction * outerRadius)
+ points.append(geometry.endPoint)
+ points.append(geometry.center)
+ elif kind == "arc":
+ # It's a part of donut
+ points = []
+ points.append(geometry.startPoint)
+ for angle in angles:
+ direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
+ points.insert(0, geometry.center + direction * innerRadius)
+ points.append(geometry.center + direction * outerRadius)
+ points.insert(0, geometry.endPoint)
+ points.append(geometry.endPoint)
else:
- if innerRadius <= 0:
- # It's a part of camembert
- points = []
- points.append(geometry.center)
- points.append(geometry.startPoint)
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- points.append(geometry.endPoint)
- points.append(geometry.center)
- else:
- # It's a part of donut
- points = []
- points.append(geometry.startPoint)
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.insert(0, geometry.endPoint)
- points.append(geometry.endPoint)
+ assert False
+
points = numpy.array(points)
return points
- def _setControlPoints(self, points):
- # Invalidate the geometry
- self._geometry = None
- RegionOfInterest._setControlPoints(self, points)
+ def _updateShape(self):
+ geometry = self._geometry
+ points = self._createShapeFromGeometry(geometry)
+ self.__shape.setPoints(points)
+
+ index = numpy.nanargmin(points[:, 1])
+ pos = points[index]
+ with utils.blockSignals(self._handleLabel):
+ self._handleLabel.setPosition(pos[0], pos[1])
+
+ if geometry.center is None:
+ movePos = geometry.startPoint * 0.34 + geometry.endPoint * 0.66
+ elif (geometry.isClosed()
+ or abs(geometry.endAngle - geometry.startAngle) > numpy.pi * 0.7):
+ movePos = geometry.center
+ else:
+ moveAngle = geometry.startAngle * 0.34 + geometry.endAngle * 0.66
+ vector = numpy.array([numpy.cos(moveAngle), numpy.sin(moveAngle)])
+ movePos = geometry.center + geometry.radius * vector
+
+ with utils.blockSignals(self._handleMove):
+ self._handleMove.setPosition(*movePos)
+
+ self.sigRegionChanged.emit()
def getGeometry(self):
"""Returns a tuple containing the geometry of this ROI
@@ -1344,7 +2498,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:raise ValueError: In case the ROI can't be represented as section of
a circle
"""
- geometry = self._getInternalGeometry()
+ geometry = self._geometry
if geometry.center is None:
raise ValueError("This ROI can't be represented as a section of circle")
return geometry.center, self.getInnerRadius(), self.getOuterRadius(), geometry.startAngle, geometry.endAngle
@@ -1354,8 +2508,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:rtype: bool
"""
- geometry = self._getInternalGeometry()
- return self._isCircle(geometry)
+ return self._geometry.isClosed()
def getCenter(self):
"""Returns the center of the circle used to draw arcs of this ROI.
@@ -1364,8 +2517,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:rtype: numpy.ndarray
"""
- geometry = self._getInternalGeometry()
- return geometry.center
+ return self._geometry.center
def getStartAngle(self):
"""Returns the angle of the start of the section of this ROI (in radian).
@@ -1375,8 +2527,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:rtype: float
"""
- geometry = self._getInternalGeometry()
- return geometry.startAngle
+ return self._geometry.startAngle
def getEndAngle(self):
"""Returns the angle of the end of the section of this ROI (in radian).
@@ -1386,15 +2537,14 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:rtype: float
"""
- geometry = self._getInternalGeometry()
- return geometry.endAngle
+ return self._geometry.endAngle
def getInnerRadius(self):
"""Returns the radius of the smaller arc used to draw this ROI.
:rtype: float
"""
- geometry = self._getInternalGeometry()
+ geometry = self._geometry
radius = geometry.radius - geometry.weight * 0.5
if radius < 0:
radius = 0
@@ -1405,7 +2555,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
:rtype: float
"""
- geometry = self._getInternalGeometry()
+ geometry = self._geometry
radius = geometry.radius + geometry.weight * 0.5
return radius
@@ -1427,96 +2577,67 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
center = numpy.array(center)
radius = (innerRadius + outerRadius) * 0.5
weight = outerRadius - innerRadius
- geometry = self._ArcGeometry(center, None, None, radius, weight, startAngle, endAngle)
- controlPoints = self._createControlPointsFromGeometry(geometry)
- self._setControlPoints(controlPoints)
-
- def _createControlPointsFromGeometry(self, geometry):
- if geometry.startPoint or geometry.endPoint:
- # Duplication with the angles
- raise NotImplementedError("This general case is not implemented")
-
- angle = geometry.startAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- startPoint = geometry.center + direction * geometry.radius
- angle = geometry.endAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- endPoint = geometry.center + direction * geometry.radius
-
- angle = (geometry.startAngle + geometry.endAngle) * 0.5
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- curvaturePoint = geometry.center + direction * geometry.radius
- weightPoint = curvaturePoint + direction * geometry.weight * 0.5
-
- return numpy.array([startPoint, curvaturePoint, endPoint, weightPoint])
-
- def _createControlPointsFromFirstShape(self, points):
- # The first shape is a line
- point0 = points[0]
- point1 = points[1]
+ vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
+ startPoint = center + vector * radius
+ vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
+ endPoint = center + vector * radius
+
+ geometry = self._Geometry.create(center, startPoint, endPoint,
+ radius, weight,
+ startAngle, endAngle, closed=None)
+ self._geometry = geometry
+ self._updateHandles()
+
+ @docstring(HandleBasedROI)
+ def contains(self, position):
+ # first check distance, fastest
+ center = self.getCenter()
+ distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
+ is_in_distance = self.getInnerRadius() <= distance <= self.getOuterRadius()
+ if not is_in_distance:
+ return False
+ rel_pos = position[1] - center[1], position[0] - center[0]
+ angle = numpy.arctan2(*rel_pos)
+ start_angle = self.getStartAngle()
+ end_angle = self.getEndAngle()
+
+ if start_angle < end_angle:
+ # I never succeed to find a condition where start_angle < end_angle
+ # so this is untested
+ is_in_angle = start_angle <= angle <= end_angle
+ else:
+ if end_angle < -numpy.pi and angle > 0:
+ angle = angle - (numpy.pi *2.0)
+ is_in_angle = end_angle <= angle <= start_angle
+ return is_in_angle
- # Compute a non colineate point for the curvature
- center = (point1 + point0) * 0.5
- normal = point1 - center
- normal = numpy.array((normal[1], -normal[0]))
- defaultCurvature = numpy.pi / 5.0
- defaultWeight = 0.20 # percentage
- curvaturePoint = center - normal * defaultCurvature
- weightPoint = center - normal * defaultCurvature * (1.0 + defaultWeight)
-
- # 3 corners
- controlPoints = numpy.array([
- point0,
- curvaturePoint,
- point1,
- weightPoint
- ])
- return controlPoints
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("polygon")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- item.setLineStyle(self.getLineStyle())
- item.setLineWidth(self.getLineWidth())
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- symbols = ['o', 'o', 'o', 's']
-
- for index, point in enumerate(points):
- if index in [1, 3]:
- constraint = self._arcCurvatureMarkerConstraint
- else:
- constraint = None
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol(symbols[index])
- anchor._setDraggable(True)
- if constraint is not None:
- anchor._setConstraint(constraint)
- anchors.append(anchor)
-
- return anchors
+ def translate(self, x, y):
+ self._geometry = self._geometry.translated(x, y)
+ self._updateHandles()
def _arcCurvatureMarkerConstraint(self, x, y):
- """Curvature marker remains on "mediatrice" """
- start = self._points[0]
- end = self._points[2]
- midPoint = (start + end) / 2.
- normal = (end - start)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- v = numpy.dot(normal, (numpy.array((x, y)) - midPoint))
- x, y = midPoint + v * normal
+ """Curvature marker remains on perpendicular bisector"""
+ geometry = self._geometry
+ if geometry.center is None:
+ center = (geometry.startPoint + geometry.endPoint) * 0.5
+ vector = geometry.startPoint - geometry.endPoint
+ vector = numpy.array((vector[1], -vector[0]))
+ vdist = numpy.linalg.norm(vector)
+ if vdist != 0:
+ normal = numpy.array((vector[1], -vector[0])) / vdist
+ else:
+ normal = numpy.array((0, 0))
+ else:
+ if geometry.isClosed():
+ midAngle = geometry.startAngle + numpy.pi * 0.5
+ else:
+ midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
+ normal = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
+ center = geometry.center
+ dist = numpy.dot(normal, (numpy.array((x, y)) - center))
+ dist = numpy.clip(dist, geometry.radius, geometry.radius * 2)
+ x, y = center + dist * normal
return x, y
@staticmethod
@@ -1530,7 +2651,7 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
w = z - x
w /= y - x
c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
- return ((-c.real, -c.imag), abs(c + x))
+ return numpy.array((-c.real, -c.imag)), abs(c + x)
def __str__(self):
try:
@@ -1540,3 +2661,221 @@ class ArcROI(RegionOfInterest, items.LineMixIn):
except ValueError:
params = "invalid"
return "%s(%s)" % (self.__class__.__name__, params)
+
+
+class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
+ """A ROI identifying an horizontal range in a 1D plot."""
+
+ ICON = 'add-range-horizontal'
+ NAME = 'horizontal range ROI'
+ SHORT_NAME = "hrange"
+
+ _plotShape = "line"
+ """Plot shape which is used for the first interaction"""
+
+ def __init__(self, parent=None):
+ RegionOfInterest.__init__(self, parent=parent)
+ items.LineMixIn.__init__(self)
+ self._markerMin = items.XMarker()
+ self._markerMax = items.XMarker()
+ self._markerCen = items.XMarker()
+ self._markerCen.setLineStyle(" ")
+ self._markerMin._setConstraint(self.__positionMinConstraint)
+ self._markerMax._setConstraint(self.__positionMaxConstraint)
+ self._markerMin.sigDragStarted.connect(self._editingStarted)
+ self._markerMin.sigDragFinished.connect(self._editingFinished)
+ self._markerMax.sigDragStarted.connect(self._editingStarted)
+ self._markerMax.sigDragFinished.connect(self._editingFinished)
+ self._markerCen.sigDragStarted.connect(self._editingStarted)
+ self._markerCen.sigDragFinished.connect(self._editingFinished)
+ self.addItem(self._markerCen)
+ self.addItem(self._markerMin)
+ self.addItem(self._markerMax)
+ self.__filterReentrant = utils.LockReentrant()
+
+ def setFirstShapePoints(self, points):
+ vmin = min(points[:, 0])
+ vmax = max(points[:, 0])
+ self._updatePos(vmin, vmax)
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.NAME:
+ self._updateText()
+ elif event == items.ItemChangedType.EDITABLE:
+ self._updateEditable()
+ self._updateText()
+ elif event == items.ItemChangedType.LINE_STYLE:
+ markers = [self._markerMin, self._markerMax]
+ self._updateItemProperty(event, self, markers)
+ elif event in [items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SELECTABLE]:
+ markers = [self._markerMin, self._markerMax, self._markerCen]
+ self._updateItemProperty(event, self, markers)
+ super(HorizontalRangeROI, self)._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ markers = [self._markerMin, self._markerMax, self._markerCen]
+ for m in markers:
+ m.setColor(style.getColor())
+ m.setLineWidth(style.getLineWidth())
+
+ def _updateText(self):
+ text = self.getName()
+ if self.isEditable():
+ self._markerMin.setText("")
+ self._markerCen.setText(text)
+ else:
+ self._markerMin.setText(text)
+ self._markerCen.setText("")
+
+ def _updateEditable(self):
+ editable = self.isEditable()
+ self._markerMin._setDraggable(editable)
+ self._markerMax._setDraggable(editable)
+ self._markerCen._setDraggable(editable)
+ if self.isEditable():
+ self._markerMin.sigItemChanged.connect(self._minPositionChanged)
+ self._markerMax.sigItemChanged.connect(self._maxPositionChanged)
+ self._markerCen.sigItemChanged.connect(self._cenPositionChanged)
+ self._markerCen.setLineStyle(":")
+ else:
+ self._markerMin.sigItemChanged.disconnect(self._minPositionChanged)
+ self._markerMax.sigItemChanged.disconnect(self._maxPositionChanged)
+ self._markerCen.sigItemChanged.disconnect(self._cenPositionChanged)
+ self._markerCen.setLineStyle(" ")
+
+ def _updatePos(self, vmin, vmax, force=False):
+ """Update marker position and emit signal.
+
+ :param float vmin:
+ :param float vmax:
+ :param bool force:
+ True to update even if already at the right position.
+ """
+ if not force and numpy.array_equal((vmin, vmax), self.getRange()):
+ return # Nothing has changed
+
+ center = (vmin + vmax) * 0.5
+ with self.__filterReentrant:
+ with utils.blockSignals(self._markerMin):
+ self._markerMin.setPosition(vmin, 0)
+ with utils.blockSignals(self._markerCen):
+ self._markerCen.setPosition(center, 0)
+ with utils.blockSignals(self._markerMax):
+ self._markerMax.setPosition(vmax, 0)
+ self.sigRegionChanged.emit()
+
+ def setRange(self, vmin, vmax):
+ """Set the range of this ROI.
+
+ :param float vmin: Staring location of the range
+ :param float vmax: Ending location of the range
+ """
+ if vmin is None or vmax is None:
+ err = "Can't set vmin or vmax to None"
+ raise ValueError(err)
+ if vmin > vmax:
+ err = "Can't set vmin and vmax because vmin >= vmax " \
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ raise ValueError(err)
+ self._updatePos(vmin, vmax)
+
+ def getRange(self):
+ """Returns the range of this ROI.
+
+ :rtype: Tuple[float,float]
+ """
+ vmin = self.getMin()
+ vmax = self.getMax()
+ return vmin, vmax
+
+ def setMin(self, vmin):
+ """Set the min of this ROI.
+
+ :param float vmin: New min
+ """
+ vmax = self.getMax()
+ self._updatePos(vmin, vmax)
+
+ def getMin(self):
+ """Returns the min value of this ROI.
+
+ :rtype: float
+ """
+ return self._markerMin.getPosition()[0]
+
+ def setMax(self, vmax):
+ """Set the max of this ROI.
+
+ :param float vmax: New max
+ """
+ vmin = self.getMin()
+ self._updatePos(vmin, vmax)
+
+ def getMax(self):
+ """Returns the max value of this ROI.
+
+ :rtype: float
+ """
+ return self._markerMax.getPosition()[0]
+
+ def setCenter(self, center):
+ """Set the center of this ROI.
+
+ :param float center: New center
+ """
+ vmin, vmax = self.getRange()
+ previousCenter = (vmin + vmax) * 0.5
+ delta = center - previousCenter
+ self._updatePos(vmin + delta, vmax + delta)
+
+ def getCenter(self):
+ """Returns the center location of this ROI.
+
+ :rtype: float
+ """
+ vmin, vmax = self.getRange()
+ return (vmin + vmax) * 0.5
+
+ def __positionMinConstraint(self, x, y):
+ """Constraint of the min marker"""
+ if self.__filterReentrant.locked():
+ # Ignore the constraint when we set an explicit value
+ return x, y
+ vmax = self.getMax()
+ if vmax is None:
+ return x, y
+ return min(x, vmax), y
+
+ def __positionMaxConstraint(self, x, y):
+ """Constraint of the max marker"""
+ if self.__filterReentrant.locked():
+ # Ignore the constraint when we set an explicit value
+ return x, y
+ vmin = self.getMin()
+ if vmin is None:
+ return x, y
+ return max(x, vmin), y
+
+ def _minPositionChanged(self, event):
+ """Handle position changed events of the marker"""
+ if event is items.ItemChangedType.POSITION:
+ marker = self.sender()
+ self._updatePos(marker.getXPosition(), self.getMax(), force=True)
+
+ def _maxPositionChanged(self, event):
+ """Handle position changed events of the marker"""
+ if event is items.ItemChangedType.POSITION:
+ marker = self.sender()
+ self._updatePos(self.getMin(), marker.getXPosition(), force=True)
+
+ def _cenPositionChanged(self, event):
+ """Handle position changed events of the marker"""
+ if event is items.ItemChangedType.POSITION:
+ marker = self.sender()
+ self.setCenter(marker.getXPosition())
+
+ def __str__(self):
+ vrange = self.getRange()
+ params = 'min: %f; max: %f' % vrange
+ return "%s(%s)" % (self.__class__.__name__, params)