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 &lt; 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}