001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojava.bio.gui.sequence;
023
024import java.awt.Color;
025import java.awt.FontMetrics;
026import java.awt.Graphics2D;
027import java.awt.event.MouseEvent;
028import java.awt.geom.AffineTransform;
029import java.awt.geom.Line2D;
030import java.util.List;
031
032import org.biojava.bio.symbol.Location;
033
034/**
035 * <p><code>RulerRenderer</code> renders numerical scales in sequence
036 * coordinates. The tick direction may be set to point upwards (or
037 * left when the scale is vertical) or downwards (right when the scale
038 * is vertical).</p>
039 *
040 * <p>Note: The Compaq Java VMs 1.3.1 - 1.4.0 on Tru64 appear to have
041 * a bug in font transformation which prevents a vertically oriented
042 * ruler displaying correctly rotated text.</p>
043 *
044 * @author Matthew Pocock
045 * @author Thomas Down
046 * @author David Huen
047 * @author Keith James
048 * @author Kalle N&auml;slund
049 */
050public class RulerRenderer implements SequenceRenderer
051{
052    /**
053     * <code>TICKS_UP</code> indicates that the ticks will point
054     * upwards from a baseline.
055     */
056    public static final int TICKS_UP = 0;
057    /**
058     * <code>TICKS_DOWN</code> indicates that the ticks will point
059     * downwards from a baseline.
060     */
061    public static final int TICKS_DOWN = 1;
062
063    private Line2D            line;
064    private double            depth;
065    private AffineTransform   antiQuarter;
066    private int               tickDirection;
067    private float             tickHeight;
068    private float             horizLabelOffset;
069    private float             vertLabelOffset;
070
071    /**
072     * Creates a new <code>RulerRenderer</code> with the default
073     * setting of ticks pointing downwards.
074     */
075    public RulerRenderer() throws IllegalArgumentException
076    {
077        this(TICKS_DOWN);
078    }
079
080    /**
081     * Creates a new <code>RulerRenderer</code> with the specified
082     * tick direction.
083     *
084     * @param tickDirection an <code>int</code>.
085     * @exception IllegalArgumentException if an error occurs.
086     */
087    public RulerRenderer(int tickDirection) throws IllegalArgumentException
088    {
089        line   = new Line2D.Double();
090        antiQuarter = AffineTransform.getRotateInstance(Math.toRadians(-90));
091
092        if (tickDirection == TICKS_UP || tickDirection == TICKS_DOWN)
093            this.tickDirection = tickDirection;
094        else
095            throw new IllegalArgumentException("Tick direction may only be set to RulerRenderer.TICKS_UP or RulerRenderer.TICKS_DOWN");
096
097        depth      = 20.0;
098        tickHeight = 4.0f;
099
100        horizLabelOffset = ((float) depth) - tickHeight - 2.0f;
101        vertLabelOffset  = ((float) depth) - ((tickHeight + 2.0f) * 2.0f);
102    }
103
104    public double getMinimumLeader(SequenceRenderContext context)
105    {
106        return 0.0;
107    }
108
109    public double getMinimumTrailer(SequenceRenderContext context)
110    {
111        return 0.0;
112    }
113
114    public double getDepth(SequenceRenderContext src)
115    {
116        return depth + 1.0;
117    }
118
119    public void paint(Graphics2D g2, SequenceRenderContext context)
120    {
121        AffineTransform prevTransform = g2.getTransform();
122
123        g2.setPaint(Color.black);
124
125        Location visible = GUITools.getVisibleRange(context, g2);
126        if( visible == Location.empty ) {
127            return;
128        }
129        
130        int min = visible.getMin();
131        int max = visible.getMax();
132        double minX = context.sequenceToGraphics(min);
133        double maxX = context.sequenceToGraphics(max);
134        double scale = context.getScale();
135
136        double halfScale = scale * 0.5;
137
138        if (context.getDirection() == SequenceRenderContext.HORIZONTAL)
139        {
140            if (tickDirection == TICKS_UP)
141            {
142                line.setLine(minX - halfScale, depth,
143                             maxX + halfScale, depth);
144            }
145            else
146            {
147                line.setLine(minX - halfScale, 0.0,
148                             maxX + halfScale, 0.0);
149            }
150        }
151        else
152        {
153            if (tickDirection == TICKS_UP)
154            {
155                line.setLine(depth, minX - halfScale,
156                             depth, maxX + halfScale);
157            }
158            else
159            {
160                line.setLine(0.0, minX - halfScale,
161                             0.0, maxX + halfScale);
162            }
163        }
164
165        g2.draw(line);
166
167        FontMetrics fMetrics = g2.getFontMetrics();
168
169        // The widest (== maxiumum) coordinate to draw
170        int coordWidth = fMetrics.stringWidth(Integer.toString(max));
171
172        // Minimum gap getween ticks
173        double minGap = (double) Math.max(coordWidth, 40);
174
175        // How many symbols does a gap represent?
176        int realSymsPerGap = (int) Math.ceil(((minGap + 5.0) / context.getScale()));
177
178        // We need to snap to a value beginning 1, 2 or 5.
179        double exponent = Math.floor(Math.log(realSymsPerGap) / Math.log(10));
180        double characteristic = realSymsPerGap / Math.pow(10.0, exponent);
181
182        int snapSymsPerGap;
183        if (characteristic > 5.0)
184        {
185            // Use unit ticks
186            snapSymsPerGap = (int) Math.pow(10.0, exponent + 1.0);
187        }
188        else if (characteristic > 2.0)
189        {
190            // Use ticks of 5
191            snapSymsPerGap = (int) (5.0 * Math.pow(10.0, exponent));
192        }
193        else
194        {
195            snapSymsPerGap = (int) (2.0 * Math.pow(10.0, exponent));
196        }
197
198        int minP = min + (snapSymsPerGap - min) % snapSymsPerGap;
199
200        for (int index = minP; index <= max; index += snapSymsPerGap)
201        {
202            double offset = context.sequenceToGraphics(index);
203            String labelString = String.valueOf(index);
204            float halfLabelWidth = fMetrics.stringWidth(labelString) / 2;
205
206            if (context.getDirection() == SequenceRenderContext.HORIZONTAL)
207            {
208                if (tickDirection == TICKS_UP)
209                {
210                    line.setLine(offset + halfScale, depth - tickHeight,
211                                 offset + halfScale, depth);
212                    g2.drawString(String.valueOf(index),
213                                  (float) (offset + halfScale - halfLabelWidth),
214                                  horizLabelOffset);
215                }
216                else
217                {
218                    line.setLine(offset + halfScale, 0.0,
219                                 offset + halfScale, tickHeight);
220                    g2.drawString(String.valueOf(index),
221                                  (float) (offset + halfScale - halfLabelWidth),
222                                  horizLabelOffset);
223                }
224            }
225            else
226            {
227                if (tickDirection == TICKS_UP)
228                {
229                    line.setLine(depth, offset + halfScale,
230                                 depth - tickHeight, offset + halfScale);
231                    g2.translate(vertLabelOffset,
232                                 offset + halfScale + halfLabelWidth);
233                    g2.transform(antiQuarter);
234                    g2.drawString(String.valueOf(index), 0.0f, 0.0f);
235                    g2.setTransform(prevTransform);
236                }
237                else
238                {
239                    line.setLine(0.0f, offset + halfScale,
240                                 tickHeight, offset + halfScale);
241                    g2.translate(vertLabelOffset,
242                                 offset + halfScale + halfLabelWidth);
243                    g2.transform(antiQuarter);
244                    g2.drawString(String.valueOf(index), 0.0f, 0.0f);
245                    g2.setTransform(prevTransform);
246                }
247            }
248            g2.draw(line);
249        }
250    }
251
252    public SequenceViewerEvent processMouseEvent(SequenceRenderContext context,
253                                                 MouseEvent            me,
254                                                 List                  path)
255    {
256        path.add(this);
257        int sPos = context.graphicsToSequence(me.getPoint());
258        return new SequenceViewerEvent(this, null, sPos, me, path);
259    }
260}