#!/usr/bin/env python3
""" Extract Box Editor for the manual adjustments tool """

import platform

import numpy as np

from lib.gui.custom_widgets import RightClickMenu
from lib.gui.utils import get_config
from ._base import Editor, logger


class ExtractBox(Editor):
    """ The Extract Box Editor.

    Adjust the calculated Extract Box to shift all of the 68 point landmarks in place.

    Parameters
    ----------
    canvas: :class:`tkinter.Canvas`
        The canvas that holds the image and annotations
    detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`
        The _detected_faces data for this manual session
    """
    def __init__(self, canvas, detected_faces):
        self._right_click_menu = RightClickMenu(["Delete Face"],
                                                [self._delete_current_face],
                                                ["Del"])
        control_text = ("Extract Box Editor\nMove the extract box that has been generated by the "
                        "aligner. Click and drag:\n\n"
                        " - Inside the bounding box to relocate the landmarks.\n"
                        " - The corner anchors to resize the landmarks.\n"
                        " - Outside of the corners to rotate the landmarks.")
        key_bindings = {"<Delete>": self._delete_current_face}
        super().__init__(canvas, detected_faces,
                         control_text=control_text, key_bindings=key_bindings)

    @property
    def _corner_order(self):
        """ dict: The position index of bounding box corners """
        return {0: ("top", "left"),
                3: ("top", "right"),
                2: ("bottom", "right"),
                1: ("bottom", "left")}

    def update_annotation(self):
        """ Draw the latest Extract Boxes around the faces. """
        color = self._control_color
        roi = self._zoomed_roi
        for idx, face in enumerate(self._face_iterator):
            logger.trace("Drawing Extract Box: (idx: %s, roi: %s)", idx, face.original_roi)
            if self._globals.is_zoomed:
                box = np.array((roi[0], roi[1], roi[2], roi[1], roi[2], roi[3], roi[0], roi[3]))
            else:
                face.load_aligned(None, force=True)
                box = self._scale_to_display(face.original_roi).flatten()
            top_left = box[:2] - 10
            kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(idx))
            self._object_tracker("eb_text", "text", idx, top_left, kwargs)
            kwargs = dict(fill="", outline=color, width=1)
            self._object_tracker("eb_box", "polygon", idx, box, kwargs)
            self._update_anchor_annotation(idx, box, color)
        logger.trace("Updated extract box annotations")

    def _update_anchor_annotation(self, face_index, extract_box, color):
        """ Update the anchor annotations for each corner of the extract box.

        The anchors only display when the extract box editor is active.

        Parameters
        ----------
        face_index: int
            The index of the face being annotated
        extract_box: :class:`numpy.ndarray`
            The scaled extract box to get the corner anchors for
        color: str
            The hex color of the extract box line
        """
        if not self._is_active or self._globals.is_zoomed:
            self.hide_annotation("eb_anc_dsp")
            self.hide_annotation("eb_anc_grb")
            return
        fill_color = "gray"
        activefill_color = "white" if self._is_active else ""
        anchor_points = self._get_anchor_points((extract_box[:2],
                                                 extract_box[2:4],
                                                 extract_box[4:6],
                                                 extract_box[6:]))
        for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)):
            dsp_kwargs = dict(outline=color, fill=fill_color, width=1)
            grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color)
            dsp_key = "eb_anc_dsp_{}".format(idx)
            grb_key = "eb_anc_grb_{}".format(idx)
            self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs)
            self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs)
        logger.trace("Updated extract box anchor annotations")

    # << MOUSE HANDLING >>
    # Mouse cursor display
    def _update_cursor(self, event):
        """ Update the cursor when it is hovering over an extract box and update
        :attr:`_mouse_location` with the current cursor position.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The current tkinter mouse event
        """
        if self._check_cursor_anchors():
            return
        if self._check_cursor_box():
            return
        if self._check_cursor_rotate(event):
            return
        self._canvas.config(cursor="")
        self._mouse_location = None

    def _check_cursor_anchors(self):
        """ Check whether the cursor is over a corner anchor.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("anchor", `face index`, `corner_index`)

        Returns
        -------
        bool
            ``True`` if cursor is over an anchor point otherwise ``False``
        """
        anchors = set(self._canvas.find_withtag("eb_anc_grb"))
        item_ids = set(self._canvas.find_withtag("current")).intersection(anchors)
        if not item_ids:
            return False
        item_id = list(item_ids)[0]
        tags = self._canvas.gettags(item_id)
        face_idx = int(next(tag for tag in tags if tag.startswith("face_")).split("_")[-1])
        corner_idx = int(next(tag for tag in tags
                              if tag.startswith("eb_anc_grb_")
                              and "face_" not in tag).split("_")[-1])

        self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx]))
        self._mouse_location = ("anchor", face_idx, corner_idx)
        return True

    def _check_cursor_box(self):
        """ Check whether the cursor is inside an extract box.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("box", `face index`)

        Returns
        -------
        bool
            ``True`` if cursor is over a rotate point otherwise ``False``
        """
        extract_boxes = set(self._canvas.find_withtag("eb_box"))
        item_ids = set(self._canvas.find_withtag("current")).intersection(extract_boxes)
        if not item_ids:
            return False
        item_id = list(item_ids)[0]
        self._canvas.config(cursor="fleur")
        self._mouse_location = ("box", next(int(tag.split("_")[-1])
                                            for tag in self._canvas.gettags(item_id)
                                            if tag.startswith("face_")))
        return True

    def _check_cursor_rotate(self, event):
        """ Check whether the cursor is in an area to rotate the extract box.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("rotate", `face index`)

        Notes
        -----
        This code is executed after the check has been completed to see if the mouse is inside
        the extract box. For this reason, we don't bother running a check to see if the mouse
        is inside the box, as this code will never run if that is the case.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The current tkinter mouse event

        Returns
        -------
        bool
            ``True`` if cursor is over a rotate point otherwise ``False``
        """
        distance = 30
        boxes = np.array([np.array(self._canvas.coords(item_id)).reshape(4, 2)
                          for item_id in self._canvas.find_withtag("eb_box")
                          if self._canvas.itemcget(item_id, "state") != "hidden"])
        position = np.array((event.x, event.y)).astype("float32")
        for face_idx, points in enumerate(boxes):
            if any(np.all(position > point - distance) and np.all(position < point + distance)
                   for point in points):
                self._canvas.config(cursor="exchange")
                self._mouse_location = ("rotate", face_idx)
                return True
        return False

    # Mouse click actions
    def set_mouse_click_actions(self):
        """ Add context menu to OS specific right click action. """
        super().set_mouse_click_actions()
        self._canvas.bind("<Button-2>" if platform.system() == "Darwin" else "<Button-3>",
                          self._context_menu)

    def _drag_start(self, event):
        """ The action to perform when the user starts clicking and dragging the mouse.

        Selects the correct extract box action based on the initial cursor position.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        if self._mouse_location is None:
            self._drag_data = dict()
            self._drag_callback = None
            return
        self._drag_data["current_location"] = np.array((event.x, event.y))
        callback = dict(anchor=self._resize, rotate=self._rotate, box=self._move)
        self._drag_callback = callback[self._mouse_location[0]]

    def _drag_stop(self, event):  # pylint: disable=unused-argument
        """ Trigger a viewport thumbnail update on click + drag release

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event. Required but unused.
        """
        if self._mouse_location is None:
            return
        self._det_faces.update.post_edit_trigger(self._globals.frame_index,
                                                 self._mouse_location[1])

    def _move(self, event):
        """ Updates the underlying detected faces landmarks based on mouse dragging delta,
        which moves the Extract box on a drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        if not self._drag_data:
            return
        shift_x = event.x - self._drag_data["current_location"][0]
        shift_y = event.y - self._drag_data["current_location"][1]
        scaled_shift = self.scale_from_display(np.array((shift_x, shift_y)), do_offset=False)
        self._det_faces.update.landmarks(self._globals.frame_index,
                                         self._mouse_location[1],
                                         *scaled_shift)
        self._drag_data["current_location"] = (event.x, event.y)

    def _resize(self, event):
        """ Resizes the landmarks contained within an extract box on a corner anchor drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        face_idx = self._mouse_location[1]
        face_tag = "eb_box_face_{}".format(face_idx)
        position = np.array((event.x, event.y))
        box = np.array(self._canvas.coords(face_tag))
        center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4))
        if not self._check_in_bounds(center, box, position):
            logger.trace("Drag out of bounds. Not updating")
            self._drag_data["current_location"] = position
            return

        start = self._drag_data["current_location"]
        distance = ((np.linalg.norm(center - start) - np.linalg.norm(center - position))
                    * get_config().scaling_factor)
        size = ((box[2] - box[0]) ** 2 + (box[3] - box[1]) ** 2) ** 0.5
        scale = 1 - (distance / size)
        logger.trace("face_index: %s, center: %s, start: %s, position: %s, distance: %s, "
                     "size: %s, scale: %s", face_idx, center, start, position, distance, size,
                     scale)
        if size * scale < 20:
            # Don't over shrink the box
            logger.trace("Box would size to less than 20px. Not updating")
            self._drag_data["current_location"] = position
            return

        self._det_faces.update.landmarks_scale(self._globals.frame_index,
                                               face_idx,
                                               scale,
                                               self.scale_from_display(center))
        self._drag_data["current_location"] = position

    def _check_in_bounds(self, center, box, position):
        """ Ensure that a resize drag does is not going to cross the center point from it's initial
        corner location.

        Parameters
        ----------
        center: :class:`numpy.ndarray`
            The (`x`, `y`) center point of the face extract box
        box: :class:`numpy.ndarray`
            The canvas coordinates of the extract box polygon's corners
        position: : class:`numpy.ndarray`
            The current (`x`, `y`) position of the mouse cursor

        Returns
        -------
        bool
            ``True`` if the drag operation does not cross the center point otherwise ``False``
        """
        # Generate lines that span the full frame (x and y) along the center point
        center_x = np.array(((center[0], 0), (center[0], self._globals.frame_display_dims[1])))
        center_y = np.array(((0, center[1]), (self._globals.frame_display_dims[0], center[1])))

        # Generate a line coming from the current corner location to the current cursor position
        full_line = np.array((box[self._mouse_location[2] * 2:self._mouse_location[2] * 2 + 2],
                              position))
        logger.trace("center: %s, center_x_line: %s, center_y_line: %s, full_line: %s",
                     center, center_x, center_y, full_line)

        # Check whether any of the generated lines intersect
        for line in (center_x, center_y):
            if (self._is_ccw(full_line[0], *line) != self._is_ccw(full_line[1], *line) and
                    self._is_ccw(*full_line, line[0]) != self._is_ccw(*full_line, line[1])):
                logger.trace("line: %s crosses center: %s", full_line, center)
                return False
        return True

    @staticmethod
    def _is_ccw(point_a, point_b, point_c):
        """ Check whether 3 points are counter clockwise from each other.

        Parameters
        ----------
        point_a: :class:`numpy.ndarray`
            The first (`x`, `y`) point to check for counter clockwise ordering
        point_b: :class:`numpy.ndarray`
            The second (`x`, `y`) point to check for counter clockwise ordering
        point_c: :class:`numpy.ndarray`
            The third (`x`, `y`) point to check for counter clockwise ordering

        Returns
        -------
        bool
            ``True`` if the 3 points are provided in counter clockwise order otherwise ``False``
        """
        return ((point_c[1] - point_a[1]) * (point_b[0] - point_a[0]) >
                (point_b[1] - point_a[1]) * (point_c[0] - point_a[0]))

    def _rotate(self, event):
        """ Rotates the landmarks contained within an extract box on a corner rotate drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        face_idx = self._mouse_location[1]
        face_tag = "eb_box_face_{}".format(face_idx)
        box = np.array(self._canvas.coords(face_tag))
        position = np.array((event.x, event.y))

        center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4))
        init_to_center = self._drag_data["current_location"] - center
        new_to_center = position - center
        angle = np.rad2deg(np.arctan2(*new_to_center) - np.arctan2(*init_to_center))
        logger.trace("face_index: %s, box: %s, center: %s, init_to_center: %s, new_to_center: %s"
                     "center: %s, angle: %s", face_idx, box, center, init_to_center, new_to_center,
                     center, angle)

        self._det_faces.update.landmarks_rotate(self._globals.frame_index,
                                                face_idx,
                                                angle,
                                                self.scale_from_display(center))
        self._drag_data["current_location"] = position

    def _get_scale(self):
        """ Obtain the scaling for the extract box resize """

    def _context_menu(self, event):
        """ Create a right click context menu to delete the alignment that is being
        hovered over. """
        if self._mouse_location is None or self._mouse_location[0] != "box":
            return
        self._right_click_menu.popup(event)

    def _delete_current_face(self, *args):  # pylint:disable=unused-argument
        """ Called by the right click delete event. Deletes the face that the mouse is currently
        over.

        Parameters
        ----------
        args: tuple (unused)
            The event parameter is passed in by the hot key binding, so args is required
        """
        if self._mouse_location is None or self._mouse_location[0] != "box":
            return
        self._det_faces.update.delete(self._globals.frame_index, self._mouse_location[1])

