/* * 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; } } }