[Biojava-l] Modifications to org.biojava.bio.gui.sequence source

David S. Huen smh1008@cus.cam.ac.uk
Mon, 7 May 2001 18:50:00 +0100


--------------Boundary-00=_C78ZS92LNIAC4IJHXQ3O
Content-Type: text/plain;
  charset="iso-8859-1"
Content-Transfer-Encoding: 8bit

Some of the classes in this package are required for das-client and they have 
not worked properly in my hands.  I have modified 3 classes get them working 
again.

The main cause has been a floating point rounding problem with the Java 
drawing system.  The magnitude of sequence coordinates can range to 10^9 and 
when that is combined with the pixel-to-symbol scales typically used, we can 
get pixel coordinates of 10^10 and up.  At these magnitudes, the fractional 
part of the floating point representation is incapable of achieving unit 
resolutions.  Pixel coordinates are further subject to transformations with 
further accumulated error.  Some operations depend subtraction of floating 
point pixel coordinates of very similar value making them particularly prone 
to error.  Floating point rounding errors have particularly affected zoomed 
in rendering at high sequence numbers.

I have modified SequencePanel to include a setPixelOrigin() method.  This 
permits me to apply an arbitrary offset to the values returned by the 
graphicsToSequence() and sequenceToGraphics() methods.  I use this to 
translate the range of pixel coordinates returned by SequenceToGraphics() to 
be in the neighbourhood of zero.  Most code in the libraries are unaffected 
by this change as it only translates the graphics space origin.  However, 
this will mean that sequenceToGraphics(1) will not necessarily yield zero any 
longer and calls of type sequenceToGraphics(<some sequence range>) will not 
yield the pixels occupied by that sequence correctly.  We may want to 
introduce a new method to do that (sequenceRangeToGraphicsWidth()?).

I have made the required changes to SymbolSequenceRenderer and RulerRenderer 
to work correctly with this modification.  When used with the das-client code 
on http://servlet.sanger.ac.uk:8080/das/, they yield reasonable ruler ticks 
and sequence symbols when adequately zoomed.  Ticks and symbols are evenly 
spaced now.

I request comments as to whether the modification proposed above is 
acceptable - it was the smallest change I could conceive of that would fix 
the floating point problem without extensively breaking existing code.  If 
so, perhaps I should place the setPixelOrigin() and getPixelOrigin() calls in 
the SequenceRenderContext interface and then modify SequencePoster and 
SubsequenceRenderContext that implement that interface to include these 
methods too.

Other fixes
RulerRenderer: 1) fix code to determine labelwidth and use it to correctly 
center coordinate labels, 2) change snap so it snaps to 1 2 or 5 divisions.
SymbolSequenceRenderer: 1) remove incorrect clip.

If acceptable, please inform me so I can fix up the SequencePoster and 
SubsequenceRenderContext classes accordingly.  I don't have test code that 
use these classes to verify correct operation.

Could someone commit these changes if acceptable.  A further series of 
commits will need to follow later.

David Huen, Dept. of Genetics, Univ. of Cambridge.
P.S. I am trying out KMail with unencoded attachments.  Can some tell me if 
that is acceptable or is something like base64 required instead?

--------------Boundary-00=_C78ZS92LNIAC4IJHXQ3O
Content-Type: text/x-java;
  charset="iso-8859-1";
  name="SequencePanel.java"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="SequencePanel.java"

/*
 *                    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.util.*;
import java.beans.*;
import java.io.Serializable;
import java.lang.reflect.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.*;

import org.biojava.utils.*;

import org.biojava.bio.*;
import org.biojava.bio.symbol.*;
import org.biojava.bio.seq.*;
import org.biojava.bio.gui.sequence.*;

import java.util.List; // usefull trick to 'hide' javax.swing.List

/**
 * A panel that visualy displays a Sequence.
 * <P>
 * A SequencePanel 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. It also has a lines property that
 * controls how to wrap the sequence off one end and onto the other.
 * <P>
 * Each line in the SequencePanel is broken down into a list of strips,
 * each rendered by an individual SequenceRenderer object.
 * You could add a SequenceRenderer that draws on genes, another that
 * draws repeats and another that prints out the DNA sequence. They are
 * responsible for rendering their view of the sequence in the place that the
 * SequencePanel positions them.  
 *
 * @author Thomas Down
 * @author Matthew Pocock
 */
