#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#

from __future__ import division
import pcbnew
import math


class FootprintWizard(pcbnew.FootprintWizardPlugin):
    """!
    A class to simplify many aspects of footprint creation, leaving only
    the foot-print specific routines to the wizards themselves.

    Inherit this class to make a new wizard.

    Provides simplified access to helpers like drawing functions, a transform
    matrix stack and simple parameter checking.

    Generally, you need to implement:
        GetValue()
        GenerateParameterList()
        CheckParameters()
        BuildThisFootprint()
        GetName()
        GetDescription()
    """

    # Copy units from pcbnew
    uMM = pcbnew.uMM
    uMils = pcbnew.uMils
    uFloat = pcbnew.uFloat
    uInteger = pcbnew.uInteger
    uBool = pcbnew.uBool
    uRadians = pcbnew.uRadians
    uDegrees = pcbnew.uDegrees
    uPercent = pcbnew.uPercent
    uString = pcbnew.uString

    def __init__(self):
        pcbnew.FootprintWizardPlugin.__init__(self)
        self.GenerateParameterList()

    def GetName(self):
        """!
        Return the name of the footprint wizard
        """
        raise NotImplementedError

    def GetDescription(self):
        """!
        Return the footprint wizard description
        """
        raise NotImplementedError

    def GetValue(self):
        """!
        Return the value (name) of the generated footprint
        """
        raise NotImplementedError

    def GenerateParameterList(self):
        """!
        Footprint parameter specification is done here
        """
        raise NotImplementedError

    def CheckParameters(self):
        """!
        Any custom parameter checking should be performed here
        """
        raise NotImplementedError

    def BuildThisFootprint(self):
        """!
        Draw the footprint.

        This is specific to each footprint class, you need to implement
        this to draw what you want
        """
        raise NotImplementedError

    # Do not override this method!
    def BuildFootprint(self):
        """!
        Actually make the footprint. We defer all but the set-up to
        the implementing class
        """

        self.buildmessages = ""
        self.module = pcbnew.FOOTPRINT(None)  # create a new module

        # Perform default checks on all parameters
        for p in self.params:
            p.ClearErrors()
            p.Check()  # use defaults

        self.CheckParameters()  # User error checks

        if self.AnyErrors():  # Errors were detected!

            self.buildmessages = ("Cannot build footprint: "
                                  "Parameters have errors:\n")

            for p in self.params:
                if len(p.error_list) > 0:
                    self.buildmessages += "['{page}']['{name}']:\n".format(
                        page=p.page, name=p.name)

                    for error in p.error_list:
                        self.buildmessages += "\t" + error + "\n"

            return

        self.buildmessages = (
            "Building new {name} footprint with the following parameters:\n"
            .format(name=self.name))

        self.buildmessages += self.Show()

        self.draw = FootprintWizardDrawingAids(
            self.module)

        self.module.SetValue(self.GetValue())
        self.module.SetReference("%s**" % self.GetReferencePrefix())

        fpid = pcbnew.LIB_ID("", self.module.GetValue())  # the lib name  (empty) and the name in library
        self.module.SetFPID(fpid)

        self.SetModule3DModel()  # add a 3D module if specified

        thick = self.GetTextThickness()

        self.module.Reference().SetTextThickness(thick)
        self.module.Value().SetTextThickness(thick)

        self.BuildThisFootprint()  # implementer's build function

        return

    def SetModule3DModel(self):
        """!
        If your plug-in sets a 3D model, override this function
        """
        pass

    def GetTextSize(self):
        """!
        Get the default text size for the footprint. Override to change it.

        Defaults to IPC nominal of 1.0mm
        """
        return pcbnew.FromMM(1.0)

    def GetTextThickness(self):
        """!
        Thicker than IPC guidelines (10% of text height = 0.12mm)
        as 5 wires/mm is a common silk screen limitation
        """
        return pcbnew.FromMM(0.15)


