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.Graphics2D;
026import java.awt.Paint;
027import java.awt.Point;
028import java.awt.Rectangle;
029import java.awt.event.MouseEvent;
030import java.awt.geom.Line2D;
031import java.util.List;
032
033import javax.swing.SwingUtilities;
034
035import org.biojava.utils.AbstractChangeable;
036import org.biojava.utils.ChangeEvent;
037import org.biojava.utils.ChangeSupport;
038import org.biojava.utils.ChangeType;
039import org.biojava.utils.ChangeVetoException;
040
041/**
042 * <p>
043 * <code>CrosshairRenderer</code> draws a crosshair, optionally
044 * with coordinates. The crosshair is set to a sequence position by a
045 * click and then stays there through scrolls/rescales until the next
046 * click. See the <code>processMouseEvent</code> documentation for
047 * details of responses to various mouse actions.
048 * </p>
049 *
050 * @author Keith James
051 * @since 1.2
052 */
053public class CrosshairRenderer extends AbstractChangeable
054    implements PairwiseSequenceRenderer 
055{
056    /**
057     * Constant <code>OUTLINE</code> indicating a change to the
058     * crosshair paint.
059     */
060    public static final ChangeType OUTLINE =
061        new ChangeType("The outline paint has changed",
062                       "org.biojava.bio.gui.sequence.CrosshairRenderer",
063                       "OUTLINE", SequenceRenderContext.REPAINT);
064    
065    /**
066     * <code>xHair</code> is the vertical line positioned along the
067     * X-axis.
068     */
069    protected Line2D xHair;
070
071    /**
072     * <code>yHair</code> is the horizontal line positioned along the
073     * Y-axis.
074     */
075    protected Line2D yHair;
076    /**
077     * <code>point</code> is the current location (in sequence
078     * coordinates) of the crosshair in the X and Y sequences.
079     */
080    protected Point point;
081
082    // Crosshair location in sequence coordinates
083    private int sPosX, sPosY;
084    // Crosshair colour
085    private Paint outline;
086    // Display coordinates?
087    private boolean display;
088
089    /**
090     * Creates a new <code>CrosshairRenderer</code> in light grey with
091     * coordinates displayed.
092     */
093    public CrosshairRenderer()
094    {
095        this(Color.lightGray);
096    }
097
098    /**
099     * Creates a new <code>CrosshairRenderer</code> of the specified
100     * colour, with coordinates displayed.
101     *
102     * @param outline a <code>Paint</code>.
103     */
104    public CrosshairRenderer(Paint outline)
105    {
106        xHair = new Line2D.Double();
107        yHair = new Line2D.Double();
108        point = new Point();
109
110        sPosX = 1;
111        sPosY = 1;
112
113        display = true;
114
115        this.outline = outline;
116    }
117
118    public void paint(Graphics2D g2, PairwiseRenderContext context)
119    {
120        Rectangle clip = g2.getClipBounds();
121
122        double xMin = clip.getMinX();
123        double xMax = clip.getMaxX();
124        double yMin = clip.getMinY();
125        double yMax = clip.getMaxY();
126
127        // Offset to get the hair to line up with the centre of a
128        // residue
129        double residueCentre = context.getScale() * 0.5;
130
131        double gPosX = context.sequenceToGraphics(sPosX);
132        gPosX += residueCentre;
133
134        double gPosY = context.secondarySequenceToGraphics(sPosY);
135        gPosY += residueCentre;
136
137        if (context.getDirection() == PairwiseRenderContext.HORIZONTAL)
138        {
139            xHair.setLine(gPosX, yMin, gPosX, yMax);
140            yHair.setLine(xMin, gPosY, xMax, gPosY);
141        }
142        else
143        {
144            xHair.setLine(xMin, gPosY, xMax, gPosY);
145            yHair.setLine(gPosX, yMin, gPosX, yMax);
146        }
147
148        g2.setPaint(outline);
149        g2.draw(xHair);
150        g2.draw(yHair);
151
152        if (display)
153        {
154            g2.setFont(context.getFont());
155            g2.drawString(sPosX + ", " + sPosY,
156                          (float) (gPosX + 5.0),
157                          (float) (gPosY - 5.0));
158        }
159    }
160
161    /**
162     * <code>coordinateDisplayOn</code> toggles the display of
163     * sequence coordinates.
164     *
165     * @param display a <code>boolean</code>.
166     */
167    public void coordinateDisplayOn(boolean display)
168    {
169        this.display = display;
170    }
171
172    /**
173     * <code>getOutline</code> returns the colour used to draw the
174     * lines.
175     *
176     * @return a <code>Paint</code>.
177     */
178    public Paint getOutline()
179    {
180        return outline;
181    }
182
183    /**
184     * <code>setOutline</code> sets the the colour used to draw the
185     * lines.
186     *
187     * @param outline a <code>Paint</code>.
188     *
189     * @exception ChangeVetoException if an error occurs.
190     */
191    public void setOutline(Paint outline) throws ChangeVetoException
192    {
193        if (hasListeners())
194        {
195            ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT);
196            synchronized(cs)
197            {
198                ChangeEvent ce = new ChangeEvent(this, SequenceRenderContext.REPAINT,
199                                                 null, null,
200                                                 new ChangeEvent(this, OUTLINE,
201                                                                 outline,
202                                                                 this.outline));
203                cs.firePreChangeEvent(ce);
204                this.outline = outline;
205                cs.firePostChangeEvent(ce);
206            }
207        }
208        else
209        {
210            this.outline = outline;
211        }
212    }
213
214    /**
215     * <p><code>processMouseEvent</code> processes any
216     * <code>MouseEvent</code>s directed to the renderer.</p>
217     *
218     * <p>
219     * Mouse actions are as follows (all are button-1 only):
220     * <ul>
221     *  <li>Click sets the crosshair position and returns an event
222     *      whose target object is the <code>Point</code> in sequence
223     *      coordinates. The X coordinate is in the primary sequence,
224     *      the Y coordinate is in the secondary sequence.</li>
225     *  <li>Press same as Click</li>
226     *  <li>Drag same as Click, except that the <code>Point</code> is
227     *      not set</li>
228     *  <li>Release same as Click, except that the <code>Point</code>
229     *      is not set</li>
230     *  <li>Move same as Click, except that the <code>Point</code> is
231     *      not set and the target is null</li>
232     * </ul>
233     * </p>
234     *
235     * @param context a <code>PairwiseRenderContext</code>.
236     * @param me a <code>MouseEvent</code>.
237     * @param path a <code>List</code>.
238     *
239     * @return a <code>SequenceViewerEvent</code>.
240     */
241    public SequenceViewerEvent processMouseEvent(PairwiseRenderContext context,
242                                                 MouseEvent            me,
243                                                 List                  path)
244    {
245        path.add(this);
246
247        // Only hit the point with left button
248        if (! SwingUtilities.isLeftMouseButton(me))
249            return new SequenceViewerEvent(this, null, sPosX, me, path);
250
251        // Only hit the point with click/release/drag
252        int id = me.getID();
253        if (! (id == MouseEvent.MOUSE_CLICKED ||
254               id == MouseEvent.MOUSE_PRESSED ||
255               id == MouseEvent.MOUSE_DRAGGED ||
256               id == MouseEvent.MOUSE_RELEASED))
257            return new SequenceViewerEvent(this, null, sPosX, me, path);
258
259        // Only move the point on clicks or presses
260        if (id == MouseEvent.MOUSE_CLICKED ||
261            id == MouseEvent.MOUSE_PRESSED)
262        {
263            double gPosX, gPosY;
264
265            if (context.getDirection() == PairwiseRenderContext.HORIZONTAL)
266            {
267                gPosX = me.getPoint().getX();
268                gPosY = me.getPoint().getY();
269            }
270            else
271            {
272                gPosX = me.getPoint().getY();
273                gPosY = me.getPoint().getX();
274            }
275
276            sPosX = context.graphicsToSequence(gPosX);
277            sPosY = context.graphicsToSecondarySequence(gPosY);
278
279            // We were clicked, so set the point here
280            point.setLocation(sPosX, sPosY);
281
282            // Inform any REPAINT listeners that they need to repaint this
283            if (hasListeners())
284            {
285                ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT);
286                synchronized(cs)
287                {
288                    ChangeEvent ce = new ChangeEvent(this, SequenceRenderContext.REPAINT);
289                    cs.firePostChangeEvent(ce);
290                }
291            }
292        }
293
294        return new SequenceViewerEvent(this, point, sPosX, me, path);
295    }
296}