All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hfg.bio.seq.PlasmidMap Maven / Gradle / Ivy

There is a newer version: 20240423
Show newest version
package com.hfg.bio.seq;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.hfg.bio.Strand;
import com.hfg.bio.molbio.RestrictionEnzyme;
import com.hfg.bio.molbio.RestrictionSite;
import com.hfg.bio.seq.format.feature.BasicFeatureKey;
import com.hfg.bio.seq.format.feature.FeatureKey;
import com.hfg.bio.seq.format.feature.FeatureQualifier;
import com.hfg.bio.seq.format.feature.SeqFeature;
import com.hfg.bio.seq.format.feature.genbank.GenBankFeatureKey;
import com.hfg.bio.seq.format.feature.genbank.GenBankFeatureQualifierName;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.html.HTML;
import com.hfg.html.attribute.HTMLColor;
import com.hfg.math.Quadrant;
import com.hfg.math.Range;
import com.hfg.svg.*;
import com.hfg.svg.attribute.SvgTextAnchor;
import com.hfg.svg.attribute.SvgTextPathMethod;
import com.hfg.svg.attribute.SvgTextPathSpacing;
import com.hfg.svg.attribute.SvgVerticalAlign;
import com.hfg.svg.path.SvgPathClosePathCmd;
import com.hfg.svg.path.SvgPathEllipticalArcCmd;
import com.hfg.svg.path.SvgPathLineToCmd;
import com.hfg.svg.path.SvgPathMoveToCmd;
import com.hfg.util.CompareUtil;
import com.hfg.util.Recursion;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;


//------------------------------------------------------------------------------
/**
 * Plasmid map generator.
 *
 * @author J. Alex Taylor, hairyfatguy.com
 */
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------

public class PlasmidMap
{
   private  NucleicAcid mSeq;
   private  PlasmidMapSettings mSettings;
   private Point2D mCenterPoint = new Point2D.Double(0, 0);
   private double mOrigin = 90;


   // Cached values
   private double mBackboneRadiusPx;
   private double mTickLengthPx;
   private int    mMajorTickStep;
   private int    mMinorTickStep;
   private List mMajorTickPositions;
   private List mMinorTickPositions;
   private Integer mPathIdSrc = 0;

   private Map mFeatureColorMap;
   private Color mDefaultFeatureColor = HTMLColor.LIGHT_GRAY;

