Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.plot.items.roi

    # /*##########################################################################
    #
    # Copyright (c) 2018-2022 European Synchrotron Radiation Facility
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    # THE SOFTWARE.
    #
    # ###########################################################################*/
    """This module provides ROI item for the :class:`~silx.gui.plot.PlotWidget`.

    .. inheritance-diagram::
       silx.gui.plot.items.roi
       :parts: 1
    """

    __authors__ = ["T. Vincent"]
    __license__ = "MIT"
    __date__ = "28/06/2018"

    import logging
    import numpy
    from typing import Tuple

    from ... import utils
    from .. import items
    from ...colors import rgba
    from silx.image.shapes import Polygon
    from silx.image._boundingbox import _BoundingBox
    from ....utils.proxy import docstring
    from ..utils.intersections import segments_intersection
    from ._roi_base import _RegionOfInterestBase

    # He following imports have to be exposed by this module
    from ._roi_base import RegionOfInterest
    from ._roi_base import HandleBasedROI
    from ._arc_roi import ArcROI  # noqa
    from ._band_roi import BandROI  # noqa
    from ._roi_base import InteractionModeMixIn  # noqa
    from ._roi_base import RoiInteractionMode  # noqa

    logger = logging.getLogger(__name__)

    class PointROI(RegionOfInterest, items.SymbolMixIn):
        """A ROI identifying a point in a 2D plot."""

        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"""

        _DEFAULT_SYMBOL = "+"
        """Default symbol of the PointROI

        It overwrite the `SymbolMixIn` class attribte.
        """

        def __init__(self, parent=None):
            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.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 _updateText(self, text: str):
            self._marker.setText(text)

        def _updatedStyle(self, event, style):
            self._marker.setColor(style.getColor())

        def getPosition(self) -> Tuple[float, float]:
            """Returns the position of this ROI"""
            return self._marker.getPosition()

        def setPosition(self, pos):
            """Set the position of this ROI

            :param pos: 2d-coordinate of this point
            """
            self._marker.setPosition(*pos)

        @docstring(_RegionOfInterestBase)
        def contains(self, position):
            roiPos = self.getPosition()
            return position[0] == roiPos[0] and position[1] == roiPos[1]

        def _pointPositionChanged(self, event):
            """Handle position changed events of the marker"""
            if event is items.ItemChangedType.POSITION:
                self.sigRegionChanged.emit()

        def __str__(self):
            params = "%f %f" % self.getPosition()
            return "%s(%s)" % (self.__class__.__name__, params)

    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) -> Tuple[float, float]:
            """Returns the position of this ROI"""
            return self._handle.getPosition()

        def setPosition(self, pos: Tuple[float, float]):
            """Set the position of this ROI

            :param 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.
        """

        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)
            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 _updatedStyle(self, event, style: items.CurveStyle):
            super(LineROI, self)._updatedStyle(event, style)
            self.__shape.setColor(style.getColor())
            self.__shape.setLineStyle(style.getLineStyle())
            self.__shape.setLineWidth(style.getLineWidth())
            self.__shape.setLineGapColor(style.getLineGapColor())

        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: numpy.ndarray, endPoint: numpy.ndarray):
            """Set this line location using the ending points

            :param numpy.ndarray startPoint: Staring bounding point of the line
            :param numpy.ndarray endPoint: Ending bounding point of the line
            """
            if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
                self.__updateEndPoints(startPoint, endPoint)

        def __updateEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
            """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 = numpy.array(self._handleStart.getPosition())
            endPoint = numpy.array(self._handleEnd.getPosition())
            return (startPoint, endPoint)

        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

            points = self.__shape.getPoints()
            line_pt1 = points[0]
            line_pt2 = points[1]

            bb1 = _BoundingBox.from_points(points)
            if not bb1.contains(position):
                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,
                )
            ) is not None

        def __str__(self):
            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)

    class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
        """A ROI identifying an horizontal line in a 2D plot."""

        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):
            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 _updated(self, event=None, checkVisibility=True):
            if 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 _updateText(self, text: str):
            self._marker.setText(text)

        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) -> float:
            """Returns the position of this line if the horizontal axis"""
            pos = self._marker.getPosition()
            return pos[1]

        def setPosition(self, pos: float):
            """Set the position of this ROI

            :param pos: Horizontal position of this line
            """
            self._marker.setPosition(0, pos)

        @docstring(_RegionOfInterestBase)
        def contains(self, position):
            return position[1] == self.getPosition()

        def _linePositionChanged(self, event):
            """Handle position changed events of the marker"""
            if event is items.ItemChangedType.POSITION:
                self.sigRegionChanged.emit()

        def __str__(self):
            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."""

        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):
            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 _updated(self, event=None, checkVisibility=True):
            if 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 _updateText(self, text: str):
            self._marker.setText(text)

        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) -> float:
            """Returns the position of this line if the horizontal axis"""
            pos = self._marker.getPosition()
            return pos[0]

        def setPosition(self, pos: float):
            """Set the position of this ROI

            :param float pos: Horizontal position of this line
            """
            self._marker.setPosition(pos, 0)

        @docstring(RegionOfInterest)
        def contains(self, position):
            return position[0] == self.getPosition()

        def _linePositionChanged(self, event):
            """Handle position changed events of the marker"""
            if event is items.ItemChangedType.POSITION:
                self.sigRegionChanged.emit()

        def __str__(self):
            params = "x: %f" % self.getPosition()
            return "%s(%s)" % (self.__class__.__name__, params)

    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.
        """

        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)
            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 _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())
            self.__shape.setLineGapColor(style.getLineGapColor())

        def setFirstShapePoints(self, points):
            assert len(points) == 2
            self._setBound(points)

        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])
            """
            pos = self._handleCenter.getPosition()
            return numpy.array(pos)

        def getOrigin(self):
            """Returns the corner point with the smaller coordinates

            :rtype: numpy.ndarray([float,float])
            """
            pos = self._handleBottomLeft.getPosition()
            return numpy.array(pos)

        def getSize(self):
            """Returns the size of this rectangle

            :rtype: numpy.ndarray([float,float])
            """
            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

            :param numpy.ndarray position: Location of the smaller corner of the ROI
            """
            size = self.getSize()
            self.setGeometry(origin=position, size=size)

        def setSize(self, size):
            """Set the size of this ROI

            :param numpy.ndarray size: Size of the center of the ROI
            """
            origin = self.getOrigin()
            self.setGeometry(origin=origin, size=size)

        def setCenter(self, position):
            """Set the size of this ROI

            :param numpy.ndarray position: Location of the center of the ROI
            """
            size = self.getSize()
            self.setGeometry(center=position, size=size)

        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])
                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])
            else:
                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
                size = self.getSize()
                self._updateGeometry(center=current, size=size)
            else:
                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()
            w, h = self.getSize()
            params = origin[0], origin[1], w, h
            params = "origin: %f %f; width: %f; height: %f" % params
            return "%s(%s)" % (self.__class__.__name__, params)

    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())
            self.__shape.setLineGapColor(style.getLineGapColor())

        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))

        @docstring(HandleBasedROI)
        def contains(self, position):
            return numpy.linalg.norm(self.getCenter() - position) <= self.getRadius()

        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, 0.0
            self._orientation = (
                0.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())
            self.__shape.setLineGapColor(style.getLineGapColor())

        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()

        @docstring(HandleBasedROI)
        def contains(self, position):
            major, minor = self.getMajorRadius(), self.getMinorRadius()
            delta = self.getOrientation()
            x, y = position - self.getCenter()
            return (
                (x * numpy.cos(delta) + y * numpy.sin(delta)) ** 2 / major**2
                + (x * numpy.sin(delta) - y * numpy.cos(delta)) ** 2 / minor**2
            ) <= 1

        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.
        """

        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)
            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())
            self.__shape.setLineGapColor(style.getLineGapColor())
            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.

            :rtype: numpy.ndarray
            """
            return self._points.copy()

        def setPoints(self, points):
            """Set the position of this ROI

            :param numpy.ndarray pos: 2d-coordinate of this point
            """
            assert len(points.shape) == 2 and points.shape[1] == 2

            if numpy.array_equal(points, self._points):
                return  # Nothing has changed

            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])

            if len(points) == 0:
                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:
                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._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 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.EDITABLE:
                self._updateEditable()
                self._updateText(self.getText())
            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: str):
            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())

        @docstring(HandleBasedROI)
        def contains(self, position):
            return self.getMin() <= position[0] <= self.getMax()

        def __str__(self):
            vrange = self.getRange()
            params = "min: %f; max: %f" % vrange
            return "%s(%s)" % (self.__class__.__name__, params)