diff --git a/src/main/java/org/broad/igv/sam/AlignmentPacker.java b/src/main/java/org/broad/igv/sam/AlignmentPacker.java index 6c269e786..48bfee048 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentPacker.java +++ b/src/main/java/org/broad/igv/sam/AlignmentPacker.java @@ -438,69 +438,64 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp Range pos = renderOptions.getGroupByPos(); String readNameParts[], movieName, zmw; - switch (groupBy) { - case CLUSTER: - return al.getClusterName(); - case STRAND: - return al.isNegativeStrand() ? "-" : "+"; - case SAMPLE: - return al.getSample(); - case LIBRARY: - return al.getLibrary(); - case READ_GROUP: - return al.getReadGroup(); - case LINKED: - return (al instanceof LinkedAlignment) ? "Linked" : ""; - case PHASE: - return al.getAttribute("HP"); - case TAG: + return switch (groupBy) { + case CLUSTER -> al.getClusterName(); + case STRAND -> al.isNegativeStrand() ? "-" : "+"; + case SAMPLE -> al.getSample(); + case LIBRARY -> al.getLibrary(); + case READ_GROUP -> al.getReadGroup(); + case LINKED -> (al instanceof LinkedAlignment) ? "Linked" : ""; + case PHASE -> al.getAttribute("HP"); + case TAG -> { Object tagValue = tag == null ? null : al.getAttribute(tag); if (tagValue == null) { - return null; + yield null; } else if (tagValue instanceof Integer || tagValue instanceof Float || tagValue instanceof Double) { - return tagValue; + yield tagValue; } else { - return tagValue.toString(); + yield tagValue.toString(); } - case FIRST_OF_PAIR_STRAND: + } + case FIRST_OF_PAIR_STRAND -> { Strand strand = al.getFirstOfPairStrand(); - String strandString = strand == Strand.NONE ? null : strand.toString(); - return strandString; - case READ_ORDER: + yield strand == Strand.NONE ? null : strand.toString(); + } + case READ_ORDER -> { if (al.isPaired() && al.isFirstOfPair()) { - return "FIRST"; + yield "FIRST"; } else if (al.isPaired() && al.isSecondOfPair()) { - return "SECOND"; + yield "SECOND"; } else { - return ""; + yield ""; } - case PAIR_ORIENTATION: + } + case PAIR_ORIENTATION -> { PEStats peStats = AlignmentRenderer.getPEStats(al, renderOptions); AlignmentTrack.OrientationType type = AlignmentRenderer.getOrientationType(al, peStats); if (type == null) { - return AlignmentTrack.OrientationType.UNKNOWN.name(); + yield AlignmentTrack.OrientationType.UNKNOWN.name(); } - return type.name(); - case MATE_CHROMOSOME: + yield type.name(); + } + case MATE_CHROMOSOME -> { ReadMate mate = al.getMate(); if (mate == null) { - return null; + yield null; } if (!mate.isMapped()) { - return "UNMAPPED"; + yield "UNMAPPED"; } else { - return mate.getChr(); + yield mate.getChr(); } - case CHIMERIC: - return al.getAttribute(SAMTag.SA.name()) != null ? "CHIMERIC" : ""; - case SUPPLEMENTARY: - return al.isSupplementary() ? "SUPPLEMENTARY" : ""; - case REFERENCE_CONCORDANCE: - return !al.isProperPair() || - al.getCigarString().toUpperCase().contains("S") || - al.isSupplementary() ? - "DISCORDANT" : ""; - case BASE_AT_POS: + } + case NONE -> null; + case CHIMERIC -> al.getAttribute(SAMTag.SA.name()) != null ? "CHIMERIC" : ""; + case SUPPLEMENTARY -> al.isSupplementary() ? "SUPPLEMENTARY" : ""; + case REFERENCE_CONCORDANCE -> !al.isProperPair() || + al.getCigarString().toUpperCase().contains("S") || + al.isSupplementary() ? + "DISCORDANT" : ""; + case BASE_AT_POS -> { // Use a string prefix to enforce grouping rules: // 1: alignments with a base at the position // 2: alignments with a gap at the position @@ -513,14 +508,15 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp byte[] baseAtPos = new byte[]{al.getBase(pos.getStart())}; if (baseAtPos[0] == 0) { // gap at position - return "2:"; + yield "2:"; } else { // base at position - return "1:" + new String(baseAtPos); + yield "1:" + new String(baseAtPos); } } else { // does not overlap position - return "3:"; + yield "3:"; } - case INSERTION_AT_POS: + } + case INSERTION_AT_POS -> { // Use a string prefix to enforce grouping rules: // 1: alignments with a base at the position // 2: alignments with a gap at the position @@ -538,31 +534,32 @@ private Object getGroupValue(Alignment al, AlignmentTrack.RenderOptions renderOp if (rightInsertion != null) { insertionBaseCount += rightInsertion.getLength(); } - return insertionBaseCount; + yield insertionBaseCount; } else { - return 0; + yield 0; } - - case MOVIE: // group PacBio reads by movie + } + case MOVIE -> { readNameParts = al.getReadName().split("/"); if (readNameParts.length < 3) { - return ""; + yield ""; } movieName = readNameParts[0]; - return movieName; - case ZMW: // group PacBio reads by ZMW + yield movieName; // group PacBio reads by movie + } + case ZMW -> { readNameParts = al.getReadName().split("/"); if (readNameParts.length < 3) { - return ""; + yield ""; } movieName = readNameParts[0]; zmw = readNameParts[1]; - return movieName + "/" + zmw; - case MAPPING_QUALITY: - return al.getMappingQuality(); - } - return null; + yield movieName + "/" + zmw; // group PacBio reads by ZMW + } + case MAPPING_QUALITY -> al.getMappingQuality(); + case DUPLICATE -> al.isDuplicate() ? "duplicate" : "non-duplicate"; + }; } interface BucketCollection { diff --git a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java index b93184bd8..12ed96470 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentRenderer.java +++ b/src/main/java/org/broad/igv/sam/AlignmentRenderer.java @@ -35,7 +35,6 @@ import org.broad.igv.prefs.IGVPreferences; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.GraphicUtils; -import org.broad.igv.renderer.SequenceRenderer; import org.broad.igv.sam.AlignmentTrack.ColorOption; import org.broad.igv.sam.BisulfiteBaseInfo.DisplayStatus; import org.broad.igv.sam.mods.BaseModificationRenderer; @@ -55,6 +54,7 @@ import java.awt.*; import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,6 +118,7 @@ public class AlignmentRenderer { private static Map f2f1OrientationTypes; private static Map rfOrientationTypes; private static Map typeToColorMap; + private static final Map duplicateTextures = new HashMap<>(); public static final HSLColorTable tenXColorTable1 = new HSLColorTable(30); public static final HSLColorTable tenXColorTable2 = new HSLColorTable(270); @@ -325,11 +326,11 @@ public void renderAlignments(List alignments, int lastPixelDrawn = -1; for (Alignment alignment : alignments) { - // Compute the start and dend of the alignment in pixels + // Compute the start and end of the alignment in pixels double pixelStart = ((alignment.getStart() - origin) / locScale); double pixelEnd = ((alignment.getEnd() - origin) / locScale); - // If any any part of the feature fits in the track rectangle draw it + // If any part of the feature fits in the track rectangle draw it if (pixelEnd < rowRect.x || pixelStart > rowRect.getMaxX()) { continue; } @@ -757,7 +758,18 @@ private void drawAlignment( else if (block.getCigarOperator() == 'X') g = context.getGraphics2D("MISMATCH"); } - g.fill(blockShape); + if(renderOptions.getDuplicatesOption() == AlignmentTrack.DuplicatesOption.TEXTURE && alignment.isDuplicate()) { + final Graphics2D tg = (Graphics2D) g.create(); + + final TexturePaint tp = getDuplicateTexture(tg.getColor()); + // Add the texture paint to the graphics context. + tg.setPaint(tp); + tg.fill(blockShape); + tg.dispose(); + } else { + g.fill(blockShape); + } + if (outlineGraphics != null) { outlineGraphics.draw(blockShape); } @@ -944,6 +956,35 @@ private void drawAlignment( } + /** + * get a texture to apply to duplicate reads, caches the created textures according to their color + * @param baseColor the color to render the read + * @return a texture matching the base color with shading + */ + private TexturePaint getDuplicateTexture(Color baseColor) { + return duplicateTextures.computeIfAbsent(baseColor, color -> { + BufferedImage texture = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB); + Graphics2D big = texture.createGraphics(); + //Render into the BufferedImage graphics to create the texture + big.setColor(baseColor); + big.fillRect(0, 0, 5, 5); + + final Color darker = baseColor.darker(); + big.setColor(darker); + // hash pattern, probably better to save it as a bitmap somewhere + big.drawLine(0, 2, 0, 3); + big.drawLine(1, 3, 1, 4); + big.drawLine(2, 4, 2, 4); + big.drawLine(2, 0, 2, 0); + big.drawLine(3, 0, 3, 1); + big.drawLine(4, 1, 4, 2); + big.dispose(); + // Create a texture paint from the buffered image + Rectangle r = new Rectangle(0, 0, 5, 5); + return new TexturePaint(texture, r); + }); + } + private static void drawClippedEnds(final Graphics2D g, final int[] xPoly, final int[] yPoly, final boolean drawLeftClip, final boolean drawRightClip, final SupplementaryAlignment.SupplementaryNeighbors sri) { diff --git a/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java b/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java index cde160667..2ea3f2a28 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTileLoader.java @@ -26,7 +26,6 @@ package org.broad.igv.sam; import htsjdk.samtools.SAMFileHeader; -import htsjdk.samtools.SAMTag; import htsjdk.samtools.util.CloseableIterator; import org.broad.igv.event.IGVEvent; import org.broad.igv.logging.*; @@ -43,7 +42,6 @@ import org.broad.igv.util.ObjectCache; import org.broad.igv.util.RuntimeUtils; -import javax.swing.*; import java.io.IOException; import java.lang.ref.WeakReference; import java.text.DecimalFormat; @@ -134,7 +132,10 @@ AlignmentTile loadTile(String chr, boolean filterSecondaryAlignments = prefMgr.getAsBoolean(SAM_FILTER_SECONDARY_ALIGNMENTS); boolean filterSupplementaryAlignments = prefMgr.getAsBoolean(SAM_FILTER_SUPPLEMENTARY_ALIGNMENTS); ReadGroupFilter filter = ReadGroupFilter.getFilter(); - boolean filterDuplicates = prefMgr.getAsBoolean(SAM_FILTER_DUPLICATES); + boolean filterDuplicates = renderOptions != null + ? renderOptions.getDuplicatesOption() == AlignmentTrack.DuplicatesOption.FILTER + : prefMgr.getAsBoolean(SAM_FILTER_DUPLICATES); + int qualityThreshold = prefMgr.getAsInt(SAM_QUALITY_THRESHOLD); int alignmentScoreTheshold = prefMgr.getAsInt(SAM_ALIGNMENT_SCORE_THRESHOLD); diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrack.java b/src/main/java/org/broad/igv/sam/AlignmentTrack.java index 1fbb85d91..feaccdef4 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrack.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrack.java @@ -135,6 +135,19 @@ public boolean isSMRTKinetics() { } } + public enum DuplicatesOption { + FILTER("filter duplicates", true), + SHOW("show duplicates", false), + TEXTURE("texture duplicates", false); + + public final String label; + public final boolean filtered; + + DuplicatesOption(String label, Boolean filtered) { + this.label = label; + this.filtered = filtered; + } + } public enum ShadeAlignmentsOption { NONE("none"), @@ -169,7 +182,8 @@ public enum GroupOption { LINKED("linked"), PHASE("phase"), REFERENCE_CONCORDANCE("reference concordance"), - MAPPING_QUALITY("mapping quality"); + MAPPING_QUALITY("mapping quality"), + DUPLICATE("duplicate flag"); public final String label; public final boolean reverse; @@ -1221,6 +1235,7 @@ public static class RenderOptions implements Cloneable, Persistable { private SortOption sortOption; private GroupOption groupByOption; private ShadeAlignmentsOption shadeAlignmentsOption; + private DuplicatesOption duplicatesOption; private Integer mappingQualityLow; private Integer mappingQualityHigh; private boolean viewPairs = false; @@ -1472,6 +1487,22 @@ public ShadeAlignmentsOption getShadeAlignmentsOption() { } } + public DuplicatesOption getDuplicatesOption() { + final IGVPreferences prefs = getPreferences(); + if (duplicatesOption != null) { + return duplicatesOption; + } else { + duplicatesOption = prefs.getAsBoolean(SAM_FILTER_DUPLICATES) + ? DuplicatesOption.FILTER + : DuplicatesOption.SHOW; + } + return duplicatesOption; + } + + public void setDuplicatesOption(final DuplicatesOption duplicatesOption) { + this.duplicatesOption = duplicatesOption; + } + public int getMappingQualityLow() { return mappingQualityLow == null ? getPreferences().getAsInt(SAM_SHADE_QUALITY_LOW) : mappingQualityLow; } @@ -1595,6 +1626,9 @@ public void marshalXML(Document document, Element element) { if (shadeAlignmentsOption != null) { element.setAttribute("shadeAlignmentsByOption", shadeAlignmentsOption.toString()); } + if (duplicatesOption != null) { + element.setAttribute("duplicatesOption", duplicatesOption.toString()); + } if (mappingQualityLow != null) { element.setAttribute("mappingQualityLow", mappingQualityLow.toString()); } @@ -1724,6 +1758,9 @@ public void unmarshalXML(Element element, Integer version) { if (element.hasAttribute("shadeAlignmentsByOption")) { shadeAlignmentsOption = ShadeAlignmentsOption.valueOf(element.getAttribute("shadeAlignmentsByOption")); } + if (element.hasAttribute("duplicatesOption")) { + duplicatesOption = CollUtils.valueOf(DuplicatesOption.class, element.getAttribute("duplicatesOption"), null); + } if (element.hasAttribute("mappingQualityLow")) { mappingQualityLow = Integer.parseInt(element.getAttribute("mappingQualityLow")); } @@ -1798,6 +1835,7 @@ public void unmarshalXML(Element element, Integer version) { minJunctionCoverage = Integer.parseInt(element.getAttribute("minJunctionCoverage")); } } + } diff --git a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java index 2bfa44700..d767857ae 100644 --- a/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java +++ b/src/main/java/org/broad/igv/sam/AlignmentTrackMenu.java @@ -2,6 +2,8 @@ import htsjdk.samtools.SAMTag; import org.broad.igv.Globals; +import org.broad.igv.event.AlignmentTrackEvent; +import org.broad.igv.event.IGVEventBus; import org.broad.igv.feature.Range; import org.broad.igv.feature.Strand; import org.broad.igv.jbrowse.CircularViewUtilities; @@ -112,6 +114,8 @@ class AlignmentTrackMenu extends IGVPopupMenu { misMatchesItem.addActionListener(new Deselector(misMatchesItem, showAllItem)); showAllItem.addActionListener(new Deselector(showAllItem, misMatchesItem)); + // Duplicates + addDuplicatesMenuItem(); // Hide small indels JMenuItem smallIndelsItem = new JCheckBoxMenuItem("Hide small indels"); @@ -192,6 +196,31 @@ class AlignmentTrackMenu extends IGVPopupMenu { addShowItems(); } + private void addDuplicatesMenuItem() { + JMenu duplicatesMenu = new JMenu("Duplicates"); + for (AlignmentTrack.DuplicatesOption option : AlignmentTrack.DuplicatesOption.values()) { + JRadioButtonMenuItem mi = new JRadioButtonMenuItem(option.label); + final AlignmentTrack.DuplicatesOption previous = renderOptions.getDuplicatesOption(); + mi.setSelected(previous == option); + mi.addActionListener(aEvt -> { + renderOptions.setDuplicatesOption(option); + if(previous != option) { + if (previous.filtered != option.filtered){ + // duplicates are filtered out when loading the read data so a reload has to be performed in this case + IGVEventBus.getInstance().post(new AlignmentTrackEvent(AlignmentTrackEvent.Type.RELOAD)); + } else { + alignmentTrack.repaint(); + } + } else { + alignmentTrack.repaint(); + } + }); + + duplicatesMenu.add(mi); + } + add(duplicatesMenu); + } + private void addShowChimericRegions(final AlignmentTrack alignmentTrack, final TrackClickEvent e, final Alignment clickedAlignment) { JMenuItem item = new JMenuItem("View chimeric alignments in split screen"); @@ -421,15 +450,14 @@ void addGroupMenuItem(final TrackClickEvent te) {//ReferenceFrame frame) { AlignmentTrack.GroupOption.SUPPLEMENTARY, AlignmentTrack.GroupOption.REFERENCE_CONCORDANCE, AlignmentTrack.GroupOption.MOVIE, AlignmentTrack.GroupOption.ZMW, AlignmentTrack.GroupOption.READ_ORDER, AlignmentTrack.GroupOption.LINKED, AlignmentTrack.GroupOption.PHASE, - AlignmentTrack.GroupOption.MAPPING_QUALITY + AlignmentTrack.GroupOption.MAPPING_QUALITY, + AlignmentTrack.GroupOption.DUPLICATE }; for (final AlignmentTrack.GroupOption option : groupOptions) { JCheckBoxMenuItem mi = new JCheckBoxMenuItem(option.label); mi.setSelected(renderOptions.getGroupByOption() == option); - mi.addActionListener(aEvt -> { - groupAlignments(option, null, null); - }); + mi.addActionListener(aEvt -> groupAlignments(option, null, null)); groupMenu.add(mi); group.add(mi); } diff --git a/src/main/resources/org/broad/igv/prefs/preferences.tab b/src/main/resources/org/broad/igv/prefs/preferences.tab index a7d2bb569..d208a2791 100644 --- a/src/main/resources/org/broad/igv/prefs/preferences.tab +++ b/src/main/resources/org/broad/igv/prefs/preferences.tab @@ -107,7 +107,7 @@ SAM.ALLELE_USE_QUALITY Quality weight allele fraction boolean TRUE SAM.COLOR_BY Color alignments by select NONE|READ_STRAND|FIRST_OF_PAIR_STRAND|PAIR_ORIENTATION|UNEXPECTED_PAIR|INSERT_SIZE|BASE_MODIFICATION|BASE_MODIFICATION_2COLOR|SAMPLE|READ_GROUP|LIBRARY|MOVIE|ZMW|BISULFITE|NOMESEQ|TAG|MAPPED_SIZE|LINK_STRAND|YC_TAG UNEXPECTED_PAIR SAM.COLOR_BY_TAG Color by TAG string -SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY +SAM.GROUP_OPTION Group alignments by select NONE|STRAND|SAMPLE|READ_GROUP|LIBRARY|FIRST_OF_PAIR_STRAND|TAG|PAIR_ORIENTATION|MATE_CHROMOSOME|SV_ALIGNMENT|SUPPLEMENTARY|BASE_AT_POS|MOVIE|ZMW|HAPLOTYPE|READ_ORDER|LINKED|PHASE|MAPPING_QUALITY|DUPLICATE SAM.GROUP_BY_TAG Group by TAG string SAM.GROUP_ALL Syncrohize alignment track grouping boolean FALSE