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 */
021package org.biojava.bio.gui.sequence;
022
023import java.awt.Color;
024import java.awt.FontMetrics;
025import java.awt.Graphics2D;
026import java.awt.event.MouseEvent;
027import java.awt.geom.AffineTransform;
028import java.awt.geom.Line2D;
029import java.util.List;
030
031import org.biojava.utils.AbstractChangeable;
032import org.biojava.utils.ChangeEvent;
033import org.biojava.utils.ChangeSupport;
034import org.biojava.utils.ChangeType;
035import org.biojava.utils.ChangeVetoException;
036
037
038/**
039 * OffsetRulerRenderer can render the ruler starting from an arbitrary offset from the sequence.
040 * For example if the Protein contained an N-Terminal His tag then coordinate 1 should correspond
041 * to the start of the protein, not the tag.  This implementation borrows heavily from
042 * RulerRenderer
043 *
044 * @author Mark Southern
045 *
046 * @since 1.5
047 */
048public class OffsetRulerRenderer extends AbstractChangeable implements SequenceRenderer {
049    public static final ChangeType OFFSET = new ChangeType("The ruler offset has changed",
050            "org.biojava.bio.gui.sequence.OffsetRulerRenderer", "OFFSET",
051            SequenceRenderContext.REPAINT
052        );
053    public static final ChangeType TICKS = new ChangeType("The ruler tick direction has changed",
054            "org.biojava.bio.gui.sequence.OffsetRulerRenderer", "TICKS",
055            SequenceRenderContext.REPAINT
056        );
057    public static final int TICKS_UP = 0;
058    public static final int TICKS_DOWN = 1;
059    private Line2D line;
060    private double depth;
061    private AffineTransform antiQuarter;
062    private int tickDirection;
063    private float tickHeight;
064    private float horizLabelOffset;
065    private float vertLabelOffset;
066    private int sequenceOffset = 0;
067
068    public OffsetRulerRenderer() throws IllegalArgumentException {
069        this(TICKS_DOWN, 0);
070    }
071
072    public OffsetRulerRenderer(int tickDirection, int sequenceOffset)
073        throws IllegalArgumentException {
074        this.sequenceOffset = sequenceOffset;
075
076        line = new Line2D.Double();
077        antiQuarter = AffineTransform.getRotateInstance(Math.toRadians(-90));
078
079        if ((tickDirection == TICKS_UP) || (tickDirection == TICKS_DOWN)) {
080            this.tickDirection = tickDirection;
081        } else {
082            throw new IllegalArgumentException(
083                "Tick direction may only be set to RulerRenderer.TICKS_UP or RulerRenderer.TICKS_DOWN"
084            );
085        }
086
087        depth = 20.0;
088        tickHeight = 4.0f;
089
090        horizLabelOffset = (( float ) depth) - tickHeight - 2.0f;
091        vertLabelOffset = (( float ) depth) - ((tickHeight + 2.0f) * 2.0f);
092    }
093
094    public void setSequenceOffset(int offset) throws ChangeVetoException {
095        if (hasListeners()) {
096            ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT);
097
098            synchronized (cs) {
099                ChangeEvent ce = new ChangeEvent(this, OFFSET);
100                cs.firePreChangeEvent(ce);
101                this.sequenceOffset = offset;
102                cs.firePostChangeEvent(ce);
103            }
104        } else {
105            this.sequenceOffset = offset;
106        }
107    }
108
109    public int getSequenceOffset() {
110        return this.sequenceOffset;
111    }
112
113    public void setTickDirection(int dir) throws ChangeVetoException {
114        if (hasListeners()) {
115            ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT);
116
117            synchronized (cs) {
118                ChangeEvent ce = new ChangeEvent(this, TICKS);
119                cs.firePreChangeEvent(ce);
120                tickDirection = dir;
121                cs.firePostChangeEvent(ce);
122            }
123        } else {
124            tickDirection = dir;
125        }
126    }
127
128    public int getTickDirection() {
129        return tickDirection;
130    }
131
132    public double getMinimumLeader(SequenceRenderContext context) {
133        return 0.0;
134    }
135
136    public double getMinimumTrailer(SequenceRenderContext context) {
137        return 0.0;
138    }
139
140    public double getDepth(SequenceRenderContext src) {
141        return depth + 1.0;
142    }
143
144    public void paint(Graphics2D g2, SequenceRenderContext context) {
145        g2.setStroke(new java.awt.BasicStroke(1F));
146
147        AffineTransform prevTransform = g2.getTransform();
148
149        g2.setPaint(Color.black);
150
151        int min = context.getRange().getMin();
152        int max = context.getRange().getMax();
153        double minX = context.sequenceToGraphics(min);
154        double maxX = context.sequenceToGraphics(max);
155        double scale = context.getScale();
156
157        double halfScale = scale * 0.5;
158
159        if (context.getDirection() == SequenceRenderContext.HORIZONTAL) {
160            if (tickDirection == TICKS_UP) {
161                line.setLine(minX - halfScale, depth, maxX + halfScale, depth);
162            } else {
163                line.setLine(minX - halfScale, 0.0, maxX + halfScale, 0.0);
164            }
165        } else {
166            if (tickDirection == TICKS_UP) {
167                line.setLine(depth, minX - halfScale, depth, maxX + halfScale);
168            } else {
169                line.setLine(0.0, minX - halfScale, 0.0, maxX + halfScale);
170            }
171        }
172
173        g2.draw(line);
174
175        FontMetrics fMetrics = g2.getFontMetrics();
176
177        // The widest (== maxiumum) coordinate to draw
178        int coordWidth = fMetrics.stringWidth(Integer.toString(max));
179
180        // Minimum gap getween ticks
181        double minGap = ( double ) Math.max(coordWidth, 40);
182
183        // How many symbols does a gap represent?
184        int realSymsPerGap = ( int ) Math.ceil(((minGap + 5.0) / context.getScale()));
185
186        // We need to snap to a value beginning 1, 2 or 5.
187        double exponent = Math.floor(Math.log(realSymsPerGap) / Math.log(10));
188        double characteristic = realSymsPerGap / Math.pow(10.0, exponent);
189
190        int snapSymsPerGap;
191
192        if (characteristic > 5.0) {
193            // Use unit ticks
194            snapSymsPerGap = ( int ) Math.pow(10.0, exponent + 1.0);
195        } else if (characteristic > 2.0) {
196            // Use ticks of 5
197            snapSymsPerGap = ( int ) (5.0 * Math.pow(10.0, exponent));
198        } else {
199            snapSymsPerGap = ( int ) (2.0 * Math.pow(10.0, exponent));
200        }
201
202        min -= Math.abs(sequenceOffset);
203        max += Math.abs(sequenceOffset);
204
205        int minP = min + ((snapSymsPerGap - min) % snapSymsPerGap);
206
207        for (int index = minP; index <= max; index += snapSymsPerGap) {
208            double offset = context.sequenceToGraphics(index + sequenceOffset);
209            String labelString = String.valueOf(index); // + sequenceOffset);
210            float halfLabelWidth = fMetrics.stringWidth(labelString) / 2;
211
212            if (context.getDirection() == SequenceRenderContext.HORIZONTAL) {
213                if (tickDirection == TICKS_UP) {
214                    line.setLine(offset + halfScale, depth - tickHeight, offset + halfScale, depth);
215                    g2.drawString(labelString, ( float ) ((offset + halfScale) - halfLabelWidth),
216                        horizLabelOffset
217                    );
218                } else {
219                    line.setLine(offset + halfScale, 0.0, offset + halfScale, tickHeight);
220                    g2.drawString(labelString, ( float ) ((offset + halfScale) - halfLabelWidth),
221                        horizLabelOffset
222                    );
223                }
224            } else { // vertical
225
226                if (tickDirection == TICKS_UP) {
227                    line.setLine(depth, offset + halfScale, depth - tickHeight, offset + halfScale);
228                    g2.translate(vertLabelOffset, offset + halfScale + halfLabelWidth);
229                    g2.transform(antiQuarter);
230                    g2.drawString(labelString, 0.0f, 0.0f);
231                    g2.setTransform(prevTransform);
232                } else {
233                    line.setLine(0.0f, offset + halfScale, tickHeight, offset + halfScale);
234                    g2.translate(vertLabelOffset, offset + halfScale + halfLabelWidth);
235                    g2.transform(antiQuarter);
236                    g2.drawString(labelString, 0.0f, 0.0f);
237                    g2.setTransform(prevTransform);
238                }
239            }
240
241            g2.draw(line);
242        }
243    }
244
245    public SequenceViewerEvent processMouseEvent(SequenceRenderContext context, MouseEvent me,
246        List path
247    ) {
248        path.add(this);
249
250        int sPos = context.graphicsToSequence(me.getPoint());
251
252        return new SequenceViewerEvent(this, null, sPos, me, path);
253    }
254}