Skip to content

Commit

Permalink
Adding additional options for duplicate reads. (#1542)
Browse files Browse the repository at this point in the history
Adding additional options for duplicate reads.

* new GroupBy duplicates option
* new Duplicates submenu which allows rapid toggling of duplicates between three states
  * Filtered: not visible
  * Visible: treated like normal reads
  * Textures: rendered with a visible slash texture
  • Loading branch information
lbergelson authored Aug 18, 2024
1 parent 1ed0fc4 commit cc37d33
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 73 deletions.
117 changes: 57 additions & 60 deletions src/main/java/org/broad/igv/sam/AlignmentPacker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down
49 changes: 45 additions & 4 deletions src/main/java/org/broad/igv/sam/AlignmentRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -118,6 +118,7 @@ public class AlignmentRenderer {
private static Map<String, AlignmentTrack.OrientationType> f2f1OrientationTypes;
private static Map<String, AlignmentTrack.OrientationType> rfOrientationTypes;
private static Map<AlignmentTrack.OrientationType, Color> typeToColorMap;
private static final Map<Color, TexturePaint> duplicateTextures = new HashMap<>();

public static final HSLColorTable tenXColorTable1 = new HSLColorTable(30);
public static final HSLColorTable tenXColorTable2 = new HSLColorTable(270);
Expand Down Expand Up @@ -325,11 +326,11 @@ public void renderAlignments(List<Alignment> 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;
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/org/broad/igv/sam/AlignmentTileLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
40 changes: 39 additions & 1 deletion src/main/java/org/broad/igv/sam/AlignmentTrack.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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"));
}
Expand Down Expand Up @@ -1798,6 +1835,7 @@ public void unmarshalXML(Element element, Integer version) {
minJunctionCoverage = Integer.parseInt(element.getAttribute("minJunctionCoverage"));
}
}

}


Expand Down
Loading

0 comments on commit cc37d33

Please sign in to comment.