mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Trackster: fixed bugs and refactored code to better support BAM visualization. Bugs fixed: (a) multi-mapping reads now display correctly; (b) no_detail display for interval index works; and (c) paired reads now have independent CIGAR strings. Also, BAI payload now mirrors interval_index payload so that both datatypes can be handled in the same code.
This commit is contained in:
@@ -235,11 +235,13 @@ class BamDataProvider( TracksDataProvider ):
|
||||
else:
|
||||
return None
|
||||
# Encode reads as list of lists; each read is a list with the format
|
||||
# [<name>, <start>, <end>, <read_1_seq>, <cigar>, <read_1>, <read_2>]
|
||||
# [<guid>, <start>, <end>, <name>, <read_1>, <read_2>]
|
||||
# where <read_1> has the format
|
||||
# [<start>, <end>, ?<read_seq>?]
|
||||
# [<start>, <end>, <cigar>, ?<read_seq>?]
|
||||
# and <read_2> has the format
|
||||
# [<start>, <end>, ?<read_seq>?]
|
||||
# [<start>, <end>, <cigar>, ?<read_seq>?]
|
||||
# For single-end reads, read has format:
|
||||
# [<guid>, <start>, <end>, <name>, cigar, seq]
|
||||
# NOTE: read end and sequence data are not valid for reads outside of
|
||||
# requested region and should not be used.
|
||||
results = []
|
||||
@@ -258,32 +260,38 @@ class BamDataProvider( TracksDataProvider ):
|
||||
if read.is_proper_pair:
|
||||
if qname in paired_pending: # one in dict is always first
|
||||
pair = paired_pending[qname]
|
||||
results.append( [ qname, pair['start'], read.pos + read_len, seq, read.cigar, [pair['start'], pair['end'], pair['seq']], [read.pos, read.pos + read_len, seq] ] )
|
||||
results.append( [ "%i_%s" % ( read.pos-start, qname ),
|
||||
pair['start'],
|
||||
read.pos + read_len,
|
||||
qname,
|
||||
[ pair['start'], pair['end'], pair['cigar'], pair['seq'] ],
|
||||
[ read.pos, read.pos + read_len, read.cigar, seq]
|
||||
] )
|
||||
del paired_pending[qname]
|
||||
else:
|
||||
paired_pending[qname] = { 'start': read.pos, 'end': read.pos + read_len, 'seq': seq, 'mate_start': read.mpos, 'rlen': read_len, 'cigar': read.cigar }
|
||||
else:
|
||||
results.append( [qname, read.pos, read.pos + read_len, seq, read.cigar] )
|
||||
results.append( [ "%i_%s" % ( read.pos-start, qname ), read.pos, read.pos + read_len, qname, read.cigar, read.seq] )
|
||||
# Take care of reads whose mates are out of range.
|
||||
for qname, read in paired_pending.iteritems():
|
||||
if read['mate_start'] < read['start']:
|
||||
# Mate is before read.
|
||||
start = read['mate_start']
|
||||
end = read['end']
|
||||
read_start = read['mate_start']
|
||||
read_end = read['end']
|
||||
# Make read_1 start=end so that length is 0 b/c we don't know
|
||||
# read length.
|
||||
r1 = [read['mate_start'], read['mate_start']]
|
||||
r2 = [read['start'], read['end'], read['seq']]
|
||||
r1 = [ read['mate_start'], read['mate_start'] ]
|
||||
r2 = [ read['start'], read['end'], read['cigar'], read['seq'] ]
|
||||
else:
|
||||
# Mate is after read.
|
||||
start = read['start']
|
||||
read_start = read['start']
|
||||
# Make read_2 start=end so that length is 0 b/c we don't know
|
||||
# read length. Hence, end of read is start of read_2.
|
||||
end = read['mate_start']
|
||||
r1 = [read['start'], read['end'], read['seq']]
|
||||
r2 = [read['mate_start'], read['mate_start']]
|
||||
read_end = read['mate_start']
|
||||
r1 = [ read['start'], read['end'], read['cigar'], read['seq'] ]
|
||||
r2 = [ read['mate_start'], read['mate_start'] ]
|
||||
|
||||
results.append( [ qname, start, end, read['seq'], read['cigar'], r1, r2 ] )
|
||||
results.append( [ "%i_%s" % ( read_start-start, qname ), read_start, read_end, qname, r1, r2 ] )
|
||||
|
||||
bamfile.close()
|
||||
return { 'data': results, 'message': message }
|
||||
@@ -437,6 +445,11 @@ class IntervalIndexDataProvider( TracksDataProvider ):
|
||||
if chrom not in index.indexes and chrom[3:] in index.indexes:
|
||||
chrom = chrom[3:]
|
||||
|
||||
#
|
||||
# Build data to return. Payload format is:
|
||||
# [ <guid/offset>, <start>, <end>, <name>, <strand>, <blocks>]
|
||||
#
|
||||
no_detail = ( "no_detail" in kwargs )
|
||||
for start, end, offset in index.find(chrom, start, end):
|
||||
if count >= MAX_VALS:
|
||||
message = "Only the first %s features are being displayed." % MAX_VALS
|
||||
@@ -445,16 +458,16 @@ class IntervalIndexDataProvider( TracksDataProvider ):
|
||||
source.seek( offset )
|
||||
# TODO: can we use column metadata to fill out payload?
|
||||
# TODO: use function to set payload data
|
||||
if "no_detail" not in kwargs:
|
||||
if isinstance( self.original_dataset.datatype, Gff ):
|
||||
# GFF dataset.
|
||||
reader = GFFReaderWrapper( source, fix_strand=True )
|
||||
feature = reader.next()
|
||||
payload = package_gff_feature( feature )
|
||||
payload.insert( 0, offset )
|
||||
elif isinstance( self.original_dataset.datatype, Bed ):
|
||||
# BED dataset.
|
||||
payload = [ offset, start, end ]
|
||||
if isinstance( self.original_dataset.datatype, Gff ):
|
||||
# GFF dataset.
|
||||
reader = GFFReaderWrapper( source, fix_strand=True )
|
||||
feature = reader.next()
|
||||
payload = package_gff_feature( feature, no_detail )
|
||||
payload.insert( 0, offset )
|
||||
elif isinstance( self.original_dataset.datatype, Bed ):
|
||||
# BED dataset.
|
||||
payload = [ offset, start, end ]
|
||||
if not no_detail:
|
||||
feature = source.readline().split()
|
||||
length = len(feature)
|
||||
if length >= 4:
|
||||
@@ -472,7 +485,7 @@ class IntervalIndexDataProvider( TracksDataProvider ):
|
||||
block_starts = [ int(n) for n in feature[11].split(',') if n != '' ]
|
||||
blocks = zip( block_sizes, block_starts )
|
||||
payload.append( [ ( start + block[1], start + block[1] + block[0] ) for block in blocks ] )
|
||||
|
||||
|
||||
if length >= 5:
|
||||
payload.append( int(feature[4]) ) # score
|
||||
|
||||
@@ -564,9 +577,15 @@ def get_data_provider( name=None, original_dataset=None ):
|
||||
pass
|
||||
return data_provider
|
||||
|
||||
def package_gff_feature( feature ):
|
||||
def package_gff_feature( feature, no_detail=False ):
|
||||
""" Package a GFF feature in an array for data providers. """
|
||||
feature = convert_gff_coords_to_bed( feature )
|
||||
|
||||
# No detail means only start, end.
|
||||
if no_detail:
|
||||
return [ feature.start, feature.end ]
|
||||
|
||||
# Return full feature.
|
||||
payload = [ feature.start,
|
||||
feature.end,
|
||||
feature.name(),
|
||||
|
||||
File diff suppressed because one or more lines are too long
+147
-74
@@ -8,7 +8,7 @@
|
||||
var
|
||||
DENSITY = 200,
|
||||
FEATURE_LEVELS = 10,
|
||||
MAX_FEATURE_DEPTH = 50,
|
||||
MAX_FEATURE_DEPTH = 100,
|
||||
DEFAULT_DATA_QUERY_WAIT = 5000,
|
||||
CONNECTOR_COLOR = "#ccc",
|
||||
DATA_ERROR = "There was an error in indexing this dataset.",
|
||||
@@ -1600,68 +1600,65 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
});
|
||||
}
|
||||
},
|
||||
incremental_slots: function( level, features, no_detail, mode ) {
|
||||
if (!this.inc_slots[level]) {
|
||||
this.inc_slots[level] = {};
|
||||
this.inc_slots[level].w_scale = level;
|
||||
this.inc_slots[level].mode = mode;
|
||||
//
|
||||
// Place features in slots for drawing (i.e. pack features).
|
||||
// this.inc_slots[level] is created in this method. this.inc_slots[level]
|
||||
// is a dictionary of slotted features; key is feature uid, value is a dictionary
|
||||
// with keys 'slot' and 'text'.
|
||||
// Returns the number of slots used to pack features.
|
||||
//
|
||||
incremental_slots: function(level, features, no_detail, mode) {
|
||||
//
|
||||
// Get/create incremental slots for level. If display mode changed,
|
||||
// need to create new slots.
|
||||
//
|
||||
var inc_slots = this.inc_slots[level];
|
||||
if (!inc_slots || (inc_slots.mode !== mode)) {
|
||||
inc_slots = {};
|
||||
inc_slots.w_scale = level;
|
||||
inc_slots.mode = mode;
|
||||
this.inc_slots[level] = inc_slots;
|
||||
this.s_e_by_tile[level] = {};
|
||||
}
|
||||
|
||||
//
|
||||
// If feature already exists in slots (from previously seen tiles), use the same slot,
|
||||
// otherwise if not seen, add to "undone" list for slot calculation.
|
||||
//
|
||||
var w_scale = inc_slots.w_scale,
|
||||
undone = [], slotted = [],
|
||||
highest_slot = 0, // To measure how big to draw canvas
|
||||
max_low = this.view.max_low;
|
||||
// TODO: Should calculate zoom tile index, which will improve performance
|
||||
// by only having to look at a smaller subset
|
||||
// if (this.s_e_by_tile[0] === undefined) { this.s_e_by_tile[0] = []; }
|
||||
var w_scale = this.inc_slots[level].w_scale,
|
||||
undone = [],
|
||||
highest_slot = 0, // To measure how big to draw canvas
|
||||
max_low = this.view.max_low;
|
||||
|
||||
var slotted = [];
|
||||
// Reset packing when we change display mode
|
||||
if (this.inc_slots[level].mode !== mode) {
|
||||
delete this.inc_slots[level];
|
||||
this.inc_slots[level] = { "mode": mode, "w_scale": w_scale };
|
||||
delete this.s_e_by_tile[level];
|
||||
this.s_e_by_tile[level] = {};
|
||||
}
|
||||
// If feature already exists in slots (from previously seen tiles), use the same slot,
|
||||
// otherwise if not seen, add to "undone" list for slot calculation
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
var feature = features[i],
|
||||
feature_uid = feature[0];
|
||||
if (this.inc_slots[level][feature_uid] !== undefined) {
|
||||
highest_slot = Math.max(highest_slot, this.inc_slots[level][feature_uid]);
|
||||
slotted.push(this.inc_slots[level][feature_uid]);
|
||||
if (inc_slots[feature_uid] !== undefined) {
|
||||
highest_slot = Math.max(highest_slot, inc_slots[feature_uid]);
|
||||
slotted.push(inc_slots[feature_uid]);
|
||||
} else {
|
||||
undone.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("Slotted: ", features.length - undone.length, "/", features.length, slotted);
|
||||
for (var i = 0, len = undone.length; i < len; i++) {
|
||||
var feature = features[undone[i]],
|
||||
feature_uid = feature[0],
|
||||
feature_start = feature[1],
|
||||
feature_end = feature[2],
|
||||
feature_name = feature[3],
|
||||
f_start = Math.floor( (feature_start - max_low) * w_scale ),
|
||||
f_end = Math.ceil( (feature_end - max_low) * w_scale );
|
||||
|
||||
if (feature_name !== undefined && !no_detail) {
|
||||
var text_len = CONTEXT.measureText(feature_name).width;
|
||||
if (f_start - text_len < 0) {
|
||||
f_end += text_len;
|
||||
} else {
|
||||
f_start -= text_len;
|
||||
}
|
||||
}
|
||||
|
||||
var j = 0;
|
||||
// Try to fit the feature to the first slot that doesn't overlap any other features in that slot
|
||||
while (j <= MAX_FEATURE_DEPTH) {
|
||||
var found = true;
|
||||
if (this.s_e_by_tile[level][j] !== undefined) {
|
||||
for (var k = 0, k_len = this.s_e_by_tile[level][j].length; k < k_len; k++) {
|
||||
var s_e = this.s_e_by_tile[level][j][k];
|
||||
//
|
||||
// Slot unslotted features.
|
||||
//
|
||||
var s_e_by_tile = this.s_e_by_tile[level];
|
||||
|
||||
// Find the first slot s/t current feature doesn't overlap any other features in that slot.
|
||||
// Returns -1 if no slot was found.
|
||||
var find_slot = function(f_start, f_end) {
|
||||
var found;
|
||||
for (var slot_num = 0, slot = undefined; slot_num <= MAX_FEATURE_DEPTH; slot_num++) {
|
||||
found = true;
|
||||
slot = s_e_by_tile[slot_num];
|
||||
if (slot !== undefined) {
|
||||
// Iterate through features already in slot to see if current feature will fit.
|
||||
for (var k = 0, k_len = slot.length; k < k_len; k++) {
|
||||
var s_e = slot[k];
|
||||
if (f_end > s_e[0] && f_start < s_e[1]) {
|
||||
found = false;
|
||||
break;
|
||||
@@ -1669,20 +1666,99 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
if (this.s_e_by_tile[level][j] === undefined) { this.s_e_by_tile[level][j] = []; }
|
||||
this.s_e_by_tile[level][j].push([f_start, f_end]);
|
||||
this.inc_slots[level][feature_uid] = j;
|
||||
highest_slot = Math.max(highest_slot, j);
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
return slot_num;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Do slotting.
|
||||
for (var i = 0, len = undone.length; i < len; i++) {
|
||||
var feature = features[undone[i]],
|
||||
feature_uid = feature[0],
|
||||
feature_start = feature[1],
|
||||
feature_end = feature[2],
|
||||
feature_name = feature[3],
|
||||
// Where to start, end drawing on screen.
|
||||
f_start = Math.floor( (feature_start - max_low) * w_scale ),
|
||||
f_end = Math.ceil( (feature_end - max_low) * w_scale ),
|
||||
text_len = CONTEXT.measureText(feature_name).width,
|
||||
text_align;
|
||||
|
||||
// Update start, end drawing locations to include feature name.
|
||||
// Try to put the name on the left, if not, put on right.
|
||||
if (feature_name !== undefined && !no_detail) {
|
||||
if (f_start - text_len < 0) {
|
||||
f_end += text_len;
|
||||
text_align = "left";
|
||||
} else {
|
||||
f_start -= text_len;
|
||||
text_align = "right";
|
||||
}
|
||||
}
|
||||
|
||||
// Find slot.
|
||||
var slot_num = find_slot(f_start, f_end);
|
||||
if (slot_num < 0) {
|
||||
/*
|
||||
TODO: this is not yet working --
|
||||
console.log(feature_uid, "looking for slot with text on the right");
|
||||
// Slot not found. If text was on left, try on right and see
|
||||
// if slot can be found.
|
||||
// TODO: are there any checks we need to do to ensure that text
|
||||
// will fit on tile?
|
||||
if (text_align == "left") {
|
||||
f_start -= text_len;
|
||||
f_end -= text_len;
|
||||
text_align = "right";
|
||||
slot_num = find_slot(f_start, f_end);
|
||||
}
|
||||
if (slot_num >= 0) {
|
||||
console.log(feature_uid, "found slot with text on the right");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Do slotting.
|
||||
if (slot_num >= 0) {
|
||||
// Add current feature to slot.
|
||||
slot = s_e_by_tile[slot_num];
|
||||
if (slot === undefined) {
|
||||
slot = s_e_by_tile[slot_num] = [];
|
||||
}
|
||||
slot.push([f_start, f_end]);
|
||||
inc_slots[feature_uid] = slot_num;
|
||||
highest_slot = Math.max(highest_slot, slot_num);
|
||||
}
|
||||
else {
|
||||
// TODO: remove this warning when skipped features are handled.
|
||||
// Show warning for skipped feature.
|
||||
console.log("WARNING: not displaying feature"); // , feature_uid, f_start, f_end);
|
||||
}
|
||||
}
|
||||
return highest_slot;
|
||||
|
||||
// Debugging: view slots data.
|
||||
/*
|
||||
for (var i = 0; i < MAX_FEATURE_DEPTH; i++) {
|
||||
var slot = s_e_by_tile[i];
|
||||
if (slot !== undefined) {
|
||||
console.log(i, "*************");
|
||||
for (var k = 0, k_len = slot.length; k < k_len; k++) {
|
||||
console.log("\t", slot[k][0], slot[k][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
return highest_slot;
|
||||
},
|
||||
// Right now this function is used only for rendering BAM reads.
|
||||
rect_or_text: function( ctx, w_scale, tile_low, tile_high, feature_start, orig_seq, cigar, y_center ) {
|
||||
rect_or_text: function( ctx, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center ) {
|
||||
ctx.textAlign = "center";
|
||||
var cur_offset = 0,
|
||||
gap = Math.round(w_scale / 2);
|
||||
@@ -1895,28 +1971,26 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
// All features need a start, end, and vertical center.
|
||||
var f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ),
|
||||
f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ),
|
||||
y_center = (mode === "Dense" ? 1 : (1 + slots[feature_uid])) * y_scale;
|
||||
|
||||
y_center = (mode === "Dense" ? 1 : (1 + slots[feature_uid])) * y_scale;
|
||||
var thickness, y_start, thick_start = null, thick_end = null;
|
||||
|
||||
// BAM/read drawing.
|
||||
if (result.dataset_type === "bai") {
|
||||
var cigar = feature[4];
|
||||
ctx.fillStyle = block_color;
|
||||
if (feature[5] instanceof Array) {
|
||||
// Read is paired.
|
||||
var b1_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ),
|
||||
b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ),
|
||||
b2_start = Math.floor( Math.max(0, (feature[6][0] - tile_low) * w_scale) ),
|
||||
b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[6][1] - tile_low) * w_scale)) );
|
||||
|
||||
var b1_start = Math.floor( Math.max(0, (feature[4][0] - tile_low) * w_scale) ),
|
||||
b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[4][1] - tile_low) * w_scale)) ),
|
||||
b2_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ),
|
||||
b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) );
|
||||
|
||||
// Draw left/forward read.
|
||||
if (feature[5][1] >= tile_low && feature[5][0] <= tile_high) {
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], cigar, y_center);
|
||||
if (feature[4][1] >= tile_low && feature[4][0] <= tile_high) {
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center);
|
||||
}
|
||||
// Draw right/reverse read.
|
||||
if (feature[6][1] >= tile_low && feature[6][0] <= tile_high) {
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[6][0], feature[6][2], cigar, y_center);
|
||||
if (feature[5][1] >= tile_low && feature[5][0] <= tile_high) {
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center);
|
||||
}
|
||||
// Draw connector.
|
||||
if (b2_start > b1_end) {
|
||||
@@ -1926,21 +2000,20 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
} else {
|
||||
// Read is single.
|
||||
ctx.fillStyle = block_color;
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature_start, feature_name, cigar, y_center);
|
||||
this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center);
|
||||
}
|
||||
if (mode !== "Dense" && !no_detail && feature_start > tile_low) {
|
||||
// Draw label
|
||||
// Draw label.
|
||||
ctx.fillStyle = this.prefs.label_color;
|
||||
if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) {
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(feature_uid, f_end + 2 + left_offset, y_center + 8);
|
||||
ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8);
|
||||
} else {
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(feature_uid, f_start - 2 + left_offset, y_center + 8);
|
||||
ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8);
|
||||
}
|
||||
ctx.fillStyle = block_color;
|
||||
}
|
||||
|
||||
}
|
||||
// Interval index drawing.
|
||||
else if (result.dataset_type === "interval_index") {
|
||||
|
||||
Reference in New Issue
Block a user