#!/usr/bin/python3

"""
This module contains the custom projection for a north-up polar plot.

The NorthPolarAxes-class is a polar projection that has its origin (0 degrees)
at the top and counts clockwise in the positive direction. This makes it
easier and fater to plot azimuth measurements. The class is called from the
PlotSettings class when the main window calls for a rose-diagram subplot. 
Source:
http://stackoverflow.com/questions/2417794/how-to-make-the-angles-in-a-
matplotlib-polar-plot-go-clockwise-with-0%C2%B0-at-the-to?lq=1
"""

import numpy as np
from matplotlib.projections import PolarAxes, register_projection
from matplotlib.transforms import Affine2D, Bbox, IdentityTransform
from matplotlib.axes import Axes
from matplotlib.patches import Wedge
import matplotlib.spines as mspines


class NorthPolarAxes(PolarAxes):
    # pylint: disable=no-init

    """
    Custom MPL-PolarAxes with theta 0 in the north and counting clockwise.

    This class inherits from, and overrides some of the methods of the
    Matplotlib PolarAxes-class. This class contains the NorthPolarTransfrom-
    and InvertedNorthPolarTransform-class. It also overrides the name-string
    and the '_set_lim_and_transforms'-function.
    """

    name = "northpolar"

    class NorthPolarTransform(PolarAxes.PolarTransform):
        # pylint: disable=no-init

        """
        Custom normal transformation for the NorthPolarAxes.

        This class contains two function that override function in the
        Matplotlib PolarAxes.PolarTransform-class. The transformation is
        changed so 0 degrees is in the North and theta counts clockwise.
        """

        def transform(self, tr):
            # pylint: disable=no-self-use,invalid-name
            """
            Overrides the transformation of the PolarTranform-class.

            This method overrides the same method in the PolarTransform-class.
            The new tranformation is North-up and counts clockwise positive.
            """
            xy = np.zeros(tr.shape, np.float_)
            t = tr[:, 0:1]  # pylint: disable=invalid-name
            r = tr[:, 1:2]  # pylint: disable=invalid-name
            x = xy[:, 0:1]  # pylint: disable=invalid-name
            y = xy[:, 1:2]  # pylint: disable=invalid-name
            x[:] = r * np.sin(t)
            y[:] = r * np.cos(t)
            return xy

        transform_non_affine = transform

        def inverted(self):
            # pylint: disable=no-self-use
            """
            Returns the inverted transformation class.

            The transformation also has to have a method to return the
            inverted transformation.
            """
            return NorthPolarAxes.InvertedNorthPolarTransform()

    class InvertedNorthPolarTransform(PolarAxes.InvertedPolarTransform):
        # pylint: disable=no-init

        """
        Custom inverted transformation for the NorthPolarAxes.

        This class contains two function that override function in the
        Matplotlib PolarAxes.InvertedPolarTransform-class. The transformation
        is changed so 0 degrees is in the North and theta counts clockwise.
        """

        def transform(self, xy):
            # pylint: disable=no-self-use,invalid-name
            """
            Overrides the transformation of the InvertedPolarTransform-class.

            This method overrides the same method in the
            InvertedPolarTransform-class. The new tranformation is North-up
            and counts clockwise positive.
            """
            x = xy[:, 0:1]  # pylint: disable=invalid-name
            y = xy[:, 1:]  # pylint: disable=invalid-name
            r = np.sqrt(x * x + y * y)  # pylint: disable=invalid-name
            theta = np.arctan2(y, x)
            return np.concatenate((theta, r), 1)

        def inverted(self):
            # pylint: disable=no-self-use
            """
            Returns the normal transformation class.

            The inverted transformation also has to have a method to return the
            normal transformation.
            """
            return NorthPolarAxes.NorthPolarTransform()

    def _set_lim_and_transforms(self):
        """
        Overrides the method with the same name in the PolarAxes-class.

        This method replaces the same method in the PolarAxes-class. It ensures
        that the limits and label placement fit the north-polar projection.
        """
        PolarAxes._set_lim_and_transforms(self)
        self.transProjection = self.NorthPolarTransform()
        # pylint: attribute-defined-outside-init,invalid-name
        self.transData = (
            self.transScale + 
            self.transProjection + 
            (self.transProjectionAffine + self.transAxes))
        # pylint: attribute-defined-outside-init,invalid-name
        self._xaxis_transform = (
            self.transProjection +
            self.PolarAffine(IdentityTransform(), Bbox.unit()) +
            self.transAxes)  # pylint: attribute-defined-outside-init
        self._xaxis_text1_transform = (
            self._theta_label1_position +
            self._xaxis_transform)  # pylint: attribute-defined-outside-init
        self._yaxis_transform = (
            Affine2D().scale(np.pi * 2.0, 1.0) +
            self.transData)  # pylint: attribute-defined-outside-init
        self._yaxis_text1_transform = (
            Affine2D().scale(1.0 / 360.0, 1.0) +
            self._yaxis_transform)  # pylint: attribute-defined-outside-init

