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.Rectangle;
026import java.awt.event.MouseEvent;
027import java.awt.geom.AffineTransform;
028import java.io.Serializable;
029import java.net.URL;
030
031import org.biojava.bio.seq.Feature;
032import org.biojava.bio.seq.FeatureHolder;
033import org.biojava.bio.seq.OptimizableFilter;
034import org.biojava.bio.symbol.Location;
035import org.biojava.utils.ChangeVetoException;
036import org.biojava.utils.net.URLFactory;
037
038/**
039 * <code>RectangularImapRenderer</code> is a decorator for
040 * <code>RectangularBeadRenderer</code> which adds the ability to
041 * create HTML image map coordinates which correspond to the feature
042 * rendering produced by the <code>RectangularBeadRenderer</code>.
043 *
044 * @author Keith James
045 * @since 1.3
046 */
047public class RectangularImapRenderer
048    implements BeadFeatureRenderer, ImageMapRenderer, Serializable
049{
050    private RectangularBeadRenderer renderer;
051    private ImageMap imageMap;
052    private URLFactory urlFactory;
053
054    /**
055     * Creates a new <code>RectangularImapRenderer</code>.
056     *
057     * @param renderer a <code>RectangularBeadRenderer</code>.
058     * @param imageMap an <code>ImageMap</code>.
059     * @param urlFactory a <code>URLFactory</code> which should be
060     * capable of creating a suitable URL from each
061     * <code>Feature</code> on the <code>Sequence</code> to be
062     * rendered.
063     */
064    public RectangularImapRenderer(RectangularBeadRenderer renderer,
065                                   ImageMap                imageMap,
066                                   URLFactory              urlFactory)
067    {
068        this.renderer   = renderer;
069        this.imageMap   = imageMap;
070        this.urlFactory = urlFactory;
071    }
072
073    /**
074     * <code>getImageMap</code> returns the current image map.
075     *
076     * @return an <code>ImageMap</code>.
077     */
078    public ImageMap getImageMap()
079    {
080        return imageMap;
081    }
082
083    /**
084     * <code>setImageMap</code> sets the current image map.
085     *
086     * @param imageMap an <code>ImageMap</code>.
087     */
088    public void setImageMap(ImageMap imageMap)
089    {
090        this.imageMap = imageMap;
091    }
092
093    /**
094     * <code>setDelegateRenderer</code> for the specified filter.
095     *
096     * @param filter an <code>OptimizableFilter</code>.
097     * @param renderer a <code>BeadFeatureRenderer</code>.
098     */
099    public void setDelegateRenderer(OptimizableFilter   filter,
100                                    BeadFeatureRenderer renderer)
101    {
102        this.renderer.setDelegateRenderer(filter, renderer);
103    }
104
105    /**
106     * <p><code>renderImageMap</code> writes a set of image map
107     * coordinates corresponding to the rectangle drawn by the
108     * renderer. The hotspots created by this method have the rendered
109     * <code>Feature</code> set as their user object.</p>
110     *
111     * <p>This method is called by <code>renderFeature</code> when a
112     * raster image is rendered.</p>
113     *
114     * @param g2 a <code>Graphics2D</code>.
115     * @param f a <code>Feature</code>.
116     * @param context a <code>SequenceRenderContext</code>.
117     */
118    public void renderImageMap(Graphics2D                 g2,
119                               Feature                     f,
120                               SequenceRenderContext context)
121    {
122        Rectangle bounds = g2.getDeviceConfiguration().getBounds();
123
124        // Safe to cast as bounds come from raster
125        int  mapWidth = (int) bounds.getWidth();
126        int mapHeight = (int) bounds.getHeight();
127
128        URL url = urlFactory.createURL(f);
129
130        double        beadDepth = getBeadDepth();
131        double beadDisplacement = getBeadDisplacement();
132        boolean scaleHeight     = getHeightScaling();
133
134        AffineTransform t = g2.getTransform();
135        double transX = t.getTranslateX();
136        double transY = t.getTranslateY();
137
138        int min, max, dif, x1, y1, x2, y2;
139        double posXW, posYN, width, height;
140
141        Location loc = f.getLocation();
142        
143        min = loc.getMin();
144        max = loc.getMax();
145        dif = max - min;
146
147        if (context.getDirection() == SequenceRenderContext.HORIZONTAL)
148        {
149            posXW  = context.sequenceToGraphics(min);
150            posYN  = beadDisplacement;
151            width  = Math.max(((double) (dif + 1)) * context.getScale(), 1.0);
152
153            if (scaleHeight)
154            {
155                height = Math.min(beadDepth, width / 2.0);
156
157                // If the bead height occupies less than the full height
158                // of the renderer, move it down so that it is central
159                if (height < beadDepth)
160                    posYN += ((beadDepth - height) / 2.0);
161            }
162            else
163            {
164                height = beadDepth;
165            }
166        }
167        else
168        {
169            posXW  = beadDisplacement;
170            posYN  = context.sequenceToGraphics(min);
171            height = Math.max(((double) dif + 1) * context.getScale(), 1.0);
172
173            if (scaleHeight)
174            {
175                width = Math.min(beadDepth, height / 2.0);
176                if (width < beadDepth)
177                    posXW += ((beadDepth - width) /  2.0);
178            }
179            else
180            {
181                width = beadDepth;
182            }
183        }
184
185        // Apply translation and round
186        x1 = (int) Math.floor(posXW + transX);
187        y1 = (int) Math.floor(posYN + transY);
188        x2 = (int) Math.floor(posXW + width + transX);
189        y2 = (int) Math.floor(posYN + height + transY);
190
191        // If the whole rectangle is outside the image then ignore
192        // it
193        if (! (x1 > mapWidth || y1 > mapHeight || x2 < 0 || y2 < 0))
194        {
195            x1 = Math.max(x1, 0);
196            y1 = Math.max(y1, 0);
197            x2 = Math.min(x2, mapWidth);
198            y2 = Math.min(y2, mapHeight);
199
200            Integer [] coordinates = new Integer[4];
201            coordinates[0] = new Integer(x1);
202            coordinates[1] = new Integer(y1);
203            coordinates[2] = new Integer(x2);
204            coordinates[3] = new Integer(y2);
205
206            imageMap.addHotSpot(new ImageMap.HotSpot(ImageMap.RECT,
207                                                     url, coordinates, f));
208        }
209    }
210
211    public void renderFeature(Graphics2D                 g2,
212                              Feature                     f,
213                              SequenceRenderContext context)
214    {
215        renderImageMap(g2, f, context);
216        renderer.renderFeature(g2, f, context);
217    }
218
219    public void renderBead(Graphics2D                 g2,
220                           Feature                     f,
221                           SequenceRenderContext context)
222    {
223        renderer.renderBead(g2, f, context);
224    }
225
226    public double getDepth(SequenceRenderContext context)
227    {
228        return renderer.getDepth(context);
229    }
230
231    public double getBeadDepth()
232    {
233        return renderer.getBeadDepth();
234    }
235
236    public double getBeadDisplacement()
237    {
238        return renderer.getBeadDisplacement();
239    }
240
241    /**
242     * <code>getHeightScaling</code> returns the state of the height
243     * scaling policy.
244     *
245     * @return a <code>boolean</code> true if height scaling is
246     * enabled.
247     */
248    public boolean getHeightScaling()
249    {
250        return renderer.getHeightScaling();
251    }
252
253    /**
254     * <code>setHeightScaling</code> sets the height scaling
255     * policy. Default behaviour is for this to be enabled leading to
256     * features being drawn with a height equal to half their width,
257     * subject to a maximum height restriction equal to the
258     * <code>beadDepth</code> set in the constructor. If disabled,
259     * features will always be drawn at the maximum height allowed by
260     * the <code>beadDepth</code> parameter.
261     *
262     * @param isEnabled a <code>boolean</code>.
263     *
264     * @exception ChangeVetoException if an error occurs.
265     */
266    public void setHeightScaling(boolean isEnabled) throws ChangeVetoException
267    {
268        renderer.setHeightScaling(isEnabled);
269    }
270
271    public FeatureHolder processMouseEvent(FeatureHolder         holder,
272                                           SequenceRenderContext context,
273                                           MouseEvent            mEvent)
274    {
275        return renderer.processMouseEvent(holder, context, mEvent);
276    }
277}