#!/usr/bin/env python3
""" Extract Box Editor for the manual adjustments tool """

import platform

import numpy as np

from lib.gui.custom_widgets import RightClickMenu
from lib.gui.utils import get_config
from ._base import Editor, logger


class ExtractBox(Editor):
    """ The Extract Box Editor.

    Adjust the calculated Extract Box to shift all of the 68 point landmarks in place.

    Parameters
    ----------
    canvas: :class:`tkinter.Canvas`
        The canvas that holds the image and annotations
    detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`
        The _detected_faces data for this manual session
    """
    def __init__(self, canvas, detected_faces):
        self._right_click_menu = RightClickMenu(["Delete Face"],
                                                [self._delete_current_face],
                                                ["Del"])
        control_text = ("Extract Box Editor\nMove the extract box that has been generated by the "
                        "aligner. Click and drag:\n\n"
                        " - Inside the bounding box to relocate the landmarks.\n"
                        " - The corner anchors to resize the landmarks.\n"
                        " - Outside of the corners to rotate the landmarks.")
        key_bindings = {"<Delete>": self._delete_current_face}
        super().__init__(canvas, detected_faces,
                         control_text=control_text, key_bindings=key_bindings)

    @property
    def _corner_order(self):
        """ dict: The position index of bounding box corners """
        return {0: ("top", "left"),
                3: ("top", "right"),
                2: ("bottom", "right"),
                1: ("bottom", "left")}

    def update_annotation(self):
        """ Draw the latest Extract Boxes around the faces. """
        color = self._control_color
        roi = self._zoomed_roi
        for idx, face in enumerate(self._face_iterator):
            logger.trace("Drawing Extract Box: (idx: %s, roi: %s)", idx, face.original_roi)
            if self._globals.is_zoomed:
                box = np.array((roi[0], roi[1], roi[2], roi[1], roi[2], roi[3], roi[0], roi[3]))
            else:
                face.load_aligned(None, force=True)
                box = self._scale_to_display(face.original_roi).flatten()
            top_left = box[:2] - 10
            kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(idx))
            self._object_tracker("eb_text", "text", idx, top_left, kwargs)
            kwargs = dict(fill="", outline=color, width=1)
            self._object_tracker("eb_box", "polygon", idx, box, kwargs)
            self._update_anchor_annotation(idx, box, color)
        logger.trace("Updated extract box annotations")

    def _update_anchor_annotation(self, face_index, extract_box, color):
        """ Update the anchor annotations for each corner of the extract box.

        The anchors only display when the extract box editor is active.

        Parameters
        ----------
        face_index: int
            The index of the face being annotated
        extract_box: :class:`numpy.ndarray`
            The scaled extract box to get the corner anchors for
        color: str
            The hex color of the extract box line
        """
        if not self._is_active or self._globals.is_zoomed:
            self.hide_annotation("eb_anc_dsp")
            self.hide_annotation("eb_anc_grb")
            return
        fill_color = "gray"
        activefill_color = "white" if self._is_active else ""
        anchor_points = self._get_anchor_points((extract_box[:2],
                                                 extract_box[2:4],
                                                 extract_box[4:6],
                                                 extract_box[6:]))
        for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)):
            dsp_kwargs = dict(outline=color, fill=fill_color, width=1)
            grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color)
            dsp_key = "eb_anc_dsp_{}".format(idx)
            grb_key = "eb_anc_grb_{}".format(idx)
            self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs)
            self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs)
        logger.trace("Updated extract box anchor annotations")

    # << MOUSE HANDLING >>
    # Mouse cursor display
    def _update_cursor(self, event):
        """ Update the cursor when it is hovering over an extract box and update
        :attr:`_mouse_location` with the current cursor position.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The current tkinter mouse event
        """
        if self._check_cursor_anchors():
            return
        if self._check_cursor_box():
            return
        if self._check_cursor_rotate(event):
            return
        self._canvas.config(cursor="")
        self._mouse_location = None

    def _check_cursor_anchors(self):
        """ Check whether the cursor is over a corner anchor.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("anchor", `face index`, `corner_index`)

        Returns
        -------
        bool
            ``True`` if cursor is over an anchor point otherwise ``False``
        """
        anchors = set(self._canvas.find_withtag("eb_anc_grb"))
        item_ids = set(self._canvas.find_withtag("current")).intersection(anchors)
        if not item_ids:
            return False
        item_id = list(item_ids)[0]
        tags = self._canvas.gettags(item_id)
        face_idx = int(next(tag for tag in tags if tag.startswith("face_")).split("_")[-1])
        corner_idx = int(next(tag for tag in tags
                              if tag.startswith("eb_anc_grb_")
                              and "face_" not in tag).split("_")[-1])

        self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx]))
        self._mouse_location = ("anchor", face_idx, corner_idx)
        return True

    def _check_cursor_box(self):
        """ Check whether the cursor is inside an extract box.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("box", `face index`)

        Returns
        -------
        bool
            ``True`` if cursor is over a rotate point otherwise ``False``
        """
        extract_boxes = set(self._canvas.find_withtag("eb_box"))
        item_ids = set(self._canvas.find_withtag("current")).intersection(extract_boxes)
        if not item_ids:
            return False
        item_id = list(item_ids)[0]
        self._canvas.config(cursor="fleur")
        self._mouse_location = ("box", next(int(tag.split("_")[-1])
                                            for tag in self._canvas.gettags(item_id)
                                            if tag.startswith("face_")))
        return True

    def _check_cursor_rotate(self, event):
        """ Check whether the cursor is in an area to rotate the extract box.

        If it is, set the appropriate cursor type and set :attr:`_mouse_location` to
        ("rotate", `face index`)

        Notes
        -----
        This code is executed after the check has been completed to see if the mouse is inside
        the extract box. For this reason, we don't bother running a check to see if the mouse
        is inside the box, as this code will never run if that is the case.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The current tkinter mouse event

        Returns
        -------
        bool
            ``True`` if cursor is over a rotate point otherwise ``False``
        """
        distance = 30
        boxes = np.array([np.array(self._canvas.coords(item_id)).reshape(4, 2)
                          for item_id in self._canvas.find_withtag("eb_box")
                          if self._canvas.itemcget(item_id, "state") != "hidden"])
        position = np.array((event.x, event.y)).astype("float32")
        for face_idx, points in enumerate(boxes):
            if any(np.all(position > point - distance) and np.all(position < point + distance)
                   for point in points):
                self._canvas.config(cursor="exchange")
                self._mouse_location = ("rotate", face_idx)
                return True
        return False

    # Mouse click actions
    def set_mouse_click_actions(self):
        """ Add context menu to OS specific right click action. """
        super().set_mouse_click_actions()
        self._canvas.bind("<Button-2>" if platform.system() == "Darwin" else "<Button-3>",
                          self._context_menu)

    def _drag_start(self, event):
        """ The action to perform when the user starts clicking and dragging the mouse.

        Selects the correct extract box action based on the initial cursor position.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        if self._mouse_location is None:
            self._drag_data = dict()
            self._drag_callback = None
            return
        self._drag_data["current_location"] = np.array((event.x, event.y))
        callback = dict(anchor=self._resize, rotate=self._rotate, box=self._move)
        self._drag_callback = callback[self._mouse_location[0]]

    def _drag_stop(self, event):  # pylint: disable=unused-argument
        """ Trigger a viewport thumbnail update on click + drag release

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event. Required but unused.
        """
        if self._mouse_location is None:
            return
        self._det_faces.update.post_edit_trigger(self._globals.frame_index,
                                                 self._mouse_location[1])

    def _move(self, event):
        """ Updates the underlying detected faces landmarks based on mouse dragging delta,
        which moves the Extract box on a drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        if not self._drag_data:
            return
        shift_x = event.x - self._drag_data["current_location"][0]
        shift_y = event.y - self._drag_data["current_location"][1]
        scaled_shift = self.scale_from_display(np.array((shift_x, shift_y)), do_offset=False)
        self._det_faces.update.landmarks(self._globals.frame_index,
                                         self._mouse_location[1],
                                         *scaled_shift)
        self._drag_data["current_location"] = (event.x, event.y)

    def _resize(self, event):
        """ Resizes the landmarks contained within an extract box on a corner anchor drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        face_idx = self._mouse_location[1]
        face_tag = "eb_box_face_{}".format(face_idx)
        position = np.array((event.x, event.y))
        box = np.array(self._canvas.coords(face_tag))
        center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4))
        if not self._check_in_bounds(center, box, position):
            logger.trace("Drag out of bounds. Not updating")
            self._drag_data["current_location"] = position
            return

        start = self._drag_data["current_location"]
        distance = ((np.linalg.norm(center - start) - np.linalg.norm(center - position))
                    * get_config().scaling_factor)
        size = ((box[2] - box[0]) ** 2 + (box[3] - box[1]) ** 2) ** 0.5
        scale = 1 - (distance / size)
        logger.trace("face_index: %s, center: %s, start: %s, position: %s, distance: %s, "
                     "size: %s, scale: %s", face_idx, center, start, position, distance, size,
                     scale)
        if size * scale < 20:
            # Don't over shrink the box
            logger.trace("Box would size to less than 20px. Not updating")
            self._drag_data["current_location"] = position
            return

        self._det_faces.update.landmarks_scale(self._globals.frame_index,
                                               face_idx,
                                               scale,
                                               self.scale_from_display(center))
        self._drag_data["current_location"] = position

    def _check_in_bounds(self, center, box, position):
        """ Ensure that a resize drag does is not going to cross the center point from it's initial
        corner location.

        Parameters
        ----------
        center: :class:`numpy.ndarray`
            The (`x`, `y`) center point of the face extract box
        box: :class:`numpy.ndarray`
            The canvas coordinates of the extract box polygon's corners
        position: : class:`numpy.ndarray`
            The current (`x`, `y`) position of the mouse cursor

        Returns
        -------
        bool
            ``True`` if the drag operation does not cross the center point otherwise ``False``
        """
        # Generate lines that span the full frame (x and y) along the center point
        center_x = np.array(((center[0], 0), (center[0], self._globals.frame_display_dims[1])))
        center_y = np.array(((0, center[1]), (self._globals.frame_display_dims[0], center[1])))

        # Generate a line coming from the current corner location to the current cursor position
        full_line = np.array((box[self._mouse_location[2] * 2:self._mouse_location[2] * 2 + 2],
                              position))
        logger.trace("center: %s, center_x_line: %s, center_y_line: %s, full_line: %s",
                     center, center_x, center_y, full_line)

        # Check whether any of the generated lines intersect
        for line in (center_x, center_y):
            if (self._is_ccw(full_line[0], *line) != self._is_ccw(full_line[1], *line) and
                    self._is_ccw(*full_line, line[0]) != self._is_ccw(*full_line, line[1])):
                logger.trace("line: %s crosses center: %s", full_line, center)
                return False
        return True

    @staticmethod
    def _is_ccw(point_a, point_b, point_c):
        """ Check whether 3 points are counter clockwise from each other.

        Parameters
        ----------
        point_a: :class:`numpy.ndarray`
            The first (`x`, `y`) point to check for counter clockwise ordering
        point_b: :class:`numpy.ndarray`
            The second (`x`, `y`) point to check for counter clockwise ordering
        point_c: :class:`numpy.ndarray`
            The third (`x`, `y`) point to check for counter clockwise ordering

        Returns
        -------
        bool
            ``True`` if the 3 points are provided in counter clockwise order otherwise ``False``
        """
        return ((point_c[1] - point_a[1]) * (point_b[0] - point_a[0]) >
                (point_b[1] - point_a[1]) * (point_c[0] - point_a[0]))

    def _rotate(self, event):
        """ Rotates the landmarks contained within an extract box on a corner rotate drag event.

        Parameters
        ----------
        event: :class:`tkinter.Event`
            The tkinter mouse event.
        """
        face_idx = self._mouse_location[1]
        face_tag = "eb_box_face_{}".format(face_idx)
        box = np.array(self._canvas.coords(face_tag))
        position = np.array((event.x, event.y))

        center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4))
        init_to_center = self._drag_data["current_location"] - center
        new_to_center = position - center
        angle = np.rad2deg(np.arctan2(*new_to_center) - np.arctan2(*init_to_center))
        logger.trace("face_index: %s, box: %s, center: %s, init_to_center: %s, new_to_center: %s"
                     "center: %s, angle: %s", face_idx, box, center, init_to_center, new_to_center,
                     center, angle)

        self._det_faces.update.landmarks_rotate(self._globals.frame_index,
                                                face_idx,
                                                angle,
                                                self.scale_from_display(center))
        self._drag_data["current_location"] = position

    def _get_scale(self):
        """ Obtain the scaling for the extract box resize """

    def _context_menu(self, event):
        """ Create a right click context menu to delete the alignment that is being
        hovered over. """
        if self._mouse_location is None or self._mouse_location[0] != "box":
            return
        self._right_click_menu.popup(event)

    def _delete_current_face(self, *args):  # pylint:disable=unused-argument
        """ Called by the right click delete event. Deletes the face that the mouse is currently
        over.

        Parameters
        ----------
        args: tuple (unused)
            The event parameter is passed in by the hot key binding, so args is required
        """
        if self._mouse_location is None or self._mouse_location[0] != "box":
            return
        self._det_faces.update.delete(self._globals.frame_index, self._mouse_location[1])

#!/usr/bin/env python3 Extract Box Editor for the manual adjustments tool The Extract Box Editor.      Adjust the calculated Extract Box to shift all of the 68 point landmarks in place.      Parameters     ----------     canvas: :class:`tkinter.Canvas`         The canvas that holds the image and annotations     detected_faces: :class:`~tools.manual.detected_faces.DetectedFaces`         The _detected_faces data for this manual session dict: The position index of bounding box corners Draw the latest Extract Boxes around the faces. Update the anchor annotations for each corner of the extract box.          The anchors only display when the extract box editor is active.          Parameters         ----------         face_index: int             The index of the face being annotated         extract_box: :class:`numpy.ndarray`             The scaled extract box to get the corner anchors for         color: str             The hex color of the extract box line # << MOUSE HANDLING >> # Mouse cursor display Update the cursor when it is hovering over an extract box and update         :attr:`_mouse_location` with the current cursor position.          Parameters         ----------         event: :class:`tkinter.Event`             The current tkinter mouse event Check whether the cursor is over a corner anchor.          If it is, set the appropriate cursor type and set :attr:`_mouse_location` to         ("anchor", `face index`, `corner_index`)          Returns         -------         bool             ``True`` if cursor is over an anchor point otherwise ``False`` Check whether the cursor is inside an extract box.          If it is, set the appropriate cursor type and set :attr:`_mouse_location` to         ("box", `face index`)          Returns         -------         bool             ``True`` if cursor is over a rotate point otherwise ``False`` Check whether the cursor is in an area to rotate the extract box.          If it is, set the appropriate cursor type and set :attr:`_mouse_location` to         ("rotate", `face index`)          Notes         -----         This code is executed after the check has been completed to see if the mouse is inside         the extract box. For this reason, we don't bother running a check to see if the mouse         is inside the box, as this code will never run if that is the case.          Parameters         ----------         event: :class:`tkinter.Event`             The current tkinter mouse event          Returns         -------         bool             ``True`` if cursor is over a rotate point otherwise ``False`` # Mouse click actions Add context menu to OS specific right click action. The action to perform when the user starts clicking and dragging the mouse.          Selects the correct extract box action based on the initial cursor position.          Parameters         ----------         event: :class:`tkinter.Event`             The tkinter mouse event. # pylint: disable=unused-argument Trigger a viewport thumbnail update on click + drag release          Parameters         ----------         event: :class:`tkinter.Event`             The tkinter mouse event. Required but unused. Updates the underlying detected faces landmarks based on mouse dragging delta,         which moves the Extract box on a drag event.          Parameters         ----------         event: :class:`tkinter.Event`             The tkinter mouse event. Resizes the landmarks contained within an extract box on a corner anchor drag event.          Parameters         ----------         event: :class:`tkinter.Event`             The tkinter mouse event. # Don't over shrink the box Ensure that a resize drag does is not going to cross the center point from it's initial         corner location.          Parameters         ----------         center: :class:`numpy.ndarray`             The (`x`, `y`) center point of the face extract box         box: :class:`numpy.ndarray`             The canvas coordinates of the extract box polygon's corners         position: : class:`numpy.ndarray`             The current (`x`, `y`) position of the mouse cursor          Returns         -------         bool             ``True`` if the drag operation does not cross the center point otherwise ``False`` # Generate lines that span the full frame (x and y) along the center point # Generate a line coming from the current corner location to the current cursor position # Check whether any of the generated lines intersect Check whether 3 points are counter clockwise from each other.          Parameters         ----------         point_a: :class:`numpy.ndarray`             The first (`x`, `y`) point to check for counter clockwise ordering         point_b: :class:`numpy.ndarray`             The second (`x`, `y`) point to check for counter clockwise ordering         point_c: :class:`numpy.ndarray`             The third (`x`, `y`) point to check for counter clockwise ordering          Returns         -------         bool             ``True`` if the 3 points are provided in counter clockwise order otherwise ``False`` Rotates the landmarks contained within an extract box on a corner rotate drag event.          Parameters         ----------         event: :class:`tkinter.Event`             The tkinter mouse event. Obtain the scaling for the extract box resize Create a right click context menu to delete the alignment that is being         hovered over. # pylint:disable=unused-argument Called by the right click delete event. Deletes the face that the mouse is currently         over.          Parameters         ----------         args: tuple (unused)             The event parameter is passed in by the hot key binding, so args is required