Skip to main content
`Ctrl`+`K`

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

    # /*##########################################################################
    #
    # Copyright (c) 2018-2024 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 Arc ROI item for the :class:`~silx.gui.plot.PlotWidget`.
    """

    __authors__ = ["V. Valls"]
    __license__ = "MIT"
    __date__ = "28/06/2018"

    import logging
    import numpy
    import enum
    from typing import Tuple

    from ... import utils
    from .. import items
    from ...colors import rgba
    from ....utils.proxy import docstring
    from ._roi_base import HandleBasedROI
    from ._roi_base import InteractionModeMixIn
    from ._roi_base import RoiInteractionMode

    logger = logging.getLogger(__name__)

    class _ArcGeometry:
        """
        Non-mutable object to store the geometry of the arc ROI.

        The aim is is to switch between consistent state without dealing with
        intermediate values.
        """

        def __init__(
            self,
            center,
            startPoint,
            endPoint,
            radius,
            weight,
            startAngle,
            endAngle,
            closed=False,
        ):
            """Constructor for a consistent arc geometry.

            There is also specific class method to create different kind of arc
            geometry.
            """
            self.center = center
            self.startPoint = startPoint
            self.endPoint = endPoint
            self.radius = radius
            self.weight = weight
            self.startAngle = startAngle
            self.endAngle = endAngle
            self._closed = closed

        @classmethod
        def createEmpty(cls):
            """Create an arc geometry from an empty shape"""
            zero = numpy.array([0, 0])
            return cls(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)

        @classmethod
        def createRect(cls, startPoint, endPoint, weight):
            """Create an arc geometry from a definition of a rectangle"""
            return cls(None, startPoint, endPoint, None, weight, None, None, False)

        @classmethod
        def createCircle(
            cls, center, startPoint, endPoint, radius, weight, startAngle, endAngle
        ):
            """Create an arc geometry from a definition of a circle"""
            return cls(
                center, startPoint, endPoint, radius, weight, startAngle, endAngle, True
            )

        def withWeight(self, weight):
            """Return a new geometry based on this object, with a specific weight"""
            return _ArcGeometry(
                self.center,
                self.startPoint,
                self.endPoint,
                self.radius,
                weight,
                self.startAngle,
                self.endAngle,
                self._closed,
            )

        def withRadius(self, radius):
            """Return a new geometry based on this object, with a specific 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 _ArcGeometry(
                self.center,
                startPoint,
                endPoint,
                radius,
                self.weight,
                self.startAngle,
                self.endAngle,
                self._closed,
            )

        def withStartAngle(self, startAngle):
            """Return a new geometry based on this object, with a specific start angle"""
            vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
            startPoint = self.center + vector * self.radius

            # Never add more than 180 to maintain coherency
            deltaAngle = startAngle - self.startAngle
            if deltaAngle > numpy.pi:
                deltaAngle -= numpy.pi * 2
            elif deltaAngle < -numpy.pi:
                deltaAngle += numpy.pi * 2

            startAngle = self.startAngle + deltaAngle
            return _ArcGeometry(
                self.center,
                startPoint,
                self.endPoint,
                self.radius,
                self.weight,
                startAngle,
                self.endAngle,
                self._closed,
            )

        def withEndAngle(self, endAngle):
            """Return a new geometry based on this object, with a specific end angle"""
            vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
            endPoint = self.center + vector * self.radius

            # Never add more than 180 to maintain coherency
            deltaAngle = endAngle - self.endAngle
            if deltaAngle > numpy.pi:
                deltaAngle -= numpy.pi * 2
            elif deltaAngle < -numpy.pi:
                deltaAngle += numpy.pi * 2

            endAngle = self.endAngle + deltaAngle
            return _ArcGeometry(
                self.center,
                self.startPoint,
                endPoint,
                self.radius,
                self.weight,
                self.startAngle,
                endAngle,
                self._closed,
            )

        def translated(self, dx, dy):
            """Return the translated geometry by dx, dy"""
            delta = numpy.array([dx, dy])
            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 _ArcGeometry(
                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,
                )
            )

    class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
        """A ROI identifying an arc of a circle with a width.

        This ROI provides
        - 3 handle to control the curvature
        - 1 handle to control the weight
        - 1 anchor to translate the shape.
        """

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

        ThreePointMode = RoiInteractionMode(
            "3 points", "Provides 3 points to define the main radius circle"
        )
        PolarMode = RoiInteractionMode(
            "Polar", "Provides anchors to edit the ROI in polar coords"
        )
        # FIXME: MoveMode was designed cause there is too much anchors
        # FIXME: It would be good replace it by a dnd on the shape
        MoveMode = RoiInteractionMode(
            "Translation", "Provides anchors to only move the ROI"
        )

        class Role(enum.Enum):
            """Identify a set of roles which can be used for now to reach some positions"""

            START = 0
            """Location of the anchor at the start of the arc"""
            STOP = 1
            """Location of the anchor at the stop of the arc"""
            MIDDLE = 2
            """Location of the anchor at the middle of the arc"""
            CENTER = 3
            """Location of the center of the circle"""

        def __init__(self, parent=None):
            HandleBasedROI.__init__(self, parent=parent)
            items.LineMixIn.__init__(self)
            InteractionModeMixIn.__init__(self)

            self._geometry = _ArcGeometry.createEmpty()
            self._handleLabel = self.addLabelHandle()

            self._handleStart = self.addHandle()
            self._handleMid = self.addHandle()
            self._handleEnd = self.addHandle()
            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)

            self._initInteractionMode(self.ThreePointMode)
            self._interactiveModeUpdated(self.ThreePointMode)

        def availableInteractionModes(self):
            """Returns the list of available interaction modes

            :rtype: List[RoiInteractionMode]
            """
            return [self.ThreePointMode, self.PolarMode, self.MoveMode]

        def _interactiveModeUpdated(self, modeId):
            """Set the interaction mode.

            :param RoiInteractionMode modeId:
            """
            if modeId is self.ThreePointMode:
                self._handleStart.setVisible(True)
                self._handleEnd.setVisible(True)
                self._handleWeight.setVisible(True)
                self._handleStart.setSymbol("s")
                self._handleMid.setSymbol("s")
                self._handleEnd.setSymbol("s")
                self._handleWeight.setSymbol("d")
                self._handleMove.setSymbol("+")
            elif modeId is self.PolarMode:
                self._handleStart.setVisible(True)
                self._handleEnd.setVisible(True)
                self._handleWeight.setVisible(True)
                self._handleStart.setSymbol("o")
                self._handleMid.setSymbol("o")
                self._handleEnd.setSymbol("o")
                self._handleWeight.setSymbol("d")
                self._handleMove.setSymbol("+")
            elif modeId is self.MoveMode:
                self._handleStart.setVisible(False)
                self._handleEnd.setVisible(False)
                self._handleWeight.setVisible(False)
                self._handleMid.setSymbol("+")
                self._handleMove.setSymbol("+")
            else:
                assert False
            if self._geometry.isClosed():
                if modeId != self.MoveMode:
                    self._handleStart.setSymbol("x")
                    self._handleEnd.setSymbol("x")
            self._updateHandles()

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

        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.
            """
            # The first shape is a line
            point0 = points[0]
            point1 = points[1]

            # 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

            geometry = self._createGeometryFromControlPoints(point0, mid, point1, weight)
            self._geometry = geometry
            self._updateHandles()

        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())
                midPos = geometry.center + geometry.center - start
            else:
                if geometry.center is None:
                    midPos = geometry.startPoint * 0.5 + geometry.endPoint * 0.5
                else:
                    midAngle = geometry.startAngle * 0.5 + geometry.endAngle * 0.5
                    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:
                    normal = normal / distance
                weightPos = center + normal * geometry.weight * 0.5
            else:
                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 _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()
            self._updateShape()

        def _updateCurvature(
            self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=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 checkClosed:
                closed = self._isCloseInPixel(start, end)
            else:
                closed = self._geometry.isClosed()
            if closed:
                if updateStart:
                    start = end
                else:
                    end = start

            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)

            weight = self._geometry.weight
            geometry = self._createGeometryFromControlPoints(
                start, mid, end, weight, closed=closed
            )
            self._geometry = geometry

            self._updateWeightHandle()
            self._updateShape()

        def _updateCloseInAngle(self, geometry, updateStart):
            azim = numpy.abs(geometry.endAngle - geometry.startAngle)
            if numpy.pi < azim < 3 * numpy.pi:
                closed = self._isCloseInPixel(geometry.startPoint, geometry.endPoint)
                geometry._closed = closed
                if closed:
                    sign = 1 if geometry.startAngle < geometry.endAngle else -1
                    if updateStart:
                        geometry.startPoint = geometry.endPoint
                        geometry.startAngle = geometry.endAngle - sign * 2 * numpy.pi
                    else:
                        geometry.endPoint = geometry.startPoint
                        geometry.endAngle = geometry.startAngle + sign * 2 * numpy.pi

        def handleDragUpdated(self, handle, origin, previous, current):
            modeId = self.getInteractionMode()
            if handle is self._handleStart:
                if modeId is self.ThreePointMode:
                    mid = numpy.array(self._handleMid.getPosition())
                    end = numpy.array(self._handleEnd.getPosition())
                    self._updateCurvature(
                        current,
                        mid,
                        end,
                        checkClosed=True,
                        updateStart=True,
                        updateCurveHandles=False,
                    )
                elif modeId is self.PolarMode:
                    v = current - self._geometry.center
                    startAngle = numpy.angle(complex(v[0], v[1]))
                    geometry = self._geometry.withStartAngle(startAngle)
                    self._updateCloseInAngle(geometry, updateStart=True)
                    self._geometry = geometry
                    self._updateHandles()
            elif handle is self._handleMid:
                if modeId is self.ThreePointMode:
                    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 modeId is self.PolarMode:
                    radius = numpy.linalg.norm(self._geometry.center - current)
                    self._geometry = self._geometry.withRadius(radius)
                    self._updateHandles()
                elif modeId is self.MoveMode:
                    delta = current - previous
                    self.translate(*delta)
            elif handle is self._handleEnd:
                if modeId is self.ThreePointMode:
                    start = numpy.array(self._handleStart.getPosition())
                    mid = numpy.array(self._handleMid.getPosition())
                    self._updateCurvature(
                        start,
                        mid,
                        current,
                        checkClosed=True,
                        updateStart=False,
                        updateCurveHandles=False,
                    )
                elif modeId is self.PolarMode:
                    v = current - self._geometry.center
                    endAngle = numpy.angle(complex(v[0], v[1]))
                    geometry = self._geometry.withEndAngle(endAngle)
                    self._updateCloseInAngle(geometry, updateStart=False)
                    self._geometry = geometry
                    self._updateHandles()
            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):
            modeId = self.getInteractionMode()
            if handle in [self._handleStart, self._handleMid, self._handleEnd]:
                if modeId is self.ThreePointMode:
                    self._normalizeGeometry()
                    self._updateHandles()

            if self._geometry.isClosed():
                if modeId is self.MoveMode:
                    self._handleStart.setSymbol("")
                    self._handleEnd.setSymbol("")
                else:
                    self._handleStart.setSymbol("x")
                    self._handleEnd.setSymbol("x")
            else:
                if modeId is self.ThreePointMode:
                    self._handleStart.setSymbol("s")
                    self._handleEnd.setSymbol("s")
                elif modeId is self.PolarMode:
                    self._handleStart.setSymbol("o")
                    self._handleEnd.setSymbol("o")
                if modeId is self.MoveMode:
                    self._handleStart.setSymbol("")
                    self._handleEnd.setSymbol("")

        def _createGeometryFromControlPoints(self, start, mid, end, weight, closed=None):
            """Returns the geometry of the object"""
            if closed or (closed is None and numpy.allclose(start, end)):
                # Special arc: It's a closed circle
                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 _ArcGeometry.createCircle(
                    center, start, end, radius, weight, startAngle, endAngle
                )

            elif (
                numpy.linalg.norm(
                    numpy.cross(numpy.append(mid - start, 0), numpy.append(end - start, 0))
                )
                < 1e-5
            ):
                # Degenerated arc, it's a rectangle
                return _ArcGeometry.createRect(start, end, weight)
            else:
                center, radius = self._circleEquation(start, mid, end)
                v = start - center
                startAngle = numpy.angle(complex(v[0], v[1]))
                v = mid - center
                midAngle = numpy.angle(complex(v[0], v[1]))
                v = end - center
                endAngle = numpy.angle(complex(v[0], v[1]))

                # Is it clockwise or anticlockwise
                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 _ArcGeometry(
                    center, start, end, radius, weight, startAngle, endAngle
                )

        def _createShapeFromGeometry(self, geometry):
            kind = geometry.getKind()
            if kind == "rect":
                # It is not an arc
                # 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)
                if distance != 0:
                    normal /= distance
                points = numpy.array(
                    [
                        geometry.startPoint + normal * geometry.weight * 0.5,
                        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.linspace(0, 2 * numpy.pi, num=50)
                # 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.linspace(0, 2 * numpy.pi, num=50)
                # 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

                sign = numpy.sign(geometry.endAngle - geometry.startAngle)
                delta = min(0.1, abs(geometry.startAngle - geometry.endAngle) / 100) * sign

                if geometry.startAngle == geometry.endAngle:
                    # Degenerated, it's a line (single radius)
                    angle = geometry.startAngle
                    direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
                    points = []
                    points.append(geometry.center + direction * innerRadius)
                    points.append(geometry.center + direction * outerRadius)
                    return numpy.array(points)

                angles = numpy.arange(geometry.startAngle, geometry.endAngle, delta)
                if angles[-1] != geometry.endAngle:
                    angles = numpy.append(angles, geometry.endAngle)

                if kind == "camembert":
                    # It's a part of camembert
                    points = []
                    points.append(geometry.center)
                    points.append(geometry.startPoint)
                    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:
                    assert False

                points = numpy.array(points)

            return 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
            else:
                movePos = geometry.center

            with utils.blockSignals(self._handleMove):
                self._handleMove.setPosition(*movePos)

            self.sigRegionChanged.emit()

        def getGeometry(self):
            """Returns a tuple containing the geometry of this ROI

            It is a symmetric function of :meth:`setGeometry`.

            If `startAngle` is smaller than `endAngle` the rotation is clockwise,
            else the rotation is anticlockwise.

            :rtype: Tuple[numpy.ndarray,float,float,float,float]
            :raise ValueError: In case the ROI can't be represented as section of
                a circle
            """
            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,
            )

        def getPosition(self, role: Role = Role.CENTER) -> Tuple[float, float]:
            """Returns a position by it's role.

            By default returns the center of the circle of the arc ROI.
            """
            if role == self.Role.START:
                return self._handleStart.getPosition()
            if role == self.Role.STOP:
                return self._handleEnd.getPosition()
            if role == self.Role.MIDDLE:
                return self._handleMid.getPosition()
            if role == self.Role.CENTER:
                p = self.getCenter()
                return p[0], p[1]
            raise ValueError(f"{role} is not supported")

        def isClosed(self):
            """Returns true if the arc is a closed shape, like a circle or a donut.

            :rtype: bool
            """
            return self._geometry.isClosed()

        def getCenter(self):
            """Returns the center of the circle used to draw arcs of this ROI.

            This center is usually outside the the shape itself.

            :rtype: numpy.ndarray
            """
            return self._geometry.center

        def getStartAngle(self):
            """Returns the angle of the start of the section of this ROI (in radian).

            If `startAngle` is smaller than `endAngle` the rotation is clockwise,
            else the rotation is anticlockwise.

            :rtype: float
            """
            return self._geometry.startAngle

        def getEndAngle(self):
            """Returns the angle of the end of the section of this ROI (in radian).

            If `startAngle` is smaller than `endAngle` the rotation is clockwise,
            else the rotation is anticlockwise.

            :rtype: float
            """
            return self._geometry.endAngle

        def getInnerRadius(self):
            """Returns the radius of the smaller arc used to draw this ROI.

            :rtype: float
            """
            geometry = self._geometry
            radius = geometry.radius - geometry.weight * 0.5
            if radius < 0:
                radius = 0
            return radius

        def getOuterRadius(self):
            """Returns the radius of the bigger arc used to draw this ROI.

            :rtype: float
            """
            geometry = self._geometry
            radius = geometry.radius + geometry.weight * 0.5
            return radius

        def setGeometry(self, center, innerRadius, outerRadius, startAngle, endAngle):
            """
            Set the geometry of this arc.

            :param numpy.ndarray center: Center of the circle.
            :param float innerRadius: Radius of the smaller arc of the section.
            :param float outerRadius: Weight of the bigger arc of the section.
                It have to be bigger than `innerRadius`
            :param float startAngle: Location of the start of the section (in radian)
            :param float endAngle: Location of the end of the section (in radian).
                If `startAngle` is smaller than `endAngle` the rotation is clockwise,
                else the rotation is anticlockwise.
            """
            if innerRadius > outerRadius:
                logger.error("inner radius larger than outer radius")
                innerRadius, outerRadius = outerRadius, innerRadius
            center = numpy.array(center)
            radius = (innerRadius + outerRadius) * 0.5
            weight = outerRadius - innerRadius

            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 = _ArcGeometry(
                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)
            # angle is inside [-pi, pi]

            # Normalize the start angle between [-pi, pi]
            # with a positive angle range
            start_angle = self.getStartAngle()
            end_angle = self.getEndAngle()
            azim_range = end_angle - start_angle
            if azim_range < 0:
                start_angle = end_angle
                azim_range = -azim_range
            start_angle = numpy.mod(start_angle + numpy.pi, 2 * numpy.pi) - numpy.pi

            if angle < start_angle:
                angle += 2 * numpy.pi
            return start_angle <= angle <= start_angle + azim_range

        def translate(self, x, y):
            self._geometry = self._geometry.translated(x, y)
            self._updateHandles()

        def _arcCurvatureMarkerConstraint(self, x, y):
            """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
        def _circleEquation(pt1, pt2, pt3):
            """Circle equation from 3 (x, y) points

            :return: Position of the center of the circle and the radius
            :rtype: Tuple[Tuple[float,float],float]
            """
            x, y, z = complex(*pt1), complex(*pt2), complex(*pt3)
            w = z - x
            w /= y - x
            c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
            return numpy.array((-c.real, -c.imag)), abs(c + x)

        def __str__(self):
            try:
                center, innerRadius, outerRadius, startAngle, endAngle = self.getGeometry()
                params = (
                    center[0],
                    center[1],
                    innerRadius,
                    outerRadius,
                    startAngle,
                    endAngle,
                )
                params = "center: %f %f; radius: %f %f; angles: %f %f" % params
            except ValueError:
                params = "invalid"
            return "%s(%s)" % (self.__class__.__name__, params)