/* * 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. */ package org.broad.igv.track; //~--- non-JDK imports -------------------------------------------------------- import org.apache.log4j.Logger; import org.broad.igv.Globals; import org.broad.igv.feature.AminoAcidManager; import org.broad.igv.feature.AminoAcidSequence; import org.broad.igv.feature.Chromosome; import org.broad.igv.feature.Strand; import org.broad.igv.feature.genome.Genome; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.GraphicUtils; import org.broad.igv.renderer.Renderer; import org.broad.igv.renderer.SequenceRenderer; import org.broad.igv.ui.FontManager; import org.broad.igv.ui.IGV; import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.ui.panel.FrameManager; import org.broad.igv.ui.panel.IGVPopupMenu; import org.broad.igv.ui.panel.ReferenceFrame; import org.broad.igv.ui.util.UIUtilities; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.broad.igv.prefs.Constants.MAX_SEQUENCE_RESOLUTION; import static org.broad.igv.prefs.Constants.SHOW_SEQUENCE_TRANSLATION; /** * @author jrobinso */ public class SequenceTrack extends AbstractTrack implements IGVEventObserver { private static Logger log = Logger.getLogger(SequenceTrack.class); private static final int SEQUENCE_HEIGHT = 14; private static String NAME = "Sequence"; private Map<String, LoadedDataInterval<SeqCache>> loadedIntervalCache = new HashMap(200); private Map<String, Boolean> sequenceVisible; private SequenceRenderer sequenceRenderer = new SequenceRenderer(); //should translated aminoacids be shown below the sequence? private boolean shouldShowTranslation = true; Strand strand = Strand.POSITIVE; private Rectangle arrowRect; public SequenceTrack(String name) { super(name); setSortable(false); shouldShowTranslation = PreferencesManager.getPreferences().getAsBoolean(SHOW_SEQUENCE_TRANSLATION); loadedIntervalCache = Collections.synchronizedMap(new HashMap<>()); sequenceVisible = Collections.synchronizedMap(new HashMap<>()); IGVEventBus.getInstance().subscribe(FrameManager.ChangeEvent.class, this); } public static String getReverseComplement(String sequence) { char[] complement = new char[sequence.length()]; int jj = complement.length; for (int ii = 0; ii < sequence.length(); ii++) { char c = sequence.charAt(ii); jj--; switch (c) { case 'T': complement[jj] = 'A'; break; case 'A': complement[jj] = 'T'; break; case 'C': complement[jj] = 'G'; break; case 'G': complement[jj] = 'C'; break; case 't': complement[jj] = 'a'; break; case 'a': complement[jj] = 't'; break; case 'c': complement[jj] = 'g'; break; case 'g': complement[jj] = 'c'; break; default: complement[jj] = c; } } return new String(complement); } public void receiveEvent(Object event) { if (event instanceof FrameManager.ChangeEvent) { Collection<ReferenceFrame> frames = ((FrameManager.ChangeEvent) event).getFrames(); Map<String, LoadedDataInterval<SeqCache>> newCache = Collections.synchronizedMap(new HashMap<>()); for (ReferenceFrame f : frames) { newCache.put(f.getName(), loadedIntervalCache.get(f.getName())); } loadedIntervalCache = newCache; } else { log.info("Unknown event type: " + event.getClass()); } } @Override public void renderName(Graphics2D graphics, Rectangle trackRectangle, Rectangle visibleRectangle) { Font font = FontManager.getFont(fontSize); boolean visible = this.sequenceVisible.values().stream().anyMatch(v -> v==true); if (visible) { graphics.setFont(font); int textBaseline = trackRectangle.y + 12; graphics.drawString(NAME, trackRectangle.x + 5, textBaseline); int rx = trackRectangle.x + trackRectangle.width - 20; arrowRect = new Rectangle(rx, trackRectangle.y + 2, 15, 10); drawArrow(graphics); //Show icon when translation non-standard if (AminoAcidManager.getInstance().getCodonTable().getId() != AminoAcidManager.STANDARD_TABLE_ID) { Font labFont = font.deriveFont(Font.BOLD); graphics.setFont(labFont); graphics.drawString("A", rx - 20, textBaseline); graphics.setFont(font); } graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT); } } private void drawArrow(Graphics2D graphics) { GraphicUtils.drawHorizontalArrow(graphics, arrowRect, strand == Strand.POSITIVE); } @Override public boolean isReadyToPaint(ReferenceFrame frame) { int resolutionThreshold = PreferencesManager.getPreferences().getAsInt(MAX_SEQUENCE_RESOLUTION); boolean visible = frame.getScale() < resolutionThreshold && !frame.getChrName().equals(Globals.CHR_ALL); if (!visible) { return true; // Nothing to paint } else { LoadedDataInterval<SeqCache> interval = loadedIntervalCache.get(frame.getName()); return interval != null && interval.contains(frame); } } @Override public void load(ReferenceFrame referenceFrame) { String chr = referenceFrame.getChrName(); final Genome currentGenome = GenomeManager.getInstance().getCurrentGenome(); Chromosome chromosome = currentGenome.getChromosome(chr); int start = (int) referenceFrame.getOrigin(); final int chromosomeLength = chromosome.getLength(); int end = (int) referenceFrame.getEnd(); int w = end - start; // Expand a bit for panning and AA caluclation start = Math.max(0, start - w / 2 + 2); end = Math.min(end + w / 2 + 2, chromosomeLength); Genome genome = currentGenome; String sequence = new String(genome.getSequence(chr, start, end)); String s1 = sequence; String s2 = sequence.substring(1); String s3 = sequence.substring(2); String s4 = sequence; String s5 = sequence.substring(0, sequence.length() - 1); String s6 = sequence.substring(0, sequence.length() - 2); AminoAcidSequence aa1 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.POSITIVE, start, s1); AminoAcidSequence aa2 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.POSITIVE, start + 1, s2); AminoAcidSequence aa3 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.POSITIVE, start + 2, s3); AminoAcidSequence aa4 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.NEGATIVE, start, s4); AminoAcidSequence aa5 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.NEGATIVE, start, s5); AminoAcidSequence aa6 = AminoAcidManager.getInstance().getAminoAcidSequence(Strand.NEGATIVE, start, s6); // Now trim sequence to prevent dangling AAs int deltaStart = start == 0 ? 0 : 2; int deltaEnd = end == chromosomeLength ? 0 : 02; start += deltaStart; end -= deltaEnd; byte[] seq = sequence.substring(deltaStart, sequence.length() - deltaEnd).getBytes(); SeqCache cache = new SeqCache(start, seq, aa1, aa2, aa3, aa4, aa5, aa6); loadedIntervalCache.put(referenceFrame.getName(), new LoadedDataInterval<>(chr, start, end, cache)); } /** * Render the sequence, and optionally the 3 frame translation table * * @param context * @param rect */ public void render(RenderContext context, Rectangle rect) { int resolutionThreshold = PreferencesManager.getPreferences().getAsInt(MAX_SEQUENCE_RESOLUTION); boolean visible = context.getReferenceFrame().getScale() < resolutionThreshold && !context.getChr().equals(Globals.CHR_ALL); final String frameName = context.getReferenceFrame().getName(); if(!sequenceVisible.containsKey(frameName)) sequenceVisible.put(frameName, false); // Default value if (visible != sequenceVisible.get(frameName)) { sequenceVisible.put(frameName, visible); UIUtilities.invokeAndWaitOnEventThread(() -> context.getPanel().revalidate()); } if (visible) { LoadedDataInterval<SeqCache> sequenceInterval = loadedIntervalCache.get(frameName); if (sequenceInterval != null) { sequenceRenderer.setStrand(strand); sequenceRenderer.draw(sequenceInterval, context, rect, shouldShowTranslation, resolutionThreshold); } } } @Override public int getHeight() { boolean visible = this.sequenceVisible.values().stream().anyMatch(v -> v==true); return visible ? SEQUENCE_HEIGHT + (shouldShowTranslation ? SequenceRenderer.TranslatedSequenceDrawer.TOTAL_HEIGHT : 0) : 0; } @Override public boolean handleDataClick(TrackClickEvent e) { setShouldShowTranslation(!shouldShowTranslation); Object source = e.getMouseEvent().getSource(); if (source instanceof JComponent) { UIUtilities.invokeOnEventThread(() -> repaint()); } return true; } @Override public void handleNameClick(final MouseEvent e) { if (arrowRect != null && arrowRect.contains(e.getPoint())) { flipStrand(); } } private void flipStrand() { strand = (strand == Strand.POSITIVE ? Strand.NEGATIVE : Strand.POSITIVE); IGV.getInstance().clearSelections(); repaint(); } public void setShouldShowTranslation(boolean shouldShowTranslation) { this.shouldShowTranslation = shouldShowTranslation; // Remember this choice PreferencesManager.getPreferences().put(SHOW_SEQUENCE_TRANSLATION, shouldShowTranslation); } /** * Override to return a specialized popup menu * * @return */ @Override public IGVPopupMenu getPopupMenu(final TrackClickEvent te) { IGVPopupMenu menu = new IGVPopupMenu(); JMenuItem m1 = new JMenuItem("Flip strand"); m1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { flipStrand(); } }); final JCheckBoxMenuItem m2 = new JCheckBoxMenuItem("Show translation"); m2.setSelected(shouldShowTranslation); m2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setShouldShowTranslation(m2.isSelected()); repaint(); IGV.getInstance().clearSelections(); } }); menu.add(m1); menu.add(m2); final JMenu transTableMenu = new JMenu("Translation Table"); for (AminoAcidManager.CodonTable codonTable : AminoAcidManager.getInstance().getAllCodonTables()) { JMenuItem item = getCodonTableMenuItem(codonTable); transTableMenu.add(item); } menu.add(transTableMenu); return menu; } private JCheckBoxMenuItem getCodonTableMenuItem(AminoAcidManager.CodonTable codonTable) { JCheckBoxMenuItem item = new JCheckBoxMenuItem(); String fullName = codonTable.getDisplayName(); String shortName = fullName; if (fullName.length() > 40) { shortName = fullName.substring(0, 37) + "..."; item.setToolTipText(fullName); } item.setText(shortName); final AminoAcidManager.CodonTableKey curKey = codonTable.getKey(); item.setSelected(curKey.equals(AminoAcidManager.getInstance().getCodonTable().getKey())); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { AminoAcidManager.getInstance().setCodonTable(curKey); repaint(); } }); return item; } private void repaint() { // TODO -- what's really needed is a repaint of all panels the sequence track intersects IGV.getMainFrame().repaint(); } // SequenceTrack does not expose its renderer public Renderer getRenderer() { return null; } @Override public String getNameValueString(int y) { String nvs = "<html>" + super.getNameValueString(y); nvs += "<br>Translation Table: "; nvs += AminoAcidManager.getInstance().getCodonTable().getDisplayName(); return nvs; } public Strand getStrand() { return this.strand; } public static class SeqCache { public int start; public byte[] seq; public AminoAcidSequence[] posAA; public AminoAcidSequence[] negAA; public SeqCache(int start, byte[] seq, AminoAcidSequence aa1, AminoAcidSequence aa2, AminoAcidSequence aa3, AminoAcidSequence aa4, AminoAcidSequence aa5, AminoAcidSequence aa6) { this.start = start; this.seq = seq; posAA = new AminoAcidSequence[]{aa1, aa2, aa3}; negAA = new AminoAcidSequence[]{aa4, aa5, aa6}; } } }