public class SequencePanel
extends JComponent
implements SwingConstants,
SequenceRenderContext,
Changeable {
  public static final ChangeType RENDERER = new ChangeType(
    "The renderer for this SequencePanel has changed",
    "org.biojava.bio.gui.sequence.SequencePanel",
    "RENDERER",
    SequenceRenderContext.LAYOUT
  );

  private SymbolList sequence;
  private RangeLocation range;
  private int direction;
  private double scale;
  private double pixelOffset; // an arbitrary offset to be applied when
                              // translating between graphics and sequence
                              // space.  
  private SequenceRenderContext.Border leadingBorder;
  private SequenceRenderContext.Border trailingBorder;

  private SequenceRenderer renderer;
  private RendererMonitor theMonitor;

  private transient ChangeSupport changeSupport = null;
  
  private SequenceViewerSupport svSupport = new SequenceViewerSupport();
  private MouseListener mouseListener = new MouseAdapter() {
    public void mouseClicked(MouseEvent me) {
      if(!isActive()) {
        return;
      }
      int [] dist = calcDist();
      me.translatePoint(+dist[0], +dist[1]);
      SequenceViewerEvent sve = renderer.processMouseEvent(
        SequencePanel.this,
        me,
        new ArrayList(),
        range
      );
      me.translatePoint(-dist[0], -dist[1]);
      svSupport.fireMouseClicked(sve);
    }
    
    public void mousePressed(MouseEvent me) {
      if(!isActive()) {
        return;
      }
      int [] dist = calcDist();
      me.translatePoint(+dist[0], +dist[1]);
      SequenceViewerEvent sve = renderer.processMouseEvent(
        SequencePanel.this,
        me,
        new ArrayList(),
        range
      );
      me.translatePoint(-dist[0], -dist[1]);
      svSupport.fireMousePressed(sve);
    }
    
    public void mouseReleased(MouseEvent me) {
      if(!isActive()) {
        return;
      }
      int [] dist = calcDist();
      me.translatePoint(+dist[0], +dist[1]);
      SequenceViewerEvent sve = renderer.processMouseEvent(
        SequencePanel.this,
        me,
        new ArrayList(),
        range
      );
      me.translatePoint(-dist[0], -dist[1]);
      svSupport.fireMouseReleased(sve);
    }
  };
  public void addSequenceViewerListener(SequenceViewerListener svl) {
    svSupport.addSequenceViewerListener(svl);
  }
  public void removeSequenceViewerListener(SequenceViewerListener svl) {
    svSupport.removeSequenceViewerListener(svl);
  }

  private SequenceViewerMotionSupport svmSupport = new SequenceViewerMotionSupport();
  private MouseMotionListener mouseMotionListener = new MouseMotionListener() {
    public void mouseDragged(MouseEvent me) {
      if(!isActive()) {
        return;
      }
      int [] dist = calcDist();
      me.translatePoint(+dist[0], +dist[1]);
      SequenceViewerEvent sve = renderer.processMouseEvent(
        SequencePanel.this,
        me,
        new ArrayList(),
        range
      );
      me.translatePoint(-dist[0], -dist[1]);
      svmSupport.fireMouseDragged(sve);
    }
    
    public void mouseMoved(MouseEvent me) {
      if(!isActive()) {
        return;
      }
      int [] dist = calcDist();
      me.translatePoint(+dist[0], +dist[1]);
      SequenceViewerEvent sve = renderer.processMouseEvent(
        SequencePanel.this,
        me,
        new ArrayList(),
        range
      );
      me.translatePoint(-dist[0], -dist[1]);
      svmSupport.fireMouseMoved(sve);
    }
  };
  public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml) {
    svmSupport.addSequenceViewerMotionListener(svml);
  }
  public void removeSequenceViewerMotionListener(SequenceViewerMotionListener svml) {
    svmSupport.removeSequenceViewerMotionListener(svml);
  }
  
  protected boolean hasChangeListeners() {
    return changeSupport != null;
  }
  
  protected ChangeSupport getChangeSupport(ChangeType ct) {
    if(changeSupport == null) {
      changeSupport = new ChangeSupport();
    }
    
    return changeSupport;
  }
  
  public void addChangeListener(ChangeListener cl) {
    addChangeListener(cl, ChangeType.UNKNOWN);
  }
  
  public void addChangeListener(ChangeListener cl, ChangeType ct) {
    ChangeSupport cs = getChangeSupport(ct);
    synchronized(cs) {
      cs.addChangeListener(cl);
    }
  }
  
  public void removeChangeListener(ChangeListener cl) {
    removeChangeListener(cl, ChangeType.UNKNOWN);
  }
  
  public void removeChangeListener(ChangeListener cl, ChangeType ct) {
    ChangeSupport cs = getChangeSupport(ct);
    synchronized(cs) {
      cs.removeChangeListener(cl);
    }
  }

  private ChangeListener layoutListener = new ChangeAdapter() {
    public void postChange(ChangeEvent ce) {
      resizeAndValidate();
    }
  };
  private ChangeListener repaintListener = new ChangeAdapter() {
    public void postChange(ChangeEvent ce) {
      repaint();
    }
  };

  /**
   * Initializer.
   */

  {
    direction = HORIZONTAL;
    scale = 12.0;
    pixelOffset = 0.0;

    theMonitor = new RendererMonitor();
    leadingBorder = new SequenceRenderContext.Border();
    trailingBorder = new SequenceRenderContext.Border();
  }

  /**
   * Create a new SequencePanel.
   */
  public SequencePanel() {
    super();
    if(getFont() == null) {
      setFont(new Font("serif", Font.PLAIN, 12));
    }
    this.addPropertyChangeListener(theMonitor);
    this.addMouseListener(mouseListener);
    this.addMouseMotionListener(mouseMotionListener);
  }
  
  /**
   * Set the SymboList to be rendered. This symbol list will be passed onto the
   * SequenceRenderer instances registered with this SequencePanel.
   *
   * @param s  the SymboList to render
   */
  public void setSequence(SymbolList s) {
    SymbolList oldSequence = sequence;
    if(oldSequence != null) {
      oldSequence.removeChangeListener(layoutListener);
    }
    this.sequence = s;
    if(s != null) {
      sequence.addChangeListener(layoutListener);
    }
    
    resizeAndValidate();
    firePropertyChange("sequence", oldSequence, s);
  }

  /**
   * Retrieve the currently rendered SymbolList
   *
   * @return  the current SymbolList
   */
  public SymbolList getSequence() {
    return sequence;
  }

  public void setRange(RangeLocation range) {
    RangeLocation oldRange = this.range;
    this.range = range;
    resizeAndValidate();
    firePropertyChange("range", oldRange, range);
  }
  
  public RangeLocation getRange() {
    return this.range;
  }
  
  /**
   * Set the direction that this SequencePanel renders in. The direction can be
   * one of HORIZONTAL or VERTICAL. Once the direction is set, the display will
   * redraw. HORIZONTAL represents left-to-right rendering. VERTICAL represents
   * AceDB-style vertical rendering.
   *
   * @param dir  the new rendering direction
   */
  public void setDirection(int dir) 
  throws IllegalArgumentException {
    if(dir != HORIZONTAL && dir != VERTICAL) {
      throw new IllegalArgumentException(
        "Direction must be either HORIZONTAL or VERTICAL"
      );
    }
    int oldDirection = direction;
    direction = dir;
    resizeAndValidate();
    firePropertyChange("direction", oldDirection, direction);
  }

  /**
   * Retrieve the current rendering direction.
   *
   * @return the rendering direction (one of HORIZONTAL and VERTICAL)
   */
  public int getDirection() {
    return direction;
  }
  
  /**
   * Set the scale.
   * <P>
   * The scale parameter is interpreted as the number of pixles per symbol. This
   * may take on a wide range of values - for example, to render the symbols as
   * text, you will need a scale of > 8, where as to render chromosome 1 you
   * will want a scale &lt; 0.00000001
   *
   * @param scale the new pixles-per-symbol ratio
   */
  public void setScale(double scale) {
    double oldScale = this.scale;
    this.scale = scale;
    resizeAndValidate();
    firePropertyChange("scale", oldScale, scale);
  }

  /**
   * Retrieve the current scale.
   *
   * @return the number of pixles used to render one symbol
   */
  public double getScale() {
    return scale;
  }
  
  /**
   * Retrieve the object that encapsulates the leading border area - the space
   * before sequence information is rendered.
   *
   * @return a SequenceRenderContext.Border instance
   */
  public SequenceRenderContext.Border getLeadingBorder() {
    return leadingBorder;
  }
  
  /**
   * Retrieve the object that encapsulates the trailing border area - the space
   * after sequence information is rendered.
   *
   * @return a SequenceRenderContext.Border instance
   */
  public SequenceRenderContext.Border getTrailingBorder() {
    return trailingBorder;
  }
  
  /**
   * Paint this component.
   * <P>
   * This calls the paint method of the currently registered SequenceRenderer
   * after setting up the graphics apropreately.
   */
  public void paintComponent(Graphics g) {
    if(!isActive()) {
      return;
    }
    Graphics2D g2 = (Graphics2D) g;
    AffineTransform oldTransform = g2.getTransform();
    Rectangle2D currentClip = g2.getClip().getBounds2D();
    
    // do a transform to offset drawing to the neighbourhood of zero.
    // the 50 here is pretty arbitrary.  The precise value doesn't matter
    setGraphicsOrigin(50.0-sequenceToGraphics(range.getMin()));

    double minAcross = sequenceToGraphics(range.getMin()) -
                       renderer.getMinimumLeader(this, range);
    double maxAcross = sequenceToGraphics(range.getMax()) + 1 +
                       renderer.getMinimumTrailer(this, range);
    double alongDim = maxAcross - minAcross;
    double depth = renderer.getDepth(this, range);
    Rectangle2D.Double clip = new Rectangle2D.Double();
    if (direction == HORIZONTAL) {
      clip.x = minAcross;
      clip.x = leadingBorder.getSize();
      clip.y = 0.0;
      clip.width = alongDim;
      clip.height = depth;
      g2.translate(leadingBorder.getSize() - minAcross, 0.0);
      // setGraphicsOrigin(leadingBorder.getSize() - minAcross);
    } else {
      clip.x = 0.0;
      clip.y = minAcross;
      clip.y = leadingBorder.getSize();
      clip.width = depth;
      clip.height = alongDim;
      g2.translate(0.0, leadingBorder.getSize() - minAcross);
      // setGraphicsOrigin(leadingBorder.getSize() - minAcross);
    }

    Shape oldClip = g2.getClip();
    g2.clip(clip);
    renderer.paint(g2, this, range);
    g2.setClip(oldClip);
    g2.setTransform(oldTransform);
  }

  public void setRenderer(SequenceRenderer r)
  throws ChangeVetoException {
    if(hasChangeListeners()) {
      ChangeEvent ce = new ChangeEvent(
        this,
        RENDERER,
        r,
        this.renderer
      );
      ChangeSupport cs = getChangeSupport(RENDERER);
      synchronized(cs) {
        cs.firePreChangeEvent(ce);
        _setRenderer(r);
        cs.firePostChangeEvent(ce);
      }
    } else {
      _setRenderer(r);
    }
    resizeAndValidate();
  }
  
  protected void _setRenderer(SequenceRenderer r) {
    if( (this.renderer != null) && (this.renderer instanceof Changeable) ) {
      Changeable c = (Changeable) this.renderer;
      c.removeChangeListener(layoutListener, SequenceRenderContext.LAYOUT);
      c.removeChangeListener(repaintListener, SequenceRenderContext.REPAINT);
    }

    this.renderer = r;

    if( (r != null) && (r instanceof Changeable) ) {
      Changeable c = (Changeable) r;
      c.addChangeListener(layoutListener, SequenceRenderContext.LAYOUT);
      c.addChangeListener(repaintListener, SequenceRenderContext.REPAINT);
    }
  }

  public double sequenceToGraphics(int seqPos) {
    return ((double) (seqPos-1)) * scale + pixelOffset;
  }

  public int graphicsToSequence(double gPos) {
    return ((int) ((gPos - pixelOffset) / scale)) + 1;
  }
  
  public int graphicsToSequence(Point point) {
    if(direction == HORIZONTAL) {
      return graphicsToSequence(point.getX());
    } else {
      return graphicsToSequence(point.getY());
    }
  }

