Java tutorial
/* * Copyright (c) 2007-2012 The Broad Institute, Inc. * SOFTWARE COPYRIGHT NOTICE * This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved. * * This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality. * * This software is licensed under the terms of the GNU Lesser General Public License (LGPL), * Version 2.1 which is available at http://www.opensource.org/licenses/lgpl-2.1.php. */ //chr2:128,565,093-128,565,156 package org.broad.igv.variant; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.broad.igv.feature.FeatureUtils; import org.broad.igv.feature.IGVFeature; import org.broad.igv.renderer.GraphicUtils; import org.broad.igv.session.IGVSessionReader; import org.broad.igv.session.SubtlyImportant; import org.broad.igv.track.*; import org.broad.igv.ui.FontManager; import org.broad.igv.ui.IGV; import org.broad.igv.ui.event.TrackGroupEvent; import org.broad.igv.ui.event.TrackGroupEventListener; import org.broad.igv.ui.panel.*; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.FileUtils; import org.broad.igv.util.LongRunningTask; import org.broad.igv.util.ParsingUtils; import org.broad.igv.util.ResourceLocator; import org.broad.tribble.Feature; import org.broadinstitute.variant.variantcontext.GenotypeType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; import java.awt.*; import java.awt.event.MouseEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.*; import java.util.List; /** * @author Jesse Whitworth, Jim Robinson, Fabien Campagne */ @XmlType(factoryMethod = "getNextTrack") public class VariantTrack extends FeatureTrack implements TrackGroupEventListener { private static Logger log = Logger.getLogger(VariantTrack.class); static final DecimalFormat numFormat = new DecimalFormat("#.###"); private static final Color GREY_170 = new Color(170, 170, 170); private static final int GROUP_BORDER_WIDTH = 3; private static final Color BAND1_COLOR = new Color(245, 245, 245); private static final Color BAND2_COLOR = Color.white; private static final Color SELECTED_BAND_COLOR = new Color(210, 210, 210); private static final Color borderGray = new Color(200, 200, 200); private final static int DEFAULT_EXPANDED_GENOTYPE_HEIGHT = 15; private final int DEFAULT_SQUISHED_GENOTYPE_HEIGHT = 4; private final static int DEFAULT_VARIANT_BAND_HEIGHT = 25; private final static int MAX_FILTER_LINES = 15; // TODO -- this needs to be settable public static int METHYLATION_MIN_BASE_COUNT = 10; public static boolean isVCF(String typeString) { return (typeString.endsWith(".vcf3") || typeString.endsWith(".vcf4") || typeString.endsWith(".vcf") || typeString.endsWith(".bcf")); } private VariantRenderer renderer; /** * When this flag is true, we have detected that the VCF file contains the FORMAT MR column representing * methylation data. This will enable the "Color By/Methylation Rate" menu item. */ private boolean enableMethylationRateSupport; /** * Top (y) position of this track. This is updated whenever the track is drawn. */ private int top; /** * The height of a single row in in squished mode */ @XmlAttribute(name = "SQUISHED_ROW_HEIGHT") private int squishedHeight = DEFAULT_SQUISHED_GENOTYPE_HEIGHT; /** * The height of the top band representing the variant call */ private int variantBandHeight = DEFAULT_VARIANT_BAND_HEIGHT; /** * List of all samples, in the order they appear in the file. */ List<String> allSamples; /** * Boolean indicating if samples are grouped. */ private boolean grouped; /** * The id of the group used to group samples. */ private String groupByAttribute; /** * Map of group -> samples. Each entry defines a group, the key is the group name and the value the list of * samples in the group. */ LinkedHashMap<String, List<String>> samplesByGroups = new LinkedHashMap<String, List<String>>(); /** * Current coloring option */ private ColorMode coloring = ColorMode.GENOTYPE; /** * When true, variants that are marked filtering are not drawn. */ private boolean hideFiltered = false; /** * The currently selected variant. This is a transient variable, set only while the popup menu is up. */ private Variant selectedVariant; /** * Transient list to keep track of the vertical bounds of each sample. Set when rendering names, used to * select correct sample for popup text. We use a list and linear lookup for now, some sort of tree structure * would be faster. */ private List<SampleBounds> sampleBounds = new ArrayList<SampleBounds>(); /** * List of selected samples. */ private List<String> selectedSamples = new ArrayList<String>(); /** * Experimental "mode" to couple VCF & BAM files */ //private boolean vcfToBamMode = false; /** * Map of sample name -> associated bam file */ Map<String, String> alignmentFiles; public void setRenderer(VariantRenderer renderer) { this.renderer = renderer; } public VariantTrack(String name, FeatureSource source) { this(null, source, Collections.<String>emptyList(), false); this.setName(name); } public VariantTrack(ResourceLocator locator, FeatureSource source, List<String> samples, boolean enableMethylationRateSupport) { super(locator, source); String path = locator != null ? locator.getPath() : null; this.renderer = new VariantRenderer(this); this.enableMethylationRateSupport = enableMethylationRateSupport; if (enableMethylationRateSupport) { // also set the default color mode to Methylation rate: coloring = ColorMode.METHYLATION_RATE; } this.allSamples = samples; // this handles the new attribute grouping mechanism: setupGroupsFromAttributes(); setDisplayMode(DisplayMode.EXPANDED); // Listen for "group by" events. if (IGV.hasInstance()) { IGV.getInstance().addGroupEventListener(this); } // If sample->bam list file is supplied enable vcfToBamMode. String bamListPath = path != null ? path + ".mapping" : null; if (ParsingUtils.pathExists(bamListPath)) { loadAlignmentMappings(bamListPath); } // Set visibility window. These values are appropriate for human dbsnp/1kg files, probably conservative otherwise // Ugly test on source is to avoid having to add "isIndexed" to a zillion feature source classes. The intent // is to skip this if using a non-indexed source. if (!(source instanceof TribbleFeatureSource && ((TribbleFeatureSource) source).isIndexed() == false)) { int vw = Math.max(10000, 2000000 - 2000 * allSamples.size()); setVisibilityWindow(vw); } } private void loadAlignmentMappings(String bamListPath) { alignmentFiles = new HashMap<String, String>(); BufferedReader br = null; try { br = ParsingUtils.openBufferedReader(bamListPath); String nextLine; while ((nextLine = br.readLine()) != null) { String[] tokens = ParsingUtils.TAB_PATTERN.split(nextLine); if (tokens.length < 2) { log.info("Skipping bam mapping file line: " + nextLine); } else { String alignmentPath = tokens[1]; boolean isAbsolute; if (alignmentPath.startsWith("http://") || alignmentPath.startsWith("ftp:")) { isAbsolute = true; } else { String absolutePath = (new File(alignmentPath)).getAbsolutePath(); String prefix = absolutePath.substring(0, 3); isAbsolute = alignmentPath.startsWith(prefix); } if (!isAbsolute) { alignmentPath = FileUtils.getAbsolutePath(alignmentPath, bamListPath); } alignmentFiles.put(tokens[0], alignmentPath); } } } catch (IOException e) { MessageUtils .showMessage("<html>Error loading bam mapping file: " + bamListPath + "<br>" + e.getMessage()); } finally { if (br != null) { try { br.close(); } catch (IOException e) { } } } } String getBamFileForSample(String sample) { return alignmentFiles == null ? null : alignmentFiles.get(sample); } /** * Set groups from global sample information attributes. */ private void setupGroupsFromAttributes() { // setup groups according to the attribute used for sorting (loaded from a sample information file): AttributeManager manager = AttributeManager.getInstance(); String newGroupByAttribute = !IGV.hasInstance() ? null : IGV.getInstance().getGroupByAttribute(); // The first equality handles the case where both are null if ((newGroupByAttribute == groupByAttribute) || (newGroupByAttribute != null && newGroupByAttribute.equals(groupByAttribute))) { // Nothing to do return; } samplesByGroups.clear(); groupByAttribute = newGroupByAttribute; if (groupByAttribute == null) { grouped = false; return; } for (String sample : allSamples) { String sampleGroup = manager.getAttribute(sample, newGroupByAttribute); List<String> sampleList = samplesByGroups.get(sampleGroup); if (sampleList == null) { sampleList = new ArrayList<String>(); samplesByGroups.put(sampleGroup, sampleList); } sampleList.add(sample); } grouped = samplesByGroups.size() > 1; groupByAttribute = newGroupByAttribute; } /** * Sort samples. Sort both the master list and groups, if any. * * @param comparator the comparator to sort by */ public void sortSamples(Comparator<String> comparator) { Collections.sort(allSamples, comparator); for (List<String> samples : samplesByGroups.values()) { Collections.sort(samples, comparator); } } public boolean isEnableMethylationRateSupport() { return enableMethylationRateSupport; } /** * Returns the height of a single sample (genotype) band * * @return */ public int getGenotypeBandHeight() { switch (getDisplayMode()) { case SQUISHED: return getSquishedHeight(); case COLLAPSED: return 0; default: return DEFAULT_EXPANDED_GENOTYPE_HEIGHT; } } /** * Returns the total height of the track (including all sample/genotypes) * * @return */ public int getHeight() { int sampleCount = allSamples.size(); if (getDisplayMode() == DisplayMode.COLLAPSED || sampleCount == 0) { return getVariantsHeight(); } else { final int groupCount = samplesByGroups.size(); int margins = groupCount * 3; return getVariantsHeight() + margins + (sampleCount * getGenotypeBandHeight()); } } public Object getHeader() { if (source instanceof TribbleFeatureSource) { return ((TribbleFeatureSource) source).getHeader(); } return null; } /** * Return the height of the variant section only (no samples/genotypes) * * @return */ private int getVariantsHeight() { return variantBandHeight * Math.max(1, getNumberOfFeatureLevels()); } /** * Set the height of the track. * * @param height */ public void setHeight(int height) { final DisplayMode displayMode = getDisplayMode(); // If collapsed there's nothing we can do to affect height if (displayMode == DisplayMode.COLLAPSED) { return; } // If height is < expanded height try "squishing" track, otherwise expand it final int groupCount = samplesByGroups.size(); final int margins = (groupCount - 1) * 3; int sampleCount = allSamples.size(); final int expandedHeight = variantBandHeight + margins + (sampleCount * getGenotypeBandHeight()); if (height < expandedHeight) { setDisplayMode(DisplayMode.SQUISHED); squishedHeight = Math.max(1, (height - variantBandHeight - margins) / sampleCount); } else { if (displayMode != DisplayMode.EXPANDED) { setDisplayMode(DisplayMode.EXPANDED); } } } /** * Render the features in the supplied rectangle. * * @param context * @param trackRectangle * @param packedFeatures */ @Override protected void renderFeatureImpl(RenderContext context, Rectangle trackRectangle, PackedFeatures packedFeatures) { Graphics2D g2D = context.getGraphics(); top = trackRectangle.y; Rectangle visibleRectangle = context.getVisibleRect(); // A disposable rect -- note this gets modified all over the place, bad practice Rectangle tmpRect = new Rectangle(trackRectangle); tmpRect.height = getGenotypeBandHeight(); tmpRect.y = trackRectangle.y; Rectangle bandRect = new Rectangle(tmpRect); bandRect.y += getVariantsHeight(); drawBackground(g2D, bandRect, visibleRectangle, BackgroundType.DATA); List<PackedFeatures.FeatureRow> rows = packedFeatures.getRows(); int overallFeatureRectHeight = getVariantsHeight(); int overallSampleRectHeight = trackRectangle.height - overallFeatureRectHeight; Rectangle overallSampleRect = new Rectangle(trackRectangle.x, top + overallFeatureRectHeight, trackRectangle.width, overallSampleRectHeight); int curRowTop = top; if (rows.size() > 0) { final double locScale = context.getScale(); final double origin = context.getOrigin(); final double pXMin = tmpRect.getMinX(); final double pXMax = tmpRect.getMaxX(); tmpRect.height = variantBandHeight; int lastEndX = -1; int minSpacing = 3; for (PackedFeatures.FeatureRow row : rows) { List<Feature> features = row.getFeatures(); for (Feature feature : features) { Variant variant = (Variant) feature; if (hideFiltered && variant.isFiltered()) { continue; } int start = variant.getStart(); int end = variant.getEnd(); int pX = (int) ((start - origin) / locScale); int dX = (int) Math.max(2, (end - start) / locScale); if (pX + dX < pXMin) { continue; } if (pX > pXMax) { break; } int w = dX; int x = pX; if (w < 3) { w = 3; x--; } //Make sure we have some whitespace between this //feature and the previous one, but only if they don't //actually overlap and the current size is reasonably large int spacing = x - lastEndX; if (spacing > 0 && spacing < minSpacing && w > 2 * minSpacing) { x += minSpacing - spacing; } tmpRect.y = curRowTop; if (tmpRect.intersects(visibleRectangle)) { renderer.renderSiteBand(variant, tmpRect, x, w, context); lastEndX = x + w - 1; } renderSamples(g2D, visibleRectangle, variant, context, overallSampleRect, x, w); boolean isSelected = selectedVariant != null && selectedVariant == variant; if (isSelected) { Graphics2D selectionGraphics = context.getGraphic2DForColor(Color.black); selectionGraphics.drawRect(x, curRowTop, w, getHeight()); } } if (areFeaturesStacked()) { curRowTop += variantBandHeight; lastEndX = -1; } } } else { tmpRect.height = variantBandHeight; tmpRect.y = trackRectangle.y; g2D.setColor(Color.gray); GraphicUtils.drawCenteredText("No Variants Found", trackRectangle, g2D); } renderBoundaryLines(g2D, trackRectangle, visibleRectangle); } private void drawLineIfVisible(Graphics2D g2D, Rectangle visibleRectangle, Color color, int yLoc, int left, int right) { if (yLoc >= visibleRectangle.y && yLoc <= visibleRectangle.getMaxY()) { if (color != null) g2D.setColor(color); g2D.drawLine(left, yLoc, right, yLoc); } } private void drawVariantBandBorder(Graphics2D g2D, Rectangle visibleRectangle, int variantBandY, int left, int right) { if (allSamples.size() > 0) { drawLineIfVisible(g2D, visibleRectangle, Color.black, variantBandY, left, right); } } private void renderSamples(Graphics2D g2D, Rectangle visibleRectangle, Variant variant, RenderContext context, Rectangle overallSampleRect, int x, int w) { Rectangle tmpRect = new Rectangle(overallSampleRect); tmpRect.height = getGenotypeBandHeight(); if (grouped) { for (Map.Entry<String, List<String>> entry : samplesByGroups.entrySet()) { for (String sample : entry.getValue()) { if (overallSampleRect.intersects(visibleRectangle)) { renderer.renderGenotypeBandSNP(variant, context, tmpRect, x, w, sample, coloring, hideFiltered); } tmpRect.y += tmpRect.height; } } } else { for (String sample : allSamples) { if (tmpRect.intersects(visibleRectangle)) { renderer.renderGenotypeBandSNP(variant, context, tmpRect, x, w, sample, coloring, hideFiltered); } tmpRect.y += tmpRect.height; } } } /** * Renders the top line, bottom track line, and border between variants / genotypes * * @param g2D * @param trackRectangle * @param visibleRectangle */ private void renderBoundaryLines(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle) { top = trackRectangle.y; final int left = trackRectangle.x; final int right = (int) trackRectangle.getMaxX(); //Top line drawLineIfVisible(g2D, visibleRectangle, Color.black, top + 1, left, right); // Bottom border int bottomY = trackRectangle.y + trackRectangle.height; drawLineIfVisible(g2D, visibleRectangle, borderGray, bottomY, left, right); // Variant / Genotype border int variantGenotypeBorderY = trackRectangle.y + getVariantsHeight(); drawVariantBandBorder(g2D, visibleRectangle, variantGenotypeBorderY, left, right); if (grouped) { g2D.setColor(Color.black); int y = trackRectangle.y + getVariantsHeight(); for (Map.Entry<String, List<String>> entry : samplesByGroups.entrySet()) { y += entry.getValue().size() * getGenotypeBandHeight() + GROUP_BORDER_WIDTH; g2D.drawLine(trackRectangle.x, y, trackRectangle.x + trackRectangle.width, y); } } } /** * Render the name panel. * <p/> * NOTE: The sample names are actually drawn in the drawBackground method! * * @param g2D * @param trackRectangle * @param visibleRectangle */ @Override public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle) { top = trackRectangle.y; Rectangle rect = new Rectangle(trackRectangle); g2D.setFont(FontManager.getFont(fontSize)); g2D.setColor(BAND2_COLOR); g2D.setColor(Color.black); rect.height = getVariantsHeight(); if (rect.intersects(visibleRectangle)) { GraphicUtils.drawWrappedText(getName(), rect, g2D, false); } rect.y += rect.height; rect.height = getGenotypeBandHeight(); if (areFeaturesStacked()) { // The sample bounds list will get reset when the names are drawn. sampleBounds.clear(); drawBackground(g2D, rect, visibleRectangle, BackgroundType.NAME); } renderBoundaryLines(g2D, trackRectangle, visibleRectangle); } /** * Render sample attributes, if any. * * @param g2D * @param trackRectangle * @param visibleRectangle * @param attributeNames * @param mouseRegions */ public void renderAttributes(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle, List<String> attributeNames, List<MouseableRegion> mouseRegions) { top = trackRectangle.y; Rectangle rect = new Rectangle(trackRectangle); rect.height = getVariantsHeight(); if (rect.intersects(visibleRectangle)) { super.renderAttributes(g2D, rect, visibleRectangle, attributeNames, mouseRegions); } if (getDisplayMode() == DisplayMode.COLLAPSED) { return; } rect.y += rect.height; rect.height = getGenotypeBandHeight(); Rectangle bandRectangle = new Rectangle(rect); // Make copy for later use drawBackground(g2D, rect, visibleRectangle, BackgroundType.ATTRIBUTE); if (grouped) { for (List<String> sampleList : samplesByGroups.values()) { renderAttributeBand(g2D, bandRectangle, visibleRectangle, attributeNames, sampleList, mouseRegions); bandRectangle.y += GROUP_BORDER_WIDTH; } } else { renderAttributeBand(g2D, bandRectangle, visibleRectangle, attributeNames, allSamples, mouseRegions); } renderBoundaryLines(g2D, trackRectangle, visibleRectangle); } /** * Render attributes for a sample. This is mostly a copy of AbstractTrack.renderAttributes(). * TODO -- refactor to eliminate duplicate code from AbstractTrack * * @param g2D * @param bandRectangle * @param visibleRectangle * @param attributeNames * @param sampleList * @param mouseRegions * @return */ private void renderAttributeBand(Graphics2D g2D, Rectangle bandRectangle, Rectangle visibleRectangle, List<String> attributeNames, List<String> sampleList, List<MouseableRegion> mouseRegions) { for (String sample : sampleList) { if (bandRectangle.intersects(visibleRectangle)) { int x = bandRectangle.x; for (String name : attributeNames) { String key = name.toUpperCase(); String attributeValue = AttributeManager.getInstance().getAttribute(sample, key); if (attributeValue != null) { Rectangle rect = new Rectangle(x, bandRectangle.y, AttributeHeaderPanel.ATTRIBUTE_COLUMN_WIDTH, bandRectangle.height); g2D.setColor(AttributeManager.getInstance().getColor(key, attributeValue)); g2D.fill(rect); mouseRegions.add(new MouseableRegion(rect, key, attributeValue)); } x += AttributeHeaderPanel.ATTRIBUTE_COLUMN_WIDTH + AttributeHeaderPanel.COLUMN_BORDER_WIDTH; } } bandRectangle.y += bandRectangle.height; } } /** * Draws the "greenbar" type background. Also, rather bizarrely, draws the sample names. * * @param g2D * @param bandRectangle * @param visibleRectangle * @param type */ private void drawBackground(Graphics2D g2D, Rectangle bandRectangle, Rectangle visibleRectangle, BackgroundType type) { if (getDisplayMode() == DisplayMode.COLLAPSED) { return; } boolean coloredLast = true; Rectangle textRectangle = new Rectangle(bandRectangle); textRectangle.height--; int bandFontSize = Math.min(fontSize, (int) bandRectangle.getHeight() - 1); Font font = FontManager.getFont(bandFontSize); Font oldFont = g2D.getFont(); g2D.setFont(font); if (grouped) { for (Map.Entry<String, List<String>> sampleGroup : samplesByGroups.entrySet()) { int y0 = bandRectangle.y; List<String> sampleList = sampleGroup.getValue(); coloredLast = colorBand(g2D, bandRectangle, visibleRectangle, coloredLast, textRectangle, sampleList, type); bandRectangle.y += GROUP_BORDER_WIDTH; if (type == BackgroundType.NAME && bandRectangle.height < 3) { String group = sampleGroup.getKey(); if (group != null) { g2D.setColor(Color.black); g2D.setFont(oldFont); int y2 = bandRectangle.y; Rectangle textRect = new Rectangle(bandRectangle.x, y0, bandRectangle.width, y2 - y0); GraphicUtils.drawWrappedText(group, textRect, g2D, true); } } } } else { coloredLast = colorBand(g2D, bandRectangle, visibleRectangle, coloredLast, textRectangle, allSamples, type); } g2D.setFont(oldFont); } private boolean colorBand(Graphics2D g2D, Rectangle bandRectangle, Rectangle visibleRectangle, boolean coloredLast, Rectangle textRectangle, List<String> sampleList, BackgroundType type) { boolean supressFill = (getDisplayMode() == DisplayMode.SQUISHED && squishedHeight < 4); for (String sample : sampleList) { if (coloredLast) { g2D.setColor(BAND1_COLOR); coloredLast = false; } else { g2D.setColor(BAND2_COLOR); coloredLast = true; } if (bandRectangle.intersects(visibleRectangle)) { if (!supressFill) { if (selectedSamples.contains(sample) && hasAlignmentFiles()) { g2D.setColor(SELECTED_BAND_COLOR); } g2D.fillRect(bandRectangle.x, bandRectangle.y, bandRectangle.width, bandRectangle.height); } if (type == BackgroundType.NAME) { sampleBounds .add(new SampleBounds(bandRectangle.y, bandRectangle.y + bandRectangle.height, sample)); if (bandRectangle.height >= 3) { String printName = sample; textRectangle.y = bandRectangle.y + 1; g2D.setColor(Color.black); GraphicUtils.drawWrappedText(printName, bandRectangle, g2D, false); } } else if (type == BackgroundType.ATTRIBUTE) { } } bandRectangle.y += bandRectangle.height; } return coloredLast; } public boolean getHideFiltered() { return hideFiltered; } public void setHideFiltered(boolean value) { this.hideFiltered = value; } @XmlAttribute public ColorMode getColorMode() { return coloring; } public void setColorMode(ColorMode mode) { this.coloring = mode; } public String getNameValueString(int y) { if (y < top + variantBandHeight) { return getName(); } else { String sample = getSampleAtPosition(y); return sample; } } /** * Return popup text for the given position * * @param chr * @param position - position in UCSC "0 based" genomic coordinates * @param y - pixel position in panel coordinates (i.e. not track coordinates) * @param frame * @return */ public String getValueStringAt(String chr, double position, int y, ReferenceFrame frame) { try { double maxDistance = 10 * frame.getScale(); if (y < top + getVariantsHeight()) { int modY = areFeaturesStacked() ? y : -1; Variant variant = getFeatureClosest(position, modY, frame.getName(), maxDistance); if (variant == null) return null; return getVariantToolTip(variant); } else { if (sampleBounds == null || sampleBounds.isEmpty()) return null; String sample = getSampleAtPosition(y); if (sample == null) return null; Variant variant = getFeatureClosest(position, -1, frame.getName(), maxDistance); return getSampleToolTip(sample, variant); } } catch (Exception e) { log.error("Error getting value string", e); return null; } } /** * Return the sample at the give pixel position * * @param y - screen position in pixels * @return */ private String getSampleAtPosition(int y) { if (sampleBounds.isEmpty()) { return null; } String sample = null; // Estimate the index of the sample, then do a linear search final int sampleCount = sampleBounds.size(); int firstSampleY = sampleBounds.get(0).top; int idx = Math.max(0, Math.min((y - firstSampleY) / getGenotypeBandHeight(), sampleCount - 1)); SampleBounds bounds = sampleBounds.get(idx); if (bounds.contains(y)) { sample = bounds.sample; } else if (bounds.top > y) { while (idx > 0) { idx--; bounds = sampleBounds.get(idx); if (bounds.contains(y)) { sample = bounds.sample; } } } else { while (idx < sampleCount - 1) { idx++; bounds = sampleBounds.get(idx); if (bounds.contains(y)) { sample = bounds.sample; } } } return sample; } /** * Return the variant closest to the genomic position in the given reference frame, within the prescribed tolerance * * @param position * @param y pixel position in panel coordinates (i.e. not track coordinates) * @param frameName * @param maxDistance * @return */ protected Variant getFeatureClosest(double position, int y, String frameName, double maxDistance) { PackedFeatures<IGVFeature> packedFeatures = packedFeaturesMap.get(frameName); if (packedFeatures == null) { return null; } Feature feature = null; List<IGVFeature> features; //We search only the specified row if y is a meaningful value. //Otherwise we search everything int row = ((y - top) / variantBandHeight); if (y < 0 || row >= getNumberOfFeatureLevels()) { features = packedFeatures.getFeatures(); } else { features = packedFeatures.getRows().get(row).getFeatures(); } if (features != null) { feature = FeatureUtils.getFeatureClosest(position, features); } if (feature == null || ((position < feature.getStart() - maxDistance) || (position > feature.getEnd() + maxDistance))) { return null; } else { return (Variant) feature; } } private String getVariantToolTip(Variant variant) { String id = variant.getID(); StringBuffer toolTip = new StringBuffer(); toolTip.append("Chr: " + variant.getChr()); toolTip.append("<br>Position: " + variant.getPositionString()); toolTip.append("<br>ID: " + id); toolTip.append("<br>Reference: " + variant.getReference()); List<Allele> alternates = variant.getAlternateAlleles(); if (alternates.size() > 0) { toolTip.append("<br>Alternate: " + StringUtils.join(alternates, ",")); } toolTip.append("<br>Qual: " + numFormat.format(variant.getPhredScaledQual())); toolTip.append("<br>Type: " + variant.getType()); if (variant.isFiltered()) { toolTip.append("<br>Is Filtered Out: Yes</b>"); toolTip = toolTip.append(getFilterTooltip(variant)); } else { toolTip.append("<br>Is Filtered Out: No</b><br>"); } toolTip.append("<br><b>Alleles:</b>"); toolTip.append(getAlleleToolTip(variant)); double[] af = variant.getAlleleFreqs(); if (af[0] < 0 && variant.getSampleNames().size() > 0) { af = new double[] { variant.getAlleleFraction() }; } String afMsg = "Unknown"; if (af[0] >= 0) { afMsg = numFormat.format(af[0]); for (int ii = 1; ii < af.length; ii++) { afMsg += ", " + numFormat.format(af[ii]); } } toolTip.append("<br>Allele Frequency: " + afMsg + "<br>"); if (variant.getSampleNames().size() > 0) { double afrac = variant.getAlleleFraction(); toolTip = toolTip.append("<br>Minor Allele Fraction: " + numFormat.format(afrac) + "<br>"); } toolTip.append("<br><b>Genotypes:</b>"); toolTip.append(getGenotypeToolTip(variant) + "<br>"); toolTip.append(getVariantInfo(variant) + "<br>"); return toolTip.toString(); } protected String getVariantInfo(Variant variant) { Set<String> keys = variant.getAttributes().keySet(); if (keys.size() > 0) { String toolTip = "<br><b>Variant Attributes</b>"; int count = 0; // Put AF and GMAF and put at the top, if present String k = "AF"; String afValue = variant.getAttributeAsString(k); if (afValue != null && afValue.length() > 0 && !afValue.equals("null")) { toolTip = toolTip.concat("<br>" + getFullName(k) + ": " + variant.getAttributeAsString(k)); } k = "GMAF"; afValue = variant.getAttributeAsString(k); if (afValue != null && afValue.length() > 0 && !afValue.equals("null")) { toolTip = toolTip.concat("<br>" + getFullName(k) + ": " + variant.getAttributeAsString(k)); } int maxFilterLines = getMaxFilterLines(); for (String key : keys) { count++; if (key.equals("AF") || key.equals("GMAF")) continue; if (count > maxFilterLines) { toolTip = toolTip.concat("<br>...."); break; } toolTip = toolTip.concat("<br>" + getFullName(key) + ": " + variant.getAttributeAsString(key)); } return toolTip; } return " "; } /** * The maximum number of filter lines to show for variants. * We show more info if the user is displaying a separate window than * if using tooltip * * @return */ private int getMaxFilterLines() { return IGV.getInstance().isShowDetailsOnHover() ? MAX_FILTER_LINES : 1000; } private String getSampleInfo(Genotype genotype) { Set<String> keys = genotype.getAttributes().keySet(); if (keys.size() > 0) { String tooltip = "<br><b>Sample Attributes</b>"; for (String key : keys) { try { tooltip = tooltip.concat("<br>" + getFullName(key) + ": " + genotype.getAttributeAsString(key)); } catch (IllegalArgumentException iae) { tooltip = tooltip.concat("<br>" + key + ": " + genotype.getAttributeAsString(key)); } } return tooltip; } return null; } public void clearSelectedVariant() { selectedVariant = null; } public List<String> getAllSamples() { return allSamples; } public int getSquishedHeight() { return squishedHeight; } public void setSquishedHeight(int squishedHeight) { this.squishedHeight = squishedHeight; } public void onTrackGroupEvent(TrackGroupEvent e) { setupGroupsFromAttributes(); } public boolean hasAlignmentFiles() { return alignmentFiles != null && !alignmentFiles.isEmpty(); } public Collection<String> getSelectedSamples() { return selectedSamples; } public static enum ColorMode { GENOTYPE, METHYLATION_RATE, ALLELE } public static enum BackgroundType { NAME, ATTRIBUTE, DATA; } static Map<String, String> fullNames = new HashMap(); static { fullNames.put("AA", "Ancestral Allele"); fullNames.put("AC", "Allele Count in Genotypes"); fullNames.put("AN", "Total Alleles in Genotypes"); fullNames.put("AF", "Allele Frequency"); fullNames.put("DP", "Depth"); fullNames.put("MQ", "Mapping Quality"); fullNames.put("NS", "Number of Samples with Data"); fullNames.put("BQ", "RMS Base Quality"); fullNames.put("SB", "Strand Bias"); fullNames.put("DB", "dbSNP Membership"); fullNames.put("GQ", "Genotype Quality"); fullNames.put("GL", "Genotype Likelihoods"); //Hom-ref, het, hom-var break this down into a group } static String getFullName(String key) { return fullNames.containsKey(key) ? fullNames.get(key) : key; } private String getSampleToolTip(String sample, Variant variant) { if (variant == null) return null; double goodBaseCount = variant.getGenotype(sample).getAttributeAsDouble("GB"); if (Double.isNaN(goodBaseCount)) goodBaseCount = 0; if (isEnableMethylationRateSupport() && goodBaseCount < 10) { return sample; } String id = variant.getID(); StringBuffer toolTip = new StringBuffer(); toolTip = toolTip.append("Chr: " + variant.getChr()); toolTip = toolTip.append("<br>Position: " + variant.getPositionString()); toolTip = toolTip.append("<br>ID: " + id + "<br>"); toolTip = toolTip.append("<br><b>Sample Information</b>"); toolTip = toolTip.append("<br>Sample: " + sample); Genotype genotype = variant.getGenotype(sample); if (genotype != null) { toolTip = toolTip.append("<br>Bases: " + genotype.getGenotypeString()); toolTip = toolTip.append("<br>Quality: " + numFormat.format(genotype.getPhredScaledQual())); toolTip = toolTip.append("<br>Type: " + genotype.getTypeString()); } if (variant.isFiltered()) { toolTip = toolTip.append("<br>Is Filtered Out: Yes</b>"); toolTip = toolTip.append(getFilterTooltip(variant)); } else { toolTip = toolTip.append("<br>Is Filtered Out: No</b><br>"); } if (genotype != null) { String sInfoStr = getSampleInfo(genotype); if (sInfoStr != null) { toolTip = toolTip.append(sInfoStr + "<br>"); } } return toolTip.toString(); } private String getFilterTooltip(Variant variant) { Collection filters = variant.getFilters(); String toolTip = "<br>"; for (Object filter : filters) { toolTip = toolTip.concat("- " + (String) filter + "<br>"); } return toolTip; } private String getAlleleToolTip(Variant counts) { double noCall = counts.getNoCallCount() * 2; double aNum = (counts.getHetCount() + counts.getHomRefCount() + counts.getHomVarCount()) * 2; double aCount = (counts.getHomVarCount() * 2 + counts.getHetCount()) * 2; String toolTip = "<br>No Call: " + (int) noCall; toolTip = toolTip.concat("<br>Allele Num: " + (int) aNum); toolTip = toolTip.concat("<br>Allele Count: " + (int) aCount); return toolTip; } private String getGenotypeToolTip(Variant counts) { int noCall = counts.getNoCallCount(); int homRef = counts.getHomRefCount(); int nonVar = noCall + homRef; int het = counts.getHetCount(); int homVar = counts.getHomVarCount(); int var = het + homVar; String toolTip = "<br>Non Variant: " + nonVar; toolTip = toolTip.concat("<br> - No Call: " + noCall); toolTip = toolTip.concat("<br> - Hom Ref: " + homRef); toolTip = toolTip.concat("<br>Variant: " + var); toolTip = toolTip.concat("<br> - Het: " + het); toolTip = toolTip.concat("<br> - Hom Var: " + homVar); return toolTip; } /** * Return the {@code Variant} object closest to the specified event * * @param te * @return * @api */ public Variant getSelectedVariant(final TrackClickEvent te) { final ReferenceFrame referenceFrame = te.getFrame(); Variant selVariant = null; if (referenceFrame != null && referenceFrame.getName() != null) { final double position = te.getChromosomePosition(); double maxDistance = 10 * referenceFrame.getScale(); selVariant = getFeatureClosest(position, te.getMouseEvent().getY(), referenceFrame.getName(), maxDistance); } return selVariant; } public IGVPopupMenu getPopupMenu(final TrackClickEvent te) { selectedVariant = getSelectedVariant(te); if (selectedVariant != null) { IGV.getInstance().doRefresh(); } return new VariantMenu(this, selectedVariant); } /** * Handle a mouse click from the name panel. * * @param e */ @Override public void handleNameClick(MouseEvent e) { String sampleAtPosition = getSampleAtPosition(e.getY()); if (e.isPopupTrigger()) { return; } if (e.isMetaDown() || e.isControlDown()) { if (sampleAtPosition != null) { if (selectedSamples.contains(sampleAtPosition)) { // selectedSamples.remove(sampleAtPosition); } else { selectedSamples.add(sampleAtPosition); } } } else if (e.isShiftDown() && !selectedSamples.isEmpty()) { int idx = getSampleIndex(sampleAtPosition); int lastIDX = getSampleIndex(selectedSamples.get(selectedSamples.size() - 1)); if (idx >= 0 && lastIDX >= 0) { selectedSamples.clear(); for (int i = Math.min(idx, lastIDX); i <= (Math.max(idx, lastIDX)); i++) { String s = sampleBounds.get(i).sample; selectedSamples.add(s); } } } else { if (sampleAtPosition != null) { if (selectedSamples.size() == 1 && selectedSamples.contains(sampleAtPosition)) { selectedSamples.clear(); IGV.getInstance().repaint(); return; } else { selectedSamples.clear(); } selectedSamples.add(sampleAtPosition); } } IGV.getInstance().repaint(); } /** * Return the index for the sample. This is a very inefficient implementation, but we don't care because * these lists are tiny. * * @param sample * @return */ private int getSampleIndex(String sample) { for (int i = 0; i < sampleBounds.size(); i++) { if (sample.equals(sampleBounds.get(i).sample)) { return i; } } return -1; } /** * Handle a mouse click from the data panel. * * @param te - wraps the MouseClickEvent and reference frame. * @return true if the click is handled, false otherwise */ @Override public boolean handleDataClick(TrackClickEvent te) { if (hasAlignmentFiles()) { final ReferenceFrame referenceFrame = te.getFrame(); final double position = te.getChromosomePosition(); double maxDistance = 10 * referenceFrame.getScale(); Variant f = getFeatureClosest(position, te.getMouseEvent().getY(), te.getFrame().getName(), maxDistance); selectedSamples.clear(); if (f != null) { String selectedSample = getSampleAtPosition(te.getMouseEvent().getY()); if (selectedSample != null) { // Select clicked sample and all other adjacent with the same genotype Genotype genotype = f.getGenotype(selectedSample); GenotypeType type = genotype.getType(); int idx = getSampleIndex(selectedSample); for (int i = idx; i < sampleBounds.size(); i++) { String s = sampleBounds.get(i).sample; Genotype gt = f.getGenotype(s); if (gt != null && type == gt.getType()) { selectedSamples.add(s); } else { break; } } for (int i = idx - 1; i >= 0; i--) { String s = sampleBounds.get(i).sample; Genotype gt = f.getGenotype(s); if (gt != null && type == gt.getType()) { selectedSamples.add(s); } else { break; } } } } IGV.getInstance().doRefresh(); } if (IGV.getInstance().isShowDetailsOnClick()) { openTooltipWindow(te); } return true; } public void loadSelectedBams() { Runnable runnable = new Runnable() { public void run() { // Use a set to enforce uniqueness final int nSamples = selectedSamples.size(); if (nSamples == 0) { return; } Set<String> bams = new HashSet<String>(nSamples); String name = ""; int n = 0; for (String sample : selectedSamples) { bams.add(getBamFileForSample(sample)); n++; if (n < 7) { if (n == 6) { name += "..."; } else { name += sample; if (n < nSamples) name += ", "; } } } if (bams.size() > 20) { boolean proceed = MessageUtils.confirm("Are you sure you want to load " + nSamples + " bams?"); if (!proceed) return; } String bamList = ""; for (String bam : bams) { bamList += bam + ","; } ResourceLocator loc = new ResourceLocator(bamList); loc.setType("alist"); loc.setName(name); List<Track> tracks = null; try { tracks = IGV.getInstance().load(loc); } catch (Exception e) { log.error("Error loading bam: " + loc.getPath(), e); } TrackPanel panel = IGV.getInstance().getVcfBamPanel(); panel.clearTracks(); panel.addTracks(tracks); } }; LongRunningTask.submit(runnable); } /** * Return the nextLine or previous feature relative to the center location. * <p/> * Loop through "next feature from super implementation until first non-filtered variant is found. * * @param chr * @param center * @param forward * @return * @throws java.io.IOException */ @Override public Feature nextFeature(String chr, double center, boolean forward, ReferenceFrame frame) throws IOException { if (getHideFiltered()) { Feature f; while ((f = super.nextFeature(chr, center, forward, frame)) != null) { if (!(f instanceof Variant) || !((Variant) f).isFiltered()) { return f; } } return null; } else { return super.nextFeature(chr, center, forward, frame); } } static class SampleBounds { int top; int bottom; String sample; SampleBounds(int top, int bottom, String sample) { this.top = top; this.bottom = bottom; this.sample = sample; } boolean contains(int y) { return y >= top && y <= bottom; } } @SubtlyImportant private static VariantTrack getNextTrack() { return (VariantTrack) IGVSessionReader.getNextTrack(); } }