/* * BioJava development code * * This code may be freely distributed and modified under the * terms of the GNU Lesser General Public Licence. This should * be distributed with the code. If you do not have a copy, * see: * * http://www.gnu.org/copyleft/lesser.html * * Copyright for this code is held jointly by the individual * authors. These should be listed in @author doc comments. * * For more information on the BioJava project and its aims, * or to join the biojava-l mailing list, visit the home page * at: * * http://www.biojava.org/ * */ package org.biojava.bio.gui.sequence; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.Serializable; import java.util.ArrayList; import javax.swing.JComponent; import org.biojava.bio.seq.FeatureHolder; import org.biojava.bio.seq.Sequence; import org.biojava.bio.symbol.RangeLocation; import org.biojava.bio.symbol.SymbolList; import org.biojava.utils.ChangeAdapter; import org.biojava.utils.ChangeEvent; import org.biojava.utils.ChangeListener; import org.biojava.utils.ChangeSupport; import org.biojava.utils.ChangeType; import org.biojava.utils.ChangeVetoException; import org.biojava.utils.Changeable; /** *

A PairwiseSequencePanel is a panel that displays a * pair of sequences; one sequence (the primary) may be either * left-to-right (HORIZONTAL) or from top-to-bottom (VERTICAL). The * other sequence (secondary) will then occupy the remaining * direction. It has an associated scale which is the number of pixels * per symbol and applies to both sequences. The leading and trailing * borders apply to the primary sequence only.

* *

The primary purpose of this component is to provide a means for * representing graphically comparisons between two sequences. This * could be anything from traditional dotplots (or variants created * with lines) to a more complex layered plot involving superimposed * renderers.

* *

Each sequence has a translation which is the number of * Symbols to skip before rendering starts. In order to * produce a scrolling effect, the setSymbolTranslation * or setSecondarySymbolTranslation method may be hooked * up to an Adjustable such as JScrollBar or * to an event listener.

* *

The exact number of Symbols rendered in each * sequence depends on the dimensions of the panel and the * scale. Resizing the panel will cause the number of * Symbols rendered to change accordingly.

* *

The panel will fill its background to the Color * defined by the setBackground() method provided that it * has been defined as opaque using setOpaque().

* *

The change event handling code is based on the original panel * and other BioJava components by Matthew and Thomas.

* * @author Keith James * @author Matthew Pocock * @since 1.2 */ public class PairwiseSequencePanel extends JComponent implements PairwiseRenderContext, Changeable, Serializable { /** * Constant RENDERER is a ChangeType * which indicates a change to the renderer, requiring a layout * update. */ public static final ChangeType RENDERER = new ChangeType("The renderer for this PairwiseSequencePanel has changed", "org.biojava.bio.gui.sequence.PairwiseSequencePanel", "RENDERER", SequenceRenderContext.LAYOUT); /** * Constant TRANSLATION is a ChangeType * which indicates a change to the translation, requiring a paint * update. */ public static final ChangeType TRANSLATION = new ChangeType("The translation for this PairwiseSequencePanel has changed", "org.biojava.bio.gui.sequence.PairwiseSequencePanel", "TRANSLATION", SequenceRenderContext.REPAINT); // The query sequence to be rendered private Sequence sequence; // The number of residues to skip before starting to render private int translation; // The rendering direction (HORIZONTAL or VERTICAL) private int direction; // The subject sequence to be rendered private Sequence secSequence; // The number of residues to skip before starting to render private int secTranslation; // The rendering direction (HORIZONTAL or VERTICAL) private int secDirection; // The rendering scale in pixels per residue private double scale; // The homology renderer private PairwiseSequenceRenderer renderer; // RenderingHints to be used by renderers private RenderingHints hints; // The rendering context borders private SequenceRenderContext.Border leadingBorder; private SequenceRenderContext.Border trailingBorder; // Listens for bound property changes which require a repaint // afterwards private PropertyChangeListener propertyListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent pce) { repaint(); } }; // ChangeSupport helper for BioJava ChangeListeners private transient ChangeSupport changeSupport = null; // Listens for BioJava changes which will require a repaint // afterwards private ChangeListener repaintListener = new ChangeAdapter() { public void postChange(ChangeEvent ce) { repaint(); } }; // Listens for BioJava changes which will require a revalidation // afterwards private ChangeListener layoutListener = new ChangeAdapter() { public void postChange(ChangeEvent ce) { revalidate(); } }; // SequenceViewerSupport helper for BioJava SequenceViewerListeners private SequenceViewerSupport svSupport = new SequenceViewerSupport(); // Listens for mouse click events private MouseListener mouseListener = new MouseAdapter() { public void mouseClicked(MouseEvent me) { if (! isActive()) return; Insets insets = getInsets(); me.translatePoint(-insets.left, -insets.top); SequenceViewerEvent sve = renderer.processMouseEvent(PairwiseSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svSupport.fireMouseClicked(sve); } public void mousePressed(MouseEvent me) { if (! isActive()) return; Insets insets = getInsets(); me.translatePoint(-insets.left, -insets.top); SequenceViewerEvent sve = renderer.processMouseEvent(PairwiseSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svSupport.fireMousePressed(sve); } public void mouseReleased(MouseEvent me) { if (! isActive()) return; Insets insets = getInsets(); me.translatePoint(-insets.left, -insets.top); SequenceViewerEvent sve = renderer.processMouseEvent(PairwiseSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svSupport.fireMouseReleased(sve); } }; // SequenceViewerMotionSupport helper for BioJava // SequenceViewerMotionListeners private SequenceViewerMotionSupport svmSupport = new SequenceViewerMotionSupport(); // Listens for mouse movement events private MouseMotionListener mouseMotionListener = new MouseMotionListener() { public void mouseDragged(MouseEvent me) { if (! isActive()) return; Insets insets = getInsets(); me.translatePoint(-insets.left, -insets.top); SequenceViewerEvent sve = renderer.processMouseEvent(PairwiseSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svmSupport.fireMouseDragged(sve); } public void mouseMoved(MouseEvent me) { if (! isActive()) return; Insets insets = getInsets(); me.translatePoint(-insets.left, -insets.top); SequenceViewerEvent sve = renderer.processMouseEvent(PairwiseSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svmSupport.fireMouseMoved(sve); } }; /** * Creates a new PairwiseSequencePanel with the * default settings (primary sequence direction HORIZONTAL, scale * 10.0 pixels per symbol, symbol translation 0, secondary symbol * translation 0, leading border 0.0, trailing border 0.0, 12 * point sanserif font). */ public PairwiseSequencePanel() { super(); // Direction of the primary sequence direction = HORIZONTAL; scale = 10.0; translation = 0; secTranslation = 0; leadingBorder = new SequenceRenderContext.Border(); trailingBorder = new SequenceRenderContext.Border(); leadingBorder.setSize(0.0); trailingBorder.setSize(0.0); hints = new RenderingHints(null); this.addPropertyChangeListener(propertyListener); this.addMouseListener(mouseListener); this.addMouseMotionListener(mouseMotionListener); } /** * getSequence returns the entire * Sequence currently being rendered. * * @return a Sequence. */ public Sequence getSequence() { return sequence; } /** * setSequence sets the Sequence * to be rendered. * * @param sequence a Sequence. */ public void setSequence(Sequence sequence) { Sequence prevSequence = sequence; // Remove out listener from the sequence, if necessary if (prevSequence != null) prevSequence.removeChangeListener(layoutListener); this.sequence = sequence; // Add our listener to the sequence, if necessary if (sequence != null) sequence.addChangeListener(layoutListener); firePropertyChange("sequence", prevSequence, sequence); resizeAndValidate(); } /** * getSecondarySequence returns the entire secondary * Sequence currently being rendered. * * @return a Sequence. */ public Sequence getSecondarySequence() { return secSequence; } /** * setSecondarySequence sets the secondary * Sequence to be rendered. * * @param sequence a Sequence. */ public void setSecondarySequence(Sequence sequence) { Sequence prevSecSequence = secSequence; // Remove out listener from the sequence, if necessary if (prevSecSequence != null) prevSecSequence.removeChangeListener(layoutListener); secSequence = sequence; // Add our listener to the sequence, if necessary if (sequence != null) sequence.addChangeListener(layoutListener); firePropertyChange("secSequence", prevSecSequence, sequence); resizeAndValidate(); } /** * getSymbols returns all of the Symbols * belonging to the currently rendered Sequence. * * @return a SymbolList. */ public SymbolList getSymbols() { return sequence; } /** * getSecondarySymbols returns all of the * Symbols belonging to the currently rendered * secondary Sequence. * * @return a SymbolList. */ public SymbolList getSecondarySymbols() { return secSequence; } /** * getFeatures returns all of the * Features belonging to the currently rendered * Sequence. * * @return a FeatureHolder. */ public FeatureHolder getFeatures() { return sequence; } /** * getSecondaryFeatures returns all of the * Features belonging to the currently rendered * secondary Sequence. * * @return a FeatureHolder. */ public FeatureHolder getSecondaryFeatures() { return secSequence; } /** * getRange returns a RangeLocation * representing the region of the sequence currently being * rendered. This is calculated from the size of the * PairwiseSequencePanel, the current rendering * translation and the current scale. The value will therefore * change when the PairwiseSequencePanel is resized * or "scrolled" by changing the translation. * * @return a RangeLocation. */ public RangeLocation getRange() { int visibleSymbols = getVisibleSymbolCount(); // This is a fudge as we have to return a RangeLocation, which // can not have start == end if (visibleSymbols == 0) return new RangeLocation(translation + 1, translation + 2); else return new RangeLocation(translation + 1, visibleSymbols); } /** * getSecondaryRange returns a * RangeLocation representing the region of the * secondary sequence currently being rendered. This is calculated * from the size of the PairwiseSequencePanel, the * current rendering translation and the current scale. The value * will therefore change when the * PairwiseSequencePanel is resized or "scrolled" by * changing the translation. * * @return a RangeLocation. */ public RangeLocation getSecondaryRange() { int visibleSecSymbols = getVisibleSecondarySymbolCount(); // This is a fudge as we have to return a RangeLocation, which // can not have start == end if (visibleSecSymbols == 0) return new RangeLocation(secTranslation + 1, secTranslation + 2); else return new RangeLocation(secTranslation + 1, visibleSecSymbols); } /** * getDirection returns the direction in which this * context expects the sequence to be rendered - HORIZONTAL or * VERTICAL. * * @return an int. */ public int getDirection() { return direction; } /** * setDirection sets the direction in which this * context will render the sequence - HORIZONTAL or VERTICAL. * * @param direction an int. * * @exception IllegalArgumentException if an invalid direction is * used. */ public void setDirection(int direction) throws IllegalArgumentException { int prevDirection = direction; int prevSecDirection = secDirection; if (direction == HORIZONTAL) secDirection = VERTICAL; else if (direction == VERTICAL) secDirection = HORIZONTAL; else throw new IllegalArgumentException("Direction must be either HORIZONTAL or VERTICAL"); this.direction = direction; firePropertyChange("direction", prevDirection, direction); firePropertyChange("secDirection", prevSecDirection, secDirection); resizeAndValidate(); } /** * getSecondaryDirection returns the direction in * which this context expects the secondary sequence to be * rendered - HORIZONTAL or VERTICAL. * * @return an int. */ public int getSecondaryDirection() { return secDirection; } /** * getScale returns the scale in pixels per * Symbol. * * @return a double. */ public double getScale() { return scale; } /** * setScale sets the scale in pixels per * Symbol. * * @param scale a double. */ public void setScale(double scale) { double prevScale = this.scale; this.scale = scale; firePropertyChange("scale", prevScale, scale); resizeAndValidate(); } /** * getSymbolTranslation returns the current * translation in Symbols which will be applied when * rendering. The sequence will be rendered starting at this * translation. Values may be from 0 to the length of the rendered * sequence. * * @return an int. */ public int getSymbolTranslation() { return translation; } /** * setSymbolTranslation sets the translation in * Symbols which will be applied when rendering. The * sequence will be rendered starting at that translation. Values * may be from 0 to the length of the rendered sequence. * * @param translation an int. * * @exception IndexOutOfBoundsException if the translation is * greater than the sequence length. */ public void setSymbolTranslation(int translation) throws IndexOutOfBoundsException { if (translation >= sequence.length()) throw new IndexOutOfBoundsException("Tried to set symbol translation offset equal to or greater than SymbolList length"); int prevTranslation = this.translation; if (hasListeners()) { ChangeSupport cs = getChangeSupport(TRANSLATION); ChangeEvent ce = new ChangeEvent(this, TRANSLATION); cs.firePostChangeEvent(ce); this.translation = translation; cs.firePostChangeEvent(ce); } else { this.translation = translation; } firePropertyChange("translation", prevTranslation, translation); resizeAndValidate(); } /** * getSecondarySymbolTranslation returns the current * translation in Symbols which will be applied when * rendering. The secondary sequence will be rendered starting at * this translation. Values may be from 0 to the length of the * rendered sequence. * * @return an int. */ public int getSecondarySymbolTranslation() { return secTranslation; } /** * setSecondarySymbolTranslation sets the translation * in Symbols which will be applied when * rendering. The secondary sequence will be rendered starting at * that translation. Values may be from 0 to the length of the * rendered sequence. * * @param translation an int. * * @exception IndexOutOfBoundsException if the translation is * greater than the sequence length. */ public void setSecondarySymbolTranslation(int translation) throws IndexOutOfBoundsException { if (translation >= secSequence.length()) throw new IndexOutOfBoundsException("Tried to set secondary symbol translation offset equal to or greater than SymbolList length"); int prevSecTranslation = secTranslation; if (hasListeners()) { ChangeSupport cs = getChangeSupport(TRANSLATION); ChangeEvent ce = new ChangeEvent(this, TRANSLATION); cs.firePostChangeEvent(ce); secTranslation = translation; cs.firePostChangeEvent(ce); } else { secTranslation = translation; } firePropertyChange("secTranslation", prevSecTranslation, translation); resizeAndValidate(); } /** * getLeadingBorder returns the leading border of the * primary sequence. * * @return a SequenceRenderContext.Border. */ public SequenceRenderContext.Border getLeadingBorder() { return leadingBorder; } /** * getTrailingBorder returns the trailing border of * the primary sequence. * * @return a SequenceRenderContext.Border. */ public SequenceRenderContext.Border getTrailingBorder() { return trailingBorder; } /** * getRenderer returns the current * PairwiseSequenceRenderer. * * @return a PairwiseSequenceRenderer. */ public PairwiseSequenceRenderer getRenderer() { return renderer; } /** * setRenderer sets the current * PairwiseSequenceRenderer. */ public void setRenderer(PairwiseSequenceRenderer renderer) throws ChangeVetoException { if (hasListeners()) { ChangeSupport cs = getChangeSupport(RENDERER); // We are originating a change event so use this // constructor ChangeEvent ce = new ChangeEvent(this, RENDERER, renderer, this.renderer); synchronized(cs) { cs.firePreChangeEvent(ce); _setRenderer(renderer); cs.firePostChangeEvent(ce); } } else { _setRenderer(renderer); } resizeAndValidate(); } /** * getRenderingHints returns the * RenderingHints currently being used by the * Graphics2D instances of delegate renderers. If * none is set, the constructor creates one with a null * Map. * * @return a RenderingHints. */ public RenderingHints getRenderingHints() { return hints; } /** * setRenderingHints sets the * RenderingHints which will be used by the * Graphics2D instances of delegate renderers. * * @param hints a RenderingHints. */ public void setRenderingHints(RenderingHints hints) { RenderingHints prevHints = this.hints; this.hints = hints; firePropertyChange("hints", prevHints, hints); } /** * sequenceToGraphics converts a sequence index * to a graphical position. * * @param sequencePos an int. * * @return a double. */ public double sequenceToGraphics(int sequencePos) { return (sequencePos - translation - 1) * scale; } /** * secondarySequenceToGraphics converts a sequence * index to a graphical position. * * @param sequencePos an int. * * @return a double. */ public double secondarySequenceToGraphics(int sequencePos) { return (sequencePos - secTranslation - 1) * scale; } /** * graphicsToSequence converts a graphical position * to a sequence index. * * @param graphicsPos a double. * * @return an int. */ public int graphicsToSequence(double graphicsPos) { return ((int) (graphicsPos / scale)) + translation + 1; } /** * graphicsToSequence converts a graphical position * to a sequence index. * * @param point a graphic position. * * @return an int. */ public int graphicsToSequence(Point2D point) { if (direction == HORIZONTAL) return graphicsToSequence(point.getX()); else return graphicsToSequence(point.getY()); } /** * graphicsToSecondarySequence converts a graphical * position to a secondary sequence index. * * @param graphicsPos a double. * * @return an int. */ public int graphicsToSecondarySequence(double graphicsPos) { return ((int) (graphicsPos / scale)) + secTranslation + 1; } /** * graphicsToSecondarySequence converts a graphical * position to a secondary sequence index. * * @param point a Point. * * @return an int. */ public int graphicsToSecondarySequence(Point point) { if (secDirection == HORIZONTAL) return graphicsToSecondarySequence(point.getX()); else return graphicsToSecondarySequence(point.getY()); } /** * getVisibleSymbolCount returns the * maximum number of Symbols which * can be rendered in the visible area (excluding all borders) of * the PairwiseSequencePanel at the current * scale. Note that if the translation is greater than 0, the * actual number of Symbols rendered will be less. * * @return an int. */ public int getVisibleSymbolCount() { // The Insets Insets insets = getInsets(); int visible; if (direction == HORIZONTAL) visible = getWidth() - insets.left - insets.right; else visible = getHeight() - insets.top - insets.bottom; return Math.min(graphicsToSequence(visible), sequence.length()); } /** * getVisibleSecondarySymbolCount returns the * maximum number of secondary * Symbols which can be rendered in the visible area * (excluding all borders) of the * PairwiseSequencePanel at the current scale. Note * that if the translation is greater than 0, the actual number of * Symbols rendered will be less. * * @return an int. */ public int getVisibleSecondarySymbolCount() { // The Insets Insets insets = getInsets(); int visible; if (secDirection == HORIZONTAL) visible = getWidth() - insets.left - insets.right; else visible = getHeight() - insets.top - insets.bottom; return Math.min(graphicsToSecondarySequence(visible), secSequence.length()); } public void paintComponent(Graphics g) { if (! isActive()) return; super.paintComponent(g); // Set hints Graphics2D g2 = (Graphics2D) g; g2.addRenderingHints(hints); // As we subclass JComponent we have to paint our own // background, but only if we are opaque if (isOpaque()) { g2.setPaint(getBackground()); g2.fillRect(0, 0, getWidth(), getHeight()); } // Save current transform and clip AffineTransform prevTransform = g2.getTransform(); Shape prevClip = g2.getClip(); Insets insets = getInsets(); Rectangle2D.Double clip = new Rectangle2D.Double(); clip.x = 0.0; clip.y = 0.0; if (direction == HORIZONTAL) { clip.width = sequenceToGraphics(getVisibleSymbolCount() + 1); clip.height = secondarySequenceToGraphics(getVisibleSecondarySymbolCount() + 1); g2.translate(leadingBorder.getSize() + insets.left, insets.top); } else { clip.width = secondarySequenceToGraphics(getVisibleSecondarySymbolCount() + 1); clip.height = sequenceToGraphics(getVisibleSymbolCount() + 1); g2.translate(insets.left, leadingBorder.getSize() + insets.top); } // Clip and paint g2.clip(clip); renderer.paint(g2, this); // Restore g2.setTransform(prevTransform); g2.setClip(prevClip); } /** * resizeAndValidate sets the minimum, preferred and * maximum sizes of the component according to the current visible * symbol count. */ public void resizeAndValidate() { Dimension d = null; if (! isActive()) { d = new Dimension(0, 0); } else { double width; double height; if (direction == HORIZONTAL) { width = sequenceToGraphics(getVisibleSymbolCount()); height = secondarySequenceToGraphics(getVisibleSecondarySymbolCount()); } else { width = secondarySequenceToGraphics(getVisibleSecondarySymbolCount()); height = sequenceToGraphics(getVisibleSymbolCount()); } d = new Dimension((int) Math.ceil(width), (int) Math.ceil(height)); } setMinimumSize(d); setPreferredSize(d); setMaximumSize(d); revalidate(); } /** * addChangeListener adds a listener for all types of * change. * * @param cl a ChangeListener. */ public void addChangeListener(ChangeListener cl) { addChangeListener(cl, ChangeType.UNKNOWN); } /** * addChangeListener adds a listener for specific * types of change. * * @param cl a ChangeListener. * @param ct a ChangeType. */ public void addChangeListener(ChangeListener cl, ChangeType ct) { ChangeSupport cs = getChangeSupport(ct); cs.addChangeListener(cl, ct); } /** * removeChangeListener removes a listener. * * @param cl a ChangeListener. */ public void removeChangeListener(ChangeListener cl) { removeChangeListener(cl, ChangeType.UNKNOWN); } /** * removeChangeListener removes a listener. * * @param cl a ChangeListener. * @param ct a ChangeType. */ public void removeChangeListener(ChangeListener cl, ChangeType ct) { if(hasListeners()) { ChangeSupport cs = getChangeSupport(ct); cs.removeChangeListener(cl); } } public boolean isUnchanging(ChangeType ct) { ChangeSupport cs = getChangeSupport(ct); return cs.isUnchanging(ct); } /** * addSequenceViewerListener adds a listener for * mouse click SequenceViewerEvents. * * @param svl a SequenceViewerListener. */ public void addSequenceViewerListener(SequenceViewerListener svl) { svSupport.addSequenceViewerListener(svl); } /** * removeSequenceViewerListener removes a listener * for mouse click SequenceViewerEvents. * * @param svl a SequenceViewerListener. */ public void removeSequenceViewerListener(SequenceViewerListener svl) { svSupport.removeSequenceViewerListener(svl); } /** * addSequenceViewerMotionListener adds a listener for * mouse motion SequenceViewerEvents. * * @param svml a SequenceViewerMotionListener. */ public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml) { svmSupport.addSequenceViewerMotionListener(svml); } /** * addSequenceViewerMotionListener removes a listener for * mouse motion SequenceViewerEvents. * * @param svml a SequenceViewerMotionListener. */ public void removeSequenceViewerMotionListener(SequenceViewerMotionListener svml) { svmSupport.removeSequenceViewerMotionListener(svml); } /** * getChangeSupport lazily instantiates a helper for * change listeners. * * @param ct a ChangeType. * * @return a ChangeSupport object. */ protected ChangeSupport getChangeSupport(ChangeType ct) { if(changeSupport != null) { return changeSupport; } synchronized(this) { if (changeSupport == null) { changeSupport = new ChangeSupport(); } return changeSupport; } } /** * hasListeners returns true if there are active * listeners for BioJava events. * * @return a boolean value. */ protected boolean hasListeners() { return changeSupport != null; } /** * isActive returns true if both the * Sequences to be rendered and the * PairwiseHomologyRenderer are not null. * * @return a boolean value. */ protected boolean isActive() { return (sequence != null) && (secSequence != null) && (renderer != null); } private void _setRenderer(PairwiseSequenceRenderer renderer) { // Remove our listeners from the old renderer, if necessary if (this.renderer != null && Changeable.class.isInstance(this.renderer)) { Changeable c = (Changeable) this.renderer; c.removeChangeListener(layoutListener, SequenceRenderContext.LAYOUT); c.removeChangeListener(repaintListener, SequenceRenderContext.REPAINT); } this.renderer = renderer; // Add our listeners to the new renderer, if necessary if (renderer != null && Changeable.class.isInstance(renderer)) { Changeable c = (Changeable) renderer; c.addChangeListener(layoutListener, SequenceRenderContext.LAYOUT); c.addChangeListener(repaintListener, SequenceRenderContext.REPAINT); } } }