//  public double getGraphicsOrigin() {
//    return this.pixelOffset;
//  }

  public void setGraphicsOrigin(double displacement) {
    // System.out.println("setGraphicsOrigin: " + displacement);  
    this.pixelOffset += displacement;
  }

  public void resizeAndValidate() {
    //System.out.println("resizeAndValidate starting");
    Dimension d = null;
    
    if(!isActive()) {
      System.out.println("No sequence");
      // no sequence - collapse down to no size at all
      leadingBorder.setSize(0.0);
      trailingBorder.setSize(0.0);
      d = new Dimension(0, 0);
    } else {
      double minAcross = sequenceToGraphics(range.getMin());
      double maxAcross = sequenceToGraphics(range.getMax());
      double lb = renderer.getMinimumLeader(this, range);
      double tb = renderer.getMinimumTrailer(this, range);
      double alongDim =
        (maxAcross - minAcross) +
        lb + tb;
      double depth = renderer.getDepth(this, range);
      if(direction == HORIZONTAL) {
        d = new Dimension((int) Math.ceil(alongDim), (int) Math.ceil(depth));
      } else {
        d = new Dimension((int) Math.ceil(depth), (int) Math.ceil(alongDim));
      }
    }
    
    setMinimumSize(d);
    setPreferredSize(d);
    setMaximumSize(d);
    revalidate();
    //System.out.println("resizeAndValidate ending");
  }

  private class RendererMonitor implements PropertyChangeListener {
    public void propertyChange(PropertyChangeEvent ev) {
      repaint();
    }
  }
  
  protected int [] calcDist() {
    double minAcross = sequenceToGraphics(range.getMin()) -
                       renderer.getMinimumLeader(this, range);
    int [] dist = new int[2];
    if(direction == HORIZONTAL) {
      dist[0] = (int) minAcross;
      dist[1] = 0;
    } else {
      dist[0] = 0;
      dist[1] = (int) minAcross;
    }
    
    return dist;
  }
  
  protected boolean isActive() {
    return
      (sequence != null) &&
      (renderer != null) &&
      (range != null);
  }
  
  public class Border
  implements Serializable, SwingConstants {
    protected final PropertyChangeSupport pcs;
    private double size = 0.0;
    private int alignment = CENTER;
    
    public double getSize() {
      return size;
    }
    
    private void setSize(double size) {
      this.size = size;
    }
    
    public int getAlignment() {
      return alignment;
    }
    
    public void setAlignment(int alignment)
        throws IllegalArgumentException 
    {
	if (alignment == LEADING || alignment == TRAILING || alignment == CENTER) {
	    int old = this.alignment;
	    this.alignment = alignment;
	    pcs.firePropertyChange("alignment", old, alignment);
	} else {
	    throw new IllegalArgumentException(
		  "Alignment must be one of the constants LEADING, TRAILING or CENTER"
            );
	}
    }
    
    private Border() {
      alignment = CENTER;
      pcs = new PropertyChangeSupport(this);
    }
    
    public void addPropertyChangeListener(PropertyChangeListener listener) {
      pcs.addPropertyChangeListener(listener);
    }
    
    public void removePropertyChangeListener(PropertyChangeListener listener) {
      pcs.removePropertyChangeListener(listener);
    }
  }
}


