25 new changesets in galaxy-central: http://bitbucket.org/galaxy/galaxy-central/changeset/e97dc2a4d5b8/ changeset: r5295:e97dc2a4d5b8 user: james_taylor date: 2011-03-29 02:32:43 summary: Extract feature slotting into a new class affected #: 1 file (648 bytes) --- a/static/scripts/trackster.js Thu Mar 31 11:27:49 2011 -0400 +++ b/static/scripts/trackster.js Mon Mar 28 20:32:43 2011 -0400 @@ -2236,150 +2236,18 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } - } - - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; - - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } - } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } - } - */ - return highest_slot + 1; + + return inc_slots.slot_features( features ); }, /** * Draw summary tree on canvas. @@ -2641,7 +2509,7 @@ else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; + slots = this.inc_slots[w_scale].slots; } // @@ -3131,4 +2999,154 @@ } }); +// ---- To be extracted ------------------------------------------------------ +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} http://bitbucket.org/galaxy/galaxy-central/changeset/661a4b169dd7/ changeset: r5296:661a4b169dd7 user: james_taylor date: 2011-03-29 03:35:29 summary: Extract SummaryTree painter, abstract canvas creation into CanvasManager class affected #: 1 file (627 bytes) --- a/static/scripts/trackster.js Mon Mar 28 20:32:43 2011 -0400 +++ b/static/scripts/trackster.js Mon Mar 28 21:35:29 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1949,20 +1950,17 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return canvas; + return $(canvas); } this.content_div.css("height", "0px"); } @@ -2253,38 +2251,17 @@ * Draw summary tree on canvas. */ draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); + // Add label to container div when displaying summary tree mode var max_label = $("<div />").addClass('yaxislabel'); max_label.text(max); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); max_label.prependTo(this.container_div); - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } + // Paint summary tree into canvas + var ctx = canvas.getContext("2d"); + var painter = new SummaryTreePainter( points, delta, max, this.prefs.show_counts ); + painter.draw( ctx, w_scale, required_height, tile_low, left_offset ); }, /** * Draw feature. @@ -2463,10 +2440,6 @@ left_offset = this.left_offset, slots, required_height; - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - // // Set mode if Auto. // @@ -2515,15 +2488,17 @@ // // Set up for drawing. // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; + var canvas = this.view.canvas_manager.new_canvas(); + + canvas.width = width + left_offset; + canvas.height = required_height; if (result.dataset_type === "summary_tree") { // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; + canvas.height += LABEL_SPACING + CHAR_HEIGHT_PX; } parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); + var ctx = canvas.getContext("2d"); ctx.fillStyle = this.prefs.block_color; ctx.font = this.default_font; ctx.textAlign = "right"; @@ -2535,14 +2510,14 @@ if (mode === "summary_tree") { this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, tile_low, left_offset); - return canvas; + return $(canvas); } // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { - canvas.css({ + $(canvas).css({ "border-top": "1px solid red" }); @@ -2555,7 +2530,7 @@ // If there's no data, return. if (!result.data) { - return canvas; + return $(canvas); } } @@ -2598,7 +2573,7 @@ width, left_offset, ref_seq); } } - return canvas; + return $(canvas); } }); @@ -3001,6 +2976,20 @@ // ---- To be extracted ------------------------------------------------------ +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + /** * FeatureSlotter determines slots in which to draw features for vertical * packing. @@ -3150,3 +3139,50 @@ */ return highest_slot + 1; } + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, show_counts ) { + this.data = data; + this.delta = delta; + this.max = max; + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, w_scale, required_height, tile_low, left_offset ) { + + var + points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + var x = Math.floor( (points[i][0] - tile_low) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * required_height; + + ctx.fillStyle = "black"; + ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + http://bitbucket.org/galaxy/galaxy-central/changeset/aebefaeeda4b/ changeset: r5297:aebefaeeda4b user: james_taylor date: 2011-03-29 19:56:44 summary: Extract LinePainter from LineTrack affected #: 1 file (281 bytes) --- a/static/scripts/trackster.js Mon Mar 28 21:35:29 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 13:56:44 2011 -0400 @@ -2008,10 +2008,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2039,9 +2043,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2082,99 +2084,23 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); - - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } - return canvas; + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + + return $(canvas); } }); @@ -3186,3 +3112,103 @@ ctx.restore(); } +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} \ No newline at end of file http://bitbucket.org/galaxy/galaxy-central/changeset/adc2649cb7a7/ changeset: r5298:adc2649cb7a7 user: james_taylor date: 2011-03-29 19:56:57 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 0 files (0 bytes) http://bitbucket.org/galaxy/galaxy-central/changeset/d0f1963a9ba2/ changeset: r5299:d0f1963a9ba2 user: james_taylor date: 2011-03-30 00:40:18 summary: Streamlining feature track drawing in preparation for pulling out a Painter, cleaning up Painter interface (not quite a common interface yet) affected #: 1 file (35 bytes) --- a/static/scripts/trackster.js Tue Mar 29 13:56:57 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 18:40:18 2011 -0400 @@ -2174,22 +2174,6 @@ return inc_slots.slot_features( features ); }, /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - - // Add label to container div when displaying summary tree mode - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - // Paint summary tree into canvas - var ctx = canvas.getContext("2d"); - var painter = new SummaryTreePainter( points, delta, max, this.prefs.show_counts ); - painter.draw( ctx, w_scale, required_height, tile_low, left_offset ); - }, - /** * Draw feature. */ draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { @@ -2350,25 +2334,18 @@ * Draw FeatureTrack tile. */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, + var track = this, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, - slots, required_height; + slots, + required_height; - // - // Set mode if Auto. - // + // Set display mode if Auto. if (mode === "Auto") { if (result.dataset_type === "summary_tree") { mode = result.dataset_type; @@ -2393,19 +2370,43 @@ } } } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } + // Start dealing with row-by-row tracks + + // y_scale is the height per row var y_scale = this.get_y_scale(mode); - // // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } if (mode === "Dense") { required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; slots = this.inc_slots[w_scale].slots; @@ -2418,10 +2419,7 @@ canvas.width = width + left_offset; canvas.height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.height += LABEL_SPACING + CHAR_HEIGHT_PX; - } + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); var ctx = canvas.getContext("2d"); @@ -2430,16 +2428,6 @@ ctx.textAlign = "right"; this.container_div.find(".yaxislabel").remove(); - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return $(canvas); - } - - // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { @@ -3071,41 +3059,50 @@ /** * SummaryTreePainter, a histogram showing number of intervals in a region */ -var SummaryTreePainter = function( data, delta, max, show_counts ) { +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties this.data = data; this.delta = delta; this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs this.show_counts = show_counts; } -SummaryTreePainter.prototype.draw = function( ctx, w_scale, required_height, tile_low, left_offset ) { +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { - var - points = this.data, delta = this.delta, max = this.max, + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases // start. However, height of each rectangle is relative to required_height; hence, the // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; delta_x_px = Math.ceil(delta * w_scale); ctx.save(); for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); var y = points[i][1]; if (!y) { continue; } - var y_px = y / max * required_height; + var y_px = y / max * height; ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); // Draw number count if it can fit the number with some padding, otherwise things clump up var text_padding_req_x = 4; if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { ctx.fillStyle = "#666"; ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); + ctx.fillText(y, x + (delta_x_px/2), 10); } } @@ -3113,7 +3110,9 @@ } var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties this.data = data; + // View this.view_start = view_start; this.view_end = view_end; // Drawing prefs http://bitbucket.org/galaxy/galaxy-central/changeset/ffc9a245604c/ changeset: r5300:ffc9a245604c user: james_taylor date: 2011-03-30 00:40:24 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 0 files (0 bytes) http://bitbucket.org/galaxy/galaxy-central/changeset/dc34db7a3248/ changeset: r5301:dc34db7a3248 user: james_taylor date: 2011-03-30 19:01:41 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 0 files (0 bytes) http://bitbucket.org/galaxy/galaxy-central/changeset/64774906acfc/ changeset: r5302:64774906acfc user: james_taylor date: 2011-03-30 19:09:42 summary: Fix accidently introduced tabs affected #: 1 file (1.9 KB) --- a/static/scripts/trackster.js Wed Mar 30 13:01:41 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 13:09:42 2011 -0400 @@ -2012,10 +2012,10 @@ }; $.extend(LineTrack.prototype, TiledTrack.prototype, { add_resize_handle: function () { - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - var track = this; + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2086,20 +2086,20 @@ var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - width = Math.ceil( tile_length * w_scale ), - height = this.height_px; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; - // Create canvas + // Create canvas var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width, canvas.height = height; - // Paint line onto full canvas - var ctx = canvas.getContext("2d"); - var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, - this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); - painter.draw( ctx, width, height ); - + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + return $(canvas); } }); @@ -2170,8 +2170,8 @@ inc_slots.mode = mode; this.inc_slots[level] = inc_slots; } - - return inc_slots.slot_features( features ); + + return inc_slots.slot_features( features ); }, /** * Draw feature. @@ -2335,15 +2335,15 @@ */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { var track = this, - tile_low = tile_index * DENSITY * resolution, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, slots, - required_height; + required_height; // Set display mode if Auto. if (mode === "Auto") { @@ -2370,37 +2370,37 @@ } } } - - // Drawing the summary tree (feature coverage histogram) - if ( mode === "summary_tree" ) { - // Set height of parent_element - required_height = this.summary_draw_height; - parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); - // Add label to container div showing maximum count - // TODO: this shouldn't be done at the tile level - this.container_div.find(".yaxislabel").remove(); - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text( result.max ); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - // Create canvas - var canvas = this.view.canvas_manager.new_canvas(); - canvas.width = width + left_offset; - // Extra padding at top of summary tree - canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - // Paint summary tree into canvas - var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); - var ctx = canvas.getContext("2d"); - // Deal with left_offset by translating - ctx.translate( left_offset, 0 ); - painter.draw( ctx, width, required_height ); - // Wrapped canvas element is returned - return $(canvas); - } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } - // Start dealing with row-by-row tracks + // Start dealing with row-by-row tracks - // y_scale is the height per row + // y_scale is the height per row var y_scale = this.get_y_scale(mode); // Pack reads, set required height. @@ -2415,8 +2415,8 @@ // // Set up for drawing. // - var canvas = this.view.canvas_manager.new_canvas(); - + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; canvas.height = required_height; @@ -2934,14 +2934,14 @@ // by only having to look at a smaller subset // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } } // Slot unslotted features. @@ -2949,106 +2949,106 @@ // Find the first slot such that current feature doesn't overlap any other features in that slot. // Returns -1 if no slot was found. var find_slot = function(f_start, f_end) { - // TODO: Fix constants - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; }; // Do slotting. for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( feature_start * w_scale ), - f_end = Math.ceil( feature_end * w_scale ), - text_len = this.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && this.include_label ) { - // Add gap for label spacing and extra pack space padding - // TODO: Fix constants - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } } // Debugging: view slots data. /* for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } } */ return highest_slot + 1; @@ -3074,36 +3074,36 @@ SummaryTreePainter.prototype.draw = function( ctx, width, height ) { var view_start = this.view_start, - view_range = this.view_end - this.view_start, - w_scale = width / view_range; + view_range = this.view_end - this.view_start, + w_scale = width / view_range; var points = this.data, delta = this.delta, max = this.max, - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); ctx.save(); for (var i = 0, len = points.length; i < len; i++) { - - var x = Math.floor( (points[i][0] - view_start) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * height; - - ctx.fillStyle = "black"; - ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + (delta_x_px/2), 10); - } + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } } ctx.restore(); @@ -3124,16 +3124,16 @@ LinePainter.prototype.draw = function( ctx, width, height ) { var - in_path = false, - min_value = this.min_value, - max_value = this.max_value, - vertical_range = max_value - min_value, - height_px = height, - view_start = this.view_start, - view_range = this.view_end - this.view_start, - w_scale = width / view_range, - mode = this.mode, - data = this.data; + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; ctx.save(); @@ -3142,72 +3142,72 @@ // Line at 0.0 if ( mode !== "Intensity" ) { - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( width, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); } ctx.beginPath(); ctx.fillStyle = this.color; var x_scaled, y, delta_x_px; if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); } else { - delta_x_px = 10; + delta_x_px = 10; } for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - view_start) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } } if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); } else { - ctx.stroke(); + ctx.stroke(); } ctx.restore(); -} \ No newline at end of file +} http://bitbucket.org/galaxy/galaxy-central/changeset/41eabf97d186/ changeset: r5303:41eabf97d186 user: james_taylor date: 2011-03-31 00:30:22 summary: Extracted feature painters for generic linked features, VCF, and reads. Not all types have been completely tested yet affected #: 1 file (3.1 KB) --- a/static/scripts/trackster.js Wed Mar 30 13:09:42 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 18:30:22 2011 -0400 @@ -1960,7 +1960,7 @@ var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return $(canvas); + return canvas; } this.content_div.css("height", "0px"); } @@ -2100,7 +2100,7 @@ this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); painter.draw( ctx, width, height ); - return $(canvas); + return canvas; } }); @@ -2150,6 +2150,8 @@ this.tile_cache = new Cache(CACHED_TILES_FEATURE); this.data_cache = new DataManager(20, this); this.left_offset = 200; + + this.painter = LinkedFeaturePainter; }; $.extend(FeatureTrack.prototype, TiledTrack.prototype, { /** @@ -2174,141 +2176,6 @@ return inc_slots.slot_features( features ); }, /** - * Draw feature. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { - var - feature_uid = feature[0], - feature_start = feature[1], - // -1 b/c intervals are half-open. - feature_end = feature[2] - 1, - feature_name = feature[3], - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, - thickness, y_start, thick_start = null, thick_end = null, - block_color = this.prefs.block_color, - label_color = this.prefs.label_color; - - // Dense mode displays the same for all data. - if (mode === "Dense") { - ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); - } - else if (mode === "no_detail") { - // No details for feature, so only one way to display. - ctx.fillStyle = block_color; - // TODO: what should width be here? - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); - } - else { // Mode is either Squish or Pack: - // Feature details. - var feature_strand = feature[4], - feature_ts = feature[5], - feature_te = feature[6], - feature_blocks = feature[7]; - - if (feature_ts && feature_te) { - thick_start = Math.floor( Math.max(0, (feature_ts - tile_low) * w_scale) ); - thick_end = Math.ceil( Math.min(width, Math.max(0, (feature_te - tile_low) * w_scale)) ); - } - - // Set vars that depend on mode. - var thin_height, thick_height; - if (mode === "Squish") { - thin_height = 1; - thick_height = SQUISH_FEATURE_HEIGHT; - } else { // mode === "Pack" - thin_height = 5; - thick_height = PACK_FEATURE_HEIGHT; - } - - // Draw feature/feature blocks + connectors. - if (!feature_blocks) { - // If there are no blocks, treat the feature as one big exon. - if ( feature.strand ) { - if (feature.strand === "+") { - ctx.fillStyle = RIGHT_STRAND_INV; - } else if (feature.strand === "-") { - ctx.fillStyle = LEFT_STRAND_INV; - } - } - else { // No strand. - ctx.fillStyle = block_color; - } - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thick_height); - } else { - // There are feature blocks and mode is either Squish or Pack. - // - // Approach: (a) draw whole feature as connector/intron and (b) draw blocks as - // needed. This ensures that whole feature, regardless of whether it starts with - // a block, is visible. - // - - // Draw whole feature as connector/intron. - var cur_y_center, cur_height; - if (mode === "Squish") { - ctx.fillStyle = CONNECTOR_COLOR; - cur_y_center = y_center + Math.floor(SQUISH_FEATURE_HEIGHT/2) + 1; - cur_height = 1; - } - else { // mode === "Pack" - if (feature_strand) { - var cur_y_center = y_center; - var cur_height = thick_height; - if (feature_strand === "+") { - ctx.fillStyle = RIGHT_STRAND; - } else if (feature_strand === "-") { - ctx.fillStyle = LEFT_STRAND; - } - } - else { - ctx.fillStyle = CONNECTOR_COLOR; - cur_y_center += (SQUISH_FEATURE_HEIGHT/2) + 1; - cur_height = 1; - } - } - ctx.fillRect(f_start + left_offset, cur_y_center, f_end - f_start, cur_height); - - for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { - var block = feature_blocks[k], - block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), - // -1 b/c intervals are half-open. - block_end = Math.ceil( Math.min(width, Math.max((block[1] - 1 - tile_low) * w_scale)) ); - - // Skip drawing if block not on tile. - if (block_start > block_end) { continue; } - - // Draw thin block. - ctx.fillStyle = block_color; - ctx.fillRect(block_start + left_offset, y_center + (thick_height-thin_height)/2 + 1, - block_end - block_start, thin_height); - - // If block intersects with thick region, draw block as thick. - if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { - var block_thick_start = Math.max(block_start, thick_start), - block_thick_end = Math.min(block_end, thick_end); - ctx.fillRect(block_thick_start + left_offset, y_center + 1, - block_thick_end - block_thick_start, thick_height); - } - } - } - - // Draw label for Pack mode. - if (mode === "Pack" && feature_start > tile_low) { - ctx.fillStyle = label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING, y_center + 8); - } - ctx.fillStyle = block_color; - } - } - }, - /** * Returns y_scale based on mode. */ get_y_scale: function(mode) { @@ -2394,27 +2261,44 @@ // Deal with left_offset by translating ctx.translate( left_offset, 0 ); painter.draw( ctx, width, required_height ); - // Wrapped canvas element is returned - return $(canvas); + // Canvas element is returned + return canvas; } // Start dealing with row-by-row tracks - - // y_scale is the height per row - var y_scale = this.get_y_scale(mode); - - // Pack reads, set required height. - if (mode === "Dense") { - required_height = min_height; - } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { - // Calculate new slots incrementally for this new chunk of data and update height if necessary. - required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; + + // If working with a mode where slotting is neccesary, update the incremental slotting + var slots, slots_required = 1; + if ( mode === "no_detail" || mode === "Squish" || mode === "Pack" ) { + slots_required = this.incremental_slots(w_scale, result.data, mode); slots = this.inc_slots[w_scale].slots; } + + // Filter features + var filtered = []; + if ( result.data ) { + for (var i = 0, len = result.data.length; i < len; i++) { + var feature = result.data[i]; + var hide_feature = false; + var filter; + for (var f = 0, flen = this.filters.length; f < flen; f++) { + filter = this.filters[f]; + filter.update_attrs(feature); + if (!filter.keep(feature)) { + hide_feature = true; + break; + } + } + if (!hide_feature) { + filtered.push( feature ); + } + } + } - // - // Set up for drawing. - // + // Create painter, and canvas of sufficient size to contain all features + // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument + var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); + var required_height = painter.get_required_height( slots_required ); var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width + left_offset; @@ -2444,112 +2328,29 @@ // If there's no data, return. if (!result.data) { - return $(canvas); + return canvas; } } - // // Set example feature. This is needed so that track can update its UI based on feature attributes. - // this.example_feature = (result.data.length ? result.data[0] : undefined); - - // - // Draw elements. - // - var data = result.data; - for (var i = 0, len = data.length; i < len; i++) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - // Slot valid only if features are slotted and this feature is slotted; - // feature may not be due to lack of space. - slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - - // Apply filters to feature. - var hide_feature = false; - var filter; - for (var f = 0; f < this.filters.length; f++) { - filter = this.filters[f]; - filter.update_attrs(feature); - if (!filter.keep(feature)) { - hide_feature = true; - break; - } - } - if (hide_feature) { - continue; - } + + // Draw features + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height, slots ); - // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). - if (is_overlap([feature_start, feature_end], [tile_low, tile_high]) && (mode == "Dense" || slot !== null)) { - this.draw_element(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, - width, left_offset, ref_seq); - } - } - return $(canvas); + return canvas; } }); var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); this.track_type = "VcfTrack"; + this.painter = VariantPainter; }; -$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Draw a VCF entry. - */ - draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - // -1 b/c intervals are half-open. - feature_end = feature[2] - 1, - feature_name = feature[3], - // All features need a start, end, and vertical center. - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, - thickness, y_start, thick_start = null, thick_end = null; - - if (no_label) { - ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); - } else { // Show blocks, labels, etc. - // Unpack. - var ref_base = feature[4], alt_base = feature[5], qual = feature[6]; - - // Draw block for entry. - thickness = 9; - y_start = 1; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thickness); - - // Add label for entry. - if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { - // Draw label - ctx.fillStyle = label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); - } - ctx.fillStyle = block_color; - } - - // Show additional data on block. - var vcf_label = ref_base + " / " + alt_base; - if (feature_start > tile_low && ctx.measureText(vcf_label).width < (f_end - f_start)) { - ctx.fillStyle = "white"; - ctx.textAlign = "center"; - ctx.fillText(vcf_label, left_offset + f_start + (f_end-f_start)/2, y_center + 8); - ctx.fillStyle = block_color; - } - } - } -}); +$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); @@ -2573,281 +2374,10 @@ this.prefs = this.track_config.values; this.track_type = "ReadTrack"; + this.painter = ReadPainter; this.make_name_popup_menu(); }; -$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Returns y_scale based on mode. - */ - get_y_scale: function(mode) { - var y_scale; - if (mode === "summary_tree") { - // No scale needed. - } - if (mode === "Dense") { - y_scale = DENSE_TRACK_HEIGHT; - } - else if (mode === "Squish") { - y_scale = SQUISH_TRACK_HEIGHT; - } - else { // mode === "Pack" - y_scale = PACK_TRACK_HEIGHT; - if (this.track_config.values.show_insertions) { - y_scale *= 2; - } - } - return y_scale; - }, - /** - * Draw a single read. - */ - draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center, ref_seq) { - ctx.textAlign = "center"; - var track = this, - tile_region = [tile_low, tile_high], - base_offset = 0, - seq_offset = 0, - gap = 0; - - // Keep list of items that need to be drawn on top of initial drawing layer. - var draw_last = []; - - // Gap is needed so that read is offset and hence first base can be drawn on read. - // TODO-FIX: using this gap offsets reads so that their start is not visually in sync with other tracks. - if ((mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - gap = Math.round(w_scale/2); - } - if (!cigar) { - // If no cigar string, then assume all matches - cigar = [ [0, orig_seq.length] ] - } - for (var cig_id = 0, len = cigar.length; cig_id < len; cig_id++) { - var cig = cigar[cig_id], - cig_op = "MIDNSHP=X"[cig[0]], - cig_len = cig[1]; - - if (cig_op === "H" || cig_op === "S") { - // Go left if it clips - base_offset -= cig_len; - } - var seq_start = feature_start + base_offset, - s_start = Math.floor( Math.max(0, (seq_start - tile_low) * w_scale) ), - s_end = Math.floor( Math.max(0, (seq_start + cig_len - tile_low) * w_scale) ); - - switch (cig_op) { - case "H": // Hard clipping. - // TODO: draw anything? - // Sequence not present, so do not increment seq_offset. - break; - case "S": // Soft clipping. - case "M": // Match. - case "=": // Equals. - var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region); - if (seq_tile_overlap !== NO_OVERLAP) { - // Draw. - var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); - if (gap > 0) { - ctx.fillStyle = this.prefs.block_color; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); - ctx.fillStyle = CONNECTOR_COLOR; - // TODO: this can be made much more efficient by computing the complete sequence - // to draw and then drawing it. - for (var c = 0, str_len = seq.length; c < str_len; c++) { - if (this.track_config.values.show_differences && ref_seq) { - var ref_char = ref_seq[seq_start - tile_low + c]; - if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { - continue; - } - } - if (seq_start + c >= tile_low && seq_start + c <= tile_high) { - var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); - } - } - } else { - ctx.fillStyle = this.prefs.block_color; - // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(s_start + this.left_offset, - y_center + (this.mode !== "Dense" ? 4 : 5), - s_end - s_start, - (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); - } - } - seq_offset += cig_len; - base_offset += cig_len; - break; - case "N": // Skipped bases. - ctx.fillStyle = CONNECTOR_COLOR; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); - //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); - // No change in seq_offset because sequence not used when skipping. - base_offset += cig_len; - break; - case "D": // Deletion. - ctx.fillStyle = "red"; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); - // TODO: is this true? No change in seq_offset because sequence not used when skipping. - base_offset += cig_len; - break; - case "P": // TODO: No good way to draw insertions/padding right now, so ignore - // Sequences not present, so do not increment seq_offset. - break; - case "I": // Insertion. - // Check to see if sequence should be drawn at all by looking at the overlap between - // the sequence region and the tile region. - var - seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), - insert_x_coord = this.left_offset + s_start - gap; - - if (seq_tile_overlap !== NO_OVERLAP) { - var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); - // Insertion point is between the sequence start and the previous base: (-gap) moves - // back from sequence start to insertion point. - if (this.track_config.values.show_insertions) { - // - // Show inserted sequence above, centered on insertion point. - // - - // Draw sequence. - // X center is offset + start - <half_sequence_length> - var x_center = this.left_offset + s_start - (s_end - s_start)/2; - if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - // Draw sequence container. - ctx.fillStyle = "yellow"; - ctx.fillRect(x_center - gap, y_center - 9, s_end - s_start, 9); - draw_last[draw_last.length] = {type: "triangle", data: [insert_x_coord, y_center + 4, 5]}; - ctx.fillStyle = CONNECTOR_COLOR; - // Based on overlap b/t sequence and tile, get sequence to be drawn. - switch(seq_tile_overlap) { - case(OVERLAP_START): - seq = seq.slice(tile_low-seq_start); - break; - case(OVERLAP_END): - seq = seq.slice(0, seq_start-tile_high); - break; - case(CONTAINED_BY): - // All of sequence drawn. - break; - case(CONTAINS): - seq = seq.slice(tile_low-seq_start, seq_start-tile_high); - break; - } - // Draw sequence. - for (var c = 0, str_len = seq.length; c < str_len; c++) { - var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); - } - } - else { - // Draw block. - ctx.fillStyle = "yellow"; - // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(x_center, y_center + (this.mode !== "Dense" ? 2 : 5), - s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT)); - } - } - else { - if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - // Show insertions with a single number at the insertion point. - draw_last[draw_last.length] = {type: "text", data: [seq.length, insert_x_coord, y_center + 9]}; - } - else { - // TODO: probably can merge this case with code above. - } - } - } - seq_offset += cig_len; - // No change to base offset because insertions are drawn above sequence/read. - break; - case "X": - // TODO: draw something? - seq_offset += cig_len; - break; - } - } - - // - // Draw last items. - // - ctx.fillStyle = "yellow"; - var item, type, data; - for (var i = 0; i < draw_last.length; i++) { - item = draw_last[i]; - type = item["type"]; - data = item["data"]; - if (type === "text") { - ctx.font = "bold " + DEFAULT_FONT; - ctx.fillText(data[0], data[1], data[2]); - ctx.font = DEFAULT_FONT; - } - else if (type == "triangle") { - ctx.drawDownwardEquilateralTriangle(data[0], data[1], data[2]); - } - } - }, - /** - * Draw a complete read. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset, ref_seq) { - // All features need a start, end, and vertical center. - var feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = (mode === "Dense" ? 1 : (1 + slot)) * y_scale, - block_color = this.prefs.block_color, - label_color = this.prefs.label_color, - // Left-gap for label text since we align chrom text to the position tick. - gap = 0; - - // TODO: fix gap usage; also see note on gap in draw_read. - if ((mode === "Pack" || this.mode === "Auto") && w_scale > CHAR_WIDTH_PX) { - var gap = Math.round(w_scale/2); - } - - // Draw read. - ctx.fillStyle = block_color; - if (feature[5] instanceof Array) { - // Read is paired. - var b1_start = Math.floor( Math.max(0, (feature[4][0] - tile_low) * w_scale) ), - b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[4][1] - tile_low) * w_scale)) ), - b2_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ), - b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ); - - // Draw left/forward read. - if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center, ref_seq); - } - // Draw right/reverse read. - if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center, ref_seq); - } - // Draw connector. - if (b2_start > b1_end) { - ctx.fillStyle = CONNECTOR_COLOR; - ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); - } - } else { - // Read is single. - ctx.fillStyle = block_color; - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center, ref_seq); - } - if (mode === "Pack" && feature_start > tile_low) { - // Draw label. - ctx.fillStyle = this.prefs.label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); - } - ctx.fillStyle = block_color; - } - } -}); +$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); /** * Feature track that displays data generated from tool. @@ -2890,6 +2420,18 @@ // ---- To be extracted ------------------------------------------------------ +// ---- Simple extend function for inheritence ---- + +var extend = function() { + var target = arguments[0]; + for ( var i = 1; i < arguments.length; i++ ) { + var other = arguments[i]; + for ( key in other ) { + target[key] = other[key]; + } + } +} + // ---- Canvas management ---- var CanvasManager = function( document ) { @@ -3211,3 +2753,561 @@ ctx.restore(); } + +var FeaturePainter = function( data, view_start, view_end, prefs, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + this.prefs = prefs; + this.mode = mode; +} + +extend( FeaturePainter.prototype, { + + get_required_height: function( rows_required ) { + // y_scale is the height per row + var required_height = y_scale = this.get_row_height(), mode = this.mode; + // If using a packing mode, need to multiply by the number of slots used + if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + // Calculate new slots incrementally for this new chunk of data and update height if necessary. + required_height = rows_required * y_scale; + } + return required_height; + }, + + draw: function( ctx, width, height, slots ) { + + var data = this.data, view_start = this.view_start, view_end = this.view_end; + + ctx.save(); + + ctx.fillStyle = this.prefs.block_color; + ctx.textAlign = "right"; + + var view_range = this.view_end - this.view_start, + w_scale = width / view_range, + y_scale = this.get_row_height(); + + for (var i = 0, len = data.length; i < len; i++) { + var feature = data[i], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + // Slot valid only if features are slotted and this feature is slotted; + // feature may not be due to lack of space. + slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); + + // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). + if (is_overlap([feature_start, feature_end], [view_start, view_end]) && (this.mode == "Dense" || slot !== null)) { + this.draw_element(ctx, this.mode, feature, slot, view_start, view_end, w_scale, y_scale, + width ); + } + } + + ctx.restore(); + } +}); + +var LinkedFeaturePainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( LinkedFeaturePainter.prototype, FeaturePainter.prototype, { + + /** + * Height of a single row, depends on mode + */ + get_row_height: function() { + var mode = this.mode, y_scale; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; + }, + + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { + var + feature_uid = feature[0], + feature_start = feature[1], + // -1 b/c intervals are half-open. + feature_end = feature[2] - 1, + feature_name = feature[3], + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + thickness, y_start, thick_start = null, thick_end = null, + block_color = this.prefs.block_color, + label_color = this.prefs.label_color; + + // Dense mode displays the same for all data. + if (mode === "Dense") { + ctx.fillStyle = block_color; + ctx.fillRect(f_start, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); + } + else if (mode === "no_detail") { + // No details for feature, so only one way to display. + ctx.fillStyle = block_color; + // TODO: what should width be here? + ctx.fillRect(f_start, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); + } + else { // Mode is either Squish or Pack: + // Feature details. + var feature_strand = feature[4], + feature_ts = feature[5], + feature_te = feature[6], + feature_blocks = feature[7]; + + if (feature_ts && feature_te) { + thick_start = Math.floor( Math.max(0, (feature_ts - tile_low) * w_scale) ); + thick_end = Math.ceil( Math.min(width, Math.max(0, (feature_te - tile_low) * w_scale)) ); + } + + // Set vars that depend on mode. + var thin_height, thick_height; + if (mode === "Squish") { + thin_height = 1; + thick_height = SQUISH_FEATURE_HEIGHT; + } else { // mode === "Pack" + thin_height = 5; + thick_height = PACK_FEATURE_HEIGHT; + } + + // Draw feature/feature blocks + connectors. + if (!feature_blocks) { + // If there are no blocks, treat the feature as one big exon. + if ( feature.strand ) { + if (feature.strand === "+") { + ctx.fillStyle = RIGHT_STRAND_INV; + } else if (feature.strand === "-") { + ctx.fillStyle = LEFT_STRAND_INV; + } + } + else { // No strand. + ctx.fillStyle = block_color; + } + ctx.fillRect(f_start, y_center, f_end - f_start, thick_height); + } else { + // There are feature blocks and mode is either Squish or Pack. + // + // Approach: (a) draw whole feature as connector/intron and (b) draw blocks as + // needed. This ensures that whole feature, regardless of whether it starts with + // a block, is visible. + // + + // Draw whole feature as connector/intron. + var cur_y_center, cur_height; + if (mode === "Squish") { + ctx.fillStyle = CONNECTOR_COLOR; + cur_y_center = y_center + Math.floor(SQUISH_FEATURE_HEIGHT/2) + 1; + cur_height = 1; + } + else { // mode === "Pack" + if (feature_strand) { + var cur_y_center = y_center; + var cur_height = thick_height; + if (feature_strand === "+") { + ctx.fillStyle = RIGHT_STRAND; + } else if (feature_strand === "-") { + ctx.fillStyle = LEFT_STRAND; + } + } + else { + ctx.fillStyle = CONNECTOR_COLOR; + cur_y_center += (SQUISH_FEATURE_HEIGHT/2) + 1; + cur_height = 1; + } + } + ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); + + for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { + var block = feature_blocks[k], + block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), + // -1 b/c intervals are half-open. + block_end = Math.ceil( Math.min(width, Math.max((block[1] - 1 - tile_low) * w_scale)) ); + + // Skip drawing if block not on tile. + if (block_start > block_end) { continue; } + + // Draw thin block. + ctx.fillStyle = block_color; + ctx.fillRect(block_start, y_center + (thick_height-thin_height)/2 + 1, + block_end - block_start, thin_height); + + // If block intersects with thick region, draw block as thick. + if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { + var block_thick_start = Math.max(block_start, thick_start), + block_thick_end = Math.min(block_end, thick_end); + ctx.fillRect(block_thick_start, y_center + 1, + block_thick_end - block_thick_start, thick_height); + } + } + } + + // Draw label for Pack mode. + if (mode === "Pack" && feature_start > tile_low) { + ctx.fillStyle = label_color; + // FIXME: do this without tile_index + var tile_index = 1; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + LABEL_SPACING, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start - LABEL_SPACING, y_center + 8); + } + ctx.fillStyle = block_color; + } + } + } +}); + + +var VariantPainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( VariantPainter.prototype, FeaturePainter.prototype, { + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { + var feature = data[i], + feature_uid = feature[0], + feature_start = feature[1], + // -1 b/c intervals are half-open. + feature_end = feature[2] - 1, + feature_name = feature[3], + // All features need a start, end, and vertical center. + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + thickness, y_start, thick_start = null, thick_end = null; + + if (no_label) { + ctx.fillStyle = block_color; + ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); + } else { // Show blocks, labels, etc. + // Unpack. + var ref_base = feature[4], alt_base = feature[5], qual = feature[6]; + + // Draw block for entry. + thickness = 9; + y_start = 1; + ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thickness); + + // Add label for entry. + if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { + // Draw label + ctx.fillStyle = label_color; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); + } + ctx.fillStyle = block_color; + } + + // Show additional data on block. + var vcf_label = ref_base + " / " + alt_base; + if (feature_start > tile_low && ctx.measureText(vcf_label).width < (f_end - f_start)) { + ctx.fillStyle = "white"; + ctx.textAlign = "center"; + ctx.fillText(vcf_label, left_offset + f_start + (f_end-f_start)/2, y_center + 8); + ctx.fillStyle = block_color; + } + } + } +}); + +var ReadPainter = function( data, view_start, view_end, prefs, mode, ref_seq ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); + this.ref_seq = ref_seq; +} + +extend( ReadPainter.prototype, FeaturePainter.prototype, { + /** + * Returns y_scale based on mode. + */ + get_row_height: function() { + var y_scale, mode = this.mode; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + if (this.track_config.values.show_insertions) { + y_scale *= 2; + } + } + return y_scale; + }, + /** + * Draw a single read. + */ + draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center) { + ctx.textAlign = "center"; + var track = this, + tile_region = [tile_low, tile_high], + base_offset = 0, + seq_offset = 0, + gap = 0 + ref_seq = this.ref_seq; + + // Keep list of items that need to be drawn on top of initial drawing layer. + var draw_last = []; + + // Gap is needed so that read is offset and hence first base can be drawn on read. + // TODO-FIX: using this gap offsets reads so that their start is not visually in sync with other tracks. + if ((mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + gap = Math.round(w_scale/2); + } + if (!cigar) { + // If no cigar string, then assume all matches + cigar = [ [0, orig_seq.length] ] + } + for (var cig_id = 0, len = cigar.length; cig_id < len; cig_id++) { + var cig = cigar[cig_id], + cig_op = "MIDNSHP=X"[cig[0]], + cig_len = cig[1]; + + if (cig_op === "H" || cig_op === "S") { + // Go left if it clips + base_offset -= cig_len; + } + var seq_start = feature_start + base_offset, + s_start = Math.floor( Math.max(0, (seq_start - tile_low) * w_scale) ), + s_end = Math.floor( Math.max(0, (seq_start + cig_len - tile_low) * w_scale) ); + + switch (cig_op) { + case "H": // Hard clipping. + // TODO: draw anything? + // Sequence not present, so do not increment seq_offset. + break; + case "S": // Soft clipping. + case "M": // Match. + case "=": // Equals. + var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region); + if (seq_tile_overlap !== NO_OVERLAP) { + // Draw. + var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); + if (gap > 0) { + ctx.fillStyle = this.prefs.block_color; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); + ctx.fillStyle = CONNECTOR_COLOR; + // TODO: this can be made much more efficient by computing the complete sequence + // to draw and then drawing it. + for (var c = 0, str_len = seq.length; c < str_len; c++) { + if (this.track_config.values.show_differences && ref_seq) { + var ref_char = ref_seq[seq_start - tile_low + c]; + if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { + continue; + } + } + if (seq_start + c >= tile_low && seq_start + c <= tile_high) { + var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); + ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); + } + } + } else { + ctx.fillStyle = this.prefs.block_color; + // TODO: This is a pretty hack-ish way to fill rectangle based on mode. + ctx.fillRect(s_start + this.left_offset, + y_center + (this.mode !== "Dense" ? 4 : 5), + s_end - s_start, + (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); + } + } + seq_offset += cig_len; + base_offset += cig_len; + break; + case "N": // Skipped bases. + ctx.fillStyle = CONNECTOR_COLOR; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); + //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); + // No change in seq_offset because sequence not used when skipping. + base_offset += cig_len; + break; + case "D": // Deletion. + ctx.fillStyle = "red"; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); + // TODO: is this true? No change in seq_offset because sequence not used when skipping. + base_offset += cig_len; + break; + case "P": // TODO: No good way to draw insertions/padding right now, so ignore + // Sequences not present, so do not increment seq_offset. + break; + case "I": // Insertion. + // Check to see if sequence should be drawn at all by looking at the overlap between + // the sequence region and the tile region. + var + seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), + insert_x_coord = this.left_offset + s_start - gap; + + if (seq_tile_overlap !== NO_OVERLAP) { + var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); + // Insertion point is between the sequence start and the previous base: (-gap) moves + // back from sequence start to insertion point. + if (this.track_config.values.show_insertions) { + // + // Show inserted sequence above, centered on insertion point. + // + + // Draw sequence. + // X center is offset + start - <half_sequence_length> + var x_center = this.left_offset + s_start - (s_end - s_start)/2; + if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + // Draw sequence container. + ctx.fillStyle = "yellow"; + ctx.fillRect(x_center - gap, y_center - 9, s_end - s_start, 9); + draw_last[draw_last.length] = {type: "triangle", data: [insert_x_coord, y_center + 4, 5]}; + ctx.fillStyle = CONNECTOR_COLOR; + // Based on overlap b/t sequence and tile, get sequence to be drawn. + switch(seq_tile_overlap) { + case(OVERLAP_START): + seq = seq.slice(tile_low-seq_start); + break; + case(OVERLAP_END): + seq = seq.slice(0, seq_start-tile_high); + break; + case(CONTAINED_BY): + // All of sequence drawn. + break; + case(CONTAINS): + seq = seq.slice(tile_low-seq_start, seq_start-tile_high); + break; + } + // Draw sequence. + for (var c = 0, str_len = seq.length; c < str_len; c++) { + var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); + ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); + } + } + else { + // Draw block. + ctx.fillStyle = "yellow"; + // TODO: This is a pretty hack-ish way to fill rectangle based on mode. + ctx.fillRect(x_center, y_center + (this.mode !== "Dense" ? 2 : 5), + s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT)); + } + } + else { + if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + // Show insertions with a single number at the insertion point. + draw_last[draw_last.length] = {type: "text", data: [seq.length, insert_x_coord, y_center + 9]}; + } + else { + // TODO: probably can merge this case with code above. + } + } + } + seq_offset += cig_len; + // No change to base offset because insertions are drawn above sequence/read. + break; + case "X": + // TODO: draw something? + seq_offset += cig_len; + break; + } + } + + // + // Draw last items. + // + ctx.fillStyle = "yellow"; + var item, type, data; + for (var i = 0; i < draw_last.length; i++) { + item = draw_last[i]; + type = item["type"]; + data = item["data"]; + if (type === "text") { + ctx.font = "bold " + DEFAULT_FONT; + ctx.fillText(data[0], data[1], data[2]); + ctx.font = DEFAULT_FONT; + } + else if (type == "triangle") { + ctx.drawDownwardEquilateralTriangle(data[0], data[1], data[2]); + } + } + }, + /** + * Draw a complete read pair + */ + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { + // All features need a start, end, and vertical center. + var feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = (mode === "Dense" ? 1 : (1 + slot)) * y_scale, + block_color = this.prefs.block_color, + label_color = this.prefs.label_color, + // Left-gap for label text since we align chrom text to the position tick. + gap = 0; + + // TODO: fix gap usage; also see note on gap in draw_read. + if ((mode === "Pack" || this.mode === "Auto") && w_scale > CHAR_WIDTH_PX) { + var gap = Math.round(w_scale/2); + } + + // Draw read. + ctx.fillStyle = block_color; + if (feature[5] instanceof Array) { + // Read is paired. + var b1_start = Math.floor( Math.max(0, (feature[4][0] - tile_low) * w_scale) ), + b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[4][1] - tile_low) * w_scale)) ), + b2_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ), + b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ); + + // Draw left/forward read. + if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center); + } + // Draw right/reverse read. + if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center); + } + // Draw connector. + if (b2_start > b1_end) { + ctx.fillStyle = CONNECTOR_COLOR; + ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); + } + } else { + // Read is single. + ctx.fillStyle = block_color; + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center); + } + if (mode === "Pack" && feature_start > tile_low) { + // Draw label. + ctx.fillStyle = this.prefs.label_color; + // FIXME: eliminate tile_index + var tile_index = 1; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); + } + ctx.fillStyle = block_color; + } + } +}); http://bitbucket.org/galaxy/galaxy-central/changeset/c4961c6e7a4c/ changeset: r5304:c4961c6e7a4c user: james_taylor date: 2011-03-31 00:30:35 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 0 files (0 bytes) http://bitbucket.org/galaxy/galaxy-central/changeset/4be86b204759/ changeset: r5305:4be86b204759 user: james_taylor date: 2011-03-31 19:05:24 summary: Remove left_offset stuff from ReadTracks, fix some other stuff related to padding and height calculation, read tracks now work fine affected #: 1 file (134 bytes) --- a/static/scripts/trackster.js Wed Mar 30 18:30:35 2011 -0400 +++ b/static/scripts/trackster.js Thu Mar 31 13:05:24 2011 -0400 @@ -2298,7 +2298,8 @@ // Create painter, and canvas of sufficient size to contain all features // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); - var required_height = painter.get_required_height( slots_required ); + // FIXME: ERROR_PADDING is an ugly gap most of the time + var required_height = painter.get_required_height( slots_required ) + ERROR_PADDING; var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width + left_offset; @@ -2336,7 +2337,7 @@ this.example_feature = (result.data.length ? result.data[0] : undefined); // Draw features - ctx.translate( left_offset, 0 ); + ctx.translate( left_offset, ERROR_PADDING ); painter.draw( ctx, width, required_height, slots ); return canvas; @@ -2772,7 +2773,8 @@ // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = rows_required * y_scale; } - return required_height; + // Pad bottom by half a row + return required_height + Math.round( y_scale / 2 ); }, draw: function( ctx, width, height, slots ) { @@ -2846,11 +2848,11 @@ feature_name = feature[3], f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null, block_color = this.prefs.block_color, label_color = this.prefs.label_color; - + // Dense mode displays the same for all data. if (mode === "Dense") { ctx.fillStyle = block_color; @@ -2989,7 +2991,7 @@ // All features need a start, end, and vertical center. f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null; if (no_label) { @@ -3052,7 +3054,7 @@ } else { // mode === "Pack" y_scale = PACK_TRACK_HEIGHT; - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { y_scale *= 2; } } @@ -3109,12 +3111,12 @@ var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); if (gap > 0) { ctx.fillStyle = this.prefs.block_color; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); + ctx.fillRect(s_start - gap, y_center + 1, s_end - s_start, 9); ctx.fillStyle = CONNECTOR_COLOR; // TODO: this can be made much more efficient by computing the complete sequence // to draw and then drawing it. for (var c = 0, str_len = seq.length; c < str_len; c++) { - if (this.track_config.values.show_differences && ref_seq) { + if (this.prefs.show_differences && ref_seq) { var ref_char = ref_seq[seq_start - tile_low + c]; if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { continue; @@ -3122,13 +3124,13 @@ } if (seq_start + c >= tile_low && seq_start + c <= tile_high) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); + ctx.fillText(seq[c], c_start, y_center + 9); } } } else { ctx.fillStyle = this.prefs.block_color; // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(s_start + this.left_offset, + ctx.fillRect(s_start, y_center + (this.mode !== "Dense" ? 4 : 5), s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); @@ -3139,14 +3141,14 @@ break; case "N": // Skipped bases. ctx.fillStyle = CONNECTOR_COLOR; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); + ctx.fillRect(s_start - gap, y_center + 5, s_end - s_start, 1); //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); // No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; case "D": // Deletion. ctx.fillStyle = "red"; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); + ctx.fillRect(s_start - gap, y_center + 4, s_end - s_start, 3); // TODO: is this true? No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; @@ -3158,20 +3160,20 @@ // the sequence region and the tile region. var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), - insert_x_coord = this.left_offset + s_start - gap; + insert_x_coord = s_start - gap; if (seq_tile_overlap !== NO_OVERLAP) { var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); // Insertion point is between the sequence start and the previous base: (-gap) moves // back from sequence start to insertion point. - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { // // Show inserted sequence above, centered on insertion point. // // Draw sequence. // X center is offset + start - <half_sequence_length> - var x_center = this.left_offset + s_start - (s_end - s_start)/2; + var x_center = s_start - (s_end - s_start)/2; if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { // Draw sequence container. ctx.fillStyle = "yellow"; @@ -3196,7 +3198,7 @@ // Draw sequence. for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); + ctx.fillText(seq[c], c_start - (s_end - s_start)/2, y_center); } } else { @@ -3288,7 +3290,7 @@ // Draw connector. if (b2_start > b1_end) { ctx.fillStyle = CONNECTOR_COLOR; - ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); + ctx.dashedLine(b1_end - gap, y_center + 5, b2_start - gap, y_center + 5); } } else { // Read is single. @@ -3302,10 +3304,10 @@ var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_end + LABEL_SPACING - gap, y_center + 8); } else { ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_start - LABEL_SPACING - gap, y_center + 8); } ctx.fillStyle = block_color; } http://bitbucket.org/galaxy/galaxy-central/changeset/88e924542585/ changeset: r5306:88e924542585 user: james_taylor date: 2011-03-29 02:32:43 summary: Extract feature slotting into a new class affected #: 1 file (648 bytes) --- a/static/scripts/trackster.js Mon Mar 28 15:27:14 2011 -0400 +++ b/static/scripts/trackster.js Mon Mar 28 20:32:43 2011 -0400 @@ -2232,150 +2232,18 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } - } - - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; - - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } - } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } - } - */ - return highest_slot + 1; + + return inc_slots.slot_features( features ); }, /** * Draw summary tree on canvas. @@ -2637,7 +2505,7 @@ else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; + slots = this.inc_slots[w_scale].slots; } // @@ -3126,4 +2994,154 @@ } }); +// ---- To be extracted ------------------------------------------------------ +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} http://bitbucket.org/galaxy/galaxy-central/changeset/643539b68e70/ changeset: r5307:643539b68e70 user: james_taylor date: 2011-03-29 03:35:29 summary: Extract SummaryTree painter, abstract canvas creation into CanvasManager class affected #: 1 file (627 bytes) --- a/static/scripts/trackster.js Mon Mar 28 20:32:43 2011 -0400 +++ b/static/scripts/trackster.js Mon Mar 28 21:35:29 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1945,20 +1946,17 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return canvas; + return $(canvas); } this.content_div.css("height", "0px"); } @@ -2249,38 +2247,17 @@ * Draw summary tree on canvas. */ draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); + // Add label to container div when displaying summary tree mode var max_label = $("<div />").addClass('yaxislabel'); max_label.text(max); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); max_label.prependTo(this.container_div); - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } + // Paint summary tree into canvas + var ctx = canvas.getContext("2d"); + var painter = new SummaryTreePainter( points, delta, max, this.prefs.show_counts ); + painter.draw( ctx, w_scale, required_height, tile_low, left_offset ); }, /** * Draw feature. @@ -2459,10 +2436,6 @@ left_offset = this.left_offset, slots, required_height; - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - // // Set mode if Auto. // @@ -2511,15 +2484,17 @@ // // Set up for drawing. // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; + var canvas = this.view.canvas_manager.new_canvas(); + + canvas.width = width + left_offset; + canvas.height = required_height; if (result.dataset_type === "summary_tree") { // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; + canvas.height += LABEL_SPACING + CHAR_HEIGHT_PX; } parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); + var ctx = canvas.getContext("2d"); ctx.fillStyle = this.prefs.block_color; ctx.font = this.default_font; ctx.textAlign = "right"; @@ -2531,14 +2506,14 @@ if (mode === "summary_tree") { this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, tile_low, left_offset); - return canvas; + return $(canvas); } // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { - canvas.css({ + $(canvas).css({ "border-top": "1px solid red" }); @@ -2551,7 +2526,7 @@ // If there's no data, return. if (!result.data) { - return canvas; + return $(canvas); } } @@ -2594,7 +2569,7 @@ width, left_offset, ref_seq); } } - return canvas; + return $(canvas); } }); @@ -2996,6 +2971,20 @@ // ---- To be extracted ------------------------------------------------------ +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + /** * FeatureSlotter determines slots in which to draw features for vertical * packing. @@ -3145,3 +3134,50 @@ */ return highest_slot + 1; } + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, show_counts ) { + this.data = data; + this.delta = delta; + this.max = max; + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, w_scale, required_height, tile_low, left_offset ) { + + var + points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + var x = Math.floor( (points[i][0] - tile_low) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * required_height; + + ctx.fillStyle = "black"; + ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + http://bitbucket.org/galaxy/galaxy-central/changeset/183482bc7c2b/ changeset: r5308:183482bc7c2b user: james_taylor date: 2011-03-29 19:56:44 summary: Extract LinePainter from LineTrack affected #: 1 file (281 bytes) --- a/static/scripts/trackster.js Mon Mar 28 21:35:29 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 13:56:44 2011 -0400 @@ -2004,10 +2004,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2035,9 +2039,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2078,99 +2080,23 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); - - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } - return canvas; + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + + return $(canvas); } }); @@ -3181,3 +3107,103 @@ ctx.restore(); } +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} \ No newline at end of file http://bitbucket.org/galaxy/galaxy-central/changeset/f48057eeedbc/ changeset: r5309:f48057eeedbc user: james_taylor date: 2011-03-29 19:56:57 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 1 file (302 bytes) --- a/static/scripts/trackster.js Tue Mar 29 13:43:24 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 13:56:57 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1945,20 +1946,17 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return canvas; + return $(canvas); } this.content_div.css("height", "0px"); } @@ -2006,10 +2004,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2037,9 +2039,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2080,99 +2080,23 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); - - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } - return canvas; + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + + return $(canvas); } }); @@ -2232,187 +2156,34 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } - } - - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; - - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } - } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } - } - */ - return highest_slot + 1; + + return inc_slots.slot_features( features ); }, /** * Draw summary tree on canvas. */ draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); + // Add label to container div when displaying summary tree mode var max_label = $("<div />").addClass('yaxislabel'); max_label.text(max); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); max_label.prependTo(this.container_div); - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } + // Paint summary tree into canvas + var ctx = canvas.getContext("2d"); + var painter = new SummaryTreePainter( points, delta, max, this.prefs.show_counts ); + painter.draw( ctx, w_scale, required_height, tile_low, left_offset ); }, /** * Draw feature. @@ -2591,10 +2362,6 @@ left_offset = this.left_offset, slots, required_height; - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - // // Set mode if Auto. // @@ -2637,21 +2404,23 @@ else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; + slots = this.inc_slots[w_scale].slots; } // // Set up for drawing. // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; + var canvas = this.view.canvas_manager.new_canvas(); + + canvas.width = width + left_offset; + canvas.height = required_height; if (result.dataset_type === "summary_tree") { // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; + canvas.height += LABEL_SPACING + CHAR_HEIGHT_PX; } parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); + var ctx = canvas.getContext("2d"); ctx.fillStyle = this.prefs.block_color; ctx.font = this.default_font; ctx.textAlign = "right"; @@ -2663,14 +2432,14 @@ if (mode === "summary_tree") { this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, tile_low, left_offset); - return canvas; + return $(canvas); } // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { - canvas.css({ + $(canvas).css({ "border-top": "1px solid red" }); @@ -2683,7 +2452,7 @@ // If there's no data, return. if (!result.data) { - return canvas; + return $(canvas); } } @@ -2726,7 +2495,7 @@ width, left_offset, ref_seq); } } - return canvas; + return $(canvas); } }); @@ -3127,4 +2896,315 @@ } }); +// ---- To be extracted ------------------------------------------------------ +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, show_counts ) { + this.data = data; + this.delta = delta; + this.max = max; + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, w_scale, required_height, tile_low, left_offset ) { + + var + points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + var x = Math.floor( (points[i][0] - tile_low) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * required_height; + + ctx.fillStyle = "black"; + ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} \ No newline at end of file http://bitbucket.org/galaxy/galaxy-central/changeset/b543cf3a0901/ changeset: r5310:b543cf3a0901 user: james_taylor date: 2011-03-30 00:40:18 summary: Streamlining feature track drawing in preparation for pulling out a Painter, cleaning up Painter interface (not quite a common interface yet) affected #: 1 file (35 bytes) --- a/static/scripts/trackster.js Tue Mar 29 13:56:57 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 18:40:18 2011 -0400 @@ -2170,22 +2170,6 @@ return inc_slots.slot_features( features ); }, /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - - // Add label to container div when displaying summary tree mode - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - // Paint summary tree into canvas - var ctx = canvas.getContext("2d"); - var painter = new SummaryTreePainter( points, delta, max, this.prefs.show_counts ); - painter.draw( ctx, w_scale, required_height, tile_low, left_offset ); - }, - /** * Draw feature. */ draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { @@ -2346,25 +2330,18 @@ * Draw FeatureTrack tile. */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, + var track = this, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, - slots, required_height; + slots, + required_height; - // - // Set mode if Auto. - // + // Set display mode if Auto. if (mode === "Auto") { if (result.dataset_type === "summary_tree") { mode = result.dataset_type; @@ -2389,19 +2366,43 @@ } } } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } + // Start dealing with row-by-row tracks + + // y_scale is the height per row var y_scale = this.get_y_scale(mode); - // // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } if (mode === "Dense") { required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; slots = this.inc_slots[w_scale].slots; @@ -2414,10 +2415,7 @@ canvas.width = width + left_offset; canvas.height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.height += LABEL_SPACING + CHAR_HEIGHT_PX; - } + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); var ctx = canvas.getContext("2d"); @@ -2426,16 +2424,6 @@ ctx.textAlign = "right"; this.container_div.find(".yaxislabel").remove(); - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return $(canvas); - } - - // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { @@ -3067,41 +3055,50 @@ /** * SummaryTreePainter, a histogram showing number of intervals in a region */ -var SummaryTreePainter = function( data, delta, max, show_counts ) { +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties this.data = data; this.delta = delta; this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs this.show_counts = show_counts; } -SummaryTreePainter.prototype.draw = function( ctx, w_scale, required_height, tile_low, left_offset ) { +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { - var - points = this.data, delta = this.delta, max = this.max, + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases // start. However, height of each rectangle is relative to required_height; hence, the // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; delta_x_px = Math.ceil(delta * w_scale); ctx.save(); for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); var y = points[i][1]; if (!y) { continue; } - var y_px = y / max * required_height; + var y_px = y / max * height; ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); // Draw number count if it can fit the number with some padding, otherwise things clump up var text_padding_req_x = 4; if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { ctx.fillStyle = "#666"; ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); + ctx.fillText(y, x + (delta_x_px/2), 10); } } @@ -3109,7 +3106,9 @@ } var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties this.data = data; + // View this.view_start = view_start; this.view_end = view_end; // Drawing prefs http://bitbucket.org/galaxy/galaxy-central/changeset/4789655a57db/ changeset: r5311:4789655a57db user: james_taylor date: 2011-03-30 00:40:24 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 1 file (337 bytes) --- a/static/scripts/trackster.js Tue Mar 29 16:27:35 2011 -0400 +++ b/static/scripts/trackster.js Tue Mar 29 18:40:24 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1948,20 +1949,17 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return canvas; + return $(canvas); } this.content_div.css("height", "0px"); } @@ -2009,10 +2007,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2040,9 +2042,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2083,99 +2083,23 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); - - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } - return canvas; + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + + return $(canvas); } }); @@ -2235,187 +2159,18 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } - } - - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; - - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } - } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } - } - */ - return highest_slot + 1; - }, - /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); - - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } + + return inc_slots.slot_features( features ); }, /** * Draw feature. @@ -2578,29 +2333,18 @@ * Draw FeatureTrack tile. */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, + var track = this, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, - slots, required_height; + slots, + required_height; - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - // - // Set mode if Auto. - // + // Set display mode if Auto. if (mode === "Auto") { if (result.dataset_type === "summary_tree") { mode = result.dataset_type; @@ -2625,55 +2369,68 @@ } } } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } + // Start dealing with row-by-row tracks + + // y_scale is the height per row var y_scale = this.get_y_scale(mode); - // // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } if (mode === "Dense") { required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; + slots = this.inc_slots[w_scale].slots; } // // Set up for drawing. // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; - } + var canvas = this.view.canvas_manager.new_canvas(); + + canvas.width = width + left_offset; + canvas.height = required_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); + var ctx = canvas.getContext("2d"); ctx.fillStyle = this.prefs.block_color; ctx.font = this.default_font; ctx.textAlign = "right"; this.container_div.find(".yaxislabel").remove(); - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return canvas; - } - - // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { - canvas.css({ + $(canvas).css({ "border-top": "1px solid red" }); @@ -2686,7 +2443,7 @@ // If there's no data, return. if (!result.data) { - return canvas; + return $(canvas); } } @@ -2729,7 +2486,7 @@ width, left_offset, ref_seq); } } - return canvas; + return $(canvas); } }); @@ -3130,4 +2887,326 @@ } }); +// ---- To be extracted ------------------------------------------------------ +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties + this.data = data; + this.delta = delta; + this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { + + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties + this.data = data; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} \ No newline at end of file http://bitbucket.org/galaxy/galaxy-central/changeset/5b89e71b55ec/ changeset: r5312:5b89e71b55ec user: james_taylor date: 2011-03-30 19:01:41 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 1 file (337 bytes) --- a/static/scripts/trackster.js Wed Mar 30 11:44:08 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 13:01:41 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1949,20 +1950,17 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return canvas; + return $(canvas); } this.content_div.css("height", "0px"); } @@ -2010,10 +2008,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2041,9 +2043,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2084,99 +2084,23 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); - - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } - return canvas; + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + + return $(canvas); } }); @@ -2236,187 +2160,18 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } - } - - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; - - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } - } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } - } - */ - return highest_slot + 1; - }, - /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); - - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } + + return inc_slots.slot_features( features ); }, /** * Draw feature. @@ -2579,29 +2334,18 @@ * Draw FeatureTrack tile. */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, + var track = this, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, - slots, required_height; + slots, + required_height; - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - // - // Set mode if Auto. - // + // Set display mode if Auto. if (mode === "Auto") { if (result.dataset_type === "summary_tree") { mode = result.dataset_type; @@ -2626,55 +2370,68 @@ } } } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } + // Start dealing with row-by-row tracks + + // y_scale is the height per row var y_scale = this.get_y_scale(mode); - // // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } if (mode === "Dense") { required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; + slots = this.inc_slots[w_scale].slots; } // // Set up for drawing. // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; - } + var canvas = this.view.canvas_manager.new_canvas(); + + canvas.width = width + left_offset; + canvas.height = required_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); + var ctx = canvas.getContext("2d"); ctx.fillStyle = this.prefs.block_color; ctx.font = this.default_font; ctx.textAlign = "right"; this.container_div.find(".yaxislabel").remove(); - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return canvas; - } - - // // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red // to indicate region where message is applicable if (result.message) { - canvas.css({ + $(canvas).css({ "border-top": "1px solid red" }); @@ -2687,7 +2444,7 @@ // If there's no data, return. if (!result.data) { - return canvas; + return $(canvas); } } @@ -2730,7 +2487,7 @@ width, left_offset, ref_seq); } } - return canvas; + return $(canvas); } }); @@ -3131,4 +2888,326 @@ } }); +// ---- To be extracted ------------------------------------------------------ +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties + this.data = data; + this.delta = delta; + this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { + + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties + this.data = data; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} \ No newline at end of file http://bitbucket.org/galaxy/galaxy-central/changeset/7366cc533040/ changeset: r5313:7366cc533040 user: james_taylor date: 2011-03-30 19:09:42 summary: Fix accidently introduced tabs affected #: 1 file (1.9 KB) --- a/static/scripts/trackster.js Wed Mar 30 13:01:41 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 13:09:42 2011 -0400 @@ -2012,10 +2012,10 @@ }; $.extend(LineTrack.prototype, TiledTrack.prototype, { add_resize_handle: function () { - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - var track = this; + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2086,20 +2086,20 @@ var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - width = Math.ceil( tile_length * w_scale ), - height = this.height_px; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; - // Create canvas + // Create canvas var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width, canvas.height = height; - // Paint line onto full canvas - var ctx = canvas.getContext("2d"); - var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, - this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); - painter.draw( ctx, width, height ); - + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); + return $(canvas); } }); @@ -2170,8 +2170,8 @@ inc_slots.mode = mode; this.inc_slots[level] = inc_slots; } - - return inc_slots.slot_features( features ); + + return inc_slots.slot_features( features ); }, /** * Draw feature. @@ -2335,15 +2335,15 @@ */ draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { var track = this, - tile_low = tile_index * DENSITY * resolution, + tile_low = tile_index * DENSITY * resolution, tile_high = ( tile_index + 1 ) * DENSITY * resolution, tile_span = tile_high - tile_low, - width = Math.ceil( tile_span * w_scale ), + width = Math.ceil( tile_span * w_scale ), mode = this.mode, min_height = 25, left_offset = this.left_offset, slots, - required_height; + required_height; // Set display mode if Auto. if (mode === "Auto") { @@ -2370,37 +2370,37 @@ } } } - - // Drawing the summary tree (feature coverage histogram) - if ( mode === "summary_tree" ) { - // Set height of parent_element - required_height = this.summary_draw_height; - parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); - // Add label to container div showing maximum count - // TODO: this shouldn't be done at the tile level - this.container_div.find(".yaxislabel").remove(); - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text( result.max ); - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - // Create canvas - var canvas = this.view.canvas_manager.new_canvas(); - canvas.width = width + left_offset; - // Extra padding at top of summary tree - canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - // Paint summary tree into canvas - var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); - var ctx = canvas.getContext("2d"); - // Deal with left_offset by translating - ctx.translate( left_offset, 0 ); - painter.draw( ctx, width, required_height ); - // Wrapped canvas element is returned - return $(canvas); - } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Wrapped canvas element is returned + return $(canvas); + } - // Start dealing with row-by-row tracks + // Start dealing with row-by-row tracks - // y_scale is the height per row + // y_scale is the height per row var y_scale = this.get_y_scale(mode); // Pack reads, set required height. @@ -2415,8 +2415,8 @@ // // Set up for drawing. // - var canvas = this.view.canvas_manager.new_canvas(); - + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; canvas.height = required_height; @@ -2934,14 +2934,14 @@ // by only having to look at a smaller subset // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); - } else { - undone.push(i); - } + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } } // Slot unslotted features. @@ -2949,106 +2949,106 @@ // Find the first slot such that current feature doesn't overlap any other features in that slot. // Returns -1 if no slot was found. var find_slot = function(f_start, f_end) { - // TODO: Fix constants - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } - } - } - if (!has_overlap) { - return slot_num; - } - } - return -1; + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; + } + } + } + if (!has_overlap) { + return slot_num; + } + } + return -1; }; // Do slotting. for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( feature_start * w_scale ), - f_end = Math.ceil( feature_end * w_scale ), - text_len = this.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && this.include_label ) { - // Add gap for label spacing and extra pack space padding - // TODO: Fix constants - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { - - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); - } + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } } // Debugging: view slots data. /* for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } } */ return highest_slot + 1; @@ -3074,36 +3074,36 @@ SummaryTreePainter.prototype.draw = function( ctx, width, height ) { var view_start = this.view_start, - view_range = this.view_end - this.view_start, - w_scale = width / view_range; + view_range = this.view_end - this.view_start, + w_scale = width / view_range; var points = this.data, delta = this.delta, max = this.max, - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); ctx.save(); for (var i = 0, len = points.length; i < len; i++) { - - var x = Math.floor( (points[i][0] - view_start) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * height; - - ctx.fillStyle = "black"; - ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + (delta_x_px/2), 10); - } + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } } ctx.restore(); @@ -3124,16 +3124,16 @@ LinePainter.prototype.draw = function( ctx, width, height ) { var - in_path = false, - min_value = this.min_value, - max_value = this.max_value, - vertical_range = max_value - min_value, - height_px = height, - view_start = this.view_start, - view_range = this.view_end - this.view_start, - w_scale = width / view_range, - mode = this.mode, - data = this.data; + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; ctx.save(); @@ -3142,72 +3142,72 @@ // Line at 0.0 if ( mode !== "Intensity" ) { - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( width, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); } ctx.beginPath(); ctx.fillStyle = this.color; var x_scaled, y, delta_x_px; if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); } else { - delta_x_px = 10; + delta_x_px = 10; } for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - view_start) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); + } + } + } } if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); } else { - ctx.stroke(); + ctx.stroke(); } ctx.restore(); -} \ No newline at end of file +} http://bitbucket.org/galaxy/galaxy-central/changeset/ac381183f760/ changeset: r5314:ac381183f760 user: james_taylor date: 2011-03-31 00:30:22 summary: Extracted feature painters for generic linked features, VCF, and reads. Not all types have been completely tested yet affected #: 1 file (3.1 KB) --- a/static/scripts/trackster.js Wed Mar 30 13:09:42 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 18:30:22 2011 -0400 @@ -1960,7 +1960,7 @@ var c_start = Math.round(c * w_scale); ctx.fillText(seq[c], c_start + track.left_offset, 10); } - return $(canvas); + return canvas; } this.content_div.css("height", "0px"); } @@ -2100,7 +2100,7 @@ this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); painter.draw( ctx, width, height ); - return $(canvas); + return canvas; } }); @@ -2150,6 +2150,8 @@ this.tile_cache = new Cache(CACHED_TILES_FEATURE); this.data_cache = new DataManager(20, this); this.left_offset = 200; + + this.painter = LinkedFeaturePainter; }; $.extend(FeatureTrack.prototype, TiledTrack.prototype, { /** @@ -2174,141 +2176,6 @@ return inc_slots.slot_features( features ); }, /** - * Draw feature. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { - var - feature_uid = feature[0], - feature_start = feature[1], - // -1 b/c intervals are half-open. - feature_end = feature[2] - 1, - feature_name = feature[3], - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, - thickness, y_start, thick_start = null, thick_end = null, - block_color = this.prefs.block_color, - label_color = this.prefs.label_color; - - // Dense mode displays the same for all data. - if (mode === "Dense") { - ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); - } - else if (mode === "no_detail") { - // No details for feature, so only one way to display. - ctx.fillStyle = block_color; - // TODO: what should width be here? - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); - } - else { // Mode is either Squish or Pack: - // Feature details. - var feature_strand = feature[4], - feature_ts = feature[5], - feature_te = feature[6], - feature_blocks = feature[7]; - - if (feature_ts && feature_te) { - thick_start = Math.floor( Math.max(0, (feature_ts - tile_low) * w_scale) ); - thick_end = Math.ceil( Math.min(width, Math.max(0, (feature_te - tile_low) * w_scale)) ); - } - - // Set vars that depend on mode. - var thin_height, thick_height; - if (mode === "Squish") { - thin_height = 1; - thick_height = SQUISH_FEATURE_HEIGHT; - } else { // mode === "Pack" - thin_height = 5; - thick_height = PACK_FEATURE_HEIGHT; - } - - // Draw feature/feature blocks + connectors. - if (!feature_blocks) { - // If there are no blocks, treat the feature as one big exon. - if ( feature.strand ) { - if (feature.strand === "+") { - ctx.fillStyle = RIGHT_STRAND_INV; - } else if (feature.strand === "-") { - ctx.fillStyle = LEFT_STRAND_INV; - } - } - else { // No strand. - ctx.fillStyle = block_color; - } - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thick_height); - } else { - // There are feature blocks and mode is either Squish or Pack. - // - // Approach: (a) draw whole feature as connector/intron and (b) draw blocks as - // needed. This ensures that whole feature, regardless of whether it starts with - // a block, is visible. - // - - // Draw whole feature as connector/intron. - var cur_y_center, cur_height; - if (mode === "Squish") { - ctx.fillStyle = CONNECTOR_COLOR; - cur_y_center = y_center + Math.floor(SQUISH_FEATURE_HEIGHT/2) + 1; - cur_height = 1; - } - else { // mode === "Pack" - if (feature_strand) { - var cur_y_center = y_center; - var cur_height = thick_height; - if (feature_strand === "+") { - ctx.fillStyle = RIGHT_STRAND; - } else if (feature_strand === "-") { - ctx.fillStyle = LEFT_STRAND; - } - } - else { - ctx.fillStyle = CONNECTOR_COLOR; - cur_y_center += (SQUISH_FEATURE_HEIGHT/2) + 1; - cur_height = 1; - } - } - ctx.fillRect(f_start + left_offset, cur_y_center, f_end - f_start, cur_height); - - for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { - var block = feature_blocks[k], - block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), - // -1 b/c intervals are half-open. - block_end = Math.ceil( Math.min(width, Math.max((block[1] - 1 - tile_low) * w_scale)) ); - - // Skip drawing if block not on tile. - if (block_start > block_end) { continue; } - - // Draw thin block. - ctx.fillStyle = block_color; - ctx.fillRect(block_start + left_offset, y_center + (thick_height-thin_height)/2 + 1, - block_end - block_start, thin_height); - - // If block intersects with thick region, draw block as thick. - if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { - var block_thick_start = Math.max(block_start, thick_start), - block_thick_end = Math.min(block_end, thick_end); - ctx.fillRect(block_thick_start + left_offset, y_center + 1, - block_thick_end - block_thick_start, thick_height); - } - } - } - - // Draw label for Pack mode. - if (mode === "Pack" && feature_start > tile_low) { - ctx.fillStyle = label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING, y_center + 8); - } - ctx.fillStyle = block_color; - } - } - }, - /** * Returns y_scale based on mode. */ get_y_scale: function(mode) { @@ -2394,27 +2261,44 @@ // Deal with left_offset by translating ctx.translate( left_offset, 0 ); painter.draw( ctx, width, required_height ); - // Wrapped canvas element is returned - return $(canvas); + // Canvas element is returned + return canvas; } // Start dealing with row-by-row tracks - - // y_scale is the height per row - var y_scale = this.get_y_scale(mode); - - // Pack reads, set required height. - if (mode === "Dense") { - required_height = min_height; - } else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { - // Calculate new slots incrementally for this new chunk of data and update height if necessary. - required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; + + // If working with a mode where slotting is neccesary, update the incremental slotting + var slots, slots_required = 1; + if ( mode === "no_detail" || mode === "Squish" || mode === "Pack" ) { + slots_required = this.incremental_slots(w_scale, result.data, mode); slots = this.inc_slots[w_scale].slots; } + + // Filter features + var filtered = []; + if ( result.data ) { + for (var i = 0, len = result.data.length; i < len; i++) { + var feature = result.data[i]; + var hide_feature = false; + var filter; + for (var f = 0, flen = this.filters.length; f < flen; f++) { + filter = this.filters[f]; + filter.update_attrs(feature); + if (!filter.keep(feature)) { + hide_feature = true; + break; + } + } + if (!hide_feature) { + filtered.push( feature ); + } + } + } - // - // Set up for drawing. - // + // Create painter, and canvas of sufficient size to contain all features + // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument + var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); + var required_height = painter.get_required_height( slots_required ); var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width + left_offset; @@ -2444,112 +2328,29 @@ // If there's no data, return. if (!result.data) { - return $(canvas); + return canvas; } } - // // Set example feature. This is needed so that track can update its UI based on feature attributes. - // this.example_feature = (result.data.length ? result.data[0] : undefined); - - // - // Draw elements. - // - var data = result.data; - for (var i = 0, len = data.length; i < len; i++) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - // Slot valid only if features are slotted and this feature is slotted; - // feature may not be due to lack of space. - slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - - // Apply filters to feature. - var hide_feature = false; - var filter; - for (var f = 0; f < this.filters.length; f++) { - filter = this.filters[f]; - filter.update_attrs(feature); - if (!filter.keep(feature)) { - hide_feature = true; - break; - } - } - if (hide_feature) { - continue; - } + + // Draw features + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height, slots ); - // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). - if (is_overlap([feature_start, feature_end], [tile_low, tile_high]) && (mode == "Dense" || slot !== null)) { - this.draw_element(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, - width, left_offset, ref_seq); - } - } - return $(canvas); + return canvas; } }); var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); this.track_type = "VcfTrack"; + this.painter = VariantPainter; }; -$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Draw a VCF entry. - */ - draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - // -1 b/c intervals are half-open. - feature_end = feature[2] - 1, - feature_name = feature[3], - // All features need a start, end, and vertical center. - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, - thickness, y_start, thick_start = null, thick_end = null; - - if (no_label) { - ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); - } else { // Show blocks, labels, etc. - // Unpack. - var ref_base = feature[4], alt_base = feature[5], qual = feature[6]; - - // Draw block for entry. - thickness = 9; - y_start = 1; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thickness); - - // Add label for entry. - if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { - // Draw label - ctx.fillStyle = label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); - } - ctx.fillStyle = block_color; - } - - // Show additional data on block. - var vcf_label = ref_base + " / " + alt_base; - if (feature_start > tile_low && ctx.measureText(vcf_label).width < (f_end - f_start)) { - ctx.fillStyle = "white"; - ctx.textAlign = "center"; - ctx.fillText(vcf_label, left_offset + f_start + (f_end-f_start)/2, y_center + 8); - ctx.fillStyle = block_color; - } - } - } -}); +$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); @@ -2573,281 +2374,10 @@ this.prefs = this.track_config.values; this.track_type = "ReadTrack"; + this.painter = ReadPainter; this.make_name_popup_menu(); }; -$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Returns y_scale based on mode. - */ - get_y_scale: function(mode) { - var y_scale; - if (mode === "summary_tree") { - // No scale needed. - } - if (mode === "Dense") { - y_scale = DENSE_TRACK_HEIGHT; - } - else if (mode === "Squish") { - y_scale = SQUISH_TRACK_HEIGHT; - } - else { // mode === "Pack" - y_scale = PACK_TRACK_HEIGHT; - if (this.track_config.values.show_insertions) { - y_scale *= 2; - } - } - return y_scale; - }, - /** - * Draw a single read. - */ - draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center, ref_seq) { - ctx.textAlign = "center"; - var track = this, - tile_region = [tile_low, tile_high], - base_offset = 0, - seq_offset = 0, - gap = 0; - - // Keep list of items that need to be drawn on top of initial drawing layer. - var draw_last = []; - - // Gap is needed so that read is offset and hence first base can be drawn on read. - // TODO-FIX: using this gap offsets reads so that their start is not visually in sync with other tracks. - if ((mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - gap = Math.round(w_scale/2); - } - if (!cigar) { - // If no cigar string, then assume all matches - cigar = [ [0, orig_seq.length] ] - } - for (var cig_id = 0, len = cigar.length; cig_id < len; cig_id++) { - var cig = cigar[cig_id], - cig_op = "MIDNSHP=X"[cig[0]], - cig_len = cig[1]; - - if (cig_op === "H" || cig_op === "S") { - // Go left if it clips - base_offset -= cig_len; - } - var seq_start = feature_start + base_offset, - s_start = Math.floor( Math.max(0, (seq_start - tile_low) * w_scale) ), - s_end = Math.floor( Math.max(0, (seq_start + cig_len - tile_low) * w_scale) ); - - switch (cig_op) { - case "H": // Hard clipping. - // TODO: draw anything? - // Sequence not present, so do not increment seq_offset. - break; - case "S": // Soft clipping. - case "M": // Match. - case "=": // Equals. - var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region); - if (seq_tile_overlap !== NO_OVERLAP) { - // Draw. - var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); - if (gap > 0) { - ctx.fillStyle = this.prefs.block_color; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); - ctx.fillStyle = CONNECTOR_COLOR; - // TODO: this can be made much more efficient by computing the complete sequence - // to draw and then drawing it. - for (var c = 0, str_len = seq.length; c < str_len; c++) { - if (this.track_config.values.show_differences && ref_seq) { - var ref_char = ref_seq[seq_start - tile_low + c]; - if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { - continue; - } - } - if (seq_start + c >= tile_low && seq_start + c <= tile_high) { - var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); - } - } - } else { - ctx.fillStyle = this.prefs.block_color; - // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(s_start + this.left_offset, - y_center + (this.mode !== "Dense" ? 4 : 5), - s_end - s_start, - (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); - } - } - seq_offset += cig_len; - base_offset += cig_len; - break; - case "N": // Skipped bases. - ctx.fillStyle = CONNECTOR_COLOR; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); - //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); - // No change in seq_offset because sequence not used when skipping. - base_offset += cig_len; - break; - case "D": // Deletion. - ctx.fillStyle = "red"; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); - // TODO: is this true? No change in seq_offset because sequence not used when skipping. - base_offset += cig_len; - break; - case "P": // TODO: No good way to draw insertions/padding right now, so ignore - // Sequences not present, so do not increment seq_offset. - break; - case "I": // Insertion. - // Check to see if sequence should be drawn at all by looking at the overlap between - // the sequence region and the tile region. - var - seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), - insert_x_coord = this.left_offset + s_start - gap; - - if (seq_tile_overlap !== NO_OVERLAP) { - var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); - // Insertion point is between the sequence start and the previous base: (-gap) moves - // back from sequence start to insertion point. - if (this.track_config.values.show_insertions) { - // - // Show inserted sequence above, centered on insertion point. - // - - // Draw sequence. - // X center is offset + start - <half_sequence_length> - var x_center = this.left_offset + s_start - (s_end - s_start)/2; - if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - // Draw sequence container. - ctx.fillStyle = "yellow"; - ctx.fillRect(x_center - gap, y_center - 9, s_end - s_start, 9); - draw_last[draw_last.length] = {type: "triangle", data: [insert_x_coord, y_center + 4, 5]}; - ctx.fillStyle = CONNECTOR_COLOR; - // Based on overlap b/t sequence and tile, get sequence to be drawn. - switch(seq_tile_overlap) { - case(OVERLAP_START): - seq = seq.slice(tile_low-seq_start); - break; - case(OVERLAP_END): - seq = seq.slice(0, seq_start-tile_high); - break; - case(CONTAINED_BY): - // All of sequence drawn. - break; - case(CONTAINS): - seq = seq.slice(tile_low-seq_start, seq_start-tile_high); - break; - } - // Draw sequence. - for (var c = 0, str_len = seq.length; c < str_len; c++) { - var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); - } - } - else { - // Draw block. - ctx.fillStyle = "yellow"; - // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(x_center, y_center + (this.mode !== "Dense" ? 2 : 5), - s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT)); - } - } - else { - if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { - // Show insertions with a single number at the insertion point. - draw_last[draw_last.length] = {type: "text", data: [seq.length, insert_x_coord, y_center + 9]}; - } - else { - // TODO: probably can merge this case with code above. - } - } - } - seq_offset += cig_len; - // No change to base offset because insertions are drawn above sequence/read. - break; - case "X": - // TODO: draw something? - seq_offset += cig_len; - break; - } - } - - // - // Draw last items. - // - ctx.fillStyle = "yellow"; - var item, type, data; - for (var i = 0; i < draw_last.length; i++) { - item = draw_last[i]; - type = item["type"]; - data = item["data"]; - if (type === "text") { - ctx.font = "bold " + DEFAULT_FONT; - ctx.fillText(data[0], data[1], data[2]); - ctx.font = DEFAULT_FONT; - } - else if (type == "triangle") { - ctx.drawDownwardEquilateralTriangle(data[0], data[1], data[2]); - } - } - }, - /** - * Draw a complete read. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset, ref_seq) { - // All features need a start, end, and vertical center. - var feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - feature_name = feature[3], - f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), - f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = (mode === "Dense" ? 1 : (1 + slot)) * y_scale, - block_color = this.prefs.block_color, - label_color = this.prefs.label_color, - // Left-gap for label text since we align chrom text to the position tick. - gap = 0; - - // TODO: fix gap usage; also see note on gap in draw_read. - if ((mode === "Pack" || this.mode === "Auto") && w_scale > CHAR_WIDTH_PX) { - var gap = Math.round(w_scale/2); - } - - // Draw read. - ctx.fillStyle = block_color; - if (feature[5] instanceof Array) { - // Read is paired. - var b1_start = Math.floor( Math.max(0, (feature[4][0] - tile_low) * w_scale) ), - b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[4][1] - tile_low) * w_scale)) ), - b2_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ), - b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ); - - // Draw left/forward read. - if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center, ref_seq); - } - // Draw right/reverse read. - if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center, ref_seq); - } - // Draw connector. - if (b2_start > b1_end) { - ctx.fillStyle = CONNECTOR_COLOR; - ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); - } - } else { - // Read is single. - ctx.fillStyle = block_color; - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center, ref_seq); - } - if (mode === "Pack" && feature_start > tile_low) { - // Draw label. - ctx.fillStyle = this.prefs.label_color; - if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { - ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); - } else { - ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); - } - ctx.fillStyle = block_color; - } - } -}); +$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); /** * Feature track that displays data generated from tool. @@ -2890,6 +2420,18 @@ // ---- To be extracted ------------------------------------------------------ +// ---- Simple extend function for inheritence ---- + +var extend = function() { + var target = arguments[0]; + for ( var i = 1; i < arguments.length; i++ ) { + var other = arguments[i]; + for ( key in other ) { + target[key] = other[key]; + } + } +} + // ---- Canvas management ---- var CanvasManager = function( document ) { @@ -3211,3 +2753,561 @@ ctx.restore(); } + +var FeaturePainter = function( data, view_start, view_end, prefs, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + this.prefs = prefs; + this.mode = mode; +} + +extend( FeaturePainter.prototype, { + + get_required_height: function( rows_required ) { + // y_scale is the height per row + var required_height = y_scale = this.get_row_height(), mode = this.mode; + // If using a packing mode, need to multiply by the number of slots used + if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + // Calculate new slots incrementally for this new chunk of data and update height if necessary. + required_height = rows_required * y_scale; + } + return required_height; + }, + + draw: function( ctx, width, height, slots ) { + + var data = this.data, view_start = this.view_start, view_end = this.view_end; + + ctx.save(); + + ctx.fillStyle = this.prefs.block_color; + ctx.textAlign = "right"; + + var view_range = this.view_end - this.view_start, + w_scale = width / view_range, + y_scale = this.get_row_height(); + + for (var i = 0, len = data.length; i < len; i++) { + var feature = data[i], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + // Slot valid only if features are slotted and this feature is slotted; + // feature may not be due to lack of space. + slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); + + // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). + if (is_overlap([feature_start, feature_end], [view_start, view_end]) && (this.mode == "Dense" || slot !== null)) { + this.draw_element(ctx, this.mode, feature, slot, view_start, view_end, w_scale, y_scale, + width ); + } + } + + ctx.restore(); + } +}); + +var LinkedFeaturePainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( LinkedFeaturePainter.prototype, FeaturePainter.prototype, { + + /** + * Height of a single row, depends on mode + */ + get_row_height: function() { + var mode = this.mode, y_scale; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; + }, + + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { + var + feature_uid = feature[0], + feature_start = feature[1], + // -1 b/c intervals are half-open. + feature_end = feature[2] - 1, + feature_name = feature[3], + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + thickness, y_start, thick_start = null, thick_end = null, + block_color = this.prefs.block_color, + label_color = this.prefs.label_color; + + // Dense mode displays the same for all data. + if (mode === "Dense") { + ctx.fillStyle = block_color; + ctx.fillRect(f_start, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); + } + else if (mode === "no_detail") { + // No details for feature, so only one way to display. + ctx.fillStyle = block_color; + // TODO: what should width be here? + ctx.fillRect(f_start, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); + } + else { // Mode is either Squish or Pack: + // Feature details. + var feature_strand = feature[4], + feature_ts = feature[5], + feature_te = feature[6], + feature_blocks = feature[7]; + + if (feature_ts && feature_te) { + thick_start = Math.floor( Math.max(0, (feature_ts - tile_low) * w_scale) ); + thick_end = Math.ceil( Math.min(width, Math.max(0, (feature_te - tile_low) * w_scale)) ); + } + + // Set vars that depend on mode. + var thin_height, thick_height; + if (mode === "Squish") { + thin_height = 1; + thick_height = SQUISH_FEATURE_HEIGHT; + } else { // mode === "Pack" + thin_height = 5; + thick_height = PACK_FEATURE_HEIGHT; + } + + // Draw feature/feature blocks + connectors. + if (!feature_blocks) { + // If there are no blocks, treat the feature as one big exon. + if ( feature.strand ) { + if (feature.strand === "+") { + ctx.fillStyle = RIGHT_STRAND_INV; + } else if (feature.strand === "-") { + ctx.fillStyle = LEFT_STRAND_INV; + } + } + else { // No strand. + ctx.fillStyle = block_color; + } + ctx.fillRect(f_start, y_center, f_end - f_start, thick_height); + } else { + // There are feature blocks and mode is either Squish or Pack. + // + // Approach: (a) draw whole feature as connector/intron and (b) draw blocks as + // needed. This ensures that whole feature, regardless of whether it starts with + // a block, is visible. + // + + // Draw whole feature as connector/intron. + var cur_y_center, cur_height; + if (mode === "Squish") { + ctx.fillStyle = CONNECTOR_COLOR; + cur_y_center = y_center + Math.floor(SQUISH_FEATURE_HEIGHT/2) + 1; + cur_height = 1; + } + else { // mode === "Pack" + if (feature_strand) { + var cur_y_center = y_center; + var cur_height = thick_height; + if (feature_strand === "+") { + ctx.fillStyle = RIGHT_STRAND; + } else if (feature_strand === "-") { + ctx.fillStyle = LEFT_STRAND; + } + } + else { + ctx.fillStyle = CONNECTOR_COLOR; + cur_y_center += (SQUISH_FEATURE_HEIGHT/2) + 1; + cur_height = 1; + } + } + ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); + + for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { + var block = feature_blocks[k], + block_start = Math.floor( Math.max(0, (block[0] - tile_low) * w_scale) ), + // -1 b/c intervals are half-open. + block_end = Math.ceil( Math.min(width, Math.max((block[1] - 1 - tile_low) * w_scale)) ); + + // Skip drawing if block not on tile. + if (block_start > block_end) { continue; } + + // Draw thin block. + ctx.fillStyle = block_color; + ctx.fillRect(block_start, y_center + (thick_height-thin_height)/2 + 1, + block_end - block_start, thin_height); + + // If block intersects with thick region, draw block as thick. + if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { + var block_thick_start = Math.max(block_start, thick_start), + block_thick_end = Math.min(block_end, thick_end); + ctx.fillRect(block_thick_start, y_center + 1, + block_thick_end - block_thick_start, thick_height); + } + } + } + + // Draw label for Pack mode. + if (mode === "Pack" && feature_start > tile_low) { + ctx.fillStyle = label_color; + // FIXME: do this without tile_index + var tile_index = 1; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + LABEL_SPACING, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start - LABEL_SPACING, y_center + 8); + } + ctx.fillStyle = block_color; + } + } + } +}); + + +var VariantPainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( VariantPainter.prototype, FeaturePainter.prototype, { + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { + var feature = data[i], + feature_uid = feature[0], + feature_start = feature[1], + // -1 b/c intervals are half-open. + feature_end = feature[2] - 1, + feature_name = feature[3], + // All features need a start, end, and vertical center. + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + thickness, y_start, thick_start = null, thick_end = null; + + if (no_label) { + ctx.fillStyle = block_color; + ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, 1); + } else { // Show blocks, labels, etc. + // Unpack. + var ref_base = feature[4], alt_base = feature[5], qual = feature[6]; + + // Draw block for entry. + thickness = 9; + y_start = 1; + ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thickness); + + // Add label for entry. + if (mode !== "Dense" && feature_name !== undefined && feature_start > tile_low) { + // Draw label + ctx.fillStyle = label_color; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + 2 + left_offset, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start - 2 + left_offset, y_center + 8); + } + ctx.fillStyle = block_color; + } + + // Show additional data on block. + var vcf_label = ref_base + " / " + alt_base; + if (feature_start > tile_low && ctx.measureText(vcf_label).width < (f_end - f_start)) { + ctx.fillStyle = "white"; + ctx.textAlign = "center"; + ctx.fillText(vcf_label, left_offset + f_start + (f_end-f_start)/2, y_center + 8); + ctx.fillStyle = block_color; + } + } + } +}); + +var ReadPainter = function( data, view_start, view_end, prefs, mode, ref_seq ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); + this.ref_seq = ref_seq; +} + +extend( ReadPainter.prototype, FeaturePainter.prototype, { + /** + * Returns y_scale based on mode. + */ + get_row_height: function() { + var y_scale, mode = this.mode; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + if (this.track_config.values.show_insertions) { + y_scale *= 2; + } + } + return y_scale; + }, + /** + * Draw a single read. + */ + draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center) { + ctx.textAlign = "center"; + var track = this, + tile_region = [tile_low, tile_high], + base_offset = 0, + seq_offset = 0, + gap = 0 + ref_seq = this.ref_seq; + + // Keep list of items that need to be drawn on top of initial drawing layer. + var draw_last = []; + + // Gap is needed so that read is offset and hence first base can be drawn on read. + // TODO-FIX: using this gap offsets reads so that their start is not visually in sync with other tracks. + if ((mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + gap = Math.round(w_scale/2); + } + if (!cigar) { + // If no cigar string, then assume all matches + cigar = [ [0, orig_seq.length] ] + } + for (var cig_id = 0, len = cigar.length; cig_id < len; cig_id++) { + var cig = cigar[cig_id], + cig_op = "MIDNSHP=X"[cig[0]], + cig_len = cig[1]; + + if (cig_op === "H" || cig_op === "S") { + // Go left if it clips + base_offset -= cig_len; + } + var seq_start = feature_start + base_offset, + s_start = Math.floor( Math.max(0, (seq_start - tile_low) * w_scale) ), + s_end = Math.floor( Math.max(0, (seq_start + cig_len - tile_low) * w_scale) ); + + switch (cig_op) { + case "H": // Hard clipping. + // TODO: draw anything? + // Sequence not present, so do not increment seq_offset. + break; + case "S": // Soft clipping. + case "M": // Match. + case "=": // Equals. + var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region); + if (seq_tile_overlap !== NO_OVERLAP) { + // Draw. + var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); + if (gap > 0) { + ctx.fillStyle = this.prefs.block_color; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); + ctx.fillStyle = CONNECTOR_COLOR; + // TODO: this can be made much more efficient by computing the complete sequence + // to draw and then drawing it. + for (var c = 0, str_len = seq.length; c < str_len; c++) { + if (this.track_config.values.show_differences && ref_seq) { + var ref_char = ref_seq[seq_start - tile_low + c]; + if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { + continue; + } + } + if (seq_start + c >= tile_low && seq_start + c <= tile_high) { + var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); + ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); + } + } + } else { + ctx.fillStyle = this.prefs.block_color; + // TODO: This is a pretty hack-ish way to fill rectangle based on mode. + ctx.fillRect(s_start + this.left_offset, + y_center + (this.mode !== "Dense" ? 4 : 5), + s_end - s_start, + (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); + } + } + seq_offset += cig_len; + base_offset += cig_len; + break; + case "N": // Skipped bases. + ctx.fillStyle = CONNECTOR_COLOR; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); + //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); + // No change in seq_offset because sequence not used when skipping. + base_offset += cig_len; + break; + case "D": // Deletion. + ctx.fillStyle = "red"; + ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); + // TODO: is this true? No change in seq_offset because sequence not used when skipping. + base_offset += cig_len; + break; + case "P": // TODO: No good way to draw insertions/padding right now, so ignore + // Sequences not present, so do not increment seq_offset. + break; + case "I": // Insertion. + // Check to see if sequence should be drawn at all by looking at the overlap between + // the sequence region and the tile region. + var + seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), + insert_x_coord = this.left_offset + s_start - gap; + + if (seq_tile_overlap !== NO_OVERLAP) { + var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); + // Insertion point is between the sequence start and the previous base: (-gap) moves + // back from sequence start to insertion point. + if (this.track_config.values.show_insertions) { + // + // Show inserted sequence above, centered on insertion point. + // + + // Draw sequence. + // X center is offset + start - <half_sequence_length> + var x_center = this.left_offset + s_start - (s_end - s_start)/2; + if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + // Draw sequence container. + ctx.fillStyle = "yellow"; + ctx.fillRect(x_center - gap, y_center - 9, s_end - s_start, 9); + draw_last[draw_last.length] = {type: "triangle", data: [insert_x_coord, y_center + 4, 5]}; + ctx.fillStyle = CONNECTOR_COLOR; + // Based on overlap b/t sequence and tile, get sequence to be drawn. + switch(seq_tile_overlap) { + case(OVERLAP_START): + seq = seq.slice(tile_low-seq_start); + break; + case(OVERLAP_END): + seq = seq.slice(0, seq_start-tile_high); + break; + case(CONTAINED_BY): + // All of sequence drawn. + break; + case(CONTAINS): + seq = seq.slice(tile_low-seq_start, seq_start-tile_high); + break; + } + // Draw sequence. + for (var c = 0, str_len = seq.length; c < str_len; c++) { + var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); + ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); + } + } + else { + // Draw block. + ctx.fillStyle = "yellow"; + // TODO: This is a pretty hack-ish way to fill rectangle based on mode. + ctx.fillRect(x_center, y_center + (this.mode !== "Dense" ? 2 : 5), + s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT)); + } + } + else { + if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { + // Show insertions with a single number at the insertion point. + draw_last[draw_last.length] = {type: "text", data: [seq.length, insert_x_coord, y_center + 9]}; + } + else { + // TODO: probably can merge this case with code above. + } + } + } + seq_offset += cig_len; + // No change to base offset because insertions are drawn above sequence/read. + break; + case "X": + // TODO: draw something? + seq_offset += cig_len; + break; + } + } + + // + // Draw last items. + // + ctx.fillStyle = "yellow"; + var item, type, data; + for (var i = 0; i < draw_last.length; i++) { + item = draw_last[i]; + type = item["type"]; + data = item["data"]; + if (type === "text") { + ctx.font = "bold " + DEFAULT_FONT; + ctx.fillText(data[0], data[1], data[2]); + ctx.font = DEFAULT_FONT; + } + else if (type == "triangle") { + ctx.drawDownwardEquilateralTriangle(data[0], data[1], data[2]); + } + } + }, + /** + * Draw a complete read pair + */ + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { + // All features need a start, end, and vertical center. + var feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), + f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), + y_center = (mode === "Dense" ? 1 : (1 + slot)) * y_scale, + block_color = this.prefs.block_color, + label_color = this.prefs.label_color, + // Left-gap for label text since we align chrom text to the position tick. + gap = 0; + + // TODO: fix gap usage; also see note on gap in draw_read. + if ((mode === "Pack" || this.mode === "Auto") && w_scale > CHAR_WIDTH_PX) { + var gap = Math.round(w_scale/2); + } + + // Draw read. + ctx.fillStyle = block_color; + if (feature[5] instanceof Array) { + // Read is paired. + var b1_start = Math.floor( Math.max(0, (feature[4][0] - tile_low) * w_scale) ), + b1_end = Math.ceil( Math.min(width, Math.max(0, (feature[4][1] - tile_low) * w_scale)) ), + b2_start = Math.floor( Math.max(0, (feature[5][0] - tile_low) * w_scale) ), + b2_end = Math.ceil( Math.min(width, Math.max(0, (feature[5][1] - tile_low) * w_scale)) ); + + // Draw left/forward read. + if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center); + } + // Draw right/reverse read. + if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center); + } + // Draw connector. + if (b2_start > b1_end) { + ctx.fillStyle = CONNECTOR_COLOR; + ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); + } + } else { + // Read is single. + ctx.fillStyle = block_color; + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center); + } + if (mode === "Pack" && feature_start > tile_low) { + // Draw label. + ctx.fillStyle = this.prefs.label_color; + // FIXME: eliminate tile_index + var tile_index = 1; + if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { + ctx.textAlign = "left"; + ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); + } else { + ctx.textAlign = "right"; + ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); + } + ctx.fillStyle = block_color; + } + } +}); http://bitbucket.org/galaxy/galaxy-central/changeset/ce135f55f510/ changeset: r5315:ce135f55f510 user: james_taylor date: 2011-03-31 00:30:35 summary: Automated merge with https://bitbucket.org/galaxy/galaxy-central affected #: 1 file (4.7 KB) --- a/static/scripts/trackster.js Wed Mar 30 17:20:10 2011 -0400 +++ b/static/scripts/trackster.js Wed Mar 30 18:30:35 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1949,13 +1950,10 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { @@ -2010,10 +2008,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2041,9 +2043,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2084,98 +2084,22 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } return canvas; } }); @@ -2226,6 +2150,8 @@ this.tile_cache = new Cache(CACHED_TILES_FEATURE); this.data_cache = new DataManager(20, this); this.left_offset = 200; + + this.painter = LinkedFeaturePainter; }; $.extend(FeatureTrack.prototype, TiledTrack.prototype, { /** @@ -2236,192 +2162,682 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); + return inc_slots.slot_features( features ); + }, + /** + * Returns y_scale based on mode. + */ + get_y_scale: function(mode) { + var y_scale; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; + }, + /** + * Draw FeatureTrack tile. + */ + draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { + var track = this, + tile_low = tile_index * DENSITY * resolution, + tile_high = ( tile_index + 1 ) * DENSITY * resolution, + tile_span = tile_high - tile_low, + width = Math.ceil( tile_span * w_scale ), + mode = this.mode, + min_height = 25, + left_offset = this.left_offset, + slots, + required_height; + + // Set display mode if Auto. + if (mode === "Auto") { + if (result.dataset_type === "summary_tree") { + mode = result.dataset_type; + } else if (result.extra_info === "no_detail") { + mode = "no_detail"; } else { - undone.push(i); + // Choose b/t Squish and Pack. + // Proxy measures for using Squish: + // (a) error message re: limiting number of features shown; + // (b) X number of features shown; + // (c) size of view shown. + // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles; + // fix this so that tiles are redrawn as necessary to use the same mode. + //if ( (result.message && result.message.match(/^Only the first [\d]+/)) || + // (result.data && result.data.length > 2000) || + var data = result.data; + if ( (data.length && data.length < 4) || + (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) { + mode = "Squish"; + } else { + mode = "Pack"; + } + } + } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Canvas element is returned + return canvas; + } + + // Start dealing with row-by-row tracks + + // If working with a mode where slotting is neccesary, update the incremental slotting + var slots, slots_required = 1; + if ( mode === "no_detail" || mode === "Squish" || mode === "Pack" ) { + slots_required = this.incremental_slots(w_scale, result.data, mode); + slots = this.inc_slots[w_scale].slots; + } + + // Filter features + var filtered = []; + if ( result.data ) { + for (var i = 0, len = result.data.length; i < len; i++) { + var feature = result.data[i]; + var hide_feature = false; + var filter; + for (var f = 0, flen = this.filters.length; f < flen; f++) { + filter = this.filters[f]; + filter.update_attrs(feature); + if (!filter.keep(feature)) { + hide_feature = true; + break; + } + } + if (!hide_feature) { + filtered.push( feature ); + } } } - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; + // Create painter, and canvas of sufficient size to contain all features + // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument + var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); + var required_height = painter.get_required_height( slots_required ); + var canvas = this.view.canvas_manager.new_canvas(); - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } + canvas.width = width + left_offset; + canvas.height = required_height; + + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); + var ctx = canvas.getContext("2d"); + ctx.fillStyle = this.prefs.block_color; + ctx.font = this.default_font; + ctx.textAlign = "right"; + this.container_div.find(".yaxislabel").remove(); + + // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red + // to indicate region where message is applicable + if (result.message) { + $(canvas).css({ + "border-top": "1px solid red" + }); + + ctx.fillStyle = "red"; + ctx.textAlign = "left"; + var old_base = ctx.textBaseline; + ctx.textBaseline = "top"; + ctx.fillText(result.message, left_offset, 0); + ctx.textBaseline = old_base; + + // If there's no data, return. + if (!result.data) { + return canvas; + } + } + + // Set example feature. This is needed so that track can update its UI based on feature attributes. + this.example_feature = (result.data.length ? result.data[0] : undefined); + + // Draw features + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height, slots ); + + return canvas; + } +}); + +var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); + this.track_type = "VcfTrack"; + this.painter = VariantPainter; +}; + +$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + + +var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); + + this.track_config = new TrackConfig( { + track: this, + params: [ + { key: 'block_color', label: 'Block color', type: 'color', default_value: '#444' }, + { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, + { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false }, + { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true }, + { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, + { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, + ], + saved_values: prefs, + onchange: function() { + this.track.tile_cache.clear(); + this.track.draw(); + } + }); + this.prefs = this.track_config.values; + + this.track_type = "ReadTrack"; + this.painter = ReadPainter; + this.make_name_popup_menu(); +}; +$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + +/** + * Feature track that displays data generated from tool. + */ +var ToolDataFeatureTrack = function(name, view, hda_ldda, dataset_id, prefs, filters, parent_track) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters, {}, parent_track); + this.track_type = "ToolDataFeatureTrack"; + + // Set up track to fetch initial data from raw data URL when the dataset--not the converted datasets-- + // is ready. + this.data_url = raw_data_url; + this.data_query_wait = 1000; + this.dataset_check_url = dataset_state_url; +}; + +$.extend(ToolDataFeatureTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { + /** + * For this track type, the predraw init sets up postdraw init. + */ + predraw_init: function() { + // Postdraw init: once data has been fetched, reset data url, wait time and start indexing. + var track = this; + var post_init = function() { + if (track.data_cache.size() === 0) { + // Track still drawing initial data, so do nothing. + setTimeout(post_init, 300); + } + else { + // Track drawing done: reset dataset check, data URL, wait time and get dataset state to start + // indexing. + track.data_url = default_data_url; + track.data_query_wait = DEFAULT_DATA_QUERY_WAIT; + track.dataset_state_url = converted_datasets_state_url; + $.getJSON(track.dataset_state_url, {dataset_id : track.dataset_id, hda_ldda: track.hda_ldda}, function(track_data) {}); + } + }; + post_init(); + } +}); + +// ---- To be extracted ------------------------------------------------------ + +// ---- Simple extend function for inheritence ---- + +var extend = function() { + var target = arguments[0]; + for ( var i = 1; i < arguments.length; i++ ) { + var other = arguments[i]; + for ( key in other ) { + target[key] = other[key]; + } + } +} + +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; } } - if (!has_overlap) { - return slot_num; + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties + this.data = data; + this.delta = delta; + this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { + + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties + this.data = data; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); } } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} + +var FeaturePainter = function( data, view_start, view_end, prefs, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + this.prefs = prefs; + this.mode = mode; +} + +extend( FeaturePainter.prototype, { + + get_required_height: function( rows_required ) { + // y_scale is the height per row + var required_height = y_scale = this.get_row_height(), mode = this.mode; + // If using a packing mode, need to multiply by the number of slots used + if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + // Calculate new slots incrementally for this new chunk of data and update height if necessary. + required_height = rows_required * y_scale; + } + return required_height; + }, + + draw: function( ctx, width, height, slots ) { + + var data = this.data, view_start = this.view_start, view_end = this.view_end; + + ctx.save(); + + ctx.fillStyle = this.prefs.block_color; + ctx.textAlign = "right"; + + var view_range = this.view_end - this.view_start, + w_scale = width / view_range, + y_scale = this.get_row_height(); + + for (var i = 0, len = data.length; i < len; i++) { + var feature = data[i], feature_uid = feature[0], feature_start = feature[1], feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { + // Slot valid only if features are slotted and this feature is slotted; + // feature may not be due to lack of space. + slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). + if (is_overlap([feature_start, feature_end], [view_start, view_end]) && (this.mode == "Dense" || slot !== null)) { + this.draw_element(ctx, this.mode, feature, slot, view_start, view_end, w_scale, y_scale, + width ); } } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } + + ctx.restore(); + } +}); + +var LinkedFeaturePainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( LinkedFeaturePainter.prototype, FeaturePainter.prototype, { + + /** + * Height of a single row, depends on mode + */ + get_row_height: function() { + var mode = this.mode, y_scale; + if (mode === "summary_tree") { + // No scale needed. } - */ - return highest_slot + 1; + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; }, - /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); - - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } - }, - /** - * Draw feature. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { + + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { var feature_uid = feature[0], feature_start = feature[1], @@ -2438,13 +2854,13 @@ // Dense mode displays the same for all data. if (mode === "Dense") { ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); + ctx.fillRect(f_start, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); } else if (mode === "no_detail") { // No details for feature, so only one way to display. ctx.fillStyle = block_color; // TODO: what should width be here? - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); + ctx.fillRect(f_start, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); } else { // Mode is either Squish or Pack: // Feature details. @@ -2481,7 +2897,7 @@ else { // No strand. ctx.fillStyle = block_color; } - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thick_height); + ctx.fillRect(f_start, y_center, f_end - f_start, thick_height); } else { // There are feature blocks and mode is either Squish or Pack. // @@ -2513,7 +2929,7 @@ cur_height = 1; } } - ctx.fillRect(f_start + left_offset, cur_y_center, f_end - f_start, cur_height); + ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { var block = feature_blocks[k], @@ -2526,14 +2942,14 @@ // Draw thin block. ctx.fillStyle = block_color; - ctx.fillRect(block_start + left_offset, y_center + (thick_height-thin_height)/2 + 1, + ctx.fillRect(block_start, y_center + (thick_height-thin_height)/2 + 1, block_end - block_start, thin_height); // If block intersects with thick region, draw block as thick. if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { var block_thick_start = Math.max(block_start, thick_start), block_thick_end = Math.min(block_end, thick_end); - ctx.fillRect(block_thick_start + left_offset, y_center + 1, + ctx.fillRect(block_thick_start, y_center + 1, block_thick_end - block_thick_start, thick_height); } } @@ -2542,207 +2958,27 @@ // Draw label for Pack mode. if (mode === "Pack" && feature_start > tile_low) { ctx.fillStyle = label_color; + // FIXME: do this without tile_index + var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING, y_center + 8); + ctx.fillText(feature_name, f_end + LABEL_SPACING, y_center + 8); } else { ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING, y_center + 8); + ctx.fillText(feature_name, f_start - LABEL_SPACING, y_center + 8); } ctx.fillStyle = block_color; } } - }, - /** - * Returns y_scale based on mode. - */ - get_y_scale: function(mode) { - var y_scale; - if (mode === "summary_tree") { - // No scale needed. - } - if (mode === "Dense") { - y_scale = DENSE_TRACK_HEIGHT; - } - else if (mode === "no_detail") { - y_scale = NO_DETAIL_TRACK_HEIGHT; - } - else if (mode === "Squish") { - y_scale = SQUISH_TRACK_HEIGHT; - } - else { // mode === "Pack" - y_scale = PACK_TRACK_HEIGHT; - } - return y_scale; - }, - /** - * Draw FeatureTrack tile. - */ - draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, - tile_high = ( tile_index + 1 ) * DENSITY * resolution, - tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), - mode = this.mode, - min_height = 25, - left_offset = this.left_offset, - slots, required_height; - - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - // - // Set mode if Auto. - // - if (mode === "Auto") { - if (result.dataset_type === "summary_tree") { - mode = result.dataset_type; - } else if (result.extra_info === "no_detail") { - mode = "no_detail"; - } else { - // Choose b/t Squish and Pack. - // Proxy measures for using Squish: - // (a) error message re: limiting number of features shown; - // (b) X number of features shown; - // (c) size of view shown. - // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles; - // fix this so that tiles are redrawn as necessary to use the same mode. - //if ( (result.message && result.message.match(/^Only the first [\d]+/)) || - // (result.data && result.data.length > 2000) || - var data = result.data; - if ( (data.length && data.length < 4) || - (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) { - mode = "Squish"; - } else { - mode = "Pack"; - } - } - } - - var y_scale = this.get_y_scale(mode); - - // - // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } - if (mode === "Dense") { - required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { - // Calculate new slots incrementally for this new chunk of data and update height if necessary. - required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; - } - - // - // Set up for drawing. - // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; - } - parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); - // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); - ctx.fillStyle = this.prefs.block_color; - ctx.font = this.default_font; - ctx.textAlign = "right"; - this.container_div.find(".yaxislabel").remove(); - - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return canvas; - } - - // - // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red - // to indicate region where message is applicable - if (result.message) { - canvas.css({ - "border-top": "1px solid red" - }); - - ctx.fillStyle = "red"; - ctx.textAlign = "left"; - var old_base = ctx.textBaseline; - ctx.textBaseline = "top"; - ctx.fillText(result.message, left_offset, 0); - ctx.textBaseline = old_base; - - // If there's no data, return. - if (!result.data) { - return canvas; - } - } - - // - // Set example feature. This is needed so that track can update its UI based on feature attributes. - // - this.example_feature = (result.data.length ? result.data[0] : undefined); - - // - // Draw elements. - // - var data = result.data; - for (var i = 0, len = data.length; i < len; i++) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - // Slot valid only if features are slotted and this feature is slotted; - // feature may not be due to lack of space. - slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - - // Apply filters to feature. - var hide_feature = false; - var filter; - for (var f = 0; f < this.filters.length; f++) { - filter = this.filters[f]; - filter.update_attrs(feature); - if (!filter.keep(feature)) { - hide_feature = true; - break; - } - } - if (hide_feature) { - continue; - } - - // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). - if (is_overlap([feature_start, feature_end], [tile_low, tile_high]) && (mode == "Dense" || slot !== null)) { - this.draw_element(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, - width, left_offset, ref_seq); - } - } - return canvas; } }); -var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); - this.track_type = "VcfTrack"; -}; -$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Draw a VCF entry. - */ +var VariantPainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( VariantPainter.prototype, FeaturePainter.prototype, { draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { var feature = data[i], feature_uid = feature[0], @@ -2794,36 +3030,17 @@ } }); -var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); - - this.track_config = new TrackConfig( { - track: this, - params: [ - { key: 'block_color', label: 'Block color', type: 'color', default_value: '#444' }, - { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, - { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false }, - { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true }, - { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, - { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, - ], - saved_values: prefs, - onchange: function() { - this.track.tile_cache.clear(); - this.track.draw(); - } - }); - this.prefs = this.track_config.values; - - this.track_type = "ReadTrack"; - this.make_name_popup_menu(); -}; -$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { +var ReadPainter = function( data, view_start, view_end, prefs, mode, ref_seq ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); + this.ref_seq = ref_seq; +} + +extend( ReadPainter.prototype, FeaturePainter.prototype, { /** * Returns y_scale based on mode. */ - get_y_scale: function(mode) { - var y_scale; + get_row_height: function() { + var y_scale, mode = this.mode; if (mode === "summary_tree") { // No scale needed. } @@ -2844,13 +3061,14 @@ /** * Draw a single read. */ - draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center, ref_seq) { + draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center) { ctx.textAlign = "center"; var track = this, tile_region = [tile_low, tile_high], base_offset = 0, seq_offset = 0, - gap = 0; + gap = 0 + ref_seq = this.ref_seq; // Keep list of items that need to be drawn on top of initial drawing layer. var draw_last = []; @@ -3029,9 +3247,9 @@ } }, /** - * Draw a complete read. + * Draw a complete read pair */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset, ref_seq) { + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { // All features need a start, end, and vertical center. var feature_uid = feature[0], feature_start = feature[1], @@ -3061,11 +3279,11 @@ // Draw left/forward read. if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center); } // Draw right/reverse read. if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center); } // Draw connector. if (b2_start > b1_end) { @@ -3075,11 +3293,13 @@ } else { // Read is single. ctx.fillStyle = block_color; - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center); } if (mode === "Pack" && feature_start > tile_low) { // Draw label. ctx.fillStyle = this.prefs.label_color; + // FIXME: eliminate tile_index + var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); @@ -3091,44 +3311,3 @@ } } }); - -/** - * Feature track that displays data generated from tool. - */ -var ToolDataFeatureTrack = function(name, view, hda_ldda, dataset_id, prefs, filters, parent_track) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters, {}, parent_track); - this.track_type = "ToolDataFeatureTrack"; - - // Set up track to fetch initial data from raw data URL when the dataset--not the converted datasets-- - // is ready. - this.data_url = raw_data_url; - this.data_query_wait = 1000; - this.dataset_check_url = dataset_state_url; -}; - -$.extend(ToolDataFeatureTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * For this track type, the predraw init sets up postdraw init. - */ - predraw_init: function() { - // Postdraw init: once data has been fetched, reset data url, wait time and start indexing. - var track = this; - var post_init = function() { - if (track.data_cache.size() === 0) { - // Track still drawing initial data, so do nothing. - setTimeout(post_init, 300); - } - else { - // Track drawing done: reset dataset check, data URL, wait time and get dataset state to start - // indexing. - track.data_url = default_data_url; - track.data_query_wait = DEFAULT_DATA_QUERY_WAIT; - track.dataset_state_url = converted_datasets_state_url; - $.getJSON(track.dataset_state_url, {dataset_id : track.dataset_id, hda_ldda: track.hda_ldda}, function(track_data) {}); - } - }; - post_init(); - } -}); - - http://bitbucket.org/galaxy/galaxy-central/changeset/5e792d8de77d/ changeset: r5316:5e792d8de77d user: dan date: 2011-03-31 16:16:40 summary: Fix for wrapping input values for change_format and label tags when tool is using check_values="false". Fixes issue seen when e.g. using an output label in a data_source tool. affected #: 1 file (309 bytes) --- a/lib/galaxy/tools/actions/__init__.py Wed Mar 30 18:30:35 2011 -0400 +++ b/lib/galaxy/tools/actions/__init__.py Thu Mar 31 10:16:40 2011 -0400 @@ -133,16 +133,18 @@ else: new_list.append( value ) return new_list - def wrap_values( inputs, input_values ): + def wrap_values( inputs, input_values, skip_missing_values = False ): # Wrap tool inputs as necessary for input in inputs.itervalues(): + if input.name not in input_values and skip_missing_values: + continue if isinstance( input, Repeat ): for d in input_values[ input.name ]: - wrap_values( input.inputs, d ) + wrap_values( input.inputs, d, skip_missing_values = skip_missing_values ) elif isinstance( input, Conditional ): values = input_values[ input.name ] current = values[ "__current_case__" ] - wrap_values( input.cases[current].inputs, values ) + wrap_values( input.cases[current].inputs, values, skip_missing_values = skip_missing_values ) elif isinstance( input, DataToolParameter ): input_values[ input.name ] = \ galaxy.tools.DatasetFilenameWrapper( input_values[ input.name ], @@ -254,7 +256,7 @@ if output.change_format: if params is None: params = make_dict_copy( incoming ) - wrap_values( tool.inputs, params ) + wrap_values( tool.inputs, params, skip_missing_values = not tool.check_values ) for change_elem in output.change_format: for when_elem in change_elem.findall( 'when' ): check = when_elem.get( 'input', None ) @@ -304,7 +306,7 @@ # <outputs> # <data format="input" name="output" label="Blat on ${<input_param>.name}" /> # </outputs> - wrap_values( tool.inputs, params ) + wrap_values( tool.inputs, params, skip_missing_values = not tool.check_values ) #tool (only needing to be set once) and on_string (set differently for each label) are overwritten for each output dataset label being determined params['tool'] = tool params['on_string'] = on_text http://bitbucket.org/galaxy/galaxy-central/changeset/129a624fcace/ changeset: r5317:129a624fcace user: james_taylor date: 2011-03-31 20:08:00 summary: Merge affected #: 2 files (143 bytes) --- a/lib/galaxy/web/api/workflows.py Thu Mar 31 10:16:40 2011 -0400 +++ b/lib/galaxy/web/api/workflows.py Thu Mar 31 14:08:00 2011 -0400 @@ -57,7 +57,7 @@ inputs = {} for step in latest_workflow.steps: if step.type == 'data_input': - inputs[step.id] = {'label':"Input Dataset", 'value':""} + inputs[step.id] = {'label':step.tool_inputs['name'], 'value':""} else: pass # Eventually, allow regular tool parameters to be inserted and modified at runtime. --- a/static/scripts/trackster.js Thu Mar 31 10:16:40 2011 -0400 +++ b/static/scripts/trackster.js Thu Mar 31 14:08:00 2011 -0400 @@ -2298,7 +2298,8 @@ // Create painter, and canvas of sufficient size to contain all features // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); - var required_height = painter.get_required_height( slots_required ); + // FIXME: ERROR_PADDING is an ugly gap most of the time + var required_height = painter.get_required_height( slots_required ) + ERROR_PADDING; var canvas = this.view.canvas_manager.new_canvas(); canvas.width = width + left_offset; @@ -2336,7 +2337,7 @@ this.example_feature = (result.data.length ? result.data[0] : undefined); // Draw features - ctx.translate( left_offset, 0 ); + ctx.translate( left_offset, ERROR_PADDING ); painter.draw( ctx, width, required_height, slots ); return canvas; @@ -2772,7 +2773,8 @@ // Calculate new slots incrementally for this new chunk of data and update height if necessary. required_height = rows_required * y_scale; } - return required_height; + // Pad bottom by half a row + return required_height + Math.round( y_scale / 2 ); }, draw: function( ctx, width, height, slots ) { @@ -2846,11 +2848,11 @@ feature_name = feature[3], f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null, block_color = this.prefs.block_color, label_color = this.prefs.label_color; - + // Dense mode displays the same for all data. if (mode === "Dense") { ctx.fillStyle = block_color; @@ -2989,7 +2991,7 @@ // All features need a start, end, and vertical center. f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null; if (no_label) { @@ -3052,7 +3054,7 @@ } else { // mode === "Pack" y_scale = PACK_TRACK_HEIGHT; - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { y_scale *= 2; } } @@ -3109,12 +3111,12 @@ var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); if (gap > 0) { ctx.fillStyle = this.prefs.block_color; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); + ctx.fillRect(s_start - gap, y_center + 1, s_end - s_start, 9); ctx.fillStyle = CONNECTOR_COLOR; // TODO: this can be made much more efficient by computing the complete sequence // to draw and then drawing it. for (var c = 0, str_len = seq.length; c < str_len; c++) { - if (this.track_config.values.show_differences && ref_seq) { + if (this.prefs.show_differences && ref_seq) { var ref_char = ref_seq[seq_start - tile_low + c]; if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { continue; @@ -3122,13 +3124,13 @@ } if (seq_start + c >= tile_low && seq_start + c <= tile_high) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); + ctx.fillText(seq[c], c_start, y_center + 9); } } } else { ctx.fillStyle = this.prefs.block_color; // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(s_start + this.left_offset, + ctx.fillRect(s_start, y_center + (this.mode !== "Dense" ? 4 : 5), s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); @@ -3139,14 +3141,14 @@ break; case "N": // Skipped bases. ctx.fillStyle = CONNECTOR_COLOR; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); + ctx.fillRect(s_start - gap, y_center + 5, s_end - s_start, 1); //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); // No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; case "D": // Deletion. ctx.fillStyle = "red"; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); + ctx.fillRect(s_start - gap, y_center + 4, s_end - s_start, 3); // TODO: is this true? No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; @@ -3158,20 +3160,20 @@ // the sequence region and the tile region. var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), - insert_x_coord = this.left_offset + s_start - gap; + insert_x_coord = s_start - gap; if (seq_tile_overlap !== NO_OVERLAP) { var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); // Insertion point is between the sequence start and the previous base: (-gap) moves // back from sequence start to insertion point. - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { // // Show inserted sequence above, centered on insertion point. // // Draw sequence. // X center is offset + start - <half_sequence_length> - var x_center = this.left_offset + s_start - (s_end - s_start)/2; + var x_center = s_start - (s_end - s_start)/2; if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { // Draw sequence container. ctx.fillStyle = "yellow"; @@ -3196,7 +3198,7 @@ // Draw sequence. for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); + ctx.fillText(seq[c], c_start - (s_end - s_start)/2, y_center); } } else { @@ -3288,7 +3290,7 @@ // Draw connector. if (b2_start > b1_end) { ctx.fillStyle = CONNECTOR_COLOR; - ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); + ctx.dashedLine(b1_end - gap, y_center + 5, b2_start - gap, y_center + 5); } } else { // Read is single. @@ -3302,10 +3304,10 @@ var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_end + LABEL_SPACING - gap, y_center + 8); } else { ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_start - LABEL_SPACING - gap, y_center + 8); } ctx.fillStyle = block_color; } http://bitbucket.org/galaxy/galaxy-central/changeset/82bbc491ab90/ changeset: r5318:82bbc491ab90 user: kanwei date: 2011-04-01 00:58:25 summary: Merge james' trackster changes affected #: 1 file (4.5 KB) --- a/static/scripts/trackster.js Thu Mar 31 18:58:02 2011 -0400 +++ b/static/scripts/trackster.js Thu Mar 31 18:58:25 2011 -0400 @@ -372,6 +372,7 @@ this.min_separation = 30; this.has_changes = false; this.init( callback ); + this.canvas_manager = new CanvasManager( container.get(0).ownerDocument ); this.reset(); }; $.extend( View.prototype, { @@ -1976,13 +1977,10 @@ track.content_div.css("height", "0px"); return; } - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - var ctx = canvas.get(0).getContext("2d"); - canvas.get(0).width = Math.ceil( tile_length * w_scale + track.left_offset); - canvas.get(0).height = track.height_px; + var canvas = this.view.canvas_manager.new_canvas(); + var ctx = canvas.getContext("2d"); + canvas.width = Math.ceil( tile_length * w_scale + track.left_offset); + canvas.height = track.height_px; ctx.font = DEFAULT_FONT; ctx.textAlign = "center"; for (var c = 0, str_len = seq.length; c < str_len; c++) { @@ -2037,10 +2035,14 @@ this.height_px = this.track_config.values.height; this.vertical_range = this.track_config.values.max_value - this.track_config.values.min_value; - // Add control for resizing - // Trickery here to deal with the hovering drag handle, can probably be - // pulled out and reused. - (function( track ){ + this.add_resize_handle(); +}; +$.extend(LineTrack.prototype, TiledTrack.prototype, { + add_resize_handle: function () { + // Add control for resizing + // Trickery here to deal with the hovering drag handle, can probably be + // pulled out and reused. + var track = this; var in_handle = false; var in_drag = false; var drag_control = $( "<div class='track-resize'>" ) @@ -2068,9 +2070,7 @@ if ( ! in_handle ) { drag_control.hide(); } track.track_config.values.height = track.height_px; }).appendTo( track.container_div ); - })(this); -}; -$.extend(LineTrack.prototype, TiledTrack.prototype, { + }, predraw_init: function() { var track = this, track_id = track.view.tracks.indexOf(track); @@ -2111,98 +2111,22 @@ return; } - var track = this, - tile_low = tile_index * DENSITY * resolution, + var tile_low = tile_index * DENSITY * resolution, tile_length = DENSITY * resolution, - key = resolution + "_" + tile_index, - params = { hda_ldda: this.hda_ldda, dataset_id: this.dataset_id, resolution: this.view.resolution }; + width = Math.ceil( tile_length * w_scale ), + height = this.height_px; + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width, + canvas.height = height; - var canvas = document.createElement("canvas"), - data = result.data; - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - canvas.get(0).width = Math.ceil( tile_length * w_scale ); - canvas.get(0).height = track.height_px; - var ctx = canvas.get(0).getContext("2d"), - in_path = false, - min_value = track.prefs.min_value, - max_value = track.prefs.max_value, - vertical_range = track.vertical_range, - total_frequency = track.total_frequency, - height_px = track.height_px, - mode = track.mode; - - // Pixel position of 0 on the y axis - var y_zero = Math.round( height_px + min_value / vertical_range * height_px ); - - // Line at 0.0 - ctx.beginPath(); - ctx.moveTo( 0, y_zero ); - ctx.lineTo( tile_length * w_scale, y_zero ); - // ctx.lineWidth = 0.5; - ctx.fillStyle = "#aaa"; - ctx.stroke(); + // Paint line onto full canvas + var ctx = canvas.getContext("2d"); + var painter = new LinePainter( result.data, tile_low, tile_low + tile_length, + this.prefs.min_value, this.prefs.max_value, this.prefs.color, this.mode ); + painter.draw( ctx, width, height ); - ctx.beginPath(); - ctx.fillStyle = track.prefs.color; - var x_scaled, y, delta_x_px; - if (data.length > 1) { - delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); - } else { - delta_x_px = 10; - } - for (var i = 0, len = data.length; i < len; i++) { - x_scaled = Math.round((data[i][0] - tile_low) * w_scale); - y = data[i][1]; - if (y === null) { - if (in_path && mode === "Filled") { - ctx.lineTo(x_scaled, height_px); - } - in_path = false; - continue; - } - if (y < min_value) { - y = min_value; - } else if (y > max_value) { - y = max_value; - } - - if (mode === "Histogram") { - // y becomes the bar height in pixels, which is the negated for canvas coords - y = Math.round( y / vertical_range * height_px ); - ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); - } else if (mode === "Intensity" ) { - y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); - ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; - ctx.fillRect(x_scaled, 0, delta_x_px, height_px); - } else { - // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); - y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); - // console.log(canvas.get(0).height, canvas.get(0).width); - if (in_path) { - ctx.lineTo(x_scaled, y); - } else { - in_path = true; - if (mode === "Filled") { - ctx.moveTo(x_scaled, height_px); - ctx.lineTo(x_scaled, y); - } else { - ctx.moveTo(x_scaled, y); - } - } - } - } - if (mode === "Filled") { - if (in_path) { - ctx.lineTo( x_scaled, y_zero ); - ctx.lineTo( 0, y_zero ); - } - ctx.fill(); - } else { - ctx.stroke(); - } return canvas; } }); @@ -2253,6 +2177,8 @@ this.tile_cache = new Cache(CACHED_TILES_FEATURE); this.data_cache = new DataManager(20, this); this.left_offset = 200; + + this.painter = LinkedFeaturePainter; }; $.extend(FeatureTrack.prototype, TiledTrack.prototype, { /** @@ -2263,192 +2189,684 @@ * Returns the number of slots used to pack features. */ incremental_slots: function(level, features, mode) { - // + // Get/create incremental slots for level. If display mode changed, // need to create new slots. - // + var inc_slots = this.inc_slots[level]; if (!inc_slots || (inc_slots.mode !== mode)) { - inc_slots = {}; - inc_slots.w_scale = level; + inc_slots = new FeatureSlotter( level, mode === "Pack", function ( x ) { return CONTEXT.measureText( x ) } ); inc_slots.mode = mode; this.inc_slots[level] = inc_slots; - this.start_end_dct[level] = {}; } - // - // If feature already exists in slots (from previously seen tiles), use the same slot, - // otherwise if not seen, add to "undone" list for slot calculation. - // - var w_scale = inc_slots.w_scale, - undone = [], slotted = [], - highest_slot = 0, // To measure how big to draw canvas - max_low = this.view.max_low; - // TODO: Should calculate zoom tile index, which will improve performance - // by only having to look at a smaller subset - // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } - for (var i = 0, len = features.length; i < len; i++) { - var feature = features[i], - feature_uid = feature[0]; - if (inc_slots[feature_uid] !== undefined) { - highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); - slotted.push(inc_slots[feature_uid]); + return inc_slots.slot_features( features ); + }, + /** + * Returns y_scale based on mode. + */ + get_y_scale: function(mode) { + var y_scale; + if (mode === "summary_tree") { + // No scale needed. + } + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; + }, + /** + * Draw FeatureTrack tile. + */ + draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { + var track = this, + tile_low = tile_index * DENSITY * resolution, + tile_high = ( tile_index + 1 ) * DENSITY * resolution, + tile_span = tile_high - tile_low, + width = Math.ceil( tile_span * w_scale ), + mode = this.mode, + min_height = 25, + left_offset = this.left_offset, + slots, + required_height; + + // Set display mode if Auto. + if (mode === "Auto") { + if (result.dataset_type === "summary_tree") { + mode = result.dataset_type; + } else if (result.extra_info === "no_detail") { + mode = "no_detail"; } else { - undone.push(i); + // Choose b/t Squish and Pack. + // Proxy measures for using Squish: + // (a) error message re: limiting number of features shown; + // (b) X number of features shown; + // (c) size of view shown. + // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles; + // fix this so that tiles are redrawn as necessary to use the same mode. + //if ( (result.message && result.message.match(/^Only the first [\d]+/)) || + // (result.data && result.data.length > 2000) || + var data = result.data; + if ( (data.length && data.length < 4) || + (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) { + mode = "Squish"; + } else { + mode = "Pack"; + } + } + } + + // Drawing the summary tree (feature coverage histogram) + if ( mode === "summary_tree" ) { + // Set height of parent_element + required_height = this.summary_draw_height; + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // Add label to container div showing maximum count + // TODO: this shouldn't be done at the tile level + this.container_div.find(".yaxislabel").remove(); + var max_label = $("<div />").addClass('yaxislabel'); + max_label.text( result.max ); + max_label.css({ position: "absolute", top: "22px", left: "10px" }); + max_label.prependTo(this.container_div); + // Create canvas + var canvas = this.view.canvas_manager.new_canvas(); + canvas.width = width + left_offset; + // Extra padding at top of summary tree + canvas.height = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; + // Paint summary tree into canvas + var painter = new SummaryTreePainter( result.data, result.delta, result.max, tile_low, tile_high, this.prefs.show_counts ); + var ctx = canvas.getContext("2d"); + // Deal with left_offset by translating + ctx.translate( left_offset, 0 ); + painter.draw( ctx, width, required_height ); + // Canvas element is returned + return canvas; + } + + // Start dealing with row-by-row tracks + + // If working with a mode where slotting is neccesary, update the incremental slotting + var slots, slots_required = 1; + if ( mode === "no_detail" || mode === "Squish" || mode === "Pack" ) { + slots_required = this.incremental_slots(w_scale, result.data, mode); + slots = this.inc_slots[w_scale].slots; + } + + // Filter features + var filtered = []; + if ( result.data ) { + for (var i = 0, len = result.data.length; i < len; i++) { + var feature = result.data[i]; + var hide_feature = false; + var filter; + for (var f = 0, flen = this.filters.length; f < flen; f++) { + filter = this.filters[f]; + filter.update_attrs(feature); + if (!filter.keep(feature)) { + hide_feature = true; + break; + } + } + if (!hide_feature) { + filtered.push( feature ); + } } } - // - // Slot unslotted features. - // - var start_end_dct = this.start_end_dct[level]; + // Create painter, and canvas of sufficient size to contain all features + // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument + var painter = new (this.painter)( filtered, tile_low, tile_high, this.prefs, mode, ref_seq ); + // FIXME: ERROR_PADDING is an ugly gap most of the time + var required_height = painter.get_required_height( slots_required ) + ERROR_PADDING; + var canvas = this.view.canvas_manager.new_canvas(); - // Find the first slot such that current feature doesn't overlap any other features in that slot. - // Returns -1 if no slot was found. - var find_slot = function(f_start, f_end) { - for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { - var has_overlap = false, - slot = start_end_dct[slot_num]; - if (slot !== undefined) { - // Iterate through features already in slot to see if current feature will fit. - for (var k = 0, k_len = slot.length; k < k_len; k++) { - var s_e = slot[k]; - if (f_end > s_e[0] && f_start < s_e[1]) { - // There is overlap - has_overlap = true; - break; - } + canvas.width = width + left_offset; + canvas.height = required_height; + + parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); + // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); + var ctx = canvas.getContext("2d"); + ctx.fillStyle = this.prefs.block_color; + ctx.font = this.default_font; + ctx.textAlign = "right"; + this.container_div.find(".yaxislabel").remove(); + + // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red + // to indicate region where message is applicable + if (result.message) { + $(canvas).css({ + "border-top": "1px solid red" + }); + + ctx.fillStyle = "red"; + ctx.textAlign = "left"; + var old_base = ctx.textBaseline; + ctx.textBaseline = "top"; + ctx.fillText(result.message, left_offset, 0); + ctx.textBaseline = old_base; + + // If there's no data, return. + if (!result.data) { + return canvas; + } + } + + // Set example feature. This is needed so that track can update its UI based on feature attributes. + this.example_feature = (result.data.length ? result.data[0] : undefined); + + // Draw features + ctx.translate( left_offset, ERROR_PADDING ); + painter.draw( ctx, width, required_height, slots ); + + return canvas; + } +}); + +var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); + this.track_type = "VcfTrack"; + this.painter = VariantPainter; +}; + +$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + + +var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); + + this.track_config = new TrackConfig( { + track: this, + params: [ + { key: 'block_color', label: 'Block color', type: 'color', default_value: '#444' }, + { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, + { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false }, + { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true }, + { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, + { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, + ], + saved_values: prefs, + onchange: function() { + this.track.tile_cache.clear(); + this.track.draw(); + } + }); + this.prefs = this.track_config.values; + + this.track_type = "ReadTrack"; + this.painter = ReadPainter; + this.make_name_popup_menu(); +}; +$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype); + +/** + * Feature track that displays data generated from tool. + */ +var ToolDataFeatureTrack = function(name, view, hda_ldda, dataset_id, prefs, filters, parent_track) { + FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters, {}, parent_track); + this.track_type = "ToolDataFeatureTrack"; + + // Set up track to fetch initial data from raw data URL when the dataset--not the converted datasets-- + // is ready. + this.data_url = raw_data_url; + this.data_query_wait = 1000; + this.dataset_check_url = dataset_state_url; +}; + +$.extend(ToolDataFeatureTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { + /** + * For this track type, the predraw init sets up postdraw init. + */ + predraw_init: function() { + // Postdraw init: once data has been fetched, reset data url, wait time and start indexing. + var track = this; + var post_init = function() { + if (track.data_cache.size() === 0) { + // Track still drawing initial data, so do nothing. + setTimeout(post_init, 300); + } + else { + // Track drawing done: reset dataset check, data URL, wait time and get dataset state to start + // indexing. + track.data_url = default_data_url; + track.data_query_wait = DEFAULT_DATA_QUERY_WAIT; + track.dataset_state_url = converted_datasets_state_url; + $.getJSON(track.dataset_state_url, {dataset_id : track.dataset_id, hda_ldda: track.hda_ldda}, function(track_data) {}); + } + }; + post_init(); + } +}); + +// ---- To be extracted ------------------------------------------------------ + +// ---- Simple extend function for inheritence ---- + +var extend = function() { + var target = arguments[0]; + for ( var i = 1; i < arguments.length; i++ ) { + var other = arguments[i]; + for ( key in other ) { + target[key] = other[key]; + } + } +} + +// ---- Canvas management ---- + +var CanvasManager = function( document ) { + this.document = document; +} + +CanvasManager.prototype.new_canvas = function() { + var canvas = this.document.createElement("canvas"); + if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK + return canvas; +} + +// ---- Feature Packing ---- + +/** + * FeatureSlotter determines slots in which to draw features for vertical + * packing. + * + * This implementation is incremental, any feature assigned a slot will be + * retained for slotting future features. + */ +var FeatureSlotter = function ( w_scale, include_label, measureText ) { + this.slots = {}; + this.start_end_dct = {}; + this.w_scale = w_scale; + this.include_label = include_label; + this.measureText = measureText; +} + +/** + * Slot a set of features, `this.slots` will be updated with slots by id, and + * the largest slot required for the passed set of features is returned + */ +FeatureSlotter.prototype.slot_features = function( features ) { + var w_scale = this.w_scale, inc_slots = this.slots, start_end_dct = this.start_end_dct, + undone = [], slotted = [], highest_slot = 0; + + // If feature already exists in slots (from previously seen tiles), use the same slot, + // otherwise if not seen, add to "undone" list for slot calculation. + + // TODO: Should calculate zoom tile index, which will improve performance + // by only having to look at a smaller subset + // if (this.start_end_dct[0] === undefined) { this.start_end_dct[0] = []; } + for (var i = 0, len = features.length; i < len; i++) { + var feature = features[i], + feature_uid = feature[0]; + if (inc_slots[feature_uid] !== undefined) { + highest_slot = Math.max(highest_slot, inc_slots[feature_uid]); + slotted.push(inc_slots[feature_uid]); + } else { + undone.push(i); + } + } + + // Slot unslotted features. + + // Find the first slot such that current feature doesn't overlap any other features in that slot. + // Returns -1 if no slot was found. + var find_slot = function(f_start, f_end) { + // TODO: Fix constants + for (var slot_num = 0; slot_num <= MAX_FEATURE_DEPTH; slot_num++) { + var has_overlap = false, + slot = start_end_dct[slot_num]; + if (slot !== undefined) { + // Iterate through features already in slot to see if current feature will fit. + for (var k = 0, k_len = slot.length; k < k_len; k++) { + var s_e = slot[k]; + if (f_end > s_e[0] && f_start < s_e[1]) { + // There is overlap + has_overlap = true; + break; } } - if (!has_overlap) { - return slot_num; + } + if (!has_overlap) { + return slot_num; + } + } + return -1; + }; + + // Do slotting. + for (var i = 0, len = undone.length; i < len; i++) { + var feature = features[undone[i]], + feature_uid = feature[0], + feature_start = feature[1], + feature_end = feature[2], + feature_name = feature[3], + // Where to start, end drawing on screen. + f_start = Math.floor( feature_start * w_scale ), + f_end = Math.ceil( feature_end * w_scale ), + text_len = this.measureText(feature_name).width, + text_align; + + // Update start, end drawing locations to include feature name. + // Try to put the name on the left, if not, put on right. + if (feature_name !== undefined && this.include_label ) { + // Add gap for label spacing and extra pack space padding + // TODO: Fix constants + text_len += (LABEL_SPACING + PACK_SPACING); + if (f_start - text_len >= 0) { + f_start -= text_len; + text_align = "left"; + } else { + f_end += text_len; + text_align = "right"; + } + } + + // Find slot. + var slot_num = find_slot(f_start, f_end); + /* + if (slot_num < 0) { + + TODO: this is not yet working -- + console.log(feature_uid, "looking for slot with text on the right"); + // Slot not found. If text was on left, try on right and see + // if slot can be found. + // TODO: are there any checks we need to do to ensure that text + // will fit on tile? + if (text_align === "left") { + f_start -= text_len; + f_end -= text_len; + text_align = "right"; + slot_num = find_slot(f_start, f_end); + } + if (slot_num >= 0) { + console.log(feature_uid, "found slot with text on the right"); + } + + } + */ + // Do slotting. + if (slot_num >= 0) { + // Add current feature to slot. + if (start_end_dct[slot_num] === undefined) { + start_end_dct[slot_num] = []; + } + start_end_dct[slot_num].push([f_start, f_end]); + inc_slots[feature_uid] = slot_num; + highest_slot = Math.max(highest_slot, slot_num); + } + else { + // TODO: remove this warning when skipped features are handled. + // Show warning for skipped feature. + //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + } + } + + // Debugging: view slots data. + /* + for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { + var slot = start_end_dct[i]; + if (slot !== undefined) { + console.log(i, "*************"); + for (var k = 0, k_len = slot.length; k < k_len; k++) { + console.log("\t", slot[k][0], slot[k][1]); + } + } + } + */ + return highest_slot + 1; +} + +// ---- Painters ---- + +/** + * SummaryTreePainter, a histogram showing number of intervals in a region + */ +var SummaryTreePainter = function( data, delta, max, view_start, view_end, show_counts ) { + // Data and data properties + this.data = data; + this.delta = delta; + this.max = max; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.show_counts = show_counts; +} + +SummaryTreePainter.prototype.draw = function( ctx, width, height ) { + + var view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range; + + var points = this.data, delta = this.delta, max = this.max, + // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases + // start. However, height of each rectangle is relative to required_height; hence, the + // max rectangle is required_height. + base_y = height + LABEL_SPACING + CHAR_HEIGHT_PX; + delta_x_px = Math.ceil(delta * w_scale); + + ctx.save(); + + for (var i = 0, len = points.length; i < len; i++) { + + var x = Math.floor( (points[i][0] - view_start) * w_scale ); + var y = points[i][1]; + + if (!y) { continue; } + var y_px = y / max * height; + + ctx.fillStyle = "black"; + ctx.fillRect( x, base_y - y_px, delta_x_px, y_px ); + + // Draw number count if it can fit the number with some padding, otherwise things clump up + var text_padding_req_x = 4; + if (this.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { + ctx.fillStyle = "#666"; + ctx.textAlign = "center"; + ctx.fillText(y, x + (delta_x_px/2), 10); + } + } + + ctx.restore(); +} + +var LinePainter = function( data, view_start, view_end, min_value, max_value, color, mode ) { + // Data and data properties + this.data = data; + // View + this.view_start = view_start; + this.view_end = view_end; + // Drawing prefs + this.min_value = min_value; + this.max_value = max_value; + this.color = color; + this.mode = mode; +} + +LinePainter.prototype.draw = function( ctx, width, height ) { + var + in_path = false, + min_value = this.min_value, + max_value = this.max_value, + vertical_range = max_value - min_value, + height_px = height, + view_start = this.view_start, + view_range = this.view_end - this.view_start, + w_scale = width / view_range, + mode = this.mode, + data = this.data; + + ctx.save(); + + // Pixel position of 0 on the y axis + var y_zero = Math.round( height + min_value / vertical_range * height ); + + // Line at 0.0 + if ( mode !== "Intensity" ) { + ctx.beginPath(); + ctx.moveTo( 0, y_zero ); + ctx.lineTo( width, y_zero ); + // ctx.lineWidth = 0.5; + ctx.fillStyle = "#aaa"; + ctx.stroke(); + } + + ctx.beginPath(); + ctx.fillStyle = this.color; + var x_scaled, y, delta_x_px; + if (data.length > 1) { + delta_x_px = Math.ceil((data[1][0] - data[0][0]) * w_scale); + } else { + delta_x_px = 10; + } + for (var i = 0, len = data.length; i < len; i++) { + x_scaled = Math.round((data[i][0] - view_start) * w_scale); + y = data[i][1]; + if (y === null) { + if (in_path && mode === "Filled") { + ctx.lineTo(x_scaled, height_px); + } + in_path = false; + continue; + } + if (y < min_value) { + y = min_value; + } else if (y > max_value) { + y = max_value; + } + + if (mode === "Histogram") { + // y becomes the bar height in pixels, which is the negated for canvas coords + y = Math.round( y / vertical_range * height_px ); + ctx.fillRect(x_scaled, y_zero, delta_x_px, - y ); + } else if (mode === "Intensity" ) { + y = 255 - Math.floor( (y - min_value) / vertical_range * 255 ); + ctx.fillStyle = "rgb(" +y+ "," +y+ "," +y+ ")"; + ctx.fillRect(x_scaled, 0, delta_x_px, height_px); + } else { + // console.log(y, track.min_value, track.vertical_range, (y - track.min_value) / track.vertical_range * track.height_px); + y = Math.round( height_px - (y - min_value) / vertical_range * height_px ); + // console.log(canvas.get(0).height, canvas.get(0).width); + if (in_path) { + ctx.lineTo(x_scaled, y); + } else { + in_path = true; + if (mode === "Filled") { + ctx.moveTo(x_scaled, height_px); + ctx.lineTo(x_scaled, y); + } else { + ctx.moveTo(x_scaled, y); } } - return -1; - }; - - // Do slotting. - for (var i = 0, len = undone.length; i < len; i++) { - var feature = features[undone[i]], + } + } + if (mode === "Filled") { + if (in_path) { + ctx.lineTo( x_scaled, y_zero ); + ctx.lineTo( 0, y_zero ); + } + ctx.fill(); + } else { + ctx.stroke(); + } + + ctx.restore(); +} + +var FeaturePainter = function( data, view_start, view_end, prefs, mode ) { + this.data = data; + this.view_start = view_start; + this.view_end = view_end; + this.prefs = prefs; + this.mode = mode; +} + +extend( FeaturePainter.prototype, { + + get_required_height: function( rows_required ) { + // y_scale is the height per row + var required_height = y_scale = this.get_row_height(), mode = this.mode; + // If using a packing mode, need to multiply by the number of slots used + if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { + // Calculate new slots incrementally for this new chunk of data and update height if necessary. + required_height = rows_required * y_scale; + } + // Pad bottom by half a row + return required_height + Math.round( y_scale / 2 ); + }, + + draw: function( ctx, width, height, slots ) { + + var data = this.data, view_start = this.view_start, view_end = this.view_end; + + ctx.save(); + + ctx.fillStyle = this.prefs.block_color; + ctx.textAlign = "right"; + + var view_range = this.view_end - this.view_start, + w_scale = width / view_range, + y_scale = this.get_row_height(); + + for (var i = 0, len = data.length; i < len; i++) { + var feature = data[i], feature_uid = feature[0], feature_start = feature[1], feature_end = feature[2], - feature_name = feature[3], - // Where to start, end drawing on screen. - f_start = Math.floor( (feature_start - max_low) * w_scale ), - f_end = Math.ceil( (feature_end - max_low) * w_scale ), - text_len = CONTEXT.measureText(feature_name).width, - text_align; - - // Update start, end drawing locations to include feature name. - // Try to put the name on the left, if not, put on right. - if (feature_name !== undefined && mode === "Pack") { - // Add gap for label spacing and extra pack space padding - text_len += (LABEL_SPACING + PACK_SPACING); - if (f_start - text_len >= 0) { - f_start -= text_len; - text_align = "left"; - } else { - f_end += text_len; - text_align = "right"; - } - } - - // Find slot. - var slot_num = find_slot(f_start, f_end); - /* - if (slot_num < 0) { + // Slot valid only if features are slotted and this feature is slotted; + // feature may not be due to lack of space. + slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - TODO: this is not yet working -- - console.log(feature_uid, "looking for slot with text on the right"); - // Slot not found. If text was on left, try on right and see - // if slot can be found. - // TODO: are there any checks we need to do to ensure that text - // will fit on tile? - if (text_align === "left") { - f_start -= text_len; - f_end -= text_len; - text_align = "right"; - slot_num = find_slot(f_start, f_end); - } - if (slot_num >= 0) { - console.log(feature_uid, "found slot with text on the right"); - } - - } - */ - // Do slotting. - if (slot_num >= 0) { - // Add current feature to slot. - if (start_end_dct[slot_num] === undefined) { - start_end_dct[slot_num] = []; - } - start_end_dct[slot_num].push([f_start, f_end]); - inc_slots[feature_uid] = slot_num; - highest_slot = Math.max(highest_slot, slot_num); - } - else { - // TODO: remove this warning when skipped features are handled. - // Show warning for skipped feature. - //console.log("WARNING: not displaying feature", feature_uid, f_start, f_end); + // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). + if (is_overlap([feature_start, feature_end], [view_start, view_end]) && (this.mode == "Dense" || slot !== null)) { + this.draw_element(ctx, this.mode, feature, slot, view_start, view_end, w_scale, y_scale, + width ); } } - - // Debugging: view slots data. - /* - for (var i = 0; i < MAX_FEATURE_DEPTH; i++) { - var slot = start_end_dct[i]; - if (slot !== undefined) { - console.log(i, "*************"); - for (var k = 0, k_len = slot.length; k < k_len; k++) { - console.log("\t", slot[k][0], slot[k][1]); - } - } + + ctx.restore(); + } +}); + +var LinkedFeaturePainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( LinkedFeaturePainter.prototype, FeaturePainter.prototype, { + + /** + * Height of a single row, depends on mode + */ + get_row_height: function() { + var mode = this.mode, y_scale; + if (mode === "summary_tree") { + // No scale needed. } - */ - return highest_slot + 1; + if (mode === "Dense") { + y_scale = DENSE_TRACK_HEIGHT; + } + else if (mode === "no_detail") { + y_scale = NO_DETAIL_TRACK_HEIGHT; + } + else if (mode === "Squish") { + y_scale = SQUISH_TRACK_HEIGHT; + } + else { // mode === "Pack" + y_scale = PACK_TRACK_HEIGHT; + } + return y_scale; }, - /** - * Draw summary tree on canvas. - */ - draw_summary_tree: function(canvas, points, delta, max, w_scale, required_height, tile_low, left_offset) { - var - // Set base Y so that max label and data do not overlap. Base Y is where rectangle bases - // start. However, height of each rectangle is relative to required_height; hence, the - // max rectangle is required_height. - base_y = required_height + LABEL_SPACING + CHAR_HEIGHT_PX; - delta_x_px = Math.ceil(delta * w_scale); - - var max_label = $("<div />").addClass('yaxislabel'); - max_label.text(max); - - max_label.css({ position: "absolute", top: "22px", left: "10px" }); - max_label.prependTo(this.container_div); - - var ctx = canvas.get(0).getContext("2d"); - for (var i = 0, len = points.length; i < len; i++) { - var x = Math.floor( (points[i][0] - tile_low) * w_scale ); - var y = points[i][1]; - - if (!y) { continue; } - var y_px = y / max * required_height; - - ctx.fillStyle = "black"; - ctx.fillRect(x + left_offset, base_y - y_px, delta_x_px, y_px); - - // Draw number count if it can fit the number with some padding, otherwise things clump up - var text_padding_req_x = 4; - if (this.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) { - ctx.fillStyle = "#666"; - ctx.textAlign = "center"; - ctx.fillText(y, x + left_offset + (delta_x_px/2), 10); - } - } - }, - /** - * Draw feature. - */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset) { + + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { var feature_uid = feature[0], feature_start = feature[1], @@ -2457,21 +2875,21 @@ feature_name = feature[3], f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null, block_color = this.prefs.block_color, label_color = this.prefs.label_color; - + // Dense mode displays the same for all data. if (mode === "Dense") { ctx.fillStyle = block_color; - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); + ctx.fillRect(f_start, y_center, f_end - f_start, DENSE_FEATURE_HEIGHT); } else if (mode === "no_detail") { // No details for feature, so only one way to display. ctx.fillStyle = block_color; // TODO: what should width be here? - ctx.fillRect(f_start + left_offset, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); + ctx.fillRect(f_start, y_center + 5, f_end - f_start, DENSE_FEATURE_HEIGHT); } else { // Mode is either Squish or Pack: // Feature details. @@ -2508,7 +2926,7 @@ else { // No strand. ctx.fillStyle = block_color; } - ctx.fillRect(f_start + left_offset, y_center, f_end - f_start, thick_height); + ctx.fillRect(f_start, y_center, f_end - f_start, thick_height); } else { // There are feature blocks and mode is either Squish or Pack. // @@ -2540,7 +2958,7 @@ cur_height = 1; } } - ctx.fillRect(f_start + left_offset, cur_y_center, f_end - f_start, cur_height); + ctx.fillRect(f_start, cur_y_center, f_end - f_start, cur_height); for (var k = 0, k_len = feature_blocks.length; k < k_len; k++) { var block = feature_blocks[k], @@ -2553,14 +2971,14 @@ // Draw thin block. ctx.fillStyle = block_color; - ctx.fillRect(block_start + left_offset, y_center + (thick_height-thin_height)/2 + 1, + ctx.fillRect(block_start, y_center + (thick_height-thin_height)/2 + 1, block_end - block_start, thin_height); // If block intersects with thick region, draw block as thick. if (thick_start !== undefined && !(block_start > thick_end || block_end < thick_start) ) { var block_thick_start = Math.max(block_start, thick_start), block_thick_end = Math.min(block_end, thick_end); - ctx.fillRect(block_thick_start + left_offset, y_center + 1, + ctx.fillRect(block_thick_start, y_center + 1, block_thick_end - block_thick_start, thick_height); } } @@ -2569,207 +2987,27 @@ // Draw label for Pack mode. if (mode === "Pack" && feature_start > tile_low) { ctx.fillStyle = label_color; + // FIXME: do this without tile_index + var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING, y_center + 8); + ctx.fillText(feature_name, f_end + LABEL_SPACING, y_center + 8); } else { ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING, y_center + 8); + ctx.fillText(feature_name, f_start - LABEL_SPACING, y_center + 8); } ctx.fillStyle = block_color; } } - }, - /** - * Returns y_scale based on mode. - */ - get_y_scale: function(mode) { - var y_scale; - if (mode === "summary_tree") { - // No scale needed. - } - if (mode === "Dense") { - y_scale = DENSE_TRACK_HEIGHT; - } - else if (mode === "no_detail") { - y_scale = NO_DETAIL_TRACK_HEIGHT; - } - else if (mode === "Squish") { - y_scale = SQUISH_TRACK_HEIGHT; - } - else { // mode === "Pack" - y_scale = PACK_TRACK_HEIGHT; - } - return y_scale; - }, - /** - * Draw FeatureTrack tile. - */ - draw_tile: function(result, resolution, tile_index, parent_element, w_scale, ref_seq) { - var track = this; - var tile_low = tile_index * DENSITY * resolution, - tile_high = ( tile_index + 1 ) * DENSITY * resolution, - tile_span = tile_high - tile_low, - params = { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, - resolution: this.view.resolution, mode: this.mode }; - - // - // Create/set/compute some useful vars. - // - var width = Math.ceil( tile_span * w_scale ), - mode = this.mode, - min_height = 25, - left_offset = this.left_offset, - slots, required_height; - - var canvas = document.createElement("canvas"); - if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); } // EXCANVAS HACK - canvas = $(canvas); - - // - // Set mode if Auto. - // - if (mode === "Auto") { - if (result.dataset_type === "summary_tree") { - mode = result.dataset_type; - } else if (result.extra_info === "no_detail") { - mode = "no_detail"; - } else { - // Choose b/t Squish and Pack. - // Proxy measures for using Squish: - // (a) error message re: limiting number of features shown; - // (b) X number of features shown; - // (c) size of view shown. - // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles; - // fix this so that tiles are redrawn as necessary to use the same mode. - //if ( (result.message && result.message.match(/^Only the first [\d]+/)) || - // (result.data && result.data.length > 2000) || - var data = result.data; - if ( (data.length && data.length < 4) || - (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) { - mode = "Squish"; - } else { - mode = "Pack"; - } - } - } - - var y_scale = this.get_y_scale(mode); - - // - // Pack reads, set required height. - // - if (mode === "summary_tree") { - required_height = this.summary_draw_height; - } - if (mode === "Dense") { - required_height = min_height; - } - else if (mode === "no_detail" || mode === "Squish" || mode === "Pack") { - // Calculate new slots incrementally for this new chunk of data and update height if necessary. - required_height = this.incremental_slots(w_scale, result.data, mode) * y_scale + min_height; - slots = this.inc_slots[w_scale]; - } - - // - // Set up for drawing. - // - canvas.get(0).width = width + left_offset; - canvas.get(0).height = required_height; - if (result.dataset_type === "summary_tree") { - // Increase canvas height in order to display max label. - canvas.get(0).height += LABEL_SPACING + CHAR_HEIGHT_PX; - } - parent_element.parent().css("height", Math.max(this.height_px, required_height) + "px"); - // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale); - var ctx = canvas.get(0).getContext("2d"); - ctx.fillStyle = this.prefs.block_color; - ctx.font = this.default_font; - ctx.textAlign = "right"; - this.container_div.find(".yaxislabel").remove(); - - // - // Draw summary tree. If tree is drawn, canvas is returned. - // - if (mode === "summary_tree") { - this.draw_summary_tree(canvas, result.data, result.delta, result.max, w_scale, required_height, - tile_low, left_offset); - return canvas; - } - - // - // If there is a message, draw it on canvas so that it moves around with canvas, and make the border red - // to indicate region where message is applicable - if (result.message) { - canvas.css({ - "border-top": "1px solid red" - }); - - ctx.fillStyle = "red"; - ctx.textAlign = "left"; - var old_base = ctx.textBaseline; - ctx.textBaseline = "top"; - ctx.fillText(result.message, left_offset, 0); - ctx.textBaseline = old_base; - - // If there's no data, return. - if (!result.data) { - return canvas; - } - } - - // - // Set example feature. This is needed so that track can update its UI based on feature attributes. - // - this.example_feature = (result.data.length ? result.data[0] : undefined); - - // - // Draw elements. - // - var data = result.data; - for (var i = 0, len = data.length; i < len; i++) { - var feature = data[i], - feature_uid = feature[0], - feature_start = feature[1], - feature_end = feature[2], - // Slot valid only if features are slotted and this feature is slotted; - // feature may not be due to lack of space. - slot = (slots && slots[feature_uid] !== undefined ? slots[feature_uid] : null); - - // Apply filters to feature. - var hide_feature = false; - var filter; - for (var f = 0; f < this.filters.length; f++) { - filter = this.filters[f]; - filter.update_attrs(feature); - if (!filter.keep(feature)) { - hide_feature = true; - break; - } - } - if (hide_feature) { - continue; - } - - // Draw feature if there's overlap and mode is dense or feature is slotted (as it must be for all non-dense modes). - if (is_overlap([feature_start, feature_end], [tile_low, tile_high]) && (mode == "Dense" || slot !== null)) { - this.draw_element(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, - width, left_offset, ref_seq); - } - } - return canvas; } }); -var VcfTrack = function(name, view, hda_ldda, dataset_id, prefs, filters) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); - this.track_type = "VcfTrack"; -}; -$.extend(VcfTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * Draw a VCF entry. - */ +var VariantPainter = function( data, view_start, view_end, prefs, mode ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); +} + +extend( VariantPainter.prototype, FeaturePainter.prototype, { draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width) { var feature = data[i], feature_uid = feature[0], @@ -2780,7 +3018,7 @@ // All features need a start, end, and vertical center. f_start = Math.floor( Math.max(0, (feature_start - tile_low) * w_scale) ), f_end = Math.ceil( Math.min(width, Math.max(0, (feature_end - tile_low) * w_scale)) ), - y_center = ERROR_PADDING + (mode === "Dense" ? 0 : (0 + slot)) * y_scale, + y_center = (mode === "Dense" ? 0 : (0 + slot)) * y_scale, thickness, y_start, thick_start = null, thick_end = null; if (no_label) { @@ -2821,36 +3059,17 @@ } }); -var ReadTrack = function (name, view, hda_ldda, dataset_id, prefs, filters) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters); - - this.track_config = new TrackConfig( { - track: this, - params: [ - { key: 'block_color', label: 'Block color', type: 'color', default_value: '#444' }, - { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' }, - { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false }, - { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true }, - { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true }, - { key: 'mode', type: 'string', default_value: this.mode, hidden: true }, - ], - saved_values: prefs, - onchange: function() { - this.track.tile_cache.clear(); - this.track.draw(); - } - }); - this.prefs = this.track_config.values; - - this.track_type = "ReadTrack"; - this.make_name_popup_menu(); -}; -$.extend(ReadTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { +var ReadPainter = function( data, view_start, view_end, prefs, mode, ref_seq ) { + FeaturePainter.call( this, data, view_start, view_end, prefs, mode ); + this.ref_seq = ref_seq; +} + +extend( ReadPainter.prototype, FeaturePainter.prototype, { /** * Returns y_scale based on mode. */ - get_y_scale: function(mode) { - var y_scale; + get_row_height: function() { + var y_scale, mode = this.mode; if (mode === "summary_tree") { // No scale needed. } @@ -2862,7 +3081,7 @@ } else { // mode === "Pack" y_scale = PACK_TRACK_HEIGHT; - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { y_scale *= 2; } } @@ -2871,13 +3090,14 @@ /** * Draw a single read. */ - draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center, ref_seq) { + draw_read: function(ctx, mode, w_scale, tile_low, tile_high, feature_start, cigar, orig_seq, y_center) { ctx.textAlign = "center"; var track = this, tile_region = [tile_low, tile_high], base_offset = 0, seq_offset = 0, - gap = 0; + gap = 0 + ref_seq = this.ref_seq; // Keep list of items that need to be drawn on top of initial drawing layer. var draw_last = []; @@ -2918,12 +3138,12 @@ var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); if (gap > 0) { ctx.fillStyle = this.prefs.block_color; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 1, s_end - s_start, 9); + ctx.fillRect(s_start - gap, y_center + 1, s_end - s_start, 9); ctx.fillStyle = CONNECTOR_COLOR; // TODO: this can be made much more efficient by computing the complete sequence // to draw and then drawing it. for (var c = 0, str_len = seq.length; c < str_len; c++) { - if (this.track_config.values.show_differences && ref_seq) { + if (this.prefs.show_differences && ref_seq) { var ref_char = ref_seq[seq_start - tile_low + c]; if (!ref_char || ref_char.toLowerCase() === seq[c].toLowerCase()) { continue; @@ -2931,13 +3151,13 @@ } if (seq_start + c >= tile_low && seq_start + c <= tile_high) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset, y_center + 9); + ctx.fillText(seq[c], c_start, y_center + 9); } } } else { ctx.fillStyle = this.prefs.block_color; // TODO: This is a pretty hack-ish way to fill rectangle based on mode. - ctx.fillRect(s_start + this.left_offset, + ctx.fillRect(s_start, y_center + (this.mode !== "Dense" ? 4 : 5), s_end - s_start, (mode !== "Dense" ? SQUISH_FEATURE_HEIGHT : DENSE_FEATURE_HEIGHT) ); @@ -2948,14 +3168,14 @@ break; case "N": // Skipped bases. ctx.fillStyle = CONNECTOR_COLOR; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 5, s_end - s_start, 1); + ctx.fillRect(s_start - gap, y_center + 5, s_end - s_start, 1); //ctx.dashedLine(s_start + this.left_offset, y_center + 5, this.left_offset + s_end, y_center + 5); // No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; case "D": // Deletion. ctx.fillStyle = "red"; - ctx.fillRect(s_start + this.left_offset - gap, y_center + 4, s_end - s_start, 3); + ctx.fillRect(s_start - gap, y_center + 4, s_end - s_start, 3); // TODO: is this true? No change in seq_offset because sequence not used when skipping. base_offset += cig_len; break; @@ -2967,20 +3187,20 @@ // the sequence region and the tile region. var seq_tile_overlap = compute_overlap([seq_start, seq_start + cig_len], tile_region), - insert_x_coord = this.left_offset + s_start - gap; + insert_x_coord = s_start - gap; if (seq_tile_overlap !== NO_OVERLAP) { var seq = orig_seq.slice(seq_offset, seq_offset + cig_len); // Insertion point is between the sequence start and the previous base: (-gap) moves // back from sequence start to insertion point. - if (this.track_config.values.show_insertions) { + if (this.prefs.show_insertions) { // // Show inserted sequence above, centered on insertion point. // // Draw sequence. // X center is offset + start - <half_sequence_length> - var x_center = this.left_offset + s_start - (s_end - s_start)/2; + var x_center = s_start - (s_end - s_start)/2; if ( (mode === "Pack" || this.mode === "Auto") && orig_seq !== undefined && w_scale > CHAR_WIDTH_PX) { // Draw sequence container. ctx.fillStyle = "yellow"; @@ -3005,7 +3225,7 @@ // Draw sequence. for (var c = 0, str_len = seq.length; c < str_len; c++) { var c_start = Math.floor( Math.max(0, (seq_start + c - tile_low) * w_scale) ); - ctx.fillText(seq[c], c_start + this.left_offset - (s_end - s_start)/2, y_center); + ctx.fillText(seq[c], c_start - (s_end - s_start)/2, y_center); } } else { @@ -3056,9 +3276,9 @@ } }, /** - * Draw a complete read. + * Draw a complete read pair */ - draw_element: function(ctx, tile_index, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width, left_offset, ref_seq) { + draw_element: function(ctx, mode, feature, slot, tile_low, tile_high, w_scale, y_scale, width ) { // All features need a start, end, and vertical center. var feature_uid = feature[0], feature_start = feature[1], @@ -3088,74 +3308,35 @@ // Draw left/forward read. if (feature[4][1] >= tile_low && feature[4][0] <= tile_high && feature[4][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[4][0], feature[4][2], feature[4][3], y_center); } // Draw right/reverse read. if (feature[5][1] >= tile_low && feature[5][0] <= tile_high && feature[5][2]) { - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature[5][0], feature[5][2], feature[5][3], y_center); } // Draw connector. if (b2_start > b1_end) { ctx.fillStyle = CONNECTOR_COLOR; - ctx.dashedLine(b1_end + left_offset - gap, y_center + 5, left_offset + b2_start - gap, y_center + 5); + ctx.dashedLine(b1_end - gap, y_center + 5, b2_start - gap, y_center + 5); } } else { // Read is single. ctx.fillStyle = block_color; - this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center, ref_seq); + this.draw_read(ctx, mode, w_scale, tile_low, tile_high, feature_start, feature[4], feature[5], y_center); } if (mode === "Pack" && feature_start > tile_low) { // Draw label. ctx.fillStyle = this.prefs.label_color; + // FIXME: eliminate tile_index + var tile_index = 1; if (tile_index === 0 && f_start - ctx.measureText(feature_name).width < 0) { ctx.textAlign = "left"; - ctx.fillText(feature_name, f_end + left_offset + LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_end + LABEL_SPACING - gap, y_center + 8); } else { ctx.textAlign = "right"; - ctx.fillText(feature_name, f_start + left_offset - LABEL_SPACING - gap, y_center + 8); + ctx.fillText(feature_name, f_start - LABEL_SPACING - gap, y_center + 8); } ctx.fillStyle = block_color; } } }); - -/** - * Feature track that displays data generated from tool. - */ -var ToolDataFeatureTrack = function(name, view, hda_ldda, dataset_id, prefs, filters, parent_track) { - FeatureTrack.call(this, name, view, hda_ldda, dataset_id, prefs, filters, {}, parent_track); - this.track_type = "ToolDataFeatureTrack"; - - // Set up track to fetch initial data from raw data URL when the dataset--not the converted datasets-- - // is ready. - this.data_url = raw_data_url; - this.data_query_wait = 1000; - this.dataset_check_url = dataset_state_url; -}; - -$.extend(ToolDataFeatureTrack.prototype, TiledTrack.prototype, FeatureTrack.prototype, { - /** - * For this track type, the predraw init sets up postdraw init. - */ - predraw_init: function() { - // Postdraw init: once data has been fetched, reset data url, wait time and start indexing. - var track = this; - var post_init = function() { - if (track.data_cache.size() === 0) { - // Track still drawing initial data, so do nothing. - setTimeout(post_init, 300); - } - else { - // Track drawing done: reset dataset check, data URL, wait time and get dataset state to start - // indexing. - track.data_url = default_data_url; - track.data_query_wait = DEFAULT_DATA_QUERY_WAIT; - track.dataset_state_url = converted_datasets_state_url; - $.getJSON(track.dataset_state_url, {dataset_id : track.dataset_id, hda_ldda: track.hda_ldda}, function(track_data) {}); - } - }; - post_init(); - } -}); - - http://bitbucket.org/galaxy/galaxy-central/changeset/a67e3e47db5d/ changeset: r5319:a67e3e47db5d user: kanwei date: 2011-04-01 01:01:12 summary: pack js affected #: 1 file (2.2 KB) --- a/static/scripts/packed/trackster.js Thu Mar 31 18:58:25 2011 -0400 +++ b/static/scripts/packed/trackster.js Thu Mar 31 19:01:12 2011 -0400 @@ -1,1 +1,1 @@ -CanvasRenderingContext2D.prototype.dashedLine=function(c,j,b,h,f){if(f===undefined){f=4}var e=b-c;var d=h-j;var g=Math.floor(Math.sqrt(e*e+d*d)/f);var l=e/g;var k=d/g;var a;for(a=0;a<g;a++,c+=l,j+=k){if(a%2!==0){continue}this.fillRect(c,j,f,1)}};CanvasRenderingContext2D.prototype.drawDownwardEquilateralTriangle=function(b,a,e){var d=b-e/2,c=b+e/2,f=a-Math.sqrt(e*3/2);this.beginPath();this.moveTo(d,f);this.lineTo(c,f);this.lineTo(b,a);this.lineTo(d,f);this.strokeStyle=this.fillStyle;this.fill();this.stroke();this.closePath()};function sortable(a,b){a.bind("drag",{handle:b,relative:true},function(h,j){var g=$(this).parent();var f=g.children();var c;for(c=0;c<f.length;c++){if(j.offsetY<$(f.get(c)).position().top){break}}if(c===f.length){if(this!==f.get(c-1)){g.append(this)}}else{if(this!==f.get(c)){$(this).insertBefore(f.get(c))}}})}var NO_OVERLAP=1001,CONTAINS=1002,OVERLAP_START=1003,OVERLAP_END=1004,CONTAINED_BY=1005;function compute_overlap(e,b){var g=e[0],f=e[1],d=b[0],c=b[1],a;if(g<d){if(f<d){a=NO_OVERLAP}else{if(f<=c){a=OVERLAP_START}else{a=CONTAINS}}}else{if(g>c){a=NO_OVERLAP}else{if(f<=c){a=CONTAINED_BY}else{a=OVERLAP_END}}}return a}function is_overlap(b,a){return(compute_overlap(b,a)!==NO_OVERLAP)}function get_slider_step(c,a){var b=a-c;return(b<=1?0.01:(b<=1000?1:5))}var DENSE_TRACK_HEIGHT=10,NO_DETAIL_TRACK_HEIGHT=3,SQUISH_TRACK_HEIGHT=5,PACK_TRACK_HEIGHT=10,NO_DETAIL_FEATURE_HEIGHT=1,DENSE_FEATURE_HEIGHT=1,SQUISH_FEATURE_HEIGHT=3,PACK_FEATURE_HEIGHT=9,ERROR_PADDING=10,LABEL_SPACING=2,PACK_SPACING=5,MIN_SQUISH_VIEW_WIDTH=12000,DEFAULT_FONT="9px Monaco, Lucida Console, monospace",DENSITY=200,FEATURE_LEVELS=10,MAX_FEATURE_DEPTH=100,DEFAULT_DATA_QUERY_WAIT=5000,MAX_CHROMS_SELECTABLE=100,CONNECTOR_COLOR="#ccc",DATA_ERROR="There was an error in indexing this dataset. ",DATA_NOCONVERTER="A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",DATA_NONE="No data for this chrom/contig.",DATA_PENDING="Currently indexing... please wait",DATA_CANNOT_RUN_TOOL="Tool cannot be rerun: ",DATA_LOADING="Loading data...",DATA_OK="Ready for display",CACHED_TILES_FEATURE=10,CACHED_TILES_LINE=5,CACHED_DATA=5,DUMMY_CANVAS=document.createElement("canvas"),RIGHT_STRAND,LEFT_STRAND;if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(DUMMY_CANVAS)}var CONTEXT=DUMMY_CANVAS.getContext("2d");CONTEXT.font=DEFAULT_FONT;var CHAR_WIDTH_PX=CONTEXT.measureText("A").width,CHAR_HEIGHT_PX=9;var right_img=new Image();right_img.src=image_path+"/visualization/strand_right.png";right_img.onload=function(){RIGHT_STRAND=CONTEXT.createPattern(right_img,"repeat")};var left_img=new Image();left_img.src=image_path+"/visualization/strand_left.png";left_img.onload=function(){LEFT_STRAND=CONTEXT.createPattern(left_img,"repeat")};var right_img_inv=new Image();right_img_inv.src=image_path+"/visualization/strand_right_inv.png";right_img_inv.onload=function(){RIGHT_STRAND_INV=CONTEXT.createPattern(right_img_inv,"repeat")};var left_img_inv=new Image();left_img_inv.src=image_path+"/visualization/strand_left_inv.png";left_img_inv.onload=function(){LEFT_STRAND_INV=CONTEXT.createPattern(left_img_inv,"repeat")};function round_1000(a){return Math.round(a*1000)/1000}var Cache=function(a){this.num_elements=a;this.clear()};$.extend(Cache.prototype,{get:function(b){var a=this.key_ary.indexOf(b);if(a!==-1){this.move_key_to_end(b,a)}return this.obj_cache[b]},set:function(b,c){if(!this.obj_cache[b]){if(this.key_ary.length>=this.num_elements){var a=this.key_ary.shift();delete this.obj_cache[a]}this.key_ary.push(b)}this.obj_cache[b]=c;return c},move_key_to_end:function(b,a){this.key_ary.splice(a,1);this.key_ary.push(b)},clear:function(){this.obj_cache={};this.key_ary=[]},size:function(){return this.key_ary.length}});var DataManager=function(b,a,c){Cache.call(this,b);this.track=a;this.subset=(c!==undefined?c:true)};$.extend(DataManager.prototype,Cache.prototype,{load_data:function(h,j,d,g,a,f){var c={chrom:h,low:j,high:d,mode:g,resolution:a,dataset_id:this.track.dataset_id,hda_ldda:this.track.hda_ldda};$.extend(c,f);var k=[];for(var e=0;e<this.track.filters.length;e++){k[k.length]=this.track.filters[e].name}c.filter_cols=JSON.stringify(k);var b=this;return $.getJSON(this.track.data_url,c,function(l){b.set_data(j,d,g,l)})},get_data:function(j,k,c,h,b,f){var l=this.get(this.gen_key(k,c,h));if(l){return l}if(this.subset){var m,g,a,e,h,l;for(var d=0;d<this.key_ary.length;d++){m=this.key_ary[d];g=this.split_key(m);a=g[0];e=g[1];if(k>=a&&c<=e){l=this.obj_cache[m];if(l.dataset_type!=="summary_tree"&&l.extra_info!=="no_detail"){this.move_key_to_end(m,d);return l}}}}return this.load_data(j,k,c,h,b,f)},set_data:function(b,c,d,a){return this.set(this.gen_key(b,c,d),a)},gen_key:function(a,c,d){var b=a+"_"+c+"_"+d;return b},split_key:function(a){return a.split("_")}});var View=function(a,d,c,b,e){this.container=a;this.chrom=null;this.vis_id=c;this.dbkey=b;this.title=d;this.tracks=[];this.label_tracks=[];this.max_low=0;this.max_high=0;this.num_tracks=0;this.track_id_counter=0;this.zoom_factor=3;this.min_separation=30;this.has_changes=false;this.init(e);this.reset()};$.extend(View.prototype,{init:function(d){var c=this.container,a=this;this.top_container=$("<div/>").addClass("top-container").appendTo(c);this.content_div=$("<div/>").addClass("content").css("position","relative").appendTo(c);this.bottom_container=$("<div/>").addClass("bottom-container").appendTo(c);this.top_labeltrack=$("<div/>").addClass("top-labeltrack").appendTo(this.top_container);this.viewport_container=$("<div/>").addClass("viewport-container").addClass("viewport-container").appendTo(this.content_div);this.intro_div=$("<div/>").addClass("intro").text("Select a chrom from the dropdown below").hide();this.nav_labeltrack=$("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);this.nav_container=$("<div/>").addClass("nav-container").prependTo(this.top_container);this.nav=$("<div/>").addClass("nav").appendTo(this.nav_container);this.overview=$("<div/>").addClass("overview").appendTo(this.bottom_container);this.overview_viewport=$("<div/>").addClass("overview-viewport").appendTo(this.overview);this.overview_close=$("<a href='javascript:void(0);'>Close Overview</a>").addClass("overview-close").hide().appendTo(this.overview_viewport);this.overview_highlight=$("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);this.overview_box_background=$("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);this.overview_box=$("<div/>").addClass("overview-box").appendTo(this.overview_viewport);this.default_overview_height=this.overview_box.height();this.nav_controls=$("<div/>").addClass("nav-controls").appendTo(this.nav);this.chrom_select=$("<select/>").attr({name:"chrom"}).css("width","15em").addClass("no-autocomplete").append("<option value=''>Loading</option>").appendTo(this.nav_controls);var b=function(f){if(f.type==="focusout"||(f.keyCode||f.which)===13||(f.keyCode||f.which)===27){if((f.keyCode||f.which)!==27){a.go_to($(this).val())}$(this).hide();$(this).val("");a.location_span.show();a.chrom_select.show()}};this.nav_input=$("<input/>").addClass("nav-input").hide().bind("keyup focusout",b).appendTo(this.nav_controls);this.location_span=$("<span/>").addClass("location").appendTo(this.nav_controls);this.location_span.bind("click",function(){a.location_span.hide();a.chrom_select.hide();a.nav_input.val(a.chrom+":"+a.low+"-"+a.high);a.nav_input.css("display","inline-block");a.nav_input.select();a.nav_input.focus()});if(this.vis_id!==undefined){this.hidden_input=$("<input/>").attr("type","hidden").val(this.vis_id).appendTo(this.nav_controls)}this.zo_link=$("<a id='zoom-out' />").click(function(){a.zoom_out();a.redraw()}).appendTo(this.nav_controls);this.zi_link=$("<a id='zoom-in' />").click(function(){a.zoom_in();a.redraw()}).appendTo(this.nav_controls);this.load_chroms({low:0},d);this.chrom_select.bind("change",function(){a.change_chrom(a.chrom_select.val())});this.intro_div.show();this.content_div.bind("click",function(f){$(this).find("input").trigger("blur")});this.content_div.bind("dblclick",function(f){a.zoom_in(f.pageX,this.viewport_container)});this.overview_box.bind("dragstart",function(f,g){this.current_x=g.offsetX}).bind("drag",function(f,h){var j=h.offsetX-this.current_x;this.current_x=h.offsetX;var g=Math.round(j/a.viewport_container.width()*(a.max_high-a.max_low));a.move_delta(-g)});this.overview_close.bind("click",function(){for(var f=0,e=a.tracks.length;f<e;f++){a.tracks[f].is_overview=false}$(this).siblings().filter("canvas").remove();$(this).parent().css("height",a.overview_box.height());a.overview_highlight.hide();$(this).hide()});this.viewport_container.bind("draginit",function(f,g){if(f.clientX>a.viewport_container.width()-16){return false}}).bind("dragstart",function(f,g){g.original_low=a.low;g.current_height=f.clientY;g.current_x=g.offsetX}).bind("drag",function(h,k){var f=$(this);var l=k.offsetX-k.current_x;var g=f.scrollTop()-(h.clientY-k.current_height);f.scrollTop(g);k.current_height=h.clientY;k.current_x=k.offsetX;var j=Math.round(l/a.viewport_container.width()*(a.high-a.low));a.move_delta(j)}).bind("mousewheel",function(h,k,g,f){if(g){var j=Math.round(-g/a.viewport_container.width()*(a.high-a.low));a.move_delta(j)}});this.top_labeltrack.bind("dragstart",function(f,g){return $("<div />").css({height:a.content_div.height()+a.top_labeltrack.height()+a.nav_labeltrack.height()+1,top:"0px",position:"absolute","background-color":"#ccf",opacity:0.5,"z-index":1000}).appendTo($(this))}).bind("drag",function(k,l){$(l.proxy).css({left:Math.min(k.pageX,l.startX),width:Math.abs(k.pageX-l.startX)});var g=Math.min(k.pageX,l.startX)-a.container.offset().left,f=Math.max(k.pageX,l.startX)-a.container.offset().left,j=(a.high-a.low),h=a.viewport_container.width();a.update_location(Math.round(g/h*j)+a.low,Math.round(f/h*j)+a.low)}).bind("dragend",function(l,m){var g=Math.min(l.pageX,m.startX),f=Math.max(l.pageX,m.startX),j=(a.high-a.low),h=a.viewport_container.width(),k=a.low;a.low=Math.round(g/h*j)+k;a.high=Math.round(f/h*j)+k;$(m.proxy).remove();a.redraw()});this.add_label_track(new LabelTrack(this,this.top_labeltrack));this.add_label_track(new LabelTrack(this,this.nav_labeltrack));$(window).bind("resize",function(){a.resize_window()});$(document).bind("redraw",function(){a.redraw()});this.reset();$(window).trigger("resize")},update_location:function(a,b){this.location_span.text(commatize(a)+" - "+commatize(b));this.nav_input.val(this.chrom+":"+commatize(a)+"-"+commatize(b))},load_chroms:function(b,c){b.num=MAX_CHROMS_SELECTABLE;$.extend(b,(this.vis_id!==undefined?{vis_id:this.vis_id}:{dbkey:this.dbkey}));var a=this;$.ajax({url:chrom_url,data:b,dataType:"json",success:function(e){if(e.chrom_info.length===0){alert("Invalid chromosome: "+b.chrom);return}if(e.reference){a.add_label_track(new ReferenceTrack(a))}a.chrom_data=e.chrom_info;var h='<option value="">Select Chrom/Contig</option>';for(var g=0,d=a.chrom_data.length;g<d;g++){var f=a.chrom_data[g].chrom;h+='<option value="'+f+'">'+f+"</option>"}if(e.prev_chroms){h+='<option value="previous">Previous '+MAX_CHROMS_SELECTABLE+"</option>"}if(e.next_chroms){h+='<option value="next">Next '+MAX_CHROMS_SELECTABLE+"</option>"}a.chrom_select.html(h);if(c){c()}a.chrom_start_index=e.start_index},error:function(){alert("Could not load chroms for this dbkey:",a.dbkey)}})},change_chrom:function(e,b,g){if(!e||e==="None"){return}var d=this;if(e==="previous"){d.load_chroms({low:this.chrom_start_index-MAX_CHROMS_SELECTABLE});return}if(e==="next"){d.load_chroms({low:this.chrom_start_index+MAX_CHROMS_SELECTABLE});return}var f=$.grep(d.chrom_data,function(j,k){return j.chrom===e})[0];if(f===undefined){d.load_chroms({chrom:e},function(){d.change_chrom(e,b,g)});return}else{if(e!==d.chrom){d.chrom=e;if(!d.chrom){d.intro_div.show()}else{d.intro_div.hide()}d.chrom_select.val(d.chrom);d.max_high=f.len-1;d.reset();d.redraw(true);for(var h=0,a=d.tracks.length;h<a;h++){var c=d.tracks[h];if(c.init){c.init()}}}if(b!==undefined&&g!==undefined){d.low=Math.max(b,0);d.high=Math.min(g,d.max_high)}d.reset_overview();d.redraw()}},go_to:function(f){var k=this,a,d,b=f.split(":"),h=b[0],j=b[1];if(j!==undefined){try{var g=j.split("-");a=parseInt(g[0].replace(/,/g,""),10);d=parseInt(g[1].replace(/,/g,""),10)}catch(c){return false}}k.change_chrom(h,a,d)},move_fraction:function(c){var a=this;var b=a.high-a.low;this.move_delta(c*b)},move_delta:function(c){var a=this;var b=a.high-a.low;if(a.low-c<a.max_low){a.low=a.max_low;a.high=a.max_low+b}else{if(a.high-c>a.max_high){a.high=a.max_high;a.low=a.max_high-b}else{a.high-=c;a.low-=c}}a.redraw()},add_track:function(a){a.view=this;a.track_id=this.track_id_counter;this.tracks.push(a);if(a.init){a.init()}a.container_div.attr("id","track_"+a.track_id);sortable(a.container_div,".draghandle");this.track_id_counter+=1;this.num_tracks+=1},add_label_track:function(a){a.view=this;this.label_tracks.push(a)},remove_track:function(a){this.has_changes=true;a.container_div.fadeOut("slow",function(){$(this).remove()});delete this.tracks[this.tracks.indexOf(a)];this.num_tracks-=1},reset:function(){this.low=this.max_low;this.high=this.max_high;this.viewport_container.find(".yaxislabel").remove()},redraw:function(h){var g=this.high-this.low,f=this.low,b=this.high;if(f<this.max_low){f=this.max_low}if(b>this.max_high){b=this.max_high}if(this.high!==0&&g<this.min_separation){b=f+this.min_separation}this.low=Math.floor(f);this.high=Math.ceil(b);this.resolution=Math.pow(10,Math.ceil(Math.log((this.high-this.low)/200)/Math.LN10));this.zoom_res=Math.pow(FEATURE_LEVELS,Math.max(0,Math.ceil(Math.log(this.resolution,FEATURE_LEVELS)/Math.log(FEATURE_LEVELS))));var a=(this.low/(this.max_high-this.max_low)*this.overview_viewport.width())||0;var e=((this.high-this.low)/(this.max_high-this.max_low)*this.overview_viewport.width())||0;var j=13;this.overview_box.css({left:a,width:Math.max(j,e)}).show();if(e<j){this.overview_box.css("left",a-(j-e)/2)}if(this.overview_highlight){this.overview_highlight.css({left:a,width:e})}this.update_location(this.low,this.high);if(!h){for(var c=0,d=this.tracks.length;c<d;c++){if(this.tracks[c]&&this.tracks[c].enabled){this.tracks[c].draw()}}for(c=0,d=this.label_tracks.length;c<d;c++){this.label_tracks[c].draw()}}},zoom_in:function(b,c){if(this.max_high===0||this.high-this.low<this.min_separation){return}var d=this.high-this.low,e=d/2+this.low,a=(d/this.zoom_factor)/2;if(b){e=b/this.viewport_container.width()*(this.high-this.low)+this.low}this.low=Math.round(e-a);this.high=Math.round(e+a);this.redraw()},zoom_out:function(){if(this.max_high===0){return}var b=this.high-this.low,c=b/2+this.low,a=(b*this.zoom_factor)/2;this.low=Math.round(c-a);this.high=Math.round(c+a);this.redraw()},resize_window:function(){this.viewport_container.height(this.container.height()-this.top_container.height()-this.bottom_container.height());this.nav_container.width(this.container.width());this.redraw()},reset_overview:function(){this.overview_viewport.find("canvas").remove();this.overview_viewport.height(this.default_overview_height);this.overview_box.height(this.default_overview_height);this.overview_close.hide();this.overview_highlight.hide()}});var Tool=function(g){this.name=g.name;this.params=[];var a=g.params;for(var e=0;e<a.length;e++){var d=a[e],c=d.name,b=d.label,f=d.type;if(f==="int"||f==="float"){this.params[this.params.length]=new NumberParameter(c,b,d.min,d.max,d.value)}else{if(f=="select"){this.params[this.params.length]=new SelectParameter(c,b,d.options)}else{console.log("WARNING: unrecognized tool parameter type:",c,f)}}}};$.extend(Tool.prototype,{get_param_values_dict:function(){var b={};for(var a=0;a<this.params.length;a++){var c=this.params[a];b[c.name]=JSON.stringify(c.value)}return b},get_param_values:function(){var b=[];for(var a=0;a<this.params.length;a++){b[a]=this.params[a].value}return b}});var NumberParameter=function(c,b,e,a,d){this.name=c;this.label=b;this.min=e;this.max=a;this.value=d};var SelectParameter=function(c,b,a){this.name=c;this.label=b;this.options=a;this.value=(a.length!==0?a[0][1]:null)};var Filter=function(b,a,c){this.name=b;this.index=a;this.value=c};var NumberFilter=function(b,a){this.name=b;this.index=a;this.low=-Number.MAX_VALUE;this.high=Number.MAX_VALUE;this.min=Number.MAX_VALUE;this.max=-Number.MAX_VALUE;this.slider=null;this.slider_label=null};$.extend(NumberFilter.prototype,{applies_to:function(a){if(a.length>this.index){return true}return false},keep:function(a){if(!this.applies_to(a)){return true}return(a[this.index]>=this.low&&a[this.index]<=this.high)},update_attrs:function(b){var a=false;if(!this.applies_to(b)){return a}if(b[this.index]<this.min){this.min=Math.floor(b[this.index]);a=true}if(b[this.index]>this.max){this.max=Math.ceil(b[this.index]);a=true}return a},update_ui_elt:function(){var b=this.slider.slider("option","min"),a=this.slider.slider("option","max");if(this.min<b||this.max>a){this.slider.slider("option","min",this.min);this.slider.slider("option","max",this.max);this.slider.slider("option","step",get_slider_step(this.min,this.max));this.slider.slider("option","values",[this.min,this.max])}}});var get_filters_from_dict=function(a){var g=[];for(var d=0;d<a.length;d++){var f=a[d];var c=f.name,e=f.type,b=f.index;if(e==="int"||e==="float"){g[d]=new NumberFilter(c,b)}else{g[d]=new Filter(c,b,e)}}return g};var TrackConfig=function(a){this.track=a.track;this.params=a.params;this.values={};if(a.saved_values){this.restore_values(a.saved_values)}this.onchange=a.onchange};$.extend(TrackConfig.prototype,{restore_values:function(a){var b=this;$.each(this.params,function(c,d){if(a[d.key]!==undefined){b.values[d.key]=a[d.key]}else{b.values[d.key]=d.default_value}})},build_form:function(){var b=this;var a=$("<div />");$.each(this.params,function(f,d){if(!d.hidden){var c="param_"+f;var l=$("<div class='form-row' />").appendTo(a);l.append($("<label />").attr("for",c).text(d.label+":"));if(d.type==="bool"){l.append($('<input type="checkbox" />').attr("id",c).attr("name",c).attr("checked",b.values[d.key]))}else{if(d.type==="color"){var h=b.values[d.key];var g=$("<input />").attr("id",c).attr("name",c).val(h);var j=$("<div class='tipsy tipsy-north' style='position: absolute;' />").hide();var e=$("<div style='background-color: black; padding: 10px;'></div>").appendTo(j);var k=$("<div/>").appendTo(e).farbtastic({width:100,height:100,callback:g,color:h});$("<div />").append(g).append(j).appendTo(l).bind("click",function(m){j.css({left:$(this).position().left+($(g).width()/2)-60,top:$(this).position().top+$(this.height)}).show();$(document).bind("click.color-picker",function(){j.hide();$(document).unbind("click.color-picker")});m.stopPropagation()})}else{l.append($("<input />").attr("id",c).attr("name",c).val(b.values[d.key]))}}}});return a},update_from_form:function(a){var c=this;var b=false;$.each(this.params,function(d,f){if(!f.hidden){var g="param_"+d;var e=a.find("#"+g).val();if(f.type==="float"){e=parseFloat(e)}else{if(f.type==="int"){e=parseInt(e)}else{if(f.type==="bool"){e=a.find("#"+g).is(":checked")}}}if(e!==c.values[f.key]){c.values[f.key]=e;b=true}}});if(b){this.onchange()}}});var Tile=function(a,b,c){this.track=a;this.canvas=b;this.histo_max=c};var Track=function(b,a,e,c,d){this.name=b;this.view=a;this.parent_element=e;this.data_url=(c?c:default_data_url);this.data_url_extra_params={};this.data_query_wait=(d?d:DEFAULT_DATA_QUERY_WAIT);this.dataset_check_url=converted_datasets_state_url;this.container_div=$("<div />").addClass("track").css("position","relative");if(!this.hidden){this.header_div=$("<div class='track-header' />").appendTo(this.container_div);if(this.view.editor){this.drag_div=$("<div class='draghandle' />").appendTo(this.header_div)}this.name_div=$("<div class='menubutton popup' />").appendTo(this.header_div);this.name_div.text(this.name);this.name_div.attr("id",this.name.replace(/\s+/g,"-").replace(/[^a-zA-Z0-9\-]/g,"").toLowerCase())}this.content_div=$("<div class='track-content'>").appendTo(this.container_div);this.parent_element.append(this.container_div)};$.extend(Track.prototype,{init:function(){var a=this;a.enabled=false;a.tile_cache.clear();a.data_cache.clear();a.initial_canvas=undefined;a.content_div.css("height","auto");a.container_div.removeClass("nodata error pending");if(!a.dataset_id){return}$.getJSON(converted_datasets_state_url,{hda_ldda:a.hda_ldda,dataset_id:a.dataset_id,chrom:a.view.chrom},function(b){if(!b||b==="error"||b.kind==="error"){a.container_div.addClass("error");a.content_div.text(DATA_ERROR);if(b.message){var d=a.view.tracks.indexOf(a);var c=$(" <a href='javascript:void(0);'></a>").text("View error").bind("click",function(){show_modal("Trackster Error","<pre>"+b.message+"</pre>",{Close:hide_modal})});a.content_div.append(c)}}else{if(b==="no converter"){a.container_div.addClass("error");a.content_div.text(DATA_NOCONVERTER)}else{if(b==="no data"||(b.data!==undefined&&(b.data===null||b.data.length===0))){a.container_div.addClass("nodata");a.content_div.text(DATA_NONE)}else{if(b==="pending"){a.container_div.addClass("pending");a.content_div.text(DATA_PENDING);setTimeout(function(){a.init()},a.data_query_wait)}else{if(b.status==="data"){if(b.valid_chroms){a.valid_chroms=b.valid_chroms;a.make_name_popup_menu()}a.content_div.text(DATA_OK);if(a.view.chrom){a.content_div.text("");a.content_div.css("height",a.height_px+"px");a.enabled=true;$.when(a.predraw_init()).done(function(){a.draw()})}}}}}}})},predraw_init:function(){},update_name:function(a){this.old_name=this.name;this.name=a;this.name_div.text(this.name)},revert_name:function(){this.name=this.old_name;this.name_div.text(this.name)}});var TiledTrack=function(b,h,o){var c=this,p=c.view;this.filters=(b!==undefined?get_filters_from_dict(b):[]);this.filters_available=false;this.filters_visible=false;this.tool=(h!==undefined&&obj_length(h)>0?new Tool(h):undefined);this.parent_track=o;this.child_tracks=[];if(c.hidden){return}var k=function(r,s,t){r.click(function(){var u=s.text();max=parseFloat(t.slider("option","max")),input_size=(max<=1?4:max<=1000000?max.toString().length:6),multi_value=false;if(t.slider("option","values")){input_size=2*input_size+1;multi_value=true}s.text("");$("<input type='text'/>").attr("size",input_size).attr("maxlength",input_size).attr("value",u).appendTo(s).focus().select().click(function(v){v.stopPropagation()}).blur(function(){$(this).remove();s.text(u)}).keyup(function(z){if(z.keyCode===27){$(this).trigger("blur")}else{if(z.keyCode===13){var x=t.slider("option","min"),v=t.slider("option","max"),y=function(A){return(isNaN(A)||A>v||A<x)},w=$(this).val();if(!multi_value){w=parseFloat(w);if(y(w)){alert("Parameter value must be in the range ["+x+"-"+v+"]");return $(this)}}else{w=w.split("-");w=[parseFloat(w[0]),parseFloat(w[1])];if(y(w[0])||y(w[1])){alert("Parameter value must be in the range ["+x+"-"+v+"]");return $(this)}}t.slider((multi_value?"values":"value"),w)}}})})};if(this.parent_track){this.header_div.find(".draghandle").removeClass("draghandle").addClass("child-track-icon").addClass("icon-button");this.parent_element.addClass("child-track");this.tool=undefined}this.filters_div=$("<div/>").addClass("filters").hide();this.header_div.after(this.filters_div);this.filters_div.bind("drag",function(r){r.stopPropagation()}).bind("click",function(r){r.stopPropagation()}).bind("dblclick",function(r){r.stopPropagation()});$.each(this.filters,function(x,s){var u=$("<div/>").addClass("slider-row").appendTo(c.filters_div);var r=$("<div/>").addClass("slider-label").appendTo(u);var z=$("<span/>").addClass("slider-name").text(s.name+" ").appendTo(r);var t=$("<span/>");var v=$("<span/>").addClass("slider-value").appendTo(r).append("[").append(t).append("]");var y=$("<div/>").addClass("slider").appendTo(u);s.control_element=$("<div/>").attr("id",s.name+"-filter-control").appendTo(y);var w=[0,0];s.control_element.slider({range:true,min:Number.MAX_VALUE,max:-Number.MIN_VALUE,values:[0,0],slide:function(A,B){w=B.values;t.text(B.values[0]+"-"+B.values[1]);setTimeout(function(){if(B.values[0]==w[0]&&B.values[1]==w[1]){var C=B.values;t.text(C[0]+"-"+C[1]);s.low=C[0];s.high=C[1];c.draw(true,true)}},50)},change:function(A,B){s.control_element.slider("option","slide").call(s.control_element,A,B)}});s.slider=s.control_element;s.slider_label=t;k(v,t,s.control_element);$("<div style='clear: both;'/>").appendTo(u)});if(this.tool){this.dynamic_tool_div=$("<div/>").addClass("dynamic-tool").hide();this.header_div.after(this.dynamic_tool_div);this.dynamic_tool_div.bind("drag",function(r){r.stopPropagation()}).bind("click",function(r){r.stopPropagation()}).bind("dblclick",function(r){r.stopPropagation()});var n=$("<div class='tool-name'>").appendTo(this.dynamic_tool_div).text(this.tool.name);var m=this.tool.params;var c=this;$.each(this.tool.params,function(A,s){if(s instanceof NumberParameter){var x=$("<div>").addClass("slider-row").appendTo(c.dynamic_tool_div);var u=$("<div>").addClass("slider-label").appendTo(x);var C=$("<span/>").addClass("slider-name").text(s.label+" ").appendTo(u);var t=$("<span/>").text(s.value);var y=$("<span/>").addClass("slider-value").appendTo(u).append("[").append(t).append("]");var B=$("<div/>").addClass("slider").appendTo(x);var r=$("<div id='"+s.name+"-param-control'>").appendTo(B);r.slider({min:s.min,max:s.max,step:get_slider_step(s.min,s.max),value:s.value,slide:function(F,H){var G=H.value;s.value=G;if(0<G&&G<1){G=parseFloat(G).toFixed(2)}t.text(G)},change:function(F,G){r.slider("option","slide").call(r,F,G)}});k(y,t,r);$("<div style='clear: both;'/>").appendTo(x)}else{if(s instanceof SelectParameter){var x=$("<div>").addClass("slider-row").appendTo(c.dynamic_tool_div);var u=$("<div>").addClass("slider-label").appendTo(x);var C=$("<span/>").addClass("slider-name").text(s.label+" ").appendTo(u);var E=$("<div/>").addClass("slider").appendTo(x);var w=$("<select/>").appendTo(E);w.change(function(){s.value=$(this).val()});var D=w.attr("options");for(var z=0;z<s.options.length;z++){var v=s.options[z];D[D.length]=new Option(v[0],v[1])}$("<div style='clear: both;'/>").appendTo(x)}}});var q=$("<div>").addClass("slider-row").appendTo(this.dynamic_tool_div);var l=$("<input type='submit'>").attr("value","Run").appendTo(q);var c=this;l.click(function(){c.run_tool()})}c.child_tracks_container=$("<div/>").addClass("child-tracks-container").hide();c.container_div.append(c.child_tracks_container);if(c.display_modes!==undefined){if(c.mode_div===undefined){c.mode_div=$("<div class='right-float menubutton popup' />").appendTo(c.header_div);var e=(c.track_config&&c.track_config.values.mode?c.track_config.values.mode:c.display_modes[0]);c.mode=e;c.mode_div.text(e);var d=function(r){c.mode_div.text(r);c.mode=r;c.track_config.values.mode=r;c.tile_cache.clear();c.draw()};var a={};for(var f=0,j=c.display_modes.length;f<j;f++){var g=c.display_modes[f];a[g]=function(r){return function(){d(r)}}(g)}make_popupmenu(c.mode_div,a)}else{c.mode_div.hide()}}this.make_name_popup_menu()};$.extend(TiledTrack.prototype,Track.prototype,{make_name_popup_menu:function(){var b=this;var a={};a["Edit configuration"]=function(){var h=function(){hide_modal();$(window).unbind("keypress.check_enter_esc")},f=function(){b.track_config.update_from_form($(".dialog-box"));hide_modal();$(window).unbind("keypress.check_enter_esc")},g=function(j){if((j.keyCode||j.which)===27){h()}else{if((j.keyCode||j.which)===13){f()}}};$(window).bind("keypress.check_enter_esc",g);show_modal("Configure Track",b.track_config.build_form(),{Cancel:h,OK:f})};if(b.filters_available>0){var e=(b.filters_div.is(":visible")?"Hide filters":"Show filters");a[e]=function(){b.filters_visible=(b.filters_div.is(":visible"));b.filters_div.toggle();b.make_name_popup_menu()}}if(b.tool){var e=(b.dynamic_tool_div.is(":visible")?"Hide tool":"Show tool");a[e]=function(){if(!b.dynamic_tool_div.is(":visible")){b.update_name(b.name+b.tool_region_and_parameters_str())}else{menu_option_text="Show dynamic tool";b.revert_name()}b.dynamic_tool_div.toggle();b.make_name_popup_menu()}}if(b.valid_chroms){a["List chrom/contigs with data"]=function(){show_modal("Chrom/contigs with data","<p>"+b.valid_chroms.join("<br/>")+"</p>",{Close:function(){hide_modal()}})}}var c=view;var d=function(){$("#no-tracks").show()};if(this.parent_track){c=this.parent_track;d=function(){}}a.Remove=function(){c.remove_track(b);if(c.num_tracks===0){d()}};make_popupmenu(b.name_div,a)},draw:function(a,d){var s=this.view.low,g=this.view.high,j=g-s,l=this.view.container.width(),f=l/j,m=this.view.resolution,e=$("<div style='position: relative;'></div>");if(!d){this.content_div.children().remove()}this.content_div.append(e);this.max_height=0;var o=Math.floor(s/m/DENSITY);var c={};while((o*DENSITY*m)<g){var r=l+"_"+f+"_"+o;var h=this.tile_cache.get(r);var p=o*DENSITY*this.view.resolution;var b=p+DENSITY*this.view.resolution;if(!a&&h){this.show_tile(h,e,p,f)}else{this.delayed_draw(a,r,p,b,o,m,e,f,c)}o+=1}var k=this;var q=setInterval(function(){if(obj_length(c)===0){clearInterval(q);if(d){var v=k.content_div.children();var u=false;for(var w=v.length-1,t=0;w>=t;w--){var z=$(v[w]);if(u){z.remove()}else{if(z.children().length!==0){u=true}}}}for(var y=0;y<k.filters.length;y++){k.filters[y].update_ui_elt()}var x=false;if(k.example_feature){for(var y=0;y<k.filters.length;y++){if(k.filters[y].applies_to(k.example_feature)){x=true;break}}}if(k.filters_available!==x){k.filters_available=x;if(!k.filters_available){k.filters_div.hide()}k.make_name_popup_menu()}}},50);for(var n=0;n<this.child_tracks.length;n++){this.child_tracks[n].draw(a,d)}},delayed_draw:function(b,j,g,l,c,e,k,m,f){var d=this;var h=function(u,n,p,o,s,t,q){returned_tile=d.draw_tile(n,p,o,s,t,q);var r=$("<div class='track-tile'>").prepend(returned_tile);tile_element=r;d.tile_cache.set(j,tile_element);d.show_tile(tile_element,s,g,t);delete f[u]};var a=setTimeout(function(){if(g<=d.view.high&&l>=d.view.low){var n=(b?undefined:d.tile_cache.get(j));if(n){d.show_tile(n,k,g,m);delete f[a]}else{$.when(d.data_cache.get_data(view.chrom,g,l,d.mode,e,d.data_url_extra_params)).then(function(o){if(view.reference_track&&m>CHAR_WIDTH_PX){$.when(view.reference_track.data_cache.get_data(view.chrom,g,l,d.mode,e,view.reference_track.data_url_extra_params)).then(function(p){h(a,o,e,c,k,m,p)})}else{h(a,o,e,c,k,m)}})}}},50);f[a]=true},show_tile:function(a,f,d,g){var b=this;var c=this.view.high-this.view.low,e=(d-this.view.low)*g;if(this.left_offset){e-=this.left_offset}a.css({position:"absolute",top:0,left:e,height:""});f.append(a);b.max_height=Math.max(b.max_height,a.height());b.content_div.css("height",b.max_height+"px");f.children().css("height",b.max_height+"px")},set_overview:function(){var a=this.view;if(this.initial_canvas&&this.is_overview){a.overview_close.show();a.overview_viewport.append(this.initial_canvas);a.overview_highlight.show().height(this.initial_canvas.height());a.overview_viewport.height(this.initial_canvas.height()+a.overview_box.height())}$(window).trigger("resize")},run_tool:function(){var b={dataset_id:this.original_dataset_id,chrom:this.view.chrom,low:this.view.low,high:this.view.high,tool_id:this.tool.name};$.extend(b,this.tool.get_param_values_dict());var d=this,c=b.tool_id+d.tool_region_and_parameters_str(b.chrom,b.low,b.high),e;if(d.track_type==="FeatureTrack"){e=new ToolDataFeatureTrack(c,view,d.hda_ldda,undefined,{},{},d)}this.add_track(e);e.content_div.text("Starting job.");view.has_changes=true;var a=function(){$.getJSON(run_tool_url,b,function(f){if(f==="no converter"){e.container_div.addClass("error");e.content_div.text(DATA_NOCONVERTER)}else{if(f.error){e.container_div.addClass("error");e.content_div.text(DATA_CANNOT_RUN_TOOL+f.message)}else{if(f==="pending"){e.container_div.addClass("pending");e.content_div.text("Converting input data so that it can be easily reused.");setTimeout(a,2000)}else{e.dataset_id=f.dataset_id;e.content_div.text("Running job.");e.init()}}}})};a()},tool_region_and_parameters_str:function(c,a,d){var b=this,e=(c!==undefined&&a!==undefined&&d!==undefined?c+":"+a+"-"+d:"all");return" - region=["+e+"], parameters=["+b.tool.get_param_values().join(", ")+"]"},add_track:function(a){a.track_id=this.track_id+"_"+this.child_tracks.length;a.container_div.attr("id","track_"+a.track_id);this.child_tracks_container.append(a.container_div);sortable(a.container_div,".child-track-icon");if(!$(this.child_tracks_container).is(":visible")){this.child_tracks_container.show()}this.child_tracks.push(a)},remove_track:function(a){a.container_div.fadeOut("slow",function(){$(this).remove()})}});var LabelTrack=function(a,b){this.track_type="LabelTrack";this.hidden=true;Track.call(this,null,a,b);this.container_div.addClass("label-track")};$.extend(LabelTrack.prototype,Track.prototype,{draw:function(){var c=this.view,d=c.high-c.low,g=Math.floor(Math.pow(10,Math.floor(Math.log(d)/Math.log(10)))),a=Math.floor(c.low/g)*g,e=this.view.container.width(),b=$("<div style='position: relative; height: 1.3em;'></div>");while(a<c.high){var f=(a-c.low)/d*e;b.append($("<div class='label'>"+commatize(a)+"</div>").css({position:"absolute",left:f-1}));a+=g}this.content_div.children(":first").remove();this.content_div.append(b)}});var ReferenceTrack=function(a){this.track_type="ReferenceTrack";this.hidden=true;Track.call(this,null,a,a.top_labeltrack);TiledTrack.call(this);a.reference_track=this;this.left_offset=200;this.height_px=12;this.font=DEFAULT_FONT;this.container_div.addClass("reference-track");this.content_div.css("background","none");this.content_div.css("min-height","0px");this.content_div.css("border","none");this.data_url=reference_url;this.data_url_extra_params={dbkey:a.dbkey};this.data_cache=new DataManager(CACHED_DATA,this,false);this.tile_cache=new Cache(CACHED_TILES_LINE)};$.extend(ReferenceTrack.prototype,TiledTrack.prototype,{draw_tile:function(m,g,b,j,n){var f=this,d=DENSITY*g;if(n>CHAR_WIDTH_PX){if(m===null){f.content_div.css("height","0px");return}var e=document.createElement("canvas");if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(e)}e=$(e);var l=e.get(0).getContext("2d");e.get(0).width=Math.ceil(d*n+f.left_offset);e.get(0).height=f.height_px;l.font=DEFAULT_FONT;l.textAlign="center";for(var h=0,k=m.length;h<k;h++){var a=Math.round(h*n);l.fillText(m[h],a+f.left_offset,10)}return e}this.content_div.css("height","0px")}});var LineTrack=function(e,c,f,a,d){var b=this;this.track_type="LineTrack";this.display_modes=["Histogram","Line","Filled","Intensity"];this.mode="Histogram";Track.call(this,e,c,c.viewport_container);TiledTrack.call(this);this.min_height_px=16;this.max_height_px=400;this.height_px=80;this.hda_ldda=f;this.dataset_id=a;this.original_dataset_id=a;this.data_cache=new DataManager(CACHED_DATA,this);this.tile_cache=new Cache(CACHED_TILES_LINE);this.track_config=new TrackConfig({track:this,params:[{key:"color",label:"Color",type:"color",default_value:"black"},{key:"min_value",label:"Min Value",type:"float",default_value:undefined},{key:"max_value",label:"Max Value",type:"float",default_value:undefined},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"height",type:"int",default_value:this.height_px,hidden:true}],saved_values:d,onchange:function(){b.vertical_range=b.prefs.max_value-b.prefs.min_value;$("#linetrack_"+b.track_id+"_minval").text(b.prefs.min_value);$("#linetrack_"+b.track_id+"_maxval").text(b.prefs.max_value);b.tile_cache.clear();b.draw()}});this.prefs=this.track_config.values;this.height_px=this.track_config.values.height;this.vertical_range=this.track_config.values.max_value-this.track_config.values.min_value;(function(g){var k=false;var j=false;var h=$("<div class='track-resize'>");$(g.container_div).hover(function(){k=true;h.show()},function(){k=false;if(!j){h.hide()}});h.hide().bind("dragstart",function(l,m){j=true;m.original_height=$(g.content_div).height()}).bind("drag",function(m,n){var l=Math.min(Math.max(n.original_height+n.deltaY,g.min_height_px),g.max_height_px);$(g.content_div).css("height",l);g.height_px=l;g.draw(true)}).bind("dragend",function(l,m){g.tile_cache.clear();j=false;if(!k){h.hide()}g.track_config.values.height=g.height_px}).appendTo(g.container_div)})(this)};$.extend(LineTrack.prototype,TiledTrack.prototype,{predraw_init:function(){var a=this,b=a.view.tracks.indexOf(a);a.vertical_range=undefined;return $.getJSON(a.data_url,{stats:true,chrom:a.view.chrom,low:null,high:null,hda_ldda:a.hda_ldda,dataset_id:a.dataset_id},function(c){a.container_div.addClass("line-track");var e=c.data;if(isNaN(parseFloat(a.prefs.min_value))||isNaN(parseFloat(a.prefs.max_value))){a.prefs.min_value=e.min;a.prefs.max_value=e.max;$("#track_"+b+"_minval").val(a.prefs.min_value);$("#track_"+b+"_maxval").val(a.prefs.max_value)}a.vertical_range=a.prefs.max_value-a.prefs.min_value;a.total_frequency=e.total_frequency;a.container_div.find(".yaxislabel").remove();var f=$("<div />").addClass("yaxislabel").attr("id","linetrack_"+b+"_minval").text(round_1000(a.prefs.min_value));var d=$("<div />").addClass("yaxislabel").attr("id","linetrack_"+b+"_maxval").text(round_1000(a.prefs.max_value));d.css({position:"absolute",top:"24px",left:"10px"});d.prependTo(a.container_div);f.css({position:"absolute",bottom:"2px",left:"10px"});f.prependTo(a.container_div)})},draw_tile:function(j,q,t,c,e){if(this.vertical_range===undefined){return}var o=this,u=t*DENSITY*q,a=DENSITY*q,B=q+"_"+t,v={hda_ldda:this.hda_ldda,dataset_id:this.dataset_id,resolution:this.view.resolution};var b=document.createElement("canvas"),z=j.data;if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(b)}b=$(b);b.get(0).width=Math.ceil(a*e);b.get(0).height=o.height_px;var p=b.get(0).getContext("2d"),k=false,l=o.prefs.min_value,g=o.prefs.max_value,n=o.vertical_range,w=o.total_frequency,d=o.height_px,m=o.mode;var A=Math.round(d+l/n*d);p.beginPath();p.moveTo(0,A);p.lineTo(a*e,A);p.fillStyle="#aaa";p.stroke();p.beginPath();p.fillStyle=o.prefs.color;var x,h,f;if(z.length>1){f=Math.ceil((z[1][0]-z[0][0])*e)}else{f=10}for(var r=0,s=z.length;r<s;r++){x=Math.round((z[r][0]-u)*e);h=z[r][1];if(h===null){if(k&&m==="Filled"){p.lineTo(x,d)}k=false;continue}if(h<l){h=l}else{if(h>g){h=g}}if(m==="Histogram"){h=Math.round(h/n*d);p.fillRect(x,A,f,-h)}else{if(m==="Intensity"){h=255-Math.floor((h-l)/n*255);p.fillStyle="rgb("+h+","+h+","+h+")";p.fillRect(x,0,f,d)}else{h=Math.round(d-(h-l)/n*d);if(k){p.lineTo(x,h)}else{k=true;if(m==="Filled"){p.moveTo(x,d);p.lineTo(x,h)}else{p.moveTo(x,h)}}}}}if(m==="Filled"){if(k){p.lineTo(x,A);p.lineTo(0,A)}p.fill()}else{p.stroke()}return b}});var FeatureTrack=function(a,f,e,j,h,c,d,g){var b=this;this.track_type="FeatureTrack";this.display_modes=["Auto","Dense","Squish","Pack"];this.track_config=new TrackConfig({track:this,params:[{key:"block_color",label:"Block color",type:"color",default_value:"#444"},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},],saved_values:h,onchange:function(){b.tile_cache.clear();b.draw()}});this.prefs=this.track_config.values;Track.call(this,a,f,f.viewport_container);TiledTrack.call(this,c,d,g);this.height_px=0;this.container_div.addClass("feature-track");this.hda_ldda=e;this.dataset_id=j;this.original_dataset_id=j;this.zo_slots={};this.show_labels_scale=0.001;this.showing_details=false;this.summary_draw_height=30;this.default_font=DEFAULT_FONT;this.inc_slots={};this.start_end_dct={};this.tile_cache=new Cache(CACHED_TILES_FEATURE);this.data_cache=new DataManager(20,this);this.left_offset=200};$.extend(FeatureTrack.prototype,TiledTrack.prototype,{incremental_slots:function(a,h,p){var q=this.inc_slots[a];if(!q||(q.mode!==p)){q={};q.w_scale=a;q.mode=p;this.inc_slots[a]=q;this.start_end_dct[a]={}}var l=q.w_scale,w=[],x=[],j=0,n=this.view.max_low;for(var u=0,v=h.length;u<v;u++){var g=h[u],k=g[0];if(q[k]!==undefined){j=Math.max(j,q[k]);x.push(q[k])}else{w.push(u)}}var d=this.start_end_dct[a];var m=function(D,E){for(var C=0;C<=MAX_FEATURE_DEPTH;C++){var A=false,F=d[C];if(F!==undefined){for(var z=0,B=F.length;z<B;z++){var y=F[z];if(E>y[0]&&D<y[1]){A=true;break}}}if(!A){return C}}return -1};for(var u=0,v=w.length;u<v;u++){var g=h[w[u]],k=g[0],s=g[1],b=g[2],o=g[3],c=Math.floor((s-n)*l),f=Math.ceil((b-n)*l),t=CONTEXT.measureText(o).width,e;if(o!==undefined&&p==="Pack"){t+=(LABEL_SPACING+PACK_SPACING);if(c-t>=0){c-=t;e="left"}else{f+=t;e="right"}}var r=m(c,f);if(r>=0){if(d[r]===undefined){d[r]=[]}d[r].push([c,f]);q[k]=r;j=Math.max(j,r)}else{}}return j+1},draw_summary_tree:function(b,o,n,l,r,j,f,e){var c=j+LABEL_SPACING+CHAR_HEIGHT_PX;delta_x_px=Math.ceil(n*r);var h=$("<div />").addClass("yaxislabel");h.text(l);h.css({position:"absolute",top:"22px",left:"10px"});h.prependTo(this.container_div);var q=b.get(0).getContext("2d");for(var d=0,g=o.length;d<g;d++){var m=Math.floor((o[d][0]-f)*r);var k=o[d][1];if(!k){continue}var p=k/l*j;q.fillStyle="black";q.fillRect(m+e,c-p,delta_x_px,p);var a=4;if(this.prefs.show_counts&&(q.measureText(k).width+a)<delta_x_px){q.fillStyle="#666";q.textAlign="center";q.fillText(k,m+e+(delta_x_px/2),10)}}},draw_element:function(p,f,g,x,j,r,I,M,N,a,A){var u=x[0],K=x[1],C=x[2]-1,s=x[3],D=Math.floor(Math.max(0,(K-r)*M)),q=Math.ceil(Math.min(a,Math.max(0,(C-r)*M))),B=ERROR_PADDING+(g==="Dense"?0:(0+j))*N,o,G,t=null,O=null,d=this.prefs.block_color,F=this.prefs.label_color;if(g==="Dense"){p.fillStyle=d;p.fillRect(D+A,B,q-D,DENSE_FEATURE_HEIGHT)}else{if(g==="no_detail"){p.fillStyle=d;p.fillRect(D+A,B+5,q-D,DENSE_FEATURE_HEIGHT)}else{var n=x[4],z=x[5],E=x[6],e=x[7];if(z&&E){t=Math.floor(Math.max(0,(z-r)*M));O=Math.ceil(Math.min(a,Math.max(0,(E-r)*M)))}var L,v;if(g==="Squish"){L=1;v=SQUISH_FEATURE_HEIGHT}else{L=5;v=PACK_FEATURE_HEIGHT}if(!e){if(x.strand){if(x.strand==="+"){p.fillStyle=RIGHT_STRAND_INV}else{if(x.strand==="-"){p.fillStyle=LEFT_STRAND_INV}}}else{p.fillStyle=d}p.fillRect(D+A,B,q-D,v)}else{var m,w;if(g==="Squish"){p.fillStyle=CONNECTOR_COLOR;m=B+Math.floor(SQUISH_FEATURE_HEIGHT/2)+1;w=1}else{if(n){var m=B;var w=v;if(n==="+"){p.fillStyle=RIGHT_STRAND}else{if(n==="-"){p.fillStyle=LEFT_STRAND}}}else{p.fillStyle=CONNECTOR_COLOR;m+=(SQUISH_FEATURE_HEIGHT/2)+1;w=1}}p.fillRect(D+A,m,q-D,w);for(var J=0,c=e.length;J<c;J++){var h=e[J],b=Math.floor(Math.max(0,(h[0]-r)*M)),y=Math.ceil(Math.min(a,Math.max((h[1]-1-r)*M)));if(b>y){continue}p.fillStyle=d;p.fillRect(b+A,B+(v-L)/2+1,y-b,L);if(t!==undefined&&!(b>O||y<t)){var H=Math.max(b,t),l=Math.min(y,O);p.fillRect(H+A,B+1,l-H,v)}}}if(g==="Pack"&&K>r){p.fillStyle=F;if(f===0&&D-p.measureText(s).width<0){p.textAlign="left";p.fillText(s,q+A+LABEL_SPACING,B+8)}else{p.textAlign="right";p.fillText(s,D+A-LABEL_SPACING,B+8)}p.fillStyle=d}}}},get_y_scale:function(b){var a;if(b==="summary_tree"){}if(b==="Dense"){a=DENSE_TRACK_HEIGHT}else{if(b==="no_detail"){a=NO_DETAIL_TRACK_HEIGHT}else{if(b==="Squish"){a=SQUISH_TRACK_HEIGHT}else{a=PACK_TRACK_HEIGHT}}}return a},draw_tile:function(o,x,C,k,m,d){var t=this;var E=C*DENSITY*x,a=(C+1)*DENSITY*x,q=a-E,F={hda_ldda:t.hda_ldda,dataset_id:t.dataset_id,resolution:this.view.resolution,mode:this.mode};var v=Math.ceil(q*m),s=this.mode,H=25,g=this.left_offset,p,h;var c=document.createElement("canvas");if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(c)}c=$(c);if(s==="Auto"){if(o.dataset_type==="summary_tree"){s=o.dataset_type}else{if(o.extra_info==="no_detail"){s="no_detail"}else{var G=o.data;if((G.length&&G.length<4)||(this.view.high-this.view.low>MIN_SQUISH_VIEW_WIDTH)){s="Squish"}else{s="Pack"}}}}var y=this.get_y_scale(s);if(s==="summary_tree"){h=this.summary_draw_height}if(s==="Dense"){h=H}else{if(s==="no_detail"||s==="Squish"||s==="Pack"){h=this.incremental_slots(m,o.data,s)*y+H;p=this.inc_slots[m]}}c.get(0).width=v+g;c.get(0).height=h;if(o.dataset_type==="summary_tree"){c.get(0).height+=LABEL_SPACING+CHAR_HEIGHT_PX}k.parent().css("height",Math.max(this.height_px,h)+"px");var w=c.get(0).getContext("2d");w.fillStyle=this.prefs.block_color;w.font=this.default_font;w.textAlign="right";this.container_div.find(".yaxislabel").remove();if(s==="summary_tree"){this.draw_summary_tree(c,o.data,o.delta,o.max,m,h,E,g);return c}if(o.message){c.css({"border-top":"1px solid red"});w.fillStyle="red";w.textAlign="left";var r=w.textBaseline;w.textBaseline="top";w.fillText(o.message,g,0);w.textBaseline=r;if(!o.data){return c}}this.example_feature=(o.data.length?o.data[0]:undefined);var G=o.data;for(var z=0,B=G.length;z<B;z++){var j=G[z],l=j[0],u=j[1],b=j[2],e=(p&&p[l]!==undefined?p[l]:null);var A=false;var n;for(var D=0;D<this.filters.length;D++){n=this.filters[D];n.update_attrs(j);if(!n.keep(j)){A=true;break}}if(A){continue}if(is_overlap([u,b],[E,a])&&(s=="Dense"||e!==null)){this.draw_element(w,C,s,j,e,E,a,m,y,v,g,d)}}return c}});var VcfTrack=function(d,b,f,a,c,e){FeatureTrack.call(this,d,b,f,a,c,e);this.track_type="VcfTrack"};$.extend(VcfTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype,{draw_element:function(u,p,j,e,x,c,m,v,s){var j=data[i],l=j[0],t=j[1],d=j[2]-1,o=j[3],g=Math.floor(Math.max(0,(t-x)*m)),k=Math.ceil(Math.min(s,Math.max(0,(d-x)*m))),f=ERROR_PADDING+(p==="Dense"?0:(0+e))*v,a,y,b=null,n=null;if(no_label){u.fillStyle=block_color;u.fillRect(g+left_offset,f+5,k-g,1)}else{var w=j[4],r=j[5],h=j[6];a=9;y=1;u.fillRect(g+left_offset,f,k-g,a);if(p!=="Dense"&&o!==undefined&&t>x){u.fillStyle=label_color;if(tile_index===0&&g-u.measureText(o).width<0){u.textAlign="left";u.fillText(o,k+2+left_offset,f+8)}else{u.textAlign="right";u.fillText(o,g-2+left_offset,f+8)}u.fillStyle=block_color}var q=w+" / "+r;if(t>x&&u.measureText(q).width<(k-g)){u.fillStyle="white";u.textAlign="center";u.fillText(q,left_offset+g+(k-g)/2,f+8);u.fillStyle=block_color}}}});var ReadTrack=function(d,b,f,a,c,e){FeatureTrack.call(this,d,b,f,a,c,e);this.track_config=new TrackConfig({track:this,params:[{key:"block_color",label:"Block color",type:"color",default_value:"#444"},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_insertions",label:"Show insertions",type:"bool",default_value:false},{key:"show_differences",label:"Show differences only",type:"bool",default_value:true},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},],saved_values:c,onchange:function(){this.track.tile_cache.clear();this.track.draw()}});this.prefs=this.track_config.values;this.track_type="ReadTrack";this.make_name_popup_menu()};$.extend(ReadTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype,{get_y_scale:function(b){var a;if(b==="summary_tree"){}if(b==="Dense"){a=DENSE_TRACK_HEIGHT}else{if(b==="Squish"){a=SQUISH_TRACK_HEIGHT}else{a=PACK_TRACK_HEIGHT;if(this.track_config.values.show_insertions){a*=2}}}return a},draw_read:function(m,d,J,n,D,G,e,o,y,a){m.textAlign="center";var l=this,u=[n,D],C=0,z=0,f=0;var r=[];if((d==="Pack"||this.mode==="Auto")&&o!==undefined&&J>CHAR_WIDTH_PX){f=Math.round(J/2)}if(!e){e=[[0,o.length]]}for(var h=0,k=e.length;h<k;h++){var b=e[h],g="MIDNSHP=X"[b[0]],v=b[1];if(g==="H"||g==="S"){C-=v}var x=G+C,B=Math.floor(Math.max(0,(x-n)*J)),E=Math.floor(Math.max(0,(x+v-n)*J));switch(g){case"H":break;case"S":case"M":case"=":var p=compute_overlap([x,x+v],u);if(p!==NO_OVERLAP){var q=o.slice(z,z+v);if(f>0){m.fillStyle=this.prefs.block_color;m.fillRect(B+this.left_offset-f,y+1,E-B,9);m.fillStyle=CONNECTOR_COLOR;for(var I=0,j=q.length;I<j;I++){if(this.track_config.values.show_differences&&a){var s=a[x-n+I];if(!s||s.toLowerCase()===q[I].toLowerCase()){continue}}if(x+I>=n&&x+I<=D){var A=Math.floor(Math.max(0,(x+I-n)*J));m.fillText(q[I],A+this.left_offset,y+9)}}}else{m.fillStyle=this.prefs.block_color;m.fillRect(B+this.left_offset,y+(this.mode!=="Dense"?4:5),E-B,(d!=="Dense"?SQUISH_FEATURE_HEIGHT:DENSE_FEATURE_HEIGHT))}}z+=v;C+=v;break;case"N":m.fillStyle=CONNECTOR_COLOR;m.fillRect(B+this.left_offset-f,y+5,E-B,1);C+=v;break;case"D":m.fillStyle="red";m.fillRect(B+this.left_offset-f,y+4,E-B,3);C+=v;break;case"P":break;case"I":var p=compute_overlap([x,x+v],u),K=this.left_offset+B-f;if(p!==NO_OVERLAP){var q=o.slice(z,z+v);if(this.track_config.values.show_insertions){var w=this.left_offset+B-(E-B)/2;if((d==="Pack"||this.mode==="Auto")&&o!==undefined&&J>CHAR_WIDTH_PX){m.fillStyle="yellow";m.fillRect(w-f,y-9,E-B,9);r[r.length]={type:"triangle",data:[K,y+4,5]};m.fillStyle=CONNECTOR_COLOR;switch(p){case (OVERLAP_START):q=q.slice(n-x);break;case (OVERLAP_END):q=q.slice(0,x-D);break;case (CONTAINED_BY):break;case (CONTAINS):q=q.slice(n-x,x-D);break}for(var I=0,j=q.length;I<j;I++){var A=Math.floor(Math.max(0,(x+I-n)*J));m.fillText(q[I],A+this.left_offset-(E-B)/2,y)}}else{m.fillStyle="yellow";m.fillRect(w,y+(this.mode!=="Dense"?2:5),E-B,(d!=="Dense"?SQUISH_FEATURE_HEIGHT:DENSE_FEATURE_HEIGHT))}}else{if((d==="Pack"||this.mode==="Auto")&&o!==undefined&&J>CHAR_WIDTH_PX){r[r.length]={type:"text",data:[q.length,K,y+9]}}else{}}}z+=v;break;case"X":z+=v;break}}m.fillStyle="yellow";var t,L,H;for(var F=0;F<r.length;F++){t=r[F];L=t.type;H=t.data;if(L==="text"){m.font="bold "+DEFAULT_FONT;m.fillText(H[0],H[1],H[2]);m.font=DEFAULT_FONT}else{if(L=="triangle"){m.drawDownwardEquilateralTriangle(H[0],H[1],H[2])}}}},draw_element:function(w,y,r,k,e,A,b,n,x,u,f,c){var m=k[0],v=k[1],d=k[2],o=k[3],h=Math.floor(Math.max(0,(v-A)*n)),j=Math.ceil(Math.min(u,Math.max(0,(d-A)*n))),g=(r==="Dense"?1:(1+e))*x,z=this.prefs.block_color,l=this.prefs.label_color,t=0;if((r==="Pack"||this.mode==="Auto")&&n>CHAR_WIDTH_PX){var t=Math.round(n/2)}w.fillStyle=z;if(k[5] instanceof Array){var s=Math.floor(Math.max(0,(k[4][0]-A)*n)),q=Math.ceil(Math.min(u,Math.max(0,(k[4][1]-A)*n))),p=Math.floor(Math.max(0,(k[5][0]-A)*n)),a=Math.ceil(Math.min(u,Math.max(0,(k[5][1]-A)*n)));if(k[4][1]>=A&&k[4][0]<=b&&k[4][2]){this.draw_read(w,r,n,A,b,k[4][0],k[4][2],k[4][3],g,c)}if(k[5][1]>=A&&k[5][0]<=b&&k[5][2]){this.draw_read(w,r,n,A,b,k[5][0],k[5][2],k[5][3],g,c)}if(p>q){w.fillStyle=CONNECTOR_COLOR;w.dashedLine(q+f-t,g+5,f+p-t,g+5)}}else{w.fillStyle=z;this.draw_read(w,r,n,A,b,v,k[4],k[5],g,c)}if(r==="Pack"&&v>A){w.fillStyle=this.prefs.label_color;if(y===0&&h-w.measureText(o).width<0){w.textAlign="left";w.fillText(o,j+f+LABEL_SPACING-t,g+8)}else{w.textAlign="right";w.fillText(o,h+f-LABEL_SPACING-t,g+8)}w.fillStyle=z}}});var ToolDataFeatureTrack=function(e,c,g,a,d,f,b){FeatureTrack.call(this,e,c,g,a,d,f,{},b);this.track_type="ToolDataFeatureTrack";this.data_url=raw_data_url;this.data_query_wait=1000;this.dataset_check_url=dataset_state_url};$.extend(ToolDataFeatureTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype,{predraw_init:function(){var b=this;var a=function(){if(b.data_cache.size()===0){setTimeout(a,300)}else{b.data_url=default_data_url;b.data_query_wait=DEFAULT_DATA_QUERY_WAIT;b.dataset_state_url=converted_datasets_state_url;$.getJSON(b.dataset_state_url,{dataset_id:b.dataset_id,hda_ldda:b.hda_ldda},function(c){})}};a()}}); \ No newline at end of file +CanvasRenderingContext2D.prototype.dashedLine=function(c,j,b,h,f){if(f===undefined){f=4}var e=b-c;var d=h-j;var g=Math.floor(Math.sqrt(e*e+d*d)/f);var l=e/g;var k=d/g;var a;for(a=0;a<g;a++,c+=l,j+=k){if(a%2!==0){continue}this.fillRect(c,j,f,1)}};CanvasRenderingContext2D.prototype.drawDownwardEquilateralTriangle=function(b,a,e){var d=b-e/2,c=b+e/2,f=a-Math.sqrt(e*3/2);this.beginPath();this.moveTo(d,f);this.lineTo(c,f);this.lineTo(b,a);this.lineTo(d,f);this.strokeStyle=this.fillStyle;this.fill();this.stroke();this.closePath()};function sortable(a,b){a.bind("drag",{handle:b,relative:true},function(h,j){var g=$(this).parent();var f=g.children();var c;for(c=0;c<f.length;c++){if(j.offsetY<$(f.get(c)).position().top){break}}if(c===f.length){if(this!==f.get(c-1)){g.append(this)}}else{if(this!==f.get(c)){$(this).insertBefore(f.get(c))}}})}var NO_OVERLAP=1001,CONTAINS=1002,OVERLAP_START=1003,OVERLAP_END=1004,CONTAINED_BY=1005;function compute_overlap(e,b){var g=e[0],f=e[1],d=b[0],c=b[1],a;if(g<d){if(f<d){a=NO_OVERLAP}else{if(f<=c){a=OVERLAP_START}else{a=CONTAINS}}}else{if(g>c){a=NO_OVERLAP}else{if(f<=c){a=CONTAINED_BY}else{a=OVERLAP_END}}}return a}function is_overlap(b,a){return(compute_overlap(b,a)!==NO_OVERLAP)}function get_slider_step(c,a){var b=a-c;return(b<=1?0.01:(b<=1000?1:5))}var DENSE_TRACK_HEIGHT=10,NO_DETAIL_TRACK_HEIGHT=3,SQUISH_TRACK_HEIGHT=5,PACK_TRACK_HEIGHT=10,NO_DETAIL_FEATURE_HEIGHT=1,DENSE_FEATURE_HEIGHT=1,SQUISH_FEATURE_HEIGHT=3,PACK_FEATURE_HEIGHT=9,ERROR_PADDING=10,LABEL_SPACING=2,PACK_SPACING=5,MIN_SQUISH_VIEW_WIDTH=12000,DEFAULT_FONT="9px Monaco, Lucida Console, monospace",DENSITY=200,FEATURE_LEVELS=10,MAX_FEATURE_DEPTH=100,DEFAULT_DATA_QUERY_WAIT=5000,MAX_CHROMS_SELECTABLE=100,CONNECTOR_COLOR="#ccc",DATA_ERROR="There was an error in indexing this dataset. ",DATA_NOCONVERTER="A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",DATA_NONE="No data for this chrom/contig.",DATA_PENDING="Currently indexing... please wait",DATA_CANNOT_RUN_TOOL="Tool cannot be rerun: ",DATA_LOADING="Loading data...",DATA_OK="Ready for display",CACHED_TILES_FEATURE=10,CACHED_TILES_LINE=5,CACHED_DATA=5,DUMMY_CANVAS=document.createElement("canvas"),RIGHT_STRAND,LEFT_STRAND;if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(DUMMY_CANVAS)}var CONTEXT=DUMMY_CANVAS.getContext("2d");CONTEXT.font=DEFAULT_FONT;var CHAR_WIDTH_PX=CONTEXT.measureText("A").width,CHAR_HEIGHT_PX=9;var right_img=new Image();right_img.src=image_path+"/visualization/strand_right.png";right_img.onload=function(){RIGHT_STRAND=CONTEXT.createPattern(right_img,"repeat")};var left_img=new Image();left_img.src=image_path+"/visualization/strand_left.png";left_img.onload=function(){LEFT_STRAND=CONTEXT.createPattern(left_img,"repeat")};var right_img_inv=new Image();right_img_inv.src=image_path+"/visualization/strand_right_inv.png";right_img_inv.onload=function(){RIGHT_STRAND_INV=CONTEXT.createPattern(right_img_inv,"repeat")};var left_img_inv=new Image();left_img_inv.src=image_path+"/visualization/strand_left_inv.png";left_img_inv.onload=function(){LEFT_STRAND_INV=CONTEXT.createPattern(left_img_inv,"repeat")};function round_1000(a){return Math.round(a*1000)/1000}var Cache=function(a){this.num_elements=a;this.clear()};$.extend(Cache.prototype,{get:function(b){var a=this.key_ary.indexOf(b);if(a!==-1){this.move_key_to_end(b,a)}return this.obj_cache[b]},set:function(b,c){if(!this.obj_cache[b]){if(this.key_ary.length>=this.num_elements){var a=this.key_ary.shift();delete this.obj_cache[a]}this.key_ary.push(b)}this.obj_cache[b]=c;return c},move_key_to_end:function(b,a){this.key_ary.splice(a,1);this.key_ary.push(b)},clear:function(){this.obj_cache={};this.key_ary=[]},size:function(){return this.key_ary.length}});var DataManager=function(b,a,c){Cache.call(this,b);this.track=a;this.subset=(c!==undefined?c:true)};$.extend(DataManager.prototype,Cache.prototype,{load_data:function(h,j,d,g,a,f){var c={chrom:h,low:j,high:d,mode:g,resolution:a,dataset_id:this.track.dataset_id,hda_ldda:this.track.hda_ldda};$.extend(c,f);var k=[];for(var e=0;e<this.track.filters.length;e++){k[k.length]=this.track.filters[e].name}c.filter_cols=JSON.stringify(k);var b=this;return $.getJSON(this.track.data_url,c,function(l){b.set_data(j,d,g,l)})},get_data:function(j,k,c,h,b,f){var l=this.get(this.gen_key(k,c,h));if(l){return l}if(this.subset){var m,g,a,e,h,l;for(var d=0;d<this.key_ary.length;d++){m=this.key_ary[d];g=this.split_key(m);a=g[0];e=g[1];if(k>=a&&c<=e){l=this.obj_cache[m];if(l.dataset_type!=="summary_tree"&&l.extra_info!=="no_detail"){this.move_key_to_end(m,d);return l}}}}return this.load_data(j,k,c,h,b,f)},set_data:function(b,c,d,a){return this.set(this.gen_key(b,c,d),a)},gen_key:function(a,c,d){var b=a+"_"+c+"_"+d;return b},split_key:function(a){return a.split("_")}});var View=function(a,d,c,b,e){this.container=a;this.chrom=null;this.vis_id=c;this.dbkey=b;this.title=d;this.tracks=[];this.label_tracks=[];this.max_low=0;this.max_high=0;this.num_tracks=0;this.track_id_counter=0;this.zoom_factor=3;this.min_separation=30;this.has_changes=false;this.init(e);this.canvas_manager=new CanvasManager(a.get(0).ownerDocument);this.reset()};$.extend(View.prototype,{init:function(d){var c=this.container,a=this;this.top_container=$("<div/>").addClass("top-container").appendTo(c);this.content_div=$("<div/>").addClass("content").css("position","relative").appendTo(c);this.bottom_container=$("<div/>").addClass("bottom-container").appendTo(c);this.top_labeltrack=$("<div/>").addClass("top-labeltrack").appendTo(this.top_container);this.viewport_container=$("<div/>").addClass("viewport-container").addClass("viewport-container").appendTo(this.content_div);this.intro_div=$("<div/>").addClass("intro").text("Select a chrom from the dropdown below").hide();this.nav_labeltrack=$("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);this.nav_container=$("<div/>").addClass("nav-container").prependTo(this.top_container);this.nav=$("<div/>").addClass("nav").appendTo(this.nav_container);this.overview=$("<div/>").addClass("overview").appendTo(this.bottom_container);this.overview_viewport=$("<div/>").addClass("overview-viewport").appendTo(this.overview);this.overview_close=$("<a href='javascript:void(0);'>Close Overview</a>").addClass("overview-close").hide().appendTo(this.overview_viewport);this.overview_highlight=$("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);this.overview_box_background=$("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);this.overview_box=$("<div/>").addClass("overview-box").appendTo(this.overview_viewport);this.default_overview_height=this.overview_box.height();this.nav_controls=$("<div/>").addClass("nav-controls").appendTo(this.nav);this.chrom_select=$("<select/>").attr({name:"chrom"}).css("width","15em").addClass("no-autocomplete").append("<option value=''>Loading</option>").appendTo(this.nav_controls);var b=function(f){if(f.type==="focusout"||(f.keyCode||f.which)===13||(f.keyCode||f.which)===27){if((f.keyCode||f.which)!==27){a.go_to($(this).val())}$(this).hide();$(this).val("");a.location_span.show();a.chrom_select.show()}};this.nav_input=$("<input/>").addClass("nav-input").hide().bind("keyup focusout",b).appendTo(this.nav_controls);this.location_span=$("<span/>").addClass("location").appendTo(this.nav_controls);this.location_span.bind("click",function(){a.location_span.hide();a.chrom_select.hide();a.nav_input.val(a.chrom+":"+a.low+"-"+a.high);a.nav_input.css("display","inline-block");a.nav_input.select();a.nav_input.focus()});if(this.vis_id!==undefined){this.hidden_input=$("<input/>").attr("type","hidden").val(this.vis_id).appendTo(this.nav_controls)}this.zo_link=$("<a id='zoom-out' />").click(function(){a.zoom_out();a.redraw()}).appendTo(this.nav_controls);this.zi_link=$("<a id='zoom-in' />").click(function(){a.zoom_in();a.redraw()}).appendTo(this.nav_controls);this.load_chroms({low:0},d);this.chrom_select.bind("change",function(){a.change_chrom(a.chrom_select.val())});this.intro_div.show();this.content_div.bind("click",function(f){$(this).find("input").trigger("blur")});this.content_div.bind("dblclick",function(f){a.zoom_in(f.pageX,this.viewport_container)});this.overview_box.bind("dragstart",function(f,g){this.current_x=g.offsetX}).bind("drag",function(f,h){var j=h.offsetX-this.current_x;this.current_x=h.offsetX;var g=Math.round(j/a.viewport_container.width()*(a.max_high-a.max_low));a.move_delta(-g)});this.overview_close.bind("click",function(){for(var f=0,e=a.tracks.length;f<e;f++){a.tracks[f].is_overview=false}$(this).siblings().filter("canvas").remove();$(this).parent().css("height",a.overview_box.height());a.overview_highlight.hide();$(this).hide()});this.viewport_container.bind("draginit",function(f,g){if(f.clientX>a.viewport_container.width()-16){return false}}).bind("dragstart",function(f,g){g.original_low=a.low;g.current_height=f.clientY;g.current_x=g.offsetX}).bind("drag",function(h,k){var f=$(this);var l=k.offsetX-k.current_x;var g=f.scrollTop()-(h.clientY-k.current_height);f.scrollTop(g);k.current_height=h.clientY;k.current_x=k.offsetX;var j=Math.round(l/a.viewport_container.width()*(a.high-a.low));a.move_delta(j)}).bind("mousewheel",function(h,k,g,f){if(g){var j=Math.round(-g/a.viewport_container.width()*(a.high-a.low));a.move_delta(j)}});this.top_labeltrack.bind("dragstart",function(f,g){return $("<div />").css({height:a.content_div.height()+a.top_labeltrack.height()+a.nav_labeltrack.height()+1,top:"0px",position:"absolute","background-color":"#ccf",opacity:0.5,"z-index":1000}).appendTo($(this))}).bind("drag",function(k,l){$(l.proxy).css({left:Math.min(k.pageX,l.startX),width:Math.abs(k.pageX-l.startX)});var g=Math.min(k.pageX,l.startX)-a.container.offset().left,f=Math.max(k.pageX,l.startX)-a.container.offset().left,j=(a.high-a.low),h=a.viewport_container.width();a.update_location(Math.round(g/h*j)+a.low,Math.round(f/h*j)+a.low)}).bind("dragend",function(l,m){var g=Math.min(l.pageX,m.startX),f=Math.max(l.pageX,m.startX),j=(a.high-a.low),h=a.viewport_container.width(),k=a.low;a.low=Math.round(g/h*j)+k;a.high=Math.round(f/h*j)+k;$(m.proxy).remove();a.redraw()});this.add_label_track(new LabelTrack(this,this.top_labeltrack));this.add_label_track(new LabelTrack(this,this.nav_labeltrack));$(window).bind("resize",function(){a.resize_window()});$(document).bind("redraw",function(){a.redraw()});this.reset();$(window).trigger("resize")},update_location:function(a,b){this.location_span.text(commatize(a)+" - "+commatize(b));this.nav_input.val(this.chrom+":"+commatize(a)+"-"+commatize(b))},load_chroms:function(b,c){b.num=MAX_CHROMS_SELECTABLE;$.extend(b,(this.vis_id!==undefined?{vis_id:this.vis_id}:{dbkey:this.dbkey}));var a=this;$.ajax({url:chrom_url,data:b,dataType:"json",success:function(e){if(e.chrom_info.length===0){alert("Invalid chromosome: "+b.chrom);return}if(e.reference){a.add_label_track(new ReferenceTrack(a))}a.chrom_data=e.chrom_info;var h='<option value="">Select Chrom/Contig</option>';for(var g=0,d=a.chrom_data.length;g<d;g++){var f=a.chrom_data[g].chrom;h+='<option value="'+f+'">'+f+"</option>"}if(e.prev_chroms){h+='<option value="previous">Previous '+MAX_CHROMS_SELECTABLE+"</option>"}if(e.next_chroms){h+='<option value="next">Next '+MAX_CHROMS_SELECTABLE+"</option>"}a.chrom_select.html(h);if(c){c()}a.chrom_start_index=e.start_index},error:function(){alert("Could not load chroms for this dbkey:",a.dbkey)}})},change_chrom:function(e,b,g){if(!e||e==="None"){return}var d=this;if(e==="previous"){d.load_chroms({low:this.chrom_start_index-MAX_CHROMS_SELECTABLE});return}if(e==="next"){d.load_chroms({low:this.chrom_start_index+MAX_CHROMS_SELECTABLE});return}var f=$.grep(d.chrom_data,function(j,k){return j.chrom===e})[0];if(f===undefined){d.load_chroms({chrom:e},function(){d.change_chrom(e,b,g)});return}else{if(e!==d.chrom){d.chrom=e;if(!d.chrom){d.intro_div.show()}else{d.intro_div.hide()}d.chrom_select.val(d.chrom);d.max_high=f.len-1;d.reset();d.redraw(true);for(var h=0,a=d.tracks.length;h<a;h++){var c=d.tracks[h];if(c.init){c.init()}}}if(b!==undefined&&g!==undefined){d.low=Math.max(b,0);d.high=Math.min(g,d.max_high)}d.reset_overview();d.redraw()}},go_to:function(f){var k=this,a,d,b=f.split(":"),h=b[0],j=b[1];if(j!==undefined){try{var g=j.split("-");a=parseInt(g[0].replace(/,/g,""),10);d=parseInt(g[1].replace(/,/g,""),10)}catch(c){return false}}k.change_chrom(h,a,d)},move_fraction:function(c){var a=this;var b=a.high-a.low;this.move_delta(c*b)},move_delta:function(c){var a=this;var b=a.high-a.low;if(a.low-c<a.max_low){a.low=a.max_low;a.high=a.max_low+b}else{if(a.high-c>a.max_high){a.high=a.max_high;a.low=a.max_high-b}else{a.high-=c;a.low-=c}}a.redraw()},add_track:function(a){a.view=this;a.track_id=this.track_id_counter;this.tracks.push(a);if(a.init){a.init()}a.container_div.attr("id","track_"+a.track_id);sortable(a.container_div,".draghandle");this.track_id_counter+=1;this.num_tracks+=1},add_label_track:function(a){a.view=this;this.label_tracks.push(a)},remove_track:function(a){this.has_changes=true;a.container_div.fadeOut("slow",function(){$(this).remove()});delete this.tracks[this.tracks.indexOf(a)];this.num_tracks-=1},reset:function(){this.low=this.max_low;this.high=this.max_high;this.viewport_container.find(".yaxislabel").remove()},redraw:function(h){var g=this.high-this.low,f=this.low,b=this.high;if(f<this.max_low){f=this.max_low}if(b>this.max_high){b=this.max_high}if(this.high!==0&&g<this.min_separation){b=f+this.min_separation}this.low=Math.floor(f);this.high=Math.ceil(b);this.resolution=Math.pow(10,Math.ceil(Math.log((this.high-this.low)/200)/Math.LN10));this.zoom_res=Math.pow(FEATURE_LEVELS,Math.max(0,Math.ceil(Math.log(this.resolution,FEATURE_LEVELS)/Math.log(FEATURE_LEVELS))));var a=(this.low/(this.max_high-this.max_low)*this.overview_viewport.width())||0;var e=((this.high-this.low)/(this.max_high-this.max_low)*this.overview_viewport.width())||0;var j=13;this.overview_box.css({left:a,width:Math.max(j,e)}).show();if(e<j){this.overview_box.css("left",a-(j-e)/2)}if(this.overview_highlight){this.overview_highlight.css({left:a,width:e})}this.update_location(this.low,this.high);if(!h){for(var c=0,d=this.tracks.length;c<d;c++){if(this.tracks[c]&&this.tracks[c].enabled){this.tracks[c].draw()}}for(c=0,d=this.label_tracks.length;c<d;c++){this.label_tracks[c].draw()}}},zoom_in:function(b,c){if(this.max_high===0||this.high-this.low<this.min_separation){return}var d=this.high-this.low,e=d/2+this.low,a=(d/this.zoom_factor)/2;if(b){e=b/this.viewport_container.width()*(this.high-this.low)+this.low}this.low=Math.round(e-a);this.high=Math.round(e+a);this.redraw()},zoom_out:function(){if(this.max_high===0){return}var b=this.high-this.low,c=b/2+this.low,a=(b*this.zoom_factor)/2;this.low=Math.round(c-a);this.high=Math.round(c+a);this.redraw()},resize_window:function(){this.viewport_container.height(this.container.height()-this.top_container.height()-this.bottom_container.height());this.nav_container.width(this.container.width());this.redraw()},reset_overview:function(){this.overview_viewport.find("canvas").remove();this.overview_viewport.height(this.default_overview_height);this.overview_box.height(this.default_overview_height);this.overview_close.hide();this.overview_highlight.hide()}});var Tool=function(g){this.name=g.name;this.params=[];var a=g.params;for(var e=0;e<a.length;e++){var d=a[e],c=d.name,b=d.label,f=d.type;if(f==="int"||f==="float"){this.params[this.params.length]=new NumberParameter(c,b,d.min,d.max,d.value)}else{if(f=="select"){this.params[this.params.length]=new SelectParameter(c,b,d.options)}else{console.log("WARNING: unrecognized tool parameter type:",c,f)}}}};$.extend(Tool.prototype,{get_param_values_dict:function(){var b={};for(var a=0;a<this.params.length;a++){var c=this.params[a];b[c.name]=JSON.stringify(c.value)}return b},get_param_values:function(){var b=[];for(var a=0;a<this.params.length;a++){b[a]=this.params[a].value}return b}});var NumberParameter=function(c,b,e,a,d){this.name=c;this.label=b;this.min=e;this.max=a;this.value=d};var SelectParameter=function(c,b,a){this.name=c;this.label=b;this.options=a;this.value=(a.length!==0?a[0][1]:null)};var Filter=function(b,a,c){this.name=b;this.index=a;this.value=c};var NumberFilter=function(b,a){this.name=b;this.index=a;this.low=-Number.MAX_VALUE;this.high=Number.MAX_VALUE;this.min=Number.MAX_VALUE;this.max=-Number.MAX_VALUE;this.slider=null;this.slider_label=null};$.extend(NumberFilter.prototype,{applies_to:function(a){if(a.length>this.index){return true}return false},keep:function(a){if(!this.applies_to(a)){return true}return(a[this.index]>=this.low&&a[this.index]<=this.high)},update_attrs:function(b){var a=false;if(!this.applies_to(b)){return a}if(b[this.index]<this.min){this.min=Math.floor(b[this.index]);a=true}if(b[this.index]>this.max){this.max=Math.ceil(b[this.index]);a=true}return a},update_ui_elt:function(){var b=this.slider.slider("option","min"),a=this.slider.slider("option","max");if(this.min<b||this.max>a){this.slider.slider("option","min",this.min);this.slider.slider("option","max",this.max);this.slider.slider("option","step",get_slider_step(this.min,this.max));this.slider.slider("option","values",[this.min,this.max])}}});var get_filters_from_dict=function(a){var g=[];for(var d=0;d<a.length;d++){var f=a[d];var c=f.name,e=f.type,b=f.index;if(e==="int"||e==="float"){g[d]=new NumberFilter(c,b)}else{g[d]=new Filter(c,b,e)}}return g};var TrackConfig=function(a){this.track=a.track;this.params=a.params;this.values={};if(a.saved_values){this.restore_values(a.saved_values)}this.onchange=a.onchange};$.extend(TrackConfig.prototype,{restore_values:function(a){var b=this;$.each(this.params,function(c,d){if(a[d.key]!==undefined){b.values[d.key]=a[d.key]}else{b.values[d.key]=d.default_value}})},build_form:function(){var b=this;var a=$("<div />");$.each(this.params,function(f,d){if(!d.hidden){var c="param_"+f;var l=$("<div class='form-row' />").appendTo(a);l.append($("<label />").attr("for",c).text(d.label+":"));if(d.type==="bool"){l.append($('<input type="checkbox" />').attr("id",c).attr("name",c).attr("checked",b.values[d.key]))}else{if(d.type==="color"){var h=b.values[d.key];var g=$("<input />").attr("id",c).attr("name",c).val(h);var j=$("<div class='tipsy tipsy-north' style='position: absolute;' />").hide();var e=$("<div style='background-color: black; padding: 10px;'></div>").appendTo(j);var k=$("<div/>").appendTo(e).farbtastic({width:100,height:100,callback:g,color:h});$("<div />").append(g).append(j).appendTo(l).bind("click",function(m){j.css({left:$(this).position().left+($(g).width()/2)-60,top:$(this).position().top+$(this.height)}).show();$(document).bind("click.color-picker",function(){j.hide();$(document).unbind("click.color-picker")});m.stopPropagation()})}else{l.append($("<input />").attr("id",c).attr("name",c).val(b.values[d.key]))}}}});return a},update_from_form:function(a){var c=this;var b=false;$.each(this.params,function(d,f){if(!f.hidden){var g="param_"+d;var e=a.find("#"+g).val();if(f.type==="float"){e=parseFloat(e)}else{if(f.type==="int"){e=parseInt(e)}else{if(f.type==="bool"){e=a.find("#"+g).is(":checked")}}}if(e!==c.values[f.key]){c.values[f.key]=e;b=true}}});if(b){this.onchange()}}});var Tile=function(a,b,c){this.track=a;this.canvas=b;this.histo_max=c};var Track=function(b,a,e,c,d){this.name=b;this.view=a;this.parent_element=e;this.data_url=(c?c:default_data_url);this.data_url_extra_params={};this.data_query_wait=(d?d:DEFAULT_DATA_QUERY_WAIT);this.dataset_check_url=converted_datasets_state_url;this.container_div=$("<div />").addClass("track").css("position","relative");if(!this.hidden){this.header_div=$("<div class='track-header' />").appendTo(this.container_div);if(this.view.editor){this.drag_div=$("<div class='draghandle' />").appendTo(this.header_div)}this.name_div=$("<div class='menubutton popup' />").appendTo(this.header_div);this.name_div.text(this.name);this.name_div.attr("id",this.name.replace(/\s+/g,"-").replace(/[^a-zA-Z0-9\-]/g,"").toLowerCase())}this.content_div=$("<div class='track-content'>").appendTo(this.container_div);this.parent_element.append(this.container_div)};$.extend(Track.prototype,{init:function(){var a=this;a.enabled=false;a.tile_cache.clear();a.data_cache.clear();a.initial_canvas=undefined;a.content_div.css("height","auto");a.container_div.removeClass("nodata error pending");if(!a.dataset_id){return}$.getJSON(converted_datasets_state_url,{hda_ldda:a.hda_ldda,dataset_id:a.dataset_id,chrom:a.view.chrom},function(b){if(!b||b==="error"||b.kind==="error"){a.container_div.addClass("error");a.content_div.text(DATA_ERROR);if(b.message){var d=a.view.tracks.indexOf(a);var c=$(" <a href='javascript:void(0);'></a>").text("View error").bind("click",function(){show_modal("Trackster Error","<pre>"+b.message+"</pre>",{Close:hide_modal})});a.content_div.append(c)}}else{if(b==="no converter"){a.container_div.addClass("error");a.content_div.text(DATA_NOCONVERTER)}else{if(b==="no data"||(b.data!==undefined&&(b.data===null||b.data.length===0))){a.container_div.addClass("nodata");a.content_div.text(DATA_NONE)}else{if(b==="pending"){a.container_div.addClass("pending");a.content_div.text(DATA_PENDING);setTimeout(function(){a.init()},a.data_query_wait)}else{if(b.status==="data"){if(b.valid_chroms){a.valid_chroms=b.valid_chroms;a.make_name_popup_menu()}a.content_div.text(DATA_OK);if(a.view.chrom){a.content_div.text("");a.content_div.css("height",a.height_px+"px");a.enabled=true;$.when(a.predraw_init()).done(function(){a.draw()})}}}}}}})},predraw_init:function(){},update_name:function(a){this.old_name=this.name;this.name=a;this.name_div.text(this.name)},revert_name:function(){this.name=this.old_name;this.name_div.text(this.name)}});var TiledTrack=function(b,h,o){var c=this,p=c.view;this.filters=(b!==undefined?get_filters_from_dict(b):[]);this.filters_available=false;this.filters_visible=false;this.tool=(h!==undefined&&obj_length(h)>0?new Tool(h):undefined);this.parent_track=o;this.child_tracks=[];if(c.hidden){return}var k=function(r,s,t){r.click(function(){var u=s.text();max=parseFloat(t.slider("option","max")),input_size=(max<=1?4:max<=1000000?max.toString().length:6),multi_value=false;if(t.slider("option","values")){input_size=2*input_size+1;multi_value=true}s.text("");$("<input type='text'/>").attr("size",input_size).attr("maxlength",input_size).attr("value",u).appendTo(s).focus().select().click(function(v){v.stopPropagation()}).blur(function(){$(this).remove();s.text(u)}).keyup(function(z){if(z.keyCode===27){$(this).trigger("blur")}else{if(z.keyCode===13){var x=t.slider("option","min"),v=t.slider("option","max"),y=function(A){return(isNaN(A)||A>v||A<x)},w=$(this).val();if(!multi_value){w=parseFloat(w);if(y(w)){alert("Parameter value must be in the range ["+x+"-"+v+"]");return $(this)}}else{w=w.split("-");w=[parseFloat(w[0]),parseFloat(w[1])];if(y(w[0])||y(w[1])){alert("Parameter value must be in the range ["+x+"-"+v+"]");return $(this)}}t.slider((multi_value?"values":"value"),w)}}})})};if(this.parent_track){this.header_div.find(".draghandle").removeClass("draghandle").addClass("child-track-icon").addClass("icon-button");this.parent_element.addClass("child-track");this.tool=undefined}this.filters_div=$("<div/>").addClass("filters").hide();this.header_div.after(this.filters_div);this.filters_div.bind("drag",function(r){r.stopPropagation()}).bind("click",function(r){r.stopPropagation()}).bind("dblclick",function(r){r.stopPropagation()});$.each(this.filters,function(x,s){var u=$("<div/>").addClass("slider-row").appendTo(c.filters_div);var r=$("<div/>").addClass("slider-label").appendTo(u);var z=$("<span/>").addClass("slider-name").text(s.name+" ").appendTo(r);var t=$("<span/>");var v=$("<span/>").addClass("slider-value").appendTo(r).append("[").append(t).append("]");var y=$("<div/>").addClass("slider").appendTo(u);s.control_element=$("<div/>").attr("id",s.name+"-filter-control").appendTo(y);var w=[0,0];s.control_element.slider({range:true,min:Number.MAX_VALUE,max:-Number.MIN_VALUE,values:[0,0],slide:function(A,B){w=B.values;t.text(B.values[0]+"-"+B.values[1]);setTimeout(function(){if(B.values[0]==w[0]&&B.values[1]==w[1]){var C=B.values;t.text(C[0]+"-"+C[1]);s.low=C[0];s.high=C[1];c.draw(true,true)}},50)},change:function(A,B){s.control_element.slider("option","slide").call(s.control_element,A,B)}});s.slider=s.control_element;s.slider_label=t;k(v,t,s.control_element);$("<div style='clear: both;'/>").appendTo(u)});if(this.tool){this.dynamic_tool_div=$("<div/>").addClass("dynamic-tool").hide();this.header_div.after(this.dynamic_tool_div);this.dynamic_tool_div.bind("drag",function(r){r.stopPropagation()}).bind("click",function(r){r.stopPropagation()}).bind("dblclick",function(r){r.stopPropagation()});var n=$("<div class='tool-name'>").appendTo(this.dynamic_tool_div).text(this.tool.name);var m=this.tool.params;var c=this;$.each(this.tool.params,function(A,s){if(s instanceof NumberParameter){var x=$("<div>").addClass("slider-row").appendTo(c.dynamic_tool_div);var u=$("<div>").addClass("slider-label").appendTo(x);var C=$("<span/>").addClass("slider-name").text(s.label+" ").appendTo(u);var t=$("<span/>").text(s.value);var y=$("<span/>").addClass("slider-value").appendTo(u).append("[").append(t).append("]");var B=$("<div/>").addClass("slider").appendTo(x);var r=$("<div id='"+s.name+"-param-control'>").appendTo(B);r.slider({min:s.min,max:s.max,step:get_slider_step(s.min,s.max),value:s.value,slide:function(F,H){var G=H.value;s.value=G;if(0<G&&G<1){G=parseFloat(G).toFixed(2)}t.text(G)},change:function(F,G){r.slider("option","slide").call(r,F,G)}});k(y,t,r);$("<div style='clear: both;'/>").appendTo(x)}else{if(s instanceof SelectParameter){var x=$("<div>").addClass("slider-row").appendTo(c.dynamic_tool_div);var u=$("<div>").addClass("slider-label").appendTo(x);var C=$("<span/>").addClass("slider-name").text(s.label+" ").appendTo(u);var E=$("<div/>").addClass("slider").appendTo(x);var w=$("<select/>").appendTo(E);w.change(function(){s.value=$(this).val()});var D=w.attr("options");for(var z=0;z<s.options.length;z++){var v=s.options[z];D[D.length]=new Option(v[0],v[1])}$("<div style='clear: both;'/>").appendTo(x)}}});var q=$("<div>").addClass("slider-row").appendTo(this.dynamic_tool_div);var l=$("<input type='submit'>").attr("value","Run").appendTo(q);var c=this;l.click(function(){c.run_tool()})}c.child_tracks_container=$("<div/>").addClass("child-tracks-container").hide();c.container_div.append(c.child_tracks_container);if(c.display_modes!==undefined){if(c.mode_div===undefined){c.mode_div=$("<div class='right-float menubutton popup' />").appendTo(c.header_div);var e=(c.track_config&&c.track_config.values.mode?c.track_config.values.mode:c.display_modes[0]);c.mode=e;c.mode_div.text(e);var d=function(r){c.mode_div.text(r);c.mode=r;c.track_config.values.mode=r;c.tile_cache.clear();c.draw()};var a={};for(var f=0,j=c.display_modes.length;f<j;f++){var g=c.display_modes[f];a[g]=function(r){return function(){d(r)}}(g)}make_popupmenu(c.mode_div,a)}else{c.mode_div.hide()}}this.make_name_popup_menu()};$.extend(TiledTrack.prototype,Track.prototype,{make_name_popup_menu:function(){var b=this;var a={};a["Edit configuration"]=function(){var h=function(){hide_modal();$(window).unbind("keypress.check_enter_esc")},f=function(){b.track_config.update_from_form($(".dialog-box"));hide_modal();$(window).unbind("keypress.check_enter_esc")},g=function(j){if((j.keyCode||j.which)===27){h()}else{if((j.keyCode||j.which)===13){f()}}};$(window).bind("keypress.check_enter_esc",g);show_modal("Configure Track",b.track_config.build_form(),{Cancel:h,OK:f})};if(b.filters_available>0){var e=(b.filters_div.is(":visible")?"Hide filters":"Show filters");a[e]=function(){b.filters_visible=(b.filters_div.is(":visible"));b.filters_div.toggle();b.make_name_popup_menu()}}if(b.tool){var e=(b.dynamic_tool_div.is(":visible")?"Hide tool":"Show tool");a[e]=function(){if(!b.dynamic_tool_div.is(":visible")){b.update_name(b.name+b.tool_region_and_parameters_str())}else{menu_option_text="Show dynamic tool";b.revert_name()}b.dynamic_tool_div.toggle();b.make_name_popup_menu()}}if(b.valid_chroms){a["List chrom/contigs with data"]=function(){show_modal("Chrom/contigs with data","<p>"+b.valid_chroms.join("<br/>")+"</p>",{Close:function(){hide_modal()}})}}var c=view;var d=function(){$("#no-tracks").show()};if(this.parent_track){c=this.parent_track;d=function(){}}a.Remove=function(){c.remove_track(b);if(c.num_tracks===0){d()}};make_popupmenu(b.name_div,a)},draw:function(a,d){var s=this.view.low,g=this.view.high,j=g-s,l=this.view.container.width(),f=l/j,m=this.view.resolution,e=$("<div style='position: relative;'></div>");if(!d){this.content_div.children().remove()}this.content_div.append(e);this.max_height=0;var o=Math.floor(s/m/DENSITY);var c={};while((o*DENSITY*m)<g){var r=l+"_"+f+"_"+o;var h=this.tile_cache.get(r);var p=o*DENSITY*this.view.resolution;var b=p+DENSITY*this.view.resolution;if(!a&&h){this.show_tile(h,e,p,f)}else{this.delayed_draw(a,r,p,b,o,m,e,f,c)}o+=1}var k=this;var q=setInterval(function(){if(obj_length(c)===0){clearInterval(q);if(d){var v=k.content_div.children();var u=false;for(var w=v.length-1,t=0;w>=t;w--){var z=$(v[w]);if(u){z.remove()}else{if(z.children().length!==0){u=true}}}}for(var y=0;y<k.filters.length;y++){k.filters[y].update_ui_elt()}var x=false;if(k.example_feature){for(var y=0;y<k.filters.length;y++){if(k.filters[y].applies_to(k.example_feature)){x=true;break}}}if(k.filters_available!==x){k.filters_available=x;if(!k.filters_available){k.filters_div.hide()}k.make_name_popup_menu()}}},50);for(var n=0;n<this.child_tracks.length;n++){this.child_tracks[n].draw(a,d)}},delayed_draw:function(b,j,g,l,c,e,k,m,f){var d=this;var h=function(u,n,p,o,s,t,q){returned_tile=d.draw_tile(n,p,o,s,t,q);var r=$("<div class='track-tile'>").prepend(returned_tile);tile_element=r;d.tile_cache.set(j,tile_element);d.show_tile(tile_element,s,g,t);delete f[u]};var a=setTimeout(function(){if(g<=d.view.high&&l>=d.view.low){var n=(b?undefined:d.tile_cache.get(j));if(n){d.show_tile(n,k,g,m);delete f[a]}else{$.when(d.data_cache.get_data(view.chrom,g,l,d.mode,e,d.data_url_extra_params)).then(function(o){if(view.reference_track&&m>CHAR_WIDTH_PX){$.when(view.reference_track.data_cache.get_data(view.chrom,g,l,d.mode,e,view.reference_track.data_url_extra_params)).then(function(p){h(a,o,e,c,k,m,p)})}else{h(a,o,e,c,k,m)}})}}},50);f[a]=true},show_tile:function(a,f,d,g){var b=this;var c=this.view.high-this.view.low,e=(d-this.view.low)*g;if(this.left_offset){e-=this.left_offset}a.css({position:"absolute",top:0,left:e,height:""});f.append(a);b.max_height=Math.max(b.max_height,a.height());b.content_div.css("height",b.max_height+"px");f.children().css("height",b.max_height+"px")},set_overview:function(){var a=this.view;if(this.initial_canvas&&this.is_overview){a.overview_close.show();a.overview_viewport.append(this.initial_canvas);a.overview_highlight.show().height(this.initial_canvas.height());a.overview_viewport.height(this.initial_canvas.height()+a.overview_box.height())}$(window).trigger("resize")},run_tool:function(){var b={dataset_id:this.original_dataset_id,chrom:this.view.chrom,low:this.view.low,high:this.view.high,tool_id:this.tool.name};$.extend(b,this.tool.get_param_values_dict());var d=this,c=b.tool_id+d.tool_region_and_parameters_str(b.chrom,b.low,b.high),e;if(d.track_type==="FeatureTrack"){e=new ToolDataFeatureTrack(c,view,d.hda_ldda,undefined,{},{},d)}this.add_track(e);e.content_div.text("Starting job.");view.has_changes=true;var a=function(){$.getJSON(run_tool_url,b,function(f){if(f==="no converter"){e.container_div.addClass("error");e.content_div.text(DATA_NOCONVERTER)}else{if(f.error){e.container_div.addClass("error");e.content_div.text(DATA_CANNOT_RUN_TOOL+f.message)}else{if(f==="pending"){e.container_div.addClass("pending");e.content_div.text("Converting input data so that it can be easily reused.");setTimeout(a,2000)}else{e.dataset_id=f.dataset_id;e.content_div.text("Running job.");e.init()}}}})};a()},tool_region_and_parameters_str:function(c,a,d){var b=this,e=(c!==undefined&&a!==undefined&&d!==undefined?c+":"+a+"-"+d:"all");return" - region=["+e+"], parameters=["+b.tool.get_param_values().join(", ")+"]"},add_track:function(a){a.track_id=this.track_id+"_"+this.child_tracks.length;a.container_div.attr("id","track_"+a.track_id);this.child_tracks_container.append(a.container_div);sortable(a.container_div,".child-track-icon");if(!$(this.child_tracks_container).is(":visible")){this.child_tracks_container.show()}this.child_tracks.push(a)},remove_track:function(a){a.container_div.fadeOut("slow",function(){$(this).remove()})}});var LabelTrack=function(a,b){this.track_type="LabelTrack";this.hidden=true;Track.call(this,null,a,b);this.container_div.addClass("label-track")};$.extend(LabelTrack.prototype,Track.prototype,{draw:function(){var c=this.view,d=c.high-c.low,g=Math.floor(Math.pow(10,Math.floor(Math.log(d)/Math.log(10)))),a=Math.floor(c.low/g)*g,e=this.view.container.width(),b=$("<div style='position: relative; height: 1.3em;'></div>");while(a<c.high){var f=(a-c.low)/d*e;b.append($("<div class='label'>"+commatize(a)+"</div>").css({position:"absolute",left:f-1}));a+=g}this.content_div.children(":first").remove();this.content_div.append(b)}});var ReferenceTrack=function(a){this.track_type="ReferenceTrack";this.hidden=true;Track.call(this,null,a,a.top_labeltrack);TiledTrack.call(this);a.reference_track=this;this.left_offset=200;this.height_px=12;this.font=DEFAULT_FONT;this.container_div.addClass("reference-track");this.content_div.css("background","none");this.content_div.css("min-height","0px");this.content_div.css("border","none");this.data_url=reference_url;this.data_url_extra_params={dbkey:a.dbkey};this.data_cache=new DataManager(CACHED_DATA,this,false);this.tile_cache=new Cache(CACHED_TILES_LINE)};$.extend(ReferenceTrack.prototype,TiledTrack.prototype,{draw_tile:function(m,g,b,j,n){var f=this,d=DENSITY*g;if(n>CHAR_WIDTH_PX){if(m===null){f.content_div.css("height","0px");return}var e=this.view.canvas_manager.new_canvas();var l=e.getContext("2d");e.width=Math.ceil(d*n+f.left_offset);e.height=f.height_px;l.font=DEFAULT_FONT;l.textAlign="center";for(var h=0,k=m.length;h<k;h++){var a=Math.round(h*n);l.fillText(m[h],a+f.left_offset,10)}return e}this.content_div.css("height","0px")}});var LineTrack=function(e,c,f,a,d){var b=this;this.track_type="LineTrack";this.display_modes=["Histogram","Line","Filled","Intensity"];this.mode="Histogram";Track.call(this,e,c,c.viewport_container);TiledTrack.call(this);this.min_height_px=16;this.max_height_px=400;this.height_px=80;this.hda_ldda=f;this.dataset_id=a;this.original_dataset_id=a;this.data_cache=new DataManager(CACHED_DATA,this);this.tile_cache=new Cache(CACHED_TILES_LINE);this.track_config=new TrackConfig({track:this,params:[{key:"color",label:"Color",type:"color",default_value:"black"},{key:"min_value",label:"Min Value",type:"float",default_value:undefined},{key:"max_value",label:"Max Value",type:"float",default_value:undefined},{key:"mode",type:"string",default_value:this.mode,hidden:true},{key:"height",type:"int",default_value:this.height_px,hidden:true}],saved_values:d,onchange:function(){b.vertical_range=b.prefs.max_value-b.prefs.min_value;$("#linetrack_"+b.track_id+"_minval").text(b.prefs.min_value);$("#linetrack_"+b.track_id+"_maxval").text(b.prefs.max_value);b.tile_cache.clear();b.draw()}});this.prefs=this.track_config.values;this.height_px=this.track_config.values.height;this.vertical_range=this.track_config.values.max_value-this.track_config.values.min_value;this.add_resize_handle()};$.extend(LineTrack.prototype,TiledTrack.prototype,{add_resize_handle:function(){var a=this;var d=false;var c=false;var b=$("<div class='track-resize'>");$(a.container_div).hover(function(){d=true;b.show()},function(){d=false;if(!c){b.hide()}});b.hide().bind("dragstart",function(f,g){c=true;g.original_height=$(a.content_div).height()}).bind("drag",function(g,h){var f=Math.min(Math.max(h.original_height+h.deltaY,a.min_height_px),a.max_height_px);$(a.content_div).css("height",f);a.height_px=f;a.draw(true)}).bind("dragend",function(f,g){a.tile_cache.clear();c=false;if(!d){b.hide()}a.track_config.values.height=a.height_px}).appendTo(a.container_div)},predraw_init:function(){var a=this,b=a.view.tracks.indexOf(a);a.vertical_range=undefined;return $.getJSON(a.data_url,{stats:true,chrom:a.view.chrom,low:null,high:null,hda_ldda:a.hda_ldda,dataset_id:a.dataset_id},function(c){a.container_div.addClass("line-track");var e=c.data;if(isNaN(parseFloat(a.prefs.min_value))||isNaN(parseFloat(a.prefs.max_value))){a.prefs.min_value=e.min;a.prefs.max_value=e.max;$("#track_"+b+"_minval").val(a.prefs.min_value);$("#track_"+b+"_maxval").val(a.prefs.max_value)}a.vertical_range=a.prefs.max_value-a.prefs.min_value;a.total_frequency=e.total_frequency;a.container_div.find(".yaxislabel").remove();var f=$("<div />").addClass("yaxislabel").attr("id","linetrack_"+b+"_minval").text(round_1000(a.prefs.min_value));var d=$("<div />").addClass("yaxislabel").attr("id","linetrack_"+b+"_maxval").text(round_1000(a.prefs.max_value));d.css({position:"absolute",top:"24px",left:"10px"});d.prependTo(a.container_div);f.css({position:"absolute",bottom:"2px",left:"10px"});f.prependTo(a.container_div)})},draw_tile:function(m,e,b,j,l){if(this.vertical_range===undefined){return}var f=b*DENSITY*e,d=DENSITY*e,a=Math.ceil(d*l),h=this.height_px;var c=this.view.canvas_manager.new_canvas();c.width=a,c.height=h;var k=c.getContext("2d");var g=new LinePainter(m.data,f,f+d,this.prefs.min_value,this.prefs.max_value,this.prefs.color,this.mode);g.draw(k,a,h);return c}});var FeatureTrack=function(a,f,e,j,h,c,d,g){var b=this;this.track_type="FeatureTrack";this.display_modes=["Auto","Dense","Squish","Pack"];this.track_config=new TrackConfig({track:this,params:[{key:"block_color",label:"Block color",type:"color",default_value:"#444"},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},],saved_values:h,onchange:function(){b.tile_cache.clear();b.draw()}});this.prefs=this.track_config.values;Track.call(this,a,f,f.viewport_container);TiledTrack.call(this,c,d,g);this.height_px=0;this.container_div.addClass("feature-track");this.hda_ldda=e;this.dataset_id=j;this.original_dataset_id=j;this.zo_slots={};this.show_labels_scale=0.001;this.showing_details=false;this.summary_draw_height=30;this.default_font=DEFAULT_FONT;this.inc_slots={};this.start_end_dct={};this.tile_cache=new Cache(CACHED_TILES_FEATURE);this.data_cache=new DataManager(20,this);this.left_offset=200;this.painter=LinkedFeaturePainter};$.extend(FeatureTrack.prototype,TiledTrack.prototype,{incremental_slots:function(d,b,c){var a=this.inc_slots[d];if(!a||(a.mode!==c)){a=new FeatureSlotter(d,c==="Pack",function(e){return CONTEXT.measureText(e)});a.mode=c;this.inc_slots[d]=a}return a.slot_features(b)},get_y_scale:function(b){var a;if(b==="summary_tree"){}if(b==="Dense"){a=DENSE_TRACK_HEIGHT}else{if(b==="no_detail"){a=NO_DETAIL_TRACK_HEIGHT}else{if(b==="Squish"){a=SQUISH_TRACK_HEIGHT}else{a=PACK_TRACK_HEIGHT}}}return a},draw_tile:function(o,w,A,k,m,d){var t=this,C=A*DENSITY*w,b=(A+1)*DENSITY*w,q=b-C,u=Math.ceil(q*m),s=this.mode,G=25,e=this.left_offset,p,g;if(s==="Auto"){if(o.dataset_type==="summary_tree"){s=o.dataset_type}else{if(o.extra_info==="no_detail"){s="no_detail"}else{var F=o.data;if((F.length&&F.length<4)||(this.view.high-this.view.low>MIN_SQUISH_VIEW_WIDTH)){s="Squish"}else{s="Pack"}}}}if(s==="summary_tree"){g=this.summary_draw_height;k.parent().css("height",Math.max(this.height_px,g)+"px");this.container_div.find(".yaxislabel").remove();var a=$("<div />").addClass("yaxislabel");a.text(o.max);a.css({position:"absolute",top:"22px",left:"10px"});a.prependTo(this.container_div);var c=this.view.canvas_manager.new_canvas();c.width=u+e;c.height=g+LABEL_SPACING+CHAR_HEIGHT_PX;var D=new SummaryTreePainter(o.data,o.delta,o.max,C,b,this.prefs.show_counts);var v=c.getContext("2d");v.translate(e,0);D.draw(v,u,g);return c}var p,j=1;if(s==="no_detail"||s==="Squish"||s==="Pack"){j=this.incremental_slots(m,o.data,s);p=this.inc_slots[m].slots}var l=[];if(o.data){for(var x=0,z=o.data.length;x<z;x++){var h=o.data[x];var y=false;var n;for(var B=0,E=this.filters.length;B<E;B++){n=this.filters[B];n.update_attrs(h);if(!n.keep(h)){y=true;break}}if(!y){l.push(h)}}}var D=new (this.painter)(l,C,b,this.prefs,s,d);var g=D.get_required_height(j)+ERROR_PADDING;var c=this.view.canvas_manager.new_canvas();c.width=u+e;c.height=g;k.parent().css("height",Math.max(this.height_px,g)+"px");var v=c.getContext("2d");v.fillStyle=this.prefs.block_color;v.font=this.default_font;v.textAlign="right";this.container_div.find(".yaxislabel").remove();if(o.message){$(c).css({"border-top":"1px solid red"});v.fillStyle="red";v.textAlign="left";var r=v.textBaseline;v.textBaseline="top";v.fillText(o.message,e,0);v.textBaseline=r;if(!o.data){return c}}this.example_feature=(o.data.length?o.data[0]:undefined);v.translate(e,ERROR_PADDING);D.draw(v,u,g,p);return c}});var VcfTrack=function(d,b,f,a,c,e){FeatureTrack.call(this,d,b,f,a,c,e);this.track_type="VcfTrack";this.painter=VariantPainter};$.extend(VcfTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype);var ReadTrack=function(d,b,f,a,c,e){FeatureTrack.call(this,d,b,f,a,c,e);this.track_config=new TrackConfig({track:this,params:[{key:"block_color",label:"Block color",type:"color",default_value:"#444"},{key:"label_color",label:"Label color",type:"color",default_value:"black"},{key:"show_insertions",label:"Show insertions",type:"bool",default_value:false},{key:"show_differences",label:"Show differences only",type:"bool",default_value:true},{key:"show_counts",label:"Show summary counts",type:"bool",default_value:true},{key:"mode",type:"string",default_value:this.mode,hidden:true},],saved_values:c,onchange:function(){this.track.tile_cache.clear();this.track.draw()}});this.prefs=this.track_config.values;this.track_type="ReadTrack";this.painter=ReadPainter;this.make_name_popup_menu()};$.extend(ReadTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype);var ToolDataFeatureTrack=function(e,c,g,a,d,f,b){FeatureTrack.call(this,e,c,g,a,d,f,{},b);this.track_type="ToolDataFeatureTrack";this.data_url=raw_data_url;this.data_query_wait=1000;this.dataset_check_url=dataset_state_url};$.extend(ToolDataFeatureTrack.prototype,TiledTrack.prototype,FeatureTrack.prototype,{predraw_init:function(){var b=this;var a=function(){if(b.data_cache.size()===0){setTimeout(a,300)}else{b.data_url=default_data_url;b.data_query_wait=DEFAULT_DATA_QUERY_WAIT;b.dataset_state_url=converted_datasets_state_url;$.getJSON(b.dataset_state_url,{dataset_id:b.dataset_id,hda_ldda:b.hda_ldda},function(c){})}};a()}});var extend=function(){var c=arguments[0];for(var b=1;b<arguments.length;b++){var a=arguments[b];for(key in a){c[key]=a[key]}}};var CanvasManager=function(a){this.document=a};CanvasManager.prototype.new_canvas=function(){var a=this.document.createElement("canvas");if(window.G_vmlCanvasManager){G_vmlCanvasManager.initElement(a)}return a};var FeatureSlotter=function(c,b,a){this.slots={};this.start_end_dct={};this.w_scale=c;this.include_label=b;this.measureText=a};FeatureSlotter.prototype.slot_features=function(g){var k=this.w_scale,n=this.slots,c=this.start_end_dct,t=[],u=[],h=0;for(var r=0,s=g.length;r<s;r++){var e=g[r],j=e[0];if(n[j]!==undefined){h=Math.max(h,n[j]);u.push(n[j])}else{t.push(r)}}var l=function(A,B){for(var z=0;z<=MAX_FEATURE_DEPTH;z++){var x=false,C=c[z];if(C!==undefined){for(var w=0,y=C.length;w<y;w++){var v=C[w];if(B>v[0]&&A<v[1]){x=true;break}}}if(!x){return z}}return -1};for(var r=0,s=t.length;r<s;r++){var e=g[t[r]],j=e[0],p=e[1],a=e[2],m=e[3],b=Math.floor(p*k),f=Math.ceil(a*k),q=this.measureText(m).width,d;if(m!==undefined&&this.include_label){q+=(LABEL_SPACING+PACK_SPACING);if(b-q>=0){b-=q;d="left"}else{f+=q;d="right"}}var o=l(b,f);if(o>=0){if(c[o]===undefined){c[o]=[]}c[o].push([b,f]);n[j]=o;h=Math.max(h,o)}else{}}return h+1};var SummaryTreePainter=function(d,f,b,e,a,c){this.data=d;this.delta=f;this.max=b;this.view_start=e;this.view_end=a;this.show_counts=c};SummaryTreePainter.prototype.draw=function(o,a,n){var f=this.view_start,q=this.view_end-this.view_start,p=a/q;var l=this.data,k=this.delta,h=this.max,c=n+LABEL_SPACING+CHAR_HEIGHT_PX;delta_x_px=Math.ceil(k*p);o.save();for(var d=0,e=l.length;d<e;d++){var j=Math.floor((l[d][0]-f)*p);var g=l[d][1];if(!g){continue}var m=g/h*n;o.fillStyle="black";o.fillRect(j,c-m,delta_x_px,m);var b=4;if(this.show_counts&&(o.measureText(g).width+b)<delta_x_px){o.fillStyle="#666";o.textAlign="center";o.fillText(g,j+(delta_x_px/2),10)}}o.restore()};var LinePainter=function(d,g,a,b,f,c,e){this.data=d;this.view_start=g;this.view_end=a;this.min_value=b;this.max_value=f;this.color=c;this.mode=e};LinePainter.prototype.draw=function(o,n,l){var g=false,h=this.min_value,e=this.max_value,k=e-h,a=l,b=this.view_start,m=this.view_end-this.view_start,c=n/m,j=this.mode,s=this.data;o.save();var t=Math.round(l+h/k*l);if(j!=="Intensity"){o.beginPath();o.moveTo(0,t);o.lineTo(n,t);o.fillStyle="#aaa";o.stroke()}o.beginPath();o.fillStyle=this.color;var r,f,d;if(s.length>1){d=Math.ceil((s[1][0]-s[0][0])*c)}else{d=10}for(var p=0,q=s.length;p<q;p++){r=Math.round((s[p][0]-b)*c);f=s[p][1];if(f===null){if(g&&j==="Filled"){o.lineTo(r,a)}g=false;continue}if(f<h){f=h}else{if(f>e){f=e}}if(j==="Histogram"){f=Math.round(f/k*a);o.fillRect(r,t,d,-f)}else{if(j==="Intensity"){f=255-Math.floor((f-h)/k*255);o.fillStyle="rgb("+f+","+f+","+f+")";o.fillRect(r,0,d,a)}else{f=Math.round(a-(f-h)/k*a);if(g){o.lineTo(r,f)}else{g=true;if(j==="Filled"){o.moveTo(r,a);o.lineTo(r,f)}else{o.moveTo(r,f)}}}}}if(j==="Filled"){if(g){o.lineTo(r,t);o.lineTo(0,t)}o.fill()}else{o.stroke()}o.restore()};var FeaturePainter=function(c,e,a,b,d){this.data=c;this.view_start=e;this.view_end=a;this.prefs=b;this.mode=d};extend(FeaturePainter.prototype,{get_required_height:function(b){var a=y_scale=this.get_row_height(),c=this.mode;if(c==="no_detail"||c==="Squish"||c==="Pack"){a=b*y_scale}return a+Math.round(y_scale/2)},draw:function(n,d,m,j){var g=this.data,k=this.view_start,o=this.view_end;n.save();n.fillStyle=this.prefs.block_color;n.textAlign="right";var r=this.view_end-this.view_start,q=d/r,c=this.get_row_height();for(var f=0,h=g.length;f<h;f++){var p=g[f],e=p[0],a=p[1],b=p[2],l=(j&&j[e]!==undefined?j[e]:null);if(is_overlap([a,b],[k,o])&&(this.mode=="Dense"||l!==null)){this.draw_element(n,this.mode,p,l,k,o,q,c,d)}}n.restore()}});var LinkedFeaturePainter=function(c,e,a,b,d){FeaturePainter.call(this,c,e,a,b,d)};extend(LinkedFeaturePainter.prototype,FeaturePainter.prototype,{get_row_height:function(){var b=this.mode,a;if(b==="summary_tree"){}if(b==="Dense"){a=DENSE_TRACK_HEIGHT}else{if(b==="no_detail"){a=NO_DETAIL_TRACK_HEIGHT}else{if(b==="Squish"){a=SQUISH_TRACK_HEIGHT}else{a=PACK_TRACK_HEIGHT}}}return a},draw_element:function(p,g,x,j,r,H,L,M,a){var u=x[0],J=x[1],B=x[2]-1,s=x[3],C=Math.floor(Math.max(0,(J-r)*L)),q=Math.ceil(Math.min(a,Math.max(0,(B-r)*L))),A=(g==="Dense"?0:(0+j))*M,o,F,t=null,N=null,d=this.prefs.block_color,E=this.prefs.label_color;if(g==="Dense"){p.fillStyle=d;p.fillRect(C,A,q-C,DENSE_FEATURE_HEIGHT)}else{if(g==="no_detail"){p.fillStyle=d;p.fillRect(C,A+5,q-C,DENSE_FEATURE_HEIGHT)}else{var n=x[4],z=x[5],D=x[6],e=x[7];if(z&&D){t=Math.floor(Math.max(0,(z-r)*L));N=Math.ceil(Math.min(a,Math.max(0,(D-r)*L)))}var K,v;if(g==="Squish"){K=1;v=SQUISH_FEATURE_HEIGHT}else{K=5;v=PACK_FEATURE_HEIGHT}if(!e){if(x.strand){if(x.strand==="+"){p.fillStyle=RIGHT_STRAND_INV}else{if(x.strand==="-"){p.fillStyle=LEFT_STRAND_INV}}}else{p.fillStyle=d}p.fillRect(C,A,q-C,v)}else{var m,w;if(g==="Squish"){p.fillStyle=CONNECTOR_COLOR;m=A+Math.floor(SQUISH_FEATURE_HEIGHT/2)+1;w=1}else{if(n){var m=A;var w=v;if(n==="+"){p.fillStyle=RIGHT_STRAND}else{if(n==="-"){p.fillStyle=LEFT_STRAND}}}else{p.fillStyle=CONNECTOR_COLOR;m+=(SQUISH_FEATURE_HEIGHT/2)+1;w=1}}p.fillRect(C,m,q-C,w);for(var I=0,c=e.length;I<c;I++){var h=e[I],b=Math.floor(Math.max(0,(h[0]-r)*L)),y=Math.ceil(Math.min(a,Math.max((h[1]-1-r)*L)));if(b>y){continue}p.fillStyle=d;p.fillRect(b,A+(v-K)/2+1,y-b,K);if(t!==undefined&&!(b>N||y<t)){var G=Math.max(b,t),l=Math.min(y,N);p.fillRect(G,A+1,l-G,v)}}}if(g==="Pack"&&J>r){p.fillStyle=E;var f=1;if(f===0&&C-p.measureText(s).width<0){p.textAlign="left";p.fillText(s,q+LABEL_SPACING,A+8)}else{p.textAlign="right";p.fillText(s,C-LABEL_SPACING,A+8)}p.fillStyle=d}}}}});var VariantPainter=function(c,e,a,b,d){FeaturePainter.call(this,c,e,a,b,d)};extend(VariantPainter.prototype,FeaturePainter.prototype,{draw_element:function(u,p,j,e,x,c,m,v,s){var j=data[i],l=j[0],t=j[1],d=j[2]-1,o=j[3],g=Math.floor(Math.max(0,(t-x)*m)),k=Math.ceil(Math.min(s,Math.max(0,(d-x)*m))),f=(p==="Dense"?0:(0+e))*v,a,y,b=null,n=null;if(no_label){u.fillStyle=block_color;u.fillRect(g+left_offset,f+5,k-g,1)}else{var w=j[4],r=j[5],h=j[6];a=9;y=1;u.fillRect(g+left_offset,f,k-g,a);if(p!=="Dense"&&o!==undefined&&t>x){u.fillStyle=label_color;if(tile_index===0&&g-u.measureText(o).width<0){u.textAlign="left";u.fillText(o,k+2+left_offset,f+8)}else{u.textAlign="right";u.fillText(o,g-2+left_offset,f+8)}u.fillStyle=block_color}var q=w+" / "+r;if(t>x&&u.measureText(q).width<(k-g)){u.fillStyle="white";u.textAlign="center";u.fillText(q,left_offset+g+(k-g)/2,f+8);u.fillStyle=block_color}}}});var ReadPainter=function(d,f,a,c,e,b){FeaturePainter.call(this,d,f,a,c,e);this.ref_seq=b};extend(ReadPainter.prototype,FeaturePainter.prototype,{get_row_height:function(){var a,b=this.mode;if(b==="summary_tree"){}if(b==="Dense"){a=DENSE_TRACK_HEIGHT}else{if(b==="Squish"){a=SQUISH_TRACK_HEIGHT}else{a=PACK_TRACK_HEIGHT;if(this.prefs.show_insertions){a*=2}}}return a},draw_read:function(z,u,p,E,d,y,m,j,h){z.textAlign="center";var x=this,b=[E,d],s=0,A=0,w=0;ref_seq=this.ref_seq;var J=[];if((u==="Pack"||this.mode==="Auto")&&j!==undefined&&p>CHAR_WIDTH_PX){w=Math.round(p/2)}if(!m){m=[[0,j.length]]}for(var q=0,C=m.length;q<C;q++){var n=m[q],e="MIDNSHP=X"[n[0]],r=n[1];if(e==="H"||e==="S"){s-=r}var k=y+s,I=Math.floor(Math.max(0,(k-E)*p)),l=Math.floor(Math.max(0,(k+r-E)*p));switch(e){case"H":break;case"S":case"M":case"=":var t=compute_overlap([k,k+r],b);if(t!==NO_OVERLAP){var v=j.slice(A,A+r);if(w>0){z.fillStyle=this.prefs.block_color;z.fillRect(I-w,h+1,l-I,9);z.fillStyle=CONNECTOR_COLOR;for(var G=0,a=v.length;G<a;G++){if(this.prefs.show_differences&&ref_seq){var o=ref_seq[k-E+G];if(!o||o.toLowerCase()===v[G].toLowerCase()){continue}}if(k+G>=E&&k+G<=d){var H=Math.floor(Math.max(0,(k+G-E)*p));z.fillText(v[G],H,h+9)}}}else{z.fillStyle=this.prefs.block_color;z.fillRect(I,h+(this.mode!=="Dense"?4:5),l-I,(u!=="Dense"?SQUISH_FEATURE_HEIGHT:DENSE_FEATURE_HEIGHT))}}A+=r;s+=r;break;case"N":z.fillStyle=CONNECTOR_COLOR;z.fillRect(I-w,h+5,l-I,1);s+=r;break;case"D":z.fillStyle="red";z.fillRect(I-w,h+4,l-I,3);s+=r;break;case"P":break;case"I":var t=compute_overlap([k,k+r],b),D=I-w;if(t!==NO_OVERLAP){var v=j.slice(A,A+r);if(this.prefs.show_insertions){var g=I-(l-I)/2;if((u==="Pack"||this.mode==="Auto")&&j!==undefined&&p>CHAR_WIDTH_PX){z.fillStyle="yellow";z.fillRect(g-w,h-9,l-I,9);J[J.length]={type:"triangle",data:[D,h+4,5]};z.fillStyle=CONNECTOR_COLOR;switch(t){case (OVERLAP_START):v=v.slice(E-k);break;case (OVERLAP_END):v=v.slice(0,k-d);break;case (CONTAINED_BY):break;case (CONTAINS):v=v.slice(E-k,k-d);break}for(var G=0,a=v.length;G<a;G++){var H=Math.floor(Math.max(0,(k+G-E)*p));z.fillText(v[G],H-(l-I)/2,h)}}else{z.fillStyle="yellow";z.fillRect(g,h+(this.mode!=="Dense"?2:5),l-I,(u!=="Dense"?SQUISH_FEATURE_HEIGHT:DENSE_FEATURE_HEIGHT))}}else{if((u==="Pack"||this.mode==="Auto")&&j!==undefined&&p>CHAR_WIDTH_PX){J[J.length]={type:"text",data:[v.length,D,h+9]}}else{}}}A+=r;break;case"X":A+=r;break}}z.fillStyle="yellow";var F,f,K;for(var B=0;B<J.length;B++){F=J[B];f=F.type;K=F.data;if(f==="text"){z.font="bold "+DEFAULT_FONT;z.fillText(K[0],K[1],K[2]);z.font=DEFAULT_FONT}else{if(f=="triangle"){z.drawDownwardEquilateralTriangle(K[0],K[1],K[2])}}}},draw_element:function(u,p,g,d,x,b,l,v,s){var k=g[0],t=g[1],c=g[2],m=g[3],f=Math.floor(Math.max(0,(t-x)*l)),h=Math.ceil(Math.min(s,Math.max(0,(c-x)*l))),e=(p==="Dense"?1:(1+d))*v,y=this.prefs.block_color,j=this.prefs.label_color,r=0;if((p==="Pack"||this.mode==="Auto")&&l>CHAR_WIDTH_PX){var r=Math.round(l/2)}u.fillStyle=y;if(g[5] instanceof Array){var q=Math.floor(Math.max(0,(g[4][0]-x)*l)),o=Math.ceil(Math.min(s,Math.max(0,(g[4][1]-x)*l))),n=Math.floor(Math.max(0,(g[5][0]-x)*l)),a=Math.ceil(Math.min(s,Math.max(0,(g[5][1]-x)*l)));if(g[4][1]>=x&&g[4][0]<=b&&g[4][2]){this.draw_read(u,p,l,x,b,g[4][0],g[4][2],g[4][3],e)}if(g[5][1]>=x&&g[5][0]<=b&&g[5][2]){this.draw_read(u,p,l,x,b,g[5][0],g[5][2],g[5][3],e)}if(n>o){u.fillStyle=CONNECTOR_COLOR;u.dashedLine(o-r,e+5,n-r,e+5)}}else{u.fillStyle=y;this.draw_read(u,p,l,x,b,t,g[4],g[5],e)}if(p==="Pack"&&t>x){u.fillStyle=this.prefs.label_color;var w=1;if(w===0&&f-u.measureText(m).width<0){u.textAlign="left";u.fillText(m,h+LABEL_SPACING-r,e+8)}else{u.textAlign="right";u.fillText(m,f-LABEL_SPACING-r,e+8)}u.fillStyle=y}}}); \ No newline at end of file Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.