package ca.ualberta.stothard.cgview;

import java.awt.*;
import java.awt.font.TextLayout;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.*;
import java.util.*;
import java.text.*;
import java.util.regex.*;

/**
 * Represents a circular map of a DNA sequence. Numerous methods are available for controlling the overall appearance of
 * the map (width, height, background color, etc). The contents of the map are described in terms of {@link
 * FeatureSlot}, {@link Feature}, {@link FeatureRange}, {@link Legend}, and {@link LegendItem} objects.
 *
 * @author Paul Stothard
 */

public class Cgview implements CgviewConstants {

    private Graphics2D gg;

    private int sequenceLength;
    private int width = 700;
    private int height = 700;
    private int smallestDimension = 700;
    private Color backgroundColor = new Color(255, 255, 255); //white

    private double backboneRadius = 190.0d;
    private float backboneThickness = 5.0f;
    private Color backboneColor = new Color(128, 128, 128);  //gray

    private double origin = 90.0d;

    private float featureThickness = 8.0f;
    private double featureSlotSpacing = 4.0d;
    private double arrowheadLength = 5.0d;
    private boolean shiftSmallFeatures = true;
    private double minimumFeatureLength = 0.02d;

    private boolean showShading = true;
    private float shadingProportion = 0.2f;
    private float highlightOpacity = 0.3f;
    private float shadowOpacity = 0.3f;

    //private int desiredNumberOfTicks = 30;
    private int desiredNumberOfTicks = 25;
    private float tickThickness = 2.0f;
    private Color longTickColor = new Color(0, 0, 0);  //black
    private Color shortTickColor = new Color(0, 0, 0);  //black
    private float shortTickThickness = 2.0f;
    private Color zeroTickColor = new Color(0, 0, 0);  //black
    private float tickLength = 7.0f;
    private boolean drawTickMarks = true;
    private double tickDensity = 1.0d;

    private int rulerUnits = BASES;
    private Font rulerFont = new Font("SansSerif", Font.PLAIN, 8);
    private double rulerTextPadding = 10.0d;
    private Color rulerFontColor = new Color(0, 0, 0);  //black

    private String title = "";
    private Font titleFont = new Font("SansSerif", Font.PLAIN, 12);
    private Color titleFontColor = new Color(0, 0, 0);  //black

    private boolean drawLegends = true;
    private Font legendFont = new Font("SansSerif", Font.PLAIN, 8);
    private Color legendTextColor = new Color(0, 0, 0);  //black

    private Font labelFont = new Font("SansSerif", Font.PLAIN, 10);
    private int globalLabel = LABEL;
    private Color globalLabelColor;
    private float labelLineThickness = 1.0f;
    private double labelLineLength = 50.0d;
    private double zoomShift = 1.01d;
    private int useInnerLabels = INNER_LABELS_AUTO;
    private boolean moveInnerLabelsToOuter = true;
    private int giveFeaturePositions = POSITIONS_NO_SHOW;
    private int labelsToKeep = 4000;
    private boolean labelShuffle = true;
    private boolean useColoredLabelBackgrounds = false;
    private int clashSpan = 100;
    private int spreadIterations = 100;
    private double radiusShiftAmount = 10.0d;
    private double radiansShiftConstant = 0.20d;
    private int labelPlacementQuality = 8;
    private boolean keepLastLabels = false;

    private boolean isLinear = false;
    private String linearBreakText = "3'   5'";
    private double zigzagWidth = 0.0d;

    private ArrayList featureSlots = new ArrayList();
    private ArrayList outerLabels = new ArrayList();
    private ArrayList innerLabels = new ArrayList();
    private ArrayList legends = new ArrayList();
    private Font warningFont = new Font("SansSerif", Font.PLAIN, 8);
    private Color warningFontColor = new Color(0, 0, 0);  //black
    private String warningText = "";
    private boolean showWarning = false;

    private Point2D centerPoint;

    private int totalLabels = 0;
    private int clashLabels = 0;
    //these values are used for zooming and label placement
    private boolean drawEntirePlasmid = true;
    private int zoomRangeOneStart;
    private int zoomRangeOneStop;
    private int zoomRangeTwoStart;
    private int zoomRangeTwoStop;
    private int centerBase;
    private double zoomMultiplier = 1.0d;
    private double virtualZoomMultiplier = 1.0d;
    private double virtualBackboneRadius = backboneRadius;
    private Rectangle2D backgroundRectangle;
    private Rectangle2D titleRectangle;
    private Rectangle2D lengthRectangle;
    private Arc2D outerArc;
    private Arc2D innerArc;

    private double desiredZoom = 1.0d;
    private int desiredZoomCenter = 1;

    private Color borderColor = new Color(0, 0, 0);  //black
    private boolean showBorder = true;

    private ArrayList labelBounds = new ArrayList();

    private Legend infoLegend;

    //some limits
    private int MAX_DNA_LENGTH = 20000000;
    //private double ZOOM_MULTIPLIER_MAX = 30000.0d;
    private double ZOOM_MULTIPLIER_MAX = 30.0d;
    private double VIRTUAL_ZOOM_MULTIPLIER_MAX = 500000.0d;

    /**
     * Constructs a new Cgview object.
     *
     * @param sequenceLength the length of the sequence to be mapped.
     */
    public Cgview(int sequenceLength) {
        setSequenceLength(sequenceLength);
    }

    /**
     * Sets the length of the sequence to be mapped.
     *
     * @param bases the sequence length.
     */
    public void setSequenceLength(int bases) {
        if (bases < 0) {
            bases = 0;
        } else if (bases > MAX_DNA_LENGTH) {
            //generate an error
        }
        sequenceLength = bases;
    }

    /**
     * Returns the length of the sequence to be mapped.
     *
     * @return the length of the sequence.
     */
    public int getSequenceLength() {
        return sequenceLength;
    }

    /**
     * Returns the Graphics2D object for this Cgview.
     *
     * @return the Graphics2D object.
     */
    protected Graphics2D getGraphics() {
        return gg;
    }

    /**
     * Sets the height of the map.
     *
     * @param height the height of the map.
     */
    public void setHeight(int height) {
        if (height < 0) {
            height = 0;
        }
        this.height = height;
    }

    /**
     * Returns the height of the map.
     *
     * @return the height of the map.
     */
    public int getHeight() {
        return height;
    }

    /**
     * Adjusts the tick density, with 0 being the minimum, 1 being the maximum.
     *
     * @param density a double between 0.0 and 1.0.
     */
    public void setTickDensity(double density) {
        if (density < 0.0d) {
            density = 0.0d;
        }
	else if (density > 1.0d) {
	    density = 1.0d;
	}
        this.tickDensity = density;
    }

    /**
     * Returns the tick density, with 0 being the minimum, 1 being the maximum.
     *
     * @return a double between 0.0 and 1.0 representing the tick density.
     */
    public double getTickDensity() {
        return tickDensity;
    }

    /**
     * Sets the width of the map.
     *
     * @param width the width of the map.
     */
    public void setWidth(int width) {
        if (width < 0) {
            width = 0;
        }
        this.width = width;
    }

    /**
     * Returns the width of the map.
     *
     * @return the width of the map.
     */
    public int getWidth() {
        return width;
    }

    /**
     * Returns the width or the height of the map, whichever is smaller.
     *
     * @return the width or the height of the map, whichever is smaller.
     */
    public int getSmallestDimension() {
        return Math.min(width, height);
    }


    /**
     * Sets the background color of the map.
     *
     * @param color the background color.
     */
    public void setBackgroundColor(Color color) {
        backgroundColor = color;
    }

    /**
     * Returns the background color of the map.
     *
     * @return the background color.
     */
    public Color getBackgroundColor() {
        return backgroundColor;
    }

    /**
     * Sets the color of the map border.
     *
     * @param color the border color.
     */
    public void setBorderColor(Color color) {
        borderColor = color;
    }

    /**
     * Returns the color of the map border.
     *
     * @return the border color.
     */
    public Color getBorderColor() {
        return borderColor;
    }

    /**
     * Sets the radius of the circle that represents the DNA sequence.
     *
     * @param radius the backbone radius.
     */
    public void setBackboneRadius(double radius) {
        if (radius < 0) {
            radius = 0;
        }
        backboneRadius = radius;
    }

    /**
     * Returns the radius of the circle that represents the DNA sequence.
     *
     * @return the backbone radius.
     */
    public double getBackboneRadius() {
        return backboneRadius;
    }

    /**
     * Sets the thickness of the line used to draw the circle that represents the DNA sequence.
     *
     * @param thickness the thickness of the line.
     */
    public void setBackboneThickness(float thickness) {
        if (thickness < 0) {
            thickness = 0.0f;
        }
        backboneThickness = thickness;
    }

    /**
     * Returns the thickness of the line used to draw the circle that represents the DNA sequence.
     *
     * @return the thickness of the line.
     */
    public double getBackboneThickness() {
        return backboneThickness;
    }

    /**
     * Sets the color of the text used for the sequence ruler.
     *
     * @param color the font color.
     */
    public void setRulerFontColor(Color color) {
        rulerFontColor = color;
    }

    /**
     * Returns the color of the text used for the sequence ruler.
     *
     * @return the font color.
     */
    public Color getRulerFontColor() {
        return rulerFontColor;
    }

    /**
     * Sets the color of the text used for the sequence title.
     *
     * @param color the color of the font.
     */
    public void setTitleFontColor(Color color) {
        titleFontColor = color;
    }

    /**
     * Returns the color of the text used for the sequence title.
     *
     * @return the font color.
     */
    public Color getTitleFontColor() {
        return titleFontColor;
    }

    /**
     * Returns the default color of the text used for {@link Legend} objects.
     *
     * @return the font color.
     */
    public Color getLegendTextColor() {
        return legendTextColor;
    }

    /**
     * Sets the default color of the text used for {@link Legend} objects.
     *
     * @param color the font color.
     */
    public void setLegendTextColor(Color color) {
        legendTextColor = color;
    }


    /**
     * Sets the color of the text used for warnings that appear at the bottom of the figure.
     *
     * @param color the font color.
     */
    public void setWarningFontColor(Color color) {
        warningFontColor = color;
    }

    /**
     * Returns the color of the text used for warnings that appear at the bottom of the figure.
     *
     * @return the font color.
     */
    public Color getWarningFontColor() {
        return warningFontColor;
    }

    /**
     * By default, feature labels are colored to match the color of the feature they represent. The
     * setGlobalLabelColor() method can be used to specify a single color for all labels.
     *
     * @param color the color of all labels.
     */
    public void setGlobalLabelColor(Color color) {
        globalLabelColor = color;
    }

    /**
     * Returns the color of all labels, or null if the labels are colored based on the feature they represent.
     *
     * @return the color of all labels.
     */
    public Color getGlobalLabelColor() {
        return globalLabelColor;
    }


    /**
     * Sets whether or not the message set using {@link #setWarningText(String) setWarningText()} should be drawn at the
     * bottom of the figure.
     *
     * @param showWarning whether or not the message should be drawn.
     */
    public void setShowWarning(boolean showWarning) {
        this.showWarning = showWarning;
    }

    /**
     * Returns whether or not the message set using {@link #setWarningText(String) setWarningText()} should be drawn at
     * the bottom of the figure.
     *
     * @return whether or not the message should be drawn.
     */
    public boolean getShowWarning() {
        return showWarning;
    }

    /**
     * Sets whether or not the map should be drawn with a broken backbone line to indicate a linear molecule.
     *
     * @param isLinear whether or not the map should be drawn with a broken backbone line.
     */
    public void setIsLinear(boolean isLinear) {
        this.isLinear = isLinear;
    }

    /**
     * Returns whether or not the map should be drawn with a broken backbone line to indicate a linear molecule.
     *
     * @return whether or not the map should be drawn with a broken backbone.
     */
    public boolean getIsLinear() {
        return this.isLinear;
    }

    /**
     * Sets the text to draw at the sequence start/end boundary if this is a linear molecule.
     *
     * @param text the text to draw at the start/end boundary.
     */
    public void setLinearBreakText(String text) {
        linearBreakText = text;
    }

    /**
     * Returns the text to draw at the sequence start/end boundary if this is a linear molecule.
     *
     * @return the text to draw at the start/end boundary.
     */
    public String getLinearBreakText() {
        return linearBreakText;
    }


    /**
     * Sets whether or not a border should be drawn around the map.
     *
     * @param showBorder whether or not a border should be drawn.
     */
    public void setShowBorder(boolean showBorder) {
        this.showBorder = showBorder;
    }

    /**
     * Returns whether or not a border should be drawn around the map.
     *
     * @return whether or not a border should be drawn.
     */
    public boolean getShowBorder() {
        return showBorder;
    }

    /**
     * Sets whether or not inner labels should be moved from the inside of the backbone circle to the outside of the
     * backbone circle when there is insufficient room on the inside of the backbone circle.
     *
     * @param moveInnerLabelsToOuter whether or not labels should be moved from the inside to the outside.
     */
    public void setMoveInnerLabelsToOuter(boolean moveInnerLabelsToOuter) {
        this.moveInnerLabelsToOuter = moveInnerLabelsToOuter;
    }

    /**
     * Returns whether or not inner labels should be moved from the inside of the backbone circle to the outside of the
     * backbone circle when there is insufficient room on the inside of the backbone circle.
     *
     * @return whether or not labels should be moved from the inside to the outside.
     */
    public boolean getMoveInnerLabelsToOuter() {
        return moveInnerLabelsToOuter;
    }

    /**
     * Sets whether or not items on the map should be drawn with shading. This setting can be overridden by individual
     * {@link FeatureSlot} objects.
     *
     * @param showShading whether or not items on the map should be drawn with shading.
     */
    public void setShowShading(boolean showShading) {
        this.showShading = showShading;
    }

    /**
     * Returns whether or not items on the map should be drawn with shading.
     *
     * @return whether or not items on the map should be drawn with shading.
     */
    public boolean getShowShading() {
        return showShading;
    }

    /**
     * Sets the color of the circle that represents the DNA sequence.
     *
     * @param color the backbone color.
     */
    public void setBackboneColor(Color color) {
        backboneColor = color;
    }

    /**
     * Returns the color of the circle that represents the DNA sequence.
     *
     * @return the backbone color.
     */
    public Color getBackboneColor() {
        return backboneColor;
    }

    /**
     * Sets the default thickness of the line used to represent sequence features. This value can be changed for
     * individual featureSlots using the {@link FeatureSlot#setFeatureThickness(float)
     * FeatureSlot.setFeatureThickness()} method.
     *
     * @param thickness the default feature thickness.
     */
    public void setFeatureThickness(float thickness) {
        if (thickness < 0) {
            thickness = 0.0f;
        }
        featureThickness = thickness;
    }

    /**
     * Returns the default thickness of the line used to represent sequence features. This value can be changed for
     * individual featureSlots using the {@link FeatureSlot#setFeatureThickness(float)
     * FeatureSlot.setFeatureThickness()} method.
     *
     * @return the default feature thickness.
     */
    public float getFeatureThickness() {
        return featureThickness;
    }

    /**
     * Sets the minimum feature length to use when drawing sequence features. Features smaller than this setting are
     * artificially increased in length, to make them more visible.
     *
     * @param length the minimum feature length.
     */
    public void setMinimumFeatureLength(double length) {
        if (length < 0) {
            length = 0.0d;
        }
        minimumFeatureLength = length;
    }

    /**
     * Returns the minimum feature length to use when drawing sequence features. Features smaller than this setting are
     * artificially increased in length, to make them more visible.
     *
     * @return the minimum feature length.
     */
    public double getMinimumFeatureLength() {
        return minimumFeatureLength;
    }


