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}