class FootprintWizardDrawingAids:
    """!
    Collection of handy functions to simplify drawing shapes from within
    footprint wizards

    A "drawing context" is provided which can be used to set and retain
    settings such as line thickness and layer. The DC also contains a
    "transform stack", which allows easy positioning and transforming of
    drawn elements without lots of geometric book-keeping.
    """

    # directions (in degrees, compass-like)
    dirN = 0
    dirNE = 45
    dirE = 90
    dirSE = 135
    dirS = 180
    dirSW = 225
    dirW = 270
    dirNW = 315

    # Flip constants
    flipNone = 0  # no flip transform
    flipX = 1  # flip X values, i.e. about the Y-axis
    flipY = 2  # flip Y values, i.e. about the X-axis
    flipBoth = 3  # flip X and Y values, equivalent to a 180-degree rotation

    xfrmIDENTITY = [1, 0, 0, 0, 1, 0]  # no transform

    # these values come from our KiCad Library Convention 0.11
    defaultLineThickness = pcbnew.FromMM(0.15)

    def DefaultGraphicLayer(self):
        return pcbnew.F_SilkS

    def DefaultTextValueLayer(self):
        return pcbnew.F_Fab

    def __init__(self, module):
        self.module = module
        # drawing context defaults
        self.dc = {
            'layer': self.DefaultGraphicLayer(),
            'lineThickness': self.defaultLineThickness,
            'transforms': [],
            'transform': self.xfrmIDENTITY
        }

    def PushTransform(self, mat):
        """!
        Add a transform to the top of the stack and recompute the
        overall transform

        @param mat: the transform matrix to add to the stack
        """
        self.dc['transforms'].append(mat)
        self.RecomputeTransforms()

    def PopTransform(self, num=1):
        """!
        Remove a transform from the top of the stack and recompute the
        overall transform

        @param num: the number of transforms to pop from the stack.
        @return the last popped transform
        """

        for i in range(num):
            mat = self.dc['transforms'].pop()
        self.RecomputeTransforms()
        return mat

    def ResetTransform(self):
        """!
        Reset the transform stack to the identity matrix.
        """
        self.dc['transforms'] = []
        self.RecomputeTransforms()

    def _ComposeMatricesWithIdentity(self, mats):
        """!
        Compose a sequence of matrices together by sequential
        pre-multiplication with the identity matrix.

        @param mats: list of matrices to compose
        @return: the composed transform matrix
        """

        x = self.xfrmIDENTITY

        for mat in mats:
            # Pre-compose with each transform in turn
            x = [
                x[0] * mat[0] + x[1] * mat[3],
                x[0] * mat[1] + x[1] * mat[4],
                x[0] * mat[2] + x[1] * mat[5] + x[2],
                x[3] * mat[0] + x[4] * mat[3],
                x[3] * mat[1] + x[4] * mat[4],
                x[3] * mat[2] + x[4] * mat[5] + x[5]]

        return x

    def RecomputeTransforms(self):
        """!
        Re-compute the transform stack into a single transform and
        store in the DC
        """
        self.dc['transform'] = self._ComposeMatricesWithIdentity(
            self.dc['transforms'])

    def TransformTranslate(self, x, y, push=True):
        """!
        Set up and return a transform matrix representing a translation
        optionally pushing onto the stack

        (   1  0   x  )
        (   0  1   y  )

        @param x: translation in x-direction
        @param y: translation in y-direction
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """
        mat = [1, 0, x, 0, 1, y]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformFlipOrigin(self, flip, push=True):
        """!
        Set up and return a transform matrix representing a horizontal,
        vertical or both flip about the origin

        @param flip: one of flipNone, flipX, flipY, flipBoth
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """
        mat = None
        if flip == self.flipX:
            mat = [-1, 0, 0, 0, 1, 0]
        elif flip == self.flipY:
            mat = [1, 0, 0, 0, -1, 0]
        elif flip == self.flipBoth:
            mat = [-1, 0, 0, 0, -1, 0]
        elif flip == self.flipNone:
            mat = self.xfrmIDENTITY
        else:
            raise ValueError

        if push:
            self.PushTransform(mat)
        return mat

    def TransformFlip(self, x, y, flip=flipNone, push=True):
        """!
        Set up and return a transform matrix representing a horizontal,
        vertical or both flip about a point (x,y)

        This is performed by a translate-to-origin, flip, translate-
        back sequence.

        @param x: the x coordinate of the flip point
        @param y: the y coordinate of the flip point
        @param flip: one of flipNone, flipX, flipY, flipBoth
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """
        mats = [self.TransformTranslate(x, y, push=False),
                self.TransformFlipOrigin(flip, push=False),
                self.TransformTranslate(-x, -y, push=False)]

        # Distil into a single matrix
        mat = self._ComposeMatricesWithIdentity(mats)

        if push:
            self.PushTransform(mat)
        return mat

    def TransformRotationOrigin(self, rot, push=True):
        """!
        Set up and return a transform matrix representing a rotation
        about the origin, and optionally push onto the stack

        (   cos(t)  -sin(t)   0  )
        (   sin(t)   cos(t)   0  )

        @param rot: the rotation angle in degrees
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """
        rads = rot * math.pi / 180
        mat = [math.cos(rads), -math.sin(rads), 0,
               math.sin(rads), math.cos(rads), 0]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformRotation(self, x, y, rot, push=True):
        """!
        Set up and return a transform matrix representing a rotation
        about the point (x,y), and optionally push onto the stack

        This is performed by a translate-to-origin, rotate, translate-
        back sequence

        @param x: the x coordinate of the rotation centre
        @param y: the y coordinate of the rotation centre
        @param rot: the rotation angle in degrees
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """

        mats = [self.TransformTranslate(x, y, push=False),
                self.TransformRotationOrigin(rot, push=False),
                self.TransformTranslate(-x, -y, push=False)]

        # Distil into a single matrix
        mat = self._ComposeMatricesWithIdentity(mats)

        if push:
            self.PushTransform(mat)
        return mat

    def TransformScaleOrigin(self, sx, sy=None, push=True):
        """!
        Set up and return a transform matrix representing a scale about
        the origin, and optionally push onto the stack

        (   sx   0   0  )
        (    0  sy   0  )

        @param sx: the scale factor in the x direction
        @param sy: the scale factor in the y direction
        @param push: add this transform to the current stack
        @return the generated transform matrix
        """

        if sy is None:
            sy = sx

        mat = [sx, 0, 0, 0, sy, 0]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformPoint(self, x, y, mat=None):
        """!
        Return a point (x, y) transformed by the given matrix, or if
        that is not given, the drawing context transform

        @param x: the x coordinate of the point to transform
        @param y: the y coordinate of the point to transform
        @param mat: the transform matrix to use or None to use the current DC's
        @return: the transformed point as a wxPoint
        """

        if not mat:
            mat = self.dc['transform']

        return pcbnew.wxPoint(x * mat[0] + y * mat[1] + mat[2],
                              x * mat[3] + y * mat[4] + mat[5])

    def SetLineThickness(self, lineThickness):
        """!
        Set the current pen lineThickness used for subsequent drawing
        operations

        @param lineThickness: the new line thickness to set
        """
        self.dc['lineThickness'] = lineThickness

    def SetLineTickness(self, lineThickness):
        """!
        Old version of SetLineThickness.
        Does the same thing, but is is only here for compatibility with old
        scripts.
        Set the current pen lineThickness used for subsequent drawing
        operations

        @param lineThickness: the new line thickness to set
        """
        self.SetLineThickness(lineThickness)

    def GetLineThickness(self):
        """!
        Get the current drawing context line thickness
        """
        return self.dc['lineThickness']

    def SetLayer(self, layer):
        """!
        Set the current drawing layer, used for subsequent drawing
        operations
        """
        self.dc['layer'] = layer

    def GetLayer(self):
        """!
        Return the current drawing layer, used for drawing operations
        """
        return self.dc['layer']

    def Line(self, x1, y1, x2, y2):
        """!
        Draw a line from (x1, y1) to (x2, y2)
        """
        outline = pcbnew.FP_SHAPE(self.module)
        outline.SetWidth(self.GetLineThickness())
        outline.SetLayer(self.GetLayer())
        outline.SetShape(pcbnew.S_SEGMENT)
        start = self.TransformPoint(x1, y1)
        end = self.TransformPoint(x2, y2)
        outline.SetStartEnd(start, end)
        self.module.Add(outline)

    def Circle(self, x, y, r, filled=False):
        """!
        Draw a circle at (x,y) of radius r
        If filled is true, the thickness and radius of the line will be set
        such that the circle appears filled

        @param x: the x coordinate of the arc centre
        @param y: the y coordinate of the arc centre
        @param r: the circle's radius
        @param filled: True to draw a filled circle, False to use the current
                       DC line thickness
        """

        circle = pcbnew.FP_SHAPE(self.module)
        start = self.TransformPoint(x, y)

        if filled:
            circle.SetWidth(r)
            end = self.TransformPoint(x, y + r/2)
        else:
            circle.SetWidth(self.dc['lineThickness'])
            end = self.TransformPoint(x, y + r)

        circle.SetLayer(self.dc['layer'])
        circle.SetShape(pcbnew.S_CIRCLE)
        circle.SetStartEnd(start, end)
        self.module.Add(circle)

    def MyCmp(self, n1, n2):
        """
        replace the cmp() of python2
        """
        if n1 < n2:
            return -1
        if n1 > n2:
            return 1
        return 0

    def Arc(self, cx, cy, sx, sy, a):
        """!
        Draw an arc based on centre, start and angle

        The transform matrix is applied

        Note that this won't work properly if the result is not a
        circular arc (e.g. a horizontal scale)

        @param cx: the x coordinate of the arc centre
        @param cy: the y coordinate of the arc centre
        @param sx: the x coordinate of the arc start point
        @param sy: the y coordinate of the arc start point
        @param a: the arc's central angle (in deci-degrees)
        """
        circle = pcbnew.FP_SHAPE(self.module)
        circle.SetWidth(self.dc['lineThickness'])

        center = self.TransformPoint(cx, cy)
        start = self.TransformPoint(sx, sy)

        circle.SetLayer(self.dc['layer'])
        circle.SetShape(pcbnew.S_ARC)

        # check if the angle needs to be reverse (a flip scaling)
        if self.MyCmp(self.dc['transform'][0], 0) != self.MyCmp(self.dc['transform'][4], 0):
            a = -a

        circle.SetAngle(a)
        circle.SetStartEnd(center, start)
        self.module.Add(circle)

    def HLine(self, x, y, l):
        """!
        Draw a horizontal line from (x,y), rightwards

        @param x: line start x coordinate
        @param y: line start y coordinate
        @param l: line length
        """
        self.Line(x, y, x + l, y)

    def VLine(self, x, y, l):
        """!
        Draw a vertical line from (x1,y1), downwards

        @param x: line start x coordinate
        @param y: line start y coordinate
        @param l: line length
        """
        self.Line(x, y, x, y + l)

    def Polyline(self, pts, mirrorX=None, mirrorY=None):
        """!
        Draw a polyline, optionally mirroring around the given points

        @param pts: list of polyline vertices (list of (x, y))
        @param mirrorX: x coordinate of mirror point (None for no x-flip)
        @param mirrorY: y coordinate of mirror point (None for no y-flip)
        """

        def _PolyLineInternal(pts):
            if len(pts) < 2:
                return

            for i in range(0, len(pts) - 1):
                self.Line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])

        _PolyLineInternal(pts)  # original

        if mirrorX is not None and mirrorY is not None:
            self.TransformFlip(mirrorX, mirrorY, self.flipBoth)  # both
            _PolyLineInternal(pts)
            self.PopTransform()

        elif mirrorX is not None:
            self.TransformFlip(mirrorX, 0, self.flipX)
            _PolyLineInternal(pts)
            self.PopTransform()

        elif mirrorY is not None:
            self.TransformFlip(0, mirrorY, self.flipY)
            _PolyLineInternal(pts)
            self.PopTransform()

    def Reference(self, x, y, size, orientation_degree=0):
        """!
        Draw the module's reference as the given point.

        The actual setting of the reference is not done in this drawing
        aid - that is up to the wizard

        @param x: the x position of the reference
        @param y: the y position of the reference
        @param size: the text size (in both directions)
        @param orientation_degree: text orientation in degrees
        """

        text_size = pcbnew.wxSize(size, size)

        self.module.Reference().SetPos0(self.TransformPoint(x, y))
        self.module.Reference().SetPosition(
            self.module.Reference().GetPos0())
        self.module.Reference().SetTextSize(text_size)
        # internal angles are in 0.1 deg
        self.module.Reference().SetTextAngle(orientation_degree * 10)

    def Value(self, x, y, size, orientation_degree=0):
        """!
        As for references, draw the module's value

        @param x: the x position of the value
        @param y: the y position of the value
        @param size: the text size (in both directions)
        @param orientation_degree: text orientation in degrees
        """
        text_size = pcbnew.wxSize(size, size)

        self.module.Value().SetPos0(self.TransformPoint(x, y))
        self.module.Value().SetPosition(self.module.Value().GetPos0())
        self.module.Value().SetTextSize(text_size)
        self.module.Value().SetLayer(self.DefaultTextValueLayer())
        # internal angles are in 0.1 deg
        self.module.Value().SetTextAngle(orientation_degree * 10)

    def Box(self, x, y, w, h):
        """!
        Draw a rectangular box, centred at (x,y), with given width and
        height

        @param x: the x coordinate of the box's centre
        @param y: the y coordinate of the box's centre
        @param w: the width of the box
        @param h: the height of the box
        """

        pts = [[x - w/2, y - h/2],  # left
               [x + w/2, y - h/2],  # right
               [x + w/2, y + h/2],  # bottom
               [x - w/2, y + h/2],  # top
               [x - w/2, y - h/2]]  # close

        self.Polyline(pts)

    def NotchedCircle(self, x, y, r, notch_w, notch_h, rotate=0):
        """!
        Circle radius r centred at (x, y) with a raised or depressed notch
        at the top
        Notch height is measured from the top of the circle radius

        @param x: the x coordinate of the circle's centre
        @param y: the y coordinate of the circle's centre
        @param r: the radius of the circle
        @param notch_w: the width of the notch
        @param notch_h: the height of the notch
        @param rotate: the rotation of the whole figure, in degrees
        """

        self.TransformRotation(x, y, rotate)

        # find the angle where the notch vertical meets the circle
        angle_intercept = math.asin(notch_w/(2 * r))

        # and find the co-ords of this point
        sx = math.sin(angle_intercept) * r
        sy = -math.cos(angle_intercept) * r

        # NOTE: this may be out by a factor of ten one day
        arc_angle = (math.pi * 2 - angle_intercept * 2) * (1800/math.pi)

        self.Arc(x, y, sx, sy, arc_angle)

        pts = [[sx,  sy],
               [sx,  -r - notch_h],
               [-sx, -r - notch_h],
               [-sx, sy]]

        self.Polyline(pts)
        self.PopTransform()

    def NotchedBox(self, x, y, w, h, notchW, notchH, rotate=0):
        """!
        Draw a box with a notch in the centre of the top edge

        @param x: the x coordinate of the circle's centre
        @param y: the y coordinate of the circle's centre
        @param w: the width of the box
        @param h: the height of the box
        @param notchW: the width of the notch
        @param notchH: the height of the notch
        @param rotate: the rotation of the whole figure, in degrees
        """

        self.TransformRotation(x, y, rotate)

        # limit to half the overall width
        notchW = min(x + w/2, notchW)

        # draw notch
        self.Polyline([  # three sides of box
            (x - w/2, y - h/2),
            (x - w/2, y + h/2),
            (x + w/2, y + h/2),
            (x + w/2, y - h/2),
            # the notch
            (notchW/2, y - h/2),
            (notchW/2, y - h/2 + notchH),
            (-notchW/2, y - h/2 + notchH),
            (-notchW/2, y - h/2),
            (x - w/2, y - h/2)
        ])

        self.PopTransform()

    def BoxWithDiagonalAtCorner(self, x, y, w, h,
                                setback=pcbnew.FromMM(1.27), flip=flipNone):
        """!
        Draw a box with a diagonal at the top left corner.

        @param x: the x coordinate of the circle's centre
        @param y: the y coordinate of the circle's centre
        @param w: the width of the box
        @param h: the height of the box
        @param setback: the set-back of the diagonal, in both x and y
        @param flip: one of flipNone, flipX, flipY or flipBoth to change the
         diagonal corner
        """

        self.TransformFlip(x, y, flip, push=True)

        pts = [[x - w/2 + setback, y - h/2],
               [x - w/2,           y - h/2 + setback],
               [x - w/2,           y + h/2],
               [x + w/2,           y + h/2],
               [x + w/2,           y - h/2],
               [x - w/2 + setback, y - h/2]]

        self.Polyline(pts)

        self.PopTransform()

    def BoxWithOpenCorner(self, x, y, w, h,
                          setback=pcbnew.FromMM(1.27), flip=flipNone):
        """!
        Draw a box with an opening at the top left corner

        @param x: the x coordinate of the circle's centre
        @param y: the y coordinate of the circle's centre
        @param w: the width of the box
        @param h: the height of the box
        @param setback: the set-back of the opening, in both x and y
        @param flip: one of flipNone, flipX, flipY or flipBoth to change the
         open corner position
        """

        self.TransformTranslate(x, y)
        self.TransformFlipOrigin(flip)

        pts = [[- w/2,           - h/2 + setback],
               [- w/2,           + h/2],
               [+ w/2,           + h/2],
               [+ w/2,           - h/2],
               [- w/2 + setback, - h/2]]

        self.Polyline(pts)

        self.PopTransform(num=2)

    def RoundedBox(self, x, y, w, h, rad):
        """!
        Draw a box with rounded corners (i.e. a 90-degree circular arc)

        :param x: the x coordinate of the box's centre
        :param y: the y coordinate of the box's centre
        :param w: the width of the box
        :param h: the height of the box
        :param rad: the radius of the corner rounds
        """

        x_inner = w - rad * 2
        y_inner = h - rad * 2

        x_left = x - w / 2
        y_top = y - h / 2

        # Draw straight sections
        self.HLine(x_left + rad, y_top, x_inner)
        self.HLine(x_left + rad, -y_top, x_inner)

        self.VLine(x_left, y_top + rad, y_inner)
        self.VLine(-x_left, y_top + rad, y_inner)

        # corner arcs
        ninety_deg = 90 * 10  # deci-degs
        cx = x - w / 2 + rad
        cy = y - h / 2 + rad

        # top left
        self.Arc(+cx, +cy, +x_left, +cy, +ninety_deg)
        self.Arc(-cx, +cy, -x_left, +cy, -ninety_deg)
        self.Arc(+cx, -cy, +x_left, -cy, -ninety_deg)
        self.Arc(-cx, -cy, -x_left, -cy, +ninety_deg)

    def ChamferedBox(self, x, y, w, h, chamfer_x, chamfer_y):
        """!
        Draw a box with chamfered corners.

        :param x: the x coordinate of the box's centre
        :param y: the y coordinate of the box's centre
        :param w: the width of the box
        :param h: the height of the box
        :param chamfer_x: the size of the chamfer set-back in the x direction
        :param chamfer_y: the size of the chamfer set-back in the y direction
        """
        # outermost dimensions
        x_left = x - w / 2
        y_top = y - h / 2

        # x and y coordinates of inner edges of chamfers
        x_inner = x_left + chamfer_x
        y_inner = y_top + chamfer_y

        pts = [
            [+x_inner, +y_top],
            [-x_inner, +y_top],
            [-x_left,  +y_inner],
            [-x_left,  -y_inner],
            [-x_inner, -y_top],
            [+x_inner, -y_top],
            [+x_left,  -y_inner],
            [+x_left,  +y_inner],
            [+x_inner, +y_top],
        ]

        self.draw.Polyline(pts)

    def MarkerArrow(self, x, y, direction=dirN, width=pcbnew.FromMM(1)):
        """!
        Draw a marker arrow facing in the given direction, with the
        point at (x,y)

        @param x: x position of the arrow tip
        @param y: y position of the arrow tip
        @param direction: arrow direction in degrees (0 is "north", can use
         dir* shorthands)
        @param width: arrow width
        """

        self.TransformTranslate(x, y)
        self.TransformRotationOrigin(direction)

        pts = [[0,          0],
               [width / 2,  width / 2],
               [-width / 2, width / 2],
               [0,          0]]

        self.Polyline(pts)
        self.PopTransform(2)
