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

com.hfg.bio.seq.graphics.ContigPlot Maven / Gradle / Ivy

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

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;

import com.hfg.bio.seq.Exon;
import com.hfg.bio.Strand;
import com.hfg.bio.seq.Gene;
import com.hfg.bio.seq.SeqLocation;
import com.hfg.css.CSS;
import com.hfg.css.CSSProperty;
import com.hfg.graphics.TextUtil;
import com.hfg.graphics.units.GfxSize;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.graphics.units.Pixels;
import com.hfg.html.Script;
import com.hfg.javascript.TooltipJS;
import com.hfg.math.Range;
import com.hfg.svg.SVG;
import com.hfg.svg.SvgGroup;
import com.hfg.svg.SvgRect;
import com.hfg.svg.SvgText;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;


//------------------------------------------------------------------------------
/**
 * Visualization of genes on a contig.
 *
 * @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 ContigPlot
{
   private String  mContigName;
   private Integer mContigLength;

   private SeqLocation mDisplayRange;
   private boolean     mAutoAdjustScale;
   private boolean     mDisplayLabels;
   private Font        mLabelFont = Font.decode("Arial-PLAIN-6");
   private Font        mScaleFont = Font.decode("Arial-PLAIN-8");
   private Font        mTitleFont = Font.decode("Arial-BOLD-10");

   private List mGenes;

   private GfxSize mLineLength = sDefaultLineLength;
   private GfxSize mGeneHeight = sDefaultGeneHeight;

   private int mLineY = 150;

   // Calculated
   private float  mXScalingFactor;
   private int    mMajorTickStep;
   private int    mMinorTickStep;
   private int    mMaxFwdLabelLength;
   private int    mMaxRevLabelLength;
   private Point  mLineStart;
   private Point  mLineEnd;


   private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true);
   private static int sSvgScalePadding   = 40;
   private static int sMajorTickHeight   = 10;
   private static int sMinorTickHeight   = 5;

   private static GfxSize sDefaultLineLength = new Pixels(1000);
   private static GfxSize sDefaultGeneHeight = new Pixels(20);
   private static Color DEFAULT_COLOR = Color.BLACK;

   //##########################################################################
   // CONSTRUCTORS
   //##########################################################################


   //--------------------------------------------------------------------------
   public ContigPlot(String inContigName, int inContigLength)
   {
      mContigName   = inContigName;
      mContigLength = inContigLength;
      mDisplayRange = new SeqLocation(1, inContigLength);
   }


   //##########################################################################
   // PUBLIC METHODS
   //##########################################################################


   //--------------------------------------------------------------------------
   public ContigPlot setAutoAdjustScale(boolean inValue)
   {
      mAutoAdjustScale = inValue;
      return this;
   }

   //--------------------------------------------------------------------------
   public ContigPlot setDisplayLabels(boolean inValue)
   {
      mDisplayLabels = inValue;
      return this;
   }

   //--------------------------------------------------------------------------
   public ContigPlot addGene(Gene inValue)
   {
      if (null == mGenes)
      {
         mGenes = new ArrayList<>();
      }

      mGenes.add(inValue);

      return this;
   }

   //--------------------------------------------------------------------------
   public SVG toSVG()
   {
      if (mAutoAdjustScale)
      {
         adjustDisplayRange();
      }

      if (mDisplayLabels)
      {
         calculateMaxLabelLengths();
      }
      else
      {
         mMaxFwdLabelLength = mMaxRevLabelLength = 0;
      }

      mXScalingFactor = mLineLength.to(GfxUnits.pixels) / mDisplayRange.length().floatValue();

      SVG svg = new SVG();

      svg.addSubtag(new Script(new TooltipJS().generateJS()).setType("text/javascript"));

      int xOffset = 10;
      int yOffset = 10;
      SvgGroup group = svg.addGroup()
            .setTransform("translate(" + xOffset + "," + yOffset + ")");

      // Add the contig name as a title
      group.addText(mContigName, mTitleFont, new Point2D.Double(0, mLineY - mGeneHeight.toInt(GfxUnits.pixels) - mMaxFwdLabelLength - 13)).setClass("title");

      // Draw the contig line
      group.addLine(getLineStart(), getLineEnd()).setStrokeWidth(4);

      // Draw genes
      group.addSubtag(drawGenes());

      // Draw the scale
      group.addSubtag(generateXAxis());

      svg.setHeight(svg.getHeight() + 25).setWidth(svg.getWidth() + 25);
      return svg;
   }

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

      Range geneRange = getGeneRange();

      while (geneRange.length() < 3 * mMajorTickStep)
      {
         mMajorTickStep = mMajorTickStep / 10;
      }

      // What are the major tick values that contain the gene range?
      for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep);
           tickValue < mDisplayRange.getEnd();
           tickValue += mMajorTickStep)
      {
         if (tickValue <= geneRange.getStart())
         {
            mDisplayRange.setStart(tickValue <= 0 ? 1 : tickValue);
         }
         else if (tickValue > geneRange.getEnd())
         {
            mDisplayRange.setEnd(tickValue);
            break;
         }
      }


   }

   //--------------------------------------------------------------------------
   private Range getGeneRange()
   {
      Range geneRange = null;

      if (CollectionUtil.hasValues(mGenes))
      {
         for (Gene gene : mGenes)
         {
            if (null == geneRange)
            {
               geneRange = gene.getLocation().toIntRange();
            }
            else
            {
               geneRange = geneRange.superUnion(gene.getLocation().toIntRange());
            }
         }
      }

      return geneRange;
   }

   //--------------------------------------------------------------------------
   private Point getLineStart()
   {
      if (null == mLineStart)
      {
         mLineStart = new Point(0, mLineY);
      }

      return mLineStart;
   }

   //--------------------------------------------------------------------------
   private Point getLineEnd()
   {
      if (null == mLineEnd)
      {
         mLineEnd = new Point((int) mLineLength.to(GfxUnits.pixels), mLineY);
      }

      return mLineEnd;
   }

   //--------------------------------------------------------------------------
   private void calculateMaxLabelLengths()
   {
      int maxFwdLength = 0;
      int maxRevLength = 0;
      if (CollectionUtil.hasValues(mGenes))
      {
         for (Gene gene : mGenes)
         {
            Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);
            if (gene.getStrand().equals(Strand.FORWARD)
                && bbox.width > maxFwdLength)
            {
               maxFwdLength = bbox.width;
            }
            else if  (gene.getStrand().equals(Strand.REVERSE)
                      && bbox.width > maxRevLength)
            {
               maxRevLength = bbox.width;
            }
         }
      }

      mMaxFwdLabelLength = maxFwdLength;
      mMaxRevLabelLength = maxRevLength;
   }

   //--------------------------------------------------------------------------
   private SvgGroup drawGenes()
   {
      SvgGroup genesGroup = new SvgGroup().setClass("genes");


      if (CollectionUtil.hasValues(mGenes))
      {
         int lineHeight = (int) TextUtil.getStringRect("A", mLabelFont).getHeight();

         for (Gene gene : mGenes)
         {
            SvgGroup geneGroup = genesGroup.addGroup().setId(gene.getId());

            if (CollectionUtil.hasValues(gene.getExons()))
            {
               for (Exon exon : gene.getExons())
               {
                  // Calculate exon location
                  int xOffset = getBoundedAndScaledLocation(exon.getLeft());

                  // Draw the exon
                  int width = getScaledWidthInPixels(getDisplayBoundedValue(exon.getLeft()),
                        getDisplayBoundedValue(exon.getRight()));

                  SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, mLineY - (exon.getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0)),
                        new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));

                  Color color = (exon.getColor() != null ? exon.getColor() : DEFAULT_COLOR);
                  if (exon.getColor() != null)
                  {
                     color = exon.getColor();
                  }

                  svgExon.setFill(color);

//                  TooltipJS tooltip = new TooltipJS();
//                  tooltip.addTooltip(svgExon, getTooltipContent(gene));
               }
            }
            else if (gene.getLocation() != null)
            {
               int xOffset = getBoundedAndScaledLocation(gene.getLocation().toIntRange().getStart());
               int width = getScaledWidthInPixels(getDisplayBoundedValue(gene.getLocation().toIntRange().getStart()),
                                                  getDisplayBoundedValue(gene.getLocation().toIntRange().getEnd()));

               int geneY = mLineY - (gene.getLocation().getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0);
               SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, geneY),
                     new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));

               Color color = (gene.getColor() != null ? gene.getColor() : DEFAULT_COLOR);
               if (gene.getColor() != null)
               {
                  color = gene.getColor();
               }

               svgExon.setFill(color);

               if (mDisplayLabels)
               {
                  Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);

                  int labelX = (int) (xOffset + (width + lineHeight/2.0)/2.0);
                  int labelY = geneY + (gene.getLocation().getStrand().equals(Strand.FORWARD) ? -5 : mGeneHeight.toInt(GfxUnits.pixels) + bbox.width + 5);
                  int rotation = 360 - 90;
                  String labelTransform = "rotate(" + SVG.formatCoordinate(rotation) + " " + SVG.formatCoordinate(labelX) + " " + SVG.formatCoordinate(labelY) + ")";

                  SvgText label = geneGroup.addText(gene.getId(), mLabelFont, new Point(labelX, labelY))
                                       .setTransform(labelTransform)
                                       .setClass("gene_label");
               }
            }

            TooltipJS tooltip = new TooltipJS();
            tooltip.addTooltip(geneGroup, getTooltipContent(gene));
         }
      }

      return genesGroup;
   }


   //--------------------------------------------------------------------------
   private int getScaledWidthInPixels(int inStart, int inEnd)
   {
      int leftPixel = (int) (inStart * mXScalingFactor);
      int rightPixel = (int) (inEnd * mXScalingFactor);
      int width = rightPixel - leftPixel + 1;
      //     System.out.println("Exon: " + inStart + "-" + inEnd + "   " + leftPixel + "-" + rightPixel + "  " + mXScalingFactor + " (" + width + ")");/////////////////////////
      //     int width = Math.abs((int) ((inEnd - inStart + 1) * mXScalingFactor));

      // Minimum width of 1
      return (0 >= width ? 1 : width);
   }

   //--------------------------------------------------------------------------
   private int getBoundedAndScaledLocation(int inValue)
   {
      int bound = (int) (getDisplayBoundedValue(inValue) * mXScalingFactor);
      int start = (int) (mDisplayRange.getStart() * mXScalingFactor);
      return bound - start;
//      return (int)((getDisplayBoundedValue(inValue) - mDisplayStart) * mXScalingFactor);
   }


   //--------------------------------------------------------------------------
   private int getDisplayBoundedValue(int inValue)
   {
      int value = inValue;

      if (value < mDisplayRange.getStart())
      {
         value = mDisplayRange.getStart();
      }
      else if (value > mDisplayRange.getEnd())
      {
         value = mDisplayRange.getEnd();
      }

      return value;
   }

   //--------------------------------------------------------------------------
   public String getTooltipContent(Gene inGene)
   {
      StringBuilderPlus text = new StringBuilderPlus().setDelimiter("
"); if (StringUtil.isSet(inGene.getId()) && (null == inGene.getDescription() || inGene.getDescription().indexOf(inGene.getId()) < 0)) { text.append(inGene.getId()); } if (inGene.getDescription() != null) { text.delimitedAppend(inGene.getDescription()); } text.delimitedAppend(""); if (inGene.getStrand() != null) { text.append("Strand: ("); text.append(inGene.getStrand().getSymbol()); text.append(") "); } text.append("bp.: "); text.append(inGene.getLeft()); text.append(inGene.getLeft() != inGene.getRight() ?" - " + inGene.getRight() : ""); text.append(""); return text.toString(); } //-------------------------------------------------------------------------- public SvgGroup generateXAxis() { SvgGroup scalegroup = new SvgGroup().setClass("scale").addStyle(CSS.fontSize(mScaleFont.getSize()) + CSSProperty.font_family + "=" + mScaleFont.getFamily()); Point contigLineStart = getLineStart(); double scaleLineY = contigLineStart.getY() + mMaxRevLabelLength + 50; Point2D scaleLineStart = new Point2D.Double(contigLineStart.getX(), scaleLineY); Point2D scaleLineEnd = new Point2D.Double(getLineEnd().getX(), scaleLineY); // Axis scalegroup.addLine(scaleLineStart, scaleLineEnd).setClass("axis"); NumberFormat commaFormatter = NumberFormat.getInstance(); commaFormatter.setGroupingUsed(true); determineTickSize(); int yOffset; for (Integer tickValue : getTickValues()) { if (tickValue%mMajorTickStep == 0 || tickValue == mDisplayRange.getStart() || tickValue == mDisplayRange.getEnd()) { yOffset = sMajorTickHeight; String label = commaFormatter.format(tickValue); TextLayout layout = new TextLayout(label, mScaleFont, sFRC); double xTranslation = ((tickValue - mDisplayRange.getStart() + 1) * mXScalingFactor) + layout.getBounds().getHeight() / 2; double yTranslation = yOffset + layout.getAdvance() + 1; double x = scaleLineStart.getX() + xTranslation; double y = scaleLineStart.getY() + yTranslation + 5; scalegroup.addText(label, null, new Point2D.Double(x, y)).setTransform("rotate(-90, " + x + ", " + y + ")").setClass("tickLabel"); } else if (tickValue%mMinorTickStep == 0) { yOffset = sMinorTickHeight; } else { continue; } int xOffset = (int) ((tickValue - mDisplayRange.getStart()) * mXScalingFactor) + 1; scalegroup.addLine(new Point2D.Double(xOffset, scaleLineStart.getY()), new Point2D.Double(xOffset, scaleLineStart.getY() + yOffset)).setClass("tick"); } return scalegroup; } //-------------------------------------------------------------------------- private void determineTickSize() { int xAxisWidth = mDisplayRange.length().intValue(); String widthString = xAxisWidth + ""; mMajorTickStep = (int) Math.pow(10, widthString.length() - 1); if (0 == mMajorTickStep%xAxisWidth || mMajorTickStep > 0.5 * xAxisWidth) { mMajorTickStep = mMajorTickStep / 10; } mMinorTickStep = mMajorTickStep / 10; } //-------------------------------------------------------------------------- private List getTickValues() { List tickValues = new ArrayList<>(); // First add the major ticks tickValues.add(mDisplayRange.getStart()); for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep); tickValue < mDisplayRange.getEnd(); tickValue += mMajorTickStep) { if (tickValue > mDisplayRange.getStart()) { tickValues.add(tickValue); } } tickValues.add(mDisplayRange.getEnd()); // Now add the minor ticks for (int tickValue = (int) (Math.ceil(mDisplayRange.getStart() / mMinorTickStep) * mMinorTickStep); tickValue < mDisplayRange.getEnd(); tickValue += mMinorTickStep) { if (tickValue%mMajorTickStep != 0 && tickValue != mDisplayRange.getStart()) { tickValues.add(tickValue); } } return tickValues; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy