001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 */ 021 022package org.biojava.bio.chromatogram.graphic; 023 024import java.awt.BasicStroke; 025import java.awt.Color; 026import java.awt.Graphics2D; 027import java.awt.Rectangle; 028import java.awt.Shape; 029import java.awt.Stroke; 030import java.awt.geom.AffineTransform; 031import java.awt.geom.GeneralPath; 032import java.awt.geom.Line2D; 033import java.awt.geom.Point2D; 034import java.awt.geom.Rectangle2D; 035import java.util.HashMap; 036import java.util.Iterator; 037import java.util.Map; 038 039import org.biojava.bio.BioError; 040import org.biojava.bio.chromatogram.Chromatogram; 041import org.biojava.bio.chromatogram.ChromatogramTools; 042import org.biojava.bio.seq.DNATools; 043import org.biojava.bio.symbol.IllegalSymbolException; 044import org.biojava.bio.symbol.SimpleSymbolList; 045import org.biojava.bio.symbol.Symbol; 046import org.biojava.bio.symbol.SymbolList; 047 048/** 049 * Encapsulates a configurable method for drawing a {@link Chromatogram} 050 * into a graphics context. 051 * 052 * @author Rhett Sutphin (<a href="http://genome.uiowa.edu/">UI CBCB</a>) 053 * @author Matthew Pocock 054 * @since 1.3 055 */ 056public class ChromatogramGraphic implements Cloneable { 057 /** A pseudo call list for use when a Chromatogram has no base calls. */ 058 private static SymbolList SINGLE_CALL = 059 new SimpleSymbolList(new Symbol[] { DNATools.getDNA().getGapSymbol() }, 060 1, DNATools.getDNA()); 061 062 // array indices for subpaths, etc. 063 private static final int A = 0, C = 1, G = 2, T = 3; 064 065 private Chromatogram chromat; 066 private float vertScale, horizScale; 067 private int width, height; 068 069 /** Flag for subpaths. */ 070 protected boolean subpathsValid; 071 072 /** Flag for call boxes. */ 073 protected boolean callboxesValid; 074 075 /** Flag for drawable call boxes. */ 076 protected boolean drawableCallboxesValid; 077 078 private GeneralPath[][] subpaths; 079 private Rectangle2D.Float[] callboxes; 080 private Rectangle2D[] drawableCallboxes; 081 private AffineTransform drawnCallboxesTx; 082 083 /** The map containing the {@link Option}s and values for this instance. */ 084 protected Map options; 085 /** The map containing the colors for drawing traces. Keys are DNA Symbols. */ 086 protected Map colors; 087 /** The map containing the fill colors for callboxes. Keys are DNA Symbols. */ 088 protected Map fillColors; 089 090 /** The default values for the {@link #colors} map. */ 091 private static final Map DEFAULT_COLORS = new HashMap(); 092 static { 093 DEFAULT_COLORS.put(DNATools.a(), Color.green); 094 DEFAULT_COLORS.put(DNATools.c(), Color.blue); 095 DEFAULT_COLORS.put(DNATools.g(), Color.black); 096 DEFAULT_COLORS.put(DNATools.t(), Color.red); 097 } 098 099 /** Default constructor with no Chromatogram. */ 100 public ChromatogramGraphic() { 101 this(null); 102 } 103 104 /** 105 * Creates a new <code>ChromatogramGraphic</code>, initially displaying 106 * the given chromatogram. 107 * 108 * @param c the Chromomatogram to display 109 */ 110 public ChromatogramGraphic(Chromatogram c) { 111 options = new HashMap(Option.DEFAULTS); 112 colors = new HashMap(); 113 fillColors = new HashMap(); 114 height = -1; width = -1; 115 vertScale = -1.0f; horizScale = -1.0f; 116 subpaths = new GeneralPath[4][]; 117 for (Iterator it = DEFAULT_COLORS.keySet().iterator() ; it.hasNext() ; ) { 118 Symbol key = (Symbol) it.next(); 119 setBaseColor(key, (Color) DEFAULT_COLORS.get(key)); 120 } 121 setChromatogram(c); 122 //if (System.getProperty("os.name").equalsIgnoreCase("mac os x")) { 123 // setOption(Option.USE_PER_SHAPE_TRANSFORM, Boolean.TRUE); 124 //} 125 } 126 127 /** 128 * Precomputes the {@link java.awt.geom.GeneralPath}s used to draw the 129 * traces. 130 */ 131 protected synchronized void generateSubpaths() { 132 if (chromat != null && !subpathsValid) { 133 if (subpaths == null) 134 subpaths = new GeneralPath[4][]; 135 int[][] samples = new int[4][]; 136 try { 137 samples[A] = chromat.getTrace(DNATools.a()); 138 samples[C] = chromat.getTrace(DNATools.c()); 139 samples[G] = chromat.getTrace(DNATools.g()); 140 samples[T] = chromat.getTrace(DNATools.t()); 141 } catch (IllegalSymbolException ise) { 142 throw new BioError("Can't happen"); 143 } 144 ChromatogramNonlinearScaler horizScaler = (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER); 145 float max = chromat.getMax(); 146 int subpathLength = getIntOption(Option.SUBPATH_LENGTH); 147 int countSubpaths = (int)Math.ceil( horizScaler.scale(chromat, samples[A].length-1) / subpathLength ); 148 int[] subpathTraceBounds = new int[countSubpaths+1]; 149 { 150 subpathTraceBounds[0] = 0; 151 int bound = 1; 152 float scaled; 153 for (int i = 0 ; i < samples[A].length ; i++) { 154 scaled = horizScaler.scale(chromat, i); 155 if (scaled >= bound*subpathLength) { 156 subpathTraceBounds[bound] = i; 157 bound++; 158 } 159 } 160 subpathTraceBounds[countSubpaths] = samples[A].length - 1; 161 } 162 int offsetIdx, thisPathLen; 163 for (int i = 0 ; i < 4 ; i++) { 164 subpaths[i] = new GeneralPath[countSubpaths]; 165 for (int j = 0 ; j < countSubpaths ; j++) { 166 thisPathLen = subpathTraceBounds[j+1] - subpathTraceBounds[j]; 167 subpaths[i][j] = new GeneralPath(GeneralPath.WIND_EVEN_ODD, thisPathLen); 168 offsetIdx = subpathTraceBounds[j]; 169 subpaths[i][j].moveTo(horizScaler.scale(chromat, offsetIdx), max - samples[i][offsetIdx]); 170 for (int k = offsetIdx+1 ; k < samples[i].length && k <= offsetIdx + thisPathLen ; k++) { 171 subpaths[i][j].lineTo(horizScaler.scale(chromat, k), max - samples[i][k]); 172 } 173 } 174 } 175 subpathsValid = true; 176 } 177 } 178 179 /** 180 * Precomputes the {@link java.awt.geom.Rectangle2D}s that are the in-memory 181 * representation of the callboxes. These rectangles are used for drawing 182 * (via generateDrawableCallboxes) as well as queries (e.g., 183 * {@link #getCallContaining}). 184 */ 185 protected synchronized void generateCallboxes() { 186 if (chromat != null && !callboxesValid) { 187 if (chromat.getSequenceLength() < 2) { 188 callboxes = new Rectangle2D.Float[1]; 189 callboxes[0] = new Rectangle2D.Float(0, 0, chromat.getTraceLength() - 1, chromat.getMax()); 190 } 191 else { 192 int[] bcOffsets = ChromatogramTools.getTraceOffsetArray(chromat); 193 if (callboxes == null || callboxes.length != bcOffsets.length) 194 callboxes = new Rectangle2D.Float[bcOffsets.length]; 195 ChromatogramNonlinearScaler horizScaler = 196 (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER); 197 float max = chromat.getMax(); 198 int leftIdx = 0; 199 int rightIdx = bcOffsets[0] + (int) Math.floor( ((double) (bcOffsets[1] - bcOffsets[0])) / 2) + 1; 200 float left = horizScaler.scale(chromat, leftIdx); 201 float right = horizScaler.scale(chromat, rightIdx); 202 callboxes[0] = new Rectangle2D.Float(left, 0, right - left, max); 203 //System.out.println("[cg.gcb] cb[0] left="+leftIdx+":"+left+" center="+bcOffsets[0]+" right="+rightIdx+":"+right+" box="+callboxes[0]); 204 int i = 1; 205 while (i < bcOffsets.length - 1) { 206 leftIdx = rightIdx; 207 rightIdx = bcOffsets[i] + (int) Math.floor( ((double) (bcOffsets[i+1] - bcOffsets[i])) / 2) + 1; 208 left = right; 209 right = horizScaler.scale(chromat, rightIdx); 210 callboxes[i] = new Rectangle2D.Float(left, 0, right - left, max); 211 //System.out.println("[cg.gcb] cb["+i+"] left="+leftIdx+":"+left+" center="+bcOffsets[i]+" right="+rightIdx+":"+right+" box="+callboxes[i]); 212 i++; 213 } 214 left = right; 215 right = horizScaler.scale(chromat, chromat.getTraceLength() - 1); 216 callboxes[i] = new Rectangle2D.Float(left, 0, right - left, max); 217 //System.out.println("[cg.gcb] cb["+i+"] left="+left+" center="+bcOffsets[i]+" right="+right+" box="+callboxes[i]); 218 } 219 callboxesValid = true; 220 drawableCallboxesValid = false; 221 } 222 //System.out.println("[cg.gcb]"); 223 //for (int i = 0 ; i < callboxes.length ; i++) 224 // System.out.println("callboxes["+i+"]="+callboxes[i]); 225 } 226 227 /** 228 * Precomputes the callboxes in screen coordinates. 229 * @param shapeTx the transform to apply to the callboxes to move them into 230 * screen space. 231 */ 232 protected synchronized void generateDrawableCallboxes(AffineTransform shapeTx) { 233 if (!callboxesValid) 234 generateCallboxes(); 235 if (!shapeTx.equals(drawnCallboxesTx) || !drawableCallboxesValid) { 236 //System.out.println("Regen drawableCallboxes with " + shapeTx + " condition: tx="+shapeTx.equals(drawnCallboxesTx)+" valid="+drawableCallboxesValid); 237 if (drawableCallboxes == null || drawableCallboxes.length != callboxes.length) 238 drawableCallboxes = new Rectangle2D[callboxes.length]; 239 for (int i = 0 ; i < drawableCallboxes.length ; i++) 240 drawableCallboxes[i] = shapeTx.createTransformedShape(callboxes[i]).getBounds2D(); 241 drawnCallboxesTx = (AffineTransform) shapeTx.clone(); 242 drawableCallboxesValid = true; 243 //System.out.println("[cg.gdcb]"); 244 //for (int i = 0 ; i < drawableCallboxes.length ; i++) 245 // System.out.println("cb["+i+"]="+callboxes[i]+"\ndcb["+i+"]="+drawableCallboxes[i]); 246 } 247 } 248 249 /** 250 * Accessor for the in-use chromatogram. 251 * @return the chromatogram that a call to {@link #drawTo} will draw 252 */ 253 public Chromatogram getChromatogram() { return chromat; } 254 /** 255 * Sets the chromatogram to draw. 256 * @param c the new chromatogram 257 * @see Option#WIDTH_IS_AUTHORITATIVE 258 * @see Option#HEIGHT_IS_AUTHORITATIVE 259 */ 260 public synchronized void setChromatogram(Chromatogram c) { 261 this.chromat = c; 262 callboxesValid = false; 263 subpathsValid = false; 264 // set width, height, horizScale, vertScale for new chromat (even if null) 265 if (optionIsTrue(Option.WIDTH_IS_AUTHORITATIVE)) 266 setWidth(width); 267 else 268 setHorizontalScale(horizScale); 269 if (optionIsTrue(Option.HEIGHT_IS_AUTHORITATIVE)) 270 setHeight(height); 271 else 272 setVerticalScale(vertScale); 273 // drawing bounds default to show the whole chromat 274 setOption(Option.FROM_TRACE_SAMPLE, new Integer(0)); 275 if (c == null) 276 setOption(Option.TO_TRACE_SAMPLE, new Integer(Integer.MAX_VALUE)); 277 else 278 setOption(Option.TO_TRACE_SAMPLE, new Integer(c.getTraceLength() - 1)); 279 //System.out.println("chromatgfx[w=" + width + "; h=" + height + "; hs=" + horizScale + "; vs=" + vertScale + "]"); 280 } 281 282 /** 283 * Returns the width of the whole graphic (in pixels). 284 * 285 * @return the width 286 */ 287 public int getWidth() { return width; } 288 289 /** 290 * Returns the height of the whole graphic (in pixels). 291 * 292 * @return the height 293 */ 294 public int getHeight() { return height; } 295 296 /** 297 * Returns the in-use horizontal scale factor. 298 * The "units" of this value are (trace samples) / pixel. 299 * For example, a horizontal scale of 1.0 means that there will be one 300 * pixel horizontally for each trace sample. 301 * 302 * @return the horizontal scale 303 */ 304 public float getHorizontalScale() { return horizScale; } 305 306 /** 307 * Returns the in use vertical scale factor. 308 * The "units" of this value are (trace value bins) / pixel. 309 * For example, a vertical scale of 1.0 means that there will be one 310 * pixel vertically for each value in the range 311 * [0, <code>getChromatogram().getMax()</code>]. 312 * 313 * @return the vertical scale 314 */ 315 public float getVerticalScale() { return vertScale; } 316 317 /** 318 * Returns the width of the graphic as it will be rendered. 319 * This means that the {@link Option#FROM_TRACE_SAMPLE} and 320 * {@link Option#TO_TRACE_SAMPLE} bounds are taken into account. 321 * 322 * @return the rendered width 323 */ 324 public int getRenderedWidth() { 325 return getRenderedWidth(horizScale); 326 } 327 328 /** 329 * Returns the width of the graphic as it would be rendered with 330 * the specified horizontal scale. The {@link Option#FROM_TRACE_SAMPLE} and 331 * {@link Option#TO_TRACE_SAMPLE} bounds are taken into account. 332 * 333 * @param horizontalScale the horizontal scale 334 * @return the rendered width at that scale 335 */ 336 public int getRenderedWidth(float horizontalScale) { 337 return (int) Math.ceil( 338 horizontalScale * 339 (getFloatOption(Option.TO_TRACE_SAMPLE) - 340 getFloatOption(Option.FROM_TRACE_SAMPLE) + 341 1) 342 ); 343 } 344 345 /** 346 * Sets the height (in pixels). This will also change the 347 * vertical scale. 348 * 349 * @param h the desired height in pixels 350 * @see Option#HEIGHT_IS_AUTHORITATIVE 351 */ 352 public void setHeight(int h) { 353 if (h != height) drawableCallboxesValid = false; 354 height = h; 355 if (chromat != null) 356 vertScale = ( (float) height ) / chromat.getMax(); 357 else 358 vertScale = -1.0f; 359 } 360 361 /** 362 * Sets the vertical scale (proportional). This will also 363 * change the height. 364 * 365 * @param vs the desired vertical scale. See {@link #getVerticalScale} 366 * for semantics. 367 * @see Option#HEIGHT_IS_AUTHORITATIVE 368 */ 369 public void setVerticalScale(float vs) { 370 if (vs != vertScale) drawableCallboxesValid = false; 371 vertScale = vs; 372 if (chromat != null) 373 height = (int) (vertScale * chromat.getMax()); 374 else 375 height = -1; 376 } 377 378 /** 379 * Sets the width of the whole graphic (in pixels). This will also change 380 * the horizontal scale. 381 * 382 * @param w the desired width in pixels 383 * @see Option#WIDTH_IS_AUTHORITATIVE 384 */ 385 public void setWidth(int w) { 386 if (w != width) drawableCallboxesValid = false; 387 width = w; 388 ChromatogramNonlinearScaler horizScaler = 389 (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER); 390 if (chromat != null) 391 horizScale = ( (float) width ) / horizScaler.scale(chromat, chromat.getTraceLength()-1); 392 else 393 horizScale = -1.0f; 394 } 395 396 /** 397 * Sets the horizontal scale (proportional). This will also 398 * change the width. 399 * 400 * @param hs the desired vertical scale. See {@link #getHorizontalScale} 401 * for semantics. 402 * @see Option#WIDTH_IS_AUTHORITATIVE 403 */ 404 public void setHorizontalScale(float hs) { 405 if (hs != horizScale) drawableCallboxesValid = false; 406 horizScale = hs; 407 408 ChromatogramNonlinearScaler horizScaler = 409 (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER); 410 if (chromat != null) 411 width = (int) (horizScale * horizScaler.scale(chromat, chromat.getTraceLength()-1)); 412 else 413 width = -1; 414 } 415 416 /** 417 * Returns the color that will be used to draw the trace for the 418 * given DNA symbol. 419 * @param b the symbol 420 * @return the color, or null if none is set 421 */ 422 public Color getBaseColor(Symbol b) { return (Color) colors.get(b); } 423 /** 424 * Returns the color that will be used to fill in the callboxes for 425 * calls with the given symbol. 426 * @param b the symbol 427 * @return the color, or null if none is set 428 */ 429 public Color getBaseFillColor(Symbol b) { return (Color) fillColors.get(b); } 430 /** 431 * Maps a color to a DNA symbol. The color as specified will be used for 432 * to draw the trace for the symbol (if any). The fill color for calls to 433 * the symbol will be derived from the trace color. 434 * @param b the symbol 435 * @param c the color 436 */ 437 public void setBaseColor(Symbol b, Color c) { 438 colors.put(b, c); 439 // fade color 440 float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null); 441 //System.out.println("Base: "+b+"; H="+hsb[0]+"; S="+hsb[1]+"; B="+hsb[2]); 442 fillColors.put(b, Color.getHSBColor(hsb[0], hsb[1] * 0.09f, Math.max(hsb[2], 0.8f))); 443 } 444 445 /** 446 * Returns the number of callboxes, regenerating them if necessary. This 447 * should almost always equal 448 * <code>getChromatogram().getSequenceLength()</code> 449 * @return the number of callboxes 450 */ 451 public int getCallboxCount() { 452 if (!callboxesValid) 453 generateCallboxes(); 454 if (callboxesValid) 455 return callboxes.length; 456 else 457 return 0; 458 } 459 460 /** 461 * Returns the screen-coordinate bounds of the callbox for a given call. 462 * 463 * @param index the callbox for which to get the bounds <b>0-based</b> 464 * @return a Rectangle2D giving the bounds of the call box 465 */ 466 public Rectangle2D getCallboxBounds(int index) { 467 return getCallboxBounds(index, true); 468 } 469 470 /** 471 * Returns the bounds of the callbox for a given call. 472 * @param index the callbox for which to get the bounds <b>0-based</b>. 473 * @param boundsOnScreen determines the coordinate system of the returned 474 * bounds 475 * @return the bounds of the callbox in screen coordinates if 476 * <code>boundsOnScreen</code> is true, otherwise the bounds 477 * of the callbox in chromatogram coordinates 478 */ 479 public Rectangle2D getCallboxBounds(int index, boolean boundsOnScreen) { 480 if (chromat != null && index >= 0 && index < getCallboxCount()) { 481 if (!callboxesValid) 482 generateCallboxes(); 483 if (boundsOnScreen) { 484 if (!drawableCallboxesValid) 485 generateDrawableCallboxes(getTransform()); 486 return drawableCallboxes[index].getBounds2D(); 487 //return getTransform().createTransformedShape(callboxes[index]).getBounds2D(); 488 } 489 else { 490 return callboxes[index].getBounds2D(); 491 } 492 } 493 else { 494 return null; 495 } 496 } 497 498 /** 499 * Returns the <b>0-based</b> index of the call containing a given 500 * point. The point may be either in screen space or chromatogram 501 * space, scale-wise. If the point is in screen space, the caller must 502 * translate the point such that if it is, for instance, from a mouse 503 * click, a click on the upper-left corner of the graphic would be (0,0). 504 * @param point the point to search for 505 * @param pointOnScreen if true, the point will be treated as though it 506 * is in screen space. Otherwise, it will be considered to be 507 * in chromatogram space. 508 * @return the <b>0-based</b> index of the callbox which contains the point 509 */ 510 public int getCallContaining(Point2D point, boolean pointOnScreen) { 511 if (chromat != null) { 512 if (!callboxesValid) 513 generateCallboxes(); 514 Point2D trans = new Point2D.Double(point.getX(), point.getY()); 515 if (pointOnScreen) 516 getInvTransform().transform(point, trans); 517 int i = 0; 518 // XXX: binary search is possible since callboxes is sorted 519 while (i < callboxes.length && trans.getX() > callboxes[i].getMaxX()) 520 i++; 521 return (i < callboxes.length) ? (i) : (i-1); 522 } 523 else { 524 return 0; 525 } 526 } 527 528 /** 529 * Synonym for {@link #getCallContaining(Point2D, boolean)} with 530 * <code>pointOnScreen</code>=true. 531 * 532 * @param point the Point2D to search with 533 * @return the call containing this point 534 */ 535 public int getCallContaining(Point2D point) { 536 return getCallContaining(point, true); 537 } 538 539 /** 540 * Same as {@link #getCallContaining(Point2D, boolean)}, except that 541 * only the x-coordinate of the point is specified. 542 * 543 * @param x the x-coordinate to search for 544 * @param xOnScreen whether the coordinate in screen space or chromatogram 545 * space 546 * @return the index of the call containing the position x 547 */ 548 public int getCallContaining(float x, boolean xOnScreen) { 549 return getCallContaining(new Point2D.Float(x, 0), xOnScreen); 550 } 551 552 /** 553 * Synonym for {@link #getCallContaining(float, boolean)} with 554 * <code>pointOnScreen</code>=true. 555 * 556 * @param x the x-coordinate to search for 557 * @return the index of the call containing the position x 558 */ 559 public int getCallContaining(float x) { 560 return getCallContaining(x, true); 561 } 562 563 private int getSubpathContaining(float x) { 564 // XXX: this could be a binary search 565 Rectangle2D bounds; 566 if (x < subpaths[A][0].getBounds2D().getX()) 567 return 0; 568 for (int i = 0 ; i < subpaths[A].length ; i++) { 569 bounds = subpaths[A][i].getBounds2D(); 570 if (bounds.getX() <= x && x <= bounds.getMaxX()) 571 return i; 572 } 573 return subpaths[A].length - 1; 574 } 575 576 /** 577 * Returns a new AffineTransform describing the transformation 578 * from chromatogram coordinates to output coordinates. 579 */ 580 public AffineTransform getTransform() { 581 AffineTransform at = new AffineTransform(); 582 getTransformAndConcat(at); 583 return at; 584 } 585 586 /** 587 * Concatenates the chromatogram-to-output transform to the 588 * provided given AffineTransform. 589 */ 590 public void getTransformAndConcat(AffineTransform target) { 591 target.scale(horizScale, vertScale); 592 target.translate(-1.0 * getFloatOption(Option.FROM_TRACE_SAMPLE), 0.0); 593 } 594 595 /** 596 * Returns a new AffineTransform describing the transformation from 597 * output space to chromatogram space. Should be much more efficient 598 * than <code>getTransform().createInverse()</code> 599 */ 600 public AffineTransform getInvTransform() { 601 AffineTransform at = new AffineTransform(); 602 at.translate(getFloatOption(Option.FROM_TRACE_SAMPLE), 0.0); 603 at.scale(1.0 / horizScale, 1.0 / vertScale); 604 return at; 605 } 606 607 /** 608 * Draws the chromatogram onto the provided graphics context. 609 * 610 * @param g2 the Graphics2D to draw to 611 */ 612 public void drawTo(Graphics2D g2) { 613 //System.out.println("drawTo(" + g2 + ", " + fromTraceSample + ", " + toTraceSample + ")"); 614 AffineTransform origTx = g2.getTransform(); 615 //System.out.println("origTx: " + origTx); 616 Color origC = g2.getColor(); 617 Shape origClip = g2.getClip(); 618 //System.out.println("origClip: " + origClip); 619 Stroke origStroke = g2.getStroke(); 620 621 Rectangle2D clip; 622 if (origClip != null) 623 clip = origClip.getBounds2D(); 624 else 625 clip = new Rectangle(0, 0, getWidth(), getHeight()); 626 //System.out.println("clipping bounds: " + clip); 627 628 // Allow user to decide whether AA should be on 629 //g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 630 //g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); 631 632 boolean usePerShpTx = optionIsTrue(Option.USE_PER_SHAPE_TRANSFORM); 633 634 AffineTransform shapeTx = (AffineTransform) origTx.clone(); 635 getTransformAndConcat(shapeTx); 636 if (usePerShpTx) 637 g2.setTransform(new AffineTransform()); // set to identity, do tx per shape 638 else 639 g2.setTransform(shapeTx); // do tx in graphics2d object 640 //System.out.println("using g2.Tx="+g2.getTransform()); 641 //System.out.println("shapeTx="+shapeTx); 642 643 if (optionIsTrue(Option.USE_CUSTOM_STROKE)) 644 g2.setStroke((Stroke) getOption(Option.SEPARATOR_STROKE)); 645 if (!callboxesValid) 646 generateCallboxes(); 647 if (usePerShpTx) { 648 generateDrawableCallboxes(AffineTransform.getScaleInstance(shapeTx.getScaleX(), shapeTx.getScaleY())); 649 g2.translate(shapeTx.getTranslateX(), shapeTx.getTranslateY()); 650 } 651 int leftCbIdx = getCallContaining((float)clip.getX()); 652 int rightCbIdx = getCallContaining((float)clip.getMaxX()); 653 //System.out.println("Drawing calls with stroke=" + basicStrokeToString((BasicStroke)g2.getStroke())); 654 //System.out.println("Drawing " + (rightCbIdx - leftCbIdx + 1) + " visible calls from " + leftCbIdx + " to " + rightCbIdx); 655 SymbolList calls = ChromatogramTools.getDNASequence(chromat); 656 // if the chromatogram has no calls, fake it 657 if (calls.length() < 1) 658 calls = SINGLE_CALL; 659 boolean drawSep = optionIsTrue(Option.DRAW_CALL_SEPARATORS); 660 Color fill, line; 661 Line2D.Double sep = new Line2D.Double(); 662 int callIdx; 663 for (int i = leftCbIdx ; i <= rightCbIdx ; i++) { 664 callIdx = i + 1; 665 if (doDrawCallbox(calls.symbolAt(callIdx))) { 666 fill = (Color) fillColors.get(calls.symbolAt(callIdx)); 667 line = (Color) colors.get(calls.symbolAt(callIdx)); 668 if (line == null) { 669 line = Color.black; 670 fill = Color.white; 671 } 672 g2.setColor(fill); 673 if (usePerShpTx) { 674 g2.fill(drawableCallboxes[i]); 675 //System.out.println("Drawing drawableCallboxes["+i+"]="+drawableCallboxes[i]); 676 } 677 else { 678 g2.fill(callboxes[i]); 679 //System.out.println("Drawing callboxes["+i+"]="+callboxes[i]); 680 } 681 } 682 if (drawSep) { 683 g2.setColor((Color) getOption(Option.SEPARATOR_COLOR)); 684 if (usePerShpTx) 685 sep.setLine(drawableCallboxes[i].getX(), 686 drawableCallboxes[i].getY(), 687 drawableCallboxes[i].getX(), 688 drawableCallboxes[i].getMaxY()); 689 else 690 sep.setLine(callboxes[i].x, callboxes[i].y, callboxes[i].x, callboxes[i].y + callboxes[i].height); 691 //System.out.println("sep["+i+"]=(" + sep.x1 + ", " + sep.y1 + ") -> (" + sep.x2 + ", " + sep.y2 + ")"); 692 //System.out.println(" " + sep.getBounds2D()); 693 g2.draw(sep); 694 } 695 } 696 if (usePerShpTx) 697 g2.translate(-1 * shapeTx.getTranslateX(), -1 * shapeTx.getTranslateY()); 698 699 700 if (optionIsTrue(Option.USE_CUSTOM_STROKE)) 701 g2.setStroke((Stroke) getOption(Option.TRACE_STROKE)); 702 if (!subpathsValid) 703 generateSubpaths(); 704 float toTraceSample = getFloatOption(Option.TO_TRACE_SAMPLE ); 705 float fromTraceSample = getFloatOption(Option.FROM_TRACE_SAMPLE); 706 //System.out.println("Drawing from trace sample " + fromTraceSample + " to " + toTraceSample); 707 //System.out.println("Drawing traces with stroke=" + basicStrokeToString((BasicStroke)g2.getStroke())); 708 int loSubpath, hiSubpath; 709 //loSubpath = (int)Math.floor((clip.getX() / shapeTx.getScaleX() + fromTraceSample) / subpathLength); 710 loSubpath = getSubpathContaining((float) (clip.getX() / shapeTx.getScaleX() + fromTraceSample)); 711 //hiSubpath = (int)Math.floor(Math.min(clip.getMaxX() / shapeTx.getScaleX() + fromTraceSample, toTraceSample) / subpathLength); 712 hiSubpath = getSubpathContaining((float) Math.min(clip.getMaxX() / shapeTx.getScaleX() + fromTraceSample, toTraceSample)); 713 //System.out.println("Drawing subpaths ["+loSubpath+","+hiSubpath+"] of "+subpaths[0].length+" each "+subpathLength+" long"); 714 if (optionIsTrue(Option.DRAW_TRACE_A)) { 715 g2.setColor((Color) colors.get(DNATools.a())); 716 if (usePerShpTx) 717 for (int j = loSubpath ; j <= hiSubpath ; j++) 718 g2.draw(shapeTx.createTransformedShape(subpaths[A][j])); 719 else 720 for (int j = loSubpath ; j <= hiSubpath ; j++) 721 g2.draw(subpaths[A][j]); 722 } 723 if (optionIsTrue(Option.DRAW_TRACE_C)) { 724 g2.setColor((Color) colors.get(DNATools.c())); 725 if (usePerShpTx) 726 for (int j = loSubpath ; j <= hiSubpath ; j++) 727 g2.draw(shapeTx.createTransformedShape(subpaths[C][j])); 728 else 729 for (int j = loSubpath ; j <= hiSubpath ; j++) 730 g2.draw(subpaths[C][j]); 731 } 732 if (optionIsTrue(Option.DRAW_TRACE_G)) { 733 g2.setColor((Color) colors.get(DNATools.g())); 734 if (usePerShpTx) 735 for (int j = loSubpath ; j <= hiSubpath ; j++) 736 g2.draw(shapeTx.createTransformedShape(subpaths[G][j])); 737 else 738 for (int j = loSubpath ; j <= hiSubpath ; j++) 739 g2.draw(subpaths[G][j]); 740 } 741 if (optionIsTrue(Option.DRAW_TRACE_T)) { 742 g2.setColor((Color) colors.get(DNATools.t())); 743 if (usePerShpTx) 744 for (int j = loSubpath ; j <= hiSubpath ; j++) 745 g2.draw(shapeTx.createTransformedShape(subpaths[T][j])); 746 else 747 for (int j = loSubpath ; j <= hiSubpath ; j++) 748 g2.draw(subpaths[T][j]); 749 } 750 751 g2.setStroke(origStroke); 752 g2.setTransform(origTx); 753 g2.setColor(origC); 754 g2.setClip(origClip); 755 } 756 757 /** 758 * Sets a new value for the specified option. Be sure that the 759 * value is appropriate per the documentation, or you'll induce a 760 * ClassCastException somewhere else. 761 * 762 * @param opt Option to set 763 * @param value new value for the option 764 * @see Option 765 */ 766 public void setOption(Option opt, Object value) { 767 options.put(opt, value); 768 if (opt == Option.SUBPATH_LENGTH) { 769 subpathsValid = false; 770 } 771 if (opt == Option.HORIZONTAL_NONLINEAR_SCALER) { 772 subpathsValid = false; 773 callboxesValid = false; 774 drawableCallboxesValid = false; 775 if (optionIsTrue(Option.WIDTH_IS_AUTHORITATIVE)) 776 setWidth(width); 777 else 778 setHorizontalScale(horizScale); 779 } 780 } 781 782 /** 783 * Returns the current value for the specified option. 784 * @see Option 785 */ 786 public Object getOption(Option opt) { 787 return options.get(opt); 788 } 789 790 /** 791 * Helper method for converting a {@link java.lang.Boolean}-valued 792 * option into a <code>boolean</code> primitive. 793 * 794 * @param opt the {@link Option} to convert 795 * @return true if the option is enabled 796 * @throws ClassCastException when the option isn't <code>Boolean</code>-valued 797 */ 798 public boolean optionIsTrue(Option opt) throws ClassCastException { 799 if (getOption(opt) instanceof Boolean) 800 return ((Boolean)getOption(opt)).booleanValue(); 801 else 802 throw new ClassCastException("Option \""+opt+"\" is not set to a Boolean value"); 803 } 804 805 /** 806 * Helper method for converting a {@link java.lang.Number}-valued 807 * option into a <code>float</code> primitive. 808 * @param opt the {@link Option} to convert 809 * @throws ClassCastException when the option isn't <code>Number</code>-valued 810 */ 811 public float getFloatOption(Option opt) throws ClassCastException { 812 if (getOption(opt) instanceof Number) 813 return ((Number) getOption(opt)).floatValue(); 814 else 815 throw new ClassCastException("Option \""+opt+"\" is not set to a Number value"); 816 } 817 818 /** 819 * Helper method for converting a {@link java.lang.Number}-valued 820 * option into an <code>int</code> primitive. 821 * @param opt the {@link Option} to convert 822 * @throws ClassCastException when the option isn't <code>Number</code>-valued 823 */ 824 public int getIntOption(Option opt) throws ClassCastException { 825 if (getOption(opt) instanceof Number) 826 return ((Number) getOption(opt)).intValue(); 827 else 828 throw new ClassCastException("Option \""+opt+"\" is not set to a Number value"); 829 } 830 831 /** 832 * Utility method for determining whether to draw a callbox for a particular called Symbol. 833 * 834 * @param bc Symbol to evaluate 835 * @return true if this should be drawn 836 */ 837 private boolean doDrawCallbox(Symbol bc) { 838 if (bc == DNATools.a()) return optionIsTrue(Option.DRAW_CALL_A); 839 else if (bc == DNATools.c()) return optionIsTrue(Option.DRAW_CALL_C); 840 else if (bc == DNATools.g()) return optionIsTrue(Option.DRAW_CALL_G); 841 else if (bc == DNATools.t()) return optionIsTrue(Option.DRAW_CALL_T); 842 else if (DNATools.getDNA().contains(bc)) return optionIsTrue(Option.DRAW_CALL_OTHER); 843 else return false; 844 } 845 846 /** 847 * Performs a partial deep copy and invalidates regenerable structures. 848 * 849 * @return an Object that is castable to ChromatogramGraphic 850 */ 851 public Object clone() { 852 ChromatogramGraphic copy = null; 853 try { 854 copy = (ChromatogramGraphic) super.clone(); 855 copy.callboxesValid = false; 856 copy.drawableCallboxesValid = false; 857 copy.drawableCallboxes = null; 858 copy.subpathsValid = false; 859 copy.subpaths = null; 860 // copy options 861 copy.options = new HashMap(); 862 for (Iterator it = this.options.keySet().iterator() ; it.hasNext() ; ) { 863 Object next = it.next(); 864 copy.options.put(next, this.options.get(next)); 865 } 866 } catch (CloneNotSupportedException e) { 867 System.err.println(e); 868 throw new BioError("Can't happen"); 869 } 870 return copy; 871 } 872 873 /** 874 * A typesafe enumeration of the options available for configuring 875 * the behavior of a {@link ChromatogramGraphic} instance. 876 * The semantics and expected values are described with the 877 * enumerated options. 878 * 879 * @author Rhett Sutphin (<a href="http://genome.uiowa.edu/">UI CBCB</a>) 880 * @since 1.3 881 */ 882 public static class Option { 883 private String desc; 884 private static Map map = new HashMap(); 885 886 /** 887 * Create a new Obtion. 888 * 889 * @param desc option description 890 * @param def option default 891 */ 892 private Option(String desc, Object def) { 893 this.desc = desc; 894 map.put(desc, this); 895 DEFAULTS.put(this, def); 896 } 897 898 public String toString() { return desc; } 899 900 /** 901 * Looks up an <code>Option</code> instance based on its 902 * string description. 903 * @param desc the description of the desired <code>Option</code> 904 * @return the <code>Option</code> with the specified description 905 * or null if there isn't one 906 */ 907 public static final Option lookup(String desc) { 908 return (Option) map.get(desc); 909 } 910 911 /** 912 * Default values table 913 */ 914 static final Map DEFAULTS = new HashMap(); 915 916 /** 917 * Option indicating whether to fill in the callboxes for calls of 918 * nucleotide A. 919 * <p> 920 * Value type: {@link java.lang.Boolean}.<br/> 921 * Default value: <code>Boolean.TRUE</code>. 922 * </p> 923 */ 924 public static final Option DRAW_CALL_A = new Option("draw-A-calls", Boolean.TRUE); 925 /** 926 * Option indicating whether to fill in the callboxes for calls of 927 * nucleotide C. 928 * <p> 929 * Value type: {@link java.lang.Boolean}.<br/> 930 * Default value: <code>Boolean.TRUE</code>. 931 * </p> 932 */ 933 public static final Option DRAW_CALL_C = new Option("draw-C-calls", Boolean.TRUE); 934 /** 935 * Option indicating whether to fill in the callboxes for calls of 936 * nucleotide G. 937 * <p> 938 * Value type: {@link java.lang.Boolean}.<br/> 939 * Default value: <code>Boolean.TRUE</code>. 940 * </p> 941 */ 942 public static final Option DRAW_CALL_G = new Option("draw-G-calls", Boolean.TRUE); 943 /** 944 * Option indicating whether to fill in the callboxes for calls of 945 * nucleotide T. 946 * <p> 947 * Value type: {@link java.lang.Boolean}.<br/> 948 * Default value: <code>Boolean.TRUE</code>. 949 * </p> 950 */ 951 public static final Option DRAW_CALL_T = new Option("draw-T-calls", Boolean.TRUE); 952 /** 953 * Option indicating whether to fill in the callboxes for non-base calls 954 * (gaps, ambiguities). 955 * <p> 956 * Value type: {@link java.lang.Boolean}.<br/> 957 * Default value: <code>Boolean.TRUE</code>. 958 * </p> 959 */ 960 public static final Option DRAW_CALL_OTHER = new Option("draw-other-calls", Boolean.TRUE); 961 962 /** 963 * Option indicating whether to draw the chromatogram trace for 964 * nucleotide A. 965 * <p> 966 * Value type: {@link java.lang.Boolean}.<br/> 967 * Default value: <code>Boolean.TRUE</code>. 968 * </p> 969 */ 970 public static final Option DRAW_TRACE_A = new Option("draw-A-trace", Boolean.TRUE); 971 /** 972 * Option indicating whether to draw the chromatogram trace for 973 * nucleotide C. 974 * <p> 975 * Value type: {@link java.lang.Boolean}.<br/> 976 * Default value: <code>Boolean.TRUE</code>. 977 * </p> 978 */ 979 public static final Option DRAW_TRACE_C = new Option("draw-C-trace", Boolean.TRUE); 980 /** 981 * Option indicating whether to draw the chromatogram trace for 982 * nucleotide G. 983 * <p> 984 * Value type: {@link java.lang.Boolean}.<br/> 985 * Default value: <code>Boolean.TRUE</code>. 986 * </p> 987 */ 988 public static final Option DRAW_TRACE_G = new Option("draw-G-trace", Boolean.TRUE); 989 /** 990 * Option indicating whether to draw the chromatogram trace for 991 * nucleotide T. 992 * <p> 993 * Value type: {@link java.lang.Boolean}.<br/> 994 * Default value: <code>Boolean.TRUE</code>. 995 * </p> 996 */ 997 public static final Option DRAW_TRACE_T = new Option("draw-T-trace", Boolean.TRUE); 998 999 /** 1000 * Option indicating whether to draw vertical lines separating 1001 * the calls. 1002 * <p> 1003 * Value type: {@link java.lang.Boolean}.<br/> 1004 * Default value: <code>Boolean.TRUE</code>. 1005 * </p> 1006 */ 1007 public static final Option DRAW_CALL_SEPARATORS = 1008 new Option("draw-call-separators", Boolean.TRUE); 1009 /** 1010 * Option indicating the color that the call separators 1011 * should be. 1012 * <p> 1013 * Value type: {@link java.awt.Color}.<br/> 1014 * Default value: <code>Color.lightGray</code>. 1015 * </p> 1016 */ 1017 public static final Option SEPARATOR_COLOR = 1018 new Option("separator-color", Color.lightGray); 1019 1020 /** 1021 * Option indicating whether width or horizontal scale is 1022 * the authoritative measure. If the value is true, then 1023 * when the Chromatogram displayed by the graphic is changed, the 1024 * horizontal scale may be changed but the width will stay the same. 1025 * If the value is false, the width may change but the horizontal 1026 * scale will stay the same. 1027 * <p> 1028 * Value type: {@link java.lang.Boolean}.<br/> 1029 * Default value: <code>Boolean.FALSE</code>. 1030 * </p> 1031 */ 1032 public static final Option WIDTH_IS_AUTHORITATIVE = 1033 new Option("width-is-authoritative", Boolean.FALSE); 1034 /** 1035 * Option indicating whether height or vertical scale is 1036 * the authoritative measure. If the value is true, then 1037 * when the Chromatogram displayed by the graphic is changed, the 1038 * vertical scale may be changed but the height will stay the same. 1039 * If the value is false, the height may change but the vertical 1040 * scale will stay the same. 1041 * <p> 1042 * Value type: {@link java.lang.Boolean}.<br/> 1043 * Default value: <code>Boolean.TRUE</code>. 1044 * </p> 1045 */ 1046 public static final Option HEIGHT_IS_AUTHORITATIVE = 1047 new Option("height-is-authoritative", Boolean.TRUE); 1048 1049 /** 1050 * Option indicating whether to use custom strokes when 1051 * drawing traces and separators. 1052 * <p> 1053 * Value type: {@link java.lang.Boolean}.<br/> 1054 * Default value: <code>Boolean.TRUE</code>. 1055 * </p> 1056 */ 1057 public static final Option USE_CUSTOM_STROKE = 1058 new Option("use-custom-stroke", Boolean.TRUE); 1059 /** 1060 * Option providing the the stroke to use for drawing 1061 * the chromatogram traces. 1062 * <p> 1063 * Value type: {@link java.awt.Stroke}.<br/> 1064 * Default value: {@link BasicStroke} with width 1.0, cap CAP_ROUND, join JOIN_ROUND. 1065 * </p> 1066 */ 1067 public static final Option TRACE_STROKE = 1068 new Option("trace-stroke", new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 1069 /** 1070 * Option providing the the stroke to use for drawing 1071 * call separators. 1072 * <p> 1073 * Value type: {@link java.awt.Stroke}.<br/> 1074 * Default value: {@link BasicStroke} with width 1.0, default cap & join. 1075 * </p> 1076 */ 1077 public static final Option SEPARATOR_STROKE = 1078 new Option("separator-stroke", new BasicStroke(1.0f)); 1079 1080 /** 1081 * Option indicating whether to apply scaling and translation 1082 * transforms to each shape individually or to apply a single 1083 * transform to the graphics context. For putative performance 1084 * reasons, the latter is the default. However, setting this 1085 * property to true may result in more attractive output, particularly 1086 * when the horizontal and vertical scales are very different. 1087 * This value must also be set to true if using a custom stroke 1088 * while drawing into a Swing graphics context on JDK 1.3.1 on Mac OS X, 1089 * due to a nasty rendering bug on that platform. 1090 * <p> 1091 * Value type: {@link java.lang.Boolean}.<br/> 1092 * Default value: <code>Boolean.FALSE</code>. 1093 * </p> 1094 */ 1095 public static final Option USE_PER_SHAPE_TRANSFORM = 1096 new Option("use-per-shape-transform", Boolean.FALSE); 1097 1098 /** 1099 * To improve performance, the drawing objects for the chromatogram 1100 * traces are precomputed. Specifically, the traces are stored as a set 1101 * of {@link java.awt.geom.GeneralPath}s. This option indicates how 1102 * long (in trace samples) each one of these should be. Ideally, this 1103 * value would be slightly more than the average number of trace samples 1104 * visible at once in the application using the graphic. However, 1105 * constantly changing this value is counterproductive as it forces the 1106 * recalculation of the subpaths. In general, having a value that is 1107 * too small should be preferred to one that is too large. 1108 * <p> 1109 * Value type: {@link java.lang.Integer}.<br/> 1110 * Default value: <code>250</code>. 1111 * </p> 1112 */ 1113 public static final Option SUBPATH_LENGTH = 1114 new Option("subpath-length", new Integer(250)); 1115 1116 /** 1117 * Option indicating the lowest (leftmost) trace sample that should be 1118 * drawn. The sample at this (0-based) index will be drawn at x=0 in the 1119 * output chromatogram. Note that this option is reset to zero every time 1120 * {@link ChromatogramGraphic#setChromatogram} is called. 1121 * <p> 1122 * Value type: {@link java.lang.Integer}.<br/> 1123 * Default value: <code>0</code>. 1124 * </p> 1125 */ 1126 public static final Option FROM_TRACE_SAMPLE = 1127 new Option("from-trace-sample", new Integer(0)); 1128 1129 /** 1130 * Option indicating the highest (rightmost) trace sample that should be 1131 * drawn. The sample at this (0-based) index will be the last drawn in the 1132 * output chromatogram. Note that this option is reset to the length of the new 1133 * chromatogram every time {@link ChromatogramGraphic#setChromatogram} is called. 1134 * <p> 1135 * Value type: {@link java.lang.Integer}.<br/> 1136 * Default value: <code>Integer.MAX_VALUE</code>. 1137 * </p> 1138 */ 1139 public static final Option TO_TRACE_SAMPLE = 1140 new Option("to-trace-sample", new Integer(Integer.MAX_VALUE)); 1141 1142 /** 1143 * Option specifying the non-linear scaling function to apply, as 1144 * embodied in a {@link ChromatogramNonlinearScaler} object. 1145 * <p> 1146 * Value type: {@link ChromatogramNonlinearScaler}.<br/> 1147 * Default value: an instance of {@link ChromatogramNonlinearScaler.Identity}. 1148 */ 1149 public static final Option HORIZONTAL_NONLINEAR_SCALER = 1150 new Option("horiz-nonlinear-scaler", ChromatogramNonlinearScaler.Identity.getInstance()); 1151 1152 //public static final Option EXAGGERATE_SAMPLE_POINTS = 1153 // new Option("exaggerate-sample-points", Boolean.FALSE); 1154 } 1155 1156 /* 1157 private final static String basicStrokeToString(BasicStroke bs) { 1158 StringBuffer sb = new StringBuffer(bs.toString()); 1159 sb.append("[width=").append(bs.getLineWidth()); 1160 sb.append("; EndCap="); 1161 switch (bs.getEndCap()) { 1162 case BasicStroke.CAP_BUTT: sb.append("CAP_BUTT"); break; 1163 case BasicStroke.CAP_ROUND: sb.append("CAP_ROUND"); break; 1164 case BasicStroke.CAP_SQUARE: sb.append("CAP_SQUARE"); break; 1165 } 1166 sb.append("; Join="); 1167 switch (bs.getLineJoin()) { 1168 case BasicStroke.JOIN_BEVEL: sb.append("JOIN_BEVEL"); break; 1169 case BasicStroke.JOIN_MITER: sb.append("JOIN_MITER"); break; 1170 case BasicStroke.JOIN_ROUND: sb.append("JOIN_ROUND"); break; 1171 } 1172 sb.append("; MiterLimit=").append(bs.getMiterLimit()).append(']'); 1173 return sb.toString(); 1174 } 1175 */ 1176}