001package org.biojava.bio.chromatogram.graphic;
002
003import org.biojava.bio.BioError;
004import org.biojava.bio.chromatogram.Chromatogram;
005import org.biojava.bio.chromatogram.ChromatogramTools;
006
007/**
008 * A {@link ChromatogramNonlinearScaler} that scales all the 
009 * base calls in a chromatogram to the same width in pixels,
010 * optionally biasing the peak of the call to the center.
011 *
012 * @author Rhett Sutphin (<a href="http://genome.uiowa.edu/">UI CBCB</a>)
013 * @author Matthew Pocock
014 * @since 1.3
015 */
016public class FixedBaseWidthScaler implements ChromatogramNonlinearScaler {
017    /** Set to true to get copious, cryptic debugging output on System.out */
018    private static final boolean DEBUG = false;
019    
020    private Chromatogram lastC;
021    private int lastSeqLength;
022    private final float baseWidth;
023    private final boolean centerPeaks;
024    private float[] scales;
025
026    /**    
027     * Creates a new scaler that will scale bases to the specified width
028     * without attempting to center their peaks.
029     * @param width the desired call width in pixels
030     */
031    public FixedBaseWidthScaler(float width) {
032        this(width, false);
033    }
034
035    /**
036     * Creates a new scaler that will scale bases to the specified width
037     * and may or may not bias the peaks to the center.
038     * @param width the desired call width in pixels
039     * @param centerPeaks if true, the scaler will try to put the peak of
040     *        in the center of the scaled call.  Otherwise, the whole call
041     *        will be scaled using the same factor.
042     */
043    public FixedBaseWidthScaler(float width, boolean centerPeaks) {
044        baseWidth = width;
045        this.centerPeaks = centerPeaks;
046        lastC = null;
047        scales = null;
048        lastSeqLength = -1;
049    }
050
051    public float scale(Chromatogram c, int traceSampleIndex) throws IndexOutOfBoundsException {
052        if (traceSampleIndex < 0 || traceSampleIndex >= c.getTraceLength())
053            throw new IndexOutOfBoundsException("Requested a scale of a trace sample outside of the chromatogram");
054        synchronized (this) {
055            calcAllScales(c); 
056            if (traceSampleIndex >= scales.length) 
057                throw new BioError("This shouldn't happen: a valid trace sample is not included in the calculated scales array.  This is probably due to a failure in dirty checking.");
058            return scales[traceSampleIndex];
059        }
060    }
061    
062    /**
063     * Calculates all the scaled x-coordinates for the given chromatogram,
064     * but only if it isn't the one that we already have the scales for.
065     *
066     * @param c  the Chromatogram to calculate the scale for
067     */
068    private synchronized void calcAllScales(Chromatogram c) {
069        if (c == null) return;
070        if (scales != null 
071            && lastC != null && lastC == c 
072            && scales.length == c.getTraceLength()
073            && lastSeqLength == c.getSequenceLength()) 
074            return;
075        if (scales == null || scales.length != c.getTraceLength())
076            scales = new float[c.getTraceLength()];
077        if (DEBUG) System.out.println("bw=" + baseWidth + " cp="+centerPeaks);
078        int[] peaks = ChromatogramTools.getTraceOffsetArray(c);
079        int left = 0;
080        int center = peaks[0];
081        int right = peaks[0] + (int) Math.floor( ((double) (peaks[1] - peaks[0])) / 2 );
082        if (DEBUG) System.out.print("b=1 ");
083        scaleBase(left, center, right, 0.0f, false);
084        for (int i = 1 ; i < peaks.length - 1 ; i++) {
085            left = right + 1;
086            center = peaks[i];
087            right = peaks[i] + (int) Math.floor( ((double) (peaks[i+1] - peaks[i])) / 2 );
088            if (DEBUG) System.out.print("b="+(i+1)+" ");
089            scaleBase(left, center, right, i * baseWidth, false);
090        }
091        left = right + 1;
092        center = peaks[peaks.length - 1];
093        right = c.getTraceLength() - 1;
094        if (DEBUG) System.out.print("b="+(peaks.length)+" ");
095        scaleBase(left, center, right, (peaks.length - 1) * baseWidth, true);
096        
097        lastC = c;
098        lastSeqLength = c.getSequenceLength();
099    }
100
101    /**
102     * Calculate the scaled x coordinates for a base call.  The
103     * base call consists of the samples in the range <code>[left, right]</code> 
104     * and the peak is at <code>center</code> (which must be in that same range).
105     * The parameter <code>widthSoFar</code> provides the x coordinate for
106     * the sample at <code>left</code> and this method must return a similar
107     * value for the base call immediately following this one (i.e., the one 
108     * whose leftmost sample would be <code>right+1</code>).
109     * @param left the leftmost sample index of the base (inclusive)
110     * @param center the peak sample index
111     * @param right the rightmost sample index of the base (inclusive)
112     * @param nextX the total width of all the scaled bases up to this one
113     * @param lastBase true if this is the last base, false otherwise.  The
114     *        last base must be handled specially so that the last sample is 
115     *        at baseWidth * c.sequenceLength.
116     */
117    private void scaleBase(int left, int center, int right, float nextX, boolean lastBase) {
118        if (left > center)  throw new BioError("Assertion failure: left > center ; l="+left+"; c="+center+"; r="+right);
119        if (center > right) throw new BioError("Assertion failure: center > right ; l="+left+"; c="+center+"; r="+right);
120        if (left < 0)   throw new BioError("Assertion failure: left < 0 ; l="+left+"; c="+center+"; r="+right);
121        if (center < 0) throw new BioError("Assertion failure: center < 0 ; l="+left+"; c="+center+"; r="+right);
122        if (right < 0)  throw new BioError("Assertion failure: right < 0 ; l="+left+"; c="+center+"; r="+right);
123        if (DEBUG) System.out.println("l="+left+" c="+center+" r="+right+" nX="+nextX);
124        if (centerPeaks) {
125            float leftIncrement = (baseWidth * 0.5f) / (center - left);
126            if (DEBUG) System.out.println("  lside ("+leftIncrement+"): ");
127            for (int i = left ; i < center ; i++) {
128                scales[i] = nextX;
129                if (DEBUG) System.out.println("    s["+i+"]="+scales[i]);
130                nextX += leftIncrement;
131            }
132            float rightIncrement = (baseWidth * 0.5f) / (right - center + (lastBase?0:1));
133            if (DEBUG) System.out.println("  rside ("+rightIncrement+"): ");
134            for (int i = center ; i <= right ; i++) {
135                scales[i] = nextX;
136                if (DEBUG) System.out.println("    s["+i+"]="+scales[i]);
137                nextX += rightIncrement;
138            }
139        }
140        else {
141            float increment = baseWidth / (right - left + (lastBase?0:1));
142            if (DEBUG) System.out.println("  noside ("+increment+"): ");
143            for (int i = left ; i <= right ; i++) {
144                scales[i] = nextX;
145                if (DEBUG) System.out.println("    "+scales[i]);
146                nextX += increment;
147            }
148        }
149    }
150}