# Copyright 2003-2008 by Leighton Pritchard.  All rights reserved.
# Revisions copyright 2008-2017 by Peter Cock.
#
# This file is part of the Biopython distribution and governed by your
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
# Please see the LICENSE file that should have been included as part of this
# package.
#
# Contact:       Leighton Pritchard, The James Hutton Institute,
#                Invergowrie, Dundee, Scotland, DD2 5DA, UK
#                Leighton.Pritchard@hutton.ac.uk
################################################################################

"""CircularDrawer module for GenomeDiagram."""

# ReportLab imports

from reportlab.graphics.shapes import Drawing, String, Group, Line, Circle, Polygon
from reportlab.lib import colors
from reportlab.graphics.shapes import ArcPath

# GenomeDiagram imports
from ._AbstractDrawer import AbstractDrawer, draw_polygon, intermediate_points
from ._AbstractDrawer import _stroke_and_fill_colors
from ._FeatureSet import FeatureSet
from ._GraphSet import GraphSet

from math import pi, cos, sin


class CircularDrawer(AbstractDrawer):
    """Object for drawing circular diagrams.

    Attributes:
     - tracklines    Boolean for whether to draw lines dilineating tracks
     - pagesize      Tuple describing the size of the page in pixels
     - x0            Float X co-ord for leftmost point of drawable area
     - xlim          Float X co-ord for rightmost point of drawable area
     - y0            Float Y co-ord for lowest point of drawable area
     - ylim          Float Y co-ord for topmost point of drawable area
     - pagewidth     Float pixel width of drawable area
     - pageheight    Float pixel height of drawable area
     - xcenter       Float X co-ord of center of drawable area
     - ycenter       Float Y co-ord of center of drawable area
     - start         Int, base to start drawing from
     - end           Int, base to stop drawing at
     - length        Size of sequence to be drawn
     - track_size    Float (0->1) the proportion of the track height to draw in
     - drawing       Drawing canvas
     - drawn_tracks  List of ints denoting which tracks are to be drawn
     - current_track_level   Int denoting which track is currently being drawn
     - track_offsets     Dictionary of number of pixels that each track top,
       center and bottom is offset from the base of a fragment, keyed by track
     - sweep     Float (0->1) the proportion of the circle circumference to
       use for the diagram
     - cross_track_links List of tuples each with four entries (track A,
       feature A, track B, feature B) to be linked.

    """

    def __init__(
        self,
        parent=None,
        pagesize="A3",
        orientation="landscape",
        x=0.05,
        y=0.05,
        xl=None,
        xr=None,
        yt=None,
        yb=None,
        start=None,
        end=None,
        tracklines=0,
        track_size=0.75,
        circular=1,
        circle_core=0.0,
        cross_track_links=None,
    ):
        """Create CircularDrawer object.

        Arguments:
         - parent    Diagram object containing the data that the drawer
           draws
         - pagesize  String describing the ISO size of the image, or a tuple
           of pixels
         - orientation   String describing the required orientation of the
           final drawing ('landscape' or 'portrait')
         - x         Float (0->1) describing the relative size of the X
           margins to the page
         - y         Float (0->1) describing the relative size of the Y
           margins to the page
         - xl        Float (0->1) describing the relative size of the left X
           margin to the page (overrides x)
         - xl        Float (0->1) describing the relative size of the left X
           margin to the page (overrides x)
         - xr        Float (0->1) describing the relative size of the right X
           margin to the page (overrides x)
         - yt        Float (0->1) describing the relative size of the top Y
           margin to the page (overrides y)
         - yb        Float (0->1) describing the relative size of the lower Y
           margin to the page (overrides y)
         - start     Int, the position to begin drawing the diagram at
         - end       Int, the position to stop drawing the diagram at
         - tracklines    Boolean flag to show (or not) lines delineating tracks
           on the diagram
         - track_size    The proportion of the available track height that
           should be taken up in drawing
         - circular      Boolean flaw to show whether the passed sequence is
           circular or not
         - circle_core   The proportion of the available radius to leave
           empty at the center of a circular diagram (0 to 1).
         - cross_track_links List of tuples each with four entries (track A,
           feature A, track B, feature B) to be linked.

        """
        # Use the superclass' instantiation method
        AbstractDrawer.__init__(
            self,
            parent,
            pagesize,
            orientation,
            x,
            y,
            xl,
            xr,
            yt,
            yb,
            start,
            end,
            tracklines,
            cross_track_links,
        )

        # Useful measurements on the page
        self.track_size = track_size
        self.circle_core = circle_core
        # Determine proportion of circumference around which information will be drawn
        if not circular:
            self.sweep = 0.9
        else:
            self.sweep = 1.0

    def set_track_heights(self):
        """Initialize track heights.

        Since tracks may not be of identical heights, the bottom and top
        radius for each track is stored in a dictionary - self.track_radii,
        keyed by track number
        """
        bot_track = min(min(self.drawn_tracks), 1)
        top_track = max(self.drawn_tracks)  # The 'highest' track to draw

        trackunit_sum = 0  # Total number of 'units' taken up by all tracks
        trackunits = {}  # Start and & units for each track keyed by track number
        heightholder = 0  # placeholder variable
        for track in range(bot_track, top_track + 1):  # track numbers to 'draw'
            try:
                trackheight = self._parent[track].height  # Get track height
            except Exception:  # TODO: ValueError? IndexError?
                trackheight = 1
            trackunit_sum += trackheight  # increment total track unit height
            trackunits[track] = (heightholder, heightholder + trackheight)
            heightholder += trackheight  # move to next height

        max_radius = 0.5 * min(self.pagewidth, self.pageheight)
        trackunit_height = max_radius * (1 - self.circle_core) / trackunit_sum
        track_core = max_radius * self.circle_core

        # Calculate top and bottom radii for each track
        self.track_radii = {}  # The inner, outer and center radii for each track
        track_crop = (
            trackunit_height * (1 - self.track_size) / 2.0
        )  # 'step back' in pixels
        for track in trackunits:
            top = trackunits[track][1] * trackunit_height - track_crop + track_core
            btm = trackunits[track][0] * trackunit_height + track_crop + track_core
            ctr = btm + (top - btm) / 2.0
            self.track_radii[track] = (btm, ctr, top)

    def draw(self):
        """Draw a circular diagram of the stored data."""
        # Instantiate the drawing canvas
        self.drawing = Drawing(self.pagesize[0], self.pagesize[1])

        feature_elements = []  # holds feature elements
        feature_labels = []  # holds feature labels
        greytrack_bgs = []  # holds track background
        greytrack_labels = []  # holds track foreground labels
        scale_axes = []  # holds scale axes
        scale_labels = []  # holds scale axis labels

        # Get tracks to be drawn and set track sizes
        self.drawn_tracks = self._parent.get_drawn_levels()
        self.set_track_heights()

        # Go through each track in the parent (if it is to be drawn) one by
        # one and collate the data as drawing elements
        for track_level in self._parent.get_drawn_levels():
            self.current_track_level = track_level
            track = self._parent[track_level]
            gbgs, glabels = self.draw_greytrack(track)  # Greytracks
            greytrack_bgs.append(gbgs)
            greytrack_labels.append(glabels)
            features, flabels = self.draw_track(track)  # Features and graphs
            feature_elements.append(features)
            feature_labels.append(flabels)
            if track.scale:
                axes, slabels = self.draw_scale(track)  # Scale axes
                scale_axes.append(axes)
                scale_labels.append(slabels)

        feature_cross_links = []
        for cross_link_obj in self.cross_track_links:
            cross_link_elements = self.draw_cross_link(cross_link_obj)
            if cross_link_elements:
                feature_cross_links.append(cross_link_elements)

        # Groups listed in order of addition to page (from back to front)
        # Draw track backgrounds
        # Draw feature cross track links
        # Draw features and graphs
        # Draw scale axes
        # Draw scale labels
        # Draw feature labels
        # Draw track labels
        element_groups = [
            greytrack_bgs,
            feature_cross_links,
            feature_elements,
            scale_axes,
            scale_labels,
            feature_labels,
            greytrack_labels,
        ]
        for element_group in element_groups:
            for element_list in element_group:
                [self.drawing.add(element) for element in element_list]

        if self.tracklines:
            # Draw test tracks over top of diagram
            self.draw_test_tracks()

    def draw_track(self, track):
        """Return list of track elements and list of track labels."""
        track_elements = []  # Holds elements for features and graphs
        track_labels = []  # Holds labels for features and graphs

        # Distribution dictionary for dealing with different set types
        set_methods = {FeatureSet: self.draw_feature_set, GraphSet: self.draw_graph_set}

        for set in track.get_sets():  # Draw the feature or graph sets
            elements, labels = set_methods[set.__class__](set)
            track_elements += elements
            track_labels += labels
        return track_elements, track_labels

    def draw_feature_set(self, set):
        """Return list of feature elements and list of labels for them."""
        # print('draw feature set')
        feature_elements = []  # Holds diagram elements belonging to the features
        label_elements = []  # Holds diagram elements belonging to feature labels

        # Collect all the elements for the feature set
        for feature in set.get_features():
            if self.is_in_bounds(feature.start) or self.is_in_bounds(feature.end):
                features, labels = self.draw_feature(feature)
                feature_elements += features
                label_elements += labels

        return feature_elements, label_elements

    def draw_feature(self, feature):
        """Return list of feature elements and list of labels for them."""
        feature_elements = []  # Holds drawable elements for a single feature
        label_elements = []  # Holds labels for a single feature

        if feature.hide:  # Don't show feature: return early
            return feature_elements, label_elements

        start, end = self._current_track_start_end()
        # A single feature may be split into subfeatures, so loop over them
        for locstart, locend in feature.locations:
            if locend < start:
                continue
            locstart = max(locstart, start)
            if end < locstart:
                continue
            locend = min(locend, end)
            # Get sigil for the feature/ each subfeature
            feature_sigil, label = self.get_feature_sigil(feature, locstart, locend)
            feature_elements.append(feature_sigil)
            if label is not None:  # If there's a label
                label_elements.append(label)

        return feature_elements, label_elements

    def get_feature_sigil(self, feature, locstart, locend, **kwargs):
        """Return graphics for feature, and any required label for it.

        Arguments:
         - feature       Feature object
         - locstart      The start position of the feature
         - locend        The end position of the feature

        """
        # Establish the coordinates for the sigil
        btm, ctr, top = self.track_radii[self.current_track_level]

        startangle, startcos, startsin = self.canvas_angle(locstart)
        endangle, endcos, endsin = self.canvas_angle(locend)
        midangle, midcos, midsin = self.canvas_angle((locend + locstart) / 2)

        # Distribution dictionary for various ways of drawing the feature
        # Each method takes the inner and outer radii, the start and end angle
        # subtended at the diagram center, and the color as arguments
        draw_methods = {
            "BOX": self._draw_sigil_box,
            "OCTO": self._draw_sigil_cut_corner_box,
            "JAGGY": self._draw_sigil_jaggy,
            "ARROW": self._draw_sigil_arrow,
            "BIGARROW": self._draw_sigil_big_arrow,
        }

        # Get sigil for the feature, location dependent on the feature strand
        method = draw_methods[feature.sigil]
        kwargs["head_length_ratio"] = feature.arrowhead_length
        kwargs["shaft_height_ratio"] = feature.arrowshaft_height

        # Support for clickable links... needs ReportLab 2.4 or later
        # which added support for links in SVG output.
        if hasattr(feature, "url"):
            kwargs["hrefURL"] = feature.url
            kwargs["hrefTitle"] = feature.name

        sigil = method(
            btm,
            ctr,
            top,
            startangle,
            endangle,
            feature.location.strand,
            color=feature.color,
            border=feature.border,
            **kwargs,
        )

        if feature.label:  # Feature needs a label
            # The spaces are a hack to force a little space between the label
            # and the edge of the feature
            label = String(
                0,
                0,
                f" {feature.name.strip()} ",
                fontName=feature.label_font,
                fontSize=feature.label_size,
                fillColor=feature.label_color,
            )
            labelgroup = Group(label)
            if feature.label_strand:
                strand = feature.label_strand
            else:
                strand = feature.location.strand
            if feature.label_position in ("start", "5'", "left"):
                # Position the label at the feature's start
                if strand != -1:
                    label_angle = startangle + 0.5 * pi  # Make text radial
                    sinval, cosval = startsin, startcos
                else:
                    label_angle = endangle + 0.5 * pi  # Make text radial
                    sinval, cosval = endsin, endcos
            elif feature.label_position in ("middle", "center", "centre"):
                # Position the label at the feature's midpoint
                label_angle = midangle + 0.5 * pi  # Make text radial
                sinval, cosval = midsin, midcos
            elif feature.label_position in ("end", "3'", "right"):
                # Position the label at the feature's end
                if strand != -1:
                    label_angle = endangle + 0.5 * pi  # Make text radial
                    sinval, cosval = endsin, endcos
                else:
                    label_angle = startangle + 0.5 * pi  # Make text radial
                    sinval, cosval = startsin, startcos
            elif startangle < pi:
                # Default to placing the label the bottom of the feature
                # as drawn on the page, meaning feature end on left half
                label_angle = endangle + 0.5 * pi  # Make text radial
                sinval, cosval = endsin, endcos
            else:
                # Default to placing the label on the bottom of the feature,
                # which means the feature end when on right hand half
                label_angle = startangle + 0.5 * pi  # Make text radial
                sinval, cosval = startsin, startcos
            if strand != -1:
                # Feature label on top
                radius = top
                if startangle < pi:  # Turn text round
                    label_angle -= pi
                else:
                    labelgroup.contents[0].textAnchor = "end"
            else:
                # Feature label on bottom
                radius = btm
                if startangle < pi:  # Turn text round and anchor end
                    label_angle -= pi
                    labelgroup.contents[0].textAnchor = "end"
            x_pos = self.xcenter + radius * sinval
            y_pos = self.ycenter + radius * cosval
            coslabel = cos(label_angle)
            sinlabel = sin(label_angle)
            labelgroup.transform = (
                coslabel,
                -sinlabel,
                sinlabel,
                coslabel,
                x_pos,
                y_pos,
            )
        else:
            # No label required
            labelgroup = None
        # if locstart > locend:
        #    print(locstart, locend, feature.location.strand, sigil, feature.name)
        # print(locstart, locend, feature.name)
        return sigil, labelgroup

    def draw_cross_link(self, cross_link):
        """Draw a cross-link between features."""
        startA = cross_link.startA
        startB = cross_link.startB
        endA = cross_link.endA
        endB = cross_link.endB

        if not self.is_in_bounds(startA) and not self.is_in_bounds(endA):
            return None
        if not self.is_in_bounds(startB) and not self.is_in_bounds(endB):
            return None

        if startA < self.start:
            startA = self.start
        if startB < self.start:
            startB = self.start
        if self.end < endA:
            endA = self.end
        if self.end < endB:
            endB = self.end

        trackobjA = cross_link._trackA(list(self._parent.tracks.values()))
        trackobjB = cross_link._trackB(list(self._parent.tracks.values()))
        assert trackobjA is not None
        assert trackobjB is not None
        if trackobjA == trackobjB:
            raise NotImplementedError

        if trackobjA.start is not None:
            if endA < trackobjA.start:
                return
            startA = max(startA, trackobjA.start)
        if trackobjA.end is not None:
            if trackobjA.end < startA:
                return
            endA = min(endA, trackobjA.end)
        if trackobjB.start is not None:
            if endB < trackobjB.start:
                return
            startB = max(startB, trackobjB.start)
        if trackobjB.end is not None:
            if trackobjB.end < startB:
                return
            endB = min(endB, trackobjB.end)

        for track_level in self._parent.get_drawn_levels():
            track = self._parent[track_level]
            if track == trackobjA:
                trackA = track_level
            if track == trackobjB:
                trackB = track_level
        if trackA == trackB:
            raise NotImplementedError

        startangleA, startcosA, startsinA = self.canvas_angle(startA)
        startangleB, startcosB, startsinB = self.canvas_angle(startB)
        endangleA, endcosA, endsinA = self.canvas_angle(endA)
        endangleB, endcosB, endsinB = self.canvas_angle(endB)

        btmA, ctrA, topA = self.track_radii[trackA]
        btmB, ctrB, topB = self.track_radii[trackB]

        if ctrA < ctrB:
            return [
                self._draw_arc_poly(
                    topA,
                    btmB,
                    startangleA,
                    endangleA,
                    startangleB,
                    endangleB,
                    cross_link.color,
                    cross_link.border,
                    cross_link.flip,
                )
            ]
        else:
            return [
                self._draw_arc_poly(
                    btmA,
                    topB,
                    startangleA,
                    endangleA,
                    startangleB,
                    endangleB,
                    cross_link.color,
                    cross_link.border,
                    cross_link.flip,
                )
            ]

    def draw_graph_set(self, set):
        """Return list of graph elements and list of their labels.

        Arguments:
         - set       GraphSet object

        """
        # print('draw graph set')
        elements = []  # Holds graph elements

        # Distribution dictionary for how to draw the graph
        style_methods = {
            "line": self.draw_line_graph,
            "heat": self.draw_heat_graph,
            "bar": self.draw_bar_graph,
        }

        for graph in set.get_graphs():
            elements += style_methods[graph.style](graph)

        return elements, []

    def draw_line_graph(self, graph):
        """Return line graph as list of drawable elements.

        Arguments:
         - graph     GraphData object

        """
        line_elements = []  # holds drawable elements

        # Get graph data
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        btm, ctr, top = self.track_radii[self.current_track_level]
        trackheight = 0.5 * (top - btm)
        datarange = maxval - minval
        if datarange == 0:
            datarange = trackheight

        start, end = self._current_track_start_end()
        data = graph[start:end]

        if not data:
            return []

        # midval is the value at which the x-axis is plotted, and is the
        # central ring in the track
        if graph.center is None:
            midval = (maxval + minval) / 2.0
        else:
            midval = graph.center
        # Whichever is the greatest difference: max-midval or min-midval, is
        # taken to specify the number of pixel units resolved along the
        # y-axis
        resolution = max((midval - minval), (maxval - midval))

        # Start from first data point
        pos, val = data[0]
        lastangle, lastcos, lastsin = self.canvas_angle(pos)
        # We calculate the track height
        posheight = trackheight * (val - midval) / resolution + ctr
        lastx = self.xcenter + posheight * lastsin  # start xy coords
        lasty = self.ycenter + posheight * lastcos
        for pos, val in data:
            posangle, poscos, possin = self.canvas_angle(pos)
            posheight = trackheight * (val - midval) / resolution + ctr
            x = self.xcenter + posheight * possin  # next xy coords
            y = self.ycenter + posheight * poscos
            line_elements.append(
                Line(
                    lastx,
                    lasty,
                    x,
                    y,
                    strokeColor=graph.poscolor,
                    strokeWidth=graph.linewidth,
                )
            )
            lastx, lasty = x, y
        return line_elements

    def draw_bar_graph(self, graph):
        """Return list of drawable elements for a bar graph.

        Arguments:
         - graph     Graph object

        """
        # At each point contained in the graph data, we draw a vertical bar
        # from the track center to the height of the datapoint value (positive
        # values go up in one color, negative go down in the alternative
        # color).
        bar_elements = []

        # Set the number of pixels per unit for the data
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        btm, ctr, top = self.track_radii[self.current_track_level]
        trackheight = 0.5 * (top - btm)
        datarange = maxval - minval
        if datarange == 0:
            datarange = trackheight
        data = graph[self.start : self.end]
        # midval is the value at which the x-axis is plotted, and is the
        # central ring in the track
        if graph.center is None:
            midval = (maxval + minval) / 2.0
        else:
            midval = graph.center

        # Convert data into 'binned' blocks, covering half the distance to the
        # next data point on either side, accounting for the ends of fragments
        # and tracks
        start, end = self._current_track_start_end()
        data = intermediate_points(start, end, graph[start:end])

        if not data:
            return []

        # Whichever is the greatest difference: max-midval or min-midval, is
        # taken to specify the number of pixel units resolved along the
        # y-axis
        resolution = max((midval - minval), (maxval - midval))
        if resolution == 0:
            resolution = trackheight

        # Create elements for the bar graph based on newdata
        for pos0, pos1, val in data:
            pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0)
            pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1)

            barval = trackheight * (val - midval) / resolution
            if barval >= 0:
                barcolor = graph.poscolor
            else:
                barcolor = graph.negcolor

            # Draw bar
            bar_elements.append(
                self._draw_arc(ctr, ctr + barval, pos0angle, pos1angle, barcolor)
            )
        return bar_elements

    def draw_heat_graph(self, graph):
        """Return list of drawable elements for the heat graph.

        Arguments:
         - graph     Graph object

        """
        # At each point contained in the graph data, we draw a box that is the
        # full height of the track, extending from the midpoint between the
        # previous and current data points to the midpoint between the current
        # and next data points
        heat_elements = []  # holds drawable elements

        # Get graph data
        data_quartiles = graph.quartiles()
        minval, maxval = data_quartiles[0], data_quartiles[4]
        midval = (maxval + minval) / 2.0  # mid is the value at the X-axis
        btm, ctr, top = self.track_radii[self.current_track_level]
        trackheight = top - btm

        start, end = self._current_track_start_end()
        data = intermediate_points(start, end, graph[start:end])

        # Create elements on the graph, indicating a large positive value by
        # the graph's poscolor, and a large negative value by the graph's
        # negcolor attributes
        for pos0, pos1, val in data:
            pos0angle, pos0cos, pos0sin = self.canvas_angle(pos0)
            pos1angle, pos1cos, pos1sin = self.canvas_angle(pos1)

            # Calculate the heat color, based on the differential between
            # the value and the median value
            heat = colors.linearlyInterpolatedColor(
                graph.poscolor, graph.negcolor, maxval, minval, val
            )

            # Draw heat box
            heat_elements.append(
                self._draw_arc(btm, top, pos0angle, pos1angle, heat, border=heat)
            )
        return heat_elements

    def draw_scale(self, track):
        """Return list of elements in the scale and list of their labels.

        Arguments:
         - track     Track object

        """
        scale_elements = []  # holds axes and ticks
        scale_labels = []  # holds labels

        if not track.scale:
            # no scale required, exit early
            return [], []

        # Get track locations
        btm, ctr, top = self.track_radii[self.current_track_level]
        trackheight = top - ctr

        # X-axis
        start, end = self._current_track_start_end()
        if track.start is not None or track.end is not None:
            # Draw an arc, leaving out the wedge
            p = ArcPath(strokeColor=track.scale_color, fillColor=None)
            startangle, startcos, startsin = self.canvas_angle(start)
            endangle, endcos, endsin = self.canvas_angle(end)
            p.addArc(
                self.xcenter,
                self.ycenter,
                ctr,
                90 - (endangle * 180 / pi),
                90 - (startangle * 180 / pi),
            )
            scale_elements.append(p)
            del p
            # Y-axis start marker
            x0, y0 = self.xcenter + btm * startsin, self.ycenter + btm * startcos
            x1, y1 = self.xcenter + top * startsin, self.ycenter + top * startcos
            scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color))
            # Y-axis end marker
            x0, y0 = self.xcenter + btm * endsin, self.ycenter + btm * endcos
            x1, y1 = self.xcenter + top * endsin, self.ycenter + top * endcos
            scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color))
        elif self.sweep < 1:
            # Draw an arc, leaving out the wedge
            p = ArcPath(strokeColor=track.scale_color, fillColor=None)
            # Note reportlab counts angles anti-clockwise from the horizontal
            # (as in mathematics, e.g. complex numbers and polar coordinates)
            # in degrees.
            p.addArc(
                self.xcenter,
                self.ycenter,
                ctr,
                startangledegrees=90 - 360 * self.sweep,
                endangledegrees=90,
            )
            scale_elements.append(p)
            del p
            # Y-axis start marker
            x0, y0 = self.xcenter, self.ycenter + btm
            x1, y1 = self.xcenter, self.ycenter + top
            scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color))
            # Y-axis end marker
            alpha = 2 * pi * self.sweep
            x0, y0 = self.xcenter + btm * sin(alpha), self.ycenter + btm * cos(alpha)
            x1, y1 = self.xcenter + top * sin(alpha), self.ycenter + top * cos(alpha)
            scale_elements.append(Line(x0, y0, x1, y1, strokeColor=track.scale_color))
        else:
            # Draw a full circle
            scale_elements.append(
                Circle(
                    self.xcenter,
                    self.ycenter,
                    ctr,
                    strokeColor=track.scale_color,
                    fillColor=None,
                )
            )

        start, end = self._current_track_start_end()
        if track.scale_ticks:  # Ticks are required on the scale
            # Draw large ticks
            # I want the ticks to be consistently positioned relative to
            # the start of the sequence (position 0), not relative to the
            # current viewpoint (self.start and self.end)

            ticklen = track.scale_largeticks * trackheight
            tickiterval = int(track.scale_largetick_interval)
            # Note that we could just start the list of ticks using
            # range(0,self.end,tickinterval) and the filter out the
            # ones before self.start - but this seems wasteful.
            # Using tickiterval * (self.start/tickiterval) is a shortcut.
            for tickpos in range(
                tickiterval * (self.start // tickiterval), int(self.end), tickiterval
            ):
                if tickpos <= start or end <= tickpos:
                    continue
                tick, label = self.draw_tick(
                    tickpos, ctr, ticklen, track, track.scale_largetick_labels
                )
                scale_elements.append(tick)
                if label is not None:  # If there's a label, add it
                    scale_labels.append(label)
            # Draw small ticks
            ticklen = track.scale_smallticks * trackheight
            tickiterval = int(track.scale_smalltick_interval)
            for tickpos in range(
                tickiterval * (self.start // tickiterval), int(self.end), tickiterval
            ):
                if tickpos <= start or end <= tickpos:
                    continue
                tick, label = self.draw_tick(
                    tickpos, ctr, ticklen, track, track.scale_smalltick_labels
                )
                scale_elements.append(tick)
                if label is not None:  # If there's a label, add it
                    scale_labels.append(label)

        # Check to see if the track contains a graph - if it does, get the
        # minimum and maximum values, and put them on the scale Y-axis
        # at 60 degree intervals, ordering the labels by graph_id
        startangle, startcos, startsin = self.canvas_angle(start)
        endangle, endcos, endsin = self.canvas_angle(end)
        if track.axis_labels:
            for set in track.get_sets():
                if set.__class__ is GraphSet:
                    # Y-axis
                    for n in range(7):
                        angle = n * 1.0471975511965976
                        if angle < startangle or endangle < angle:
                            continue
                        ticksin, tickcos = sin(angle), cos(angle)
                        x0, y0 = (
                            self.xcenter + btm * ticksin,
                            self.ycenter + btm * tickcos,
                        )
                        x1, y1 = (
                            self.xcenter + top * ticksin,
                            self.ycenter + top * tickcos,
                        )
                        scale_elements.append(
                            Line(x0, y0, x1, y1, strokeColor=track.scale_color)
                        )

                        graph_label_min = []
                        graph_label_max = []
                        graph_label_mid = []
                        for graph in set.get_graphs():
                            quartiles = graph.quartiles()
                            minval, maxval = quartiles[0], quartiles[4]
                            if graph.center is None:
                                midval = (maxval + minval) / 2.0
                                graph_label_min.append(f"{minval:.3f}")
                                graph_label_max.append(f"{maxval:.3f}")
                                graph_label_mid.append(f"{midval:.3f}")
                            else:
                                diff = max(
                                    (graph.center - minval), (maxval - graph.center)
                                )
                                minval = graph.center - diff
                                maxval = graph.center + diff
                                midval = graph.center
                                graph_label_mid.append(f"{midval:.3f}")
                                graph_label_min.append(f"{minval:.3f}")
                                graph_label_max.append(f"{maxval:.3f}")
                        xmid, ymid = (x0 + x1) / 2.0, (y0 + y1) / 2.0
                        for limit, x, y in [
                            (graph_label_min, x0, y0),
                            (graph_label_max, x1, y1),
                            (graph_label_mid, xmid, ymid),
                        ]:
                            label = String(
                                0,
                                0,
                                ";".join(limit),
                                fontName=track.scale_font,
                                fontSize=track.scale_fontsize,
                                fillColor=track.scale_color,
                            )
                            label.textAnchor = "middle"
                            labelgroup = Group(label)
                            labelgroup.transform = (
                                tickcos,
                                -ticksin,
                                ticksin,
                                tickcos,
                                x,
                                y,
                            )
                            scale_labels.append(labelgroup)

        return scale_elements, scale_labels

    def draw_tick(self, tickpos, ctr, ticklen, track, draw_label):
        """Return drawing element for a tick on the scale.

        Arguments:
         - tickpos   Int, position of the tick on the sequence
         - ctr       Float, Y co-ord of the center of the track
         - ticklen   How long to draw the tick
         - track     Track, the track the tick is drawn on
         - draw_label    Boolean, write the tick label?

        """
        # Calculate tick coordinates
        tickangle, tickcos, ticksin = self.canvas_angle(tickpos)
        x0, y0 = self.xcenter + ctr * ticksin, self.ycenter + ctr * tickcos
        x1, y1 = (
            self.xcenter + (ctr + ticklen) * ticksin,
            self.ycenter + (ctr + ticklen) * tickcos,
        )
        # Calculate height of text label so it can be offset on lower half
        # of diagram
        # LP: not used, as not all fonts have ascent_descent data in reportlab.pdfbase._fontdata
        # label_offset = _fontdata.ascent_descent[track.scale_font][0]*\
        #               track.scale_fontsize/1000.
        tick = Line(x0, y0, x1, y1, strokeColor=track.scale_color)
        if draw_label:
            # Put tick position on as label
            if track.scale_format == "SInt":
                if tickpos >= 1000000:
                    tickstring = str(tickpos // 1000000) + " Mbp"
                elif tickpos >= 1000:
                    tickstring = str(tickpos // 1000) + " Kbp"
                else:
                    tickstring = str(tickpos)
            else:
                tickstring = str(tickpos)
            label = String(
                0,
                0,
                tickstring,  # Make label string
                fontName=track.scale_font,
                fontSize=track.scale_fontsize,
                fillColor=track.scale_color,
            )
            if tickangle > pi:
                label.textAnchor = "end"
            # LP: This label_offset depends on ascent_descent data, which is not available for all
            # fonts, so has been deprecated.
            # if 0.5*pi < tickangle < 1.5*pi:
            #    y1 -= label_offset
            labelgroup = Group(label)
            labelgroup.transform = (1, 0, 0, 1, x1, y1)
        else:
            labelgroup = None
        return tick, labelgroup

    def draw_test_tracks(self):
        """Draw blue test tracks with grene line down their center."""
        # Add lines only for drawn tracks
        for track in self.drawn_tracks:
            btm, ctr, top = self.track_radii[track]
            self.drawing.add(
                Circle(
                    self.xcenter,
                    self.ycenter,
                    top,
                    strokeColor=colors.blue,
                    fillColor=None,
                )
            )  # top line
            self.drawing.add(
                Circle(
                    self.xcenter,
                    self.ycenter,
                    ctr,
                    strokeColor=colors.green,
                    fillColor=None,
                )
            )  # middle line
            self.drawing.add(
                Circle(
                    self.xcenter,
                    self.ycenter,
                    btm,
                    strokeColor=colors.blue,
                    fillColor=None,
                )
            )  # bottom line

    def draw_greytrack(self, track):
        """Drawing element for grey background to passed Track object."""
        greytrack_bgs = []  # Holds track backgrounds
        greytrack_labels = []  # Holds track foreground labels

        if not track.greytrack:  # No greytrack required, return early
            return [], []

        # Get track location
        btm, ctr, top = self.track_radii[self.current_track_level]

        start, end = self._current_track_start_end()
        startangle, startcos, startsin = self.canvas_angle(start)
        endangle, endcos, endsin = self.canvas_angle(end)

        # Make background
        if track.start is not None or track.end is not None:
            # Draw an arc, leaving out the wedge
            p = ArcPath(strokeColor=track.scale_color, fillColor=None)
            greytrack_bgs.append(
                self._draw_arc(
                    btm, top, startangle, endangle, colors.Color(0.96, 0.96, 0.96)
                )
            )
        elif self.sweep < 1:
            # Make a partial circle, a large arc box
            # This method assumes the correct center for us.
            greytrack_bgs.append(
                self._draw_arc(
                    btm, top, 0, 2 * pi * self.sweep, colors.Color(0.96, 0.96, 0.96)
                )
            )
        else:
            # Make a full circle (using a VERY thick linewidth)
            greytrack_bgs.append(
                Circle(
                    self.xcenter,
                    self.ycenter,
                    ctr,
                    strokeColor=colors.Color(0.96, 0.96, 0.96),
                    fillColor=None,
                    strokeWidth=top - btm,
                )
            )

        if track.greytrack_labels:
            # Labels are required for this track
            labelstep = self.length // track.greytrack_labels  # label interval
            for pos in range(self.start, self.end, labelstep):
                label = String(
                    0,
                    0,
                    track.name,  # Add a new label at
                    fontName=track.greytrack_font,  # each interval
                    fontSize=track.greytrack_fontsize,
                    fillColor=track.greytrack_fontcolor,
                )
                theta, costheta, sintheta = self.canvas_angle(pos)
                if theta < startangle or endangle < theta:
                    continue
                x, y = (
                    self.xcenter + btm * sintheta,
                    self.ycenter + btm * costheta,
                )  # start text halfway up marker
                labelgroup = Group(label)
                labelangle = (
                    self.sweep * 2 * pi * (pos - self.start) / self.length - pi / 2
                )
                if theta > pi:
                    label.textAnchor = "end"  # Anchor end of text to inner radius
                    labelangle += pi  # and reorient it
                cosA, sinA = cos(labelangle), sin(labelangle)
                labelgroup.transform = (cosA, -sinA, sinA, cosA, x, y)
                if not self.length - x <= labelstep:  # Don't overrun the circle
                    greytrack_labels.append(labelgroup)

        return greytrack_bgs, greytrack_labels

    def canvas_angle(self, base):
        """Given base-pair position, return (angle, cosine, sin) (PRIVATE)."""
        angle = self.sweep * 2 * pi * (base - self.start) / self.length
        return (angle, cos(angle), sin(angle))

    def _draw_sigil_box(
        self, bottom, center, top, startangle, endangle, strand, **kwargs
    ):
        """Draw BOX sigil (PRIVATE)."""
        if strand == 1:
            inner_radius = center
            outer_radius = top
        elif strand == -1:
            inner_radius = bottom
            outer_radius = center
        else:
            inner_radius = bottom
            outer_radius = top
        return self._draw_arc(
            inner_radius, outer_radius, startangle, endangle, **kwargs
        )

    def _draw_arc(
        self,
        inner_radius,
        outer_radius,
        startangle,
        endangle,
        color,
        border=None,
        colour=None,
        **kwargs,
    ):
        """Return closed path describing an arc box (PRIVATE).

        Arguments:
         - inner_radius  Float distance of inside of arc from drawing center
         - outer_radius  Float distance of outside of arc from drawing center
         - startangle    Float angle subtended by start of arc at drawing center
           (in radians)
         - endangle      Float angle subtended by end of arc at drawing center
           (in radians)
         - color        colors.Color object for arc (overridden by backwards
           compatible argument with UK spelling, colour).

        Returns a closed path object describing an arced box corresponding to
        the passed values.  For very small angles, a simple four sided
        polygon is used.
        """
        # Let the UK spelling (colour) override the USA spelling (color)
        if colour is not None:
            color = colour

        strokecolor, color = _stroke_and_fill_colors(color, border)

        if abs(endangle - startangle) > 0.01:
            # Wide arc, must use full curves
            p = ArcPath(strokeColor=strokecolor, fillColor=color, strokewidth=0)
            # Note reportlab counts angles anti-clockwise from the horizontal
            # (as in mathematics, e.g. complex numbers and polar coordinates)
            # but we use clockwise from the vertical.  Also reportlab uses
            # degrees, but we use radians.
            p.addArc(
                self.xcenter,
                self.ycenter,
                inner_radius,
                90 - (endangle * 180 / pi),
                90 - (startangle * 180 / pi),
                moveTo=True,
            )
            p.addArc(
                self.xcenter,
                self.ycenter,
                outer_radius,
                90 - (endangle * 180 / pi),
                90 - (startangle * 180 / pi),
                reverse=True,
            )
            p.closePath()
            return p
        else:
            # Cheat and just use a four sided polygon.
            # Calculate trig values for angle and coordinates
            startcos, startsin = cos(startangle), sin(startangle)
            endcos, endsin = cos(endangle), sin(endangle)
            x0, y0 = self.xcenter, self.ycenter  # origin of the circle
            x1, y1 = (x0 + inner_radius * startsin, y0 + inner_radius * startcos)
            x2, y2 = (x0 + inner_radius * endsin, y0 + inner_radius * endcos)
            x3, y3 = (x0 + outer_radius * endsin, y0 + outer_radius * endcos)
            x4, y4 = (x0 + outer_radius * startsin, y0 + outer_radius * startcos)
            return draw_polygon([(x1, y1), (x2, y2), (x3, y3), (x4, y4)], color, border)

    def _draw_arc_line(
        self, path, start_radius, end_radius, start_angle, end_angle, move=False
    ):
        """Add a list of points to a path object (PRIVATE).

        Assumes angles given are in degrees!

        Represents what would be a straight line on a linear diagram.
        """
        x0, y0 = self.xcenter, self.ycenter  # origin of the circle
        radius_diff = end_radius - start_radius
        angle_diff = end_angle - start_angle
        dx = 0.01  # heuristic
        a = start_angle * pi / 180
        if move:
            path.moveTo(x0 + start_radius * cos(a), y0 + start_radius * sin(a))
        else:
            path.lineTo(x0 + start_radius * cos(a), y0 + start_radius * sin(a))
        x = dx
        if 0.01 <= abs(dx):
            while x < 1:
                r = start_radius + x * radius_diff
                a = (
                    (start_angle + x * (angle_diff)) * pi / 180
                )  # to radians for sin/cos
                # print(x0+r*cos(a), y0+r*sin(a))
                path.lineTo(x0 + r * cos(a), y0 + r * sin(a))
                x += dx
        a = end_angle * pi / 180
        path.lineTo(x0 + end_radius * cos(a), y0 + end_radius * sin(a))

    def _draw_arc_poly(
        self,
        inner_radius,
        outer_radius,
        inner_startangle,
        inner_endangle,
        outer_startangle,
        outer_endangle,
        color,
        border=None,
        flip=False,
        **kwargs,
    ):
        """Return polygon path describing an arc."""
        strokecolor, color = _stroke_and_fill_colors(color, border)

        x0, y0 = self.xcenter, self.ycenter  # origin of the circle
        if (
            abs(inner_endangle - outer_startangle) > 0.01
            or abs(outer_endangle - inner_startangle) > 0.01
            or abs(inner_startangle - outer_startangle) > 0.01
            or abs(outer_startangle - outer_startangle) > 0.01
        ):
            # Wide arc, must use full curves
            p = ArcPath(
                strokeColor=strokecolor,
                fillColor=color,
                # default is mitre/miter which can stick out too much:
                strokeLineJoin=1,  # 1=round
                strokewidth=0,
            )
            # Note reportlab counts angles anti-clockwise from the horizontal
            # (as in mathematics, e.g. complex numbers and polar coordinates)
            # but we use clockwise from the vertical.  Also reportlab uses
            # degrees, but we use radians.
            i_start = 90 - (inner_startangle * 180 / pi)
            i_end = 90 - (inner_endangle * 180 / pi)
            o_start = 90 - (outer_startangle * 180 / pi)
            o_end = 90 - (outer_endangle * 180 / pi)
            p.addArc(x0, y0, inner_radius, i_end, i_start, moveTo=True, reverse=True)
            if flip:
                # Flipped, join end to start,
                self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_start)
                p.addArc(x0, y0, outer_radius, o_end, o_start, reverse=True)
                self._draw_arc_line(p, outer_radius, inner_radius, o_end, i_start)
            else:
                # Not flipped, join start to start, end to end
                self._draw_arc_line(p, inner_radius, outer_radius, i_end, o_end)
                p.addArc(x0, y0, outer_radius, o_end, o_start, reverse=False)
                self._draw_arc_line(p, outer_radius, inner_radius, o_start, i_start)
            p.closePath()
            return p
        else:
            # Cheat and just use a four sided polygon.
            # Calculate trig values for angle and coordinates
            inner_startcos, inner_startsin = (
                cos(inner_startangle),
                sin(inner_startangle),
            )
            inner_endcos, inner_endsin = cos(inner_endangle), sin(inner_endangle)
            outer_startcos, outer_startsin = (
                cos(outer_startangle),
                sin(outer_startangle),
            )
            outer_endcos, outer_endsin = cos(outer_endangle), sin(outer_endangle)
            x1, y1 = (
                x0 + inner_radius * inner_startsin,
                y0 + inner_radius * inner_startcos,
            )
            x2, y2 = (
                x0 + inner_radius * inner_endsin,
                y0 + inner_radius * inner_endcos,
            )
            x3, y3 = (
                x0 + outer_radius * outer_endsin,
                y0 + outer_radius * outer_endcos,
            )
            x4, y4 = (
                x0 + outer_radius * outer_startsin,
                y0 + outer_radius * outer_startcos,
            )
            return draw_polygon(
                [(x1, y1), (x2, y2), (x3, y3), (x4, y4)],
                color,
                border,
                # default is mitre/miter which can stick out too much:
                strokeLineJoin=1,  # 1=round
            )

    def _draw_sigil_cut_corner_box(
        self,
        bottom,
        center,
        top,
        startangle,
        endangle,
        strand,
        color,
        border=None,
        corner=0.5,
        **kwargs,
    ):
        """Draw OCTO sigil, box with corners cut off (PRIVATE)."""
        if strand == 1:
            inner_radius = center
            outer_radius = top
        elif strand == -1:
            inner_radius = bottom
            outer_radius = center
        else:
            inner_radius = bottom
            outer_radius = top

        strokecolor, color = _stroke_and_fill_colors(color, border)

        startangle, endangle = min(startangle, endangle), max(startangle, endangle)
        angle = endangle - startangle

        middle_radius = 0.5 * (inner_radius + outer_radius)
        boxheight = outer_radius - inner_radius

        corner_len = min(0.5 * boxheight, 0.5 * boxheight * corner)
        shaft_inner_radius = inner_radius + corner_len
        shaft_outer_radius = outer_radius - corner_len

        cornerangle_delta = max(
            0.0, min(abs(boxheight) * 0.5 * corner / middle_radius, abs(angle * 0.5))
        )
        if angle < 0:
            cornerangle_delta *= -1  # reverse it

        # Calculate trig values for angle and coordinates
        startcos, startsin = cos(startangle), sin(startangle)
        endcos, endsin = cos(endangle), sin(endangle)
        x0, y0 = self.xcenter, self.ycenter  # origin of the circle
        p = ArcPath(
            strokeColor=strokecolor,
            fillColor=color,
            strokeLineJoin=1,  # 1=round
            strokewidth=0,
            **kwargs,
        )
        # Inner curved edge
        p.addArc(
            self.xcenter,
            self.ycenter,
            inner_radius,
            90 - ((endangle - cornerangle_delta) * 180 / pi),
            90 - ((startangle + cornerangle_delta) * 180 / pi),
            moveTo=True,
        )
        # Corner edge - straight lines assumes small angle!
        # TODO - Use self._draw_arc_line(p, ...) here if we expose corner setting
        p.lineTo(x0 + shaft_inner_radius * startsin, y0 + shaft_inner_radius * startcos)
        p.lineTo(x0 + shaft_outer_radius * startsin, y0 + shaft_outer_radius * startcos)
        # Outer curved edge
        p.addArc(
            self.xcenter,
            self.ycenter,
            outer_radius,
            90 - ((endangle - cornerangle_delta) * 180 / pi),
            90 - ((startangle + cornerangle_delta) * 180 / pi),
            reverse=True,
        )
        # Corner edges
        p.lineTo(x0 + shaft_outer_radius * endsin, y0 + shaft_outer_radius * endcos)
        p.lineTo(x0 + shaft_inner_radius * endsin, y0 + shaft_inner_radius * endcos)
        p.closePath()
        return p

    def _draw_sigil_arrow(
        self, bottom, center, top, startangle, endangle, strand, **kwargs
    ):
        """Draw ARROW sigil (PRIVATE)."""
        if strand == 1:
            inner_radius = center
            outer_radius = top
            orientation = "right"
        elif strand == -1:
            inner_radius = bottom
            outer_radius = center
            orientation = "left"
        else:
            inner_radius = bottom
            outer_radius = top
            orientation = "right"  # backwards compatibility
        return self._draw_arc_arrow(
            inner_radius,
            outer_radius,
            startangle,
            endangle,
            orientation=orientation,
            **kwargs,
        )

    def _draw_sigil_big_arrow(
        self, bottom, center, top, startangle, endangle, strand, **kwargs
    ):
        """Draw BIGARROW sigil, like ARROW but straddles the axis (PRIVATE)."""
        if strand == -1:
            orientation = "left"
        else:
            orientation = "right"
        return self._draw_arc_arrow(
            bottom, top, startangle, endangle, orientation=orientation, **kwargs
        )

    def _draw_arc_arrow(
        self,
        inner_radius,
        outer_radius,
        startangle,
        endangle,
        color,
        border=None,
        shaft_height_ratio=0.4,
        head_length_ratio=0.5,
        orientation="right",
        colour=None,
        **kwargs,
    ):
        """Draw an arrow along an arc (PRIVATE)."""
        # Let the UK spelling (colour) override the USA spelling (color)
        if colour is not None:
            color = colour

        strokecolor, color = _stroke_and_fill_colors(color, border)

        # if orientation == 'right':
        #    startangle, endangle = min(startangle, endangle), max(startangle, endangle)
        # elif orientation == 'left':
        #    startangle, endangle = max(startangle, endangle), min(startangle, endangle)
        # else:
        startangle, endangle = min(startangle, endangle), max(startangle, endangle)
        if orientation != "left" and orientation != "right":
            raise ValueError(
                f"Invalid orientation {orientation!r}, should be 'left' or 'right'"
            )

        angle = endangle - startangle  # angle subtended by arc
        middle_radius = 0.5 * (inner_radius + outer_radius)
        boxheight = outer_radius - inner_radius
        shaft_height = boxheight * shaft_height_ratio
        shaft_inner_radius = middle_radius - 0.5 * shaft_height
        shaft_outer_radius = middle_radius + 0.5 * shaft_height
        headangle_delta = max(
            0.0, min(abs(boxheight) * head_length_ratio / middle_radius, abs(angle))
        )
        if angle < 0:
            headangle_delta *= -1  # reverse it
        if orientation == "right":
            headangle = endangle - headangle_delta
        else:
            headangle = startangle + headangle_delta
        if startangle <= endangle:
            headangle = max(min(headangle, endangle), startangle)
        else:
            headangle = max(min(headangle, startangle), endangle)
        if not (
            startangle <= headangle <= endangle or endangle <= headangle <= startangle
        ):
            raise RuntimeError(
                "Problem drawing arrow, invalid positions. "
                "Start angle: %s, Head angle: %s, "
                "End angle: %s, Angle: %s" % (startangle, headangle, endangle, angle)
            )

        # Calculate trig values for angle and coordinates
        startcos, startsin = cos(startangle), sin(startangle)
        headcos, headsin = cos(headangle), sin(headangle)
        endcos, endsin = cos(endangle), sin(endangle)
        x0, y0 = self.xcenter, self.ycenter  # origin of the circle
        if 0.5 >= abs(angle) and abs(headangle_delta) >= abs(angle):
            # If the angle is small, and the arrow is all head,
            # cheat and just use a triangle.
            if orientation == "right":
                x1, y1 = (x0 + inner_radius * startsin, y0 + inner_radius * startcos)
                x2, y2 = (x0 + outer_radius * startsin, y0 + outer_radius * startcos)
                x3, y3 = (x0 + middle_radius * endsin, y0 + middle_radius * endcos)
            else:
                x1, y1 = (x0 + inner_radius * endsin, y0 + inner_radius * endcos)
                x2, y2 = (x0 + outer_radius * endsin, y0 + outer_radius * endcos)
                x3, y3 = (x0 + middle_radius * startsin, y0 + middle_radius * startcos)
            # return draw_polygon([(x1,y1),(x2,y2),(x3,y3)], color, border,
            #                    stroke_line_join=1)
            return Polygon(
                [x1, y1, x2, y2, x3, y3],
                strokeColor=border or color,
                fillColor=color,
                strokeLineJoin=1,  # 1=round, not mitre!
                strokewidth=0,
            )
        elif orientation == "right":
            p = ArcPath(
                strokeColor=strokecolor,
                fillColor=color,
                # default is mitre/miter which can stick out too much:
                strokeLineJoin=1,  # 1=round
                strokewidth=0,
                **kwargs,
            )
            # Note reportlab counts angles anti-clockwise from the horizontal
            # (as in mathematics, e.g. complex numbers and polar coordinates)
            # but we use clockwise from the vertical.  Also reportlab uses
            # degrees, but we use radians.
            p.addArc(
                self.xcenter,
                self.ycenter,
                shaft_inner_radius,
                90 - (headangle * 180 / pi),
                90 - (startangle * 180 / pi),
                moveTo=True,
            )
            p.addArc(
                self.xcenter,
                self.ycenter,
                shaft_outer_radius,
                90 - (headangle * 180 / pi),
                90 - (startangle * 180 / pi),
                reverse=True,
            )
            if abs(angle) < 0.5:
                p.lineTo(x0 + outer_radius * headsin, y0 + outer_radius * headcos)
                p.lineTo(x0 + middle_radius * endsin, y0 + middle_radius * endcos)
                p.lineTo(x0 + inner_radius * headsin, y0 + inner_radius * headcos)
            else:
                self._draw_arc_line(
                    p,
                    outer_radius,
                    middle_radius,
                    90 - (headangle * 180 / pi),
                    90 - (endangle * 180 / pi),
                )
                self._draw_arc_line(
                    p,
                    middle_radius,
                    inner_radius,
                    90 - (endangle * 180 / pi),
                    90 - (headangle * 180 / pi),
                )
            p.closePath()
            return p
        else:
            p = ArcPath(
                strokeColor=strokecolor,
                fillColor=color,
                # default is mitre/miter which can stick out too much:
                strokeLineJoin=1,  # 1=round
                strokewidth=0,
                **kwargs,
            )
            # Note reportlab counts angles anti-clockwise from the horizontal
            # (as in mathematics, e.g. complex numbers and polar coordinates)
            # but we use clockwise from the vertical.  Also reportlab uses
            # degrees, but we use radians.
            p.addArc(
                self.xcenter,
                self.ycenter,
                shaft_inner_radius,
                90 - (endangle * 180 / pi),
                90 - (headangle * 180 / pi),
                moveTo=True,
                reverse=True,
            )
            p.addArc(
                self.xcenter,
                self.ycenter,
                shaft_outer_radius,
                90 - (endangle * 180 / pi),
                90 - (headangle * 180 / pi),
                reverse=False,
            )
            # Note - two staight lines is only a good approximation for small
            # head angle, in general will need to curved lines here:
            if abs(angle) < 0.5:
                p.lineTo(x0 + outer_radius * headsin, y0 + outer_radius * headcos)
                p.lineTo(x0 + middle_radius * startsin, y0 + middle_radius * startcos)
                p.lineTo(x0 + inner_radius * headsin, y0 + inner_radius * headcos)
            else:
                self._draw_arc_line(
                    p,
                    outer_radius,
                    middle_radius,
                    90 - (headangle * 180 / pi),
                    90 - (startangle * 180 / pi),
                )
                self._draw_arc_line(
                    p,
                    middle_radius,
                    inner_radius,
                    90 - (startangle * 180 / pi),
                    90 - (headangle * 180 / pi),
                )
            p.closePath()
            return p

    def _draw_sigil_jaggy(
        self,
        bottom,
        center,
        top,
        startangle,
        endangle,
        strand,
        color,
        border=None,
        **kwargs,
    ):
        """Draw JAGGY sigil (PRIVATE).

        Although we may in future expose the head/tail jaggy lengths, for now
        both the left and right edges are drawn jagged.
        """
        if strand == 1:
            inner_radius = center
            outer_radius = top
            teeth = 2
        elif strand == -1:
            inner_radius = bottom
            outer_radius = center
            teeth = 2
        else:
            inner_radius = bottom
            outer_radius = top
            teeth = 4

        # TODO, expose these settings?
        tail_length_ratio = 1.0
        head_length_ratio = 1.0

        strokecolor, color = _stroke_and_fill_colors(color, border)

        startangle, endangle = min(startangle, endangle), max(startangle, endangle)
        angle = endangle - startangle  # angle subtended by arc
        height = outer_radius - inner_radius

        assert startangle <= endangle and angle >= 0
        if head_length_ratio and tail_length_ratio:
            headangle = max(
                endangle
                - min(height * head_length_ratio / (center * teeth), angle * 0.5),
                startangle,
            )
            tailangle = min(
                startangle
                + min(height * tail_length_ratio / (center * teeth), angle * 0.5),
                endangle,
            )
            # With very small features, can due to floating point calculations
            # violate the assertion below that start <= tail <= head <= end
            tailangle = min(tailangle, headangle)
        elif head_length_ratio:
            headangle = max(
                endangle - min(height * head_length_ratio / (center * teeth), angle),
                startangle,
            )
            tailangle = startangle
        else:
            headangle = endangle
            tailangle = min(
                startangle + min(height * tail_length_ratio / (center * teeth), angle),
                endangle,
            )

        if not startangle <= tailangle <= headangle <= endangle:
            raise RuntimeError(
                "Problem drawing jaggy sigil, invalid "
                "positions. Start angle: %s, "
                "Tail angle: %s, Head angle: %s, End angle %s, "
                "Angle: %s" % (startangle, tailangle, headangle, endangle, angle)
            )

        # Calculate trig values for angle and coordinates
        startcos, startsin = cos(startangle), sin(startangle)
        headcos, headsin = cos(headangle), sin(headangle)
        endcos, endsin = cos(endangle), sin(endangle)
        x0, y0 = self.xcenter, self.ycenter  # origin of the circle

        p = ArcPath(
            strokeColor=strokecolor,
            fillColor=color,
            # default is mitre/miter which can stick out too much:
            strokeLineJoin=1,  # 1=round
            strokewidth=0,
            **kwargs,
        )
        # Note reportlab counts angles anti-clockwise from the horizontal
        # (as in mathematics, e.g. complex numbers and polar coordinates)
        # but we use clockwise from the vertical.  Also reportlab uses
        # degrees, but we use radians.
        p.addArc(
            self.xcenter,
            self.ycenter,
            inner_radius,
            90 - (headangle * 180 / pi),
            90 - (tailangle * 180 / pi),
            moveTo=True,
        )
        for i in range(teeth):
            p.addArc(
                self.xcenter,
                self.ycenter,
                inner_radius + i * height / teeth,
                90 - (tailangle * 180 / pi),
                90 - (startangle * 180 / pi),
            )
            # Curved line needed when drawing long jaggies
            self._draw_arc_line(
                p,
                inner_radius + i * height / teeth,
                inner_radius + (i + 1) * height / teeth,
                90 - (startangle * 180 / pi),
                90 - (tailangle * 180 / pi),
            )
        p.addArc(
            self.xcenter,
            self.ycenter,
            outer_radius,
            90 - (headangle * 180 / pi),
            90 - (tailangle * 180 / pi),
            reverse=True,
        )
        for i in range(teeth):
            p.addArc(
                self.xcenter,
                self.ycenter,
                outer_radius - i * height / teeth,
                90 - (endangle * 180 / pi),
                90 - (headangle * 180 / pi),
                reverse=True,
            )
            # Curved line needed when drawing long jaggies
            self._draw_arc_line(
                p,
                outer_radius - i * height / teeth,
                outer_radius - (i + 1) * height / teeth,
                90 - (endangle * 180 / pi),
                90 - (headangle * 180 / pi),
            )
        p.closePath()
        return p
