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 | }); |
---|