/*
* The MIT License (MIT)
*
* Copyright (c) 2007-2015 Broad Institute
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.broad.igv.renderer;
import org.apache.log4j.Logger;
import org.broad.igv.feature.AminoAcid;
import org.broad.igv.feature.AminoAcidSequence;
import org.broad.igv.feature.Strand;
import org.broad.igv.prefs.IGVPreferences;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.track.LoadedDataInterval;
import org.broad.igv.track.RenderContext;
import org.broad.igv.track.SequenceTrack;
import org.broad.igv.ui.FontManager;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.color.ColorUtilities;
import java.awt.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.broad.igv.prefs.Constants.*;
/**
* @author jrobinso
*/
public class SequenceRenderer {
private static Logger log = Logger.getLogger(SequenceRenderer.class);
private static final int AMINO_ACID_RESOLUTION = 5;
public static Map<Character, Color> nucleotideColors;
public static Map<Character, Color> getNucleotideColors() {
if (nucleotideColors == null) setNucleotideColors();
return nucleotideColors;
}
private synchronized static void setNucleotideColors() {
IGVPreferences prefs = PreferencesManager.getPreferences();
nucleotideColors = new HashMap();
Color a = ColorUtilities.stringToColor(prefs.get(COLOR_A), new Color(0, 150, 0));
Color c = ColorUtilities.stringToColor(prefs.get(COLOR_C), Color.blue);
Color t = ColorUtilities.stringToColor(prefs.get(COLOR_T), Color.red);
Color g = ColorUtilities.stringToColor(prefs.get(COLOR_G), Color.gray);
Color n = ColorUtilities.stringToColor(prefs.get(COLOR_N), Color.gray);
nucleotideColors.put('A', a);
nucleotideColors.put('a', a);
nucleotideColors.put('C', c);
nucleotideColors.put('c', c);
nucleotideColors.put('T', t);
nucleotideColors.put('t', t);
nucleotideColors.put('G', g);
nucleotideColors.put('g', g);
nucleotideColors.put('N', n);
nucleotideColors.put('n', n);
nucleotideColors.put('-', Color.lightGray);
}
private TranslatedSequenceDrawer translatedSequenceDrawer;
private Strand strand = Strand.POSITIVE;
public SequenceRenderer() {
if (nucleotideColors == null) setNucleotideColors();
translatedSequenceDrawer = new TranslatedSequenceDrawer();
}
public void draw(LoadedDataInterval<SequenceTrack.SeqCache> sequenceInterval,
RenderContext context,
Rectangle trackRectangle,
boolean showTranslation,
int resolutionThreshold) {
String chr = context.getChr();
if (!chr.equals(sequenceInterval.range.chr)) {
log.error("Chromosome mismatch in sequence track");
return;
}
if (context.getScale() >= resolutionThreshold) {
// Zoomed out too far to see sequences. This can happen when in gene list view and one of the frames
// is zoomed in but others are not
context.getGraphic2DForColor(UIConstants.LIGHT_GREY).fill(trackRectangle);
} else {
double locScale = context.getScale();
int start = (int) context.getOrigin();
int end = (int) (start + trackRectangle.width * locScale) + 1;
SequenceTrack.SeqCache cache = sequenceInterval.getFeatures();
byte[] seq = cache.seq;
int sequenceStart = cache.start;
if (end <= sequenceStart) return;
//The combined height of sequence and (optionally) colorspace bands
int untranslatedSequenceHeight = (int) trackRectangle.getHeight();
if (showTranslation) {
untranslatedSequenceHeight = (int) (trackRectangle.getHeight() / 4);
// Draw translated sequence
Rectangle translatedSequenceRect = new Rectangle(trackRectangle.x, trackRectangle.y + untranslatedSequenceHeight,
(int) trackRectangle.getWidth(), (int) trackRectangle.getHeight() - untranslatedSequenceHeight);
if (context.getScale() < AMINO_ACID_RESOLUTION) {
translatedSequenceDrawer.draw(context, sequenceStart, translatedSequenceRect, cache, strand);
}
}
//Rectangle containing the sequence and (optionally) colorspace bands
Rectangle untranslatedSequenceRect = new Rectangle(trackRectangle.x, trackRectangle.y,
(int) trackRectangle.getWidth(), untranslatedSequenceHeight);
byte[] seqCS = null;
if (seq != null && seq.length > 0) {
int yBase = untranslatedSequenceRect.y + 2;
int yCS = untranslatedSequenceRect.y + 2;
int dY = untranslatedSequenceRect.height - 4;
int dX = (int) (1.0 / locScale);
// Create a graphics to use
Graphics2D g = context.getGraphics2D("SEQUENCE");
//dhmay adding check for adequate track height
int fontSize = Math.min(untranslatedSequenceRect.height, Math.min(dX, 12));
if (fontSize >= 8) {
Font f = FontManager.getFont(Font.BOLD, fontSize);
g.setFont(f);
}
// Loop through base pair coordinates
int lastVisibleNucleotideEnd = Math.min(end, seq.length + sequenceStart);
int lastPx0 = -1;
int scale = Math.max(1, (int) context.getScale());
double origin = context.getOrigin();
for (int loc = start - 1; loc < lastVisibleNucleotideEnd; loc += scale) {
int pX0 = (int) ((loc - origin) / locScale);
// Skip drawing if we haven't advanced 1 pixel past last nt. Low zoom
if (pX0 > lastPx0) {
lastPx0 = pX0;
int idx = loc - sequenceStart;
if(idx < 0 ) continue;
if(idx >= seq.length) break;
char c = (char) seq[idx];
if (Strand.NEGATIVE.equals(strand)) c = complementChar(c);
Color color = nucleotideColors.get(c);
if (fontSize >= 8) {
if (color == null) {
color = Color.black;
}
g.setColor(color);
drawCenteredText(g, new char[]{c}, pX0, yBase + 2, dX, dY - 2);
} else {
int bw = Math.max(1, dX - 1);
if (color != null) {
g.setColor(color);
g.fillRect(pX0, yBase, bw, dY);
}
}
}
}
}
}
}
/**
* Return the complement of a nucleotide or ambiguity code
*
* @param inputChar
* @return
*/
protected char complementChar(char inputChar) {
switch (inputChar) {
case 'A':
return 'T';
case 'T':
case 'U':
return 'A';
case 'G':
return 'C';
case 'C':
return 'G';
case 'M':
return 'K';
case 'R':
return 'Y';
case 'Y':
return 'R';
case 'K':
return 'M';
case 'V':
return 'B';
case 'H':
return 'D';
case 'D':
return 'H';
case 'B':
return 'V';
case 'a':
return 't';
case 't':
case 'u':
return 'a';
case 'g':
return 'c';
case 'c':
return 'g';
case 'm':
return 'k';
case 'r':
return 'y';
case 'y':
return 'r';
case 'k':
return 'm';
case 'v':
return 'b';
case 'h':
return 'd';
case 'd':
return 'h';
case 'b':
return 'v';
default:
return inputChar;
}
}
private void drawCenteredText(Graphics2D g, char[] chars, int x, int y, int w, int h) {
// Get measures needed to center the message
FontMetrics fm = g.getFontMetrics();
// How many pixels wide is the string
int msg_width = fm.charsWidth(chars, 0, 1);
// How far above the baseline can the font go?
int ascent = fm.getMaxAscent();
// How far below the baseline?
int descent = fm.getMaxDescent();
// Use the string width to find the starting point
int msgX = x + w / 2 - msg_width / 2;
// Use the vertical height of this font to find
// the vertical starting coordinate
int msgY = y + h / 2 - descent / 2 + ascent / 2;
g.drawChars(chars, 0, 1, msgX, msgY);
}
public Strand getStrand() {
return strand;
}
public void setStrand(Strand strand) {
this.strand = strand;
}
/**
* @author Damon May
* This class draws three amino acid bands representing the 3-frame translation of one strand
* of the associated SequenceTrack
*/
public static class TranslatedSequenceDrawer {
public static final int HEIGHT_PER_BAND = 14;
public static final int TOTAL_HEIGHT = 3 * HEIGHT_PER_BAND;
//alternating colors for aminoacids
public static final Color AA_COLOR_1 = new Color(128, 128, 128);
public static final Color AA_COLOR_2 = new Color(170, 170, 170);
public static final Color AA_FONT_COLOR = Color.WHITE;
//minimum font size for drawing AA characters
public static final int MIN_FONT_SIZE = 6;
//minimum vertical buffer around AA characters in band
public static final int MIN_FONT_VBUFFER = 1;
//ideal vertical buffer around AA characters in band
public static final int IDEAL_FONT_VBUFFER = 2;
protected static final Color STOP_CODON_COLOR = Color.RED;
protected static final Color METHIONINE_COLOR = Color.GREEN;
protected static final Color NUCLEOTIDE_SEPARATOR_COLOR = new Color(150, 150, 150, 120);
public void draw(RenderContext context, int start, Rectangle trackRectangle, SequenceTrack.SeqCache cache, Strand strand) {
//each band gets 1/3 of the height, rounded
int idealHeightPerBand = trackRectangle.height / 3;
//In this situation, band height is more equal if we tweak things a bit
if (trackRectangle.height % 3 == 2)
idealHeightPerBand++;
int minHeightPerBand = Math.min(idealHeightPerBand, trackRectangle.height - (2 * idealHeightPerBand));
double locScale = context.getScale();
double origin = context.getOrigin();
//Figure out the dimensions of a single box containing an aminoacid, at this zoom
int oneAcidBoxWidth = getPixelFromChromosomeLocation(context.getChr(), 3, origin, locScale) -
getPixelFromChromosomeLocation(context.getChr(), 0, origin, locScale) + 1;
int oneAcidBoxMinDimension = Math.min(oneAcidBoxWidth, minHeightPerBand);
//Calculate the font size. If that's less than MIN_FONT_SIZE, we won't draw amino acids
int fontSize = 0;
boolean shouldDrawLetters = false;
if (oneAcidBoxMinDimension >= 2 * MIN_FONT_VBUFFER + MIN_FONT_SIZE) {
int idealFontSize = oneAcidBoxMinDimension - 2 * IDEAL_FONT_VBUFFER;
fontSize = Math.max(idealFontSize, MIN_FONT_SIZE);
shouldDrawLetters = true;
}
boolean shouldDrawNucleotideLines = shouldDrawLetters && oneAcidBoxWidth >= 2.5 * fontSize;
Rectangle bandRectangle = new Rectangle(trackRectangle.x, 0, trackRectangle.width, 0);
int heightAlreadyUsed = 0;
//rf 0
bandRectangle.y = trackRectangle.y;
bandRectangle.height = idealHeightPerBand;
heightAlreadyUsed += bandRectangle.height;
//This set collects the X positions for nucleotide lines, if we choose to draw them.
//Technically we could calculate these, but I haven't managed to do that without some wiggle
Set<Integer> nucleotideLineXPositions = new HashSet<Integer>();
AminoAcidSequence[] aa = strand == Strand.POSITIVE ? cache.posAA : cache.negAA;
//only draw nucleotide lines the last time this is called
drawOneTranslation(context, bandRectangle, 0, shouldDrawLetters, fontSize,
nucleotideLineXPositions, aa[0], strand);
//rf 1
bandRectangle.y = trackRectangle.y + heightAlreadyUsed;
bandRectangle.height = idealHeightPerBand;
heightAlreadyUsed += bandRectangle.height;
drawOneTranslation(context, bandRectangle, 1, shouldDrawLetters, fontSize,
nucleotideLineXPositions, aa[1], strand);
//rf 2
bandRectangle.y = trackRectangle.y + heightAlreadyUsed;
bandRectangle.height = trackRectangle.height - heightAlreadyUsed;
drawOneTranslation(context, bandRectangle, 2, shouldDrawLetters, fontSize,
nucleotideLineXPositions, aa[2], strand);
if (shouldDrawNucleotideLines) {
Graphics2D graphicsForNucleotideLines = context.getGraphic2DForColor(NUCLEOTIDE_SEPARATOR_COLOR);
//use a dashed stroke
graphicsForNucleotideLines.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
0, new float[]{1, 2}, 0));
int topYCoord = trackRectangle.y - 1;
for (int xVal : nucleotideLineXPositions) {
if (xVal >= trackRectangle.x && xVal <= trackRectangle.x + trackRectangle.width)
graphicsForNucleotideLines.drawLine(xVal,
topYCoord,
xVal, topYCoord + trackRectangle.height);
}
}
}
/**
* Draw the band representing a translation of the sequence in one reading frame
*
* @param context
* @param start the index of the first base in seq. Should be the first nucleotide that's in a codon
* that's even partially visible, in any frame
* @param bandRectangle
* @param readingFrame
* @param shouldDrawLetters
* @param fontSize
* @param nucleotideLineXPositions a Set that will accrue all of the x positions that we define here
* @param seq nucleotide sequence starting at start
* for the beginning and end of aminoacid boxes
*/
protected void drawOneTranslation(RenderContext context,
Rectangle bandRectangle, int readingFrame,
boolean shouldDrawLetters, int fontSize,
Set<Integer> nucleotideLineXPositions, AminoAcidSequence aaSequence,
Strand strand) {
double locScale = context.getScale();
double origin = context.getOrigin();
Graphics2D fontGraphics = context.getGraphics2D("AA_FONT");
fontGraphics.setColor(AA_FONT_COLOR);
if (aaSequence != null && aaSequence.hasNonNullSequence()) {
Graphics2D g = context.getGraphics2D("TRANSLATION");
//This rectangle holds a single AA glyph. x and width will be updated in the for loop
Rectangle aaRect = new Rectangle(0, bandRectangle.y, 1, bandRectangle.height);
//start position for this amino acid. Will increment in for loop below
int aaSeqStartPosition = aaSequence.getStart(); // + readingFrame;
//calculated oddness or evenness of first amino acid
int firstFullAcidIndex = (int) Math.floor((aaSeqStartPosition - readingFrame) / 3);
boolean odd = (firstFullAcidIndex % 2) == 1;
if (shouldDrawLetters) {
Font f = FontManager.getFont(Font.BOLD, fontSize);
g.setFont(f);
}
for (AminoAcid acid : aaSequence.getSequence()) {
if (acid != null) {
//calculate x pixel boundaries of this AA rectangle
int px = getPixelFromChromosomeLocation(context.getChr(), aaSeqStartPosition, origin, locScale);
int px2 = getPixelFromChromosomeLocation(context.getChr(), aaSeqStartPosition + 3,
origin, locScale);
//if x boundaries of this AA overlap the band rectangle
if ((px <= bandRectangle.getMaxX()) && (px2 >= bandRectangle.getX())) {
aaRect.x = px;
aaRect.width = px2 - px;
nucleotideLineXPositions.add(aaRect.x);
nucleotideLineXPositions.add(aaRect.x + aaRect.width);
Graphics2D bgGraphics =
context.getGraphic2DForColor(getColorForAminoAcid(acid.getSymbol(), odd));
bgGraphics.fill(aaRect);
if (shouldDrawLetters) {
String acidString = new String(new char[]{acid.getSymbol()});
GraphicUtils.drawCenteredText(acidString, aaRect, fontGraphics);
}
}
//need to switch oddness whether we displayed the AA or not,
//because oddness is calculated from first AA
odd = !odd;
aaSeqStartPosition += 3;
}
}
}
}
protected Color getColorForAminoAcid(char acidSymbol, boolean odd) {
switch (acidSymbol) {
case 'M':
return METHIONINE_COLOR;
case '*':
return STOP_CODON_COLOR;
default:
return odd ? AA_COLOR_1 : AA_COLOR_2;
}
}
protected int getPixelFromChromosomeLocation(String chr, int chromosomeLocation, double origin,
double locationScale) {
return (int) Math.round((chromosomeLocation - origin) / locationScale);
}
public Color getDefaultColor() {
return Color.BLACK;
}
}
}