diff --git a/lib/galaxy/visualization/tracks/data/array_tree.py b/lib/galaxy/visualization/tracks/data/array_tree.py index dcac488f693..a13123dc41d 100644 --- a/lib/galaxy/visualization/tracks/data/array_tree.py +++ b/lib/galaxy/visualization/tracks/data/array_tree.py @@ -7,8 +7,9 @@ pkg_resources.require( "numpy" ) pkg_resources.require( "bx-python" ) from bx.arrays.array_tree import FileArrayTreeDict from math import floor, ceil, log, pow +from base import TracksDataProvider -class ArrayTreeDataProvider( object ): +class ArrayTreeDataProvider( TracksDataProvider ): def __init__( self, dataset, original_dataset ): self.dataset = dataset diff --git a/lib/galaxy/visualization/tracks/data/bam.py b/lib/galaxy/visualization/tracks/data/bam.py index 90440d5988c..9278066a736 100644 --- a/lib/galaxy/visualization/tracks/data/bam.py +++ b/lib/galaxy/visualization/tracks/data/bam.py @@ -9,10 +9,11 @@ from pysam import csamtools from math import floor, ceil, log import logging log = logging.getLogger(__name__) +from base import TracksDataProvider MAX_VALS = 5000 # only display first MAX_VALS datapoints -class BamDataProvider( object ): +class BamDataProvider( TracksDataProvider ): """ Provides access to intervals from a sorted indexed BAM file. """ diff --git a/lib/galaxy/visualization/tracks/data/base.py b/lib/galaxy/visualization/tracks/data/base.py new file mode 100644 index 00000000000..957663f9cc3 --- /dev/null +++ b/lib/galaxy/visualization/tracks/data/base.py @@ -0,0 +1,11 @@ +class TracksDataProvider( object ): + """ Base class for tracks data providers. """ + + def __init__( self, converted_dataset, original_dataset ): + self.converted_dataset = converted_dataset + self.original_dataset = original_dataset + + def get_data( self, chrom, start, end, **kwargs ): + """ Returns data in region defined by chrom, start, and end. """ + # Override. + pass \ No newline at end of file diff --git a/lib/galaxy/visualization/tracks/data/interval_index.py b/lib/galaxy/visualization/tracks/data/interval_index.py index d9deb24e023..106a3515546 100644 --- a/lib/galaxy/visualization/tracks/data/interval_index.py +++ b/lib/galaxy/visualization/tracks/data/interval_index.py @@ -8,15 +8,11 @@ Payload format: [ uid (offset), start, end, name, strand, thick_start, thick_end import pkg_resources; pkg_resources.require( "bx-python" ) from bx.interval_index_file import Indexes from galaxy.datatypes.interval import Bed, Gff -from galaxy.datatypes.tabular import Vcf +from base import TracksDataProvider MAX_VALS = 5000 # only display first MAX_VALS features -class IntervalIndexDataProvider( object ): - def __init__( self, converted_dataset, original_dataset ): - self.original_dataset = original_dataset - self.converted_dataset = converted_dataset - +class IntervalIndexDataProvider( TracksDataProvider ): def get_data( self, chrom, start, end, **kwargs ): start, end = int(start), int(end) source = open( self.original_dataset.file_name ) @@ -66,10 +62,7 @@ class IntervalIndexDataProvider( object ): 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] ) - elif isinstance( self.original_dataset.datatype, Vcf ): - # VCF dataset. - payload.append( feature[2] ) # name - + results.append(payload) return { 'data': results, 'message': message } diff --git a/lib/galaxy/visualization/tracks/data/vcf.py b/lib/galaxy/visualization/tracks/data/vcf.py new file mode 100644 index 00000000000..aa70553f3a6 --- /dev/null +++ b/lib/galaxy/visualization/tracks/data/vcf.py @@ -0,0 +1,53 @@ +""" +VCF data provider for the Galaxy track browser. + +Payload format: +[ uid (offset), start, end, ID, reference base(s), alternate base(s), quality score] +""" + +import pkg_resources; pkg_resources.require( "bx-python" ) +from bx.interval_index_file import Indexes +from galaxy.datatypes.tabular import Vcf +from base import TracksDataProvider + +MAX_VALS = 5000 # only display first MAX_VALS features + +class VCFDataProvider( TracksDataProvider ): + """ Provides data for VCF tracks. """ + + def get_data( self, chrom, start, end, **kwargs ): + """ Returns data in region defined by chrom, start, and end. """ + start, end = int(start), int(end) + source = open( self.original_dataset.file_name ) + index = Indexes( self.converted_dataset.file_name ) + results = [] + count = 0 + message = None + + # If chrom is not found in indexes, try removing the first three + # characters (e.g. 'chr') and see if that works. This enables the + # provider to handle chrome names defined as chrXXX and as XXX. + chrom = str(chrom) + if chrom not in index.indexes and chrom[3:] in index.indexes: + chrom = chrom[3:] + + 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 + break + count += 1 + source.seek(offset) + feature = source.readline().split() + + payload = [ offset, start, end, \ + # ID: + feature[2], \ + # reference base(s): + feature[3], \ + # alternative base(s) + feature[4], \ + # phred quality score + feature[5] ] + results.append(payload) + + return { 'data_type' : 'vcf', 'data': results, 'message': message } diff --git a/lib/galaxy/web/controllers/tracks.py b/lib/galaxy/web/controllers/tracks.py index a1995c781d9..12d7ddb92b0 100644 --- a/lib/galaxy/web/controllers/tracks.py +++ b/lib/galaxy/web/controllers/tracks.py @@ -24,6 +24,7 @@ from galaxy.visualization.tracks.data.array_tree import ArrayTreeDataProvider from galaxy.visualization.tracks.data.interval_index import IntervalIndexDataProvider from galaxy.visualization.tracks.data.bam import BamDataProvider from galaxy.visualization.tracks.data.summary_tree import SummaryTreeDataProvider +from galaxy.visualization.tracks.data.vcf import VCFDataProvider # Message strings returned to browser messages = Bunch( @@ -36,10 +37,11 @@ messages = Bunch( ) # Mapping from dataset type to a class that can fetch data from a file of that -# type. This also needs to be more flexible. +# type. First key is converted dataset type; if result is another dict, second key +# is original dataset type. TODO: This needs to be more flexible. dataset_type_to_data_provider = { "array_tree": ArrayTreeDataProvider, - "interval_index": IntervalIndexDataProvider, + "interval_index": { "vcf": VCFDataProvider, "default" : IntervalIndexDataProvider }, "bai": BamDataProvider, "summary_tree": SummaryTreeDataProvider } @@ -300,15 +302,28 @@ class TracksController( BaseController, UsesVisualization ): frequencies, max_v, avg_v, delta = summary return { 'dataset_type': data_sources['index'], 'data': frequencies, 'max': max_v, 'avg': avg_v, 'delta': delta } - dataset_type = data_sources['data'] - data_provider = dataset_type_to_data_provider[ dataset_type ]( dataset.get_converted_dataset(trans, dataset_type), dataset ) + # Get data provider. + tracks_dataset_type = data_sources['data'] + value = dataset_type_to_data_provider[ tracks_dataset_type ] + if isinstance( value, dict ): + # Get converter by dataset extension; if there is no data provider, + # get the default + data_provider_class = value.get( dataset.ext, value.get( "default" ) ) + else: + data_provider_class = value + data_provider = data_provider_class( dataset.get_converted_dataset(trans, tracks_dataset_type), dataset ) + # Get and return data from data_provider. data = data_provider.get_data( chrom, low, high, **kwargs ) message = None if isinstance(data, dict) and 'message' in data: message = data['message'] - data = data['data'] - return { 'dataset_type': dataset_type, 'extra_info': extra_info, 'data': data, 'message': message } + dataset_type = data.get( 'data_type', tracks_dataset_type ) + track_data = data['data'] + else: + track_data = data + return { 'dataset_type': dataset_type, 'extra_info': extra_info, 'data': track_data, 'message': message } + @web.json def save( self, trans, **kwargs ): diff --git a/static/scripts/packed/trackster.js b/static/scripts/packed/trackster.js index 40979042563..f27c956d1ad 100644 --- a/static/scripts/packed/trackster.js +++ b/static/scripts/packed/trackster.js @@ -1 +1 @@ -var DENSITY=200,FEATURE_LEVELS=10,MAX_FEATURE_DEPTH=100,DATA_ERROR="There was an error in indexing this dataset. ",DATA_NOCONVERTER="A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",DATA_NONE="No data for this chrom/contig.",DATA_PENDING="Currently indexing... please wait",DATA_LOADING="Loading data...",CACHED_TILES_FEATURE=10,CACHED_TILES_LINE=30,CACHED_DATA=5,CONTEXT=$("").get(0).getContext("2d"),PX_PER_CHAR=CONTEXT.measureText("A").width,RIGHT_STRAND,LEFT_STRAND;var right_img=new Image();right_img.src=image_path+"/visualization/strand_right.png";right_img.onload=function(){RIGHT_STRAND=CONTEXT.createPattern(right_img,"repeat")};var left_img=new Image();left_img.src=image_path+"/visualization/strand_left.png";left_img.onload=function(){LEFT_STRAND=CONTEXT.createPattern(left_img,"repeat")};var right_img_inv=new Image();right_img_inv.src=image_path+"/visualization/strand_right_inv.png";right_img_inv.onload=function(){RIGHT_STRAND_INV=CONTEXT.createPattern(right_img_inv,"repeat")};var left_img_inv=new Image();left_img_inv.src=image_path+"/visualization/strand_left_inv.png";left_img_inv.onload=function(){LEFT_STRAND_INV=CONTEXT.createPattern(left_img_inv,"repeat")};function round_1000(a){return Math.round(a*1000)/1000}var Cache=function(a){this.num_elements=a;this.clear()};$.extend(Cache.prototype,{get:function(b){var a=this.key_ary.indexOf(b);if(a!=-1){this.key_ary.splice(a,1);this.key_ary.push(b)}return this.obj_cache[b]},set:function(b,c){if(!this.obj_cache[b]){if(this.key_ary.length>=this.num_elements){var a=this.key_ary.shift();delete this.obj_cache[a]}this.key_ary.push(b)}this.obj_cache[b]=c;return c},clear:function(){this.obj_cache={};this.key_ary=[]}});var View=function(a,c,e,d,b){this.container=a;this.vis_id=d;this.dbkey=b;this.title=e;this.chrom=c;this.tracks=[];this.label_tracks=[];this.max_low=0;this.max_high=0;this.num_tracks=0;this.track_id_counter=0;this.zoom_factor=3;this.min_separation=30;this.has_changes=false;this.init();this.reset()};$.extend(View.prototype,{init:function(){var c=this.container,a=this;this.top_labeltrack=$("
").addClass("top-labeltrack").appendTo(c);this.content_div=$("
").addClass("content").css("position","relative").hide().appendTo(c);this.intro_div=$("
").addClass("intro").text("Select a chrom from the dropdown below").hide().appendTo(c);this.viewport_container=$("
").addClass("viewport-container").addClass("viewport-container").appendTo(this.content_div);this.nav_container=$("
").addClass("nav-container").appendTo(c);this.nav_labeltrack=$("
").addClass("nav-labeltrack").appendTo(this.nav_container);this.nav=$("
").addClass("nav").appendTo(this.nav_container);this.overview=$("
").addClass("overview").appendTo(this.nav);this.overview_viewport=$("
").addClass("overview-viewport").appendTo(this.overview);this.overview_box_background=$("
").addClass("overview-boxback").appendTo(this.overview_viewport);this.overview_box=$("
").addClass("overview-box").appendTo(this.overview_viewport);this.default_overview_height=this.overview_box.height();this.nav_controls=$("
").addClass("nav-controls").appendTo(this.nav);this.chrom_form=$("
").attr("action",function(){void (0)}).appendTo(this.nav_controls);this.chrom_select=$("").addClass("nav-input").hide().bind("keypress focusout",b).appendTo(this.chrom_form);this.location_span=$("").addClass("location").appendTo(this.chrom_form);this.location_span.bind("click",function(){a.location_span.hide();a.chrom_select.hide();a.nav_input.css("display","inline-block");a.nav_input.select();a.nav_input.focus()});if(this.vis_id!==undefined){this.hidden_input=$("").attr("type","hidden").val(this.vis_id).appendTo(this.chrom_form)}this.zo_link=$("").click(function(){a.zoom_out();a.redraw()}).html('').appendTo(this.chrom_form);this.zi_link=$("").click(function(){a.zoom_in();a.redraw()}).html('').appendTo(this.chrom_form);$.ajax({url:chrom_url,data:(this.vis_id!==undefined?{vis_id:this.vis_id}:{dbkey:this.dbkey}),dataType:"json",success:function(d){if(d.reference){a.add_label_track(new ReferenceTrack(a))}a.chrom_data=d.chrom_info;var f='';for(i in a.chrom_data){var e=a.chrom_data[i]["chrom"];f+='"}a.chrom_select.html(f);a.intro_div.show();a.content_div.hide();a.chrom_select.bind("change",function(){a.change_chrom(a.chrom_select.val())})},error:function(){alert("Could not load chroms for this dbkey:",a.dbkey)}});this.content_div.bind("dblclick",function(d){a.zoom_in(d.pageX,this.viewport_container)});this.overview_box.bind("dragstart",function(d){this.current_x=d.offsetX}).bind("drag",function(d){var g=d.offsetX-this.current_x;this.current_x=d.offsetX;var f=Math.round(g/a.viewport_container.width()*(a.max_high-a.max_low));a.move_delta(-f)});this.viewport_container.bind("dragstart",function(d){this.original_low=a.low;this.current_height=d.clientY;this.current_x=d.offsetX;this.enable_pan=(d.clientX").css({height:a.content_div.height()+30,top:"0px",position:"absolute","background-color":"#cfc",border:"1px solid #6a6",opacity:0.5,"z-index":1000}).appendTo($(this))}).bind("drag",function(j){var f=Math.min(j.clientX,this.drag_origin_x)-a.container.offset().left,d=Math.max(j.clientX,this.drag_origin_x)-a.container.offset().left,h=(a.high-a.low),g=a.viewport_container.width();a.update_location(Math.round(f/g*h)+a.low,Math.round(d/g*h)+a.low);this.drag_div.css({left:f+"px",width:(d-f)+"px"})}).bind("dragend",function(k){var f=Math.min(k.clientX,this.drag_origin_x),d=Math.max(k.clientX,this.drag_origin_x),h=(a.high-a.low),g=a.viewport_container.width(),j=a.low;a.low=Math.round(f/g*h)+j;a.high=Math.round(d/g*h)+j;this.drag_div.remove();a.redraw()});this.add_label_track(new LabelTrack(this,this.top_labeltrack));this.add_label_track(new LabelTrack(this,this.nav_labeltrack))},update_location:function(a,b){this.location_span.text(commatize(a)+" - "+commatize(b));this.nav_input.val(this.chrom+":"+commatize(a)+"-"+commatize(b))},change_chrom:function(d,a,f){var c=this;var e=$.grep(c.chrom_data,function(h,j){return h.chrom===d})[0];if(e===undefined){return}if(d!==c.chrom){c.chrom=d;if(c.chrom===""){c.intro_div.show();c.content_div.hide()}else{c.intro_div.hide();c.content_div.show()}c.chrom_select.val(c.chrom);c.max_high=e.len;c.reset();c.redraw(true);for(var g in c.tracks){var b=c.tracks[g];if(b.init){b.init()}}}if(a!==undefined&&f!==undefined){c.low=Math.max(a,0);c.high=Math.min(f,c.max_high)}c.overview_viewport.find("canvas").remove();c.redraw()},go_to:function(f){var k=this,b=f.split(":"),h=b[0],j=b[1];if(j!==undefined){try{var g=j.split("-"),a=parseInt(g[0].replace(/,/g,"")),d=parseInt(g[1].replace(/,/g,""))}catch(c){return false}}k.change_chrom(h,a,d)},move_delta:function(c){var a=this;var b=a.high-a.low;if(a.low-ca.max_high){a.high=a.max_high;a.low=a.max_high-b}else{a.high-=c;a.low-=c}}a.redraw()},add_track:function(a){a.view=this;a.track_id=this.track_id_counter;this.tracks.push(a);if(a.init){a.init()}a.container_div.attr("id","track_"+a.track_id);this.track_id_counter+=1;this.num_tracks+=1},add_label_track:function(a){a.view=this;this.label_tracks.push(a)},remove_track:function(a){this.has_changes=true;a.container_div.fadeOut("slow",function(){$(this).remove()});delete this.tracks[this.tracks.indexOf(a)];this.num_tracks-=1},reset:function(){this.low=this.max_low;this.high=this.max_high;this.viewport_container.find(".yaxislabel").remove()},redraw:function(h){var d=this.high-this.low,b=this.low,f=this.high;if(bthis.max_high){f=this.max_high}if(this.high!==0&&d").addClass("track");if(!this.hidden){this.header_div=$("
").appendTo(this.container_div);this.drag_div=$("
").appendTo(this.header_div);this.name_div=$("