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.gui.sequence; 023 024import java.awt.Dimension; 025import java.awt.Font; 026import java.awt.Graphics; 027import java.awt.Graphics2D; 028import java.awt.Insets; 029import java.awt.RenderingHints; 030import java.awt.Shape; 031import java.awt.event.MouseAdapter; 032import java.awt.event.MouseEvent; 033import java.awt.event.MouseListener; 034import java.awt.event.MouseMotionListener; 035import java.awt.geom.AffineTransform; 036import java.awt.geom.Point2D; 037import java.awt.geom.Rectangle2D; 038import java.beans.PropertyChangeEvent; 039import java.beans.PropertyChangeListener; 040import java.beans.PropertyChangeSupport; 041import java.io.Serializable; 042import java.util.ArrayList; 043 044import javax.swing.JComponent; 045import javax.swing.SwingConstants; 046 047import org.biojava.bio.seq.FeatureHolder; 048import org.biojava.bio.symbol.Location; 049import org.biojava.bio.symbol.LocationTools; 050import org.biojava.bio.symbol.RangeLocation; 051import org.biojava.bio.symbol.SymbolList; 052import org.biojava.utils.ChangeAdapter; 053import org.biojava.utils.ChangeEvent; 054import org.biojava.utils.ChangeListener; 055import org.biojava.utils.ChangeSupport; 056import org.biojava.utils.ChangeType; 057import org.biojava.utils.ChangeVetoException; 058import org.biojava.utils.Changeable; 059 060/** 061 * A panel that displays a Sequence. 062 * <p> 063 * A SequencePanel can either display the sequence from left-to-right 064 * (HORIZONTAL) or from top-to-bottom (VERTICAL). It has an associated scale 065 * which is the number of pixels per symbol. It also has a lines property that 066 * controls how to wrap the sequence off one end and onto the other. 067 * <p> 068 * Each line in the SequencePanel is broken down into a list of strips, 069 * each rendered by an individual SequenceRenderer object. 070 * You could add a SequenceRenderer that draws on genes, another that 071 * draws repeats and another that prints out the DNA sequence. They are 072 * responsible for rendering their view of the sequence in the place that the 073 * SequencePanel positions them. 074 * 075 * @author Thomas Down 076 * @author Matthew Pocock 077 * @author David Huen 078 */ 079public class SequencePanel 080 extends 081 JComponent 082 implements 083 SwingConstants, 084 SequenceRenderContext, 085 Changeable 086{ 087 public static final ChangeType RENDERER = new ChangeType( 088 "The renderer for this SequencePanel has changed", 089 "org.biojava.bio.gui.sequence.SequencePanel", 090 "RENDERER", 091 SequenceRenderContext.LAYOUT 092 ); 093 094 private SymbolList sequence; 095 private RangeLocation range; 096 private int direction; 097 private double scale; 098 private double pixelOffset; 099 100 private SequenceRenderContext.Border leadingBorder; 101 private SequenceRenderContext.Border trailingBorder; 102 103 private SequenceRenderer renderer; 104 private RendererMonitor theMonitor; 105 106 private RenderingHints hints = null; 107 108 private transient ChangeSupport changeSupport = null; 109 110 private SequenceViewerSupport svSupport = new SequenceViewerSupport(); 111 112 /** 113 * Use this to switch on effects like Anti-aliasing etc 114 * @param hints the desired rendering properties 115 */ 116 public void setRenderingHints(RenderingHints hints){ 117 this.hints = hints; 118 } 119 120 /** 121 * @return the current rendering properties 122 */ 123 public RenderingHints getRenderingHints(){ 124 return hints; 125 } 126 127 private MouseListener mouseListener = new MouseAdapter() { 128 public void mouseClicked(MouseEvent me) { 129 if(!isActive()) { 130 return; 131 } 132 133 int [] dist = calcDist(); 134 me.translatePoint(+dist[0], +dist[1]); 135 SequenceViewerEvent sve = renderer.processMouseEvent( 136 SequencePanel.this, 137 me, 138 new ArrayList() 139 ); 140 me.translatePoint(-dist[0], -dist[1]); 141 svSupport.fireMouseClicked(sve); 142 } 143 144 public void mousePressed(MouseEvent me) { 145 if(!isActive()) { 146 return; 147 } 148 149 int [] dist = calcDist(); 150 me.translatePoint(+dist[0], +dist[1]); 151 SequenceViewerEvent sve = renderer.processMouseEvent( 152 SequencePanel.this, 153 me, 154 new ArrayList() 155 ); 156 me.translatePoint(-dist[0], -dist[1]); 157 svSupport.fireMousePressed(sve); 158 } 159 160 public void mouseReleased(MouseEvent me) { 161 if(!isActive()) { 162 return; 163 } 164 165 int [] dist = calcDist(); 166 me.translatePoint(+dist[0], +dist[1]); 167 SequenceViewerEvent sve = renderer.processMouseEvent( 168 SequencePanel.this, 169 me, 170 new ArrayList() 171 ); 172 me.translatePoint(-dist[0], -dist[1]); 173 svSupport.fireMouseReleased(sve); 174 } 175 }; 176 public void addSequenceViewerListener(SequenceViewerListener svl) { 177 svSupport.addSequenceViewerListener(svl); 178 } 179 public void removeSequenceViewerListener(SequenceViewerListener svl) { 180 svSupport.removeSequenceViewerListener(svl); 181 } 182 183 private SequenceViewerMotionSupport svmSupport = new SequenceViewerMotionSupport(); 184 private MouseMotionListener mouseMotionListener = new MouseMotionListener() { 185 public void mouseDragged(MouseEvent me) { 186 if(!isActive()) { 187 return; 188 } 189 190 int [] dist = calcDist(); 191 me.translatePoint(+dist[0], +dist[1]); 192 SequenceViewerEvent sve = renderer.processMouseEvent( 193 SequencePanel.this, 194 me, 195 new ArrayList() 196 ); 197 me.translatePoint(-dist[0], -dist[1]); 198 svmSupport.fireMouseDragged(sve); 199 } 200 201 public void mouseMoved(MouseEvent me) { 202 if(!isActive()) { 203 return; 204 } 205 206 int [] dist = calcDist(); 207 me.translatePoint(+dist[0], +dist[1]); 208 SequenceViewerEvent sve = renderer.processMouseEvent( 209 SequencePanel.this, 210 me, 211 new ArrayList() 212 ); 213 me.translatePoint(-dist[0], -dist[1]); 214 svmSupport.fireMouseMoved(sve); 215 } 216 }; 217 public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml) { 218 svmSupport.addSequenceViewerMotionListener(svml); 219 } 220 public void removeSequenceViewerMotionListener(SequenceViewerMotionListener svml) { 221 svmSupport.removeSequenceViewerMotionListener(svml); 222 } 223 224 protected boolean hasChangeListeners() { 225 return changeSupport != null; 226 } 227 228 protected ChangeSupport getChangeSupport(ChangeType ct) { 229 if(changeSupport != null) { 230 return changeSupport; 231 } 232 233 synchronized(this) { 234 if(changeSupport == null) { 235 changeSupport = new ChangeSupport(); 236 } 237 238 return changeSupport; 239 } 240 } 241 242 protected boolean hasListeners() { 243 return changeSupport != null; 244 } 245 246 public void addChangeListener(ChangeListener cl) { 247 addChangeListener(cl, ChangeType.UNKNOWN); 248 } 249 250 public void addChangeListener(ChangeListener cl, ChangeType ct) { 251 ChangeSupport cs = getChangeSupport(ct); 252 cs.addChangeListener(cl, ct); 253 } 254 255 public void removeChangeListener(ChangeListener cl) { 256 removeChangeListener(cl, ChangeType.UNKNOWN); 257 } 258 259 public void removeChangeListener(ChangeListener cl, ChangeType ct) { 260 if(hasListeners()) { 261 ChangeSupport cs = getChangeSupport(ct); 262 cs.removeChangeListener(cl, ct); 263 } 264 } 265 266 public boolean isUnchanging(ChangeType ct) { 267 ChangeSupport cs = getChangeSupport(ct); 268 return cs.isUnchanging(ct); 269 } 270 271 private ChangeListener layoutListener = new ChangeAdapter() { 272 public void postChange(ChangeEvent ce) { 273 System.err.println("Layout event"); 274 resizeAndValidate(); 275 } 276 }; 277 private ChangeListener repaintListener = new ChangeAdapter() { 278 public void postChange(ChangeEvent ce) { 279 System.err.println("Repaint event for " + hashCode()); 280 repaint(); 281 } 282 }; 283 284 /** 285 * Initializer. 286 */ 287 288 { 289 direction = HORIZONTAL; 290 scale = 12.0; 291 pixelOffset = 0.0; 292 293 theMonitor = new RendererMonitor(); 294 leadingBorder = new SequenceRenderContext.Border(); 295 trailingBorder = new SequenceRenderContext.Border(); 296 } 297 298 /** 299 * Create a new SequencePanel. 300 */ 301 public SequencePanel() { 302 super(); 303 if(getFont() == null) { 304 setFont(new Font("serif", Font.PLAIN, 12)); 305 } 306 this.addPropertyChangeListener(theMonitor); 307 this.addMouseListener(mouseListener); 308 this.addMouseMotionListener(mouseMotionListener); 309 } 310 311 /** 312 * Set the SymboList to be rendered. This symbol list will be passed onto the 313 * SequenceRenderer instances registered with this SequencePanel. 314 * 315 * @param s the SymboList to render 316 */ 317 public void setSequence(SymbolList s) { 318 SymbolList oldSequence = sequence; 319 if(oldSequence != null) { 320 oldSequence.removeChangeListener(layoutListener, ChangeType.UNKNOWN); 321 } 322 this.sequence = s; 323 if(s != null) { 324 sequence.addChangeListener(layoutListener, ChangeType.UNKNOWN); 325 } 326 327 resizeAndValidate(); 328 firePropertyChange("sequence", oldSequence, s); 329 } 330 331 public SymbolList getSequence() { 332 return sequence; 333 } 334 335 /** 336 * Retrieve the currently rendered SymbolList 337 * 338 * @return the current SymbolList 339 */ 340 public SymbolList getSymbols() { 341 return sequence; 342 } 343 344 public FeatureHolder getFeatures() { 345 if(sequence instanceof FeatureHolder) { 346 return (FeatureHolder) sequence; 347 } else { 348 return FeatureHolder.EMPTY_FEATURE_HOLDER; 349 } 350 } 351 352 public void setRange(RangeLocation range) { 353 RangeLocation oldRange = this.range; 354 this.range = range; 355 resizeAndValidate(); 356 firePropertyChange("range", oldRange, range); 357 } 358 359 public RangeLocation getRange() { 360 return this.range; 361 } 362 363 /** 364 * Set the direction that this SequencePanel renders in. The direction can be 365 * one of HORIZONTAL or VERTICAL. Once the direction is set, the display will 366 * redraw. HORIZONTAL represents left-to-right rendering. VERTICAL represents 367 * AceDB-style vertical rendering. 368 * 369 * @param dir the new rendering direction 370 */ 371 public void setDirection(int dir) 372 throws IllegalArgumentException { 373 if(dir != HORIZONTAL && dir != VERTICAL) { 374 throw new IllegalArgumentException( 375 "Direction must be either HORIZONTAL or VERTICAL" 376 ); 377 } 378 int oldDirection = direction; 379 direction = dir; 380 resizeAndValidate(); 381 firePropertyChange("direction", oldDirection, direction); 382 } 383 384 /** 385 * Retrieve the current rendering direction. 386 * 387 * @return the rendering direction (one of HORIZONTAL and VERTICAL) 388 */ 389 public int getDirection() { 390 return direction; 391 } 392 393 /** 394 * Set the scale. 395 * <p> 396 * The scale parameter is interpreted as the number of pixels per symbol. This 397 * may take on a wide range of values - for example, to render the symbols as 398 * text, you will need a scale of > 8, where as to render chromosome 1 you 399 * will want a scale < 0.00000001 400 * 401 * @param scale the new pixels-per-symbol ratio 402 */ 403 public void setScale(double scale) { 404 double oldScale = this.scale; 405 this.scale = scale; 406 resizeAndValidate(); 407 firePropertyChange("scale", oldScale, scale); 408 } 409 410 /** 411 * Retrieve the current scale. 412 * 413 * @return the number of pixels used to render one symbol 414 */ 415 public double getScale() { 416 return scale; 417 } 418 419 /** 420 * Retrieve the object that encapsulates the leading border area - the space 421 * before sequence information is rendered. 422 * 423 * @return a SequenceRenderContext.Border instance 424 */ 425 public SequenceRenderContext.Border getLeadingBorder() { 426 return leadingBorder; 427 } 428 429 /** 430 * Retrieve the object that encapsulates the trailing border area - the space 431 * after sequence information is rendered. 432 * 433 * @return a SequenceRenderContext.Border instance 434 */ 435 public SequenceRenderContext.Border getTrailingBorder() { 436 return trailingBorder; 437 } 438 439 /** 440 * Paint this component. 441 * <p> 442 * This calls the paint method of the currently registered SequenceRenderer 443 * after setting up the graphics appropriately. 444 */ 445 public synchronized void paintComponent(Graphics g) { 446 if(!isActive()) { 447 return; 448 } 449 450 Graphics2D g2 = (Graphics2D) g; 451 if(hints != null){ 452 g2.setRenderingHints(hints); 453 } 454 super.paintComponent(g); 455 456 457 AffineTransform oldTransform = g2.getTransform(); 458 //Rectangle2D currentClip = g2.getClip().getBounds2D(); 459 460 Insets insets = getInsets(); 461 462 if (isOpaque()) 463 { 464 g2.setPaint(getBackground()); 465 g2.fillRect(0, 0, getWidth(), getHeight()); 466 } 467 468 // do a transform to offset drawing to the neighbourhood of zero. 469 adjustOffset(sequenceToGraphics(range.getMin())); 470 471 double minAcross = sequenceToGraphics(range.getMin()) - 472 renderer.getMinimumLeader(this); 473 double maxAcross = sequenceToGraphics(range.getMax()) + 1 + 474 renderer.getMinimumTrailer(this); 475 double alongDim = maxAcross - minAcross; 476 double depth = renderer.getDepth(this); 477 Rectangle2D.Double clip = new Rectangle2D.Double(); 478 if (direction == HORIZONTAL) { 479 clip.x = minAcross; 480 clip.y = 0.0; 481 clip.width = alongDim; 482 clip.height = depth; 483 g2.translate(leadingBorder.getSize() - minAcross + insets.left, insets.top); 484 } else { 485 clip.x = 0.0; 486 clip.y = minAcross; 487 clip.width = depth; 488 clip.height = alongDim; 489 g2.translate(insets.left, leadingBorder.getSize() - minAcross + insets.top); 490 } 491 492 Shape oldClip = g2.getClip(); 493 g2.clip(clip); 494 renderer.paint(g2, new PaintContext()); 495 g2.setClip(oldClip); 496 g2.setTransform(oldTransform); 497 } 498 499 public void setRenderer(SequenceRenderer r) 500 throws ChangeVetoException { 501 if(hasChangeListeners()) { 502 ChangeEvent ce = new ChangeEvent( 503 this, 504 RENDERER, 505 r, 506 this.renderer 507 ); 508 ChangeSupport cs = getChangeSupport(RENDERER); 509 synchronized(cs) { 510 cs.firePreChangeEvent(ce); 511 _setRenderer(r); 512 cs.firePostChangeEvent(ce); 513 } 514 } else { 515 _setRenderer(r); 516 } 517 resizeAndValidate(); 518 } 519 520 protected void _setRenderer(SequenceRenderer r) { 521 if( (this.renderer != null) && (this.renderer instanceof Changeable) ) { 522 Changeable c = (Changeable) this.renderer; 523 c.removeChangeListener(layoutListener, SequenceRenderContext.LAYOUT); 524 c.removeChangeListener(repaintListener, SequenceRenderContext.REPAINT); 525 } 526 527 this.renderer = r; 528 529 if( (r != null) && (r instanceof Changeable) ) { 530 Changeable c = (Changeable) r; 531 c.addChangeListener(layoutListener, SequenceRenderContext.LAYOUT); 532 c.addChangeListener(repaintListener, SequenceRenderContext.REPAINT); 533 } 534 } 535 536 private void adjustOffset(double newOrigin) { 537 pixelOffset -= newOrigin; 538 } 539 540 public double sequenceToGraphics(int seqPos) { 541 return ((double) (seqPos-1)) * scale + pixelOffset; 542 } 543 544 public int graphicsToSequence(double gPos) { 545 return ((int) ((gPos - pixelOffset) / scale)) + 1; 546 } 547 548 public int graphicsToSequence(Point2D point) { 549 if(direction == HORIZONTAL) { 550 return graphicsToSequence(point.getX()); 551 } else { 552 return graphicsToSequence(point.getY()); 553 } 554 } 555 556 public void resizeAndValidate() { 557 //System.out.println("resizeAndValidate starting"); 558 Dimension mind = null; 559 Dimension maxd = null; 560 561 if(!isActive()) { 562 // System.out.println("No sequence"); 563 // no sequence - collapse down to no size at all 564 leadingBorder.setSize(0.0); 565 trailingBorder.setSize(0.0); 566 mind = maxd = new Dimension(0, 0); 567 } else { 568 double minAcross = sequenceToGraphics(range.getMin()); 569 double maxAcross = sequenceToGraphics(range.getMax()); 570 double maxDropAcross = sequenceToGraphics(range.getMax() - 1); 571 double lb = renderer.getMinimumLeader(this); 572 double tb = renderer.getMinimumTrailer(this) + trailingBorder.getSize(); 573 double alongDim = 574 (maxAcross - minAcross) + 575 lb + tb; 576 double alongDropDim = 577 (maxDropAcross - minAcross) + 578 lb + tb; 579 double depth = renderer.getDepth(this); 580 if(direction == HORIZONTAL) { 581 mind = new Dimension((int) Math.ceil(alongDropDim), (int) Math.ceil(depth)); 582 maxd = new Dimension((int) Math.ceil(alongDim), (int) Math.ceil(depth)); 583 } else { 584 mind = new Dimension((int) Math.ceil(depth), (int) Math.ceil(alongDropDim)); 585 maxd = new Dimension((int) Math.ceil(depth), (int) Math.ceil(alongDim)); 586 } 587 } 588 589 setMinimumSize(mind); 590 setPreferredSize(maxd); 591 setMaximumSize(maxd); 592 revalidate(); 593 // System.out.println("resizeAndValidate ending"); 594 } 595 596 private class RendererMonitor implements PropertyChangeListener { 597 public void propertyChange(PropertyChangeEvent ev) { 598 repaint(); 599 } 600 } 601 602 protected int [] calcDist() { 603 double minAcross = sequenceToGraphics(range.getMin()) - 604 renderer.getMinimumLeader(this); 605 Insets insets = getInsets(); 606 607 int [] dist = new int[2]; 608 if(direction == HORIZONTAL) { 609 dist[0] = (int) minAcross - insets.left; 610 dist[1] = -insets.top; 611 } else { 612 dist[0] = -insets.left; 613 dist[1] = (int) minAcross - insets.top; 614 } 615 616 return dist; 617 } 618 619 protected boolean isActive() { 620 return 621 (sequence != null) && 622 (renderer != null) && 623 (range != null); 624 } 625 626 public class Border 627 implements Serializable, SwingConstants { 628 protected final PropertyChangeSupport pcs; 629 private double size = 0.0; 630 private int alignment = CENTER; 631 632 public double getSize() { 633 return size; 634 } 635 636 public int getAlignment() { 637 return alignment; 638 } 639 640 public void setAlignment(int alignment) 641 throws IllegalArgumentException 642 { 643 if (alignment == LEADING || alignment == TRAILING || alignment == CENTER) { 644 int old = this.alignment; 645 this.alignment = alignment; 646 pcs.firePropertyChange("alignment", old, alignment); 647 } else { 648 throw new IllegalArgumentException( 649 "Alignment must be one of the constants LEADING, TRAILING or CENTER" 650 ); 651 } 652 } 653 654 private Border() { 655 alignment = CENTER; 656 pcs = new PropertyChangeSupport(this); 657 } 658 659 public void addPropertyChangeListener(PropertyChangeListener listener) { 660 pcs.addPropertyChangeListener(listener); 661 } 662 663 public void removePropertyChangeListener(PropertyChangeListener listener) { 664 pcs.removePropertyChangeListener(listener); 665 } 666 } 667 668 private boolean eq(Object a, Object b) { 669 if (a == null || b == null) { 670 return a == b; 671 } else { 672 return a.equals(b); 673 } 674 } 675 676 677 public boolean equals(Object o) { 678 if (! (o instanceof SequencePanel)) { 679 return false; 680 } 681 682 SequencePanel osp = (SequencePanel) o; 683 return (eq(getSymbols(), osp.getSymbols()) && eq(getRange(), osp.getRange())); 684 } 685 686 public int hashCode() { 687 int hc = 653; 688 SymbolList sl = getSymbols(); 689 if (sl != null) { 690 hc = hc ^ sl.hashCode(); 691 } 692 693 Location l = getRange(); 694 if (l != null) { 695 hc = hc ^ l.hashCode(); 696 } 697 698 return hc; 699 } 700 701 private class PaintContext 702 implements SequenceRenderContext 703 { 704 private final RangeLocation range; 705 706 public PaintContext() { 707 this.range = (RangeLocation) LocationTools.intersection( 708 SequencePanel.this.getRange(), 709 new RangeLocation(1, SequencePanel.this.getSequence().length())); 710 } 711 712 public RangeLocation getRange() 713 { 714 return range; 715 } 716 717 public int getDirection() 718 { 719 return SequencePanel.this.getDirection(); 720 } 721 722 public double getScale() 723 { 724 return SequencePanel.this.getScale(); 725 } 726 727 public double sequenceToGraphics(int i) 728 { 729 return SequencePanel.this.sequenceToGraphics(i); 730 } 731 732 public int graphicsToSequence(double d) 733 { 734 return SequencePanel.this.graphicsToSequence(d); 735 } 736 737 public int graphicsToSequence(Point2D point) 738 { 739 return SequencePanel.this.graphicsToSequence(point); 740 } 741 742 public SymbolList getSymbols() 743 { 744 return SequencePanel.this.getSymbols(); 745 } 746 747 public FeatureHolder getFeatures() 748 { 749 return SequencePanel.this.getFeatures(); 750 } 751 752 public SequenceRenderContext.Border getLeadingBorder() 753 { 754 return SequencePanel.this.getLeadingBorder(); 755 } 756 757 public SequenceRenderContext.Border getTrailingBorder() 758 { 759 return SequencePanel.this.getTrailingBorder(); 760 } 761 762 public Font getFont() 763 { 764 return SequencePanel.this.getFont(); 765 } 766 } 767}