/* * 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.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; 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.util.ArrayList; import javax.swing.JComponent; import org.biojava.bio.seq.FeatureHolder; 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; /** *

TranslatedSequencePanel is a panel that displays a * Sequence. Its features are that it will always draw at low pixel * coordinates when using Java2D to render very long sequences and it * is quite fast (approximately 8x faster than * SequencePanel

. * *

A TranslatedSequencePanel can either display the * sequence from left-to-right (HORIZONTAL) or from top-to-bottom * (VERTICAL). It has an associated scale which is the number of * pixels per symbol and a translation which is the number of * Symbols to skip before rendering starts. In order to * produce a scrolling effect, the setSymbolTranslation * method may be hooked up to an Adjustable such as * JScrollBar or to an event listener.

* *

The exact number of Symbols rendered depends on the * width 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 * @author Thomas Down * @author Jolyon Holdstock * @since 1.2 */ public class TranslatedSequencePanel extends JComponent implements SequenceRenderContext, Changeable { /** * Generated Serial Version UID */ private static final long serialVersionUID = 3269477379497205817L; /** * 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 TranslatedSequencePanel has changed", "org.biojava.bio.gui.sequence.TranslatedSequencePanel", "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 TranslatedSequencePanel has changed", "org.biojava.bio.gui.sequence.TranslatedSequencePanel", "TRANSLATION", SequenceRenderContext.REPAINT); // The sequence to be rendered private SymbolList sequence; // The number of residues to skip before starting to render private int translation; // The rendering direction (HORIZONTAL or VERTICAL) private int direction; // The rendering scale in pixels per residue private double scale; // The sequence renderer private SequenceRenderer renderer; // The total border size of the renderer. Cached to avoid // recursive method calls between us and the renderer. private double rendererBorders; // 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(TranslatedSequencePanel.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(TranslatedSequencePanel.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(TranslatedSequencePanel.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(TranslatedSequencePanel.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(TranslatedSequencePanel.this, me, new ArrayList()); me.translatePoint(insets.left, insets.top); svmSupport.fireMouseMoved(sve); } }; /** * Creates a new TranslatedSequencePanel with the * default settings (direction HORIZONTAL, scale 10.0 pixels per * symbol, symbol translation 0, leading border 0.0, trailing * border 0.0, 12 point sanserif font). */ public TranslatedSequencePanel() { super(); if (getFont() == null) setFont(new Font("sanserif", Font.PLAIN, 12)); direction = HORIZONTAL; scale = 10.0; translation = 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 SymbolList getSequence() { return sequence; } /** * setSequence sets the Sequence to be * rendered. * * @param sequence a Sequence. */ public void setSequence(SymbolList sequence) { SymbolList prevSequence = this.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. Also update // rendererBorders cache which can be affected by changes in // sequence if (sequence != null) { if (renderer != null) rendererBorders = renderer.getMinimumLeader(this) + renderer.getMinimumTrailer(this); sequence.addChangeListener(layoutListener); } firePropertyChange("sequence", prevSequence, sequence); resizeAndValidate(); } /** * getSymbols returns all of the Symbols * belonging to the currently rendered Sequence. * * @return a SymbolList. */ public SymbolList getSymbols() { return sequence; } /** * getFeatures returns all of the * Features belonging to the currently rendered * Sequence. * * @return a FeatureHolder. */ public FeatureHolder getFeatures() { if(sequence instanceof FeatureHolder) { return (FeatureHolder) sequence; } else { return FeatureHolder.EMPTY_FEATURE_HOLDER; } } /** * getRange returns a RangeLocation * representing the region of the sequence currently being * rendered. This is calculated from the size of the * TranslatedSequencePanel, minus its * SequenceRenderContext.Borders and its delegate * renderer borders (if any), the current rendering translation * and the current scale. The value will therefore change when the * TranslatedSequencePanel 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); } /** * getDirection returns the direction in which this * context expects sequences to be rendered - HORIZONTAL or * VERTICAL. * * @return an int. */ public int getDirection() { return direction; } /** * setDirection sets the direction in which this * context will render sequences - HORIZONTAL or VERTICAL. * * @param direction an int. * * @exception IllegalArgumentException if an error occurs. */ public void setDirection(int direction) throws IllegalArgumentException { if (direction != HORIZONTAL && direction != VERTICAL) throw new IllegalArgumentException("Direction must be either HORIZONTAL or VERTICAL"); int prevDirection = this.direction; this.direction = direction; firePropertyChange("direction", prevDirection, direction); resizeAndValidate(); } /** * 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, immediately after any * borders, 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, immediately after any borders, * 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(); } /** * getLeadingBorder returns the leading border. * * @return a SequenceRenderContext.Border. */ public SequenceRenderContext.Border getLeadingBorder() { return leadingBorder; } /** * getTrailingBorder returns the trailing border. * * @return a SequenceRenderContext.Border. */ public SequenceRenderContext.Border getTrailingBorder() { return trailingBorder; } /** * getRenderer returns the current * SequenceRenderer. * * @return a SequenceRenderer. */ public SequenceRenderer getRenderer() { return renderer; } /** * setRenderer sets the current * SequenceRenderer. * * @param renderer set the SequenceRenderer used */ public void setRenderer(SequenceRenderer renderer) throws ChangeVetoException { if (! isActive()) _setRenderer(renderer); 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; } /** * 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 the Point2D to transform * * @return an int. */ public int graphicsToSequence(Point2D point) { if (direction == HORIZONTAL) return graphicsToSequence(point.getX()); else return graphicsToSequence(point.getY()); } /** * getVisibleSymbolCount returns the * maximum number of Symbols which * can be rendered in the visible area (excluding all borders) of * the TranslatedSequencePanel 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 BioJava borders double totalBorders = leadingBorder.getSize() + trailingBorder.getSize() + rendererBorders; // The Insets Insets insets = getInsets(); int visible; if (direction == HORIZONTAL) { int width = getWidth() - insets.left - insets.right; if (width <= totalBorders) return 0; else visible = width - (int) totalBorders; } else { int height = getHeight() - insets.top - insets.bottom; if (height <= totalBorders) return 0; else visible = height - (int) totalBorders; } return Math.min(graphicsToSequence(visible), sequence.length()); } /** * paintComponent paints this component. * * @param g a Graphics object. */ public void paintComponent(Graphics g) { if (! isActive()) return; super.paintComponent(g); // Set hints Graphics2D g2 = (Graphics2D) g; g2.addRenderingHints(hints); // Save current transform and clip AffineTransform prevTransform = g2.getTransform(); Shape prevClip = g2.getClip(); Insets insets = getInsets(); // 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()); } Rectangle2D.Double clip = new Rectangle2D.Double(); if (direction == HORIZONTAL) { // Clip x to edge of delegate renderer's leader //clip.x = renderer.getMinimumLeader(this); clip.x = 0 - renderer.getMinimumLeader(this); clip.y = 0.0; // Set the width to visible symbols + the delegate // renderer's minimum trailer (which may have something in // it to render). clip.width = sequenceToGraphics(getVisibleSymbolCount() + 1) + renderer.getMinimumLeader(this) + renderer.getMinimumTrailer(this); clip.height = renderer.getDepth(this); g2.translate(leadingBorder.getSize() - clip.x + insets.left, insets.top); } else { clip.x = 0.0; // Clip y to edge of delegate renderer's leader clip.y = renderer.getMinimumLeader(this); clip.width = renderer.getDepth(this); // Set the height to visible symbols + the delegate // renderer's minimum trailer (which may have something in // it to render). clip.height = sequenceToGraphics(getVisibleSymbolCount() + 1) + renderer.getMinimumTrailer(this); 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 leading * and trailing borders, renderer depth and visible symbol count. */ public void resizeAndValidate() { Dimension d = null; if (! isActive()) { d = new Dimension(0, 0); } else { double width = sequenceToGraphics(getVisibleSymbolCount()) + rendererBorders; double depth = renderer.getDepth(this); if (direction == HORIZONTAL) { d = new Dimension((int) Math.ceil(width), (int) Math.ceil(depth)); } else { d = new Dimension((int) Math.ceil(depth), (int) Math.ceil(width)); } } 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, ct); } } 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 * Sequence to be rendered and the * SequenceRenderer are not null. * * @return a boolean value. */ protected boolean isActive() { return (sequence != null) && (renderer != null); } /** * _setRenderer handles the details of listeners * during changes. * * @param renderer a SequenceRenderer. */ private void _setRenderer(SequenceRenderer 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; // Update our cache of the renderer's total border size, but // only is the sequence is not null. If the sequence was null // this value is no longer correct, so we have to update // rendererBorders in setSequence too. if (sequence != null) rendererBorders = renderer.getMinimumLeader(this) + renderer.getMinimumTrailer(this); // 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); } } }