[2] | 1 | /* Trackster |
---|
| 2 | 2010, James Taylor, Kanwei Li |
---|
| 3 | */ |
---|
| 4 | |
---|
| 5 | var DENSITY = 200, |
---|
| 6 | FEATURE_LEVELS = 10, |
---|
| 7 | MAX_FEATURE_DEPTH = 50, |
---|
| 8 | CONNECTOR_COLOR = "#ccc", |
---|
| 9 | DATA_ERROR = "There was an error in indexing this dataset.", |
---|
| 10 | DATA_NOCONVERTER = "A converter for this dataset is not installed. Please check your datatypes_conf.xml file.", |
---|
| 11 | DATA_NONE = "No data for this chrom/contig.", |
---|
| 12 | DATA_PENDING = "Currently indexing... please wait", |
---|
| 13 | DATA_LOADING = "Loading data...", |
---|
| 14 | FILTERABLE_CLASS = "filterable", |
---|
| 15 | CACHED_TILES_FEATURE = 10, |
---|
| 16 | CACHED_TILES_LINE = 30, |
---|
| 17 | CACHED_DATA = 5, |
---|
| 18 | CONTEXT = $("<canvas></canvas>").get(0).getContext("2d"), |
---|
| 19 | PX_PER_CHAR = CONTEXT.measureText("A").width, |
---|
| 20 | RIGHT_STRAND, LEFT_STRAND; |
---|
| 21 | |
---|
| 22 | var right_img = new Image(); |
---|
| 23 | right_img.src = image_path + "/visualization/strand_right.png"; |
---|
| 24 | right_img.onload = function() { |
---|
| 25 | RIGHT_STRAND = CONTEXT.createPattern(right_img, "repeat"); |
---|
| 26 | }; |
---|
| 27 | var left_img = new Image(); |
---|
| 28 | left_img.src = image_path + "/visualization/strand_left.png"; |
---|
| 29 | left_img.onload = function() { |
---|
| 30 | LEFT_STRAND = CONTEXT.createPattern(left_img, "repeat"); |
---|
| 31 | }; |
---|
| 32 | var right_img_inv = new Image(); |
---|
| 33 | right_img_inv.src = image_path + "/visualization/strand_right_inv.png"; |
---|
| 34 | right_img_inv.onload = function() { |
---|
| 35 | RIGHT_STRAND_INV = CONTEXT.createPattern(right_img_inv, "repeat"); |
---|
| 36 | }; |
---|
| 37 | var left_img_inv = new Image(); |
---|
| 38 | left_img_inv.src = image_path + "/visualization/strand_left_inv.png"; |
---|
| 39 | left_img_inv.onload = function() { |
---|
| 40 | LEFT_STRAND_INV = CONTEXT.createPattern(left_img_inv, "repeat"); |
---|
| 41 | }; |
---|
| 42 | |
---|
| 43 | function round_1000(num) { |
---|
| 44 | return Math.round(num * 1000) / 1000; |
---|
| 45 | } |
---|
| 46 | |
---|
| 47 | var Cache = function( num_elements ) { |
---|
| 48 | this.num_elements = num_elements; |
---|
| 49 | this.clear(); |
---|
| 50 | }; |
---|
| 51 | $.extend( Cache.prototype, { |
---|
| 52 | get: function( key ) { |
---|
| 53 | var index = this.key_ary.indexOf(key); |
---|
| 54 | if (index != -1) { |
---|
| 55 | // Move to the end |
---|
| 56 | this.key_ary.splice(index, 1); |
---|
| 57 | this.key_ary.push(key); |
---|
| 58 | } |
---|
| 59 | return this.obj_cache[key]; |
---|
| 60 | }, |
---|
| 61 | set: function( key, value ) { |
---|
| 62 | if (!this.obj_cache[key]) { |
---|
| 63 | if (this.key_ary.length >= this.num_elements) { |
---|
| 64 | // Remove first element |
---|
| 65 | var deleted_key = this.key_ary.shift(); |
---|
| 66 | delete this.obj_cache[deleted_key]; |
---|
| 67 | } |
---|
| 68 | this.key_ary.push(key); |
---|
| 69 | } |
---|
| 70 | this.obj_cache[key] = value; |
---|
| 71 | return value; |
---|
| 72 | }, |
---|
| 73 | clear: function() { |
---|
| 74 | this.obj_cache = {}; |
---|
| 75 | this.key_ary = []; |
---|
| 76 | } |
---|
| 77 | }); |
---|
| 78 | |
---|
| 79 | var View = function( container, title, vis_id, dbkey ) { |
---|
| 80 | this.container = container; |
---|
| 81 | this.vis_id = vis_id; |
---|
| 82 | this.dbkey = dbkey; |
---|
| 83 | this.title = title; |
---|
| 84 | this.tracks = []; |
---|
| 85 | this.label_tracks = []; |
---|
| 86 | this.max_low = 0; |
---|
| 87 | this.max_high = 0; |
---|
| 88 | this.num_tracks = 0; |
---|
| 89 | this.track_id_counter = 0; |
---|
| 90 | this.zoom_factor = 3; |
---|
| 91 | this.min_separation = 30; |
---|
| 92 | this.has_changes = false; |
---|
| 93 | this.init(); |
---|
| 94 | this.reset(); |
---|
| 95 | }; |
---|
| 96 | $.extend( View.prototype, { |
---|
| 97 | init: function() { |
---|
| 98 | // Create DOM elements |
---|
| 99 | var parent_element = this.container, |
---|
| 100 | view = this; |
---|
| 101 | this.top_labeltrack = $("<div/>").addClass("top-labeltrack").appendTo(parent_element); |
---|
| 102 | this.content_div = $("<div/>").addClass("content").css("position", "relative").appendTo(parent_element); |
---|
| 103 | this.viewport_container = $("<div/>").addClass("viewport-container").addClass("viewport-container").appendTo(this.content_div); |
---|
| 104 | this.intro_div = $("<div/>").addClass("intro").text("Select a chrom from the dropdown below").hide(); // Future overlay |
---|
| 105 | |
---|
| 106 | this.nav_container = $("<div/>").addClass("nav-container").appendTo(parent_element); |
---|
| 107 | this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.nav_container); |
---|
| 108 | this.nav = $("<div/>").addClass("nav").appendTo(this.nav_container); |
---|
| 109 | this.overview = $("<div/>").addClass("overview").appendTo(this.nav); |
---|
| 110 | this.overview_viewport = $("<div/>").addClass("overview-viewport").appendTo(this.overview); |
---|
| 111 | this.overview_close = $("<a href='javascript:void(0);'>Close Overview</a>").addClass("overview-close").hide().appendTo(this.overview_viewport); |
---|
| 112 | this.overview_highlight = $("<div />").addClass("overview-highlight").hide().appendTo(this.overview_viewport); |
---|
| 113 | this.overview_box_background = $("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport); |
---|
| 114 | this.overview_box = $("<div/>").addClass("overview-box").appendTo(this.overview_viewport); |
---|
| 115 | this.default_overview_height = this.overview_box.height(); |
---|
| 116 | |
---|
| 117 | this.nav_controls = $("<div/>").addClass("nav-controls").appendTo(this.nav); |
---|
| 118 | this.chrom_form = $("<form/>").attr("action", function() { void(0); } ).appendTo(this.nav_controls); |
---|
| 119 | this.chrom_select = $("<select/>").attr({ "name": "chrom"}).css("width", "15em").addClass("no-autocomplete").append("<option value=''>Loading</option>").appendTo(this.chrom_form); |
---|
| 120 | var submit_nav = function(e) { |
---|
| 121 | if (e.type === "focusout" || (e.keyCode || e.which) === 13 || (e.keyCode || e.which) === 27 ) { |
---|
| 122 | if ((e.keyCode || e.which) !== 27) { // Not escape key |
---|
| 123 | view.go_to( $(this).val() ); |
---|
| 124 | } |
---|
| 125 | $(this).hide(); |
---|
| 126 | view.location_span.show(); |
---|
| 127 | view.chrom_select.show(); |
---|
| 128 | return false; |
---|
| 129 | } |
---|
| 130 | }; |
---|
| 131 | this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keypress focusout", submit_nav).appendTo(this.chrom_form); |
---|
| 132 | this.location_span = $("<span/>").addClass("location").appendTo(this.chrom_form); |
---|
| 133 | this.location_span.bind("click", function() { |
---|
| 134 | view.location_span.hide(); |
---|
| 135 | view.chrom_select.hide(); |
---|
| 136 | view.nav_input.css("display", "inline-block"); |
---|
| 137 | view.nav_input.select(); |
---|
| 138 | view.nav_input.focus(); |
---|
| 139 | }); |
---|
| 140 | if (this.vis_id !== undefined) { |
---|
| 141 | this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.chrom_form); |
---|
| 142 | } |
---|
| 143 | this.zo_link = $("<a/>").click(function() { view.zoom_out(); view.redraw() }).html('<img src="'+image_path+'/fugue/magnifier-zoom-out.png" />').appendTo(this.chrom_form); |
---|
| 144 | this.zi_link = $("<a/>").click(function() { view.zoom_in(); view.redraw() }).html('<img src="'+image_path+'/fugue/magnifier-zoom.png" />').appendTo(this.chrom_form); |
---|
| 145 | |
---|
| 146 | $.ajax({ |
---|
| 147 | url: chrom_url, |
---|
| 148 | data: (this.vis_id !== undefined ? { vis_id: this.vis_id } : { dbkey: this.dbkey }), |
---|
| 149 | dataType: "json", |
---|
| 150 | success: function ( result ) { |
---|
| 151 | if (result['reference']) { |
---|
| 152 | view.add_label_track( new ReferenceTrack(view) ); |
---|
| 153 | } |
---|
| 154 | view.chrom_data = result['chrom_info']; |
---|
| 155 | var chrom_options = '<option value="">Select Chrom/Contig</option>'; |
---|
| 156 | for (i in view.chrom_data) { |
---|
| 157 | var chrom = view.chrom_data[i]['chrom']; |
---|
| 158 | chrom_options += '<option value="' + chrom + '">' + chrom + '</option>'; |
---|
| 159 | } |
---|
| 160 | view.chrom_select.html(chrom_options); |
---|
| 161 | view.intro_div.show(); |
---|
| 162 | view.chrom_select.bind("change", function() { |
---|
| 163 | view.change_chrom(view.chrom_select.val()); |
---|
| 164 | }); |
---|
| 165 | }, |
---|
| 166 | error: function() { |
---|
| 167 | alert( "Could not load chroms for this dbkey:", view.dbkey ); |
---|
| 168 | } |
---|
| 169 | }); |
---|
| 170 | |
---|
| 171 | /* |
---|
| 172 | this.content_div.bind("mousewheel", function( e, delta ) { |
---|
| 173 | if (Math.abs(delta) < 0.5) { |
---|
| 174 | return; |
---|
| 175 | } |
---|
| 176 | if (delta > 0) { |
---|
| 177 | view.zoom_in(e.pageX, this.viewport_container); |
---|
| 178 | } else { |
---|
| 179 | view.zoom_out(); |
---|
| 180 | } |
---|
| 181 | e.preventDefault(); |
---|
| 182 | }); |
---|
| 183 | */ |
---|
| 184 | |
---|
| 185 | this.content_div.bind("dblclick", function( e ) { |
---|
| 186 | view.zoom_in(e.pageX, this.viewport_container); |
---|
| 187 | }); |
---|
| 188 | |
---|
| 189 | // To let the overview box be draggable |
---|
| 190 | this.overview_box.bind("dragstart", function( e ) { |
---|
| 191 | this.current_x = e.offsetX; |
---|
| 192 | }).bind("drag", function( e ) { |
---|
| 193 | var delta = e.offsetX - this.current_x; |
---|
| 194 | this.current_x = e.offsetX; |
---|
| 195 | |
---|
| 196 | var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.max_high - view.max_low) ); |
---|
| 197 | view.move_delta(-delta_chrom); |
---|
| 198 | }); |
---|
| 199 | |
---|
| 200 | this.overview_close.bind("click", function() { |
---|
| 201 | for (var track_id in view.tracks) { |
---|
| 202 | view.tracks[track_id].is_overview = false; |
---|
| 203 | } |
---|
| 204 | $(this).siblings().filter("canvas").remove(); |
---|
| 205 | $(this).parent().css("height", view.overview_box.height()); |
---|
| 206 | view.overview_highlight.hide(); |
---|
| 207 | $(this).hide(); |
---|
| 208 | }); |
---|
| 209 | |
---|
| 210 | this.viewport_container.bind( "dragstart", function( e ) { |
---|
| 211 | this.original_low = view.low; |
---|
| 212 | this.current_height = e.clientY; |
---|
| 213 | this.current_x = e.offsetX; |
---|
| 214 | this.enable_pan = (e.clientX < view.viewport_container.width() - 16) ? true : false; // Fix webkit scrollbar |
---|
| 215 | }).bind( "drag", function( e ) { |
---|
| 216 | if (!this.enable_pan || this.in_reordering) { return; } |
---|
| 217 | var container = $(this); |
---|
| 218 | var delta = e.offsetX - this.current_x; |
---|
| 219 | var new_scroll = container.scrollTop() - (e.clientY - this.current_height); |
---|
| 220 | container.scrollTop(new_scroll); |
---|
| 221 | this.current_height = e.clientY; |
---|
| 222 | this.current_x = e.offsetX; |
---|
| 223 | |
---|
| 224 | var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low)); |
---|
| 225 | view.move_delta(delta_chrom); |
---|
| 226 | }); |
---|
| 227 | |
---|
| 228 | this.top_labeltrack.bind( "dragstart", function(e) { |
---|
| 229 | this.drag_origin_x = e.clientX; |
---|
| 230 | this.drag_origin_pos = e.clientX / view.viewport_container.width() * (view.high - view.low) + view.low; |
---|
| 231 | this.drag_div = $("<div />").css( { |
---|
| 232 | "height": view.content_div.height()+30, "top": "0px", "position": "absolute", |
---|
| 233 | "background-color": "#cfc", "border": "1px solid #6a6", "opacity": 0.5, "z-index": 1000 |
---|
| 234 | } ).appendTo( $(this) ); |
---|
| 235 | }).bind( "drag", function(e) { |
---|
| 236 | var min = Math.min(e.clientX, this.drag_origin_x) - view.container.offset().left, |
---|
| 237 | max = Math.max(e.clientX, this.drag_origin_x) - view.container.offset().left, |
---|
| 238 | span = (view.high - view.low), |
---|
| 239 | width = view.viewport_container.width(); |
---|
| 240 | |
---|
| 241 | view.update_location(Math.round(min / width * span) + view.low, Math.round(max / width * span) + view.low); |
---|
| 242 | this.drag_div.css( { "left": min + "px", "width": (max - min) + "px" } ); |
---|
| 243 | }).bind( "dragend", function(e) { |
---|
| 244 | var min = Math.min(e.clientX, this.drag_origin_x), |
---|
| 245 | max = Math.max(e.clientX, this.drag_origin_x), |
---|
| 246 | span = (view.high - view.low), |
---|
| 247 | width = view.viewport_container.width(), |
---|
| 248 | old_low = view.low; |
---|
| 249 | |
---|
| 250 | view.low = Math.round(min / width * span) + old_low; |
---|
| 251 | view.high = Math.round(max / width * span) + old_low; |
---|
| 252 | this.drag_div.remove(); |
---|
| 253 | view.redraw(); |
---|
| 254 | }); |
---|
| 255 | |
---|
| 256 | this.add_label_track( new LabelTrack( this, this.top_labeltrack ) ); |
---|
| 257 | this.add_label_track( new LabelTrack( this, this.nav_labeltrack ) ); |
---|
| 258 | }, |
---|
| 259 | update_location: function(low, high) { |
---|
| 260 | this.location_span.text( commatize(low) + ' - ' + commatize(high) ); |
---|
| 261 | this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) ); |
---|
| 262 | }, |
---|
| 263 | change_chrom: function(chrom, low, high) { |
---|
| 264 | var view = this; |
---|
| 265 | var found = $.grep(view.chrom_data, function(v, i) { |
---|
| 266 | return v.chrom === chrom; |
---|
| 267 | })[0]; |
---|
| 268 | if (found === undefined) { |
---|
| 269 | // Invalid chrom |
---|
| 270 | return; |
---|
| 271 | } |
---|
| 272 | if (chrom !== view.chrom) { |
---|
| 273 | view.chrom = chrom; |
---|
| 274 | if (!view.chrom) { |
---|
| 275 | // No chrom selected |
---|
| 276 | view.intro_div.show(); |
---|
| 277 | } else { |
---|
| 278 | view.intro_div.hide(); |
---|
| 279 | } |
---|
| 280 | view.chrom_select.val(view.chrom); |
---|
| 281 | view.max_high = found.len; |
---|
| 282 | view.reset(); |
---|
| 283 | view.redraw(true); |
---|
| 284 | |
---|
| 285 | for (var track_id in view.tracks) { |
---|
| 286 | var track = view.tracks[track_id]; |
---|
| 287 | if (track.init) { |
---|
| 288 | track.init(); |
---|
| 289 | } |
---|
| 290 | } |
---|
| 291 | } |
---|
| 292 | if (low !== undefined && high !== undefined) { |
---|
| 293 | view.low = Math.max(low, 0); |
---|
| 294 | view.high = Math.min(high, view.max_high); |
---|
| 295 | } |
---|
| 296 | view.reset_overview(); |
---|
| 297 | view.redraw(); |
---|
| 298 | |
---|
| 299 | }, |
---|
| 300 | go_to: function(str) { |
---|
| 301 | var view = this, |
---|
| 302 | chrom_pos = str.split(":"), |
---|
| 303 | chrom = chrom_pos[0], |
---|
| 304 | pos = chrom_pos[1]; |
---|
| 305 | |
---|
| 306 | if (pos !== undefined) { |
---|
| 307 | try { |
---|
| 308 | var pos_split = pos.split("-"), |
---|
| 309 | new_low = parseInt(pos_split[0].replace(/,/g, "")), |
---|
| 310 | new_high = parseInt(pos_split[1].replace(/,/g, "")); |
---|
| 311 | } catch (e) { |
---|
| 312 | return false; |
---|
| 313 | } |
---|
| 314 | } |
---|
| 315 | view.change_chrom(chrom, new_low, new_high); |
---|
| 316 | }, |
---|
| 317 | move_delta: function(delta_chrom) { |
---|
| 318 | var view = this; |
---|
| 319 | var current_chrom_span = view.high - view.low; |
---|
| 320 | // Check for left and right boundaries |
---|
| 321 | if (view.low - delta_chrom < view.max_low) { |
---|
| 322 | view.low = view.max_low; |
---|
| 323 | view.high = view.max_low + current_chrom_span; |
---|
| 324 | } else if (view.high - delta_chrom > view.max_high) { |
---|
| 325 | view.high = view.max_high; |
---|
| 326 | view.low = view.max_high - current_chrom_span; |
---|
| 327 | } else { |
---|
| 328 | view.high -= delta_chrom; |
---|
| 329 | view.low -= delta_chrom; |
---|
| 330 | } |
---|
| 331 | view.redraw(); |
---|
| 332 | }, |
---|
| 333 | add_track: function(track) { |
---|
| 334 | track.view = this; |
---|
| 335 | track.track_id = this.track_id_counter; |
---|
| 336 | this.tracks.push(track); |
---|
| 337 | if (track.init) { track.init(); } |
---|
| 338 | track.container_div.attr('id', 'track_' + track.track_id); |
---|
| 339 | this.track_id_counter += 1; |
---|
| 340 | this.num_tracks += 1; |
---|
| 341 | }, |
---|
| 342 | add_label_track: function (label_track) { |
---|
| 343 | label_track.view = this; |
---|
| 344 | this.label_tracks.push(label_track); |
---|
| 345 | }, |
---|
| 346 | remove_track: function(track) { |
---|
| 347 | this.has_changes = true; |
---|
| 348 | track.container_div.fadeOut('slow', function() { $(this).remove(); }); |
---|
| 349 | delete this.tracks[this.tracks.indexOf(track)]; |
---|
| 350 | this.num_tracks -= 1; |
---|
| 351 | },/* No longer needed as config is done inline, one track at a time |
---|
| 352 | update_options: function() { |
---|
| 353 | this.has_changes = true; |
---|
| 354 | var sorted = $("ul#sortable-ul").sortable('toArray'); |
---|
| 355 | for (var id_i in sorted) { |
---|
| 356 | var id = sorted[id_i].split("_li")[0].split("track_")[1]; |
---|
| 357 | this.viewport_container.append( $("#track_" + id) ); |
---|
| 358 | } |
---|
| 359 | |
---|
| 360 | for (var track_id in view.tracks) { |
---|
| 361 | var track = view.tracks[track_id]; |
---|
| 362 | if (track && track.update_options) { |
---|
| 363 | track.update_options(track_id); |
---|
| 364 | } |
---|
| 365 | } |
---|
| 366 | },*/ |
---|
| 367 | reset: function() { |
---|
| 368 | this.low = this.max_low; |
---|
| 369 | this.high = this.max_high; |
---|
| 370 | this.viewport_container.find(".yaxislabel").remove(); |
---|
| 371 | }, |
---|
| 372 | redraw: function(nodraw) { |
---|
| 373 | var span = this.high - this.low, |
---|
| 374 | low = this.low, |
---|
| 375 | high = this.high; |
---|
| 376 | |
---|
| 377 | if (low < this.max_low) { |
---|
| 378 | low = this.max_low; |
---|
| 379 | } |
---|
| 380 | if (high > this.max_high) { |
---|
| 381 | high = this.max_high; |
---|
| 382 | } |
---|
| 383 | if (this.high !== 0 && span < this.min_separation) { |
---|
| 384 | high = low + this.min_separation; |
---|
| 385 | } |
---|
| 386 | this.low = Math.floor(low); |
---|
| 387 | this.high = Math.ceil(high); |
---|
| 388 | |
---|
| 389 | // 10^log10(range / DENSITY) Close approximation for browser window, assuming DENSITY = window width |
---|
| 390 | this.resolution = Math.pow( 10, Math.ceil( Math.log( (this.high - this.low) / 200 ) / Math.LN10 ) ); |
---|
| 391 | this.zoom_res = Math.pow( FEATURE_LEVELS, Math.max(0,Math.ceil( Math.log( this.resolution, FEATURE_LEVELS ) / Math.log(FEATURE_LEVELS) ))); |
---|
| 392 | |
---|
| 393 | // Overview |
---|
| 394 | var left_px = this.low / (this.max_high - this.max_low) * this.overview_viewport.width(); |
---|
| 395 | var width_px = (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width(); |
---|
| 396 | var min_width_px = 13; |
---|
| 397 | |
---|
| 398 | this.overview_box.css({ left: left_px, width: Math.max(min_width_px, width_px) }).show(); |
---|
| 399 | if (width_px < min_width_px) { |
---|
| 400 | this.overview_box.css("left", left_px - (min_width_px - width_px)/2); |
---|
| 401 | } |
---|
| 402 | if (this.overview_highlight) { |
---|
| 403 | this.overview_highlight.css({ left: left_px, width: width_px }); |
---|
| 404 | } |
---|
| 405 | |
---|
| 406 | this.update_location(this.low, this.high); |
---|
| 407 | if (!nodraw) { |
---|
| 408 | for (var i = 0, len = this.tracks.length; i < len; i++) { |
---|
| 409 | if (this.tracks[i] && this.tracks[i].enabled) { |
---|
| 410 | this.tracks[i].draw(); |
---|
| 411 | } |
---|
| 412 | } |
---|
| 413 | for (var i = 0, len = this.label_tracks.length; i < len; i++) { |
---|
| 414 | this.label_tracks[i].draw(); |
---|
| 415 | } |
---|
| 416 | } |
---|
| 417 | }, |
---|
| 418 | zoom_in: function (point, container) { |
---|
| 419 | if (this.max_high === 0 || this.high - this.low < this.min_separation) { |
---|
| 420 | return; |
---|
| 421 | } |
---|
| 422 | var span = this.high - this.low, |
---|
| 423 | cur_center = span / 2 + this.low, |
---|
| 424 | new_half = (span / this.zoom_factor) / 2; |
---|
| 425 | if (point) { |
---|
| 426 | cur_center = point / this.viewport_container.width() * (this.high - this.low) + this.low; |
---|
| 427 | } |
---|
| 428 | this.low = Math.round(cur_center - new_half); |
---|
| 429 | this.high = Math.round(cur_center + new_half); |
---|
| 430 | this.redraw(); |
---|
| 431 | }, |
---|
| 432 | zoom_out: function () { |
---|
| 433 | if (this.max_high === 0) { |
---|
| 434 | return; |
---|
| 435 | } |
---|
| 436 | var span = this.high - this.low, |
---|
| 437 | cur_center = span / 2 + this.low, |
---|
| 438 | new_half = (span * this.zoom_factor) / 2; |
---|
| 439 | this.low = Math.round(cur_center - new_half); |
---|
| 440 | this.high = Math.round(cur_center + new_half); |
---|
| 441 | this.redraw(); |
---|
| 442 | }, |
---|
| 443 | reset_overview: function() { |
---|
| 444 | this.overview_viewport.find("canvas").remove(); |
---|
| 445 | this.overview_viewport.height(this.default_overview_height); |
---|
| 446 | this.overview_box.height(this.default_overview_height); |
---|
| 447 | this.overview_close.hide(); |
---|
| 448 | this.overview_highlight.hide(); |
---|
| 449 | } |
---|
| 450 | }); |
---|
| 451 | |
---|
| 452 | // Generic filter. |
---|
| 453 | var Filter = function( name, index, value ) { |
---|
| 454 | this.name = name; |
---|
| 455 | this.index = index; |
---|
| 456 | this.value = value; |
---|
| 457 | }; |
---|
| 458 | |
---|
| 459 | // Number filter for a track. |
---|
| 460 | var NumberFilter = function( name, index ) { |
---|
| 461 | this.name = name; |
---|
| 462 | // Index into payload to filter. |
---|
| 463 | this.index = index; |
---|
| 464 | // Filter low/high. These values are used to filter elements. |
---|
| 465 | this.low = -Number.MAX_VALUE; |
---|
| 466 | this.high = Number.MAX_VALUE; |
---|
| 467 | // Slide min/max. These values are used to set/update slider. |
---|
| 468 | this.slider_min = Number.MAX_VALUE; |
---|
| 469 | this.slider_max = -Number.MAX_VALUE; |
---|
| 470 | // UI Slider element and label that is associated with filter. |
---|
| 471 | this.slider = null; |
---|
| 472 | this.slider_label = null; |
---|
| 473 | }; |
---|
| 474 | $.extend( NumberFilter.prototype, { |
---|
| 475 | // Returns true if filter can be applied to element. |
---|
| 476 | applies_to: function( element ) { |
---|
| 477 | if ( element.length > this.index ) |
---|
| 478 | return true; |
---|
| 479 | return false; |
---|
| 480 | }, |
---|
| 481 | // Returns true iff element is in [low, high]; range is inclusive. |
---|
| 482 | keep: function( element ) { |
---|
| 483 | if ( !this.applies_to( element ) ) |
---|
| 484 | // No element to filter on. |
---|
| 485 | return true; |
---|
| 486 | return ( element[this.index] >= this.low && element[this.index] <= this.high ); |
---|
| 487 | }, |
---|
| 488 | // Update filter's min and max values based on element's values. |
---|
| 489 | update_attrs: function( element ) { |
---|
| 490 | var updated = false; |
---|
| 491 | if ( !this.applies_to( element ) ) { |
---|
| 492 | return updated; |
---|
| 493 | } |
---|
| 494 | |
---|
| 495 | // Update filter's min, max based on element values. |
---|
| 496 | if ( element[this.index] < this.slider_min ) { |
---|
| 497 | this.slider_min = element[this.index]; |
---|
| 498 | updated = true; |
---|
| 499 | } |
---|
| 500 | if ( element[this.index] > this.slider_max ) { |
---|
| 501 | this.slider_max = element[this.index]; |
---|
| 502 | updated = false; |
---|
| 503 | } |
---|
| 504 | return updated; |
---|
| 505 | }, |
---|
| 506 | // Update filter's slider. |
---|
| 507 | update_ui_elt: function () { |
---|
| 508 | var |
---|
| 509 | slider_min = this.slider.slider( "option", "min" ), |
---|
| 510 | slider_max = this.slider.slider( "option", "max" ); |
---|
| 511 | if (this.slider_min < slider_min || this.slider_max > slider_max) { |
---|
| 512 | // Need to update slider. |
---|
| 513 | this.slider.slider( "option", "min", this.slider_min ); |
---|
| 514 | this.slider.slider( "option", "max", this.slider_max ); |
---|
| 515 | // Refresh slider: |
---|
| 516 | // TODO: do we want to keep current values or reset to min/max? |
---|
| 517 | // Currently we reset values: |
---|
| 518 | this.slider.slider( "option", "values", [ this.slider_min, this.slider_max ] ); |
---|
| 519 | // To use the current values. |
---|
| 520 | //var values = this.slider.slider( "option", "values" ); |
---|
| 521 | //this.slider.slider( "option", "values", values ); |
---|
| 522 | } |
---|
| 523 | } |
---|
| 524 | }); |
---|
| 525 | |
---|
| 526 | // Parse filters dict and return filters. |
---|
| 527 | var get_filters = function( filters_dict ) { |
---|
| 528 | var filters = [] |
---|
| 529 | for (var i = 0; i < filters_dict.length; i++) { |
---|
| 530 | var filter_dict = filters_dict[i]; |
---|
| 531 | var name = filter_dict['name'], type = filter_dict['type'], index = filter_dict['index']; |
---|
| 532 | if ( type == 'int' || type == 'float' ) { |
---|
| 533 | filters[i] = new NumberFilter( name, index ); |
---|
| 534 | } |
---|
| 535 | else |
---|
| 536 | filters[i] = new Filter( name, index, type ); |
---|
| 537 | } |
---|
| 538 | return filters; |
---|
| 539 | }; |
---|
| 540 | |
---|
| 541 | var Track = function (name, view, parent_element, filters) { |
---|
| 542 | this.name = name; |
---|
| 543 | this.view = view; |
---|
| 544 | this.parent_element = parent_element; |
---|
| 545 | this.filters = (filters !== undefined ? get_filters( filters ) : []); |
---|
| 546 | this.init_global(); |
---|
| 547 | }; |
---|
| 548 | $.extend( Track.prototype, { |
---|
| 549 | init_global: function () { |
---|
| 550 | this.container_div = $("<div />").addClass('track').css("position", "relative"); |
---|
| 551 | if (!this.hidden) { |
---|
| 552 | this.header_div = $("<div class='track-header' />").appendTo(this.container_div); |
---|
| 553 | if (this.view.editor) { this.drag_div = $("<div class='draghandle' />").appendTo(this.header_div); } |
---|
| 554 | this.name_div = $("<div class='menubutton popup' />").appendTo(this.header_div); |
---|
| 555 | this.name_div.text(this.name); |
---|
| 556 | this.name_div.attr( "id", this.name.replace(/\s+/g,'-').replace(/[^a-zA-Z0-9\-]/g,'').toLowerCase() ); |
---|
| 557 | } |
---|
| 558 | // Create track filtering div. |
---|
| 559 | this.filtering_div = $("<div class='track-filters'>").appendTo(this.container_div); |
---|
| 560 | this.filtering_div.hide(); |
---|
| 561 | // Dragging is disabled on div so that actions on slider do not impact viz. |
---|
| 562 | this.filtering_div.bind( "drag", function(e) { |
---|
| 563 | e.stopPropagation(); |
---|
| 564 | }); |
---|
| 565 | var filters_table = $("<table class='filters'>").appendTo(this.filtering_div); |
---|
| 566 | var track = this; |
---|
| 567 | for (var i = 0; i < this.filters.length; i++) { |
---|
| 568 | var filter = this.filters[i]; |
---|
| 569 | var table_row = $("<tr>").appendTo(filters_table); |
---|
| 570 | var filter_th = $("<th class='filter-info'>").appendTo(table_row); |
---|
| 571 | var name_span = $("<span class='name'>").appendTo(filter_th); |
---|
| 572 | name_span.text(filter.name + " "); // Extra spacing to separate name and values |
---|
| 573 | var values_span = $("<span class='values'>").appendTo(filter_th); |
---|
| 574 | // TODO: generate custom interaction elements based on filter type. |
---|
| 575 | var table_data = $("<td>").appendTo(table_row); |
---|
| 576 | filter.control_element = $("<div id='" + filter.name + "-filter-control' style='width: 200px; position: relative'>").appendTo(table_data); |
---|
| 577 | filter.control_element.slider({ |
---|
| 578 | range: true, |
---|
| 579 | min: Number.MAX_VALUE, |
---|
| 580 | max: -Number.MIN_VALUE, |
---|
| 581 | values: [0, 0], |
---|
| 582 | slide: function( event, ui ) { |
---|
| 583 | var values = ui.values; |
---|
| 584 | // Set new values in UI. |
---|
| 585 | values_span.text( "[" + values[0] + "-" + values[1] + "]" ); |
---|
| 586 | // Set new values in filter. |
---|
| 587 | filter.low = values[0]; |
---|
| 588 | filter.high = values[1]; |
---|
| 589 | // Redraw track. |
---|
| 590 | track.draw( true ); |
---|
| 591 | }, |
---|
| 592 | change: function( event, ui ) { |
---|
| 593 | filter.control_element.slider( "option", "slide" ).call( filter.control_element, event, ui ); |
---|
| 594 | } |
---|
| 595 | }); |
---|
| 596 | filter.slider = filter.control_element; |
---|
| 597 | filter.slider_label = values_span; |
---|
| 598 | } |
---|
| 599 | |
---|
| 600 | this.content_div = $("<div class='track-content'>").appendTo(this.container_div); |
---|
| 601 | this.parent_element.append(this.container_div); |
---|
| 602 | }, |
---|
| 603 | init_each: function(params, success_fn) { |
---|
| 604 | var track = this; |
---|
| 605 | track.enabled = false; |
---|
| 606 | track.data_queue = {}; |
---|
| 607 | track.tile_cache.clear(); |
---|
| 608 | track.data_cache.clear(); |
---|
| 609 | track.initial_canvas = undefined; |
---|
| 610 | track.content_div.css("height", "auto"); |
---|
| 611 | if (!track.content_div.text()) { |
---|
| 612 | track.content_div.text(DATA_LOADING); |
---|
| 613 | } |
---|
| 614 | track.container_div.removeClass("nodata error pending"); |
---|
| 615 | |
---|
| 616 | if (track.view.chrom) { |
---|
| 617 | $.getJSON( data_url, params, function (result) { |
---|
| 618 | if (!result || result === "error" || result.kind === "error") { |
---|
| 619 | track.container_div.addClass("error"); |
---|
| 620 | track.content_div.text(DATA_ERROR); |
---|
| 621 | if (result.message) { |
---|
| 622 | var track_id = track.view.tracks.indexOf(track); |
---|
| 623 | var error_link = $("<a href='javascript:void(0);'></a>").attr("id", track_id + "_error"); |
---|
| 624 | error_link.text("Click to view error"); |
---|
| 625 | $("#" + track_id + "_error").live("click", function() { |
---|
| 626 | show_modal( "Trackster Error", "<pre>" + result.message + "</pre>", { "Close" : hide_modal } ); |
---|
| 627 | }); |
---|
| 628 | track.content_div.append(error_link); |
---|
| 629 | } |
---|
| 630 | } else if (result === "no converter") { |
---|
| 631 | track.container_div.addClass("error"); |
---|
| 632 | track.content_div.text(DATA_NOCONVERTER); |
---|
| 633 | } else if (result.data !== undefined && (result.data === null || result.data.length === 0)) { |
---|
| 634 | track.container_div.addClass("nodata"); |
---|
| 635 | track.content_div.text(DATA_NONE); |
---|
| 636 | } else if (result === "pending") { |
---|
| 637 | track.container_div.addClass("pending"); |
---|
| 638 | track.content_div.text(DATA_PENDING); |
---|
| 639 | setTimeout(function() { track.init(); }, 5000); |
---|
| 640 | } else { |
---|
| 641 | track.content_div.text(""); |
---|
| 642 | track.content_div.css( "height", track.height_px + "px" ); |
---|
| 643 | track.enabled = true; |
---|
| 644 | success_fn(result); |
---|
| 645 | track.draw(); |
---|
| 646 | } |
---|
| 647 | }); |
---|
| 648 | } else { |
---|
| 649 | track.container_div.addClass("nodata"); |
---|
| 650 | track.content_div.text(DATA_NONE); |
---|
| 651 | } |
---|
| 652 | } |
---|
| 653 | }); |
---|
| 654 | |
---|
| 655 | var TiledTrack = function() { |
---|
| 656 | var track = this, |
---|
| 657 | view = track.view; |
---|
| 658 | |
---|
| 659 | if (track.hidden) { return; } |
---|
| 660 | |
---|
| 661 | if (track.display_modes !== undefined) { |
---|
| 662 | if (track.mode_div === undefined) { |
---|
| 663 | track.mode_div = $("<div class='right-float menubutton popup' />").appendTo(track.header_div); |
---|
| 664 | var init_mode = track.display_modes[0]; |
---|
| 665 | track.mode = init_mode; |
---|
| 666 | track.mode_div.text(init_mode); |
---|
| 667 | |
---|
| 668 | var change_mode = function(name) { |
---|
| 669 | track.mode_div.text(name); |
---|
| 670 | track.mode = name; |
---|
| 671 | track.tile_cache.clear(); |
---|
| 672 | track.draw(); |
---|
| 673 | }; |
---|
| 674 | var mode_mapping = {}; |
---|
| 675 | for (var i in track.display_modes) { |
---|
| 676 | var mode = track.display_modes[i]; |
---|
| 677 | mode_mapping[mode] = function(mode) { |
---|
| 678 | return function() { change_mode(mode); } |
---|
| 679 | }(mode); |
---|
| 680 | } |
---|
| 681 | make_popupmenu(track.mode_div, mode_mapping); |
---|
| 682 | } else { |
---|
| 683 | track.mode_div.hide(); |
---|
| 684 | } |
---|
| 685 | } |
---|
| 686 | var track_dropdown = {}; |
---|
| 687 | track_dropdown["Set as overview"] = function() { |
---|
| 688 | view.overview_viewport.find("canvas").remove(); |
---|
| 689 | track.is_overview = true; |
---|
| 690 | track.set_overview(); |
---|
| 691 | for (var track_id in view.tracks) { |
---|
| 692 | if (view.tracks[track_id] !== track) { |
---|
| 693 | view.tracks[track_id].is_overview = false; |
---|
| 694 | } |
---|
| 695 | } |
---|
| 696 | }; |
---|
| 697 | track_dropdown["Edit configuration"] = function() { |
---|
| 698 | var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); }, |
---|
| 699 | ok_fn = function() { track.update_options(track.track_id); hide_modal(); $(window).unbind("keypress.check_enter_esc"); }, |
---|
| 700 | check_enter_esc = function(e) { |
---|
| 701 | if ((e.keyCode || e.which) === 27) { // Escape key |
---|
| 702 | cancel_fn(); |
---|
| 703 | } else if ((e.keyCode || e.which) === 13) { // Enter key |
---|
| 704 | ok_fn(); |
---|
| 705 | } |
---|
| 706 | }; |
---|
| 707 | |
---|
| 708 | $(window).bind("keypress.check_enter_esc", check_enter_esc); |
---|
| 709 | show_modal("Configure Track", track.gen_options(track.track_id), { |
---|
| 710 | "Cancel": cancel_fn, |
---|
| 711 | "OK": ok_fn |
---|
| 712 | }); |
---|
| 713 | }; |
---|
| 714 | if (track.filters.length > 0) { |
---|
| 715 | track_dropdown["Show filters"] = function() { |
---|
| 716 | // Set option text and toggle filtering div. |
---|
| 717 | var menu_option_text; |
---|
| 718 | if (!track.filtering_div.is(":visible")) { |
---|
| 719 | menu_option_text = "Hide filters"; |
---|
| 720 | track.filters_visible = true; |
---|
| 721 | } |
---|
| 722 | else { |
---|
| 723 | menu_option_text = "Show filters"; |
---|
| 724 | track.filters_visible = false; |
---|
| 725 | } |
---|
| 726 | $("#" + track.name_div.attr("id") + "-menu").find("li").eq(2).text(menu_option_text); |
---|
| 727 | track.filtering_div.toggle(); |
---|
| 728 | }; |
---|
| 729 | } |
---|
| 730 | track_dropdown["Remove"] = function() { |
---|
| 731 | view.remove_track(track); |
---|
| 732 | if (view.num_tracks === 0) { |
---|
| 733 | $("#no-tracks").show(); |
---|
| 734 | } |
---|
| 735 | }; |
---|
| 736 | track.popup_menu = make_popupmenu(track.name_div, track_dropdown); |
---|
| 737 | show_hide_popupmenu_options(track.popup_menu, "(Show|Hide) filters", false); |
---|
| 738 | /* |
---|
| 739 | if (track.overview_check_div === undefined) { |
---|
| 740 | track.overview_check_div = $("<div class='right-float' />").css("margin-top", "-3px").appendTo(track.header_div); |
---|
| 741 | track.overview_check = $("<input type='checkbox' class='overview_check' />").appendTo(track.overview_check_div); |
---|
| 742 | track.overview_check.bind("click", function() { |
---|
| 743 | var curr = this; |
---|
| 744 | view.overview_viewport.find("canvas").remove(); |
---|
| 745 | track.set_overview(); |
---|
| 746 | $(".overview_check").each(function() { |
---|
| 747 | if (this !== curr) { |
---|
| 748 | $(this).attr("checked", false); |
---|
| 749 | } |
---|
| 750 | }); |
---|
| 751 | }); |
---|
| 752 | track.overview_check_div.append( $("<label />").text("Overview") ); |
---|
| 753 | } |
---|
| 754 | */ |
---|
| 755 | }; |
---|
| 756 | $.extend( TiledTrack.prototype, Track.prototype, { |
---|
| 757 | draw: function( force ) { |
---|
| 758 | var low = this.view.low, |
---|
| 759 | high = this.view.high, |
---|
| 760 | range = high - low, |
---|
| 761 | resolution = this.view.resolution; |
---|
| 762 | |
---|
| 763 | var parent_element = $("<div style='position: relative;'></div>"), |
---|
| 764 | w_scale = this.content_div.width() / range, |
---|
| 765 | tile_element; |
---|
| 766 | |
---|
| 767 | this.content_div.append( parent_element ), |
---|
| 768 | this.max_height = 0; |
---|
| 769 | // Index of first tile that overlaps visible region |
---|
| 770 | var tile_index = Math.floor( low / resolution / DENSITY ); |
---|
| 771 | // A list of setTimeout() ids used when drawing tiles. |
---|
| 772 | var draw_tile_ids = new Object(); |
---|
| 773 | while ( ( tile_index * DENSITY * resolution ) < high ) { |
---|
| 774 | // Check in cache |
---|
| 775 | var key = this.content_div.width() + '_' + w_scale + '_' + tile_index; |
---|
| 776 | var cached = this.tile_cache.get(key); |
---|
| 777 | if ( !force && cached ) { |
---|
| 778 | var tile_low = tile_index * DENSITY * resolution; |
---|
| 779 | var left = ( tile_low - low ) * w_scale; |
---|
| 780 | if (this.left_offset) { |
---|
| 781 | left -= this.left_offset; |
---|
| 782 | } |
---|
| 783 | cached.css({ left: left }); |
---|
| 784 | this.show_tile( cached, parent_element ); |
---|
| 785 | } else { |
---|
| 786 | this.delayed_draw(this, key, low, high, tile_index, resolution, parent_element, w_scale, draw_tile_ids); |
---|
| 787 | } |
---|
| 788 | tile_index += 1; |
---|
| 789 | } |
---|
| 790 | |
---|
| 791 | // |
---|
| 792 | // Actions to take after new tiles have been loaded/drawn: |
---|
| 793 | // (1) remove old tile(s); |
---|
| 794 | // (2) update filtering UI elements. |
---|
| 795 | // |
---|
| 796 | var track = this; |
---|
| 797 | var intervalId = setInterval(function() { |
---|
| 798 | if ( draw_tile_ids.length != 0 ) { |
---|
| 799 | // Add drawing has finished; if there is more than one child in the content div, |
---|
| 800 | // remove the first one, which is the oldest. |
---|
| 801 | if ( track.content_div.children().length > 1 ) { |
---|
| 802 | track.content_div.children( ":first" ).remove(); |
---|
| 803 | } |
---|
| 804 | |
---|
| 805 | // Update filtering UI. |
---|
| 806 | for (var f = 0; f < track.filters.length; f++) { |
---|
| 807 | track.filters[f].update_ui_elt(); |
---|
| 808 | } |
---|
| 809 | // Method complete; do not call it again. |
---|
| 810 | clearInterval(intervalId); |
---|
| 811 | } |
---|
| 812 | }, 50); |
---|
| 813 | }, delayed_draw: function(track, key, low, high, tile_index, resolution, parent_element, w_scale, draw_tile_ids) { |
---|
| 814 | // Put a 50ms delay on drawing so that if the user scrolls fast, we don't load extra data |
---|
| 815 | var id = setTimeout(function() { |
---|
| 816 | if ( !(low > track.view.high || high < track.view.low) ) { |
---|
| 817 | tile_element = track.draw_tile( resolution, tile_index, parent_element, w_scale ); |
---|
| 818 | if (tile_element) { |
---|
| 819 | // Store initial canvas in we need to use it for overview |
---|
| 820 | if (!track.initial_canvas) { |
---|
| 821 | track.initial_canvas = $(tile_element).clone(); |
---|
| 822 | var src_ctx = tile_element.get(0).getContext("2d"); |
---|
| 823 | var tgt_ctx = track.initial_canvas.get(0).getContext("2d"); |
---|
| 824 | var data = src_ctx.getImageData(0, 0, src_ctx.canvas.width, src_ctx.canvas.height); |
---|
| 825 | tgt_ctx.putImageData(data, 0, 0); |
---|
| 826 | track.set_overview(); |
---|
| 827 | } |
---|
| 828 | // Add tile to cache and show tile. |
---|
| 829 | track.tile_cache.set(key, tile_element); |
---|
| 830 | track.show_tile( tile_element, parent_element ) |
---|
| 831 | } |
---|
| 832 | } |
---|
| 833 | // Remove setTimeout id. |
---|
| 834 | delete draw_tile_ids.id; |
---|
| 835 | }, 50); |
---|
| 836 | draw_tile_ids.id = true; |
---|
| 837 | }, |
---|
| 838 | // Show track tile and perform associated actions. |
---|
| 839 | show_tile: function( tile_element, parent_element ) { |
---|
| 840 | // Readability. |
---|
| 841 | var track = this; |
---|
| 842 | |
---|
| 843 | // Setup and show tile element. |
---|
| 844 | parent_element.append( tile_element ); |
---|
| 845 | track.max_height = Math.max( track.max_height, tile_element.height() ); |
---|
| 846 | track.content_div.css("height", track.max_height + "px"); |
---|
| 847 | |
---|
| 848 | // Show/hide filters based on whether tile is filterable. |
---|
| 849 | if ( tile_element.hasClass(FILTERABLE_CLASS) ) { |
---|
| 850 | show_hide_popupmenu_options(track.popup_menu, "(Show|Hide) filters"); |
---|
| 851 | if (track.filters_visible) |
---|
| 852 | track.filtering_div.show(); |
---|
| 853 | } |
---|
| 854 | else { |
---|
| 855 | show_hide_popupmenu_options(track.popup_menu, "(Show|Hide) filters", false); |
---|
| 856 | track.filtering_div.hide(); |
---|
| 857 | } |
---|
| 858 | }, set_overview: function() { |
---|
| 859 | var view = this.view; |
---|
| 860 | |
---|
| 861 | if (this.initial_canvas && this.is_overview) { |
---|
| 862 | view.overview_close.show(); |
---|
| 863 | view.overview_viewport.append(this.initial_canvas); |
---|
| 864 | view.overview_highlight.show().height(this.initial_canvas.height()); |
---|
| 865 | view.overview_viewport.height(this.initial_canvas.height() + view.overview_box.height()); |
---|
| 866 | } |
---|
| 867 | $(window).trigger("resize"); |
---|
| 868 | } |
---|
| 869 | }); |
---|
| 870 | |
---|
| 871 | var LabelTrack = function (view, parent_element) { |
---|
| 872 | this.track_type = "LabelTrack"; |
---|
| 873 | this.hidden = true; |
---|
| 874 | Track.call( this, null, view, parent_element ); |
---|
| 875 | this.container_div.addClass( "label-track" ); |
---|
| 876 | }; |
---|
| 877 | $.extend( LabelTrack.prototype, Track.prototype, { |
---|
| 878 | draw: function() { |
---|
| 879 | var view = this.view, |
---|
| 880 | range = view.high - view.low, |
---|
| 881 | tickDistance = Math.floor( Math.pow( 10, Math.floor( Math.log( range ) / Math.log( 10 ) ) ) ), |
---|
| 882 | position = Math.floor( view.low / tickDistance ) * tickDistance, |
---|
| 883 | width = this.content_div.width(), |
---|
| 884 | new_div = $("<div style='position: relative; height: 1.3em;'></div>"); |
---|
| 885 | while ( position < view.high ) { |
---|
| 886 | var screenPosition = ( position - view.low ) / range * width; |
---|
| 887 | new_div.append( $("<div class='label'>" + commatize( position ) + "</div>").css( { |
---|
| 888 | position: "absolute", |
---|
| 889 | // Reduce by one to account for border |
---|
| 890 | left: screenPosition - 1 |
---|
| 891 | })); |
---|
| 892 | position += tickDistance; |
---|
| 893 | } |
---|
| 894 | this.content_div.children( ":first" ).remove(); |
---|
| 895 | this.content_div.append( new_div ); |
---|
| 896 | } |
---|
| 897 | }); |
---|
| 898 | |
---|
| 899 | var ReferenceTrack = function (view) { |
---|
| 900 | this.track_type = "ReferenceTrack"; |
---|
| 901 | this.hidden = true; |
---|
| 902 | Track.call( this, null, view, view.top_labeltrack ); |
---|
| 903 | TiledTrack.call( this ); |
---|
| 904 | |
---|
| 905 | this.left_offset = 200; |
---|
| 906 | this.height_px = 12; |
---|
| 907 | this.container_div.addClass( "reference-track" ); |
---|
| 908 | this.dummy_canvas = $("<canvas></canvas>").get(0).getContext("2d"); |
---|
| 909 | this.data_queue = {}; |
---|
| 910 | this.data_cache = new Cache(CACHED_DATA); |
---|
| 911 | this.tile_cache = new Cache(CACHED_TILES_LINE); |
---|
| 912 | }; |
---|
| 913 | $.extend( ReferenceTrack.prototype, TiledTrack.prototype, { |
---|
| 914 | get_data: function(resolution, position) { |
---|
| 915 | var track = this, |
---|
| 916 | low = position * DENSITY * resolution, |
---|
| 917 | high = ( position + 1 ) * DENSITY * resolution, |
---|
| 918 | key = resolution + "_" + position; |
---|
| 919 | |
---|
| 920 | if (!track.data_queue[key]) { |
---|
| 921 | track.data_queue[key] = true; |
---|
| 922 | $.ajax({ 'url': reference_url, 'dataType': 'json', 'data': { "chrom": this.view.chrom, |
---|
| 923 | "low": low, "high": high, "dbkey": this.view.dbkey }, |
---|
| 924 | success: function (seq) { |
---|
| 925 | track.data_cache.set(key, seq); |
---|
| 926 | delete track.data_queue[key]; |
---|
| 927 | track.draw(); |
---|
| 928 | }, error: function(r, t, e) { |
---|
| 929 | console.log(r, t, e); |
---|
| 930 | } |
---|
| 931 | }); |
---|
| 932 | } |
---|
| 933 | }, |
---|
| 934 | draw_tile: function( resolution, tile_index, parent_element, w_scale ) { |
---|
| 935 | var tile_low = tile_index * DENSITY * resolution, |
---|
| 936 | tile_length = DENSITY * resolution, |
---|
| 937 | canvas = $("<canvas class='tile'></canvas>"), |
---|
| 938 | ctx = canvas.get(0).getContext("2d"), |
---|
| 939 | key = resolution + "_" + tile_index; |
---|
| 940 | |
---|
| 941 | if (w_scale > PX_PER_CHAR) { |
---|
| 942 | if (this.data_cache.get(key) === undefined) { |
---|
| 943 | this.get_data( resolution, tile_index ); |
---|
| 944 | return; |
---|
| 945 | } |
---|
| 946 | |
---|
| 947 | var seq = this.data_cache.get(key); |
---|
| 948 | if (seq === null) { |
---|
| 949 | this.content_div.css("height", "0px"); |
---|
| 950 | return; |
---|
| 951 | } |
---|
| 952 | |
---|
| 953 | canvas.get(0).width = Math.ceil( tile_length * w_scale + this.left_offset); |
---|
| 954 | canvas.get(0).height = this.height_px; |
---|
| 955 | |
---|
| 956 | canvas.css( { |
---|
| 957 | position: "absolute", |
---|
| 958 | top: 0, |
---|
| 959 | left: ( tile_low - this.view.low ) * w_scale - this.left_offset |
---|
| 960 | }); |
---|
| 961 | |
---|
| 962 | for (var c = 0, str_len = seq.length; c < str_len; c++) { |
---|
| 963 | var c_start = Math.round(c * w_scale), |
---|
| 964 | gap = Math.round(w_scale / 2); |
---|
| 965 | ctx.fillText(seq[c], c_start + this.left_offset + gap, 10); |
---|
| 966 | } |
---|
| 967 | parent_element.append(canvas); |
---|
| 968 | return canvas; |
---|
| 969 | } |
---|
| 970 | this.content_div.css("height", "0px"); |
---|
| 971 | } |
---|
| 972 | }); |
---|
| 973 | |
---|
| 974 | var LineTrack = function ( name, view, dataset_id, prefs ) { |
---|
| 975 | this.track_type = "LineTrack"; |
---|
| 976 | this.display_modes = ["Histogram", "Line", "Filled", "Intensity"]; |
---|
| 977 | this.mode = "Histogram"; |
---|
| 978 | Track.call( this, name, view, view.viewport_container ); |
---|
| 979 | TiledTrack.call( this ); |
---|
| 980 | |
---|
| 981 | this.height_px = 80; |
---|
| 982 | this.dataset_id = dataset_id; |
---|
| 983 | this.data_cache = new Cache(CACHED_DATA); |
---|
| 984 | this.tile_cache = new Cache(CACHED_TILES_LINE); |
---|
| 985 | this.prefs = { 'color': 'black', 'min_value': undefined, 'max_value': undefined, 'mode': this.mode }; |
---|
| 986 | if (prefs.min_value !== undefined) { this.prefs.min_value = prefs.min_value; } |
---|
| 987 | if (prefs.max_value !== undefined) { this.prefs.max_value = prefs.max_value; } |
---|
| 988 | }; |
---|
| 989 | $.extend( LineTrack.prototype, TiledTrack.prototype, { |
---|
| 990 | init: function() { |
---|
| 991 | var track = this, |
---|
| 992 | track_id = track.view.tracks.indexOf(track); |
---|
| 993 | |
---|
| 994 | track.vertical_range = undefined; |
---|
| 995 | this.init_each({ stats: true, chrom: track.view.chrom, low: null, high: null, |
---|
| 996 | dataset_id: track.dataset_id }, function(result) { |
---|
| 997 | |
---|
| 998 | track.container_div.addClass( "line-track" ); |
---|
| 999 | data = result.data; |
---|
| 1000 | if ( isNaN(parseFloat(track.prefs.min_value)) || isNaN(parseFloat(track.prefs.max_value)) ) { |
---|
| 1001 | track.prefs.min_value = data.min; |
---|
| 1002 | track.prefs.max_value = data.max; |
---|
| 1003 | // Update the config |
---|
| 1004 | $('#track_' + track_id + '_minval').val(track.prefs.min_value); |
---|
| 1005 | $('#track_' + track_id + '_maxval').val(track.prefs.max_value); |
---|
| 1006 | } |
---|
| 1007 | track.vertical_range = track.prefs.max_value - track.prefs.min_value; |
---|
| 1008 | track.total_frequency = data.total_frequency; |
---|
| 1009 | |
---|
| 1010 | // Draw y-axis labels if necessary |
---|
| 1011 | track.container_div.find(".yaxislabel").remove(); |
---|
| 1012 | |
---|
| 1013 | var min_label = $("<div />").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_minval').text(round_1000(track.prefs.min_value)); |
---|
| 1014 | var max_label = $("<div />").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_maxval').text(round_1000(track.prefs.max_value)); |
---|
| 1015 | |
---|
| 1016 | max_label.css({ position: "absolute", top: "22px", left: "10px" }); |
---|
| 1017 | max_label.prependTo(track.container_div); |
---|
| 1018 | |
---|
| 1019 | min_label.css({ position: "absolute", top: track.height_px + 11 + "px", left: "10px" }); |
---|
| 1020 | min_label.prependTo(track.container_div); |
---|
| 1021 | }); |
---|
| 1022 | }, |
---|
| 1023 | get_data: function( resolution, position ) { |
---|
| 1024 | var track = this, |
---|
| 1025 | low = position * DENSITY * resolution, |
---|
| 1026 | high = ( position + 1 ) * DENSITY * resolution, |
---|
| 1027 | key = resolution + "_" + position; |
---|
| 1028 | |
---|
| 1029 | if (!track.data_queue[key]) { |
---|
| 1030 | track.data_queue[key] = true; |
---|
| 1031 | /*$.getJSON( data_url, { "chrom": this.view.chrom, |
---|
| 1032 | "low": low, "high": high, "dataset_id": this.dataset_id, |
---|
| 1033 | "resolution": this.view.resolution }, function (data) { |
---|
| 1034 | track.data_cache.set(key, data); |
---|
| 1035 | delete track.data_queue[key]; |
---|
| 1036 | track.draw(); |
---|
| 1037 | });*/ |
---|
| 1038 | $.ajax({ 'url': data_url, 'dataType': 'json', 'data': { "chrom": this.view.chrom, |
---|
| 1039 | "low": low, "high": high, "dataset_id": this.dataset_id, |
---|
| 1040 | "resolution": this.view.resolution }, |
---|
| 1041 | success: function (result) { |
---|
| 1042 | data = result.data; |
---|
| 1043 | track.data_cache.set(key, data); |
---|
| 1044 | delete track.data_queue[key]; |
---|
| 1045 | track.draw(); |
---|
| 1046 | }, error: function(r, t, e) { |
---|
| 1047 | console.log(r, t, e); |
---|
| 1048 | } |
---|
| 1049 | }); |
---|
| 1050 | } |
---|
| 1051 | }, |
---|
| 1052 | draw_tile: function( resolution, tile_index, parent_element, w_scale ) { |
---|
| 1053 | if (this.vertical_range === undefined) { |
---|
| 1054 | return; |
---|
| 1055 | } |
---|
| 1056 | |
---|
| 1057 | var tile_low = tile_index * DENSITY * resolution, |
---|
| 1058 | tile_length = DENSITY * resolution, |
---|
| 1059 | canvas = $("<canvas class='tile'></canvas>"), |
---|
| 1060 | key = resolution + "_" + tile_index; |
---|
| 1061 | |
---|
| 1062 | if (this.data_cache.get(key) === undefined) { |
---|
| 1063 | this.get_data( resolution, tile_index ); |
---|
| 1064 | return; |
---|
| 1065 | } |
---|
| 1066 | |
---|
| 1067 | var result = this.data_cache.get(key); |
---|
| 1068 | if (result === null) { return; } |
---|
| 1069 | |
---|
| 1070 | canvas.css( { |
---|
| 1071 | position: "absolute", |
---|
| 1072 | top: 0, |
---|
| 1073 | left: ( tile_low - this.view.low ) * w_scale |
---|
| 1074 | }); |
---|
| 1075 | |
---|
| 1076 | canvas.get(0).width = Math.ceil( tile_length * w_scale ); |
---|
| 1077 | canvas.get(0).height = this.height_px; |
---|
| 1078 | var ctx = canvas.get(0).getContext("2d"), |
---|
| 1079 | in_path = false, |
---|
| 1080 | min_value = this.prefs.min_value, |
---|
| 1081 | max_value = this.prefs.max_value, |
---|
| 1082 | vertical_range = this.vertical_range, |
---|
| 1083 | total_frequency = this.total_frequency, |
---|
| 1084 | height_px = this.height_px, |
---|
| 1085 | mode = this.mode; |
---|
| 1086 | |
---|
| 1087 | ctx.beginPath(); |
---|
| 1088 | ctx.fillStyle = this.prefs.color; |
---|
| 1089 | // for intensity, calculate delta x in pixels to for width of box |
---|
| 1090 | if (data.length > 1) { |
---|
| 1091 | var delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); |
---|
| 1092 | } else { |
---|
| 1093 | var delta_x_px = 10; |
---|
| 1094 | } |
---|
| 1095 | |
---|
| 1096 | var x_scaled, y; |
---|
| 1097 | |
---|
| 1098 | for (var i = 0, len = data.length; i < len; i++) { |
---|
| 1099 | x_scaled = Math.round((data[i][0] - tile_low) * w_scale); |
---|
| 1100 | y = data[i][1]; |
---|
| 1101 | if (y === null) { |
---|
| 1102 | if (in_path && mode === "Filled") { |
---|
| 1103 | ctx.lineTo(x_scaled, height_px); |
---|
| 1104 | } |
---|
| 1105 | in_path = false; |
---|
| 1106 | continue; |
---|
| 1107 | } |
---|
| 1108 | if (y < min_value) { |
---|
| 1109 | y = min_value; |
---|
| 1110 | } else if (y > max_value) { |
---|
| 1111 | y = max_value; |
---|
| 1112 | } |
---|
| 1113 | |
---|
| 1114 | if (mode === "Histogram") { |
---|
| 1115 | y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); |
---|
| 1116 | ctx.fillRect(x_scaled, y, delta_x_px, height_px - y); |
---|
| 1117 | } else if (mode === "Intensity" ) { |
---|
| 1118 | y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); |
---|
| 1119 | ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; |
---|
| 1120 | ctx.fillRect(x_scaled, 0, delta_x_px, height_px); |
---|
| 1121 | } else { |
---|
| 1122 | // console.log(y, this.min_value, this.vertical_range, (y - this.min_value) / this.vertical_range * this.height_px); |
---|
| 1123 | y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); |
---|
| 1124 | // console.log(canvas.get(0).height, canvas.get(0).width); |
---|
| 1125 | if (in_path) { |
---|
| 1126 | ctx.lineTo(x_scaled, y); |
---|
| 1127 | } else { |
---|
| 1128 | in_path = true; |
---|
| 1129 | if (mode === "Filled") { |
---|
| 1130 | ctx.moveTo(x_scaled, height_px); |
---|
| 1131 | ctx.lineTo(x_scaled, y); |
---|
| 1132 | } else { |
---|
| 1133 | ctx.moveTo(x_scaled, y); |
---|
| 1134 | } |
---|
| 1135 | } |
---|
| 1136 | } |
---|
| 1137 | } |
---|
| 1138 | if (mode === "Filled") { |
---|
| 1139 | if (in_path) { |
---|
| 1140 | ctx.lineTo(x_scaled, height_px); |
---|
| 1141 | } |
---|
| 1142 | ctx.fill(); |
---|
| 1143 | } else { |
---|
| 1144 | ctx.stroke(); |
---|
| 1145 | } |
---|
| 1146 | parent_element.append(canvas); |
---|
| 1147 | return canvas; |
---|
| 1148 | }, gen_options: function(track_id) { |
---|
| 1149 | var container = $("<div />").addClass("form-row"); |
---|
| 1150 | |
---|
| 1151 | var color = 'track_' + track_id + '_color', |
---|
| 1152 | color_label = $('<label />').attr("for", color).text("Color:"), |
---|
| 1153 | color_input = $('<input />').attr("id", color).attr("name", color).val(this.prefs.color), |
---|
| 1154 | minval = 'track_' + track_id + '_minval', |
---|
| 1155 | min_label = $('<label></label>').attr("for", minval).text("Min value:"), |
---|
| 1156 | min_val = (this.prefs.min_value === undefined ? "" : this.prefs.min_value), |
---|
| 1157 | min_input = $('<input></input>').attr("id", minval).val(min_val), |
---|
| 1158 | maxval = 'track_' + track_id + '_maxval', |
---|
| 1159 | max_label = $('<label></label>').attr("for", maxval).text("Max value:"), |
---|
| 1160 | max_val = (this.prefs.max_value === undefined ? "" : this.prefs.max_value), |
---|
| 1161 | max_input = $('<input></input>').attr("id", maxval).val(max_val); |
---|
| 1162 | |
---|
| 1163 | return container.append(min_label).append(min_input).append(max_label) |
---|
| 1164 | .append(max_input).append(color_label).append(color_input); |
---|
| 1165 | }, update_options: function(track_id) { |
---|
| 1166 | var min_value = $('#track_' + track_id + '_minval').val(), |
---|
| 1167 | max_value = $('#track_' + track_id + '_maxval').val(); |
---|
| 1168 | color = $('#track_' + track_id + '_color').val(); |
---|
| 1169 | if ( min_value !== this.prefs.min_value || max_value !== this.prefs.max_value || color !== this.prefs.color ) { |
---|
| 1170 | this.prefs.min_value = parseFloat(min_value); |
---|
| 1171 | this.prefs.max_value = parseFloat(max_value); |
---|
| 1172 | this.prefs.color = color; |
---|
| 1173 | this.vertical_range = this.prefs.max_value - this.prefs.min_value; |
---|
| 1174 | // Update the y-axis |
---|
| 1175 | $('#linetrack_' + track_id + '_minval').text(this.prefs.min_value); |
---|
| 1176 | $('#linetrack_' + track_id + '_maxval').text(this.prefs.max_value); |
---|
| 1177 | this.tile_cache.clear(); |
---|
| 1178 | this.draw(); |
---|
| 1179 | } |
---|
| 1180 | } |
---|
| 1181 | }); |
---|
| 1182 | |
---|
| 1183 | var FeatureTrack = function ( name, view, dataset_id, filters, prefs ) { |
---|
| 1184 | this.track_type = "FeatureTrack"; |
---|
| 1185 | this.display_modes = ["Auto", "Dense", "Squish", "Pack"]; |
---|
| 1186 | Track.call( this, name, view, view.viewport_container, filters ); |
---|
| 1187 | TiledTrack.call( this ); |
---|
| 1188 | |
---|
| 1189 | this.height_px = 0; |
---|
| 1190 | this.container_div.addClass( "feature-track" ); |
---|
| 1191 | this.dataset_id = dataset_id; |
---|
| 1192 | this.zo_slots = {}; |
---|
| 1193 | this.show_labels_scale = 0.001; |
---|
| 1194 | this.showing_details = false; |
---|
| 1195 | this.vertical_detail_px = 10; |
---|
| 1196 | this.vertical_nodetail_px = 2; |
---|
| 1197 | this.summary_draw_height = 30; |
---|
| 1198 | this.default_font = "9px Monaco, Lucida Console, monospace"; |
---|
| 1199 | this.inc_slots = {}; |
---|
| 1200 | this.data_queue = {}; |
---|
| 1201 | this.s_e_by_tile = {}; |
---|
| 1202 | this.tile_cache = new Cache(CACHED_TILES_FEATURE); |
---|
| 1203 | this.data_cache = new Cache(20); |
---|
| 1204 | this.left_offset = 200; |
---|
| 1205 | |
---|
| 1206 | this.prefs = { 'block_color': '#444', 'label_color': 'black', 'show_counts': true }; |
---|
| 1207 | if (prefs.block_color !== undefined) { this.prefs.block_color = prefs.block_color; } |
---|
| 1208 | if (prefs.label_color !== undefined) { this.prefs.label_color = prefs.label_color; } |
---|
| 1209 | if (prefs.show_counts !== undefined) { this.prefs.show_counts = prefs.show_counts; } |
---|
| 1210 | }; |
---|
| 1211 | $.extend( FeatureTrack.prototype, TiledTrack.prototype, { |
---|
| 1212 | init: function() { |
---|
| 1213 | var track = this, |
---|
| 1214 | key = "initial"; |
---|
| 1215 | |
---|
| 1216 | this.init_each({ low: track.view.max_low, high: track.view.max_high, dataset_id: track.dataset_id, |
---|
| 1217 | chrom: track.view.chrom, resolution: this.view.resolution, mode: track.mode }, function (result) { |
---|
| 1218 | track.mode_div.show(); |
---|
| 1219 | track.data_cache.set(key, result); |
---|
| 1220 | track.draw(); |
---|
| 1221 | }); |
---|
| 1222 | }, |
---|
| 1223 | get_data: function( low, high ) { |
---|
| 1224 | var track = this, |
---|
| 1225 | key = low + '_' + high; |
---|
| 1226 | |
---|
| 1227 | if (!track.data_queue[key]) { |
---|
| 1228 | track.data_queue[key] = true; |
---|
| 1229 | $.getJSON( data_url, { chrom: track.view.chrom, |
---|
| 1230 | low: low, high: high, dataset_id: track.dataset_id, |
---|
| 1231 | resolution: this.view.resolution, mode: this.mode }, function (result) { |
---|
| 1232 | track.data_cache.set(key, result); |
---|
| 1233 | // console.log("datacache", track.data_cache.get(key)); |
---|
| 1234 | delete track.data_queue[key]; |
---|
| 1235 | track.draw(); |
---|
| 1236 | }); |
---|
| 1237 | } |
---|
| 1238 | }, |
---|
| 1239 | incremental_slots: function( level, features, no_detail, mode ) { |
---|
| 1240 | if (!this.inc_slots[level]) { |
---|
| 1241 | this.inc_slots[level] = {}; |
---|
| 1242 | this.inc_slots[level].w_scale = level; |
---|
| 1243 | this.inc_slots[level].mode = mode; |
---|
| 1244 | this.s_e_by_tile[level] = {}; |
---|
| 1245 | } |
---|
| 1246 | // TODO: Should calculate zoom tile index, which will improve performance |
---|
| 1247 | // by only having to look at a smaller subset |
---|
| 1248 | // if (this.s_e_by_tile[0] === undefined) { this.s_e_by_tile[0] = []; } |
---|
| 1249 | var w_scale = this.inc_slots[level].w_scale, |
---|
| 1250 | undone = [], |
---|
| 1251 | highest_slot = 0, // To measure how big to draw canvas |
---|
| 1252 | dummy_canvas = $("<canvas></canvas>").get(0).getContext("2d"), |
---|
| 1253 | max_low = this.view.max_low; |
---|
| 1254 | |
---|
| 1255 | var slotted = []; |
---|
| 1256 | // Reset packing when we change display mode |
---|
| 1257 | if (this.inc_slots[level].mode !== mode) { |
---|
| 1258 | delete this.inc_slots[level]; |
---|
| 1259 | this.inc_slots[level] = { "mode": mode, "w_scale": w_scale }; |
---|
| 1260 | delete this.s_e_by_tile[level]; |
---|
| 1261 | this.s_e_by_tile[level] = {}; |
---|
| 1262 | } |
---|
| 1263 | // If feature already exists in slots (from previously seen tiles), use the same slot, |
---|
| 1264 | // otherwise if not seen, add to "undone" list for slot calculation |
---|
| 1265 | for (var i = 0, len = features.length; i < len; i++) { |
---|
| 1266 | var feature = features[i], |
---|
| 1267 | feature_uid = feature[0]; |
---|
| 1268 | if (this.inc_slots[level][feature_uid] !== undefined) { |
---|
| 1269 | highest_slot = Math.max(highest_slot, this.inc_slots[level][feature_uid]); |
---|
| 1270 | slotted.push(this.inc_slots[level][feature_uid]); |
---|
| 1271 | } else { |
---|
| 1272 | undone.push(i); |
---|
| 1273 | } |
---|
| 1274 | } |
---|
| 1275 | |
---|
| 1276 | // console.log("Slotted: ", features.length - undone.length, "/", features.length, slotted); |
---|
| 1277 | for (var i = 0, len = undone.length; i < len; i++) { |
---|
| 1278 | var feature = features[undone[i]], |
---|
| 1279 | feature_uid = feature[0], |
---|
| 1280 | feature_start = feature[1], |
---|
| 1281 | feature_end = feature[2], |
---|
| 1282 | feature_name = feature[3], |
---|
| 1283 | f_start = Math.floor( (feature_start - max_low) * w_scale ), |
---|
| 1284 | f_end = Math.ceil( (feature_end - max_low) * w_scale ); |
---|
| 1285 | |
---|
| 1286 | if (feature_name !== undefined && !no_detail) { |
---|
| 1287 | var text_len = dummy_canvas.measureText(feature_name).width; |
---|
| 1288 | if (f_start - text_len < 0) { |
---|
| 1289 | f_end += text_len; |
---|
| 1290 | } else { |
---|
| 1291 | f_start -= text_len; |
---|
| 1292 | } |
---|
| 1293 | } |
---|
| 1294 | |
---|
| 1295 | var j = 0; |
---|
| 1296 | // Try to fit the feature to the first slot that doesn't overlap any other features in that slot |
---|
| 1297 | while (j <= MAX_FEATURE_DEPTH) { |
---|
| 1298 | var found = true; |
---|
| 1299 | if (this.s_e_by_tile[level][j] !== undefined) { |
---|
| 1300 | for (var k = 0, k_len = this.s_e_by_tile[level][j].length; k < k_len; k++) { |
---|
| 1301 | var s_e = this.s_e_by_tile[level][j][k]; |
---|
| 1302 | if (f_end > s_e[0] && f_start < s_e[1]) { |
---|
| 1303 | found = false; |
---|
| 1304 | break; |
---|
| 1305 | } |
---|
| 1306 | } |
---|
| 1307 | } |
---|
| 1308 | if (found) { |
---|
| 1309 | if (this.s_e_by_tile[level][j] === undefined) { this.s_e_by_tile[level][j] = []; } |
---|
| 1310 | this.s_e_by_tile[level][j].push([f_start, f_end]); |
---|
| 1311 | this.inc_slots[level][feature_uid] = j; |
---|
| 1312 | highest_slot = Math.max(highest_slot, j); |
---|
| 1313 | break; |
---|
| 1314 | } |
---|
| 1315 | j++; |
---|
| 1316 | } |
---|
| 1317 | } |
---|
| 1318 | return highest_slot; |
---|
| 1319 | |
---|
| 1320 | }, |
---|
| 1321 | rect_or_text: function( ctx, w_scale, tile_low, tile_high, feature_start, orig_seq, cigar, y_center ) { |
---|
| 1322 | ctx.textAlign = "center"; |
---|
| 1323 | var cur_offset = 0, |
---|
| 1324 | gap = Math.round(w_scale / 2); |
---|
| 1325 | |
---|
| 1326 | for (cig_id in cigar) { |
---|
| 1327 | var cig = cigar[cig_id], |
---|
| 1328 | cig_op = "MIDNSHP"[cig[0]], |
---|
| 1329 | cig_len = cig[1]; |
---|
| 1330 | |
---|
| 1331 | if (cig_op === "H" || cig_op === "S") { |
---|
| 1332 | // Go left if it clips |
---|
| 1333 | cur_offset -= cig_len; |
---|
| 1334 | } |
---|
| 1335 | var seq_start = feature_start + cur_offset, |
---|
| 1336 | s_start = Math.floor( Math.max(0, (seq_start - tile_low) * w_scale) ), |
---|
| 1337 | s_end = Math.floor( Math.max(0, (seq_start + cig_len - tile_low) * w_scale) ); |
---|
| 1338 | |
---|
| 1339 | switch (cig_op) { |
---|
| 1340 | case "S": // Soft clipping |
---|
| 1341 | case "H": // Hard clipping |
---|
| 1342 | case "M": // Match |
---|
| 1343 | var seq = orig_seq.slice(cur_offset, cig_len); |
---|
| 1344 | if ( (this.mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > PX_PER_CHAR) { |
---|
| 1345 | ctx.fillStyle = this.prefs.block_color; |
---|
| 1346 | ctx.fillRect(s_start + this.left_offset, y_center + 1, s_end - s_start, 9); |
---|
| 1347 | ctx.fillStyle = CONNECTOR_COLOR; |
---|
| 1348 | for (var c = 0, str_len = seq.length; c < str_len; c++) { |
---|
| 1349 | if (seq_start + c >= tile_low && seq_start + c <= tile_high) { |
---|
| 1350 | var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); |
---|
| 1351 | ctx.fillText(seq[c], c_start + this.left_offset + gap, y_center + 9); |
---|
| 1352 | } |
---|
| 1353 | } |
---|
| 1354 | } else { |
---|
| 1355 | ctx.fillStyle = this.prefs.block_color; |
---|
| 1356 | ctx.fillRect(s_start + this.left_offset, y_center + 4, s_end - s_start, 3); |
---|
| 1357 | } |
---|
| 1358 | break; |
---|
| 1359 | case "N": // Skipped bases |
---|
| 1360 | ctx.fillStyle = CONNECTOR_COLOR; |
---|
| 1361 | ctx.fillRect(s_start + this.left_offset, y_center + 5, s_end - s_start, 1); |
---|
| 1362 | break; |
---|
| 1363 | case "D": // Deletion |
---|
| 1364 | ctx.fillStyle = "red"; |
---|
| 1365 | ctx.fillRect(s_start + this.left_offset, y_center + 4, s_end - s_start, 3); |
---|
| 1366 | break; |
---|
| 1367 | case "P": // TODO: No good way to draw insertions/padding right now, so ignore |
---|
| 1368 | case "I": |
---|
| 1369 | break; |
---|
| 1370 | } |
---|
| 1371 | cur_offset += cig_len; |
---|
| 1372 | } |
---|
| 1373 | }, |
---|
| 1374 | draw_tile: function( resolution, tile_index, parent_element, w_scale ) { |
---|
| 1375 | var tile_low = tile_index * DENSITY * resolution, |
---|
| 1376 | tile_high = ( tile_index + 1 ) * DENSITY * resolution, |
---|
| 1377 | tile_span = tile_high - tile_low; |
---|
| 1378 | // console.log("drawing " + tile_low + " to " + tile_high); |
---|
| 1379 | |
---|
| 1380 | /*for (var k in this.data_cache.obj_cache) { |
---|
| 1381 | var k_split = k.split("_"), k_low = k_split[0], k_high = k_split[1]; |
---|
| 1382 | if (k_low <= tile_low && k_high >= tile_high) { |
---|
| 1383 | data = this.data_cache.get(k); |
---|
| 1384 | break; |
---|
| 1385 | } |
---|
| 1386 | }*/ |
---|
| 1387 | var k = (!this.initial_canvas ? "initial" : tile_low + '_' + tile_high); |
---|
| 1388 | var result = this.data_cache.get(k); |
---|
| 1389 | var cur_mode; |
---|
| 1390 | |
---|
| 1391 | if (result === undefined || (this.mode !== "Auto" && result.dataset_type === "summary_tree")) { |
---|
| 1392 | this.data_queue[ [tile_low, tile_high] ] = true; |
---|
| 1393 | this.get_data(tile_low, tile_high); |
---|
| 1394 | return; |
---|
| 1395 | } |
---|
| 1396 | |
---|
| 1397 | var width = Math.ceil( tile_span * w_scale ), |
---|
| 1398 | new_canvas = $("<canvas class='tile'></canvas>"), |
---|
| 1399 | label_color = this.prefs.label_color, |
---|
| 1400 | block_color = this.prefs.block_color, |
---|
| 1401 | mode = this.mode, |
---|
| 1402 | min_height = 25, |
---|
| 1403 | no_detail = (mode === "Squish") || (mode === "Dense") && (mode !== "Pack") || (mode === "Auto" && (result.extra_info === "no_detail")), |
---|
| 1404 | left_offset = this.left_offset, |
---|
| 1405 | slots, required_height, y_scale; |
---|
| 1406 | |
---|
| 1407 | if (result.dataset_type === "summary_tree") { |
---|
| 1408 | required_height = this.summary_draw_height; |
---|
| 1409 | } else if (mode === "Dense") { |
---|
| 1410 | required_height = min_height; |
---|
| 1411 | y_scale = 10; |
---|
| 1412 | } else { |
---|
| 1413 | // Calculate new slots incrementally for this new chunk of data and update height if necessary |
---|
| 1414 | y_scale = ( no_detail ? this.vertical_nodetail_px : this.vertical_detail_px ); |
---|
| 1415 | var inc_scale = (w_scale < 0.0001 ? 1/view.zoom_res : w_scale); |
---|
| 1416 | required_height = this.incremental_slots( inc_scale, result.data, no_detail, mode ) * y_scale + min_height; |
---|
| 1417 | slots = this.inc_slots[inc_scale]; |
---|
| 1418 | } |
---|
| 1419 | |
---|
| 1420 | new_canvas.css({ |
---|
| 1421 | position: "absolute", |
---|
| 1422 | top: 0, |
---|
| 1423 | left: ( tile_low - this.view.low ) * w_scale - left_offset |
---|
| 1424 | }); |
---|
| 1425 | new_canvas.get(0).width = width + left_offset; |
---|
| 1426 | new_canvas.get(0).height = required_height; |
---|
| 1427 | parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); |
---|
| 1428 | // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); |
---|
| 1429 | var ctx = new_canvas.get(0).getContext("2d"); |
---|
| 1430 | ctx.fillStyle = block_color; |
---|
| 1431 | ctx.font = this.default_font; |
---|
| 1432 | ctx.textAlign = "right"; |
---|
| 1433 | this.container_div.find(".yaxislabel").remove(); |
---|
| 1434 | |
---|
| 1435 | if (result.dataset_type == "summary_tree") { |
---|
| 1436 | var points = result.data, |
---|
| 1437 | max = result.max, |
---|
| 1438 | avg = result.avg, |
---|
| 1439 | delta_x_px = Math.ceil(result.delta * w_scale); |
---|
| 1440 | |
---|
| 1441 | var max_label = $("<div />").addClass('yaxislabel').text(max); |
---|
| 1442 | |
---|
| 1443 | max_label.css({ position: "absolute", top: "22px", left: "10px" }); |
---|
| 1444 | max_label.prependTo(this.container_div); |
---|
| 1445 | |
---|
| 1446 | for ( var i = 0, len = points.length; i < len; i++ ) { |
---|
| 1447 | var x = Math.floor( (points[i][0] - tile_low) * w_scale ); |
---|
| 1448 | var y = points[i][1]; |
---|
| 1449 | |
---|
| 1450 | if (!y) { continue; } |
---|
| 1451 | var y_px = y / max * this.summary_draw_height; |
---|
| 1452 | |
---|
| 1453 | ctx.fillStyle = "black"; |
---|
| 1454 | ctx.fillRect(x + left_offset, this.summary_draw_height - y_px, delta_x_px, y_px); |
---|
| 1455 | |
---|
| 1456 | if (this.prefs.show_counts && ctx.measureText(y).width < delta_x_px) { |
---|
| 1457 | ctx.fillStyle = "#bbb"; |
---|
| 1458 | ctx.textAlign = "center"; |
---|
| 1459 | ctx.fillText(y, x + left_offset + (delta_x_px/2), this.summary_draw_height - 5); |
---|
| 1460 | } |
---|
| 1461 | } |
---|
| 1462 | cur_mode = "Summary"; |
---|
| 1463 | parent_element.append( new_canvas ); |
---|
| 1464 | return new_canvas; |
---|
| 1465 | } |
---|
| 1466 | |
---|
| 1467 | if (result.message) { |
---|
| 1468 | new_canvas.css({ |
---|
| 1469 | border: "solid red", |
---|
| 1470 | "border-width": "2px 2px 2px 0px" |
---|
| 1471 | }); |
---|
| 1472 | ctx.fillStyle = "red"; |
---|
| 1473 | ctx.textAlign = "left"; |
---|
| 1474 | ctx.fillText(result.message, 100 + left_offset, y_scale); |
---|
| 1475 | } |
---|
| 1476 | |
---|
| 1477 | // |
---|
| 1478 | // We're now working at the level of individual data points. |
---|
| 1479 | // |
---|
| 1480 | |
---|
| 1481 | // See if tile is filterable. If so, add class. |
---|
| 1482 | var filterable = false; |
---|
| 1483 | if ( result.data.length != 0 ) { |
---|
| 1484 | filterable = true; |
---|
| 1485 | for (var f = 0; f < this.filters.length; f++) |
---|
| 1486 | if ( !this.filters[f].applies_to( result.data[0] ) ) { |
---|
| 1487 | filterable = false; |
---|
| 1488 | } |
---|
| 1489 | } |
---|
| 1490 | if ( filterable ) { |
---|
| 1491 | new_canvas.addClass(FILTERABLE_CLASS); |
---|
| 1492 | } |
---|
| 1493 | |
---|
| 1494 | // Draw data points. |
---|
| 1495 | var data = result.data; |
---|
| 1496 | var j = 0; |
---|
| 1497 | for (var i = 0, len = data.length; i < len; i++) { |
---|
| 1498 | var feature = data[i], |
---|
| 1499 | feature_uid = feature[0], |
---|
| 1500 | feature_start = feature[1], |
---|
| 1501 | feature_end = feature[2], |
---|
| 1502 | feature_name = feature[3]; |
---|
| 1503 | |
---|
| 1504 | if (slots[feature_uid] === undefined) { |
---|
| 1505 | continue; |
---|
| 1506 | } |
---|
| 1507 | |
---|
| 1508 | // Apply filters to feature. |
---|
| 1509 | var hide_feature = false; |
---|
| 1510 | var filter; |
---|
| 1511 | for (var f = 0; f < this.filters.length; f++) { |
---|
| 1512 | filter = this.filters[f]; |
---|
| 1513 | filter.update_attrs( feature ); |
---|
| 1514 | if ( !filter.keep( feature ) ) { |
---|
| 1515 | hide_feature = true; |
---|
| 1516 | break; |
---|
| 1517 | } |
---|
| 1518 | } |
---|
| 1519 | if ( hide_feature ) |
---|
| 1520 | continue; |
---|
| 1521 | |
---|
| 1522 | if (feature_start <= tile_high && feature_end >= tile_low) { |
---|
| 1523 | var f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), |
---|
| 1524 | f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), |
---|
| 1525 | y_center = (mode === "Dense" ? 1 : (1 + slots[feature_uid])) * y_scale; |
---|
| 1526 | |
---|
| 1527 | if (result.dataset_type === "bai") { |
---|
| 1528 | var cigar = feature[4]; |
---|
| 1529 | ctx.fillStyle = block_color; |
---|
| 1530 | if (feature[5] instanceof Array) { |
---|
| 1531 | var b1_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ), |
---|
| 1532 | b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ), |
---|
| 1533 | b2_start = Math.floor( Math.max(0, (feature[6][0] - tile_low) * w_scale) ), |
---|
| 1534 | b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[6][1] - tile_low) * w_scale)) ); |
---|
| 1535 | |
---|
| 1536 | if (feature[5][1] >= tile_low && feature[5][0] <= tile_high) { |
---|
| 1537 | this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], cigar, y_center); |
---|
| 1538 | } |
---|
| 1539 | if (feature[6][1] >= tile_low && feature[6][0] <= tile_high) { |
---|
| 1540 | this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature[6][0], feature[6][2], cigar, y_center); |
---|
| 1541 | } |
---|
| 1542 | if (b2_start > b1_end) { |
---|
| 1543 | ctx.fillStyle = CONNECTOR_COLOR; |
---|
| 1544 | ctx.fillRect(b1_end + left_offset, y_center + 5, b2_start - b1_end, 1); |
---|
| 1545 | } |
---|
| 1546 | } else { |
---|
| 1547 | ctx.fillStyle = block_color; |
---|
| 1548 | this.rect_or_text(ctx, w_scale, tile_low, tile_high, feature_start, feature_name, cigar, y_center); |
---|
| 1549 | } |
---|
| 1550 | if (mode !== "Dense" && !no_detail && feature_start > tile_low) { |
---|
| 1551 | // Draw label |
---|
| 1552 | ctx.fillStyle = this.prefs.label_color; |
---|
| 1553 | if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { |
---|
| 1554 | ctx.textAlign = "left"; |
---|
| 1555 | ctx.fillText(feature_uid, f_end + 2 + left_offset, y_center + 8); |
---|
| 1556 | } else { |
---|
| 1557 | ctx.textAlign = "right"; |
---|
| 1558 | ctx.fillText(feature_uid, f_start - 2 + left_offset, y_center + 8); |
---|
| 1559 | } |
---|
| 1560 | ctx.fillStyle = block_color; |
---|
| 1561 | } |
---|
| 1562 | |
---|
| 1563 | } else if (result.dataset_type === "interval_index") { |
---|
| 1564 | |
---|
| 1565 | // console.log(feature_uid, feature_start, feature_end, f_start, f_end, y_center); |
---|
| 1566 | if (no_detail) { |
---|
| 1567 | ctx.fillStyle = block_color; |
---|
| 1568 | ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); |
---|
| 1569 | } else { |
---|
| 1570 | // Showing labels, blocks, details |
---|
| 1571 | var feature_strand = feature[4], |
---|
| 1572 | feature_ts = feature[5], |
---|
| 1573 | feature_te = feature[6], |
---|
| 1574 | feature_blocks = feature[7]; |
---|
| 1575 | |
---|
| 1576 | var thickness, y_start, thick_start = null, thick_end = null; |
---|
| 1577 | if (feature_ts && feature_te) { |
---|
| 1578 | thick_start = Math.floor( Math.max(0, (feature_ts - tile_low) * w_scale) ); |
---|
| 1579 | thick_end = Math.ceil( Math.min(width, Math.max(0, (feature_te - tile_low) * w_scale)) ); |
---|
| 1580 | } |
---|
| 1581 | if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { |
---|
| 1582 | // Draw label |
---|
| 1583 | ctx.fillStyle = label_color; |
---|
| 1584 | if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { |
---|
| 1585 | ctx.textAlign = "left"; |
---|
| 1586 | ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); |
---|
| 1587 | } else { |
---|
| 1588 | ctx.textAlign = "right"; |
---|
| 1589 | ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); |
---|
| 1590 | } |
---|
| 1591 | ctx.fillStyle = block_color; |
---|
| 1592 | } |
---|
| 1593 | if (feature_blocks) { |
---|
| 1594 | // Draw introns |
---|
| 1595 | if (feature_strand) { |
---|
| 1596 | if (feature_strand == "+") { |
---|
| 1597 | ctx.fillStyle = RIGHT_STRAND; |
---|
| 1598 | } else if (feature_strand == "-") { |
---|
| 1599 | ctx.fillStyle = LEFT_STRAND; |
---|
| 1600 | } |
---|
| 1601 | ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, 10); |
---|
| 1602 | ctx.fillStyle = block_color; |
---|
| 1603 | } |
---|
| 1604 | |
---|
| 1605 | for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { |
---|
| 1606 | var block = feature_blocks[k], |
---|
| 1607 | block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), |
---|
| 1608 | block_end = Math.ceil( Math.min(width, Math.max((block[1] - tile_low) * w_scale)) ); |
---|
| 1609 | if (block_start > block_end) { continue; } |
---|
| 1610 | // Draw the block |
---|
| 1611 | thickness = 5; |
---|
| 1612 | y_start = 3; |
---|
| 1613 | ctx.fillRect(block_start + left_offset, y_center + y_start, block_end - block_start, thickness); |
---|
| 1614 | |
---|
| 1615 | // Draw thick regions: check if block intersects with thick region |
---|
| 1616 | if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { |
---|
| 1617 | thickness = 9; |
---|
| 1618 | y_start = 1; |
---|
| 1619 | var block_thick_start = Math.max(block_start, thick_start), |
---|
| 1620 | block_thick_end = Math.min(block_end, thick_end); |
---|
| 1621 | ctx.fillRect(block_thick_start + left_offset, y_center + y_start, block_thick_end - block_thick_start, thickness); |
---|
| 1622 | |
---|
| 1623 | } |
---|
| 1624 | } |
---|
| 1625 | } else { |
---|
| 1626 | // If there are no blocks, we treat the feature as one big exon |
---|
| 1627 | thickness = 9; |
---|
| 1628 | y_start = 1; |
---|
| 1629 | ctx.fillRect(f_start + left_offset, y_center + y_start, f_end - f_start, thickness); |
---|
| 1630 | if ( feature.strand ) { |
---|
| 1631 | if (feature.strand == "+") { |
---|
| 1632 | ctx.fillStyle = RIGHT_STRAND_INV; |
---|
| 1633 | } else if (feature.strand == "-") { |
---|
| 1634 | ctx.fillStyle = LEFT_STRAND_INV; |
---|
| 1635 | } |
---|
| 1636 | ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, 10); |
---|
| 1637 | ctx.fillStyle = block_color; |
---|
| 1638 | } |
---|
| 1639 | } |
---|
| 1640 | } |
---|
| 1641 | } else if (result.dataset_type === 'vcf') { |
---|
| 1642 | // VCF track. |
---|
| 1643 | if (no_detail) { |
---|
| 1644 | ctx.fillStyle = block_color; |
---|
| 1645 | ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); |
---|
| 1646 | } |
---|
| 1647 | else { // Show blocks, labels, etc. |
---|
| 1648 | // Unpack. |
---|
| 1649 | var ref_base = feature[4], alt_base = feature[5], qual = feature[6]; |
---|
| 1650 | |
---|
| 1651 | // Draw block for entry. |
---|
| 1652 | thickness = 9; |
---|
| 1653 | y_start = 1; |
---|
| 1654 | ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thickness); |
---|
| 1655 | |
---|
| 1656 | // Add label for entry. |
---|
| 1657 | if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { |
---|
| 1658 | // Draw label |
---|
| 1659 | ctx.fillStyle = label_color; |
---|
| 1660 | if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { |
---|
| 1661 | ctx.textAlign = "left"; |
---|
| 1662 | ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); |
---|
| 1663 | } else { |
---|
| 1664 | ctx.textAlign = "right"; |
---|
| 1665 | ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); |
---|
| 1666 | } |
---|
| 1667 | ctx.fillStyle = block_color; |
---|
| 1668 | } |
---|
| 1669 | |
---|
| 1670 | // Show additional data on block. |
---|
| 1671 | var vcf_label = ref_base + " / " + alt_base; |
---|
| 1672 | if (feature_start > tile_low && ctx.measureText(vcf_label).width < (f_end - f_start)) { |
---|
| 1673 | ctx.fillStyle = "white"; |
---|
| 1674 | ctx.textAlign = "center"; |
---|
| 1675 | ctx.fillText(vcf_label, left_offset + f_start + (f_end-f_start)/2, y_center + 8); |
---|
| 1676 | ctx.fillStyle = block_color; |
---|
| 1677 | } |
---|
| 1678 | } |
---|
| 1679 | } |
---|
| 1680 | j++; |
---|
| 1681 | } |
---|
| 1682 | } |
---|
| 1683 | return new_canvas; |
---|
| 1684 | }, gen_options: function(track_id) { |
---|
| 1685 | var container = $("<div />").addClass("form-row"); |
---|
| 1686 | |
---|
| 1687 | var block_color = 'track_' + track_id + '_block_color', |
---|
| 1688 | block_color_label = $('<label />').attr("for", block_color).text("Block color:"), |
---|
| 1689 | block_color_input = $('<input />').attr("id", block_color).attr("name", block_color).val(this.prefs.block_color), |
---|
| 1690 | label_color = 'track_' + track_id + '_label_color', |
---|
| 1691 | label_color_label = $('<label />').attr("for", label_color).text("Text color:"), |
---|
| 1692 | label_color_input = $('<input />').attr("id", label_color).attr("name", label_color).val(this.prefs.label_color), |
---|
| 1693 | show_count = 'track_' + track_id + '_show_count', |
---|
| 1694 | show_count_label = $('<label />').attr("for", show_count).text("Show summary counts"), |
---|
| 1695 | show_count_input = $('<input type="checkbox" style="float:left;"></input>').attr("id", show_count).attr("name", show_count).attr("checked", this.prefs.show_counts), |
---|
| 1696 | show_count_div = $('<div />').append(show_count_input).append(show_count_label); |
---|
| 1697 | |
---|
| 1698 | return container.append(block_color_label).append(block_color_input).append(label_color_label).append(label_color_input).append(show_count_div); |
---|
| 1699 | }, update_options: function(track_id) { |
---|
| 1700 | var block_color = $('#track_' + track_id + '_block_color').val(), |
---|
| 1701 | label_color = $('#track_' + track_id + '_label_color').val(), |
---|
| 1702 | mode = $('#track_' + track_id + '_mode option:selected').val(), |
---|
| 1703 | show_counts = $('#track_' + track_id + '_show_count').attr("checked"); |
---|
| 1704 | if (block_color !== this.prefs.block_color || label_color !== this.prefs.label_color || show_counts !== this.prefs.show_counts) { |
---|
| 1705 | this.prefs.block_color = block_color; |
---|
| 1706 | this.prefs.label_color = label_color; |
---|
| 1707 | this.prefs.show_counts = show_counts; |
---|
| 1708 | this.tile_cache.clear(); |
---|
| 1709 | this.draw(); |
---|
| 1710 | } |
---|
| 1711 | } |
---|
| 1712 | }); |
---|
| 1713 | |
---|
| 1714 | var ReadTrack = function ( name, view, dataset_id, filters, prefs ) { |
---|
| 1715 | FeatureTrack.call( this, name, view, dataset_id, filters, prefs ); |
---|
| 1716 | this.track_type = "ReadTrack"; |
---|
| 1717 | this.vertical_detail_px = 10; |
---|
| 1718 | this.vertical_nodetail_px = 5; |
---|
| 1719 | |
---|
| 1720 | }; |
---|
| 1721 | $.extend( ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { |
---|
| 1722 | }); |
---|