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.chromatogram.graphic;
023
024import java.awt.BasicStroke;
025import java.awt.Color;
026import java.awt.Graphics2D;
027import java.awt.Rectangle;
028import java.awt.Shape;
029import java.awt.Stroke;
030import java.awt.geom.AffineTransform;
031import java.awt.geom.GeneralPath;
032import java.awt.geom.Line2D;
033import java.awt.geom.Point2D;
034import java.awt.geom.Rectangle2D;
035import java.util.HashMap;
036import java.util.Iterator;
037import java.util.Map;
038
039import org.biojava.bio.BioError;
040import org.biojava.bio.chromatogram.Chromatogram;
041import org.biojava.bio.chromatogram.ChromatogramTools;
042import org.biojava.bio.seq.DNATools;
043import org.biojava.bio.symbol.IllegalSymbolException;
044import org.biojava.bio.symbol.SimpleSymbolList;
045import org.biojava.bio.symbol.Symbol;
046import org.biojava.bio.symbol.SymbolList;
047
048/**
049 * Encapsulates a configurable method for drawing a {@link Chromatogram} 
050 * into a graphics context.  
051 *
052 * @author Rhett Sutphin (<a href="http://genome.uiowa.edu/">UI CBCB</a>)
053 * @author Matthew Pocock
054 * @since 1.3
055 */
056public class ChromatogramGraphic implements Cloneable {
057    /** A pseudo call list for use when a Chromatogram has no base calls. */
058    private static SymbolList SINGLE_CALL = 
059        new SimpleSymbolList(new Symbol[] { DNATools.getDNA().getGapSymbol() }, 
060                             1, DNATools.getDNA());
061
062    // array indices for subpaths, etc.
063    private static final int A = 0, C = 1, G = 2, T = 3;
064    
065    private Chromatogram chromat;
066    private float vertScale, horizScale;
067    private int width, height;
068
069  /** Flag for subpaths. */
070    protected boolean subpathsValid;
071
072  /** Flag for call boxes. */
073  protected boolean callboxesValid;
074
075  /** Flag for drawable call boxes. */
076  protected boolean drawableCallboxesValid;
077    
078    private GeneralPath[][] subpaths;
079    private Rectangle2D.Float[] callboxes;
080    private Rectangle2D[] drawableCallboxes;
081    private AffineTransform drawnCallboxesTx;
082
083    /** The map containing the {@link Option}s and values for this instance. */
084    protected Map options;
085    /** The map containing the colors for drawing traces.  Keys are DNA Symbols. */
086    protected Map colors;
087    /** The map containing the fill colors for callboxes.  Keys are DNA Symbols. */
088    protected Map fillColors;
089    
090    /** The default values for the {@link #colors} map. */
091    private static final Map DEFAULT_COLORS = new HashMap();
092    static {
093        DEFAULT_COLORS.put(DNATools.a(),   Color.green);
094        DEFAULT_COLORS.put(DNATools.c(),   Color.blue);
095        DEFAULT_COLORS.put(DNATools.g(),   Color.black);
096        DEFAULT_COLORS.put(DNATools.t(),   Color.red);
097    }
098    
099    /** Default constructor with no Chromatogram. */
100    public ChromatogramGraphic() {
101        this(null);
102    }
103    
104    /**
105     * Creates a new <code>ChromatogramGraphic</code>, initially displaying
106     * the given chromatogram.
107     *
108     * @param c the Chromomatogram to display
109     */
110    public ChromatogramGraphic(Chromatogram c) {
111        options = new HashMap(Option.DEFAULTS);
112        colors = new HashMap();
113        fillColors = new HashMap();
114        height = -1; width = -1;
115        vertScale = -1.0f; horizScale = -1.0f;
116        subpaths = new GeneralPath[4][];
117        for (Iterator it = DEFAULT_COLORS.keySet().iterator() ; it.hasNext() ; ) {
118            Symbol key = (Symbol) it.next();
119            setBaseColor(key, (Color) DEFAULT_COLORS.get(key));
120        }
121        setChromatogram(c);
122        //if (System.getProperty("os.name").equalsIgnoreCase("mac os x")) {
123        //    setOption(Option.USE_PER_SHAPE_TRANSFORM, Boolean.TRUE);
124        //}
125    }
126
127    /**
128     * Precomputes the {@link java.awt.geom.GeneralPath}s used to draw the
129     * traces.
130     */
131    protected synchronized void generateSubpaths() {
132        if (chromat != null && !subpathsValid) {
133            if (subpaths == null)
134                subpaths = new GeneralPath[4][];
135            int[][] samples = new int[4][];
136            try {
137                samples[A] = chromat.getTrace(DNATools.a());
138                samples[C] = chromat.getTrace(DNATools.c());
139                samples[G] = chromat.getTrace(DNATools.g());
140                samples[T] = chromat.getTrace(DNATools.t());
141            } catch (IllegalSymbolException ise) {
142                throw new BioError("Can't happen");
143            }
144            ChromatogramNonlinearScaler horizScaler = (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER);
145            float max = chromat.getMax();
146            int subpathLength = getIntOption(Option.SUBPATH_LENGTH);
147            int countSubpaths = (int)Math.ceil( horizScaler.scale(chromat, samples[A].length-1) / subpathLength );
148            int[] subpathTraceBounds = new int[countSubpaths+1];
149            {
150                subpathTraceBounds[0] = 0;
151                int bound = 1;
152                float scaled;
153                for (int i = 0 ; i < samples[A].length ; i++) {
154                    scaled = horizScaler.scale(chromat, i);
155                    if (scaled >= bound*subpathLength) {
156                        subpathTraceBounds[bound] = i;
157                        bound++;
158                    }
159                }
160                subpathTraceBounds[countSubpaths] = samples[A].length - 1;
161            }
162            int offsetIdx, thisPathLen;
163            for (int i = 0 ; i < 4 ; i++) {
164                subpaths[i] = new GeneralPath[countSubpaths];
165                for (int j = 0 ; j < countSubpaths ; j++) {
166                    thisPathLen = subpathTraceBounds[j+1] - subpathTraceBounds[j];
167                    subpaths[i][j] = new GeneralPath(GeneralPath.WIND_EVEN_ODD, thisPathLen);
168                    offsetIdx = subpathTraceBounds[j];
169                    subpaths[i][j].moveTo(horizScaler.scale(chromat, offsetIdx), max - samples[i][offsetIdx]);
170                    for (int k = offsetIdx+1 ; k < samples[i].length && k <= offsetIdx + thisPathLen ; k++) {
171                        subpaths[i][j].lineTo(horizScaler.scale(chromat, k), max - samples[i][k]);
172                    }
173                }
174            }
175            subpathsValid = true;
176        }
177    }
178    
179    /**
180     * Precomputes the {@link java.awt.geom.Rectangle2D}s that are the in-memory
181     * representation of the callboxes.  These rectangles are used for drawing
182     * (via generateDrawableCallboxes) as well as queries (e.g., 
183     * {@link #getCallContaining}).
184     */
185    protected synchronized void generateCallboxes() {
186        if (chromat != null && !callboxesValid) {
187            if (chromat.getSequenceLength() < 2) {
188                callboxes = new Rectangle2D.Float[1];
189                callboxes[0] = new Rectangle2D.Float(0, 0, chromat.getTraceLength() - 1, chromat.getMax());
190            }
191            else {
192                int[] bcOffsets = ChromatogramTools.getTraceOffsetArray(chromat);
193                if (callboxes == null || callboxes.length != bcOffsets.length)
194                    callboxes = new Rectangle2D.Float[bcOffsets.length];
195                ChromatogramNonlinearScaler horizScaler = 
196                    (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER);
197                float max = chromat.getMax();
198                int leftIdx  = 0;
199                int rightIdx = bcOffsets[0] + (int) Math.floor( ((double) (bcOffsets[1] - bcOffsets[0])) / 2) + 1;
200                float left  = horizScaler.scale(chromat, leftIdx);
201                float right = horizScaler.scale(chromat, rightIdx);
202                callboxes[0] = new Rectangle2D.Float(left, 0, right - left, max);
203                //System.out.println("[cg.gcb] cb[0] left="+leftIdx+":"+left+" center="+bcOffsets[0]+" right="+rightIdx+":"+right+" box="+callboxes[0]);
204                int i = 1;
205                while (i < bcOffsets.length - 1) {
206                    leftIdx = rightIdx;
207                    rightIdx = bcOffsets[i] + (int) Math.floor( ((double) (bcOffsets[i+1] - bcOffsets[i])) / 2) + 1;
208                    left = right;
209                    right = horizScaler.scale(chromat, rightIdx);
210                    callboxes[i] = new Rectangle2D.Float(left, 0, right - left, max);
211                    //System.out.println("[cg.gcb] cb["+i+"] left="+leftIdx+":"+left+" center="+bcOffsets[i]+" right="+rightIdx+":"+right+" box="+callboxes[i]);
212                    i++;
213                }
214                left = right;
215                right = horizScaler.scale(chromat, chromat.getTraceLength() - 1);
216                callboxes[i] = new Rectangle2D.Float(left, 0, right - left, max);
217                //System.out.println("[cg.gcb] cb["+i+"] left="+left+" center="+bcOffsets[i]+" right="+right+" box="+callboxes[i]);
218            }
219            callboxesValid = true;
220            drawableCallboxesValid = false;
221        }
222        //System.out.println("[cg.gcb]");
223        //for (int i = 0 ; i < callboxes.length ; i++)
224        //    System.out.println("callboxes["+i+"]="+callboxes[i]);
225    }
226    
227    /**
228     * Precomputes the callboxes in screen coordinates.
229     * @param shapeTx the transform to apply to the callboxes to move them into
230     *        screen space.
231     */
232    protected synchronized void generateDrawableCallboxes(AffineTransform shapeTx) {
233        if (!callboxesValid)
234            generateCallboxes();
235        if (!shapeTx.equals(drawnCallboxesTx) || !drawableCallboxesValid) {
236            //System.out.println("Regen drawableCallboxes with " + shapeTx + " condition: tx="+shapeTx.equals(drawnCallboxesTx)+" valid="+drawableCallboxesValid);
237            if (drawableCallboxes == null || drawableCallboxes.length != callboxes.length)
238                drawableCallboxes = new Rectangle2D[callboxes.length];
239            for (int i = 0 ; i < drawableCallboxes.length ; i++)
240                drawableCallboxes[i] = shapeTx.createTransformedShape(callboxes[i]).getBounds2D();
241            drawnCallboxesTx = (AffineTransform) shapeTx.clone();
242            drawableCallboxesValid = true;
243            //System.out.println("[cg.gdcb]");
244            //for (int i = 0 ; i < drawableCallboxes.length ; i++)
245            //    System.out.println("cb["+i+"]="+callboxes[i]+"\ndcb["+i+"]="+drawableCallboxes[i]);
246        }
247    }
248    
249    /**
250     * Accessor for the in-use chromatogram.
251     * @return the chromatogram that a call to {@link #drawTo} will draw
252     */
253    public Chromatogram getChromatogram() { return chromat; }
254    /**
255     * Sets the chromatogram to draw.
256     * @param c the new chromatogram
257     * @see Option#WIDTH_IS_AUTHORITATIVE
258     * @see Option#HEIGHT_IS_AUTHORITATIVE
259     */
260    public synchronized void setChromatogram(Chromatogram c) {
261        this.chromat = c;
262        callboxesValid = false;
263        subpathsValid = false;
264        // set width, height, horizScale, vertScale for new chromat (even if null)
265        if (optionIsTrue(Option.WIDTH_IS_AUTHORITATIVE)) 
266            setWidth(width);
267        else
268            setHorizontalScale(horizScale);
269        if (optionIsTrue(Option.HEIGHT_IS_AUTHORITATIVE))
270            setHeight(height);
271        else
272            setVerticalScale(vertScale);
273        // drawing bounds default to show the whole chromat
274        setOption(Option.FROM_TRACE_SAMPLE, new Integer(0));
275        if (c == null)
276            setOption(Option.TO_TRACE_SAMPLE, new Integer(Integer.MAX_VALUE));
277        else
278            setOption(Option.TO_TRACE_SAMPLE, new Integer(c.getTraceLength() - 1));
279        //System.out.println("chromatgfx[w=" + width + "; h=" + height + "; hs=" + horizScale + "; vs=" + vertScale + "]");
280    }
281    
282    /**
283     * Returns the width of the whole graphic (in pixels).
284     *
285     * @return the width
286     */
287    public int getWidth()  { return width;  }
288
289    /**
290     * Returns the height of the whole graphic (in pixels).
291     *
292     * @return the height
293     */
294    public int getHeight() { return height; }
295
296    /**
297     * Returns the in-use horizontal scale factor.
298     * The "units" of this value are (trace samples) / pixel.
299     * For example, a horizontal scale of 1.0 means that there will be one 
300     * pixel horizontally for each trace sample.
301     *
302     * @return the horizontal scale
303     */
304    public float getHorizontalScale() { return horizScale; }
305
306    /**
307     * Returns the in use vertical scale factor.  
308     * The "units" of this value are (trace value bins) / pixel.
309     * For example, a vertical scale of 1.0 means that there will be one 
310     * pixel vertically for each value in the range 
311     * [0, <code>getChromatogram().getMax()</code>].
312     *
313     * @return the vertical scale
314     */
315    public float getVerticalScale()   { return vertScale;  }
316    
317    /** 
318     * Returns the width of the graphic as it will be rendered.
319     * This means that the {@link Option#FROM_TRACE_SAMPLE} and 
320     * {@link Option#TO_TRACE_SAMPLE} bounds are taken into account.
321     *
322     * @return the rendered width
323     */
324    public int getRenderedWidth() { 
325        return getRenderedWidth(horizScale);
326    }
327    
328    /** 
329     * Returns the width of the graphic as it would be rendered with
330     * the specified horizontal scale. The {@link Option#FROM_TRACE_SAMPLE} and 
331     * {@link Option#TO_TRACE_SAMPLE} bounds are taken into account.
332     *
333     * @param horizontalScale  the horizontal scale
334     * @return the rendered width at that scale
335     */
336    public int getRenderedWidth(float horizontalScale) {
337        return (int) Math.ceil(
338                        horizontalScale *
339                            (getFloatOption(Option.TO_TRACE_SAMPLE)   -
340                             getFloatOption(Option.FROM_TRACE_SAMPLE) + 
341                             1)
342                     );
343    }
344    
345    /**
346     * Sets the height (in pixels).  This will also change the
347     * vertical scale.
348     *
349     * @param h the desired height in pixels
350     * @see Option#HEIGHT_IS_AUTHORITATIVE
351     */
352    public void setHeight(int h) {
353        if (h != height) drawableCallboxesValid = false;
354        height = h;
355        if (chromat != null) 
356            vertScale = ( (float) height ) / chromat.getMax();
357        else
358            vertScale = -1.0f;
359    }
360    
361    /**
362     * Sets the vertical scale (proportional).  This will also
363     * change the height.
364     *
365     * @param vs the desired vertical scale.  See {@link #getVerticalScale}
366     *        for semantics.
367     * @see Option#HEIGHT_IS_AUTHORITATIVE
368     */
369    public void setVerticalScale(float vs) {
370        if (vs != vertScale) drawableCallboxesValid = false;
371        vertScale = vs;
372        if (chromat != null)
373            height = (int) (vertScale * chromat.getMax());
374        else
375            height = -1;
376    }
377    
378    /**
379     * Sets the width of the whole graphic (in pixels).  This will also change 
380     * the horizontal scale.
381     *
382     * @param w the desired width in pixels
383     * @see Option#WIDTH_IS_AUTHORITATIVE
384     */
385    public void setWidth(int w) {
386        if (w != width) drawableCallboxesValid = false;
387        width = w;
388        ChromatogramNonlinearScaler horizScaler = 
389            (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER);
390        if (chromat != null)
391            horizScale = ( (float) width ) / horizScaler.scale(chromat, chromat.getTraceLength()-1);
392        else
393            horizScale = -1.0f;
394    }
395    
396    /**
397     * Sets the horizontal scale (proportional).  This will also
398     * change the width.
399     *
400     * @param hs the desired vertical scale.  See {@link #getHorizontalScale}
401     *        for semantics.
402     * @see Option#WIDTH_IS_AUTHORITATIVE
403     */
404    public void setHorizontalScale(float hs) {
405        if (hs != horizScale) drawableCallboxesValid = false;
406        horizScale = hs;
407        
408        ChromatogramNonlinearScaler horizScaler = 
409            (ChromatogramNonlinearScaler) getOption(Option.HORIZONTAL_NONLINEAR_SCALER);
410        if (chromat != null)
411            width = (int) (horizScale * horizScaler.scale(chromat, chromat.getTraceLength()-1));
412        else
413            width = -1;
414    }
415
416    /** 
417     * Returns the color that will be used to draw the trace for the
418     * given DNA symbol.
419     * @param b the symbol
420     * @return the color, or null if none is set
421     */
422    public Color getBaseColor(Symbol b)     { return (Color) colors.get(b); }
423    /** 
424     * Returns the color that will be used to fill in the callboxes for
425     * calls with the given symbol.
426     * @param b the symbol
427     * @return the color, or null if none is set
428     */
429    public Color getBaseFillColor(Symbol b) { return (Color) fillColors.get(b); }
430    /**
431     * Maps a color to a DNA symbol.  The color as specified will be used for
432     * to draw the trace for the symbol (if any).  The fill color for calls to
433     * the symbol will be derived from the trace color.
434     * @param b the symbol
435     * @param c the color
436     */
437    public void setBaseColor(Symbol b, Color c) {
438        colors.put(b, c);
439        // fade color
440        float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null);
441        //System.out.println("Base: "+b+"; H="+hsb[0]+"; S="+hsb[1]+"; B="+hsb[2]);
442        fillColors.put(b, Color.getHSBColor(hsb[0], hsb[1] * 0.09f, Math.max(hsb[2], 0.8f)));
443    }
444    
445    /**
446     * Returns the number of callboxes, regenerating them if necessary.  This 
447     * should almost always equal 
448     * <code>getChromatogram().getSequenceLength()</code>
449     * @return the number of callboxes
450     */
451    public int getCallboxCount() {
452        if (!callboxesValid)
453            generateCallboxes();
454        if (callboxesValid)
455            return callboxes.length;
456        else
457            return 0;
458    }
459    
460    /**
461     * Returns the screen-coordinate bounds of the callbox for a given call.
462     *
463     * @param index the callbox for which to get the bounds <b>0-based</b>
464     * @return a Rectangle2D giving the bounds of the call box
465     */
466    public Rectangle2D getCallboxBounds(int index) {
467        return getCallboxBounds(index, true);
468    }
469    
470    /**
471     * Returns the bounds of the callbox for a given call.
472     * @param index the callbox for which to get the bounds <b>0-based</b>.
473     * @param boundsOnScreen determines the coordinate system of the returned
474     *        bounds
475     * @return the bounds of the callbox in screen coordinates if 
476     *          <code>boundsOnScreen</code> is true, otherwise the bounds
477     *          of the callbox in chromatogram coordinates
478     */
479    public Rectangle2D getCallboxBounds(int index, boolean boundsOnScreen) {
480        if (chromat != null && index >= 0 && index < getCallboxCount()) {
481            if (!callboxesValid)
482                generateCallboxes();
483            if (boundsOnScreen) {
484                if (!drawableCallboxesValid)
485                    generateDrawableCallboxes(getTransform());
486                return drawableCallboxes[index].getBounds2D();
487                //return getTransform().createTransformedShape(callboxes[index]).getBounds2D();
488            }
489            else {
490                return callboxes[index].getBounds2D();
491            }
492        }
493        else {
494            return null;
495        }
496    }
497    
498    /**
499     * Returns the <b>0-based</b> index of the call containing a given
500     * point.  The point may be either in screen space or chromatogram
501     * space, scale-wise.  If the point is in screen space, the caller must 
502     * translate the point such that if it is, for instance, from a mouse 
503     * click, a click on the upper-left corner of the graphic would be (0,0).
504     * @param point the point to search for
505     * @param pointOnScreen if true, the point will be treated as though it
506     *        is in screen space.  Otherwise, it will be considered to be
507     *        in chromatogram space.
508     * @return the <b>0-based</b> index of the callbox which contains the point
509     */
510    public int getCallContaining(Point2D point, boolean pointOnScreen) {
511        if (chromat != null) {
512            if (!callboxesValid)
513                generateCallboxes();
514            Point2D trans = new Point2D.Double(point.getX(), point.getY());
515            if (pointOnScreen)
516                getInvTransform().transform(point, trans);
517            int i = 0;
518            // XXX: binary search is possible since callboxes is sorted
519            while (i < callboxes.length && trans.getX() > callboxes[i].getMaxX())
520                i++;
521            return (i < callboxes.length) ? (i) : (i-1);
522        }
523        else {
524            return 0;
525        }
526    }
527    
528    /** 
529     * Synonym for {@link #getCallContaining(Point2D, boolean)} with
530     * <code>pointOnScreen</code>=true.
531     *
532     * @param point  the Point2D to search with
533     * @return the call containing this point
534     */
535    public int getCallContaining(Point2D point) {
536        return getCallContaining(point, true);
537    }
538    
539    /**
540     * Same as {@link #getCallContaining(Point2D, boolean)}, except that
541     * only the x-coordinate of the point is specified.
542     *
543     * @param x the x-coordinate to search for
544     * @param xOnScreen whether the coordinate in screen space or chromatogram
545     *        space
546     * @return the index of the call containing the position x
547     */
548    public int getCallContaining(float x, boolean xOnScreen) {
549        return getCallContaining(new Point2D.Float(x, 0), xOnScreen);
550    }
551    
552    /** 
553     * Synonym for {@link #getCallContaining(float, boolean)} with
554     * <code>pointOnScreen</code>=true.
555     *
556     * @param x the x-coordinate to search for
557     * @return the index of the call containing the position x
558     */
559    public int getCallContaining(float x) {
560        return getCallContaining(x, true);
561    }
562    
563    private int getSubpathContaining(float x) {
564        // XXX: this could be a binary search
565        Rectangle2D bounds;
566        if (x < subpaths[A][0].getBounds2D().getX())
567            return 0;
568        for (int i = 0 ; i < subpaths[A].length ; i++) {
569            bounds = subpaths[A][i].getBounds2D();
570            if (bounds.getX() <= x && x <= bounds.getMaxX())
571                return i;
572        }
573        return subpaths[A].length - 1;
574    }
575    
576    /**
577     * Returns a new AffineTransform describing the transformation
578     * from chromatogram coordinates to output coordinates.
579     */
580    public AffineTransform getTransform() {
581        AffineTransform at = new AffineTransform();
582        getTransformAndConcat(at);
583        return at;
584    }
585    
586    /**
587     * Concatenates the chromatogram-to-output transform to the 
588     * provided given AffineTransform.
589     */
590    public void getTransformAndConcat(AffineTransform target) {
591        target.scale(horizScale, vertScale);
592        target.translate(-1.0 * getFloatOption(Option.FROM_TRACE_SAMPLE), 0.0);
593    }
594    
595    /** 
596     * Returns a new AffineTransform describing the transformation from
597     * output space to chromatogram space.  Should be much more efficient
598     * than <code>getTransform().createInverse()</code>
599     */
600    public AffineTransform getInvTransform() {
601        AffineTransform at = new AffineTransform();
602        at.translate(getFloatOption(Option.FROM_TRACE_SAMPLE), 0.0);
603        at.scale(1.0 / horizScale, 1.0 / vertScale);
604        return at;
605    }
606    
607    /**
608     * Draws the chromatogram onto the provided graphics context.
609     *
610     * @param g2  the Graphics2D to draw to
611     */
612    public void drawTo(Graphics2D g2) {
613        //System.out.println("drawTo(" + g2 + ", " + fromTraceSample + ", " + toTraceSample + ")");
614        AffineTransform origTx = g2.getTransform();
615        //System.out.println("origTx:   " + origTx);
616        Color origC = g2.getColor();
617        Shape origClip = g2.getClip();
618        //System.out.println("origClip: " + origClip);
619        Stroke origStroke = g2.getStroke();
620        
621        Rectangle2D clip;
622        if (origClip != null)
623            clip = origClip.getBounds2D();
624        else
625            clip = new Rectangle(0, 0, getWidth(), getHeight());
626        //System.out.println("clipping bounds: " + clip);
627
628        // Allow user to decide whether AA should be on
629        //g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
630        //g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
631
632        boolean usePerShpTx = optionIsTrue(Option.USE_PER_SHAPE_TRANSFORM);
633        
634        AffineTransform shapeTx = (AffineTransform) origTx.clone();
635        getTransformAndConcat(shapeTx);
636        if (usePerShpTx)
637            g2.setTransform(new AffineTransform()); // set to identity, do tx per shape
638        else
639            g2.setTransform(shapeTx); // do tx in graphics2d object
640        //System.out.println("using g2.Tx="+g2.getTransform());
641        //System.out.println("shapeTx="+shapeTx);
642        
643        if (optionIsTrue(Option.USE_CUSTOM_STROKE))
644            g2.setStroke((Stroke) getOption(Option.SEPARATOR_STROKE));
645        if (!callboxesValid)
646            generateCallboxes();
647        if (usePerShpTx) {
648            generateDrawableCallboxes(AffineTransform.getScaleInstance(shapeTx.getScaleX(), shapeTx.getScaleY()));
649            g2.translate(shapeTx.getTranslateX(), shapeTx.getTranslateY());
650        }
651        int leftCbIdx = getCallContaining((float)clip.getX()); 
652        int rightCbIdx = getCallContaining((float)clip.getMaxX());
653        //System.out.println("Drawing calls with stroke=" + basicStrokeToString((BasicStroke)g2.getStroke()));
654        //System.out.println("Drawing " + (rightCbIdx - leftCbIdx + 1) + " visible calls from " + leftCbIdx + " to " + rightCbIdx);
655        SymbolList calls = ChromatogramTools.getDNASequence(chromat);
656        // if the chromatogram has no calls, fake it
657        if (calls.length() < 1)
658            calls = SINGLE_CALL;
659        boolean drawSep = optionIsTrue(Option.DRAW_CALL_SEPARATORS);
660        Color fill, line;
661        Line2D.Double sep = new Line2D.Double();
662        int callIdx;
663        for (int i = leftCbIdx ; i <= rightCbIdx ; i++) {
664            callIdx = i + 1;
665            if (doDrawCallbox(calls.symbolAt(callIdx))) {
666                fill = (Color) fillColors.get(calls.symbolAt(callIdx));
667                line = (Color) colors.get(calls.symbolAt(callIdx));
668                if (line == null) {
669                    line = Color.black;
670                    fill = Color.white;
671                }
672                g2.setColor(fill);
673                if (usePerShpTx) {
674                    g2.fill(drawableCallboxes[i]);
675                    //System.out.println("Drawing drawableCallboxes["+i+"]="+drawableCallboxes[i]);
676                }
677                else {
678                    g2.fill(callboxes[i]);
679                    //System.out.println("Drawing callboxes["+i+"]="+callboxes[i]);
680                }
681            }
682            if (drawSep) {
683                g2.setColor((Color) getOption(Option.SEPARATOR_COLOR));
684                if (usePerShpTx)
685                    sep.setLine(drawableCallboxes[i].getX(), 
686                                drawableCallboxes[i].getY(), 
687                                drawableCallboxes[i].getX(), 
688                                drawableCallboxes[i].getMaxY());
689                else
690                    sep.setLine(callboxes[i].x, callboxes[i].y, callboxes[i].x, callboxes[i].y + callboxes[i].height);
691                //System.out.println("sep["+i+"]=(" + sep.x1 + ", " + sep.y1 + ") -> (" + sep.x2 + ", " + sep.y2 + ")");
692                //System.out.println("        " + sep.getBounds2D());
693                g2.draw(sep);
694            }
695        }
696        if (usePerShpTx)
697            g2.translate(-1 * shapeTx.getTranslateX(), -1 * shapeTx.getTranslateY());
698
699        
700        if (optionIsTrue(Option.USE_CUSTOM_STROKE))
701            g2.setStroke((Stroke) getOption(Option.TRACE_STROKE));
702        if (!subpathsValid)
703            generateSubpaths();
704        float toTraceSample   = getFloatOption(Option.TO_TRACE_SAMPLE  );
705        float fromTraceSample = getFloatOption(Option.FROM_TRACE_SAMPLE);
706        //System.out.println("Drawing from trace sample " + fromTraceSample + " to " + toTraceSample);
707        //System.out.println("Drawing traces with stroke=" + basicStrokeToString((BasicStroke)g2.getStroke()));
708        int loSubpath, hiSubpath;
709        //loSubpath = (int)Math.floor((clip.getX() / shapeTx.getScaleX() + fromTraceSample) / subpathLength);
710        loSubpath = getSubpathContaining((float) (clip.getX() / shapeTx.getScaleX() + fromTraceSample));
711        //hiSubpath = (int)Math.floor(Math.min(clip.getMaxX() / shapeTx.getScaleX() + fromTraceSample, toTraceSample) / subpathLength);
712        hiSubpath = getSubpathContaining((float) Math.min(clip.getMaxX() / shapeTx.getScaleX() + fromTraceSample, toTraceSample));
713        //System.out.println("Drawing subpaths ["+loSubpath+","+hiSubpath+"] of "+subpaths[0].length+" each "+subpathLength+" long");
714        if (optionIsTrue(Option.DRAW_TRACE_A)) {
715            g2.setColor((Color) colors.get(DNATools.a()));
716            if (usePerShpTx)
717                for (int j = loSubpath ; j <= hiSubpath ; j++)
718                    g2.draw(shapeTx.createTransformedShape(subpaths[A][j]));
719            else
720                for (int j = loSubpath ; j <= hiSubpath ; j++)
721                    g2.draw(subpaths[A][j]);
722        }
723        if (optionIsTrue(Option.DRAW_TRACE_C)) {
724            g2.setColor((Color) colors.get(DNATools.c()));
725            if (usePerShpTx)
726                for (int j = loSubpath ; j <= hiSubpath ; j++)
727                    g2.draw(shapeTx.createTransformedShape(subpaths[C][j]));
728            else
729                for (int j = loSubpath ; j <= hiSubpath ; j++)
730                    g2.draw(subpaths[C][j]);
731        }
732        if (optionIsTrue(Option.DRAW_TRACE_G)) {
733            g2.setColor((Color) colors.get(DNATools.g()));
734            if (usePerShpTx)
735                for (int j = loSubpath ; j <= hiSubpath ; j++)
736                    g2.draw(shapeTx.createTransformedShape(subpaths[G][j]));
737            else
738                for (int j = loSubpath ; j <= hiSubpath ; j++)
739                    g2.draw(subpaths[G][j]);
740        }
741        if (optionIsTrue(Option.DRAW_TRACE_T)) {
742            g2.setColor((Color) colors.get(DNATools.t()));
743            if (usePerShpTx)
744                for (int j = loSubpath ; j <= hiSubpath ; j++)
745                    g2.draw(shapeTx.createTransformedShape(subpaths[T][j]));
746            else
747                for (int j = loSubpath ; j <= hiSubpath ; j++)
748                    g2.draw(subpaths[T][j]);
749        }
750        
751        g2.setStroke(origStroke);
752        g2.setTransform(origTx);
753        g2.setColor(origC);
754        g2.setClip(origClip);
755    }
756
757    /**
758     * Sets a new value for the specified option.  Be sure that the
759     * value is appropriate per the documentation, or you'll induce a 
760     * ClassCastException somewhere else.
761     *
762     * @param opt Option to set
763     * @param value new value for the option
764     * @see Option
765     */
766    public void setOption(Option opt, Object value) {
767        options.put(opt, value);
768        if (opt == Option.SUBPATH_LENGTH) {
769            subpathsValid = false;
770        }
771        if (opt == Option.HORIZONTAL_NONLINEAR_SCALER) {
772            subpathsValid = false;
773            callboxesValid = false;
774            drawableCallboxesValid = false;
775            if (optionIsTrue(Option.WIDTH_IS_AUTHORITATIVE))
776                setWidth(width);
777            else
778                setHorizontalScale(horizScale);
779        }
780    }
781    
782    /**
783     * Returns the current value for the specified option.
784     * @see Option
785     */
786    public Object getOption(Option opt) {
787        return options.get(opt);
788    }
789    
790    /**
791     * Helper method for converting a {@link java.lang.Boolean}-valued
792     * option into a <code>boolean</code> primitive.
793     *
794     * @param opt the {@link Option} to convert
795     * @return true if the option is enabled
796     * @throws ClassCastException when the option isn't <code>Boolean</code>-valued
797     */
798    public boolean optionIsTrue(Option opt) throws ClassCastException {
799        if (getOption(opt) instanceof Boolean)
800            return ((Boolean)getOption(opt)).booleanValue();
801        else
802            throw new ClassCastException("Option \""+opt+"\" is not set to a Boolean value");
803    }
804    
805    /**
806     * Helper method for converting a {@link java.lang.Number}-valued
807     * option into a <code>float</code> primitive.
808     * @param opt the {@link Option} to convert
809     * @throws ClassCastException when the option isn't <code>Number</code>-valued
810     */
811    public float getFloatOption(Option opt) throws ClassCastException {
812        if (getOption(opt) instanceof Number)
813            return ((Number) getOption(opt)).floatValue();
814        else
815            throw new ClassCastException("Option \""+opt+"\" is not set to a Number value");
816    }
817    
818    /**
819     * Helper method for converting a {@link java.lang.Number}-valued
820     * option into an <code>int</code> primitive.
821     * @param opt the {@link Option} to convert
822     * @throws ClassCastException when the option isn't <code>Number</code>-valued
823     */
824    public int getIntOption(Option opt) throws ClassCastException {
825        if (getOption(opt) instanceof Number)
826            return ((Number) getOption(opt)).intValue();
827        else
828            throw new ClassCastException("Option \""+opt+"\" is not set to a Number value");
829    }
830    
831    /**
832     * Utility method for determining whether to draw a callbox for a particular called Symbol.
833     *
834     * @param bc Symbol to evaluate
835     * @return true if this should be drawn
836     */
837    private boolean doDrawCallbox(Symbol bc) {
838        if      (bc == DNATools.a()) return optionIsTrue(Option.DRAW_CALL_A);
839        else if (bc == DNATools.c()) return optionIsTrue(Option.DRAW_CALL_C);
840        else if (bc == DNATools.g()) return optionIsTrue(Option.DRAW_CALL_G);
841        else if (bc == DNATools.t()) return optionIsTrue(Option.DRAW_CALL_T);
842        else if (DNATools.getDNA().contains(bc)) return optionIsTrue(Option.DRAW_CALL_OTHER);
843        else return false;
844    }
845    
846    /**
847     * Performs a partial deep copy and invalidates regenerable structures.
848     *
849     * @return an Object that is castable to ChromatogramGraphic
850     */
851    public Object clone() {
852        ChromatogramGraphic copy = null;
853        try {
854            copy = (ChromatogramGraphic) super.clone();
855            copy.callboxesValid = false;
856            copy.drawableCallboxesValid = false;
857            copy.drawableCallboxes = null;
858            copy.subpathsValid = false;
859            copy.subpaths = null;
860            // copy options
861            copy.options = new HashMap();
862            for (Iterator it = this.options.keySet().iterator() ; it.hasNext() ; ) {
863                Object next = it.next();
864                copy.options.put(next, this.options.get(next));
865            }
866        } catch (CloneNotSupportedException e) {
867            System.err.println(e);
868            throw new BioError("Can't happen");
869        }
870        return copy;
871    }
872    
873    /**
874     * A typesafe enumeration of the options available for configuring
875     * the behavior of a {@link ChromatogramGraphic} instance.
876     * The semantics and expected values are described with the
877     * enumerated options.
878     *
879     * @author Rhett Sutphin (<a href="http://genome.uiowa.edu/">UI CBCB</a>)
880     * @since 1.3
881     */
882    public static class Option {
883        private String desc;
884        private static Map map = new HashMap();
885
886      /**
887       * Create a new Obtion.
888       *
889       * @param desc  option description
890       * @param def   option default
891       */
892        private Option(String desc, Object def) {
893            this.desc = desc;
894            map.put(desc, this);
895            DEFAULTS.put(this, def);
896        }
897        
898        public String toString() { return desc; }
899        
900        /**
901         * Looks up an <code>Option</code> instance based on its
902         * string description.
903         * @param desc the description of the desired <code>Option</code>
904         * @return the <code>Option</code> with the specified description
905         *          or null if there isn't one
906         */
907        public static final Option lookup(String desc) {
908            return (Option) map.get(desc);
909        }
910        
911        /**
912         * Default values table
913         */
914        static final Map DEFAULTS = new HashMap();
915        
916        /**
917         * Option indicating whether to fill in the callboxes for calls of
918         * nucleotide A.  
919         * <p>
920         * Value type: {@link java.lang.Boolean}.<br/>
921         * Default value: <code>Boolean.TRUE</code>.
922         * </p>
923         */
924        public static final Option DRAW_CALL_A   = new Option("draw-A-calls",     Boolean.TRUE);
925        /**
926         * Option indicating whether to fill in the callboxes for calls of
927         * nucleotide C.  
928         * <p>
929         * Value type: {@link java.lang.Boolean}.<br/>
930         * Default value: <code>Boolean.TRUE</code>.
931         * </p>
932         */
933        public static final Option DRAW_CALL_C   = new Option("draw-C-calls",     Boolean.TRUE);
934        /**
935         * Option indicating whether to fill in the callboxes for calls of
936         * nucleotide G.  
937         * <p>
938         * Value type: {@link java.lang.Boolean}.<br/>
939         * Default value: <code>Boolean.TRUE</code>.
940         * </p>
941         */
942        public static final Option DRAW_CALL_G   = new Option("draw-G-calls",     Boolean.TRUE);
943        /**
944         * Option indicating whether to fill in the callboxes for calls of
945         * nucleotide T.  
946         * <p>
947         * Value type: {@link java.lang.Boolean}.<br/>
948         * Default value: <code>Boolean.TRUE</code>.
949         * </p>
950         */
951        public static final Option DRAW_CALL_T   = new Option("draw-T-calls",     Boolean.TRUE);
952        /**
953         * Option indicating whether to fill in the callboxes for non-base calls
954         * (gaps, ambiguities).
955         * <p>
956         * Value type: {@link java.lang.Boolean}.<br/>
957         * Default value: <code>Boolean.TRUE</code>.
958         * </p>
959         */
960        public static final Option DRAW_CALL_OTHER = new Option("draw-other-calls", Boolean.TRUE);
961        
962        /**
963         * Option indicating whether to draw the chromatogram trace for 
964         * nucleotide A.
965         * <p>
966         * Value type: {@link java.lang.Boolean}.<br/>
967         * Default value: <code>Boolean.TRUE</code>.
968         * </p>
969         */
970        public static final Option DRAW_TRACE_A = new Option("draw-A-trace", Boolean.TRUE);
971        /**
972         * Option indicating whether to draw the chromatogram trace for 
973         * nucleotide C.
974         * <p>
975         * Value type: {@link java.lang.Boolean}.<br/>
976         * Default value: <code>Boolean.TRUE</code>.
977         * </p>
978         */
979        public static final Option DRAW_TRACE_C = new Option("draw-C-trace", Boolean.TRUE);
980        /**
981         * Option indicating whether to draw the chromatogram trace for 
982         * nucleotide G.
983         * <p>
984         * Value type: {@link java.lang.Boolean}.<br/>
985         * Default value: <code>Boolean.TRUE</code>.
986         * </p>
987         */
988        public static final Option DRAW_TRACE_G = new Option("draw-G-trace", Boolean.TRUE);
989        /**
990         * Option indicating whether to draw the chromatogram trace for 
991         * nucleotide T.
992         * <p>
993         * Value type: {@link java.lang.Boolean}.<br/>
994         * Default value: <code>Boolean.TRUE</code>.
995         * </p>
996         */
997        public static final Option DRAW_TRACE_T = new Option("draw-T-trace", Boolean.TRUE);
998        
999        /**
1000         * Option indicating whether to draw vertical lines separating
1001         * the calls.
1002         * <p>
1003         * Value type: {@link java.lang.Boolean}.<br/>
1004         * Default value: <code>Boolean.TRUE</code>.
1005         * </p>
1006         */
1007        public static final Option DRAW_CALL_SEPARATORS = 
1008            new Option("draw-call-separators", Boolean.TRUE);
1009        /**
1010         * Option indicating the color that the call separators
1011         * should be.
1012         * <p>
1013         * Value type: {@link java.awt.Color}.<br/>
1014         * Default value: <code>Color.lightGray</code>.
1015         * </p>
1016         */
1017        public static final Option SEPARATOR_COLOR = 
1018            new Option("separator-color", Color.lightGray);
1019        
1020        /**
1021         * Option indicating whether width or horizontal scale is
1022         * the authoritative measure.  If the value is true, then
1023         * when the Chromatogram displayed by the graphic is changed, the 
1024         * horizontal scale may be changed but the width will stay the same.
1025         * If the value is false, the width may change but the horizontal
1026         * scale will stay the same.
1027         * <p>
1028         * Value type: {@link java.lang.Boolean}.<br/>
1029         * Default value: <code>Boolean.FALSE</code>.
1030         * </p>
1031         */
1032        public static final Option WIDTH_IS_AUTHORITATIVE  = 
1033            new Option("width-is-authoritative", Boolean.FALSE);
1034        /**
1035         * Option indicating whether height or vertical scale is
1036         * the authoritative measure.  If the value is true, then
1037         * when the Chromatogram displayed by the graphic is changed, the 
1038         * vertical scale may be changed but the height will stay the same.
1039         * If the value is false, the height may change but the vertical
1040         * scale will stay the same.
1041         * <p>
1042         * Value type: {@link java.lang.Boolean}.<br/>
1043         * Default value: <code>Boolean.TRUE</code>.
1044         * </p>
1045         */
1046        public static final Option HEIGHT_IS_AUTHORITATIVE = 
1047            new Option("height-is-authoritative", Boolean.TRUE);
1048        
1049        /**
1050         * Option indicating whether to use custom strokes when
1051         * drawing traces and separators.
1052         * <p>
1053         * Value type: {@link java.lang.Boolean}.<br/>
1054         * Default value: <code>Boolean.TRUE</code>.
1055         * </p>
1056         */
1057        public static final Option USE_CUSTOM_STROKE = 
1058            new Option("use-custom-stroke", Boolean.TRUE);
1059        /**
1060         * Option providing the the stroke to use for drawing
1061         * the chromatogram traces.
1062         * <p>
1063         * Value type: {@link java.awt.Stroke}.<br/>
1064         * Default value: {@link BasicStroke} with width 1.0, cap CAP_ROUND, join JOIN_ROUND.
1065         * </p>
1066         */
1067        public static final Option TRACE_STROKE = 
1068            new Option("trace-stroke", new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
1069        /**
1070         * Option providing the the stroke to use for drawing
1071         * call separators.
1072         * <p>
1073         * Value type: {@link java.awt.Stroke}.<br/>
1074         * Default value: {@link BasicStroke} with width 1.0, default cap & join.
1075         * </p>
1076         */
1077        public static final Option SEPARATOR_STROKE = 
1078            new Option("separator-stroke", new BasicStroke(1.0f));
1079
1080        /**
1081         * Option indicating whether to apply scaling and translation
1082         * transforms to each shape individually or to apply a single
1083         * transform to the graphics context.  For putative performance
1084         * reasons, the latter is the default.  However, setting this 
1085         * property to true may result in more attractive output, particularly
1086         * when the horizontal and vertical scales are very different.
1087         * This value must also be set to true if using a custom stroke
1088         * while drawing into a Swing graphics context on JDK 1.3.1 on Mac OS X,
1089         * due to a nasty rendering bug on that platform.
1090         * <p>
1091         * Value type: {@link java.lang.Boolean}.<br/>
1092         * Default value: <code>Boolean.FALSE</code>.
1093         * </p>
1094         */
1095        public static final Option USE_PER_SHAPE_TRANSFORM =
1096            new Option("use-per-shape-transform", Boolean.FALSE);
1097        
1098        /**
1099         * To improve performance, the drawing objects for the chromatogram
1100         * traces are precomputed.  Specifically, the traces are stored as a set
1101         * of {@link java.awt.geom.GeneralPath}s. This option indicates how 
1102         * long (in trace samples) each one of these should be.  Ideally, this
1103         * value would be slightly more than the average number of trace samples
1104         * visible at once in the application using the graphic.  However,
1105         * constantly changing this value is counterproductive as it forces the
1106         * recalculation of the subpaths.  In general, having a value that is
1107         * too small should be preferred to one that is too large.
1108         * <p>
1109         * Value type: {@link java.lang.Integer}.<br/>
1110         * Default value: <code>250</code>.
1111         * </p>
1112         */
1113        public static final Option SUBPATH_LENGTH =
1114            new Option("subpath-length", new Integer(250));
1115        
1116        /**
1117         * Option indicating the lowest (leftmost) trace sample that should be
1118         * drawn.  The sample at this (0-based) index will be drawn at x=0 in the
1119         * output chromatogram.  Note that this option is reset to zero every time
1120         * {@link ChromatogramGraphic#setChromatogram} is called.
1121         * <p>
1122         * Value type: {@link java.lang.Integer}.<br/>
1123         * Default value: <code>0</code>.
1124         * </p>
1125         */
1126        public static final Option FROM_TRACE_SAMPLE = 
1127            new Option("from-trace-sample", new Integer(0));
1128        
1129        /**
1130         * Option indicating the highest (rightmost) trace sample that should be
1131         * drawn.  The sample at this (0-based) index will be the last drawn in the
1132         * output chromatogram.  Note that this option is reset to the length of the new
1133         * chromatogram every time {@link ChromatogramGraphic#setChromatogram} is called.
1134         * <p>
1135         * Value type: {@link java.lang.Integer}.<br/>
1136         * Default value: <code>Integer.MAX_VALUE</code>.
1137         * </p>
1138         */
1139        public static final Option TO_TRACE_SAMPLE =
1140            new Option("to-trace-sample",   new Integer(Integer.MAX_VALUE));
1141        
1142        /**
1143         * Option specifying the non-linear scaling function to apply, as
1144         * embodied in a {@link ChromatogramNonlinearScaler} object.
1145         * <p>
1146         * Value type: {@link ChromatogramNonlinearScaler}.<br/>
1147         * Default value: an instance of {@link ChromatogramNonlinearScaler.Identity}.
1148         */
1149        public static final Option HORIZONTAL_NONLINEAR_SCALER =
1150            new Option("horiz-nonlinear-scaler", ChromatogramNonlinearScaler.Identity.getInstance());
1151        
1152        //public static final Option EXAGGERATE_SAMPLE_POINTS =
1153        //    new Option("exaggerate-sample-points", Boolean.FALSE);
1154    }
1155    
1156    /*
1157    private final static String basicStrokeToString(BasicStroke bs) {
1158        StringBuffer sb = new StringBuffer(bs.toString());
1159        sb.append("[width=").append(bs.getLineWidth());
1160        sb.append("; EndCap=");
1161        switch (bs.getEndCap()) {
1162            case BasicStroke.CAP_BUTT:   sb.append("CAP_BUTT");   break;
1163            case BasicStroke.CAP_ROUND:  sb.append("CAP_ROUND");  break;
1164            case BasicStroke.CAP_SQUARE: sb.append("CAP_SQUARE"); break;
1165        }
1166        sb.append("; Join=");
1167        switch (bs.getLineJoin()) {
1168            case BasicStroke.JOIN_BEVEL: sb.append("JOIN_BEVEL"); break;
1169            case BasicStroke.JOIN_MITER: sb.append("JOIN_MITER"); break;
1170            case BasicStroke.JOIN_ROUND: sb.append("JOIN_ROUND"); break;
1171        }
1172        sb.append("; MiterLimit=").append(bs.getMiterLimit()).append(']');
1173        return sb.toString();
1174    }
1175     */       
1176}