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.Graphics2D;
025import java.awt.event.MouseEvent;
026import java.io.Serializable;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030
031import org.biojava.bio.BioError;
032import org.biojava.bio.seq.FeatureFilter;
033import org.biojava.bio.seq.FeatureHolder;
034import org.biojava.utils.AbstractChangeable;
035import org.biojava.utils.ChangeEvent;
036import org.biojava.utils.ChangeForwarder;
037import org.biojava.utils.ChangeListener;
038import org.biojava.utils.ChangeSupport;
039import org.biojava.utils.ChangeType;
040import org.biojava.utils.ChangeVetoException;
041import org.biojava.utils.Changeable;
042import org.biojava.utils.cache.CacheMap;
043import org.biojava.utils.cache.FixedSizeMap;
044
045/**
046 * <p><code>PairwiseFilteringRenderer</code> wraps a
047 * <code>PairwiseSequenceRenderer</code> and filters the
048 * <code>PairwiseRenderContext</code>s passed to it. The renderer
049 * receives a new <code>PairwiseRenderContext</code> which has had
050 * both of its <code>FeatureHolder</code>s filtered with the
051 * <code>FeatureFilter</code>.<p>
052 *
053 * <p>Instances of this class cache up to 5 of the derived
054 * <code>PairwiseRenderContext</code>s. Therefore cycling through up
055 * to 5 different <code>FeatureFilter</code>s will only be hitting the
056 * cache rather than recalculating everthing. Should the
057 * <code>FeatureHolder</code>s themselves change, the cache will be
058 * flushed. As only the features overlapping the context's range are
059 * filtered, changing the range will also result in re-filtering.</p>
060 *
061 * @author Keith James
062 * @author Matthew Pocock
063 * @since 1.2
064 */
065public class PairwiseFilteringRenderer extends AbstractChangeable
066    implements PairwiseSequenceRenderer, Serializable
067{
068    /**
069     * Constant <code>FILTER</code> indicating a change to the
070     * renderer's filter.
071     */
072    public static final ChangeType FILTER =
073        new ChangeType("The filter has changed",
074                       "org.biojava.bio.gui.sequence.PairwiseFilteringRenderer",
075                       "FILTER",
076                       SequenceRenderContext.LAYOUT);
077
078    /**
079     * Constant <code>RECURSE</code> indicating a change to the
080     * renderer's filter recursion flag.
081     */
082    public static final ChangeType RECURSE =
083        new ChangeType("The recurse flag has changed",
084                       "org.biojava.bio.gui.sequence.PairwiseFilteringRenderer",
085                       "RECURSE",
086                       SequenceRenderContext.LAYOUT);
087
088    /**
089     * Constant <code>RENDERER</code> indicating a change to the
090     * renderer.
091     */
092    public static final ChangeType RENDERER =
093        new ChangeType("The renderer has changed",
094                       "org.biojava.bio.gui.sequence.PairwiseFilteringRenderer",
095                       "RENDERER",
096                       SequenceRenderContext.REPAINT);
097
098    /**
099     * <code>filter</code> is the filter applied to both
100     * <code>FeatureHolder</code>s.
101     */
102    protected FeatureFilter filter;
103    /**
104     * <code>recurse</code> indicates whether the filter should
105     * recurse through any subfeatures.
106     */
107    protected boolean recurse;
108
109    // Cache of previously created subcontexts
110    private CacheMap subContextCache = new FixedSizeMap(5);
111    // Set of listeners to cache keys
112    private Set        cacheFlushers = new HashSet();
113
114    // Delegate renderer
115    private PairwiseSequenceRenderer  renderer;
116    private transient ChangeForwarder rendererForwarder;
117
118    /**
119     * Creates a new <code>PairwiseFilteringRenderer</code> which uses
120     * a filter which accepts all features.
121     *
122     * @param renderer a <code>PairwiseSequenceRenderer</code>.
123     */
124    public PairwiseFilteringRenderer(PairwiseSequenceRenderer renderer)
125    {
126        this(renderer, FeatureFilter.all, false);
127    }
128
129    /**
130     * Creates a new <code>PairwiseFilteringRenderer</code>.
131     *
132     * @param renderer a <code>PairwiseSequenceRenderer</code>.
133     * @param filter a <code>FeatureFilter</code>.
134     * @param recurse a <code>boolean</code>.
135     */
136    public PairwiseFilteringRenderer(PairwiseSequenceRenderer renderer,
137                                     FeatureFilter            filter,
138                                     boolean                  recurse)
139    {
140        try
141        {
142            this.renderer = renderer;
143            setFilter(filter);
144            setRecurse(recurse);
145        }
146        catch (ChangeVetoException cve)
147        {
148            throw new BioError("Assertion failed: should have no listeners", cve);
149        }
150    }
151
152    protected ChangeSupport getChangeSupport(ChangeType ct)
153    {
154        ChangeSupport cs = super.getChangeSupport(ct);
155
156        if (rendererForwarder == null)
157        {
158            rendererForwarder =
159                new PairwiseSequenceRenderer.PairwiseRendererForwarder(this, cs);
160
161            if (renderer instanceof Changeable)
162            {
163                Changeable c = (Changeable) renderer;
164                c.addChangeListener(rendererForwarder,
165                                    SequenceRenderContext.REPAINT);
166            }
167        }
168
169        return cs;
170    }
171
172    /**
173     * <code>getRenderer</code> return the current renderer.
174     *
175     * @return a <code>PairwiseSequenceRenderer</code>.
176     */
177    public PairwiseSequenceRenderer getRenderer()
178    {
179        return renderer;
180    }
181
182    /**
183     * <code>setRenderer</code> sets the renderer.
184     *
185     * @param renderer a <code>PairwiseSequenceRenderer</code>.
186     *
187     * @exception ChangeVetoException if the change is vetoed.
188     */
189    public void setRenderer(PairwiseSequenceRenderer renderer)
190        throws ChangeVetoException
191    {
192        if (hasListeners())
193        {
194            ChangeEvent ce = new ChangeEvent(this, RENDERER,
195                                             renderer, this.renderer);
196
197            ChangeSupport cs = getChangeSupport(RENDERER);
198            synchronized(cs)
199            {
200                cs.firePreChangeEvent(ce);
201
202                if (this.renderer instanceof Changeable)
203                {
204                    Changeable c = (Changeable) this.renderer;
205                    c.removeChangeListener(rendererForwarder);
206                }
207
208                this.renderer = renderer;
209
210                if (renderer instanceof Changeable)
211                {
212                    Changeable c = (Changeable) renderer;
213                    c.addChangeListener(rendererForwarder);
214                }
215                cs.firePostChangeEvent(ce);
216            }
217        }
218        else
219        {
220            this.renderer = renderer;
221        }
222    }
223
224    /**
225     * <code>getFilter</code> returns the current filter.
226     *
227     * @return a <code>FeatureFilter</code>.
228     */
229    public FeatureFilter getFilter()
230    {
231        return filter;
232    }
233
234    /**
235     * <code>setFilter</code> sets the filter.
236     *
237     * @param filter a <code>FeatureFilter</code>.
238     *
239     * @exception ChangeVetoException if the change is vetoed.
240     */
241    public void setFilter(FeatureFilter filter)
242        throws ChangeVetoException
243    {
244        if (hasListeners())
245        {
246            ChangeSupport cs = getChangeSupport(FILTER);
247            synchronized(cs)
248            {
249                ChangeEvent ce = new ChangeEvent(this, FILTER,
250                                                 this.filter, filter);
251                cs.firePreChangeEvent(ce);
252                this.filter = filter;
253                cs.firePostChangeEvent(ce);
254            }
255        }
256        else
257        {
258            this.filter = filter;
259        }
260    }
261
262    /**
263     * <code>getRecurse</code> returns the recursion flag of the
264     * filter.
265     *
266     * @return a <code>boolean</code>.
267     */
268    public boolean getRecurse()
269    {
270        return recurse;
271    }
272
273    /**
274     * <code>setRecurse</code> sets the recursion flag on the filter.
275     *
276     * @param recurse a <code>boolean</code>.
277     *
278     * @exception ChangeVetoException if the change is vetoed.
279     */
280    public void setRecurse(boolean recurse) throws ChangeVetoException
281    {
282        if (hasListeners())
283        {
284            ChangeSupport cs = getChangeSupport(RECURSE);
285            synchronized(cs)
286            {
287                ChangeEvent ce = new ChangeEvent(this, RECURSE,
288                                                 new Boolean(recurse),
289                                                 new Boolean(this.recurse));
290                cs.firePreChangeEvent(ce);
291                this.recurse = recurse;
292                cs.firePostChangeEvent(ce);
293            }
294        }
295        else
296        {
297            this.recurse = recurse;
298        }
299    }
300
301    public void paint(Graphics2D g2, PairwiseRenderContext context)
302    {
303        renderer.paint(g2, getSubContext(context));
304    }
305
306    public SequenceViewerEvent processMouseEvent(PairwiseRenderContext context,
307                                                 MouseEvent            me,
308                                                 List                  path)
309    {
310        path.add(this);
311        return renderer.processMouseEvent(getSubContext(context), me, path);
312    }
313
314    /**
315     * <code>getSubContext</code> creates a new context which has
316     * <code>FeatureHolder</code>s filtered using the current filter.
317     *
318     * @param context a <code>PairwiseRenderContext</code>.
319     *
320     * @return a <code>PairwiseRenderContext</code>.
321     */
322    protected PairwiseRenderContext getSubContext(PairwiseRenderContext context)
323    {
324        // Filter the sequence features
325        FeatureFilter ff =
326            new FeatureFilter.And(filter,
327                                  new FeatureFilter.OverlapsLocation(context.getRange()));
328
329        // Filter the secondary sequence features
330        FeatureFilter ffSec =
331            new FeatureFilter.And(filter,
332                                  new FeatureFilter.OverlapsLocation(context.getSecondaryRange()));
333
334        // Create a cache key
335        FilteredSubContext cacheKey = new FilteredSubContext(context,
336                                                             ff,
337                                                             ffSec,
338                                                             recurse);
339        // Try the cache for a context first
340        PairwiseRenderContext filtered =
341            (PairwiseRenderContext) subContextCache.get(cacheKey);
342
343        if (filtered == null)
344        {
345            // None in cache, so make a new one
346            filtered =
347                new SubPairwiseRenderContext(context, // context delegate
348                                             null,    // symbols
349                                             null,    // secondary symbols
350                                             context.getFeatures().filter(ff,    recurse),
351                                             context.getFeatures().filter(ffSec, recurse),
352                                             null,    // range
353                                             null);   // secondary range
354
355            // Add to cache
356            subContextCache.put(cacheKey, filtered);
357            // Create a listener for changes in the feature holder
358            CacheFlusher cf = new CacheFlusher(cacheKey);
359            // Add the listener for changes in features
360            ((Changeable) context.getSymbols())
361                .addChangeListener(cf, FeatureHolder.FEATURES);
362            cacheFlushers.add(cf);
363        }
364
365        return filtered;
366    }
367
368    /**
369     * <code>FilteredSubContext</code> is a cache key whose equality
370     * with another such key is determined by context, filter and
371     * recurse values.
372     */
373    private class FilteredSubContext
374    {
375        private PairwiseRenderContext context;
376        private FeatureFilter         filter;
377        private FeatureFilter         secondaryFilter;
378        private boolean               recurse;
379
380        public FilteredSubContext(PairwiseRenderContext context,
381                                  FeatureFilter         filter,
382                                  FeatureFilter         secondaryFilter,
383                                  boolean               recurse)
384        {
385            this.context         = context;
386            this.filter          = filter;
387            this.secondaryFilter = secondaryFilter;
388            this.recurse         = recurse;
389        }
390
391        public boolean equals(Object o)
392        {
393            if (! (o instanceof FilteredSubContext))
394            {
395                return false;
396            }
397
398            FilteredSubContext that = (FilteredSubContext) o;
399
400            return
401                context.equals(that.context) &&
402                filter.equals(that.filter) &&
403                secondaryFilter.equals(that.secondaryFilter) &&
404                (recurse == that.recurse);
405        }
406
407        public int hashCode()
408        {
409            return context.hashCode() ^ filter.hashCode();
410        }
411    }
412
413    /**
414     * <code>CacheFlusher</code> is a listener for changes to the
415     * original context's feature holders. A change forces removal of
416     * its associated cache key from the cache.
417     */
418    private class CacheFlusher implements ChangeListener
419    {
420        private FilteredSubContext fsc;
421
422        public CacheFlusher(FilteredSubContext fsc)
423        {
424            this.fsc = fsc;
425        }
426
427        public void preChange(ChangeEvent ce) { }
428
429        public void postChange(ChangeEvent ce)
430        {
431            subContextCache.remove(fsc);
432            cacheFlushers.remove(this);
433
434            if (hasListeners())
435            {
436                ChangeSupport cs = getChangeSupport(SequenceRenderContext.LAYOUT);
437                synchronized(cs)
438                {
439                    ChangeEvent ce2 =
440                        new ChangeEvent(PairwiseFilteringRenderer.this,
441                                        SequenceRenderContext.LAYOUT);
442                    cs.firePostChangeEvent(ce2);
443                }
444            }
445        }
446    }
447}