--------------Boundary-00=_C78ZS92LNIAC4IJHXQ3O
Content-Type: text/x-java;
  charset="iso-8859-1";
  name="SymbolSequenceRenderer.java"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="SymbolSequenceRenderer.java"

/*
 *                    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.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;

import org.biojava.bio.*;
import org.biojava.bio.seq.*;
import org.biojava.bio.symbol.*;
import org.biojava.bio.gui.*;

import java.util.List;

// The graphics model in Java
// drawing space -> applet space -> device space
// All operations are cumulative, including translates
// translates will move drawing rightward/downward for any supplied value
// 
public class SymbolSequenceRenderer implements SequenceRenderer {
    private double depth = 25.0;
    
    public double getDepth(SequenceRenderContext src, RangeLocation pos) {
      return depth + 1.0;
    }

    public double getMinimumLeader(SequenceRenderContext src, RangeLocation pos) {
      return 0.0;
    }

    public double getMinimumTrailer(SequenceRenderContext src, RangeLocation pos) {
      return 0.0;
    }

    public void paint(
      Graphics2D g, SequenceRenderContext src,
      RangeLocation pos
    ) {
      SymbolList seq = src.getSequence();
      int direction = src.getDirection();
      
      g.setFont(src.getFont());
      Rectangle2D oldClip = g.getClipBounds();
//      AffineTransform oldTrans = g.getTransform();
      
      g.setColor(Color.black);
      
      double scale = src.getScale();
      Rectangle2D maxBounds =
        g.getFont().getMaxCharBounds(g.getFontRenderContext());
      if(
        // symbol must be larger than 30% of char size
        // attempting to render
        src.getScale() >= maxBounds.getWidth()*0.3 &&
        src.getScale() >= maxBounds.getHeight()*0.3
      ) {
        double fudgeAcross = 0.0;
        double fudgeDown = 0.0;
        if (direction == src.HORIZONTAL) {
          fudgeAcross = 0.0 /*- maxBounds.getCenterX()*/;
          // intended to center text in band
          fudgeDown = depth * 0.5 - maxBounds.getCenterY();
        } else {
          fudgeAcross = depth * 0.5 - maxBounds.getCenterX();
          fudgeDown = scale * 0.5 - maxBounds.getCenterY();
        }
        
        int leading;  // these correspond to the symbol index value
        int trailing; // of the ends of the clip region
        int symOffset = pos.getMin(); // first symbol by base index value
        double graphOffset = src.sequenceToGraphics(symOffset); // by pixels
        if(src.getDirection() == src.HORIZONTAL) {
          // compute base nos. associated with ends of clip region
          leading = src.graphicsToSequence(oldClip.getMinX());
          trailing = src.graphicsToSequence(oldClip.getMaxX());
          // A transform to render the symbols is setup in 
          // SequencePanel.paintComponent().
          // start of leader will place you at leftmost edge of draw area.

          // the default clip region from SequencePanel.paintComponent()
          // spans the leader, sequence range and trailer.
          // it is adequate for this method although we could further
          // restrict it to the sequence region itself 
        } else {
          leading = src.graphicsToSequence(oldClip.getMinY());
          trailing = src.graphicsToSequence(oldClip.getMaxY());
        }