    /**
     * Sets the amount of blank space placed between the concentric feature rings ({@link FeatureSlot} objects).
     *
     * @param spacing the spacing between FeatureSlot objects.
     */
    public void setFeatureSlotSpacing(double spacing) {
        if (spacing < 0) {
            spacing = 0.0d;
        }
        featureSlotSpacing = spacing;
    }

    /**
     * Returns the amount of blank space placed between the concentric feature rings ({@link FeatureSlot} objects).
     *
     * @return the spacing between FeatureSlot objects.
     */
    public double getFeatureSlotSpacing() {
        return featureSlotSpacing;
    }

    /**
     * Sets the length of the arrowheads used for features that are drawn as arrows.
     *
     * @param length the arrowhead length.
     */
    public void setArrowheadLength(double length) {
        if (length < 0) {
            length = 0.0d;
        }
        arrowheadLength = length;
    }

    /**
     * Returns the length of the arrowheads used for features that are drawn as arrows.
     *
     * @return the arrowhead length.
     */
    public double getArrowheadLength() {
        return arrowheadLength;
    }

    /**
     * Sets the alignment behavior of small features when they are drawn. Small features are those features drawn larger
     * than they actually are, in order to make them visible on the map. If <code>shift</code> is set to
     * <code>true</code>, the drawn representation of the feature is shifted so that its center coincides with the true
     * center of the feature. If shift is set to false, the drawing begins at the true beginning of the feature, and
     * extends the length specified using the {@link #setMinimumFeatureLength(double) setMinimumFeatureLength()}
     * method.
     *
     * @param shift whether or not to shift small features so that their center coincides with the true center of the
     *              feature.
     * @see #setMinimumFeatureLength(double)
     */
    public void setShiftSmallFeatures(boolean shift) {
        shiftSmallFeatures = shift;
    }

    /**
     * Returns the alignment behavior of small features when they are drawn. Small features are those features drawn
     * larger than they actually are, in order to make them visible on the map. If <code>shift</code> is set to
     * <code>true</code>, the drawing of the feature is shifted so that its center coincides with the true center of the
     * feature. If shift is set to false, the drawing begins at the true beginning of the feature, and extends the
     * length specified using the {@link #setMinimumFeatureLength(double) setMinimumFeatureLength()}.
     *
     * @return whether or not to shift small features so that their center coincides with the true center of the
     *         feature.
     */
    public boolean getShiftSmallFeatures() {
        return shiftSmallFeatures;
    }

    /**
     * Sets the proportion of the width of the features to be redrawn for highlighting and shadowing purposes.
     *
     * @param proportion the proportion (between <code>0</code> and <code>1</code>).
     */
    public void setShadingProportion(float proportion) {
        if (proportion < 0) {
            proportion = 0.0f;
        }
        if (proportion > 1) {
            proportion = 1.0f;
        }
        //divide by 2.0f because half will be used for shadows and half for highlights.
        shadingProportion = proportion / 2.0f;
    }

    /**
     * Returns the proportion of the width of the features to be redrawn for highlighting and shadowing purposes.
     *
     * @return the proportion (between <code>0</code> and <code>1</code>).
     */
    public float getShadingProportion() {
        return shadingProportion;
    }

    /**
     * Sets the opacity of the highlighting added to map items. The higher the opacity, the more obvious the
     * highlighting.
     *
     * @param opacity the opacity (between <code>0</code> and <code>1</code>).
     */
    public void setHighlightOpacity(float opacity) {
        if (opacity < 0) {
            opacity = 0.0f;
        }
        if (opacity > 1) {
            opacity = 1.0f;
        }
        highlightOpacity = opacity;
    }

    /**
     * Returns the opacity of the highlighting added to map items. The higher the opacity, the more obvious the
     * highlighting.
     *
     * @return the opacity (between <code>0</code> and <code>1</code>).
     */
    public float getHighlightOpacity() {
        return highlightOpacity;
    }


    /**
     * Sets the opacity of the shadowing added to map items. The higher the opacity, the more obvious the shadowing.
     *
     * @param opacity the opacity (between <code>0</code> and <code>1</code>).
     */
    public void setShadowOpacity(float opacity) {
        if (opacity < 0) {
            opacity = 0.0f;
        }
        if (opacity > 1) {
            opacity = 1.0f;
        }
        shadowOpacity = opacity;
    }

    /**
     * Returns the opacity of the shadowing added to map items. The higher the opacity, the more obvious the shadowing.
     *
     * @return the opacity (between <code>0</code> and <code>1</code>).
     */
    public float getShadowOpacity() {
        return shadowOpacity;
    }


    /**
     * Sets the thickness of the tick marks in the sequence ruler.
     *
     * @param thickness the thickness of the tick marks.
     */
    public void setTickThickness(float thickness) {
        if (thickness < 0) {
            thickness = 0.0f;
        }
        tickThickness = thickness;
    }

    /**
     * Returns the thickness of the tick marks in the sequence ruler.
     *
     * @return the thickness of the tick marks.
     */
    public float getTickThickness() {
        return tickThickness;
    }


    /**
     * Sets the length of the tick marks in the sequence ruler.
     *
     * @param length the length of the tick marks.
     */
    public void setTickLength(float length) {
        if (length < 0) {
            length = 0.0f;
        }
        tickLength = length;
    }

    /**
     * Returns the length of the tick marks in the sequence ruler.
     *
     * @return the length of the tick marks.
     */
    public float getTickLength() {
        return tickLength;
    }

    /**
     * Sets the thickness of the short tick marks in the sequence ruler.
     *
     * @param thickness the thickness of the short tick marks.
     */
    public void setShortTickThickness(float thickness) {
        if (thickness < 0) {
            thickness = 0.0f;
        }
        shortTickThickness = thickness;
    }

    /**
     * Returns the thickness of the short tick marks in the sequence ruler.
     *
     * @return the thickness of the short tick marks.
     */
    public float getShortTickThickness() {
        return shortTickThickness;
    }

    /**
     * Sets the color of the tick marks in the sequence ruler.
     *
     * @param color the color of the tick marks.
     */
    public void setLongTickColor(Color color) {
        longTickColor = color;
    }

    /**
     * Returns the color of the tick marks in the sequence ruler.
     *
     * @return the color of the tick marks.
     */
    public Color getLongTickColor() {
        return longTickColor;
    }

    /**
     * Sets the color of the short tick marks in the sequence ruler.
     *
     * @param color the color of the short tick marks.
     */
    public void setShortTickColor(Color color) {
        shortTickColor = color;
    }

    /**
     * Returns the color of the short tick marks in the sequence ruler.
     *
     * @return the color of the short tick marks.
     */
    public Color getShortTickColor() {
        return shortTickColor;
    }

    /**
     * Sets the color of the tick mark drawn at the boundary between the end and beginning of the sequence.
     *
     * @param color the color of the tick mark drawn at the boundary between the end and beginning of the sequence.
     */
    public void setZeroTickColor(Color color) {
        zeroTickColor = color;
    }

    /**
     * Returns the color of the tick mark drawn at the boundary between the end and beginning of the sequence.
     *
     * @return the color of the tick mark drawn at the boundary between the end and beginning of the sequence.
     */
    public Color getZeroTickColor() {
        return zeroTickColor;
    }

    /**
     * Sets the approximate number of long tick marks to be drawn in the sequence ruler.
     *
     * @param ticks the approximate number of tick marks.
     */
    public void setDesiredNumberOfTicks(int ticks) {
        if (ticks < 0) {
            ticks = 0;
        }
        desiredNumberOfTicks = ticks;
    }

    /**
     * Returns the approximate number of long tick marks to be drawn in the sequence ruler.
     *
     * @return the approximate number of tick marks.
     */
    public int getDesiredNumberOfTicks() {
        return desiredNumberOfTicks;
    }

    /**
     * Can be used to store a zoom center value when a Cgview object is read from XML data. This value does not alter
     * drawing directly.
     *
     * @param zoomCenter a base position to center the map on.
     */
    public void setDesiredZoomCenter(int zoomCenter) {
        desiredZoomCenter = zoomCenter;
    }

    /**
     * Returns the zoomCenter value stored using the setDesiredZoomCenter() method.
     *
     * @return a base position to center the map on.
     */
    public int getDesiredZoomCenter() {
        return desiredZoomCenter;
    }

    /**
     * Can be used to store a zoom value when a Cgview object is read from XML data. This value does not alter drawing
     * directly.
     *
     * @param zoom a factor to zoom in by.
     */
    public void setDesiredZoom(double zoom) {
        desiredZoom = zoom;
    }

    /**
     * Returns the zoom value stored using the setDesiredZoom() method.
     *
     * @return a factor to zoom in by.
     */
    public double getDesiredZoom() {
        return desiredZoom;
    }

    /**
     * Sets the font of the number labels in the sequence ruler.
     *
     * @param font the font of the number labels.
     */
    public void setRulerFont(Font font) {
        rulerFont = font;
    }

    /**
     * Returns the font of the number labels in the sequence ruler.
     *
     * @return the font of the number labels.
     */
    public Font getRulerFont() {
        return rulerFont;
    }


    /**
     * Sets the default font used for text in {@link Legend} objects added to this Cgview.
     *
     * @param font the fault font for Legend objects.
     */
    public void setLegendFont(Font font) {
        legendFont = font;
    }

    /**
     * Returns the default font used for text in {@link Legend} objects added to this Cgview.
     *
     * @return the default font for Legend objects.
     */
    public Font getLegendFont() {
        return legendFont;
    }

    /**
     * Returns an arrayList of the Legend objects in this Cgview.
     *
     * @return the Legend objects in this Cgview.
     */
    protected ArrayList getLegends() {
        return legends;
    }


    /**
     * Sets the font used for any warnings that appear at the bottom of the map.
     *
     * @param font the font used for warning messages.
     */
    public void setWarningFont(Font font) {
        warningFont = font;
    }

    /**
     * Returns the font used for any warnings that appear at the bottom of the map.
     *
     * @return the font used for warning messages.
     */
    public Font getWarningFont() {
        return warningFont;
    }

    /**
     * Specifies a warning message to appear at the bottom of the map.
     *
     * @param message the contents of the message.
     */
    public void setWarningText(String message) {
        warningText = message;
    }

    /**
     * Returns a warning message to appear at the bottom of the map.
     *
     * @return the warning message.
     */
    public String getWarningText() {
        return warningText;
    }

    /**
     * Sets the units to be used for the sequence ruler.
     *
     * @param rulerUnits {@link CgviewConstants#BASES CgviewConstants.BASES} or {@link CgviewConstants#CENTISOMES
     *                   CgviewConstants.CENTISOMES}.
     */
    public void setRulerUnits(int rulerUnits) {
        this.rulerUnits = rulerUnits;
    }

    /**
     * Returns the units to be used for the sequence ruler.
     *
     * @return {@link CgviewConstants#BASES CgviewConstants.BASES} or {@link CgviewConstants#CENTISOMES
     *         CgviewConstants.CENTISOMES}.
     */
    public int getRulerUnits() {
        return rulerUnits;
    }

    /**
     * Sets whether or not feature labels should be shown on this map. This setting is not overridden by Feature and
     * FeatureRange objects.
     *
     * @param labelType {@link CgviewConstants#LABEL_NONE CgviewConstants.LABEL_NONE}, {@link CgviewConstants#LABEL
     *                  CgviewConstants.LABEL}, or {@link CgviewConstants#LABEL_ZOOMED CgviewConstants.LABEL_ZOOMED}.
     */
    public void setGlobalLabel(int labelType) {
        this.globalLabel = labelType;
    }

    /**
     * Returns whether or not feature labels should be shown on this map. This setting is not overridden by Feature and
     * FeatureRange objects.
     *
     * @return {@link CgviewConstants#LABEL_NONE CgviewConstants.LABEL_NONE}, {@link CgviewConstants#LABEL
     *         CgviewConstants.LABEL}, or {@link CgviewConstants#LABEL_ZOOMED CgviewConstants.LABEL_ZOOMED}.
     */
    public int getGlobalLabel() {
        return globalLabel;
    }

    /**
     * Sets whether or not legends should be drawn on this map.
     *
     * @param drawLegends whether or not to draw legends.
     */
    public void setDrawLegends(boolean drawLegends) {
        this.drawLegends = drawLegends;
    }


    /**
     * Returns boolean indicating whether or not legends should be drawn on this map.
     *
     * @return whether or not legends should be drawn on this map.
     */
    public boolean getDrawLegends() {
        return this.drawLegends;
    }

    /**
     * Sets the font of the map title.
     *
     * @param font the font of the map title.
     */
    public void setTitleFont(Font font) {
        titleFont = font;
    }

    /**
     * Returns the font of the map title.
     *
     * @return the font of the map title.
     */
    public Font getTitleFont() {
        return titleFont;
    }

    /**
     * Sets the default font of the feature labels. This font selection will be ignored if a new font is specified in
     * the Feature or FeatureRange objects.
     *
     * @param font the font of the feature labels.
     * @see Feature#setFont(Font) Feature.setFont()
     * @see FeatureRange#setFont(Font) FeatureRange.setFont()
     */
    public void setLabelFont(Font font) {
        labelFont = font;
    }

    /**
     * Returns the font of the feature labels. This font selection will be ignored if a font is specified in the Feature
     * or FeatureRange objects.
     *
     * @return the font of the feature labels.
     */
    public Font getLabelFont() {
        return labelFont;
    }

    /**
     * Sets the spacing between the ruler number labels and the tick marks.
     *
     * @param padding the spacing between the ruler number labels and the tick marks.
     */
    public void setRulerTextPadding(double padding) {
        if (padding < 0) {
            padding = 0.0d;
        }
        rulerTextPadding = padding;
    }

    /**
     * Returns the spacing between the ruler number labels and the tick marks.
     *
     * @return the spacing between the ruler number labels and the tick marks.
     */
    public double getRulerTextPadding() {
        return rulerTextPadding;
    }


    /**
     * Sets the origin of the sequence in relation to the backbone drawing. By default the sequence begins at the twelve
     * o'clock position.
     *
     * @param degrees the number of degrees (between <code>-360.0</code> and <code>360.0d</code>) to advance the
     *                sequence origin in the counterclockwise direction from the three o'clock position.
     */
    public void setOrigin(double degrees) {
        if (degrees < -360) {
            degrees = -360.0d;
        }
        if (degrees > 360) {
            degrees = 360.0d;
        }
        origin = degrees;
    }

    /**
     * Returns the number of degrees that the origin is to be moved in the counterclockwise direction from the three
     * o'clock position.
     *
     * @return the number of degrees.
     */
    public double getOrigin() {
        return origin;
    }

    /**
     * Sets the thickness of the line that extends from features to feature labels.
     *
     * @param thickness the thickness of the line.
     */
    public void setLabelLineThickness(float thickness) {
        if (thickness < 0) {
            thickness = 0.0f;
        }
        labelLineThickness = thickness;
    }

    /**
     * Returns the thickness of the line that extends from features to feature labels.
     *
     * @return the thickness of the line.
     */
    public float getLabelLineThickness() {
        return labelLineThickness;
    }


    /**
     * Sets the length of the line that extends from the feature to the feature label. If feature labels clash with the
     * number labels in the sequence ruler, the length of the label line should be increased using this method.
     *
     * @param length the length of the label line.
     */
    public void setLabelLineLength(double length) {
        if (length < 10) {
            length = 10.0d;
        }
        labelLineLength = length;
    }

    /**
     * Returns the length of the line that extends from the feature to the feature label.
     *
     * @return the length of the label line.
     */
    public double getLabelLineLength() {
        return labelLineLength;
    }

