package org.biojava.bio.chromatogram.graphic;
import org.biojava.bio.BioError;
import org.biojava.bio.chromatogram.Chromatogram;
import org.biojava.bio.chromatogram.ChromatogramTools;
/**
* A {@link ChromatogramNonlinearScaler} that scales all the
* base calls in a chromatogram to the same width in pixels,
* optionally biasing the peak of the call to the center.
*
* @author Rhett Sutphin (UI CBCB)
* @author Matthew Pocock
* @since 1.3
*/
public class FixedBaseWidthScaler implements ChromatogramNonlinearScaler {
/** Set to true to get copious, cryptic debugging output on System.out */
private static final boolean DEBUG = false;
private Chromatogram lastC;
private int lastSeqLength;
private final float baseWidth;
private final boolean centerPeaks;
private float[] scales;
/**
* Creates a new scaler that will scale bases to the specified width
* without attempting to center their peaks.
* @param width the desired call width in pixels
*/
public FixedBaseWidthScaler(float width) {
this(width, false);
}
/**
* Creates a new scaler that will scale bases to the specified width
* and may or may not bias the peaks to the center.
* @param width the desired call width in pixels
* @param centerPeaks if true, the scaler will try to put the peak of
* in the center of the scaled call. Otherwise, the whole call
* will be scaled using the same factor.
*/
public FixedBaseWidthScaler(float width, boolean centerPeaks) {
baseWidth = width;
this.centerPeaks = centerPeaks;
lastC = null;
scales = null;
lastSeqLength = -1;
}
public float scale(Chromatogram c, int traceSampleIndex) throws IndexOutOfBoundsException {
if (traceSampleIndex < 0 || traceSampleIndex >= c.getTraceLength())
throw new IndexOutOfBoundsException("Requested a scale of a trace sample outside of the chromatogram");
synchronized (this) {
calcAllScales(c);
if (traceSampleIndex >= scales.length)
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.");
return scales[traceSampleIndex];
}
}
/**
* Calculates all the scaled x-coordinates for the given chromatogram,
* but only if it isn't the one that we already have the scales for.
*
* @param c the Chromatogram to calculate the scale for
*/
private synchronized void calcAllScales(Chromatogram c) {
if (c == null) return;
if (scales != null
&& lastC != null && lastC == c
&& scales.length == c.getTraceLength()
&& lastSeqLength == c.getSequenceLength())
return;
if (scales == null || scales.length != c.getTraceLength())
scales = new float[c.getTraceLength()];
if (DEBUG) System.out.println("bw=" + baseWidth + " cp="+centerPeaks);
int[] peaks = ChromatogramTools.getTraceOffsetArray(c);
int left = 0;
int center = peaks[0];
int right = peaks[0] + (int) Math.floor( ((double) (peaks[1] - peaks[0])) / 2 );
if (DEBUG) System.out.print("b=1 ");
scaleBase(left, center, right, 0.0f, false);
for (int i = 1 ; i < peaks.length - 1 ; i++) {
left = right + 1;
center = peaks[i];
right = peaks[i] + (int) Math.floor( ((double) (peaks[i+1] - peaks[i])) / 2 );
if (DEBUG) System.out.print("b="+(i+1)+" ");
scaleBase(left, center, right, i * baseWidth, false);
}
left = right + 1;
center = peaks[peaks.length - 1];
right = c.getTraceLength() - 1;
if (DEBUG) System.out.print("b="+(peaks.length)+" ");
scaleBase(left, center, right, (peaks.length - 1) * baseWidth, true);
lastC = c;
lastSeqLength = c.getSequenceLength();
}
/**
* Calculate the scaled x coordinates for a base call. The
* base call consists of the samples in the range [left, right]
* and the peak is at center
(which must be in that same range).
* The parameter widthSoFar
provides the x coordinate for
* the sample at left
and this method must return a similar
* value for the base call immediately following this one (i.e., the one
* whose leftmost sample would be right+1
).
* @param left the leftmost sample index of the base (inclusive)
* @param center the peak sample index
* @param right the rightmost sample index of the base (inclusive)
* @param nextX the total width of all the scaled bases up to this one
* @param lastBase true if this is the last base, false otherwise. The
* last base must be handled specially so that the last sample is
* at baseWidth * c.sequenceLength.
*/
private void scaleBase(int left, int center, int right, float nextX, boolean lastBase) {
if (left > center) throw new BioError("Assertion failure: left > center ; l="+left+"; c="+center+"; r="+right);
if (center > right) throw new BioError("Assertion failure: center > right ; l="+left+"; c="+center+"; r="+right);
if (left < 0) throw new BioError("Assertion failure: left < 0 ; l="+left+"; c="+center+"; r="+right);
if (center < 0) throw new BioError("Assertion failure: center < 0 ; l="+left+"; c="+center+"; r="+right);
if (right < 0) throw new BioError("Assertion failure: right < 0 ; l="+left+"; c="+center+"; r="+right);
if (DEBUG) System.out.println("l="+left+" c="+center+" r="+right+" nX="+nextX);
if (centerPeaks) {
float leftIncrement = (baseWidth * 0.5f) / (center - left);
if (DEBUG) System.out.println(" lside ("+leftIncrement+"): ");
for (int i = left ; i < center ; i++) {
scales[i] = nextX;
if (DEBUG) System.out.println(" s["+i+"]="+scales[i]);
nextX += leftIncrement;
}
float rightIncrement = (baseWidth * 0.5f) / (right - center + (lastBase?0:1));
if (DEBUG) System.out.println(" rside ("+rightIncrement+"): ");
for (int i = center ; i <= right ; i++) {
scales[i] = nextX;
if (DEBUG) System.out.println(" s["+i+"]="+scales[i]);
nextX += rightIncrement;
}
}
else {
float increment = baseWidth / (right - left + (lastBase?0:1));
if (DEBUG) System.out.println(" noside ("+increment+"): ");
for (int i = left ; i <= right ; i++) {
scales[i] = nextX;
if (DEBUG) System.out.println(" "+scales[i]);
nextX += increment;
}
}
}
}