//        Rectangle2D clip = g.getClipBounds();

        // can this ever happen? leading > pos.getMin?, 
        //                      pos.getMax() > trailing?        
        int min = Math.max(pos.getMin(), leading);
        int max = Math.min(pos.getMax(), trailing+1);

//        System.out.println("oldClip: " + oldClip);
//        System.out.println("oldTrans: " + oldTrans);
/*        System.out.println("pos: " + pos);
        System.out.println("symOffset: " + symOffset);
        System.out.println("graphOffset: " + graphOffset);
        System.out.println("leading: " + leading);
        System.out.println("trailing: " + trailing);
        System.out.println("min: " + min);
        System.out.println("max: " + max);
        System.out.println("-");*/
        
        for (int sPos = min; sPos <= max; ++sPos) {
          // double gPos = src.sequenceToGraphics(sPos - symOffset + 1);
          double gPos = src.sequenceToGraphics(sPos/* - symOffset*/ + 1);
          char c = seq.symbolAt(sPos).getToken();
          if (direction == SequencePanel.HORIZONTAL) {
            //charBox.x = gPos;
            g.drawString(
              String.valueOf(c),
              (int) (gPos + fudgeAcross), (int) fudgeDown
            );
            if(sPos == 10) {
              g.draw(new Rectangle2D.Double(gPos, 0.0, src.getScale(), 10.0));
            }
          } else {
            //charBox.y = gPos;
            g.drawString(
              String.valueOf(c),
              (int) fudgeAcross, (int) (gPos + fudgeDown)
            );
          }
          //g.draw(charBox);
        }
      }
      