    /**
     * Sets the minimum zoom value necessary for labels to be drawn below the backbone. When the map is drawn using a
     * zoom value less than the value set using this method, all the labels are drawn on the outside of the backbone
     * circle. When the map is drawn using a zoom value greater than the value set using this method, the labels for
     * features on the reverse strand are drawn on the inside of the backbone circle. Also, when the zoom value is
     * greater than the value set using this method, only the features in the visible portion of the map are drawn.
     *
     * @param zoom the minimum zoom value necessary for labels to be drawn below the backbone.
     */
    protected void setZoomShift(double zoom) {
        if (zoom < 0) {
            zoom = 0.0d;
        }
        zoomShift = zoom;
    }

    /**
     * Returns the minimum zoom value necessary for labels to be drawn below the backbone. When the map is drawn using a
     * zoom value less than the value set using this method, all the labels are drawn on the outside of the backbone
     * circle. When the map is drawn using a zoom value greater than the value set using this method, the labels for
     * features on the reverse strand are drawn on the inside of the backbone circle. Also, when the zoom value is
     * greater than the value set using this method, only the features in the visible portion of the map are drawn.
     *
     * @return the minimum zoom value necessary for labels to be drawn below the backbone.
     */
    protected double getZoomShift() {
        return zoomShift;
    }

    /**
     * Sets the maximum number of labels to attempt to arrange for display. Additional labels are discarded prior to the
     * arrangement process.
     *
     * @param number the maximum number of labels to attempt to arrange for display.
     */
    public void setLabelsToKeep(int number) {
        if (number < 0) {
            number = 0;
        }
        labelsToKeep = number;
    }

    /**
     * Returns the maximum number of labels to attempt to arrange for display. Additional labels are discarded prior to
     * the arrangement process.
     *
     * @return the maximum number of labels to attempt to arrange for display.
     */
    public int getLabelsToKeep() {
        return labelsToKeep;
    }

    /**
     * Sets whether or not labels should be randomly shuffled before removing excess labels.
     *
     * @param shuffle whether or not labels should be randomly shuffled before removing excess labels.
     */
    public void setLabelShuffle(boolean shuffle) {
        labelShuffle = shuffle;
    }

    /**
     * Returns true if labels are to be randomly shuffled before removing excess labels.
     *
     * @return whether or not labels should be randomly shuffled before removing excess labels.
     */
    public boolean getLabelShuffle() {
        return labelShuffle;
    }

    /**
     * Sets whether or not labels should be drawn with a colored background.
     *
     * @param coloredBackground whether or not labels should be drawn with a colored background.
     */
    public void setUseColoredLabelBackgrounds(boolean coloredBackground) {
        useColoredLabelBackgrounds = coloredBackground;
    }

    /**
     * Returns true if labels are to be drawn with a colored background.
     *
     * @return whether or not labels are to be drawn with a colored background.
     */
    public boolean getUseColoredLabelBackgrounds() {
        return useColoredLabelBackgrounds;
    }

    /**
     * Sets whether or not tick marks are drawn.
     *
     * @param draw whether or not tick marks are drawn.
     */
    public void setDrawTickMarks(boolean draw) {
        drawTickMarks = draw;
    }

    /**
     * Returns true if tick marks are to be drawn.
     *
     * @return whether or not tick marks are to be drawn.
     */
    public boolean getDrawTickMarks() {
        return drawTickMarks;
    }

    /**
     * Sets the title of of the map. This title is drawn in the center of the backbone circle when an unzoomed map is
     * drawn.
     *
     * @param title the title of the map.
     */
    public void setTitle(String title) {
        this.title = title.trim();
    }

    /**
     * Returns the title of the map. This title is drawn in the center of the backbone circle when an unzoomed map is
     * drawn.
     *
     * @return the title of the map.
     */
    public String getTitle() {
        return title;
    }

    /**
     * Returns an arrayList containing the FeatureSlot objects associated with this Cgview.
     *
     * @return an arrayList of FeatureSlot objects.
     */
    protected ArrayList getFeatureSlots() {
        return featureSlots;
    }

    /**
     * Removes legends from this Cgview.
     *
     */
    protected void removeLegends() {
        this.legends.clear();
    }

    /**
     * Removes labels from this Cgview.
     *
     */
    protected void removeLabels() {
        this.outerLabels.clear();
        this.innerLabels.clear();
    }

    /**
     * Returns an arrayList of OuterLabel objects associated with this Cgview.
     *
     * @return an arrayList of OuterLabel objects.
     */
    protected ArrayList getOuterLabels() {
        return outerLabels;
    }

    /**
     * Returns an arrayList of InnerLabel objects associated with this Cgview.
     *
     * @return an arrayList of InnerLabel objects.
     */
    protected ArrayList getInnerLabels() {
        return innerLabels;
    }

    /**
     * Returns an arrayList of LabelBounds objects associated with this Cgview.
     *
     * @return an arrayList of LabelBounds objects.
     */
    public ArrayList getLabelBounds() {
        return labelBounds;
    }

    /**
     * Returns the radius of the first (nearest to the backbone) direct strand FeatureSlot in this Cgview.
     *
     * @return the radius.
     */
    protected double getFirstOuterFeatureRadius() {
        return backboneRadius + 0.5d * backboneThickness + featureSlotSpacing;
    }

    /**
     * Returns the radius of the first (nearest to the backbone) reverse strand FeatureSlot in this Cgview.
     *
     * @return the radius.
     */
    protected double getFirstInnerFeatureRadius() {
        return backboneRadius - 0.5d * backboneThickness - featureSlotSpacing;
    }

    /**
     * Returns the radius of the last (furthest from the backbone) direct strand FeatureSlot in this Cgview.
     *
     * @return the radius.
     */
    protected double getLastOuterFeatureRadius() {
        double radius = this.getFirstOuterFeatureRadius();
        Iterator i = featureSlots.iterator();
        while (i.hasNext()) {
            FeatureSlot currentFeatureSlot = (FeatureSlot) i.next();
            if (currentFeatureSlot.getStrand() == DIRECT_STRAND) {
                radius = radius + featureSlotSpacing + currentFeatureSlot.getFeatureThickness();
            }

        }
        return radius;
    }

    /**
     * Returns the radius of the last (furthest from the backbone) reverse strand FeatureSlot in this Cgview.
     *
     * @return the radius.
     */
    protected double getLastInnerFeatureRadius() {
        double radius = this.getFirstInnerFeatureRadius();
        Iterator i = featureSlots.iterator();
        while (i.hasNext()) {
            FeatureSlot currentFeatureSlot = (FeatureSlot) i.next();
            if (currentFeatureSlot.getStrand() == REVERSE_STRAND) {
                radius = radius - featureSlotSpacing - currentFeatureSlot.getFeatureThickness();
            }

        }
        return radius;
    }

    /**
     * Returns a rectangle corresponding to the visible portion of the map.
     *
     * @return a rectangle.
     */
    protected Rectangle2D getBackgroundRectangle() {
        return backgroundRectangle;
    }

    /**
     * Returns a rectangle that covers the map title.
     *
     * @return a rectangle.
     */
    protected Rectangle2D getTitleRectangle() {
        return titleRectangle;
    }

    /**
     * Returns a rectangle that covers the length portion of the map title.
     *
     * @return a rectangle.
     */
    protected Rectangle2D getLengthRectangle() {
        return lengthRectangle;
    }

