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.event.MouseEvent;
028import java.awt.geom.Line2D;
029import java.awt.geom.Rectangle2D;
030import java.io.Serializable;
031import java.util.Iterator;
032import java.util.List;
033
034import org.biojava.bio.BioError;
035import org.biojava.bio.seq.FeatureFilter;
036import org.biojava.bio.seq.FeatureHolder;
037import org.biojava.bio.seq.StrandedFeature.Strand;
038import org.biojava.bio.seq.homol.SimilarityPairFeature;
039import org.biojava.bio.symbol.Location;
040import org.biojava.bio.symbol.RangeLocation;
041import org.biojava.utils.AbstractChangeable;
042import org.biojava.utils.ChangeEvent;
043import org.biojava.utils.ChangeSupport;
044import org.biojava.utils.ChangeType;
045import org.biojava.utils.ChangeVetoException;
046
047/**
048 * <p><code>PairwiseDiagonalRenderer</code> renders a region of
049 * similarity between two sequences as a straight line. The effect
050 * produced is similar to a dotplot. This implementation requires that
051 * these regions be represented by
052 * <code>SimilarityPairFeature</code>s.<p>
053 *
054 * <p>Drawing outside the visible area using a range of valid
055 * <code>double</code>s may cause Java to hang (Sun JDK 1.3.1 on
056 * Linux, Compaq JDK 1.3.1 on Tru64, but not Sun JDK 1.4.0-beta2-b77
057 * on Linux). I got round this by manual clipping of the lines to the
058 * clip area. The code uses an implementation of the Cohen-Sutherland
059 * line-clipping algorithm which clips lines to within a
060 * rectangle.</p>
061 *
062 * <p>The clipping code is taken from Computer Graphics for Java
063 * Programmers by Leen Ammeraal (1998, ISBN 0-471-98142-7) and
064 * cosmetically altered to support Java2D objects. Any bugs introduced
065 * are my responsibility.</p>
066 *
067 * @author Keith James
068 * @author Leen Ammeraal
069 * @since 1.2
070 */
071public class PairwiseDiagonalRenderer extends AbstractChangeable
072    implements PairwiseSequenceRenderer, Serializable
073{
074    /**
075     * Constant <code>OUTLINE</code> indicating a change to the fill of
076     * the features.
077     */
078    public static final ChangeType OUTLINE =
079        new ChangeType("The outline paint has changed",
080                       "org.biojava.bio.gui.sequence.PairwiseDiagonalRenderer",
081                       "OUTLINE", SequenceRenderContext.REPAINT);
082
083    /**
084     * <code>spf</code> is a filter which excludes all features except
085     * <code>SimilarityPairFeature</code>s.
086     */
087    private static FeatureFilter spf;
088
089    static
090    {
091        String className =
092            "org.biojava.bio.seq.homol.SimilarityPairFeature";
093
094        try
095        {
096            spf = new FeatureFilter.ByClass(Class.forName(className));
097        }
098        catch (Exception e)
099        {
100            throw new BioError("Failed to load Class for " + className, e);
101        }
102    }
103
104    /**
105     * <code>line</code> is the line to be drawn for each feature.
106     */
107    protected Line2D.Float line;
108
109    /**
110     * <code>outline</code> is the line colour.
111     */
112    protected Paint outline;
113
114    /**
115     * Creates a new <code>PairwiseDiagonalRenderer</code> which will
116     * draw black lines.
117     */
118    public PairwiseDiagonalRenderer()
119    {
120        this(Color.black);
121    }
122
123    /**
124     * Creates a new <code>PairwiseDiagonalRenderer</code> which will
125     * draw lines using the specified <code>Paint</code>.
126     *
127     * @param outline a <code>Paint</code>.
128     */
129    public PairwiseDiagonalRenderer(Paint outline)
130    {
131        line = new Line2D.Float();
132        this.outline = outline;
133    }
134
135    /**
136     * <code>paint</code> renders the feature as a simple line.
137     *
138     * @param g2 a <code>Graphics2D</code>.
139     * @param context a <code>PairwiseRenderContext</code>.
140     */
141    public void paint(Graphics2D g2, PairwiseRenderContext context)
142    {
143        FeatureHolder fh;
144
145        if (context.getDirection() == PairwiseRenderContext.HORIZONTAL)
146        {
147            fh = context.getFeatures().filter(new
148                FeatureFilter.And(new FeatureFilter.OverlapsLocation(context.getRange()),
149                                  spf), false);
150        }
151        else
152        {
153            fh = context.getFeatures().filter(new
154                FeatureFilter.And(new FeatureFilter.OverlapsLocation(context.getSecondaryRange()),
155                                  spf), false);
156        }
157
158        for (Iterator fi = fh.features(); fi.hasNext();)
159        {
160            SimilarityPairFeature f1 = (SimilarityPairFeature) fi.next();
161            SimilarityPairFeature f2 = f1.getSibling();
162
163            Strand s1 = f1.getStrand();
164            Strand s2 = f2.getStrand();
165
166            Location loc1 = f1.getLocation();
167            Location loc2 = f2.getLocation();
168
169            int min1 = loc1.getMin();
170            int max1 = loc1.getMax();
171
172            int min2 = loc2.getMin();
173            int max2 = loc2.getMax();
174
175            if (context.getDirection() == PairwiseRenderContext.HORIZONTAL)
176            {
177                float posX1 = (float) context.sequenceToGraphics(min1);
178                float posY1 = (float) context.secondarySequenceToGraphics(min2);
179                float posX2 = (float) context.sequenceToGraphics(max1);
180                float posY2 = (float) context.secondarySequenceToGraphics(max2);
181
182                if (s1 == s2)
183                    line.setLine(posX1, posY1, posX2, posY2);
184                else
185                    line.setLine(posX2, posY1, posX1, posY2);
186            }
187            else
188            {
189                float posY1 = (float) context.sequenceToGraphics(min1);
190                float posX1 = (float) context.secondarySequenceToGraphics(min2);
191                float posY2 = (float) context.sequenceToGraphics(max1);
192                float posX2 = (float) context.secondarySequenceToGraphics(max2);
193
194                if (s1 == s2)
195                    line.setLine(posX1, posY1, posX2, posY2);
196                else
197                    line.setLine(posX2, posY1, posX1, posY2);
198            }
199
200            Rectangle2D clip = g2.getClipBounds();
201
202            clipLine((float) clip.getMinX(), (float) clip.getMaxX(),
203                     (float) clip.getMinY(), (float) clip.getMaxY(),line);
204
205            g2.setPaint(outline);
206            g2.draw(line);
207        }
208    }
209
210    /**
211     * <code>getOutline</code> returns the colour used to draw the
212     * lines.
213     *
214     * @return a <code>Paint</code>.
215     */
216    public Paint getOutline()
217    {
218        return outline;
219    }
220
221    /**
222     * <code>setOutline</code> sets the the colour used to draw the
223     * lines.
224     *
225     * @param outline a <code>Paint</code>.
226     *
227     * @exception ChangeVetoException if an error occurs.
228     */
229    public void setOutline(Paint outline) throws ChangeVetoException
230    {
231        if (hasListeners())
232        {
233            ChangeSupport cs = getChangeSupport(SequenceRenderContext.REPAINT);
234            synchronized(cs)
235            {
236                ChangeEvent ce = new ChangeEvent(this, SequenceRenderContext.REPAINT,
237                                                 null, null,
238                                                 new ChangeEvent(this, OUTLINE,
239                                                                 outline,
240                                                                 this.outline));
241                cs.firePreChangeEvent(ce);
242                this.outline = outline;
243                cs.firePostChangeEvent(ce);
244            }
245        }
246        else
247        {
248            this.outline = outline;
249        }
250    }
251
252    /**
253     * <code>processMouseEvent</code> acts on a mouse gesture. The
254     * target object is a <code>FeatureHolder</code> containing the
255     * features on the primary sequence which contain the mouse
256     * pointer.
257     *
258     * @param context a <code>PairwiseRenderContext</code>.
259     * @param me a <code>MouseEvent</code>.
260     * @param path a <code>List</code>.
261     *
262     * @return a <code>SequenceViewerEvent</code>.
263     */
264    public SequenceViewerEvent processMouseEvent(PairwiseRenderContext context,
265                                                 MouseEvent            me,
266                                                 List                  path)
267    {
268        path.add(this);
269
270        double gPos;
271
272        if (context.getDirection() == PairwiseRenderContext.HORIZONTAL)
273            gPos = me.getPoint().getX();
274        else
275            gPos = me.getPoint().getY();
276
277        int priMin = context.graphicsToSequence(gPos);
278        int priMax = context.graphicsToSequence(gPos + 1);
279
280        FeatureHolder fh = context.getFeatures().filter(new
281            FeatureFilter.And(new FeatureFilter.OverlapsLocation(new
282                RangeLocation(priMin, priMax)), spf), false);
283
284        return new SequenceViewerEvent(this, fh, priMin, me, path);
285    }
286
287    /**
288     * <code>clipLine</code> clips the line to within the rectangle.
289     * Cohen-Sutherland clipping implementation by Leen Ammeraal.
290     *
291     * @param xMin a <code>float</code>.
292     * @param xMax a <code>float</code>.
293     * @param yMin a <code>float</code>.
294     * @param yMax a <code>float</code>.
295     * @param line a <code>Line2D.Float</code>.
296     */
297    private void clipLine(float xMin, float xMax,
298                          float yMin, float yMax, Line2D.Float line)
299    {
300        int clipTypeX = calcClipType(xMin, xMax,
301                                     yMin, yMax,
302                                     line.x1, line.y1);
303
304        int clipTypeY = calcClipType(xMin, xMax,
305                                     yMin, yMax,
306                                     line.x2, line.y2);
307
308        float dx, dy;
309
310        if ((clipTypeX | clipTypeY) != 0)
311        {
312            if ((clipTypeX & clipTypeY) != 0)
313                return;
314
315            dx = line.x2 - line.x1;
316            dy = line.y2 - line.y1;
317
318            if (clipTypeX != 0)
319            {
320                if ((clipTypeX & 8) == 8)
321                {
322                    line.y1 += (xMin - line.x1) * dy / dx;
323                    line.x1 = xMin;
324                }
325                else if ((clipTypeX & 4) == 4)
326                {
327                    line.y1 += (xMax - line.x1) * dy / dx;
328                    line.x1 = xMax;
329                }
330                else if ((clipTypeX & 2) == 2)
331                {
332                    line.x1 += (yMin - line.y1) * dx / dy;
333                    line.y1 = yMin;
334                }
335                else if ((clipTypeX & 1) == 1)
336                {
337                    line.x1 += (yMax - line.y1) * dx / dy;
338                    line.y1 = yMax;
339                }
340            }
341            else if (clipTypeY != 0)
342            {
343                if ((clipTypeY & 8) == 8)
344                {
345                    line.y2 += (xMin - line.x2) * dy / dx;
346                    line.x2 = xMin;
347                }
348                else if ((clipTypeY & 4) == 4)
349                {
350                    line.y2 += (xMax - line.x2) * dy / dx;
351                    line.x2 = xMax;
352                }
353                else if ((clipTypeY & 2) == 2)
354                {
355                    line.x2 += (yMin - line.y2) * dx / dy;
356                    line.y2 = yMin;
357                }
358                else if ((clipTypeY & 1) == 1)
359                {
360                    line.x2 += (yMax - line.y2) * dx / dy;
361                    line.y2 = yMax;
362                }
363            }
364        }
365    }
366
367    /**
368     * <code>calcClipType</code> calculates which sort of clipping to
369     * carry out and returns the relevant code.
370     *
371     * @param xMin a <code>float</code>.
372     * @param xMax a <code>float</code>.
373     * @param yMin a <code>float</code>.
374     * @param yMax a <code>float</code>.
375     * @param x a <code>float</code>.
376     * @param y a <code>float</code>.
377     *
378     * @return an <code>int</code> code.
379     */
380    private int calcClipType(float xMin, float xMax,
381                             float yMin, float yMax, float x, float y)
382    {
383        return
384            (x < xMin ? 8 : 0) |
385            (x > xMax ? 4 : 0) |
386            (y < yMin ? 2 : 0) |
387            (y > yMax ? 1 : 0);
388    }
389}