//      g.setTransform(oldTrans);
//      g.setClip(oldClip);
    }
  
  public SequenceViewerEvent processMouseEvent(
    SequenceRenderContext src,
    MouseEvent me,
    List path,
    RangeLocation pos
  ) {
    path.add(this);
    int sPos = src.graphicsToSequence(me.getPoint());
    return new SequenceViewerEvent(this, null, sPos, me, path);
  }
}

--------------Boundary-00=_C78ZS92LNIAC4IJHXQ3O
Content-Type: text/x-java;
  charset="iso-8859-1";
  name="RulerRenderer.java"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="RulerRenderer.java"

/*
 *                    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.util.*;
import java.awt.event.*;

import org.biojava.bio.*;
import org.biojava.bio.seq.*;
import org.biojava.bio.symbol.*;
import org.biojava.bio.gui.*;

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;

import java.util.List;

public class RulerRenderer implements SequenceRenderer {
  private double depth = 25.0;
  
  public double getDepth(SequenceRenderContext src, RangeLocation pos) {
    return depth + 1.0;
  }
  
  public double getMinimumLeader(SequenceRenderContext src, RangeLocation pos) {
    return 0.0;
  }
  
  public double getMinimumTrailer(SequenceRenderContext src, RangeLocation pos) {
    String lengthString = String.valueOf(src.getSequence().length());
    Font f = src.getFont();
    FontRenderContext frc = new FontRenderContext(null, true, true);
    GlyphVector gv = f.createGlyphVector(frc, lengthString);
    return gv.getVisualBounds().getWidth();
  }
  
  public void paint(
    Graphics2D g, SequenceRenderContext src,
    RangeLocation pos
  ) {
    g.setPaint(Color.black);
    
    int min = pos.getMin();
    int max = pos.getMax();
    double minX = src.sequenceToGraphics(min);
    double maxX = src.sequenceToGraphics(max);
    double scale = src.getScale();
    double halfScale = scale * 0.5;
    Line2D line;
    Rectangle2D activeClip = g.getClipBounds();

    // dump some info
    //System.out.println("ruler transform: " + g.getTransform());
    //System.out.println("ruler min max: " + min + " " + max);
    //System.out.println("ruler minX maxX: " + minX + " " + maxX);
    //System.out.println("ruler activeClip:" + activeClip);
    
    if(src.getDirection() == src.HORIZONTAL) {
      line = new Line2D.Double(minX - halfScale, 0.0, maxX + halfScale, 0.0);
    } else {
      line = new Line2D.Double(0.0, minX - halfScale, 0.0, maxX + halfScale);
    }
    
    g.draw(line);

    // tick spacing should be decided by the size of the text needed
    // to display the largest coordinate value and some
    // minimum spacing limit.
    // we want ticks no closer than 40 pixels apart.
    double ten = Math.log(10);
    FontMetrics myFontMetrics = g.getFontMetrics();    
    int coordWidth = myFontMetrics.stringWidth(Integer.toString(max));
    // System.out.println("coordWidth: " + coordWidth);
    double minGap = (double) Math.max(coordWidth, 40);
    int realSymsPerGap = (int) Math.ceil(((minGap + 5.0) / src.getScale()));
    // System.out.println("Real syms: " + realSymsPerGap);

    // we need to snap to a value beginning 1, 2 or 5.
    double exponent =  Math.floor(Math.log(realSymsPerGap) / ten);
    double characteristic = realSymsPerGap 
                            / Math.pow(10.0, exponent);
    int snapSymsPerGap;
    if (characteristic > 5.0) {
      // use unit ticks
      snapSymsPerGap = (int) Math.pow(10.0, exponent + 1.0);
    } else if (characteristic > 2.0) {
      // use ticks of 5
      snapSymsPerGap = (int)(5.0 * Math.pow(10.0, exponent));
    } else {
      snapSymsPerGap = (int)(2.0 * Math.pow(10.0, exponent)); 
    }
    // System.out.println("Snapped syms: " + snapSymsPerGap);
    
    int minP = min + (snapSymsPerGap - min) % snapSymsPerGap;
    for(int indx = minP; indx <= max; indx += snapSymsPerGap) {
      double offset = src.sequenceToGraphics(indx);
      // System.out.println("ruler indx offset: " + indx + " " +  offset);
      if(src.getDirection() == src.HORIZONTAL) {
        line.setLine(offset + halfScale, 0.0, offset + halfScale, 5.0);
        String labelString = String.valueOf(indx);
        int halfLabelWidth = myFontMetrics.stringWidth(labelString) / 2;
        g.drawString(String.valueOf(indx), 
                     (float) (offset + halfScale - halfLabelWidth), 20.0f);
      } else {
        line.setLine(0.0, offset + halfScale, 5.0, offset + halfScale);
      }
      g.draw(line);
    }
  }
  
  public SequenceViewerEvent processMouseEvent(
    SequenceRenderContext src,
    MouseEvent me,
    List path,
    RangeLocation pos
  ) {
    path.add(this);
    int sPos = src.graphicsToSequence(me.getPoint());
    return new SequenceViewerEvent(this, null, sPos, me, path);
  }
}

--------------Boundary-00=_C78ZS92LNIAC4IJHXQ3O--