/* * 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
* Symbol
s 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 Symbol
s rendered depends on the
* width of the panel and the scale. Resizing the panel will cause the
* number of Symbol
s 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; /** * ConstantRENDERER
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 Symbol
s
* belonging to the currently rendered Sequence
.
*
* @return a SymbolList
.
*/
public SymbolList getSymbols()
{
return sequence;
}
/**
* getFeatures
returns all of the
* Feature
s 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.Border
s 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 Symbol
s 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
* Symbol
s 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 Symbol
s 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 Symbol
s 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 SequenceViewerEvent
s.
*
* @param svl a SequenceViewerListener
.
*/
public void addSequenceViewerListener(SequenceViewerListener svl)
{
svSupport.addSequenceViewerListener(svl);
}
/**
* removeSequenceViewerListener
removes a listener
* for mouse click SequenceViewerEvent
s.
*
* @param svl a SequenceViewerListener
.
*/
public void removeSequenceViewerListener(SequenceViewerListener svl)
{
svSupport.removeSequenceViewerListener(svl);
}
/**
* addSequenceViewerMotionListener
adds a listener for
* mouse motion SequenceViewerEvent
s.
*
* @param svml a SequenceViewerMotionListener
.
*/
public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml)
{
svmSupport.addSequenceViewerMotionListener(svml);
}
/**
* addSequenceViewerMotionListener
removes a listener for
* mouse motion SequenceViewerEvent
s.
*
* @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);
}
}
}