register_projection(NorthPolarAxes)


class DipPolarAxes(PolarAxes):
    # pylint: disable=no-init

    """
    """

    name = "dippolar"

    def cla(self):
        PolarAxes.cla(self)
        self.set_thetagrids([0, 15, 30, 45, 60, 75, 90])

    def _gen_axes_patch(self):
        return Wedge((0.5, 0.5), 0.5, 270, 360)

    def _gen_axes_spines(self):
        path = Wedge((0, 0), 1.0, 270, 360).get_path()
        spine = mspines.Spine(self, 'circle', path)
        spine.set_patch_circle((0.5, 0.5), 0.5)
        return {'wedge':spine}

    class DipPolarTransform(PolarAxes.PolarTransform):
        # pylint: disable=no-init

        """
        """

        def transform(self, tr):
            # pylint: disable=no-self-use,invalid-name
            """
            """
            xy = np.zeros(tr.shape, np.float_)
            t = tr[:, 0:1]  # pylint: disable=invalid-name
            r = tr[:, 1:2]  # pylint: disable=invalid-name
            x = xy[:, 0:1]  # pylint: disable=invalid-name
            y = xy[:, 1:2]  # pylint: disable=invalid-name
            x[:] = r * np.sin(t + np.pi / 2)
            y[:] = r * np.cos(t + np.pi / 2)
            return xy

        transform_non_affine = transform

        def inverted(self):
            # pylint: disable=no-self-use
            """
            """
            return DipPolarAxes.InvertedDipPolarTransform()

    class InvertedDipPolarTransform(PolarAxes.InvertedPolarTransform):
        # pylint: disable=no-init

        """
        """

        def transform(self, xy):
            # pylint: disable=no-self-use,invalid-name
            """
            """
            x = xy[:, 0:1]  # pylint: disable=invalid-name
            y = xy[:, 1:]  # pylint: disable=invalid-name
            r = np.sqrt(x * x + y * y)  # pylint: disable=invalid-name
            theta = np.arctan2(y, x)
            return np.concatenate((theta, r), 1)

        def inverted(self):
            # pylint: disable=no-self-use
            """
            """
            return DipPolarAxes.DipPolarTransform()

    def _set_lim_and_transforms(self):
        """
        """
        PolarAxes._set_lim_and_transforms(self)
        self.transProjection = self.DipPolarTransform()
        # pylint: attribute-defined-outside-init,invalid-name
        self.transData = (
            self.transScale +
            self.transProjection +
            (self.transProjectionAffine + self.transAxes))
        # pylint: attribute-defined-outside-init,invalid-name
        self._xaxis_transform = (
            self.transProjection +
            self.PolarAffine(IdentityTransform(), Bbox.unit()) +
            self.transAxes)  # pylint: attribute-defined-outside-init
        self._xaxis_text1_transform = (
            self._theta_label1_position +
            self._xaxis_transform)  # pylint: attribute-defined-outside-init
        self._yaxis_transform = (
            Affine2D().scale(np.pi * 2.0, 1.0) +
            self.transData)  # pylint: attribute-defined-outside-init
        self._yaxis_text1_transform = (
            Affine2D().scale(1.0 / 360.0, 1.0) +
            self._yaxis_transform)  # pylint: attribute-defined-outside-init

register_projection(DipPolarAxes)