    /**
     * Returns a radians representation of the given base.
     *
     * @return a radians representation of the given base.
     */
    protected double getRadians(double base) {
        if (virtualZoomMultiplier <= 1.0d) {
            return (base * ((2.0d * Math.PI) / (double) (sequenceLength))) - ((Math.PI / 180.0d) * origin);
        } else {
            double centerRadians = (centerBase * ((2.0d * Math.PI) / (double) (sequenceLength))) - ((Math.PI / 180.0d) * origin);
            double baseDiff;
            double baseDiffRadians;

            if ((inZoomRangeOne(centerBase)) && (inZoomRangeOne(base))) {
                baseDiff = (double) centerBase - base;
                baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                return centerRadians - baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);

            } else if ((inZoomRangeTwo(centerBase)) && (inZoomRangeOne(base))) {
                if (zoomRangeTwoStart != 0) {
                    baseDiff = (double) centerBase - base;
                    baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                    return centerRadians - baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                } else {
                    baseDiff = (double) sequenceLength - base + (double) centerBase;
                    baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                    return centerRadians - baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                }
            } else if ((inZoomRangeOne(centerBase)) && (inZoomRangeTwo(base))) {
                if (zoomRangeTwoStart != 0) {
                    baseDiff = (double) centerBase - base;
                    baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                    return centerRadians - baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                } else {
                    baseDiff = (double) sequenceLength - (double) centerBase + base;
                    baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                    return centerRadians + baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                }
            } else { //if ((inZoomRangeTwo(centerBase)) && (inZoomRangeTwo(base))) {
                baseDiff = (double) centerBase - base;
                baseDiffRadians = (baseDiff * ((2.0d * Math.PI) / (double) (sequenceLength)));
                return centerRadians - baseDiffRadians * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
            }
        }
    }


    /**
     * Returns a radians representation of the given base.
     *
     * @return a radians representation of the given base.
     */
    protected double getRadians(int base) {
        if ((virtualZoomMultiplier <= 1.0f) || (base == centerBase)) {
            return (base * ((2.0d * Math.PI) / (double) (sequenceLength))) - ((Math.PI / 180.0d) * origin);
        } else {
            return getRadians((double) base);
        }
    }

    /**
     * Returns a Point2D specifying the center of this cgview.
     *
     * @return the center of this cgview.
     */
    protected Point2D getCenter() {
        return centerPoint;
    }

    /**
     * Returns a degrees representation of the given base. The virtualZoomMultiplier stretches features.
     *
     * @return a degrees representation of the given base.
     */
    protected double getDegrees(int base) {
        //System.out.print ("The base is " + base + " ");
        //System.out.println ("the degrees was " + (((double)base / (double)sequenceLength) * 360.0d));
        if ((virtualZoomMultiplier <= 1.0d) || (base == centerBase)) {
            return ((double) base / (double) sequenceLength) * 360.0d;
        } else {
            double centerDegrees = ((double) centerBase / (double) sequenceLength) * 360.0d;
            int baseDiff;
            double baseDiffDegrees;

            if ((inZoomRangeOne(centerBase)) && (almostInZoomRangeOne(base))) {
                baseDiff = centerBase - base;
                baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                //System.out.println ("the degrees is A " + (centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                return centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);

            } else if ((inZoomRangeTwo(centerBase)) && (almostInZoomRangeOne(base))) {
                if (zoomRangeTwoStart != 0) {
                    baseDiff = centerBase - base;
                    baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                    //System.out.println ("the degrees is B " + (centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                    return centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                } else {
                    baseDiff = sequenceLength - base + centerBase;
                    baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                    //System.out.println ("the degrees is C " + (centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                    return centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                }
            } else if ((inZoomRangeOne(centerBase)) && (inZoomRangeTwo(base))) {
                if (zoomRangeTwoStart != 0) {
                    baseDiff = centerBase - base;
                    baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                    //System.out.println ("the degrees is D " + (centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                    return centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                } else {
                    baseDiff = sequenceLength - centerBase + base;
                    baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                    //System.out.println ("the degrees is E " + (centerDegrees + baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                    return centerDegrees + baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
                }
            } else { //if ((inZoomRangeTwo(centerBase)) && (inZoomRangeTwo(base))) {
                baseDiff = centerBase - base;
                baseDiffDegrees = ((double) baseDiff / (double) sequenceLength) * 360.0d;
                //System.out.println ("the degrees is F " + (centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier)));
                return centerDegrees - baseDiffDegrees * ((virtualZoomMultiplier + zoomMultiplier) / zoomMultiplier);
            }
        }
    }


    /**
     * Specifies under what circumstances labels should be drawn on the inside of the backbone circle. When set to
     * {@link CgviewConstants#INNER_LABELS_NO_SHOW  CgviewConstants.INNER_LABELS_NO_SHOW}, all the labels are drawn on
     * the outside of the backbone circle.  When set to {@link CgviewConstants#INNER_LABELS_AUTO
     * CgviewConstants.INNER_LABELS_AUTO}, all the labels are drawn on the outside of the backbone circle, unless a
     * zoomed map is drawn. When set to {@link CgviewConstants#INNER_LABELS_SHOW  CgviewConstants.INNER_LABELS_SHOW},
     * the labels for features on the reverse strand are drawn on the inside of the backbone circle.
     *
     * @param useInnerLabels
     */
    public void setUseInnerLabels(int useInnerLabels) {
        this.useInnerLabels = useInnerLabels;
    }

    /**
     * Returns the inner label behaviour of this Cgview.
     *
     * @return {@link CgviewConstants#INNER_LABELS_NO_SHOW  CgviewConstants.INNER_LABELS_NO_SHOW}, {@link
     *         CgviewConstants#INNER_LABELS_AUTO  CgviewConstants.INNER_LABELS_AUTO}, or {@link
     *         CgviewConstants#INNER_LABELS_SHOW  CgviewConstants.INNER_LABELS_SHOW}.
     */
    public int getUseInnerLabels() {
        return useInnerLabels;
    }

    /**
     * Specifies under what circumstances feature position information should be added to the label text. When set to
     * {@link CgviewConstants#POSITIONS_NO_SHOW  CgviewConstants.POSITIONS_NO_SHOW}, position information is not added
     * to the label.  When set to {@link CgviewConstants#POSITIONS_AUTO  CgviewConstants.POSITIONS_AUTO}, position
     * information is added only when a zoomed map is drawn. When set to {@link CgviewConstants#POSITIONS_SHOW
     * CgviewConstants.POSITIONS_SHOW}, position information is added to the labels.
     *
     * @param giveFeaturePositions
     */
    public void setGiveFeaturePositions(int giveFeaturePositions) {
        this.giveFeaturePositions = giveFeaturePositions;
    }

    /**
     * Returns the feature position labelling behaviour of this Cgview.
     *
     * @return {@link CgviewConstants#POSITIONS_NO_SHOW  CgviewConstants.POSITIONS_NO_SHOW}, {@link
     *         CgviewConstants#POSITIONS_AUTO  CgviewConstants.POSITIONS_AUTO}, or {@link CgviewConstants#POSITIONS_SHOW
     *         CgviewConstants.POSITIONS_SHOW}.
     */
    public int getGiveFeaturePositions() {
        return giveFeaturePositions;
    }

    /**
     * Returns the distance moved by the labels each time they are moved away from the map backbone during label
     * repositioning.
     *
     * @return the distance moved by the labels.
     */
    protected double getRadiusShiftAmount() {
        return radiusShiftAmount;
    }

    /**
     * Controls label movement along the map backbone. When labels are moved along the backbone during label placement,
     * the increment moved by labels is proportional to the <code>shiftValue</code> constant. If the lines extending to
     * labels are crossing over one another the <code>shiftValue</code> constant should be set to a smaller value. The
     * default value is <code>0.20</code>.
     *
     * @param shiftValue a value that controls label movement along the map backbone.
     */
    protected void setRadiansShiftConstant(double shiftValue) {
        if (shiftValue < 0) {
            radiansShiftConstant = 0.0f;
        } else {
            radiansShiftConstant = shiftValue;
        }
    }

    /**
     * Returns a constant that controls label movement along the map backbone. When labels are moved along the backbone
     * during label placement, the increment moved by labels is proportional to the <code>RADIANS_SHIFT</code> constant.
     * If the lines extending to labels are crossing over one another the <code>RADIANS_SHIFT</code> constant should be
     * set to a smaller value. The default value is <code>0.20</code>.
     *
     * @return the <code>shiftValue</code>. It controls label movement along the map backbone.
     */
    protected double getRadiansShiftConstant() {
        return radiansShiftConstant;
    }

    /**
     * Specifies how carefully labels placed on the map. Higher values lead to more labels being placed without
     * overlaps. Higher values also lead to slower program execution. Hight quality label placement can be used when
     * there are fewer than 100 labels, but when there are more than 100 a low quality setting is recommended.
     *
     * @param labelPlacementQuality a value between <code>0</code> and <code>10</code> specifying how carefully labels
     *                              are placed on the map. The default value is <code>5</code>.
     * @see #setLabelsToKeep(int) setLabelsToKeep
     */
    public void setLabelPlacementQuality(int labelPlacementQuality) {
        if (labelPlacementQuality < 1) {
            this.labelPlacementQuality = 1;
        } else if (labelPlacementQuality > 10) {
            this.labelPlacementQuality = 10;
        } else {
            this.labelPlacementQuality = labelPlacementQuality;
        }

        if (this.labelPlacementQuality == 0) {
            spreadIterations = 0;
            clashSpan = 0;
        } else if (this.labelPlacementQuality == 1) {
            spreadIterations = 10;
            clashSpan = 10;
            radiusShiftAmount = 20.0d;
        } else if (this.labelPlacementQuality == 2) {
            spreadIterations = 20;
            clashSpan = 40;
            radiusShiftAmount = 15.0d;
        } else if (this.labelPlacementQuality == 3) {
            spreadIterations = 30;
            clashSpan = 60;
            radiusShiftAmount = 14.0d;
        } else if (this.labelPlacementQuality == 4) {
            spreadIterations = 40;
            clashSpan = 80;
            radiusShiftAmount = 12.0d;
        } else if (this.labelPlacementQuality == 5) {
            spreadIterations = 50;
            clashSpan = 100;
            radiusShiftAmount = 10.0d;
        } else if (this.labelPlacementQuality == 6) {
            spreadIterations = 60;
            clashSpan = 120;
            radiusShiftAmount = 8.0d;
        } else if (this.labelPlacementQuality == 7) {
            spreadIterations = 65;
            clashSpan = 130;
            radiusShiftAmount = 6.0d;
        } else if (this.labelPlacementQuality == 8) {
            spreadIterations = 70;
            clashSpan = 140;
            radiusShiftAmount = 4.0d;
        } else if (this.labelPlacementQuality == 9) {
            spreadIterations = 200;
            clashSpan = 150;
            radiusShiftAmount = 2.0d;
        } else if (this.labelPlacementQuality == 10) {
            spreadIterations = 500;
            clashSpan = 1000;
            radiusShiftAmount = 1.0d;
        }
    }

    /**
     * Returns the zoom multiplier for the map, as set using the {@link #drawZoomed(Graphics2D, double, int, boolean)
     * drawZoomed()} method.
     *
     * @return the zoom multiplier.
     */
    protected double getZoomMultiplier() {
        return zoomMultiplier;
    }

    /**
     * Returns true if the entire plasmid should be drawn.
     *
     * @return true if the entire plasmid should be drawn.
     */
    protected boolean getDrawEntirePlasmid() {
        return drawEntirePlasmid;
    }

    /**
     * Returns the number of the first base inside the first zoom range. When drawing a zoomed map, two ranges are
     * calculated. These ranges contain all the base positions that should be drawn.
     *
     * @return the number of the first base inside the first zoom range.
     */
    protected int getZoomRangeOneStart() {
        return zoomRangeOneStart;
    }

    /**
     * Returns the number of the last base inside the first zoom range. When drawing a zoomed map, two ranges are
     * calculated. These ranges contain all the base positions that should be drawn.
     *
     * @return the number of the last base inside the first zoom range.
     */
    protected int getZoomRangeOneStop() {
        return zoomRangeOneStop;
    }

    /**
     * Returns the number of the first base inside the second zoom range. When drawing a zoomed map, two ranges are
     * calculated. These ranges contain all the base positions that should be drawn.
     *
     * @return the number of the first base inside the second zoom range.
     */
    protected int getZoomRangeTwoStart() {
        return zoomRangeTwoStart;
    }

    /**
     * Returns the number of the last base inside the second zoom range. When drawing a zoomed map, two ranges are
     * calculated. These ranges contain all the base positions that should be drawn.
     *
     * @return the number of the last base inside the second zoom range.
     */
    protected int getZoomRangeTwoStop() {
        return zoomRangeTwoStop;
    }

    /**
     * Returns true if the existing labels (those generated by the previous draw operation) are to be drawn. Returns
     * false if a new set of labels is to be generated and positioned.
     *
     * @return whether or not the existing labels are to be drawn.
     */
    protected boolean getKeepLastLabels() {
        return keepLastLabels;
    }

    /**
     * Returns true if the base is located within the visible region of the map.
     *
     * @return whether or not the base is located within the visible region of the map.
     */
    protected boolean baseIsDrawable(int base) {
        if (zoomMultiplier >= zoomShift) {
            if ((base >= zoomRangeOneStart) && (base <= zoomRangeOneStop)) {
                return true;
            } else if ((base >= zoomRangeTwoStart) && (base <= zoomRangeTwoStop)) {
                return true;
            } else if (base < zoomRangeOneStart) {
                return false;
            } else if (base > zoomRangeTwoStop) {
                return false;
            } else {
                return true;
            }
        } else {
            return true;
        }
    }

    /**
     * Returns true if the base is located within the first zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRangeOne(int base) {
        if ((base >= zoomRangeOneStart) && (base <= zoomRangeOneStop)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the base is located within the first zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRangeOne(double base) {
        if ((base >= zoomRangeOneStart) && (base <= zoomRangeOneStop)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the base is located within the first zoom range, or if it is one base less than the start.
     *
     * @return whether or not the base is located within the first zoom range, or if it is one base less than the
     *         start.
     */
    protected boolean almostInZoomRangeOne(int base) {
        if ((base >= zoomRangeOneStart) && (base <= zoomRangeOneStop)) {
            return true;
        } else if (((base + 1) >= zoomRangeOneStart) && ((base + 1) <= zoomRangeOneStop)) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Returns true if the base is located within the second zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRangeTwo(int base) {
        if ((base >= zoomRangeTwoStart) && (base <= zoomRangeTwoStop)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the base is located within the second zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRangeTwo(double base) {
        if ((base >= zoomRangeTwoStart) && (base <= zoomRangeTwoStop)) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Returns true if the base is located within the first zoom range or the second zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRange(int base) {
        if ((inZoomRangeOne(base)) || (inZoomRangeTwo(base))) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the base is located within the first zoom range or the second zoom range.
     *
     * @return whether or not the base is located within the first zoom range.
     */
    protected boolean inZoomRange(double base) {
        if ((inZoomRangeOne(base)) || (inZoomRangeTwo(base))) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Adds an outerLabel object to this cgview object.
     *
     * @param label an outerLabel object.
     */
    protected void addOuterLabel(Label label) {
        outerLabels.add(label);
    }

    /**
     * Adds an InnerLabel object to this cgview object.
     *
     * @param label an innerLabel object.
     */
    protected void addInnerLabel(Label label) {
        innerLabels.add(label);
    }

    /**
     * Returns an estimate of the maximum suitable zoom value for this map.
     *
     * @return an estimate of the maximum suitable zoom value for this map.
     */
    public double getZoomMax() {

        smallestDimension = Math.min(width, height);

        //this is an estimate of the viewing area
        double viewDiagonal = Math.sqrt(smallestDimension * smallestDimension + smallestDimension * smallestDimension);
        double basePerCircum = (double) (sequenceLength) / (2.0d * Math.PI * backboneRadius);

        //this limits zoom to the point where 10 bases are shown on the map
        double zoomMultiplierMaxForThisSequence = (viewDiagonal * basePerCircum) / 10;
	return zoomMultiplierMaxForThisSequence;
    }

    /**
     * Translates the canvas.
     *
     */
    protected void translateCanvas() {
	if (centerPoint != null) {

	    AffineTransform at = new AffineTransform();
	    at.setToTranslation((width / 2) - centerPoint.getX(), (height / 2) - centerPoint.getY());
	    gg.transform(at);

	    //create a rectangle for the background
	    backgroundRectangle = new Rectangle2D.Double(centerPoint.getX() - (width / 2), centerPoint.getY() - (height / 2), width, height);
	}
    }

    /**
     * Undoes the canvas translation, so that point(0,0) is in upper left.
     *
     */
    protected void untranslateCanvas() {
	if (centerPoint != null) {

	    AffineTransform at = new AffineTransform();

	    at.setToTranslation((-width / 2) + centerPoint.getX(), (-height / 2) + centerPoint.getY());
	    gg.transform(at);

	    //create a rectangle for the background
	    backgroundRectangle = new Rectangle2D.Double(0.0d, 0.0d, width, height);

	}
    }

    /**
     * Translates the canvas so that point (0,0) is in the center.
     *
     */
    protected void translateZeroCenter() {

	AffineTransform at = new AffineTransform();
        at.setToTranslation((width / 2), (height / 2));
        gg.transform(at);

        backgroundRectangle = new Rectangle2D.Double(-(width / 2), -(height / 2), width, height);
	
    }

    /**
     * Undoes the translation of the canvas that was done to place point (0,0) at the center, so that point(0,0) becomes upper left.
     *
     */
    protected void untranslateZeroCenter() {

	AffineTransform at = new AffineTransform();
        at.setToTranslation((-width / 2), (-height / 2));
        gg.transform(at);

        backgroundRectangle = new Rectangle2D.Double(0.0d, 0.0d, width, height);
	
    }

    /**
     * Checks the zoomMultiplier value and makes adjustments if necessary
     *
     * @param zoomMultiplier the factor to zoom in by.
     */
    protected double adjustZoom(double zoomMultiplier) {

        //determine a suitable maximum zoomMultiplier for the sequence

        smallestDimension = Math.min(width, height);

        //this is an estimate of the viewing area
        double viewDiagonal = Math.sqrt(smallestDimension * smallestDimension + smallestDimension * smallestDimension);
        double basePerCircum = (double) (sequenceLength) / (2.0d * Math.PI * backboneRadius);

        //this limits zoom to the point where 10 bases are shown on the map
        double zoomMultiplierMaxForThisSequence = (viewDiagonal * basePerCircum) / 10;

        //check the zoomMultiplier.
        if (zoomMultiplier < 1.0d) {
            zoomMultiplier = 1.0d;
        }

        if (zoomMultiplierMaxForThisSequence < 1.0d) {
            zoomMultiplierMaxForThisSequence = 1.0d;
        }

        if (zoomMultiplier > zoomMultiplierMaxForThisSequence) {
            zoomMultiplier = zoomMultiplierMaxForThisSequence;
        }

        if (zoomMultiplier > ZOOM_MULTIPLIER_MAX) {
            virtualZoomMultiplier = zoomMultiplier - ZOOM_MULTIPLIER_MAX;
            zoomMultiplier = ZOOM_MULTIPLIER_MAX;
        }

        if (virtualZoomMultiplier > VIRTUAL_ZOOM_MULTIPLIER_MAX) {
            virtualZoomMultiplier = VIRTUAL_ZOOM_MULTIPLIER_MAX;
        }
        return zoomMultiplier;
    }

    /**
     * Draws this Cgview map into the specified Graphics2D context. The map is drawn such that it is zoomed in by a
     * factor of <code>zoom</code>, and centered on the base at position <code>centerBase</code>.
     *
     * @param gg             the <code>Graphics2D</code> context for rendering.
     * @param zoom           the factor to zoom in by.
     * @param center         the base position to center the map on.
     * @param keepLastLabels <code>true</code> if the labels from the last draw operation should be redrawn without
     *                       repositioning, or <code>false</code> if instead new labels should be generated and then
     *                       positioned.
     */
    public void drawZoomed(Graphics2D gg, double zoom, int center, boolean keepLastLabels) {
        this.keepLastLabels = keepLastLabels;
        drawZoomed(gg, zoom, center);
    }

    /**
     * Draws this Cgview map into the specified Graphics2D context. The map is drawn such that it is zoomed in by a
     * factor of <code>zoom</code>, and centered on the base at position <code>centerBase</code>.
     *
     * @param gg     the <code>graphics2D</code> context for rendering.
     * @param zoom   the factor to zoom in by.
     * @param center the base position to center the map on.
     */
    public void drawZoomed(Graphics2D gg, double zoom, int center) {

        this.gg = gg;
        zoomMultiplier = adjustZoom(zoom);
        centerBase = center;

        smallestDimension = Math.min(width, height);
        if (backboneRadius > 0.80d * smallestDimension / 2.0d) {
            backboneRadius = 0.80d * smallestDimension / 2.0d;
            System.err.println("[warning] backbone radius was adjusted to fit inside of canvas.");
        }
        if (backboneRadius < 10.0d) {
            backboneRadius = 10.0d;
            System.err.println("[warning] backbone radius was increased to 10.0.");
        }

        //use the zoomMultiplier to adjust the backboneRadius;
        double originalBackboneRadius = backboneRadius;
        backboneRadius = backboneRadius * zoomMultiplier;
        virtualBackboneRadius = originalBackboneRadius * (zoomMultiplier + virtualZoomMultiplier - 1.0d);

        if (centerBase < 0) {
            centerBase = 0;
        } else if (centerBase > sequenceLength) {
            centerBase = sequenceLength;
        }


        //determine the radians for the centerBase
        double radians = getRadians(centerBase);

        //now determine the x and y coordinates on the backbone
        double x = Math.cos(radians) * backboneRadius;
        double y = Math.sin(radians) * backboneRadius;

        //set centerPoint
        centerPoint = new Point2D.Double(x, y);

        //set render quality
        setRenderQuality();

        //now complete the translation
	translateCanvas();

        //fill the background
        gg.setPaint(backgroundColor);
        gg.fill(backgroundRectangle);

        //change background rectangle to a square
        //backgroundRectangle = new Rectangle2D.Double(x - (smallestDimension/2), y - (smallestDimension/2), smallestDimension, smallestDimension);

        //now determine the length of the backbone arc that spans the viewing area by
        //shifting radians down, and then up
        //this is intended to determine which bases on the plasmid should be drawn. It doesn't have to be completely accurate because the graphics2d clipping region is set so that things are not drawn outside of the canvas.

        if (zoomMultiplier >= zoomShift) {

            double innerMostRadiusToDraw = this.getLastInnerFeatureRadius() - featureSlotSpacing - tickLength;
            double outerMostRadiusToDraw = this.getLastOuterFeatureRadius() + featureSlotSpacing + tickLength;

            double downshift = 0.0d;
            double upshift = 0.0d;

            double xInner = Math.cos(radians) * innerMostRadiusToDraw;
            double yInner = Math.sin(radians) * innerMostRadiusToDraw;

            double tempX1 = xInner;
            double tempY1 = yInner;
            double tempX2 = xInner;
            double tempY2 = yInner;

            double tempRadians = radians;
            double shiftAmount = ((1.0d / 2.0d) * Math.PI) / (backboneRadius);
            Point2D checkPointInner = new Point2D.Double(xInner, yInner);
            Point2D checkPointOuter = new Point2D.Double(xInner, yInner);
            drawEntirePlasmid = false;

            while (((backgroundRectangle.contains(checkPointInner)) || (backgroundRectangle.contains(checkPointOuter))) && ((radians - tempRadians) < 2.0d * Math.PI)) {
                tempRadians = tempRadians - shiftAmount;
                tempX1 = Math.cos(tempRadians) * innerMostRadiusToDraw;
                tempY1 = Math.sin(tempRadians) * innerMostRadiusToDraw;
                tempX2 = Math.cos(tempRadians) * outerMostRadiusToDraw;
                tempY2 = Math.sin(tempRadians) * outerMostRadiusToDraw;
                checkPointInner.setLocation(tempX1, tempY1);
                checkPointOuter.setLocation(tempX2, tempY2);
            }

            if ((radians - tempRadians) >= 2.0d * Math.PI) {
                drawEntirePlasmid = true;
            }

            downshift = radians - tempRadians;
            //System.out.println ("shiftamount is " + shiftAmount);

            //System.out.println ("downshift is " + downshift);
            checkPointInner.setLocation(xInner, yInner);
            checkPointOuter.setLocation(xInner, yInner);

            //new
            tempRadians = radians;

            while (((backgroundRectangle.contains(checkPointInner)) || (backgroundRectangle.contains(checkPointOuter))) && ((tempRadians - radians) < 2.0d * Math.PI)) {
                tempRadians = tempRadians + shiftAmount;
                tempX1 = Math.cos(tempRadians) * innerMostRadiusToDraw;
                tempY1 = Math.sin(tempRadians) * innerMostRadiusToDraw;
                tempX2 = Math.cos(tempRadians) * outerMostRadiusToDraw;
                tempY2 = Math.sin(tempRadians) * outerMostRadiusToDraw;
                checkPointInner.setLocation(tempX1, tempY1);
                checkPointOuter.setLocation(tempX2, tempY2);
            }

            if ((tempRadians - radians) >= 2.0d * Math.PI) {
                drawEntirePlasmid = true;
            }

            upshift = tempRadians - radians;
            //System.out.println ("upshift is " + upshift);

            double basePerCircum = (double) (sequenceLength) / (2.0d * Math.PI * backboneRadius);
            double baseSpanUpd = upshift * basePerCircum * backboneRadius;
            double baseSpanDownd = downshift * basePerCircum * backboneRadius;

            baseSpanUpd = baseSpanUpd * (zoomMultiplier / (zoomMultiplier + virtualZoomMultiplier - 1));
            baseSpanDownd = baseSpanDownd * (zoomMultiplier / (zoomMultiplier + virtualZoomMultiplier - 1));

            //add 20% to each to make sure they extend off of the canvas
            baseSpanUpd = baseSpanUpd + baseSpanUpd * 0.20d;
            baseSpanDownd = baseSpanDownd + baseSpanDownd * 0.20d;

            //System.out.println ("baseSpanUpd is " + baseSpanUpd);
            int baseSpanUp = Math.round((float) (baseSpanUpd));
            int baseSpanDown = Math.round((float) (baseSpanDownd));

            if ((!drawEntirePlasmid) && ((centerBase + baseSpanUp) > sequenceLength)) {
                zoomRangeOneStart = centerBase - baseSpanDown;
                zoomRangeOneStop = sequenceLength;
                zoomRangeTwoStart = 0;
                zoomRangeTwoStop = baseSpanUp - (sequenceLength - centerBase);
            } else if ((!drawEntirePlasmid) && ((centerBase - baseSpanDown) < 1)) {
                zoomRangeOneStart = sequenceLength - (baseSpanDown - centerBase);
                zoomRangeOneStop = sequenceLength;
                zoomRangeTwoStart = 0;
                zoomRangeTwoStop = centerBase + baseSpanUp;
            } else if (!drawEntirePlasmid) {
                zoomRangeOneStart = centerBase - baseSpanDown;
                zoomRangeOneStop = centerBase;
                zoomRangeTwoStart = centerBase;
                zoomRangeTwoStop = centerBase + baseSpanUp;
            }
        }

        //System.out.println ("zoomRangeOneStart is " + zoomRangeOneStart);
        //System.out.println ("zoomRangeOneStop is " + zoomRangeOneStop);
        //System.out.println ("zoomRangeTwoStart is " + zoomRangeTwoStart);
        //System.out.println ("zoomRangeTwoStop is " + zoomRangeTwoStop);

        drawMain();

        //return the backboneRadius to its original value
        backboneRadius = originalBackboneRadius;
        virtualBackboneRadius = backboneRadius;
        drawEntirePlasmid = true;

        System.out.println("The map has been drawn.");

    }

    /**
     * Draws this Cgview map into the specified Graphics2D context
     *
     * @param gg             the <code>graphics2D</code> context for rendering.
     * @param keepLastLabels <code>true</code> if the labels from the last draw operation should be redrawn without
     *                       repositioning, or <code>false</code> if instead new labels should be generated and then
     *                       positioned.
     */
    public void draw(Graphics2D gg, boolean keepLastLabels) {
        this.keepLastLabels = keepLastLabels;
        draw(gg);
    }

    /**
     * Draws this Cgview map into the specified Graphics2D context
     *
     * @param gg the Graphics2D context for rendering.
     */
    public void draw(Graphics2D gg) {

        this.gg = gg;
        zoomMultiplier = 1.0d;

        smallestDimension = Math.min(width, height);
        if (backboneRadius > 0.80d * smallestDimension / 2.0d) {
            backboneRadius = 0.80d * smallestDimension / 2.0d;
            System.err.println("[warning] backbone radius was adjusted to fit inside of canvas.");
        }
        if (backboneRadius < 10.0d) {
            backboneRadius = 10.0d;
            System.err.println("[warning] backbone radius was increased to 10.0.");
        }

        virtualBackboneRadius = backboneRadius;

        setRenderQuality();

        centerPoint = new Point2D.Double(0, 0);

	translateCanvas();

        //fill the background
        gg.setPaint(backgroundColor);
        gg.fill(backgroundRectangle);

        //change background rectangle to a square
        //backgroundRectangle = new Rectangle2D.Double(-(smallestDimension/2), -(smallestDimension/2), smallestDimension, smallestDimension);

        drawMain();

	System.out.println("The map has been drawn.");

    }

    /**
     * Sets the render quality for the Graphics2D object.
     */
    private void setRenderQuality() {
        gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        gg.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        gg.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        gg.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        gg.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        gg.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
        gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

	//gg.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    /**
     * Calls more specialized drawing methods.
     */
    private void drawMain() {

        Iterator i;

        if (drawEntirePlasmid) {
            placeTitle();
        }

        //remove labels from last draw if necessary
        if (!keepLastLabels) {
            innerLabels.clear();
            outerLabels.clear();
            labelBounds.clear();
            clashLabels = 0;
            totalLabels = 0;
        }

        //draw the contens of the FeatureSlots
        System.out.print("Drawing features.");
        i = featureSlots.iterator();
        while (i.hasNext()) {
            FeatureSlot currentFeatureSlot = (FeatureSlot) i.next();
            currentFeatureSlot.draw();
            System.out.print(".");
        }
        System.out.println(".");

	drawBackbone();

	if (isLinear) {
	    drawLinearDividerLine();	    
	}

        if ((globalLabel == LABEL) || ((globalLabel == LABEL_ZOOMED) && (zoomMultiplier >= zoomShift))) {
            totalLabels = outerLabels.size() + innerLabels.size();

            System.out.print("Positioning and drawing " + totalLabels + " labels.");

            //draw the new labels
            if (!keepLastLabels) {
                drawLabels(innerLabels);
                drawLabels(outerLabels);
            } else {
                //or draw the labels from the last draw operation.
                i = innerLabels.iterator();
                while (i.hasNext()) {
                    Label currentLabel = (Label) i.next();
                    currentLabel.drawLabelLine();
                }
                i = outerLabels.iterator();
                while (i.hasNext()) {
                    Label currentLabel = (Label) i.next();
                    currentLabel.drawLabelLine();
                }

		untranslateCanvas();
                i = innerLabels.iterator();
                while (i.hasNext()) {
                    Label currentLabel = (Label) i.next();
                    currentLabel.drawLabelText();
                }
                i = outerLabels.iterator();
                while (i.hasNext()) {
                    Label currentLabel = (Label) i.next();
                    currentLabel.drawLabelText();
                }
		translateCanvas();
            }

            System.out.println(".");

            System.out.println(clashLabels + " labels were removed.");
        }

        if (drawTickMarks) {

            System.out.println("Drawing tick marks.");

            drawTickMarks(DIRECT_STRAND, this.getLastOuterFeatureRadius() + 0.5d * tickThickness);

            drawTickMarks(REVERSE_STRAND, this.getLastInnerFeatureRadius() - 0.5d * tickThickness);

        }

	//undo the translation here
	untranslateCanvas();
	translateZeroCenter();

        drawWarningMessage();

        if (drawEntirePlasmid) {
            drawTitle();
        }

        //draw border
        if (showBorder) {
            double borderThickness = 2.0d;
            Rectangle2D border = new Rectangle2D.Double(backgroundRectangle.getX() + 0.5d * borderThickness, backgroundRectangle.getY() + 0.5d * borderThickness, width - borderThickness, height - borderThickness);
            gg.setPaint(borderColor);
            gg.setStroke(new BasicStroke((float) borderThickness, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
            gg.draw(border);
        }

	if (this.drawLegends) {
	    System.out.println("Drawing legends.");
	    //draw legends
	    i = legends.iterator();
	    while (i.hasNext()) {
		Legend currentLegend = (Legend) i.next();
		currentLegend.setBounds(0.0d, 0.0d);
		currentLegend.draw();
		//remove once drawn.
		//i.remove();
	    }
	}
 
	untranslateZeroCenter();	

        keepLastLabels = false;
        legends.remove(infoLegend);
        zoomMultiplier = 1.0f;
        virtualZoomMultiplier = 1.0f;
    }

    /**
     * Draws the sequence backbone.
     */
    private void drawBackbone() {

        double startOfArc;
        double extentOfArc;

        int startBase;
        int stopBase;

        if (!drawEntirePlasmid) {
            startBase = zoomRangeOneStart;
            stopBase = zoomRangeTwoStop;
        } else {
            startBase = 1;
            stopBase = sequenceLength;
        }

        //typical case where start is less than stop
        if (startBase <= stopBase) {
            startOfArc = getDegrees(startBase - 1);
            extentOfArc = getDegrees(stopBase) - startOfArc;
        }
        //case where feature spans start/stop boundary
        else {
            startOfArc = ((double) (startBase - 1.0d) / (double) (sequenceLength)) * 360.0d;
            startOfArc = getDegrees(startBase - 1);
            extentOfArc = getDegrees(sequenceLength) - startOfArc;

            double startOfArcB = getDegrees(1 - 1);
            double extentOfArcB = getDegrees(stopBase) - startOfArcB;

            extentOfArc = extentOfArc + extentOfArcB;
        }

        //draw the plasmid backbone
        BasicStroke arcStroke = new BasicStroke(backboneThickness, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
        Area centralArc = new Area();
        centralArc.add(new Area(arcStroke.createStrokedShape(new Arc2D.Double(-backboneRadius, -backboneRadius, (backboneRadius * 2.0d), (backboneRadius * 2.0d), -startOfArc - extentOfArc + origin, extentOfArc, Arc2D.OPEN))));

	//create an Area to subtract from the backbone if this is a linear molecule
	Area blockArc = null;
	//restrict to case where origin is set to 90.0.
	if ((this.isLinear) && (this.origin == 90.0d)) {
	    if ((drawEntirePlasmid) || (zoomRangeTwoStart == 0)) {
		//create a 3' 5' label to indicate molecule is linear
		FontRenderContext frc = gg.getFontRenderContext();
		TextLayout layout = new TextLayout(this.linearBreakText, rulerFont, frc);
		Rectangle2D bounds = layout.getBounds();
		double textHeight = bounds.getHeight();
		double textWidth = bounds.getWidth();

		double zeroLineRadians = getRadians(0);
		double zeroStartX = (Math.cos(zeroLineRadians) * backboneRadius);
		double zeroStartY = (Math.sin(zeroLineRadians) * backboneRadius);

		double textPositionX = zeroStartX - textWidth / 2.0d - layout.getDescent() * 0.4d;
		double textPositionY = zeroStartY + textHeight / 2.0d;

		//adjust because all caps
		textPositionY = textPositionY + layout.getDescent();

		//draw bounds
                gg.setPaint(backgroundColor);
                bounds.setRect(bounds.getX() + textPositionX - 1.5d, bounds.getY() + textPositionY - layout.getDescent() - 1.5d, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);

		//gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f));
                //gg.fill(bounds);
                //gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));		

                gg.setPaint(rulerFontColor);

		untranslateCanvas();

		layout.draw(gg, (float) (textPositionX + width / 2 - centerPoint.getX()), (float) (textPositionY + height / 2 - centerPoint.getY()) - layout.getDescent());
		translateCanvas();

		//zero base is visible
		//blockLength should be changed to the number of degrees needed to create an arc of length equals textWidth
		double arcInDegrees = Math.toDegrees(textWidth / backboneRadius);
		double blockLength = arcInDegrees;
		//add extra white space
		blockLength = blockLength + blockLength * 0.1d;
		this.zigzagWidth = blockLength / 2.0d;
		//double blockLength = 5.0d;
		double startOfArcBlock = getDegrees(1 - 1) - blockLength / 2.0d;
		double extentOfArcBlock = blockLength;

		BasicStroke blockArcStroke = new BasicStroke(backboneThickness + 1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
		blockArc = new Area(blockArcStroke.createStrokedShape(new Arc2D.Double(-backboneRadius, -backboneRadius, (backboneRadius * 2.0d), (backboneRadius * 2.0d), -startOfArcBlock - extentOfArcBlock + origin, extentOfArcBlock, Arc2D.OPEN)));	
		
	    }
	}

        //to prevent drawing off canvas
	if (blockArc != null) {
	    centralArc.subtract(blockArc);
	}
        centralArc.intersect(new Area(this.getBackgroundRectangle()));
        gg.setPaint(backboneColor);
        gg.fill(centralArc);


        if (showShading) {

            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightOpacity));
            gg.setPaint(Color.white);

            double radiusIncrease = 0.5d * backboneThickness - 0.5d * (backboneThickness * shadingProportion);

            BasicStroke highlightArcStroke = new BasicStroke(backboneThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
            Area highlightArc = new Area();
            highlightArc.add(new Area(highlightArcStroke.createStrokedShape(new Arc2D.Double(-backboneRadius - radiusIncrease, -backboneRadius - radiusIncrease, (backboneRadius + radiusIncrease) * 2.0d, (backboneRadius + radiusIncrease) * 2.0d, -startOfArc - extentOfArc + origin, extentOfArc, Arc2D.OPEN))));
	    if (blockArc != null) {
		highlightArc.subtract(blockArc);
	    }
            highlightArc.intersect(new Area(this.getBackgroundRectangle()));
            gg.fill(highlightArc);


            //draw a shadow arc on inner edge of backbone
            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowOpacity));
            gg.setPaint(Color.black);

            double radiusDecrease = -0.5d * backboneThickness + 0.5d * (backboneThickness * shadingProportion);


            BasicStroke shadowArcStroke = new BasicStroke(backboneThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
            Area shadowArc = new Area();
            shadowArc.add(new Area(shadowArcStroke.createStrokedShape(new Arc2D.Double(-backboneRadius - radiusDecrease, -backboneRadius - radiusDecrease, (backboneRadius + radiusDecrease) * 2.0d, (backboneRadius + radiusDecrease) * 2.0d, -startOfArc - extentOfArc + origin, extentOfArc, Arc2D.OPEN))));
	    if (blockArc != null) {
		shadowArc.subtract(blockArc);
	    }
            shadowArc.intersect(new Area(this.getBackgroundRectangle()));
            gg.fill(shadowArc);

            //return to non transparent
            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
        }

        //set the values of innerArc and outerArc, which will be used for collision testing with labels
        double outerArcRadius = this.getLastOuterFeatureRadius();

        double innerArcRadius = this.getLastInnerFeatureRadius();

        outerArc = new Arc2D.Double(-outerArcRadius, -outerArcRadius, (outerArcRadius * 2.0d), (outerArcRadius * 2.0d), -startOfArc - extentOfArc + origin, extentOfArc, Arc2D.OPEN);

        innerArc = new Arc2D.Double(-innerArcRadius, -innerArcRadius, (innerArcRadius * 2.0d), (innerArcRadius * 2.0d), -startOfArc - extentOfArc + origin, extentOfArc, Arc2D.OPEN);
    }

    private void drawLinearDividerLine() {

        //only draw divider line if it should be visible.
        if (!((drawEntirePlasmid) || (zoomRangeTwoStart == 0))) {
	    return;
	}

	double outerRadius = this.getLastOuterFeatureRadius() - featureSlotSpacing;
        double innerRadius = this.getLastInnerFeatureRadius() + featureSlotSpacing;
        double zeroLineRadians = getRadians(0);
	int zigzagNum = featureSlots.size() * 4;

	double zigzagRadiansShift = Math.toRadians(this.zigzagWidth / 8.0d);
	boolean shiftLeft = true;
	double zigzagRadius = innerRadius;
	double zigzagLength = ((outerRadius - innerRadius) / (double)zigzagNum) / 2.0d;
	double highlightArc = 0.5f * tickThickness - 0.5f * (shadingProportion * tickThickness);

	ArrayList points = new ArrayList();
	ArrayList highlightPoints = new ArrayList();
	ArrayList shadowPoints = new ArrayList();

	points.add(new Point2D.Double(Math.cos(zeroLineRadians) * innerRadius, Math.sin(zeroLineRadians) * innerRadius));
	highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + highlightArc / innerRadius) * innerRadius, Math.sin(zeroLineRadians + highlightArc / innerRadius) * innerRadius));
	shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - highlightArc / innerRadius) * innerRadius, Math.sin(zeroLineRadians - highlightArc / innerRadius) * innerRadius));

	for (int i = 0; i < zigzagNum; i++) {
	    if (shiftLeft) {
		shiftLeft = false;
		zigzagRadius = zigzagRadius + zigzagLength;
		points.add(new Point2D.Double(Math.cos(zeroLineRadians + zigzagRadiansShift) * (zigzagRadius), Math.sin(zeroLineRadians + zigzagRadiansShift) * (zigzagRadius)));
		highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + zigzagRadiansShift + highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians + zigzagRadiansShift + highlightArc / zigzagRadius) * (zigzagRadius)));
		shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + zigzagRadiansShift - highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians + zigzagRadiansShift - highlightArc / zigzagRadius) * (zigzagRadius)));
		zigzagRadius = zigzagRadius + zigzagLength;
		points.add(new Point2D.Double(Math.cos(zeroLineRadians) * (zigzagRadius), Math.sin(zeroLineRadians) * (zigzagRadius)));
		highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians + highlightArc / zigzagRadius) * (zigzagRadius)));
		shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians - highlightArc / zigzagRadius) * (zigzagRadius)));

	    }
	    else {
		shiftLeft = true;
		zigzagRadius = zigzagRadius + zigzagLength;
		points.add(new Point2D.Double(Math.cos(zeroLineRadians - zigzagRadiansShift) * (zigzagRadius), Math.sin(zeroLineRadians - zigzagRadiansShift) * (zigzagRadius)));
		highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - zigzagRadiansShift + highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians - zigzagRadiansShift + highlightArc / zigzagRadius) * (zigzagRadius)));
		shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - zigzagRadiansShift - highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians - zigzagRadiansShift - highlightArc / zigzagRadius) * (zigzagRadius)));
		zigzagRadius = zigzagRadius + zigzagLength;
		points.add(new Point2D.Double(Math.cos(zeroLineRadians) * (zigzagRadius), Math.sin(zeroLineRadians) * (zigzagRadius)));
		highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians + highlightArc / zigzagRadius) * (zigzagRadius)));
		shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - highlightArc / zigzagRadius) * (zigzagRadius), Math.sin(zeroLineRadians - highlightArc / zigzagRadius) * (zigzagRadius)));
	    }   
	}

	//add end points
	points.add(new Point2D.Double(Math.cos(zeroLineRadians) * outerRadius, Math.sin(zeroLineRadians) * outerRadius));
	highlightPoints.add(new Point2D.Double(Math.cos(zeroLineRadians + highlightArc / outerRadius) * outerRadius, Math.sin(zeroLineRadians + highlightArc / outerRadius) * outerRadius));
	shadowPoints.add(new Point2D.Double(Math.cos(zeroLineRadians - highlightArc / outerRadius) * outerRadius, Math.sin(zeroLineRadians - highlightArc / outerRadius) * outerRadius));


	//now draw points
	Point2D previousPoint = null;
	Point2D currentPoint = null;
	Iterator i = points.iterator();
	Area zigzagArea = new Area();
	BasicStroke zigzagStroke = new BasicStroke(tickThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
	if (i.hasNext()) {
	    previousPoint = (Point2D) i.next();	    
	}
        while (i.hasNext()) {
	    currentPoint = (Point2D) i.next();
	    zigzagArea.add(new Area(zigzagStroke.createStrokedShape(new Line2D.Double(previousPoint.getX(), previousPoint.getY(), currentPoint.getX(), currentPoint.getY()))));
	    previousPoint = currentPoint;
	}
	gg.setPaint(longTickColor);
	zigzagArea.intersect(new Area(this.getBackgroundRectangle()));
	gg.fill(zigzagArea);


	if (showShading) {
	    //now draw highlight points
	    previousPoint = null;
	    currentPoint = null;
	    i = highlightPoints.iterator();
	    zigzagArea = new Area();
	    zigzagStroke = new BasicStroke(tickThickness * shadingProportion, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
	    if (i.hasNext()) {
		previousPoint = (Point2D) i.next();	    
	    }
	    while (i.hasNext()) {
		currentPoint = (Point2D) i.next();
		zigzagArea.add(new Area(zigzagStroke.createStrokedShape(new Line2D.Double(previousPoint.getX(), previousPoint.getY(), currentPoint.getX(), currentPoint.getY()))));
		previousPoint = currentPoint;
	    }
	    gg.setPaint(Color.white);
    	    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightOpacity));
	    zigzagArea.intersect(new Area(this.getBackgroundRectangle()));
	    gg.fill(zigzagArea);  
    
    
	    //now draw shadow points
	    previousPoint = null;
	    currentPoint = null;
	    i = shadowPoints.iterator();
	    zigzagArea = new Area();
	    zigzagStroke = new BasicStroke(tickThickness * shadingProportion, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
	    if (i.hasNext()) {
		previousPoint = (Point2D) i.next();	    
	    }
	    while (i.hasNext()) {
		currentPoint = (Point2D) i.next();
		zigzagArea.add(new Area(zigzagStroke.createStrokedShape(new Line2D.Double(previousPoint.getX(), previousPoint.getY(), currentPoint.getX(), currentPoint.getY()))));
		previousPoint = currentPoint;
	    }
	    gg.setPaint(Color.black);
    	    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowOpacity));
	    zigzagArea.intersect(new Area(this.getBackgroundRectangle()));
	    gg.fill(zigzagArea);   

            //return to non transparent
            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));   	
	}
    }	


    /**
     * Draws the tick marks.
     */
    private void drawTickMarks(int strand, double startRadius) {

        NumberFormat format = NumberFormat.getInstance();
        
        double approxBasesPerTick;
        double chosenBasesPerTick = 100000.0d;
        double chosenIncrement = 10.0d;
        double baseLabel = 0.0d;
        String chosenUnits = "mbp";
        int tickSig[];

//         if ((strand == REVERSE_STRAND)) {
//             tickSig = new int[5];
//             tickSig[0] = 1;
//             tickSig[1] = 5;
//             tickSig[2] = 5;
//             tickSig[3] = 5;
//             tickSig[4] = 5;
//         } else {
//             tickSig = new int[4];
//             tickSig[0] = 1;
//             tickSig[1] = 4;
//             tickSig[2] = 2;
//             tickSig[3] = 4;
//         }

// 	tickSig = new int[8];
// 	tickSig[0] = 1;
// 	tickSig[1] = 6;
// 	tickSig[2] = 4;
// 	tickSig[3] = 6;
// 	tickSig[4] = 2;
// 	tickSig[5] = 6;
// 	tickSig[6] = 4;
// 	tickSig[7] = 6;

        tickSig = new int[5];
        tickSig[0] = 1;
        tickSig[1] = 5;
        tickSig[2] = 5;
        tickSig[3] = 5;
        tickSig[4] = 5;

        int tickSigIndex = 0;
        //goodTicks is the actual number of bases covered per major tick mark
        int goodTicks[] = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000, 5000000, 10000000};
        //goodIncrements is the amount added to the number label placed next to the tick mark.
        double goodIncrements[] = {1.0d, 2.0d, 5.0d, 10.0d, 20.0d, 50.0d, 100.0d, 200.0d, 500.0d, 1.0d, 2.0d, 5.0d, 10.0d, 20.0d, 50.0d, 100.0d, 200.0d, 500.0d, 1.0d, 2.0d, 5.0d, 10.0d};
        //goodUnits is the base pair units used for the corresponding goodIncrements.
        String goodUnits[] = {" bp", " bp", " bp", " bp", " bp", " bp", " bp", " bp", " bp", " kbp", " kbp", " kbp", " kbp", " kbp", " kbp", " kbp", " kbp", " kbp", " mbp", " mbp", " mbp", " mbp"};
        int strandDirection;

        //goodCentisomeTickNumbers is an array of suitable numbers of ticks for marking centisome position.
        double goodCentisomeTickNumbers[] = {1.0d, 2.0d, 5.0d, 10.0d, 20.0d, 50.0d, 100.0d, 200.0d, 500.0d, 1000.0d, 2000.0d, 5000.0d, 10000.0d, 20000.0d, 50000.0d, 100000.0d, 200000.0d, 500000.0d, 1000000.0d, 2000000.0d, 5000000.0d, 10000000.0d};
        //these are the increments that correspond to the goodCentisomeTickNumbers
        double goodCentisomeIncrements[] = {100.0d, 50.0d, 20.0d, 10.0d, 5.0d, 2.0d, 1.0d, 0.5d, 0.2d, 0.1d, 0.05d, 0.02d, 0.01d, 0.005d, 0.002d, 0.001d, 0.0005d, 0.0002d, 0.0001d, 0.00005d, 0.00002d, 0.00001d};
        //these are the number of decimal places to round the number label to
        int goodCentisomeRounds[] = {0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5};
        int chosenCentisomeRound = 0;

        if (strand == DIRECT_STRAND) {
            strandDirection = 1;
        } else {
            strandDirection = -1;
        }

        //determine the base intervals for ticks.
        //may need to be reduced if there are not enough ticks or increased if there are too many ticks
        approxBasesPerTick = (double) (sequenceLength) / ((double) (desiredNumberOfTicks) * ((virtualBackboneRadius - (backboneRadius - this.getLastInnerFeatureRadius())) / 200.0d) * tickDensity);

        if (rulerUnits == BASES) {
            for (int i = 0; i < goodTicks.length; i++) {
                if ((approxBasesPerTick < goodTicks[i]) || (i == (goodTicks.length - 1))) {
                    chosenBasesPerTick = goodTicks[i];
                    chosenIncrement = goodIncrements[i];
                    chosenUnits = goodUnits[i];
                    break;
                }
            }
        } else if (rulerUnits == CENTISOMES) {
            for (int i = 0; i < goodCentisomeTickNumbers.length; i++) {
                //if ((((double) (desiredNumberOfTicks) * (virtualBackboneRadius / 200.0d)) < goodCentisomeTickNumbers[i]) || (i == (goodCentisomeTickNumbers.length - 1))) {
		if ((((double) (desiredNumberOfTicks) *  ((virtualBackboneRadius - (backboneRadius - this.getLastInnerFeatureRadius())) / 200.0d) * tickDensity) < goodCentisomeTickNumbers[i]) || (i == (goodCentisomeTickNumbers.length - 1))) {
                    if (i > 0) {
                        chosenBasesPerTick = sequenceLength / goodCentisomeTickNumbers[i - 1];
                        chosenIncrement = goodCentisomeIncrements[i - 1];
                        chosenCentisomeRound = goodCentisomeRounds[i - 1];
                        chosenUnits = "centisome";

                        format.setMaximumFractionDigits(chosenCentisomeRound);
                        format.setMinimumFractionDigits(chosenCentisomeRound);
                        break;
                    } else {
                        chosenBasesPerTick = sequenceLength / goodCentisomeTickNumbers[i];
                        chosenIncrement = goodCentisomeIncrements[i];
                        chosenCentisomeRound = goodCentisomeRounds[i];
                        chosenUnits = "centisome";

                        format.setMaximumFractionDigits(chosenCentisomeRound);
                        format.setMinimumFractionDigits(chosenCentisomeRound);
                        break;
                    }
                }
            }
        }

        chosenBasesPerTick = chosenBasesPerTick / (double) (tickSig.length);

        double j = 0.0d;
        int endBase = sequenceLength;

        boolean finishedRanges = false;
        boolean finishedFirstRange = false;


        while (!(finishedRanges)) {  //outer while loop
            if (drawEntirePlasmid) {
                finishedRanges = true;
            } else if (zoomRangeTwoStart != 0) {
                j = zoomRangeOneStart;
                //this next line makes j the nearest even multiple of chosenBasesPerTick
                j = (chosenBasesPerTick - j % chosenBasesPerTick) + j;
                endBase = zoomRangeTwoStop;
                finishedRanges = true;
            } else {
                if (finishedFirstRange) {
                    j = zoomRangeTwoStart;
                    //this next line makes j the nearest even multiple of chosenBasesPerTick
                    j = (chosenBasesPerTick - j % chosenBasesPerTick) + j;
                    endBase = zoomRangeTwoStop;
                    finishedRanges = true;
                } else {
                    j = zoomRangeOneStart;
                    //this next line makes j the nearest even multiple of chosenBasesPerTick
                    j = (chosenBasesPerTick - j % chosenBasesPerTick) + j;
                    endBase = zoomRangeOneStop;
                    finishedFirstRange = true;
                }
            }


            //while (j < endBase) {  //inner while loop
            while (j - 0.5d < endBase) {

                //the second part is the radians per base.
                double radians = getRadians(j);

                int iterations = (int) Math.floor(j / chosenBasesPerTick + 0.5f);

                tickSigIndex = iterations % tickSig.length;

                baseLabel = (iterations / tickSig.length) * chosenIncrement;

                double startX;
                double startY;

                double endX;
                double endY;

                double heightAdjust = (double) (tickSig[tickSigIndex]);
                Color currentTickColor = longTickColor;
                float currentTickThickness = tickThickness;

                if (tickSig[tickSigIndex] != 1) {
                    currentTickColor = shortTickColor;
                    currentTickThickness = shortTickThickness;
                }

                startX = (Math.cos(radians) * startRadius);
                startY = (Math.sin(radians) * startRadius);
                endX = (Math.cos(radians) * (startRadius + strandDirection * tickLength / heightAdjust));
                endY = (Math.sin(radians) * (startRadius + strandDirection * tickLength / heightAdjust));

                //check if tick marks are inside of canvas. Important for some output formats.

                if ((!backgroundRectangle.contains(startX, startY)) || (!backgroundRectangle.contains(endX, endY))) {
                    j = j + chosenBasesPerTick;
                    if ((strand == REVERSE_STRAND) && (tickSig[tickSigIndex] == 1)) {
                        baseLabel = baseLabel + chosenIncrement;
                    }
                    continue;
                }


                gg.setPaint(currentTickColor);
                gg.setStroke(new BasicStroke(currentTickThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL));
                gg.draw(new Line2D.Double(startX, startY, endX, endY));


                //below code creates labelBounds for every tick mark
                //if ((strand == REVERSE_STRAND)) {

                    int tickBase;
                    double TICK_BOUNDS_PADDING = 1.5d;

                    double tickBoundsX = Math.cos(radians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + currentTickThickness + TICK_BOUNDS_PADDING);
                    double tickBoundsY = Math.sin(radians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + currentTickThickness + TICK_BOUNDS_PADDING);

                    if ((int) Math.floor(j + 0.5d) <= 0) {
                        tickBase = 1;
                    } else {
                        tickBase = (int) Math.floor(j + 0.5d);
                    }

                    Rectangle2D tickBounds = new Rectangle2D.Double(tickBoundsX + width / 2 - centerPoint.getX(), tickBoundsY + height / 2 - centerPoint.getY(), tickLength + currentTickThickness + TICK_BOUNDS_PADDING, tickLength + currentTickThickness + TICK_BOUNDS_PADDING);

                    LabelBounds rulerLabelBounds = new LabelBounds(this);
                    rulerLabelBounds.setBounds(tickBounds);
                    rulerLabelBounds.setLabel(Integer.toString(tickBase));
                    rulerLabelBounds.setType(BOUNDS_RULER);
                    rulerLabelBounds.setUse(true);
                    rulerLabelBounds.setBase(tickBase);

                    //draw bounds
                    //gg.setPaint(Color.blue);
                    //gg.setStroke( new BasicStroke( 0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL ));
                    //gg.draw(tickBounds);
		    //}

                if (showShading) {
                    //for drawing highlights on ticks.
                    double highlightArc = 0.5d * currentTickThickness - 0.5d * (shadingProportion * currentTickThickness);

                    //now draw highlight line
                    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightOpacity));
                    gg.setPaint(Color.white);
                    startX = (Math.cos(radians + highlightArc / startRadius) * startRadius);
                    startY = (Math.sin(radians + highlightArc / startRadius) * startRadius);
                    endX = (Math.cos(radians + highlightArc / (startRadius + strandDirection * tickLength / heightAdjust)) * (startRadius + strandDirection * tickLength / heightAdjust));
                    endY = (Math.sin(radians + highlightArc / (startRadius + strandDirection * tickLength / heightAdjust)) * (startRadius + strandDirection * tickLength / heightAdjust));
                    gg.setStroke(new BasicStroke(currentTickThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
                    gg.draw(new Line2D.Double(startX, startY, endX, endY));

                    //draw shadow line
                    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowOpacity));
                    gg.setPaint(Color.black);
                    startX = (Math.cos(radians - highlightArc / startRadius) * startRadius);
                    startY = (Math.sin(radians - highlightArc / startRadius) * startRadius);
                    endX = (Math.cos(radians - highlightArc / (startRadius + strandDirection * tickLength / heightAdjust)) * (startRadius + strandDirection * tickLength / heightAdjust));
                    endY = (Math.sin(radians - highlightArc / (startRadius + strandDirection * tickLength / heightAdjust)) * (startRadius + strandDirection * tickLength / heightAdjust));
                    gg.setStroke(new BasicStroke(currentTickThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
                    gg.draw(new Line2D.Double(startX, startY, endX, endY));

                    //set the composite back to 1
                    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
                }

                if ((strand == REVERSE_STRAND) && (tickSig[tickSigIndex] == 1)) {
                    //add numbering to the inside of the figure.
                    if (baseLabel > 0) {
                        //recalculate startX and startY using rulerTextPadding.
                        startX = (Math.cos(radians) * (startRadius - rulerTextPadding - tickLength - 0.5d * tickThickness));
                        startY = (Math.sin(radians) * (startRadius - rulerTextPadding - tickLength - 0.5d * tickThickness));

                        FontRenderContext frc = gg.getFontRenderContext();
                        TextLayout layout;

                        String rulerLabel;

                        if (rulerUnits == BASES) {
                            rulerLabel = Integer.toString((int) baseLabel) + chosenUnits;
                            //layout = new TextLayout(Integer.toString((int)baseLabel) + chosenUnits, rulerFont, frc);
                        } else {
                            rulerLabel = format.format(baseLabel);
                            //String baseLabelString = format.format(baseLabel);
                            //layout = new TextLayout(baseLabelString, rulerFont, frc);
                        }

                        layout = new TextLayout(rulerLabel, rulerFont, frc);
                        Rectangle2D bounds = layout.getBounds();
                        double textHeight = bounds.getHeight();
                        double textWidth = bounds.getWidth();

                        //double textHeight = layout.getAscent() + layout.getDescent();
                        //double textWidth = layout.getAdvance();

                        double textPositionX = startX;
                        double textPositionY = startY;

                        //adjust text position based on radians for label.
                        if ((Math.sin(radians) <= 1.0d) && (Math.sin(radians) >= 0.0d) && (Math.cos(radians) >= 0.0d) && (Math.cos(radians) <= 1.0d)) { // 0 to 90 degrees
                            textPositionX = textPositionX - textWidth + ((0.5d * textWidth) * (Math.sin(radians)));
                            textPositionY = textPositionY + 0.5d * textHeight - ((0.5d * textHeight) * (Math.sin(radians)));
                        } else if ((Math.sin(radians) <= 1.0d) && (Math.sin(radians) >= 0.0d) && (Math.cos(radians) <= 0.0d) && (Math.cos(radians) >= -1.0d)) { // 90 to 180 degrees
                            textPositionX = textPositionX - ((0.5d * textWidth) * (Math.sin(radians)));
                            textPositionY = textPositionY + 0.5d * textHeight - ((0.5d * textHeight) * (Math.sin(radians)));
                        } else if ((Math.sin(radians) <= 0.0d) && (Math.sin(radians) >= -1.0d) && (Math.cos(radians) <= 0.0d) && (Math.cos(radians) >= -1.0d)) { // 180 to 270 degrees
                            textPositionX = textPositionX + ((0.5d * textWidth) * (Math.sin(radians)));
                            textPositionY = textPositionY + 0.5d * textHeight - ((0.5d * textHeight) * (Math.sin(radians)));
                        } else {
                            textPositionX = textPositionX - textWidth - ((0.5d * textWidth) * (Math.sin(radians)));
                            textPositionY = textPositionY + 0.5d * textHeight - ((0.5d * textHeight) * (Math.sin(radians)));
                        }

                        //compensate is to correct for the way that java seems to shift the bound box down and to the right.
                        //double compensate = 0.5d;
                        double compensate = 0.0d;
                        gg.setPaint(backgroundColor);
                        bounds.setRect(bounds.getX() + textPositionX - 1.5d - compensate, bounds.getY() + textPositionY - layout.getDescent() - 1.5d - compensate, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);

                        //check if label fits inside canvas

                        if (backgroundRectangle.contains(bounds)) {
                            //gg.setPaint(Color.blue);
                            //gg.setStroke( new BasicStroke( 0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL ));
                            //gg.draw(bounds);

                            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f));
                            gg.fill(bounds);
                            gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));

                            gg.setPaint(rulerFontColor);
                            //layout.draw(gg, (float) textPositionX, (float) textPositionY - layout.getDescent());

			    //this is to avoid problem with imprecisely positioned text when using large coordinates
			    untranslateCanvas();
			    layout.draw(gg, (float) (textPositionX + width / 2 - centerPoint.getX()), (float) (textPositionY + height / 2 - centerPoint.getY()) - layout.getDescent());
			    translateCanvas();
			    /////

                        }


                    }
                    baseLabel = baseLabel + chosenIncrement;
                }

                j = j + chosenBasesPerTick;

            }// end of inner while loop


        }// end of outer while loop

        //draw a zero line if it should be visible.
        if ((drawEntirePlasmid) || (zoomRangeTwoStart == 0)) {

            //now draw a zero line
            double zeroLineRadians = getRadians(0);
            double zeroStartX = (Math.cos(zeroLineRadians) * startRadius);
            double zeroStartY = (Math.sin(zeroLineRadians) * startRadius);
            double zeroEndX = (Math.cos(zeroLineRadians) * (startRadius + strandDirection * tickLength));
            double zeroEndY = (Math.sin(zeroLineRadians) * (startRadius + strandDirection * tickLength));

	    //check whether zero line will fit in canvas
            if ((backgroundRectangle.contains(zeroStartX, zeroStartY)) || (backgroundRectangle.contains(zeroEndX, zeroEndY))) {

		gg.setPaint(zeroTickColor);
		gg.setStroke(new BasicStroke(tickThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL));
		gg.draw(new Line2D.Double(zeroStartX, zeroStartY, zeroEndX, zeroEndY));



		double TICK_BOUNDS_PADDING = 1.5d;

		//double tickBoundsX = Math.cos(zeroLineRadians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + tickThickness + TICK_BOUNDS_PADDING);
		//double tickBoundsY = Math.sin(zeroLineRadians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + tickThickness + TICK_BOUNDS_PADDING);
		double heightAdjust = 1.0d;
		double tickBoundsX = Math.cos(zeroLineRadians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + tickThickness + TICK_BOUNDS_PADDING);
		double tickBoundsY = Math.sin(zeroLineRadians) * (startRadius + strandDirection * tickLength * 0.5d) - 0.5d * (tickLength + tickThickness + TICK_BOUNDS_PADDING);

		Rectangle2D tickBounds = new Rectangle2D.Double(tickBoundsX + width / 2 - centerPoint.getX(), tickBoundsY + height / 2 - centerPoint.getY(), tickLength + tickThickness + TICK_BOUNDS_PADDING, tickLength + tickThickness + TICK_BOUNDS_PADDING);

		//Rectangle2D tickBounds = new Rectangle2D.Double(tickBoundsX, tickBoundsY, tickLength + tickThickness + TICK_BOUNDS_PADDING, tickLength + tickThickness + TICK_BOUNDS_PADDING);

		LabelBounds rulerLabelBounds = new LabelBounds(this);
		rulerLabelBounds.setBounds(tickBounds);
		rulerLabelBounds.setLabel(Integer.toString(1));
		rulerLabelBounds.setType(BOUNDS_RULER);
		rulerLabelBounds.setUse(true);
		rulerLabelBounds.setBase(1);

		gg.setPaint(Color.red);
		////gg.setStroke( new BasicStroke( 0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL ));
		//gg.fill(tickBounds);


		if (showShading) {
		    //for drawing highlights on ticks.
		    double highlightArc = 0.5f * tickThickness - 0.5f * (shadingProportion * tickThickness);

		    //now draw highlight line
		    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightOpacity));
		    gg.setPaint(Color.white);
		    zeroStartX = (Math.cos(zeroLineRadians + highlightArc / startRadius) * startRadius);
		    zeroStartY = (Math.sin(zeroLineRadians + highlightArc / startRadius) * startRadius);
		    zeroEndX = (Math.cos(zeroLineRadians + highlightArc / (startRadius + strandDirection * tickLength)) * (startRadius + strandDirection * tickLength));
		    zeroEndY = (Math.sin(zeroLineRadians + highlightArc / (startRadius + strandDirection * tickLength)) * (startRadius + strandDirection * tickLength));
		    gg.setStroke(new BasicStroke(tickThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
		    gg.draw(new Line2D.Double(zeroStartX, zeroStartY, zeroEndX, zeroEndY));

		    //draw shadow line
		    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowOpacity));
		    gg.setPaint(Color.black);
		    zeroStartX = (Math.cos(zeroLineRadians - highlightArc / startRadius) * startRadius);
		    zeroStartY = (Math.sin(zeroLineRadians - highlightArc / startRadius) * startRadius);
		    zeroEndX = (Math.cos(zeroLineRadians - highlightArc / (startRadius + strandDirection * tickLength)) * (startRadius + strandDirection * tickLength));
		    zeroEndY = (Math.sin(zeroLineRadians - highlightArc / (startRadius + strandDirection * tickLength)) * (startRadius + strandDirection * tickLength));
		    gg.setStroke(new BasicStroke(tickThickness * shadingProportion, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
		    gg.draw(new Line2D.Double(zeroStartX, zeroStartY, zeroEndX, zeroEndY));

		    //set the composite back to 1
		    gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
		}

	    }

        }

    }

    /**
     * Places the map title.
     */
    private void placeTitle() {

        FontRenderContext frc;
        TextLayout layout;
        Rectangle2D bounds;
        titleRectangle = new Rectangle2D.Double();
        lengthRectangle = new Rectangle2D.Double();
        double titleTextPositionX = 0.0d;
        double titleTextPositionY = 0.0d;

        double numberTextPositionX = 0.0d;
        double numberTextPositionY = 0.0d;

        //draw the title of the plasmid if there is one.
        if (title == null) {
            title = "";
        }
        Pattern p = Pattern.compile("\\S");
        Matcher m = p.matcher(title);

        if ((m.find())) {
            frc = gg.getFontRenderContext();
            layout = new TextLayout(title, titleFont, frc);
            bounds = layout.getBounds();
            titleTextPositionX = 0.0d - 0.5d * layout.getAdvance();
            titleTextPositionY = 0.0d;
            titleRectangle.setRect(bounds.getX() + titleTextPositionX - 1.5d, bounds.getY() + titleTextPositionY - layout.getDescent() - 1.5d, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);

            //draw the length of the plasmid.
            frc = gg.getFontRenderContext();
            layout = new TextLayout(Integer.toString(sequenceLength) + " bp", titleFont, frc);
            bounds = layout.getBounds();
            numberTextPositionX = numberTextPositionX - 0.5d * layout.getAdvance();
            numberTextPositionY = titleTextPositionY + layout.getDescent() + layout.getLeading() + layout.getDescent() + layout.getAscent();
            lengthRectangle.setRect(bounds.getX() + numberTextPositionX - 1.5d, bounds.getY() + numberTextPositionY - layout.getDescent() - 1.5d, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);
        }
        //if no title, create a small titleRectangle to prevent innerLabels from crossing center
        else {
            titleRectangle.setRect(-5.0d, -5.0d, 10.0d, 10.0d);
            //gg.setPaint(Color.red);
            //gg.draw(titleRectangle);
        }
    }

    /**
     * Draws the map title.
     */
    private void drawTitle() {

        FontRenderContext frc;
        TextLayout layout;
        Rectangle2D bounds;
        titleRectangle = new Rectangle2D.Double();
        lengthRectangle = new Rectangle2D.Double();
        double titleTextPositionX = 0.0d;
        double titleTextPositionY = 0.0d;

        double numberTextPositionX = 0.0d;
        double numberTextPositionY = 0.0d;

        //draw the title of the plasmid if there is one.
        if (title == null) {
            title = "";
        }
        Pattern p = Pattern.compile("\\S");
        Matcher m = p.matcher(title);

        if ((m.find())) {
            frc = gg.getFontRenderContext();
            layout = new TextLayout(title, titleFont, frc);
            bounds = layout.getBounds();
            titleTextPositionX = 0.0d - 0.5d * layout.getAdvance();
            titleTextPositionY = 0.0d;
            gg.setPaint(titleFontColor);
            titleRectangle.setRect(bounds.getX() + titleTextPositionX - 1.5d, bounds.getY() + titleTextPositionY - layout.getDescent() - 1.5d, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);
            if (backgroundRectangle.contains(titleRectangle)) {
                layout.draw(gg, (float) titleTextPositionX, (float) titleTextPositionY);
            } else {
                System.err.println("[warning] the plasmid title was too big for the canvas and was removed.");
            }

            //draw the length of the plasmid.
            frc = gg.getFontRenderContext();
            layout = new TextLayout(Integer.toString(sequenceLength) + " bp", titleFont, frc);
            bounds = layout.getBounds();
            numberTextPositionX = numberTextPositionX - 0.5d * layout.getAdvance();
            numberTextPositionY = titleTextPositionY + layout.getDescent() + layout.getLeading() + layout.getDescent() + layout.getAscent();
            lengthRectangle.setRect(bounds.getX() + numberTextPositionX - 1.5d, bounds.getY() + numberTextPositionY - layout.getDescent() - 1.5d, bounds.getWidth() + 3.0d, bounds.getHeight() + 3.0d);
            gg.setPaint(titleFontColor);
            if (backgroundRectangle.contains(lengthRectangle)) {
                layout.draw(gg, (float) numberTextPositionX, (float) numberTextPositionY);
            } else {
                System.out.println("[warning] the plasmid length was too big for the canvas and was removed.");
            }
        }
        //if no title, create a small titleRectangle to prevent innerLabels from crossing center
        else {
            titleRectangle.setRect(-5.0d, -5.0d, 10.0d, 10.0d);
            //gg.setPaint(Color.red);
            //gg.draw(titleRectangle);
        }
    }

    /**
     * Draws a warning message at the bottom of the map.
     */
    private void drawWarningMessage() {

        if ((showWarning) && (totalLabels > 0)) {

            String textToShow = "";
            if (showWarning) {
                textToShow = textToShow + warningText;
            }

            if ((zoomMultiplier == 1.0d) && (clashLabels != 0)) {
                if (clashLabels == 1) {
                    textToShow = textToShow + "Warning: " + clashLabels + " of the " + totalLabels + " labels is not shown. ";
                } else {
                    textToShow = textToShow + "Warning: " + clashLabels + " of the " + totalLabels + " labels are not shown. ";
                }
            } else if ((zoomMultiplier == 1.0d) && (clashLabels == 0)) {
                textToShow = textToShow + totalLabels + " of the " + totalLabels + " labels are shown. ";
            }

            Pattern p = Pattern.compile("\\S");
            Matcher m = p.matcher(textToShow);

            if ((m.find())) {

                infoLegend = new Legend(this);
                infoLegend.setFont(warningFont);
                infoLegend.setBackgroundColor(backgroundColor);
                infoLegend.setBackgroundOpacity(0.2f);
                infoLegend.setAllowLabelClash(false);
                infoLegend.setPosition(LEGEND_LOWER_LEFT);
                LegendItem infoContent = new LegendItem(infoLegend);
                infoContent.setDrawSwatch(SWATCH_NO_SHOW);
                infoContent.setLabel(textToShow);
                infoContent.setFontColor(warningFontColor);

            }
        }
    }

    /**
     * Draws the feature labels. Call this method for InnerLabel objects, and then OuterLabel objects.
     *
     * @param labels a collection of labels. The collection must consist of OuterLabel objects, or InnerLabel objects,
     *               but not both.
     */
    private void drawLabels(ArrayList labels) {

        Iterator i;
        Comparator comparator;

        double outerLabelStart;
        double innerLabelStart;

        if (drawTickMarks) {
            outerLabelStart = this.getLastOuterFeatureRadius() + tickLength + tickThickness + featureSlotSpacing;

            innerLabelStart = this.getLastInnerFeatureRadius() - tickLength - tickThickness - featureSlotSpacing;
        } else {
            outerLabelStart = this.getLastOuterFeatureRadius() + featureSlotSpacing;

            innerLabelStart = this.getLastInnerFeatureRadius() - featureSlotSpacing;
        }

        int adjustedClashSpan = clashSpan;

	//the below code will keep labels in a square with width = min(canvas length, canvas width)
	//this will be undone automatically at the end of this method
	//because of a call to untranslateCanvas()
	//if (drawEntirePlasmid) {
	//    backgroundRectangle = new Rectangle2D.Double(backgroundRectangle.getX(), backgroundRectangle.getY(), smallestDimension, smallestDimension);
	//}

        //System.out.println ("Set radius");
        System.out.print(".");
        i = labels.iterator();
        while (i.hasNext()) {
            Label currentLabel = (Label) i.next();
            if (currentLabel instanceof OuterLabel) {
                currentLabel.setLineStartRadius(outerLabelStart);
            } else {
                currentLabel.setLineStartRadius(innerLabelStart);
            }
        }

        //System.out.println ("Shuffle");
        System.out.print(".");
        if ((labelShuffle) && (labels.size() > 50)) {
            Collections.shuffle(labels);
        }

        //System.out.println ("Trim");
        //Remove labels until labelsToKeep labels remain. If a label does not clash with any labels, do not remove it.
        System.out.print(".");
        if (labels.size() > labelsToKeep) {
            for (int j = 0; j < labels.size() - labelsToKeep; j++) {
                if (((Label) labels.get(j)).getForceLabel()) {
                    continue;
                }
                for (int k = j; k < labels.size(); k++) {
                    if ((((Label) labels.get(j)).getBounds()).intersects(((Label) labels.get(k)).getBounds())) {
                        labels.remove(j);
                        j--;
                        clashLabels++;
                        break;
                    }
                }
            }
        }

        //System.out.println ("Sort by radians");
        System.out.print(".");
        comparator = new SortLabelsByRadians();
        Collections.sort(labels, comparator);

	//set bounds with padding set to 3
        i = labels.iterator();
        while (i.hasNext()) {
            ((Label) i.next()).updateBounds(3.0d);	    
        }	

        //System.out.println ("Radians shifting");
        System.out.print(".");
        if (labels.size() > 1) {
            boolean noClash;
            int upper;
            int lower;
            for (int outer = 0; outer < spreadIterations; outer++) {
                noClash = true;
                for (int inner = 0; inner < labels.size(); inner++) {
                    if (inner == 0) {
                        if (((Label) labels.get(1)).getLineStartRadians() - ((Label) labels.get(0)).getLineStartRadians() > Math.PI) {
                            continue;
                        } else {
                            upper = 1;
                            lower = 0;
                        }
                    }

                    if (inner == (labels.size() - 1)) {
                        if (((Label) labels.get(inner)).getLineStartRadians() - ((Label) labels.get(0)).getLineStartRadians() < Math.PI) {
                            continue;
                        } else {
                            upper = inner;
                            lower = 0;
                        }
                    } else {
                        upper = inner;
                        lower = inner + 1;
                    }

                    if ((((Label) labels.get(upper)).getBounds()).intersects(((Label) labels.get(lower)).getBounds())) {
                        noClash = false;
                        ((Label) labels.get(upper)).shiftRadiansLower();
                        ((Label) labels.get(lower)).shiftRadiansHigher();
                    }
                }//inner loop
                if (noClash) {
                    break;
                }
            }//outer loop
        }

	//set bounds with padding set to 2
        i = labels.iterator();
        while (i.hasNext()) {
            ((Label) i.next()).updateBounds(2.0d);	    
        }

        //System.out.println ("Radius shifting and removing");
        System.out.print(".");
        if (labels.size() > 1) {
            int j;
            int checked;

            for (int outer = 0; outer < labels.size(); outer++) {
                if (((Label) labels.get(outer)).getForceLabel()) {
                    continue;
                }

                if ((adjustedClashSpan) > (labels.size() / 2)) {
                    adjustedClashSpan = labels.size() / 2;
                }

                j = outer - adjustedClashSpan;
                checked = 0;
                while (checked < (adjustedClashSpan * 2)) {

                    if (j == labels.size()) {
                        j = 0;
                    } else if (j < 0) {
                        j = labels.size() + j;
                    }

                    if (j == outer) {
                        j++;
                        continue;
                    }

                    if ((((Label) labels.get(outer)).getBounds()).intersects(((Label) labels.get(j)).getBounds())) {

                        if (((Label) labels.get(outer)).extendRadius()) {

                            j = outer - adjustedClashSpan;
                            checked = 0;

                            continue;
                        } else {
                            if ((!moveInnerLabelsToOuter) || (((Label) labels.get(outer)) instanceof OuterLabel)) {
                                labels.remove(outer);
                                clashLabels++;
                                outer = outer - 1;
                                break;
                            } else {
                                //convert innerLabel to an outerLabel
                                new OuterLabel((InnerLabel) labels.get(outer));
                                labels.remove(outer);
                                outer = outer - 1;
                                break;
                            }
                        }
                    }
                    checked++;
                    j++;
                }
            }
        }

	//set bounds with padding set to 1.5
        i = labels.iterator();
        while (i.hasNext()) {
            ((Label) i.next()).updateBounds(1.5d);	    
        }

        //System.out.println ("Remove labels that clash with other labels");
        System.out.print(".");
        Collections.shuffle(labels);
        if (labels.size() > 1) {
            for (int outer = 0; outer < labels.size(); outer++) {

                if (((Label) labels.get(outer)).getForceLabel()) {
                    continue;
                }
                for (int inner = outer + 1; inner < labels.size(); inner++) {
                    if ((((Label) labels.get(outer)).getBounds()).intersects(((Label) labels.get(inner)).getBounds())) {

                        if ((!moveInnerLabelsToOuter) || (((Label) labels.get(outer)) instanceof OuterLabel)) {
                            //((Label)labels.get(outer)).draw();
                            labels.remove(outer);
                            clashLabels++;
                            outer = outer - 1;
                            break;
                        } else {
                            //convert innerLabel to an outerLabel
                            new OuterLabel((InnerLabel) labels.get(outer));
                            labels.remove(outer);
                            outer = outer - 1;
                            break;
                        }
                    }
                }
            }
        }

        //System.out.println ("Sort by radians");
        //sort labels by radius
        //System.out.print(".");
        //comparator = new SortLabelsByRadians();
        //Collections.sort(labels, comparator);

        //System.out.println ("Sort by radius");
        //sort labels by radius
        //System.out.print(".");
        //comparator = new SortLabelsByRadius();
        //Collections.sort(labels, comparator);


        //System.out.println ("Remove labels that still clash with map elements");
        //go through the labels again and remove those that clash with the map title, map length, inner FeatureSlot, or outer FeatureSlot.
        System.out.print(".");
        if (labels.size() > 0) {
            Label currentLabel;
            for (int j = 0; j < labels.size(); j++) {
                currentLabel = (Label) (labels.get(j));
                if (currentLabel instanceof OuterLabel) {
                    if ((outerArc.intersects(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getLineStart()))) {
                        if (!currentLabel.getForceLabel()) {

                            if ((!moveInnerLabelsToOuter) || (currentLabel instanceof OuterLabel)) {
                                labels.remove(j);
                                clashLabels++;
                                j = j - 1;
                            } else {
                                //convert innerLabel to an outerLabel
                                new OuterLabel((InnerLabel) currentLabel);
                                labels.remove(j);
                                j = j - 1;
                            }
                        }
                    }
                } else {
                    if (drawEntirePlasmid) {

                        if ((titleRectangle.intersects(currentLabel.getBounds())) || (lengthRectangle.intersects(currentLabel.getBounds())) || !(innerArc.contains(currentLabel.getBounds())) || (titleRectangle.contains(currentLabel.getBounds())) || (lengthRectangle.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getLineStart()))) {
                            if (!currentLabel.getForceLabel()) {
                                if ((!moveInnerLabelsToOuter) || (currentLabel instanceof OuterLabel)) {
                                    labels.remove(j);
                                    clashLabels++;
                                    j = j - 1;
                                } else {
                                    //convert innerLabel to an outerLabel
                                    new OuterLabel((InnerLabel) currentLabel);
                                    labels.remove(j);
                                    j = j - 1;
                                }
                            }
                        }
                    } else {

 //this may need fixing if labels on the inside of the backbone circle are being drawn when the shouldn't
                        if (!(backgroundRectangle.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getLineStart()))) {
                            if (!currentLabel.getForceLabel()) {
                                if ((!moveInnerLabelsToOuter) || (currentLabel instanceof OuterLabel)) {
                                    labels.remove(j);
                                    clashLabels++;
                                    j = j - 1;
                                } else {
                                    //convert innerLabel to an outerLabel
                                    new OuterLabel((InnerLabel) currentLabel);
                                    labels.remove(j);
                                    j = j - 1;
                                }
                            }
                        }


			//this wass a bug. with the code below lots of inner labels were not being drawn. new code above
//                         if (!(innerArc.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getBounds())) || !(backgroundRectangle.contains(currentLabel.getLineStart()))) {
//                             if (!currentLabel.getForceLabel()) {
//                                 if ((!moveInnerLabelsToOuter) || (currentLabel instanceof OuterLabel)) {
//                                     labels.remove(j);
//                                     clashLabels++;
//                                     j = j - 1;
//                                 } else {
//                                     //convert innerLabel to an outerLabel
//                                     new OuterLabel((InnerLabel) currentLabel);
//                                     labels.remove(j);
//                                     j = j - 1;
//                                 }
//                             }
//                         }

                    }
                }
            }
        }

        //System.out.println ("Remove labels that clash with legends");
        //go through the labels again and remove those that clash with legends.
        System.out.print(".");
        if (labels.size() > 0) {
            i = legends.iterator();
            Rectangle2D legendBounds;
            Label currentLabel;
            Legend currentLegend;
            while (i.hasNext()) {
                currentLegend = (Legend) i.next();
                if (!(currentLegend.getAllowLabelClash())) {
                    legendBounds = currentLegend.getBounds();

                    for (int j = 0; j < labels.size(); j++) {
                        currentLabel = (Label) (labels.get(j));
                        if ((!currentLabel.getForceLabel()) && ((legendBounds.contains(currentLabel.getBounds())) || (legendBounds.intersects(currentLabel.getBounds())))) {
                            if ((!moveInnerLabelsToOuter) || (currentLabel instanceof OuterLabel)) {
                                labels.remove(j);
                                clashLabels++;
                                j = j - 1;
                            } else {
                                //convert innerLabel to an outerLabel
                                new OuterLabel((InnerLabel) currentLabel);
                                labels.remove(j);
                                j = j - 1;
                            }
                        }
                    }
                }
            }
        }

        //System.out.println ("Sort by forceLabel");
        //sort labels by forceLabel so that those that have forceLabel = true are drawn last.
        System.out.print(".");
        comparator = new SortLabelsByForceLabel();
        Collections.sort(labels, comparator);

        //System.out.println ("Draw label lines");
        //draw label lines
        System.out.print(".");
        i = labels.iterator();
        while (i.hasNext()) {
            Label currentLabel = (Label) i.next();
            currentLabel.drawLabelLine();
        }

        //System.out.println ("Draw label text");
        //draw label text
	untranslateCanvas();
        System.out.print(".");
        i = labels.iterator();
        while (i.hasNext()) {
            Label currentLabel = (Label) i.next();
            currentLabel.drawLabelText();

            //now create a labelBounds object
            if ((currentLabel.getHyperlink() != null) || (currentLabel.getMouseover() != null)) {
                LabelBounds labelBounds = new LabelBounds(this);
                labelBounds.setBounds(currentLabel.getBounds());
                labelBounds.setLabel(currentLabel.getLabelText());
                labelBounds.setType(BOUNDS_FEATURE);
                labelBounds.setHyperlink(currentLabel.getHyperlink());
                labelBounds.setMouseover(currentLabel.getMouseover());
                labelBounds.setUse(true);
            }

            //remove once drawn
            //i.remove();
        }
	translateCanvas();
    }

    public static void main(String ars[]) {

    }

}
