root/galaxy-central/static/scripts/trackster.js @ 2

リビジョン 2, 75.9 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

行番号 
1/* Trackster
2    2010, James Taylor, Kanwei Li
3*/
4
5var 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   
22var right_img = new Image();
23right_img.src = image_path + "/visualization/strand_right.png";
24right_img.onload = function() {
25    RIGHT_STRAND = CONTEXT.createPattern(right_img, "repeat");
26};
27var left_img = new Image();
28left_img.src = image_path + "/visualization/strand_left.png";
29left_img.onload = function() {
30    LEFT_STRAND = CONTEXT.createPattern(left_img, "repeat");
31};
32var right_img_inv = new Image();
33right_img_inv.src = image_path + "/visualization/strand_right_inv.png";
34right_img_inv.onload = function() {
35    RIGHT_STRAND_INV = CONTEXT.createPattern(right_img_inv, "repeat");
36};
37var left_img_inv = new Image();
38left_img_inv.src = image_path + "/visualization/strand_left_inv.png";
39left_img_inv.onload = function() {
40    LEFT_STRAND_INV = CONTEXT.createPattern(left_img_inv, "repeat");
41};
42
43function round_1000(num) {
44    return Math.round(num * 1000) / 1000;   
45}
46   
47var 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
79var 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.
453var 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.
460var 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.
527var 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
541var 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
655var 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
871var 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
899var 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
974var 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
1183var 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
1714var 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});
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。