001package com.hfg.bio.seq.graphics;
002
003import java.awt.Color;
004import java.awt.Dimension;
005import java.awt.Font;
006import java.awt.Point;
007import java.awt.Rectangle;
008import java.awt.font.FontRenderContext;
009import java.awt.font.TextLayout;
010import java.awt.geom.AffineTransform;
011import java.awt.geom.Point2D;
012import java.text.NumberFormat;
013import java.util.ArrayList;
014import java.util.List;
015
016import com.hfg.bio.seq.Exon;
017import com.hfg.bio.Strand;
018import com.hfg.bio.seq.Gene;
019import com.hfg.bio.seq.SeqLocation;
020import com.hfg.css.CSS;
021import com.hfg.css.CSSProperty;
022import com.hfg.graphics.TextUtil;
023import com.hfg.graphics.units.GfxSize;
024import com.hfg.graphics.units.GfxUnits;
025import com.hfg.graphics.units.Pixels;
026import com.hfg.html.Script;
027import com.hfg.javascript.TooltipJS;
028import com.hfg.math.Range;
029import com.hfg.svg.SVG;
030import com.hfg.svg.SvgGroup;
031import com.hfg.svg.SvgRect;
032import com.hfg.svg.SvgText;
033import com.hfg.util.StringBuilderPlus;
034import com.hfg.util.StringUtil;
035import com.hfg.util.collection.CollectionUtil;
036
037
038//------------------------------------------------------------------------------
039/**
040 * Visualization of genes on a contig.
041 *
042 * @author J. Alex Taylor, hairyfatguy.com
043 */
044//------------------------------------------------------------------------------
045// com.hfg XML/HTML Coding Library
046//
047// This library is free software; you can redistribute it and/or
048// modify it under the terms of the GNU Lesser General Public
049// License as published by the Free Software Foundation; either
050// version 2.1 of the License, or (at your option) any later version.
051//
052// This library is distributed in the hope that it will be useful,
053// but WITHOUT ANY WARRANTY; without even the implied warranty of
054// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
055// Lesser General Public License for more details.
056//
057// You should have received a copy of the GNU Lesser General Public
058// License along with this library; if not, write to the Free Software
059// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
060//
061// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
062// jataylor@hairyfatguy.com
063//------------------------------------------------------------------------------
064
065public class ContigPlot
066{
067   private String  mContigName;
068   private Integer mContigLength;
069
070   private SeqLocation mDisplayRange;
071   private boolean     mAutoAdjustScale;
072   private boolean     mDisplayLabels;
073   private Font        mLabelFont = Font.decode("Arial-PLAIN-6");
074   private Font        mScaleFont = Font.decode("Arial-PLAIN-8");
075   private Font        mTitleFont = Font.decode("Arial-BOLD-10");
076
077   private List<Gene> mGenes;
078
079   private GfxSize mLineLength = sDefaultLineLength;
080   private GfxSize mGeneHeight = sDefaultGeneHeight;
081
082   private int mLineY = 150;
083
084   // Calculated
085   private float  mXScalingFactor;
086   private int    mMajorTickStep;
087   private int    mMinorTickStep;
088   private int    mMaxFwdLabelLength;
089   private int    mMaxRevLabelLength;
090   private Point  mLineStart;
091   private Point  mLineEnd;
092
093
094   private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true);
095   private static int sSvgScalePadding   = 40;
096   private static int sMajorTickHeight   = 10;
097   private static int sMinorTickHeight   = 5;
098
099   private static GfxSize sDefaultLineLength = new Pixels(1000);
100   private static GfxSize sDefaultGeneHeight = new Pixels(20);
101   private static Color DEFAULT_COLOR = Color.BLACK;
102
103   //##########################################################################
104   // CONSTRUCTORS
105   //##########################################################################
106
107
108   //--------------------------------------------------------------------------
109   public ContigPlot(String inContigName, int inContigLength)
110   {
111      mContigName   = inContigName;
112      mContigLength = inContigLength;
113      mDisplayRange = new SeqLocation(1, inContigLength);
114   }
115
116
117   //##########################################################################
118   // PUBLIC METHODS
119   //##########################################################################
120
121
122   //--------------------------------------------------------------------------
123   public ContigPlot setAutoAdjustScale(boolean inValue)
124   {
125      mAutoAdjustScale = inValue;
126      return this;
127   }
128
129   //--------------------------------------------------------------------------
130   public ContigPlot setDisplayLabels(boolean inValue)
131   {
132      mDisplayLabels = inValue;
133      return this;
134   }
135
136   //--------------------------------------------------------------------------
137   public ContigPlot addGene(Gene inValue)
138   {
139      if (null == mGenes)
140      {
141         mGenes = new ArrayList<>();
142      }
143
144      mGenes.add(inValue);
145
146      return this;
147   }
148
149   //--------------------------------------------------------------------------
150   public SVG toSVG()
151   {
152      if (mAutoAdjustScale)
153      {
154         adjustDisplayRange();
155      }
156
157      if (mDisplayLabels)
158      {
159         calculateMaxLabelLengths();
160      }
161      else
162      {
163         mMaxFwdLabelLength = mMaxRevLabelLength = 0;
164      }
165
166      mXScalingFactor = mLineLength.to(GfxUnits.pixels) / mDisplayRange.length().floatValue();
167
168      SVG svg = new SVG();
169
170      svg.addSubtag(new Script(new TooltipJS().generateJS()).setType("text/javascript"));
171
172      int xOffset = 10;
173      int yOffset = 10;
174      SvgGroup group = svg.addGroup()
175            .setTransform("translate(" + xOffset + "," + yOffset + ")");
176
177      // Add the contig name as a title
178      group.addText(mContigName, mTitleFont, new Point2D.Double(0, mLineY - mGeneHeight.toInt(GfxUnits.pixels) - mMaxFwdLabelLength - 13)).setClass("title");
179
180      // Draw the contig line
181      group.addLine(getLineStart(), getLineEnd()).setStrokeWidth(4);
182
183      // Draw genes
184      group.addSubtag(drawGenes());
185
186      // Draw the scale
187      group.addSubtag(generateXAxis());
188
189      svg.setHeight(svg.getHeight() + 25).setWidth(svg.getWidth() + 25);
190      return svg;
191   }
192
193   //--------------------------------------------------------------------------
194   private void adjustDisplayRange()
195   {
196      String widthString = mContigLength + "";
197      mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
198      if (0 == mMajorTickStep%mContigLength
199          || mMajorTickStep > 0.5 * mContigLength)
200      {
201         mMajorTickStep = mMajorTickStep / 10;
202      }
203
204      Range<Integer> geneRange = getGeneRange();
205
206      while (geneRange.length() < 3 * mMajorTickStep)
207      {
208         mMajorTickStep = mMajorTickStep / 10;
209      }
210
211      // What are the major tick values that contain the gene range?
212      for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep);
213           tickValue < mDisplayRange.getEnd();
214           tickValue += mMajorTickStep)
215      {
216         if (tickValue <= geneRange.getStart())
217         {
218            mDisplayRange.setStart(tickValue <= 0 ? 1 : tickValue);
219         }
220         else if (tickValue > geneRange.getEnd())
221         {
222            mDisplayRange.setEnd(tickValue);
223            break;
224         }
225      }
226
227
228   }
229
230   //--------------------------------------------------------------------------
231   private Range<Integer> getGeneRange()
232   {
233      Range<Integer> geneRange = null;
234
235      if (CollectionUtil.hasValues(mGenes))
236      {
237         for (Gene gene : mGenes)
238         {
239            if (null == geneRange)
240            {
241               geneRange = gene.getLocation().toIntRange();
242            }
243            else
244            {
245               geneRange = geneRange.superUnion(gene.getLocation().toIntRange());
246            }
247         }
248      }
249
250      return geneRange;
251   }
252
253   //--------------------------------------------------------------------------
254   private Point getLineStart()
255   {
256      if (null == mLineStart)
257      {
258         mLineStart = new Point(0, mLineY);
259      }
260
261      return mLineStart;
262   }
263
264   //--------------------------------------------------------------------------
265   private Point getLineEnd()
266   {
267      if (null == mLineEnd)
268      {
269         mLineEnd = new Point((int) mLineLength.to(GfxUnits.pixels), mLineY);
270      }
271
272      return mLineEnd;
273   }
274
275   //--------------------------------------------------------------------------
276   private void calculateMaxLabelLengths()
277   {
278      int maxFwdLength = 0;
279      int maxRevLength = 0;
280      if (CollectionUtil.hasValues(mGenes))
281      {
282         for (Gene gene : mGenes)
283         {
284            Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);
285            if (gene.getStrand().equals(Strand.FORWARD)
286                && bbox.width > maxFwdLength)
287            {
288               maxFwdLength = bbox.width;
289            }
290            else if  (gene.getStrand().equals(Strand.REVERSE)
291                      && bbox.width > maxRevLength)
292            {
293               maxRevLength = bbox.width;
294            }
295         }
296      }
297
298      mMaxFwdLabelLength = maxFwdLength;
299      mMaxRevLabelLength = maxRevLength;
300   }
301
302   //--------------------------------------------------------------------------
303   private SvgGroup drawGenes()
304   {
305      SvgGroup genesGroup = new SvgGroup().setClass("genes");
306
307
308      if (CollectionUtil.hasValues(mGenes))
309      {
310         int lineHeight = (int) TextUtil.getStringRect("A", mLabelFont).getHeight();
311
312         for (Gene gene : mGenes)
313         {
314            SvgGroup geneGroup = genesGroup.addGroup().setId(gene.getId());
315
316            if (CollectionUtil.hasValues(gene.getExons()))
317            {
318               for (Exon exon : gene.getExons())
319               {
320                  // Calculate exon location
321                  int xOffset = getBoundedAndScaledLocation(exon.getLeft());
322
323                  // Draw the exon
324                  int width = getScaledWidthInPixels(getDisplayBoundedValue(exon.getLeft()),
325                        getDisplayBoundedValue(exon.getRight()));
326
327                  SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, mLineY - (exon.getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0)),
328                        new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));
329
330                  Color color = (exon.getColor() != null ? exon.getColor() : DEFAULT_COLOR);
331                  if (exon.getColor() != null)
332                  {
333                     color = exon.getColor();
334                  }
335
336                  svgExon.setFill(color);
337
338//                  TooltipJS tooltip = new TooltipJS();
339//                  tooltip.addTooltip(svgExon, getTooltipContent(gene));
340               }
341            }
342            else if (gene.getLocation() != null)
343            {
344               int xOffset = getBoundedAndScaledLocation(gene.getLocation().toIntRange().getStart());
345               int width = getScaledWidthInPixels(getDisplayBoundedValue(gene.getLocation().toIntRange().getStart()),
346                                                  getDisplayBoundedValue(gene.getLocation().toIntRange().getEnd()));
347
348               int geneY = mLineY - (gene.getLocation().getStrand().equals(Strand.FORWARD) ? mGeneHeight.toInt(GfxUnits.pixels) : 0);
349               SvgRect svgExon = geneGroup.addRect(new Rectangle(new Point(xOffset, geneY),
350                     new Dimension(width, mGeneHeight.toInt(GfxUnits.pixels))));
351
352               Color color = (gene.getColor() != null ? gene.getColor() : DEFAULT_COLOR);
353               if (gene.getColor() != null)
354               {
355                  color = gene.getColor();
356               }
357
358               svgExon.setFill(color);
359
360               if (mDisplayLabels)
361               {
362                  Rectangle bbox = TextUtil.getStringRect(gene.getId(), mLabelFont);
363
364                  int labelX = (int) (xOffset + (width + lineHeight/2.0)/2.0);
365                  int labelY = geneY + (gene.getLocation().getStrand().equals(Strand.FORWARD) ? -5 : mGeneHeight.toInt(GfxUnits.pixels) + bbox.width + 5);
366                  int rotation = 360 - 90;
367                  String labelTransform = "rotate(" + SVG.formatCoordinate(rotation) + " " + SVG.formatCoordinate(labelX) + " " + SVG.formatCoordinate(labelY) + ")";
368
369                  SvgText label = geneGroup.addText(gene.getId(), mLabelFont, new Point(labelX, labelY))
370                                       .setTransform(labelTransform)
371                                       .setClass("gene_label");
372               }
373            }
374
375            TooltipJS tooltip = new TooltipJS();
376            tooltip.addTooltip(geneGroup, getTooltipContent(gene));
377         }
378      }
379
380      return genesGroup;
381   }
382
383
384   //--------------------------------------------------------------------------
385   private int getScaledWidthInPixels(int inStart, int inEnd)
386   {
387      int leftPixel = (int) (inStart * mXScalingFactor);
388      int rightPixel = (int) (inEnd * mXScalingFactor);
389      int width = rightPixel - leftPixel + 1;
390      //     System.out.println("Exon: " + inStart + "-" + inEnd + "   " + leftPixel + "-" + rightPixel + "  " + mXScalingFactor + " (" + width + ")");/////////////////////////
391      //     int width = Math.abs((int) ((inEnd - inStart + 1) * mXScalingFactor));
392
393      // Minimum width of 1
394      return (0 >= width ? 1 : width);
395   }
396
397   //--------------------------------------------------------------------------
398   private int getBoundedAndScaledLocation(int inValue)
399   {
400      int bound = (int) (getDisplayBoundedValue(inValue) * mXScalingFactor);
401      int start = (int) (mDisplayRange.getStart() * mXScalingFactor);
402      return bound - start;
403//      return (int)((getDisplayBoundedValue(inValue) - mDisplayStart) * mXScalingFactor);
404   }
405
406
407   //--------------------------------------------------------------------------
408   private int getDisplayBoundedValue(int inValue)
409   {
410      int value = inValue;
411
412      if (value < mDisplayRange.getStart())
413      {
414         value = mDisplayRange.getStart();
415      }
416      else if (value > mDisplayRange.getEnd())
417      {
418         value = mDisplayRange.getEnd();
419      }
420
421      return value;
422   }
423
424   //--------------------------------------------------------------------------
425   public String getTooltipContent(Gene inGene)
426   {
427      StringBuilderPlus text = new StringBuilderPlus().setDelimiter("<br />");
428
429      if (StringUtil.isSet(inGene.getId())
430          && (null == inGene.getDescription()
431              || inGene.getDescription().indexOf(inGene.getId()) < 0))
432      {
433         text.append(inGene.getId());
434      }
435
436      if (inGene.getDescription() != null)
437      {
438         text.delimitedAppend(inGene.getDescription());
439      }
440
441      text.delimitedAppend("<i>");
442      if (inGene.getStrand() != null)
443      {
444         text.append("Strand: (");
445         text.append(inGene.getStrand().getSymbol());
446         text.append(") ");
447      }
448
449      text.append("bp.: ");
450      text.append(inGene.getLeft());
451      text.append(inGene.getLeft() != inGene.getRight() ?" - " + inGene.getRight() : "");
452      text.append("</i>");
453
454      return text.toString();
455   }
456
457
458   //--------------------------------------------------------------------------
459   public SvgGroup generateXAxis()
460   {
461      SvgGroup scalegroup = new SvgGroup().setClass("scale").addStyle(CSS.fontSize(mScaleFont.getSize()) + CSSProperty.font_family + "=" + mScaleFont.getFamily());
462
463      Point contigLineStart = getLineStart();
464
465      double scaleLineY = contigLineStart.getY() + mMaxRevLabelLength + 50;
466
467      Point2D scaleLineStart = new Point2D.Double(contigLineStart.getX(), scaleLineY);
468      Point2D scaleLineEnd   = new Point2D.Double(getLineEnd().getX(), scaleLineY);
469
470      // Axis
471      scalegroup.addLine(scaleLineStart, scaleLineEnd).setClass("axis");
472
473      NumberFormat commaFormatter = NumberFormat.getInstance();
474      commaFormatter.setGroupingUsed(true);
475
476      determineTickSize();
477      int yOffset;
478      for (Integer tickValue : getTickValues())
479      {
480         if (tickValue%mMajorTickStep == 0
481               || tickValue == mDisplayRange.getStart()
482               || tickValue == mDisplayRange.getEnd())
483         {
484            yOffset = sMajorTickHeight;
485
486            String label = commaFormatter.format(tickValue);
487
488            TextLayout layout = new TextLayout(label, mScaleFont, sFRC);
489            double xTranslation = ((tickValue - mDisplayRange.getStart() + 1) * mXScalingFactor)
490                                  + layout.getBounds().getHeight() / 2;
491            double yTranslation = yOffset + layout.getAdvance() + 1;
492
493            double x = scaleLineStart.getX() + xTranslation;
494            double y = scaleLineStart.getY() + yTranslation + 5;
495
496            scalegroup.addText(label, null, new Point2D.Double(x, y)).setTransform("rotate(-90, " + x + ", " + y + ")").setClass("tickLabel");
497         }
498         else if (tickValue%mMinorTickStep == 0)
499         {
500            yOffset = sMinorTickHeight;
501         }
502         else
503         {
504            continue;
505         }
506
507         int xOffset = (int) ((tickValue - mDisplayRange.getStart()) * mXScalingFactor) + 1;
508         scalegroup.addLine(new Point2D.Double(xOffset, scaleLineStart.getY()), new Point2D.Double(xOffset, scaleLineStart.getY() + yOffset)).setClass("tick");
509      }
510
511      return scalegroup;
512   }
513
514
515   //--------------------------------------------------------------------------
516   private void determineTickSize()
517   {
518      int xAxisWidth = mDisplayRange.length().intValue();
519      String widthString = xAxisWidth + "";
520      mMajorTickStep = (int) Math.pow(10, widthString.length() - 1);
521      if (0 == mMajorTickStep%xAxisWidth
522          || mMajorTickStep > 0.5 * xAxisWidth)
523      {
524         mMajorTickStep = mMajorTickStep / 10;
525      }
526
527      mMinorTickStep = mMajorTickStep / 10;
528   }
529
530   //--------------------------------------------------------------------------
531   private List<Integer> getTickValues()
532   {
533      List<Integer> tickValues = new ArrayList<>();
534
535      // First add the major ticks
536      tickValues.add(mDisplayRange.getStart());
537
538      for (int tickValue = (int) (Math.ceil(1 / mMajorTickStep) * mMajorTickStep);
539           tickValue < mDisplayRange.getEnd();
540           tickValue += mMajorTickStep)
541      {
542         if (tickValue > mDisplayRange.getStart())
543         {
544            tickValues.add(tickValue);
545         }
546      }
547
548      tickValues.add(mDisplayRange.getEnd());
549
550      // Now add the minor ticks
551      for (int tickValue = (int) (Math.ceil(mDisplayRange.getStart() / mMinorTickStep) * mMinorTickStep);
552           tickValue < mDisplayRange.getEnd();
553           tickValue += mMinorTickStep)
554      {
555         if (tickValue%mMajorTickStep != 0
556               && tickValue != mDisplayRange.getStart())
557         {
558            tickValues.add(tickValue);
559         }
560      }
561
562      return tickValues;
563   }
564
565}