   private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true);

   // Labeling CSS classes
   private static final String STEM = "stem";
   private static final String STEMMED_LABEL = "stemmedLabel";


   //**************************************************************************
   // CONSTRUCTORS
   //**************************************************************************

   //---------------------------------------------------------------------------
   public PlasmidMap(NucleicAcid inSeq, PlasmidMapSettings inSettings)
   {
      mSeq = inSeq;
      mSettings = inSettings;
   }

   //**************************************************************************
   // PUBLIC METHODS
   //**************************************************************************

   //---------------------------------------------------------------------------

   //---------------------------------------------------------------------------
   public SVG toSVG()
   {
      initSVG();

      SVG svg = new SVG()
            .setWidth((int) mSettings.getWidth().to(GfxUnits.pixels))
            .setHeight((int) mSettings.getHeight().to(GfxUnits.pixels));

      mCenterPoint = new Point2D.Double(mSettings.getWidth().to(GfxUnits.pixels) / 2, mSettings.getHeight().to(GfxUnits.pixels) / 2);

      double backboneRadiusPx = mSettings.getBackboneRadius().to(GfxUnits.pixels);
      double tickLengthPx = mSettings.getTickLength().to(GfxUnits.pixels);

      // Backbone
      svg.addCircle()
            .setR(backboneRadiusPx)
            .setCenter(mCenterPoint)
            .setFill(null)
            .setStroke(HTMLColor.GREY)
            .setStrokeWidth(3);

      // Add the plasmid title in the center
      svg.addSubtag(generateTitle());

      // Add the plasmid size in the center
      svg.addText(StringUtil.generateLocalizedNumberString(mSeq.length()) + " bp")
            .setTextAnchor(SvgTextAnchor.middle)
            .setVerticalAlign(SvgVerticalAlign.first)
            .addStyle(mSettings.getSubTitleStyle())
            .setX((int)mCenterPoint.getX())
            .setY((int)mCenterPoint.getY() + 20);

      // Determine the major and minor tick positions
      
      // Add ticks
      svg.addSubtag(generateTicks());

      // Add features
      svg.addSubtag(generateFeatures());

      // Add restriction sites
      svg.addSubtag(generateRestrictionSites());

      // Finally, the labels might be overlapping.
      // Try to adjust them if that is the case.
      resolveOverlappingLabels(svg);

      return svg;
   }

   //**************************************************************************
   // PRIVATE METHODS
   //**************************************************************************

   //---------------------------------------------------------------------------
   private void initSVG()
   {

      mBackboneRadiusPx = mSettings.getBackboneRadius().to(GfxUnits.pixels);
      mTickLengthPx = mSettings.getTickLength().to(GfxUnits.pixels);

      mPathIdSrc = 0;

      mFeatureColorMap = mSettings.getFeatureColorMap();
   }

   //---------------------------------------------------------------------------
   private Point2D getCenter()
   {
      return mCenterPoint;
   }

   //---------------------------------------------------------------------------
   private double getOrigin()
   {
      return mOrigin;
   }

   //---------------------------------------------------------------------------
   private double bpToRadians(int inBp)
   {
      double radians;

      radians = (inBp * ((Math.PI * 2.0) / mSeq.length())) - ((Math.PI / 180.0) * getOrigin());


      return radians;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateTitle()
   {
      SvgGroup g = new SvgGroup();

      String title = mSeq.getDescription();
      if (StringUtil.isSet(title))
      {
         if (title.endsWith("."))
         {
            title = title.substring(0, title.length() - 1);
         }

         if (title.endsWith(", complete sequence"))
         {
            title = title.substring(0, title.length() - 19);
         }
      }

      if (! StringUtil.isSet(title)
          || title.length() > 50)
      {
         title = mSeq.getID();
      }

      g.addText(title)
            .setTextAnchor(SvgTextAnchor.middle)
            .setVerticalAlign(SvgVerticalAlign.first)
            .addStyle(mSettings.getTitleStyle())
            .setX((int)mCenterPoint.getX())
            .setY((int)mCenterPoint.getY());

      return g;
   }
   
   //---------------------------------------------------------------------------
   private SvgNode generateTicks()
   {
      // Determine the major and minor tick positions
      determineTickPositions();

      SvgGroup g = new SvgGroup().setId("Ticks");

      // Minor ticks
      for (Integer bp : mMinorTickPositions)
      {
         g.addSubtag(generateTick(bp, mTickLengthPx / 2));
      }

      // Major ticks
      for (Integer bp : mMajorTickPositions)
      {
         g.addSubtag(generateTick(bp, mTickLengthPx));
      }

      // Major tick labels
      Font tickLabelFont = mSettings.getTickLabelFont();
      int tickLength = (int) mSettings.getTickLength().to(GfxUnits.pixels);
      for (Integer bp : mMajorTickPositions)
      {
         String label = bp + "";

         TextLayout layout = new TextLayout(label, tickLabelFont, sFRC);
         Rectangle2D bounds = layout.getBounds();
         double labelHeight = bounds.getHeight();
         double labelWidth = bounds.getWidth();

         double radians = bpToRadians(bp);
         double x = mCenterPoint.getX() + (Math.cos(radians) * (mBackboneRadiusPx + -1 * tickLength - 5));
         double y = mCenterPoint.getY() + (Math.sin(radians) * (mBackboneRadiusPx + -1 * tickLength - 5));

         // Adjust the position
         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
            x = x - 6 - labelWidth + ((0.5 * labelWidth) * Math.sin(radians));
            y = y + 0.5 * labelHeight - ((0.5 * labelHeight) * 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
            x = x - ((0.5 * labelWidth) * Math.sin(radians));
            y = y + 0.5 * labelHeight - ((0.5 * labelHeight) * 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
            x = x + 4 + ((0.5 * labelWidth) * Math.sin(radians));
            y = y + 4 + 0.5 * labelHeight - ((0.5 * labelHeight) * Math.sin(radians));
         }
         else if ((Math.sin(radians) == -1.0))
         {
            // 270 degrees
            x = x - labelWidth - ((0.5 * labelWidth) * Math.sin(radians));
            y = y + 4 + 0.5 * labelHeight - ((0.5 * labelHeight) * Math.sin(radians));
         }
         else
         {
            // 270 to 360 degrees
            x = x - 4 - labelWidth - ((0.5 * labelWidth) * Math.sin(radians));
            y = y + 4 + 0.5 * labelHeight - ((0.5 * labelHeight) * Math.sin(radians));
         }

         g.addText(label)
               .setFont(tickLabelFont)
               .setX((int)x)
               .setY((int)y);
      }
      
      return g;
   }

   //---------------------------------------------------------------------------
   private void determineTickPositions()
   {
      String widthString = mSeq.length() + "";
      mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
      if (0 == mMajorTickStep%mSeq.length()
          || mMajorTickStep > 0.5 * mSeq.length())
      {
         mMajorTickStep = mMajorTickStep / 10;
      }

      mMinorTickStep = mMajorTickStep / 10;


      mMajorTickPositions = new ArrayList<>(10);
      mMinorTickPositions = new ArrayList<>(100);

      for (int bp = 0; bp < mSeq.length(); bp+= mMinorTickStep)
      {
         if (bpToRadians(bp) > 4.65)
         {
            break;
         }

         if (0 == bp
             || 0 == bp%mMajorTickStep)
         {
            mMajorTickPositions.add(bp);
         }
         else
         {
            mMinorTickPositions.add(bp);
         }
      }
   }

   //---------------------------------------------------------------------------
   private SvgNode generateTick(int inBp, double inTickLength)
   {
      double radians = bpToRadians(inBp);
      double startX = mCenterPoint.getX() + (Math.cos(radians) * mBackboneRadiusPx);
      double startY = mCenterPoint.getY() + (Math.sin(radians) * mBackboneRadiusPx);
      double endX = mCenterPoint.getX() + (Math.cos(radians) * (mBackboneRadiusPx + -1 * inTickLength));
      double endY = mCenterPoint.getY() + (Math.sin(radians) * (mBackboneRadiusPx + -1 * inTickLength));

      return new SvgLine(new Point2D.Double(startX, startY), new Point2D.Double(endX, endY)).addStyle(mSettings.getTickStyle());
   }

   //---------------------------------------------------------------------------
   private SvgNode generateFeatures()
   {
      SvgGroup g = new SvgGroup().setId("Features");

      List featureTypes = mSettings.getFeatureTypes();

      if (CollectionUtil.hasValues(featureTypes))
      {
         List allFeatures =  new ArrayList<>(25);
         for (String featureType : featureTypes)
         {
            FeatureKey key = new BasicFeatureKey(featureType);
            List features = mSeq.getFeatures(key);
            if (CollectionUtil.hasValues(features))
            {
               allFeatures.addAll(features);
            }
         }

         if (CollectionUtil.hasValues(allFeatures))
         {
            Collections.sort(allFeatures);
            for (SeqFeature feature : allFeatures)
            {
               g.addSubtag(generateFeature(feature));
            }
         }
      }

      return g;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateFeature(SeqFeature inFeature)
   {
      SvgNode node;

      if (inFeature.name().name().equalsIgnoreCase(GenBankFeatureKey.CDS.name()))
      {
         if (Strand.FORWARD.equals(inFeature.getLocation().getStrand()))
         {
            node = generateFwdFrameFeature(inFeature);
         }
         else
         {
            node = generateRevFrameFeature(inFeature);
         }
      }
      else
      {
         node = generateDirectionlessFeature(inFeature);
      }

      return node;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateFwdFrameFeature(SeqFeature inFeature)
   {
      Range location = inFeature.getLocation().toIntRange();

      double featureRadiusPx = mBackboneRadiusPx + 3;
      int featureHeight = 20;

      SvgGroup g = new SvgGroup();

      double arrowheadSize = 0.05; // in radians

      double startRadians = bpToRadians(location.getStart());
      double endRadians = bpToRadians(location.getEnd());

      if (endRadians - startRadians < arrowheadSize)
      {
         arrowheadSize = (endRadians - startRadians);
      }

      double start1X = mCenterPoint.getX() + (Math.cos(startRadians) * (featureRadiusPx));
      double start1Y = mCenterPoint.getY() + (Math.sin(startRadians) * (featureRadiusPx));
      double start2X = mCenterPoint.getX() + (Math.cos(startRadians) * (featureRadiusPx + featureHeight));
      double start2Y = mCenterPoint.getY() + (Math.sin(startRadians) * (featureRadiusPx + featureHeight));

      double headStart1X = mCenterPoint.getX() + (Math.cos(endRadians - arrowheadSize) * (featureRadiusPx + featureHeight));
      double headStart1Y = mCenterPoint.getY() + (Math.sin(endRadians - arrowheadSize) * (featureRadiusPx + featureHeight));

      double tipX   = mCenterPoint.getX() + (Math.cos(endRadians) * (featureRadiusPx + (featureHeight/2)));
      double tipY   = mCenterPoint.getY() + (Math.sin(endRadians) * (featureRadiusPx + (featureHeight/2)));

      double headStart2X = mCenterPoint.getX() + (Math.cos(endRadians - arrowheadSize) * (featureRadiusPx));
      double headStart2Y = mCenterPoint.getY() + (Math.sin(endRadians - arrowheadSize) * (featureRadiusPx));

      List arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // rx
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(1f); // sweep-flag
      arcNumbers.add((float)headStart1X);
      arcNumbers.add((float)headStart1Y);
      SvgPathEllipticalArcCmd outerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);

      arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx)); // rx
      arcNumbers.add((float) (featureRadiusPx)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(0f); // sweep-flag
      arcNumbers.add((float)start1X);
      arcNumbers.add((float)start1Y);
      SvgPathEllipticalArcCmd innerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);


      Color color = mFeatureColorMap.get(inFeature.name());
      if (null == color)
      {
         color = mDefaultFeatureColor;
      }

      g.addPath()
            .addStyle(mSettings.getFeatureStyle())
            .setFill(color)
            .addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Double(start1X, start1Y)))
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(start2X, start2Y)))
            .addPathCommand(outerArc)
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(tipX, tipY)))
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(headStart2X, headStart2Y)))
            .addPathCommand(innerArc)
            .addPathCommand(new SvgPathClosePathCmd());

      // Label
      g.addSubtag(generateFeatureLabel(inFeature, startRadians, endRadians - arrowheadSize));

      // Tooltip
      g.addSubtag(generateTooltip(inFeature));

      return g;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateRevFrameFeature(SeqFeature inFeature)
   {
      Range location = inFeature.getLocation().toIntRange();

      double featureRadiusPx = mBackboneRadiusPx + 3;
      int featureHeight = 20;

      SvgGroup g = new SvgGroup();

      double arrowheadSize = 0.05; // in radians

      double startRadians = bpToRadians(location.getStart());
      double endRadians = bpToRadians(location.getEnd());

      if (endRadians - startRadians < arrowheadSize)
      {
         arrowheadSize = (endRadians - startRadians);
      }

      double tipX   = mCenterPoint.getX() + (Math.cos(startRadians) * (featureRadiusPx + (featureHeight/2f)));
      double tipY   = mCenterPoint.getY() + (Math.sin(startRadians) * (featureRadiusPx + (featureHeight/2f)));

      double headStartTopX = mCenterPoint.getX() + (Math.cos(startRadians + arrowheadSize) * (featureRadiusPx + featureHeight));
      double headStartTopY = mCenterPoint.getY() + (Math.sin(startRadians + arrowheadSize) * (featureRadiusPx + featureHeight));

      double endTopX   = mCenterPoint.getX() + (Math.cos(endRadians) * (featureRadiusPx + featureHeight));
      double endTopY  = mCenterPoint.getY() + (Math.sin(endRadians) * (featureRadiusPx + featureHeight));

      double endBottomX = mCenterPoint.getX() + (Math.cos(endRadians) * (featureRadiusPx));
      double endBottomY = mCenterPoint.getY() + (Math.sin(endRadians) * (featureRadiusPx));

      double headStartBottomX = mCenterPoint.getX() + (Math.cos(startRadians + arrowheadSize) * featureRadiusPx);
      double headStartBottomY = mCenterPoint.getY() + (Math.sin(startRadians + arrowheadSize) * featureRadiusPx);

      List arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // rx
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(1f); // sweep-flag
      arcNumbers.add((float)endTopX);
      arcNumbers.add((float)endTopY);
      SvgPathEllipticalArcCmd outerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);

      arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx)); // rx
      arcNumbers.add((float) (featureRadiusPx)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(0f); // sweep-flag
      arcNumbers.add((float)headStartBottomX);
      arcNumbers.add((float)headStartBottomY);
      SvgPathEllipticalArcCmd innerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);


      Color color = mFeatureColorMap.get(inFeature.name());
      if (null == color)
      {
         color = mDefaultFeatureColor;
      }

      g.addPath()
            .addStyle(mSettings.getFeatureStyle())
            .setFill(color)
            .addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Double(tipX, tipY)))
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(headStartTopX, headStartTopY)))
            .addPathCommand(outerArc)
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(endBottomX, endBottomY)))
            .addPathCommand(innerArc)
            .addPathCommand(new SvgPathClosePathCmd());

      // Label
      g.addSubtag(generateFeatureLabel(inFeature, startRadians + arrowheadSize, endRadians));

      // Tooltip
      g.addSubtag(generateTooltip(inFeature));


      return g;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateDirectionlessFeature(SeqFeature inFeature)
   {
      Range location = inFeature.getLocation().toIntRange();

      double featureRadiusPx = mBackboneRadiusPx + 3;
      int featureHeight = 20;

      SvgGroup g = new SvgGroup();

      double startRadians = bpToRadians(location.getStart());
      double endRadians = bpToRadians(location.getEnd());

      double startTopX = mCenterPoint.getX() + (Math.cos(startRadians) * (featureRadiusPx + featureHeight));
      double startTopY = mCenterPoint.getY() + (Math.sin(startRadians) * (featureRadiusPx + featureHeight));

      double endTopX   = mCenterPoint.getX() + (Math.cos(endRadians) * (featureRadiusPx + featureHeight));
      double endTopY  = mCenterPoint.getY() + (Math.sin(endRadians) * (featureRadiusPx + featureHeight));

      double endBottomX = mCenterPoint.getX() + (Math.cos(endRadians) * (featureRadiusPx));
      double endBottomY = mCenterPoint.getY() + (Math.sin(endRadians) * (featureRadiusPx));

      double startBottomX = mCenterPoint.getX() + (Math.cos(startRadians) * featureRadiusPx);
      double startBottomY = mCenterPoint.getY() + (Math.sin(startRadians) * featureRadiusPx);

      List arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // rx
      arcNumbers.add((float) (featureRadiusPx + featureHeight)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(1f); // sweep-flag
      arcNumbers.add((float)endTopX);
      arcNumbers.add((float)endTopY);
      SvgPathEllipticalArcCmd outerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);

      arcNumbers = new ArrayList<>(7);
      arcNumbers.add((float) (featureRadiusPx)); // rx
      arcNumbers.add((float) (featureRadiusPx)); // ry
      arcNumbers.add(0f); // x-rotation
      arcNumbers.add((endRadians - startRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
      arcNumbers.add(0f); // sweep-flag
      arcNumbers.add((float)startBottomX);
      arcNumbers.add((float)startBottomY);
      SvgPathEllipticalArcCmd innerArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);


      Color color = mFeatureColorMap.get(inFeature.name());
      if (null == color)
      {
         color = mDefaultFeatureColor;
      }

      g.addPath()
            .addStyle(mSettings.getFeatureStyle())
            .setFill(color)
            .addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Double(startBottomX, startBottomY)))
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(startTopX, startTopY)))
            .addPathCommand(outerArc)
            .addPathCommand(new SvgPathLineToCmd().addPoint(new Point2D.Double(endBottomX, endBottomY)))
            .addPathCommand(innerArc)
            .addPathCommand(new SvgPathClosePathCmd());

      // Label
      g.addSubtag(generateFeatureLabel(inFeature, startRadians, endRadians));

      // Tooltip
      g.addSubtag(generateTooltip(inFeature));

      return g;
   }

   //---------------------------------------------------------------------------
   private SvgTitle generateTooltip(SeqFeature inFeature)
   {
      StringBuilderPlus tooltip = new StringBuilderPlus().setDelimiter("\n");
      List notes = inFeature.getQualifiers(GenBankFeatureQualifierName.note.name());
      if (CollectionUtil.hasValues(notes))
      {
         tooltip.delimitedAppend(notes.get(0).getValue());
      }

      List functions = inFeature.getQualifiers(GenBankFeatureQualifierName.function.name());
      if (CollectionUtil.hasValues(functions))
      {
         tooltip.delimitedAppend("(" + functions.get(0).getValue() + ")");
      }

      tooltip.delimitedAppend(inFeature.getLocation().toString());

      return new SvgTitle(tooltip);
   }

   //---------------------------------------------------------------------------
   private SvgNode generateFeatureLabel(SeqFeature inFeature, double inLeftRadians, double inRightRadians)
   {
      double featureRadiusPx = mBackboneRadiusPx + 3;
      int featureHeightPx = mSettings.getFeatureHeight().toInt(GfxUnits.pixels);

      SvgGroup g = new SvgGroup();

      String label = null;
      List productQualifiers = inFeature.getQualifiers(GenBankFeatureQualifierName.product.name());
      if (CollectionUtil.hasValues(productQualifiers))
      {
         label = productQualifiers.get(0).getValue();
      }
      else
      {
         List noteQualifiers = inFeature.getQualifiers(GenBankFeatureQualifierName.note.name());
         if (CollectionUtil.hasValues(noteQualifiers))
         {
            label = noteQualifiers.get(0).getValue();
            label = StringUtil.replaceAll(label, "origin of replication", "origin");
         }
      }


      if (StringUtil.isSet(label))
      {
         TextLayout layout = new TextLayout(label, mSettings.getFeatureLabelFont(), sFRC);

         Rectangle2D bounds = layout.getBounds();
         double labelHeight = bounds.getHeight();
         double labelWidth = bounds.getWidth();

         String pathId = "featureLabelPath" + (mPathIdSrc++);

         double labelStartX = mCenterPoint.getX() + (Math.cos(inLeftRadians) * (featureRadiusPx + (featureHeightPx/4)));
         double labelStartY = mCenterPoint.getY() + (Math.sin(inLeftRadians) * (featureRadiusPx + (featureHeightPx/4)));

         double labelEndX = mCenterPoint.getX() + (Math.cos(inRightRadians) * (featureRadiusPx + (featureHeightPx/4)));
         double labelEndY = mCenterPoint.getY() + (Math.sin(inRightRadians) * (featureRadiusPx + (featureHeightPx/4)));

         double arcLength = featureRadiusPx * (inRightRadians - inLeftRadians);
         if (labelWidth > arcLength - 25)
         {
            // Attach the label via a stem

            // Stem
            double centerRadians = inLeftRadians + (inRightRadians - inLeftRadians) / 2;
            double x1 = mCenterPoint.getX() + (Math.cos(centerRadians) * (featureRadiusPx + featureHeightPx));
            double y1 = mCenterPoint.getY() + (Math.sin(centerRadians) * (featureRadiusPx + featureHeightPx));
            double x2 = mCenterPoint.getX() + (Math.cos(centerRadians) * (featureRadiusPx + featureHeightPx + 20));
            double y2 = mCenterPoint.getY() + (Math.sin(centerRadians) * (featureRadiusPx + featureHeightPx + 20));

            g.addLine(new Point2D.Double(x1, y1), new Point2D.Double(x2, y2)).addClass(STEM).addStyle(mSettings.getFeatureStyle());

            // Add label
            double x = x2;
            double y = y2;


            String[] labelLines = StringUtil.lines(StringUtil.wrap(label, 15));
            if (labelLines.length > 1)
            {
               String longestLine = null;
               for (String line : labelLines)
               {
                  if (null == longestLine
                        || line.length() > longestLine.length())
                  {
                     longestLine = line;
                  }
               }

               layout = new TextLayout(longestLine, mSettings.getFeatureLabelFont(), sFRC);

               bounds = layout.getBounds();
               labelHeight = bounds.getHeight() * labelLines.length;
               labelWidth = bounds.getWidth();
            }

            Range featureRange = inFeature.getLocation().toIntRange();
            int midpoint = featureRange.getStart() - 1 + (featureRange.getEnd() - featureRange.getStart() + 1) / 2;
            PlasmidLabel plasmidLabel = new PlasmidLabel(midpoint);
            g.addSubtag(plasmidLabel);


            /*

            // Adjust the position
            if (centerRadians >= 0
                && centerRadians < 0.785398)
            {
               // 0 (3 o'clock) to 45 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x += labelWidth * .75;
                  y += labelHeight / 2;
               }
               else
               {
                  y += labelHeight + 5;
               }
            }
            else if (centerRadians >= 0.785398
                     && centerRadians < 1.5708)
            {
               // 45 to 90 degrees (6 o'clock)
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x += labelWidth * .75;
                  y += labelHeight;
               }
               else
               {
                  y += labelHeight + 5;
               }
            }
            else if (centerRadians >= 1.5708
                     && centerRadians < 2.35619)
            {
               // 90 degrees (6 o'clock) to 135 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x -= labelWidth * .75;
                  y += labelHeight / 2f;
               }
               else
               {
                  x -=  15 + labelWidth;
                  y += labelHeight + 5;
               }
            }
            else if (centerRadians >= 2.35619
                     && centerRadians < Math.PI)
            {
               // 135 degrees to 180 degrees (9 o'clock)
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x -= labelWidth * .75;
                  y += labelHeight / 2f;
               }
               else
               {
                  x -=  15 + labelWidth;
                  y += labelHeight + 5;
               }
            }
            else if (centerRadians >= Math.PI
                     && centerRadians < 4.71239)
            {
               // 180 degrees (9 o'clock) to 270 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x -= labelWidth / 2f + 10;
                  y -= labelHeight - 5;
               }
               else
               {
                  x -= labelWidth / 2f + 10;
                  y -= 5;
               }
            }
            else
            {
               // 270 to 360 degrees (Noon to 3 o'clock)
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x += labelWidth * .75;
                  y -= labelHeight - 5;
               }
            }
            */
            // Adjust the position
            if (plasmidLabel.getQuadrant().equals(Quadrant.I))
            {
               // 0 to 90 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x += labelWidth * .75;
                  y -= labelHeight - 5;
               }
            }
            else if (plasmidLabel.getQuadrant().equals(Quadrant.IV))
            {
               // 90 to 180 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x += labelWidth * .75;
                  y += labelHeight;
               }
               else
               {
                  y += labelHeight + 5;
               }
            }
            else if (plasmidLabel.getQuadrant().equals(Quadrant.III))
            {
               // 180 to 270 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x -= labelWidth * .75;
                  y += labelHeight / 2f;
               }
               else
               {
                  x -=  15 + labelWidth;
                  y += labelHeight + 5;
               }
            }
            else if (plasmidLabel.getQuadrant().equals(Quadrant.II))
            {
               // 270 to 360 degrees
               if (labelLines.length > 1)
               {
                  // Multi-line labels are center-aligned
                  // so they need to be treated differently
                  x -= labelWidth / 2f + 10;
                  y -= labelHeight - 5;
               }
               else
               {
                  x -= labelWidth / 2f + 10;
                  y -= 5;
               }
            }

            if (1 == labelLines.length) // Single line?
            {
               plasmidLabel.setContent(label)
                     .setTextAnchor(SvgTextAnchor.start)
                     .setVerticalAlign(SvgVerticalAlign.first)
                     .setX((int)x)
                     .setY((int)y);
            }
            else
            {
               plasmidLabel.setTextAnchor(SvgTextAnchor.middle);
               for (int i = 0; i < labelLines.length; i++)
               {
                  String line = labelLines[i];
                  SvgTSpan tspan = plasmidLabel.addTSpan(line)
                                           .setX((int) x);
                  if (0 == i)
                  {
                     tspan.setY((int)(y - labelHeight/2));
                  }
                  else
                  {
                     tspan.setDy(12);
                  }
               }
            }

            plasmidLabel.setFont(mSettings.getFeatureLabelFont());
         }
         else
         {
            // Display the label within the path
            List arcNumbers = new ArrayList<>(7);
            arcNumbers.add((float) (featureRadiusPx + (featureHeightPx / 4))); // rx
            arcNumbers.add((float) (featureRadiusPx + (featureHeightPx / 4))); // ry
            arcNumbers.add(0f); // x-rotation
            arcNumbers.add((inRightRadians - inLeftRadians) > Math.PI ? 1f : 0f); // large-arc-sweep-flag
            arcNumbers.add(1f); // sweep-flag
            arcNumbers.add((float) labelEndX);
            arcNumbers.add((float) labelEndY);
            SvgPathEllipticalArcCmd labelArc = new SvgPathEllipticalArcCmd().setRawNumbers(arcNumbers);

            g.addPath()
                  .setId(pathId)
                  .setFill(null)
                  .setStroke(null)
                  .addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Double(labelStartX, labelStartY)))
                  .addPathCommand(labelArc);

            SvgText labelNode = g.addText()
                                 .setFont(mSettings.getFeatureLabelFont())
                                 .setTextAnchor(SvgTextAnchor.middle)
                                 .setVerticalAlign(SvgVerticalAlign.first);
            labelNode.addSubtag(new SvgTextPath(label).setHref("#" + pathId)
                                                      .setStartOffsetPct(50)
                                                      .setSpacing(SvgTextPathSpacing.auto)
                                                      .setMethod(SvgTextPathMethod.align));
         }
      }

      return g;
   }

   //---------------------------------------------------------------------------
   private SvgNode generateRestrictionSites()
   {
      SvgGroup g = new SvgGroup().setId("RestrictionSites");

      List restrictionEnzymes = mSettings.getRestrictionEnzymes();
      if (CollectionUtil.hasValues(restrictionEnzymes))
      {
         List restrictionSites = new ArrayList<>();

         // First, scan the plasmid with the specified restriction enzymes.
         for (RestrictionEnzyme re : restrictionEnzymes)
         {
            List matches = re.matcher(mSeq).findAll();
            if (CollectionUtil.hasValues(matches))
            {
               restrictionSites.addAll(matches);
            }
         }

         if (CollectionUtil.hasValues(restrictionSites))
         {
            restrictionSites.sort(RestrictionEnzyme.LOCATION_COMPARATOR);

            Font restrictionEnzymeFont = mSettings.getRestrictionEnzymeLabelFont();
            int labelStemLengthPx = mSettings.getRestrictionEnzymeStemLength().toInt(GfxUnits.pixels);

            for (RestrictionSite site : restrictionSites)
            {
               SvgGroup labelGroup = g.addGroup();

               int[] fwdCutSiteIndices = site.getFwdStrandCutSiteIndices();

               // Draw the label stem
               double radians = bpToRadians(site.getSeqLocation().getStart());
               double x1 = mCenterPoint.getX() + (Math.cos(radians) * (mBackboneRadiusPx));
               double y1 = mCenterPoint.getY() + (Math.sin(radians) * (mBackboneRadiusPx));
               double x2 = mCenterPoint.getX() + (Math.cos(radians) * (mBackboneRadiusPx + (labelStemLengthPx)));
               double y2 = mCenterPoint.getY() + (Math.sin(radians) * (mBackboneRadiusPx + (labelStemLengthPx)));

               labelGroup.addLine(new Point2D.Double(x1, y1), new Point2D.Double(x2, y2)).addClass(STEM).addStyle(mSettings.getRestrictionEnzymeStemStyle());

               // Add label
               StringBuilderPlus buffer = new StringBuilderPlus().setDelimiter(", ");
               buffer.append(site.getRestrictionEnzyme().name());
               buffer.append(" (");
               buffer.append(StringUtil.generateLocalizedNumberString(fwdCutSiteIndices[0]));
               if (fwdCutSiteIndices.length > 1)
               {
                  buffer.delimitedAppend(StringUtil.generateLocalizedNumberString(fwdCutSiteIndices[1]));
               }
               buffer.append(")");
               String label =  buffer.toString();

               PlasmidLabel plasmidLabel = new PlasmidLabel(site.getSeqLocation().getStart(), label);

               // Determine label placement
               TextLayout layout = new TextLayout(label, restrictionEnzymeFont, sFRC);

               Rectangle2D bounds = layout.getBounds();
               double labelHeight = bounds.getHeight();
               double labelWidth = bounds.getWidth();

               double x = x2;
               double y = y2;

               // Adjust the position
               if (plasmidLabel.getQuadrant().equals(Quadrant.I))
               {
                  // 0 to 90 degrees
                  x += 5;
                  y += labelHeight / 2f;
               }
               else if (plasmidLabel.getQuadrant().equals(Quadrant.IV))
               {
                  // 90 to 180 degrees
                  x += 5;
                  y += labelHeight / 2f;
               }
               else if (plasmidLabel.getQuadrant().equals(Quadrant.III))
               {
                  // 180 to 270 degrees
                  x -= (10 + labelWidth);
                  y += labelHeight;
               }
               else if (plasmidLabel.getQuadrant().equals(Quadrant.II))
               {
                  // 270 to 360 degrees
                  x -= (10 + labelWidth);
   //               y += 5;
               }

               plasmidLabel
                     .setFont(restrictionEnzymeFont)
                     .setX((int)x)
                     .setY((int)y);


               // Tooltip
               plasmidLabel.addSubtag(new SvgTitle(label));


               labelGroup.addSubtag(plasmidLabel);
            }
         }

      }

      return g;
   }

   //---------------------------------------------------------------------------
   private void resolveOverlappingLabels(SVG inSVG)
   {
      // First, gather the stemmed labels.
      List labelBoxes = inSVG.getSubtagsByAttribute(HTML.CLASS, STEMMED_LABEL, Recursion.ON);
      if (CollectionUtil.hasValues(labelBoxes))
      {
         Collections.sort(labelBoxes);

         int iterations = 0;
         boolean overlapsFound = true;
         while (overlapsFound
                && iterations < 2)
         {
            iterations++;
            overlapsFound = false;

            for (int i = 0; i < labelBoxes.size() - 1; i++)
            {
               PlasmidLabel labelBox1 = labelBoxes.get(i);

               for (int j = i + 1; j < labelBoxes.size(); j++)
               {
                  PlasmidLabel labelBox2 = labelBoxes.get(j);

                  // Do they overlap?
                  Rectangle2D boundsBox1 = labelBox1.getBoundsBox();
                  Rectangle2D boundsBox2 = labelBox2.getBoundsBox();

                  if (boundsBox1.intersects(boundsBox2))
                  {
                     overlapsFound = true;

                     Quadrant quadrant = labelBox1.getQuadrant();

                     // Note that the 'y' of the bounding box is the top of the box
                     // while the 'y' of the SVG text element is the bottom of the box.

                     PlasmidLabel labelToStay;
                     PlasmidLabel labelToMove = null;
                     Rectangle2D labelToStayBoundsBox;
                     Rectangle2D labelToMoveBoundsBox;

                     if (quadrant.equals(Quadrant.I))
                     {
                        // Try to move the higher label up and to the left.
                        labelToMove = labelBox1.getBpPosition() < labelBox2.getBpPosition() ? labelBox1 : labelBox2;
                        labelToStay = (labelToMove == labelBox1 ? labelBox2 : labelBox1);

                        labelToMoveBoundsBox = (labelToMove == labelBox1 ? boundsBox1 : boundsBox2);
                        labelToStayBoundsBox = (labelToStay == labelBox1 ? boundsBox1 : boundsBox2);

                        Rectangle2D newBoundsBox = new Rectangle((int) labelToMoveBoundsBox.getX() - 5,
                                                                 (int) (labelToStayBoundsBox.getY() - labelToMoveBoundsBox.getHeight() - 2),
                                                                 (int) labelToMoveBoundsBox.getWidth(),
                                                                 (int) labelToMoveBoundsBox.getHeight());
                        if (hasCollision(labelBoxes, labelToMove, newBoundsBox))
                        {
                           // Try extending the stem
                           boolean newSiteFound = false;
                           SvgLine stem = (SvgLine) ((SvgGroup) labelToMove.getParentNode()).getSubtagsByAttribute(HTML.CLASS, STEM).get(0);
                           float initialStemLength = stem.length();

                           for (int extraStemLength = 5; extraStemLength <= 70; extraStemLength+=5)
                           {
                              double x = mCenterPoint.getX() + (Math.cos(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));
                              double y = mCenterPoint.getY() + (Math.sin(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));

                              Rectangle2D newBoundsBox2 = new Rectangle((int) x,
                                                                        (int) (y - labelToMoveBoundsBox.getHeight() / 2f),
                                                                        (int) labelToMoveBoundsBox.getWidth(),
                                                                        (int) labelToMoveBoundsBox.getHeight());
                              if (! hasCollision(labelBoxes, labelToMove, newBoundsBox2, 2))
                              {
                                 newSiteFound = true;
                                 newBoundsBox = newBoundsBox2;
                                 break;
                              }
                           }

                           if (! newSiteFound)
                           {
                              // Try moving the label below the other value
                              Rectangle2D newBoundsBox2 = new Rectangle((int) labelToMoveBoundsBox.getX() + 5,
                                                                        (int) (labelToStayBoundsBox.getY() + labelToStayBoundsBox.getHeight() + 2),
                                                                        (int) labelToMoveBoundsBox.getWidth(),
                                                                        (int) labelToMoveBoundsBox.getHeight());
                              if (! hasCollision(labelBoxes, labelToMove, newBoundsBox2))
                              {
                                 newBoundsBox = newBoundsBox2;
                              }
                           }
                        }

                        moveLabel(labelToMove, newBoundsBox);
                     }
                     else if (quadrant.equals(Quadrant.II))
                     {
                        labelToMove = labelBox1.getBpPosition() < labelBox2.getBpPosition() ? labelBox2 : labelBox1;
                        labelToMoveBoundsBox = (labelToMove == labelBox1 ? boundsBox1 : boundsBox2);

                        SvgLine stem = (SvgLine) ((SvgGroup) labelToMove.getParentNode()).getSubtagsByAttribute(HTML.CLASS, STEM).get(0);
                        float initialStemLength = stem.length();

                        for (int extraStemLength = 5; extraStemLength <= 50; extraStemLength+=5)
                        {
                           double x = mCenterPoint.getX() + (Math.cos(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));
                           double y = mCenterPoint.getY() + (Math.sin(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));

                           Rectangle2D newBoundsBox = new Rectangle((int) (x - labelToMoveBoundsBox.getWidth()),
                                                                    (int) (y - labelToMoveBoundsBox.getHeight() / 2f),
                                                                    (int) labelToMoveBoundsBox.getWidth(),
                                                                    (int) labelToMoveBoundsBox.getHeight());
                           if (! hasCollision(labelBoxes, labelToMove, newBoundsBox, 2))
                           {
                              moveLabel(labelToMove, newBoundsBox);
                              break;
                           }
                        }
                     }
                     else if (quadrant.equals(Quadrant.III))
                     {
                        labelToMove = labelBox1.getBpPosition() < labelBox2.getBpPosition() ? labelBox1 : labelBox2;
                        labelToMoveBoundsBox = (labelToMove == labelBox1 ? boundsBox1 : boundsBox2);

                        SvgLine stem = (SvgLine) ((SvgGroup) labelToMove.getParentNode()).getSubtagsByAttribute(HTML.CLASS, STEM).get(0);
                        float initialStemLength = stem.length();

                        for (int extraStemLength = 5; extraStemLength <= 50; extraStemLength+=5)
                        {
                           double x = mCenterPoint.getX() + (Math.cos(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));
                           double y = mCenterPoint.getY() + (Math.sin(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));

                           Rectangle2D newBoundsBox = new Rectangle((int) (x - labelToMoveBoundsBox.getWidth()),
                                                                    (int) (y + labelToMoveBoundsBox.getHeight() / 2f),
                                                                    (int) labelToMoveBoundsBox.getWidth(),
                                                                    (int) labelToMoveBoundsBox.getHeight());
                           if (! hasCollision(labelBoxes, labelToMove, newBoundsBox, 2))
                           {
                              moveLabel(labelToMove, newBoundsBox);
                              break;
                           }
                        }
                     }
                     else if (quadrant.equals(Quadrant.IV))
                     {
                        labelToMove = labelBox1.getBpPosition() < labelBox2.getBpPosition() ? labelBox2 : labelBox1;

                        labelToMoveBoundsBox = (labelToMove == labelBox1 ? boundsBox1 : boundsBox2);

                        SvgLine stem = (SvgLine) ((SvgGroup) labelToMove.getParentNode()).getSubtagsByAttribute(HTML.CLASS, STEM).get(0);
                        float initialStemLength = stem.length();

                        for (int extraStemLength = 5; extraStemLength <= 50; extraStemLength+=5)
                        {
                           double x = mCenterPoint.getX() + (Math.cos(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));
                           double y = mCenterPoint.getY() + (Math.sin(labelToMove.getRadians()) * (mBackboneRadiusPx + (initialStemLength + extraStemLength)));

                           Rectangle2D newBoundsBox = new Rectangle((int) x,
                                                                    (int) (y + labelToMoveBoundsBox.getHeight() / 2f),
                                                                    (int) labelToMoveBoundsBox.getWidth(),
                                                                    (int) labelToMoveBoundsBox.getHeight());
                           if (! hasCollision(labelBoxes, labelToMove, newBoundsBox, 2))
                           {
                              moveLabel(labelToMove, newBoundsBox);
                              break;
                           }
                        }

                     }

                     // Adjust the stem placement
                     SvgLine stem = (SvgLine) ((SvgGroup) labelToMove.getParentNode()).getSubtagsByAttribute(HTML.CLASS, STEM).get(0);
                     labelToMoveBoundsBox = labelToMove.getBoundsBox();
                     if (quadrant.equals(Quadrant.I))
                     {
                        stem.setX2((int) labelToMoveBoundsBox.getX() - 2)
                              .setY2((int) labelToMoveBoundsBox.getY() + (int) (labelToMoveBoundsBox.getHeight() * 0.75));
                     }
                     else if (quadrant.equals(Quadrant.II))
                     {
                        stem.setX2((int) (labelToMoveBoundsBox.getX() + labelToMoveBoundsBox.getWidth()))
                              .setY2((int) (labelToMoveBoundsBox.getY() + labelToMoveBoundsBox.getHeight()));
                     }
                     else if (quadrant.equals(Quadrant.III))
                     {
                        stem.setX2((int) (labelToMoveBoundsBox.getX() + labelToMoveBoundsBox.getWidth()))
                              .setY2((int) (labelToMoveBoundsBox.getY()));
                     }
                     else if (quadrant.equals(Quadrant.IV))
                     {
                        stem.setX2((int) labelToMoveBoundsBox.getX())
                              .setY2((int) (labelToMoveBoundsBox.getY()));
                     }
                  }
               }
            }
         }
      }
   }

   //---------------------------------------------------------------------------
   private void moveLabel(PlasmidLabel inLabel, Rectangle2D inNewBoundsBox)
   {
      SvgTextAnchor textAnchor = SvgTextAnchor.start;
      if (inLabel.hasAttribute(SvgAttr.textAnchor))
      {
         textAnchor = SvgTextAnchor.valueOf(inLabel.getAttributeValue(SvgAttr.textAnchor));
      }

      float x = (float) inNewBoundsBox.getX();
      if (textAnchor.equals(SvgTextAnchor.middle))
      {
         x += inNewBoundsBox.getWidth() / 2f;
      }

      // TSpans?
      List textSpans = inLabel.getSubtagsByName(SVG.tspan);
      if (CollectionUtil.hasValues(textSpans))
      {
         for (SvgTSpan tSpan : textSpans)
         {
            if (tSpan.hasAttribute(SvgAttr.x))
            {
               tSpan.setX(x);
            }

            if (tSpan.hasAttribute(SvgAttr.y))
            {
               tSpan.setY((int) (inNewBoundsBox.getY() + inNewBoundsBox.getHeight() / textSpans.size()));
            }
         }
      }
      else
      {
         inLabel.setX((int) x);
         inLabel.setY((int) (inNewBoundsBox.getY() + inNewBoundsBox.getHeight()));
      }
   }

   //---------------------------------------------------------------------------
   private boolean hasCollision(List inLabels, PlasmidLabel inLabel, Rectangle2D inNewBoundsBox)
   {
      return hasCollision(inLabels, inLabel, inNewBoundsBox, 0);
   }

   //---------------------------------------------------------------------------
   private boolean hasCollision(List inLabels, PlasmidLabel inLabel, Rectangle2D inNewBoundsBox, int inMargin)
   {
      boolean hasCollision = false;

      Rectangle2D boundsBox = inNewBoundsBox;
      if (inMargin > 0)
      {
         boundsBox = new Rectangle((int) inNewBoundsBox.getX() - inMargin,
                                   (int) inNewBoundsBox.getY() - inMargin,
                                   (int) inNewBoundsBox.getWidth() + (2 * inMargin),
                                   (int) inNewBoundsBox.getHeight() + (2 * inMargin));
      }

      for (PlasmidLabel label : inLabels)
      {
         if (label != inLabel
             && label.getBoundsBox().intersects(boundsBox))
         {
            hasCollision = true;
            break;
         }
      }

      return hasCollision;
   }

             
   //**************************************************************************
   // INNER CLASS
   //**************************************************************************

   private class PlasmidLabel extends SvgText implements Comparable
   {
      private int mBpPosition;
      private Double mRadians;
      private Quadrant mQuadrant;

      //------------------------------------------------------------------------
      public PlasmidLabel(int inBpPosition, String inContent)
      {
         super(inContent);
         mBpPosition = inBpPosition;
         addClass(STEMMED_LABEL);
      }

      //------------------------------------------------------------------------
      public PlasmidLabel(int inBpPosition)
      {
         super();
         mBpPosition = inBpPosition;
         addClass(STEMMED_LABEL);
      }



      //---------------------------------------------------------------------------
      @Override
      public boolean equals(Object inObj2)
      {
         return (0 == compareTo(inObj2));
      }

      //---------------------------------------------------------------------------
      @Override
      public int hashCode()
      {
         return mBpPosition;
      }

      //---------------------------------------------------------------------------
      @Override
      public int compareTo(Object inObj2)
      {
         int result = -1;

         if (inObj2 instanceof PlasmidLabel)
         {
            result = CompareUtil.compare(mBpPosition, ((PlasmidLabel) inObj2).mBpPosition);
         }

         return result;
      }

      //------------------------------------------------------------------------
      public int getBpPosition()
      {
         return mBpPosition;
      }

      //------------------------------------------------------------------------
      public Quadrant getQuadrant()
      {
         if (null == mQuadrant)
         {
            double radians = getRadians();
            if (radians < 0
                && radians >= - 1.57)
            {
               // 0 to 90 degrees
               mQuadrant = Quadrant.I;
            }
            else if (radians >= 0
                     && radians < 1.57)
            {
               // 90 to 180 degrees
               mQuadrant = Quadrant.IV;
            }
            else if (radians >= 1.57
                     && radians < 3.14)
            {
               // 180 to 270 degrees
               mQuadrant = Quadrant.III;
            }
            else
            {
               // 270 to 360 degrees
               mQuadrant = Quadrant.II;
            }
         }

         return mQuadrant;
      }

      //------------------------------------------------------------------------
      public double getRadians()
      {
         if (null == mRadians)
         {
            mRadians = (mBpPosition * ((Math.PI * 2.0) / mSeq.length())) - ((Math.PI / 180.0) * getOrigin());
         }

         return mRadians;
      }
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy