").appendTo(this.container_div);
this.parent_element.append(this.container_div);
};
extend(Track.prototype, {
/**
* Initialize and draw the track.
*/
init: function() {
var track = this;
track.enabled = false;
track.tile_cache.clear();
track.data_cache.clear();
track.initial_canvas = undefined;
track.content_div.css("height", "auto");
/*
if (!track.content_div.text()) {
track.content_div.text(DATA_LOADING);
}
*/
track.container_div.removeClass("nodata error pending");
//
// Tracks with no dataset id are handled differently.
//
if (!track.dataset_id) {
return;
}
// Get dataset state; if state is fine, enable and draw track. Otherwise, show message
// about track status.
$.getJSON(converted_datasets_state_url, { hda_ldda: track.hda_ldda, dataset_id: track.dataset_id, chrom: track.view.chrom},
function (result) {
if (!result || result === "error" || result.kind === "error") {
track.container_div.addClass("error");
track.content_div.text(DATA_ERROR);
if (result.message) {
var track_id = track.view.tracks.indexOf(track);
var error_link = $("
").text("View error").bind("click", function() {
show_modal( "Trackster Error", "
", { "Close" : hide_modal } );
});
track.content_div.append(error_link);
}
} else if (result === "no converter") {
track.container_div.addClass("error");
track.content_div.text(DATA_NOCONVERTER);
} else if (result === "no data" || (result.data !== undefined && (result.data === null || result.data.length === 0))) {
track.container_div.addClass("nodata");
track.content_div.text(DATA_NONE);
} else if (result === "pending") {
track.container_div.addClass("pending");
track.content_div.text(DATA_PENDING);
setTimeout(function() { track.init(); }, track.data_query_wait);
} else if (result['status'] === "data") {
if (result['valid_chroms']) {
track.valid_chroms = result['valid_chroms'];
track.make_name_popup_menu();
}
track.content_div.text(DATA_OK);
if (track.view.chrom) {
track.content_div.text("");
track.content_div.css( "height", track.height_px + "px" );
track.enabled = true;
// predraw_init may be asynchronous, wait for it and then draw
$.when(track.predraw_init()).done(function() {
track.container_div.removeClass("nodata error pending");
track.draw()
});
}
}
});
},
/**
* Additional initialization required before drawing track for the first time.
*/
predraw_init: function() {},
// Provide support for updating and reverting track name. Currently, it's only possible to revert once.
update_name: function(new_name) {
this.old_name = this.name;
this.name = new_name;
this.name_div.text(this.name);
},
revert_name: function() {
this.name = this.old_name;
this.name_div.text(this.name);
}
});
var TiledTrack = function(filters_list, tool_dict, parent_track) {
var track = this,
view = track.view;
// Attribute init.
this.filters_manager = (filters_list !== undefined ? new FiltersManager(this, filters_list) : undefined);
// filters_available is determined by data, filters_visible is set by user.
this.filters_available = false;
this.filters_visible = false;
this.tool = (tool_dict !== undefined && obj_length(tool_dict) > 0 ? new Tool(this, tool_dict) : undefined);
//
// TODO: Right now there is only the notion of a parent track and multiple child tracks. However,
// a more general notion of a 'track group' is probably needed and can be easily created using
// the code already in place for parent/child tracks. The view would then manage track groups, and
// each track group could be managed on its own.
//
this.parent_track = parent_track;
this.child_tracks = [];
if (track.hidden) { return; }
//
// If track has parent:
// -replace drag handle with child-track icon button; (TODO: eventually, we'll want to be able
// to make a set of child tracks dragable.)
// -remove tool b/c child tracks cannot have tools.
//
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;
}
//
// Child tracks container setup.
//
track.child_tracks_container = $("
").addClass("child-tracks-container").hide();
track.container_div.append(track.child_tracks_container);
//
// Create filters div.
//
if (this.filters_manager) {
this.filters_div = this.filters_manager.parent_div
this.header_div.after(this.filters_div);
}
//
// Create dynamic tool div.
//
if (this.tool) {
this.dynamic_tool_div = this.tool.parent_div;
this.header_div.after(this.dynamic_tool_div);
}
//
// Create modes control.
//
if (track.display_modes !== undefined) {
if (track.mode_div === undefined) {
track.mode_div = $("").appendTo(track.header_div);
var init_mode = (track.track_config && track.track_config.values['mode'] ?
track.track_config.values['mode'] : track.display_modes[0]);
track.mode = init_mode;
track.mode_div.text(init_mode);
var mode_mapping = {};
for (var i = 0, len = track.display_modes.length; i < len; i++) {
var mode = track.display_modes[i];
mode_mapping[mode] = function(mode) {
return function() { track.change_mode(mode); };
}(mode);
}
make_popupmenu(track.mode_div, mode_mapping);
} else {
track.mode_div.hide();
}
}
this.make_name_popup_menu();
/*
if (track.overview_check_div === undefined) {
track.overview_check_div = $("
").css("margin-top", "-3px").appendTo(track.header_div);
track.overview_check = $("
").appendTo(track.overview_check_div);
track.overview_check.bind("click", function() {
var curr = this;
view.overview_viewport.find("canvas").remove();
track.set_overview();
$(".overview_check").each(function() {
if (this !== curr) {
$(this).attr("checked", false);
}
});
});
track.overview_check_div.append( $("
").text("Overview") );
}
*/
};
extend(TiledTrack.prototype, Track.prototype, {
/**
* Change track's mode.
*/
change_mode: function(name) {
var track = this;
track.mode_div.text(name);
// TODO: is it necessary to store the mode in two places (.mode and track_config)?
track.mode = name;
track.track_config.values['mode'] = name;
track.tile_cache.clear();
track.draw();
},
/**
* Make popup menu for track name.
*/
make_name_popup_menu: function() {
var track = this;
var track_dropdown = {};
//
// Edit config option.
//
track_dropdown["Edit configuration"] = function() {
var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
ok_fn = function() {
track.track_config.update_from_form( $(".dialog-box") );
hide_modal();
$(window).unbind("keypress.check_enter_esc");
},
check_enter_esc = function(e) {
if ((e.keyCode || e.which) === 27) { // Escape key
cancel_fn();
} else if ((e.keyCode || e.which) === 13) { // Enter key
ok_fn();
}
};
$(window).bind("keypress.check_enter_esc", check_enter_esc);
show_modal("Configure Track", track.track_config.build_form(), {
"Cancel": cancel_fn,
"OK": ok_fn
});
};
/*
track_dropdown["Set as overview"] = function() {
view.overview_viewport.find("canvas").remove();
track.is_overview = true;
track.set_overview();
for (var track_id in view.tracks) {
if (view.tracks[track_id] !== track) {
view.tracks[track_id].is_overview = false;
}
}
};*/
//
// Show/hide filters option.
//
if (track.filters_available > 0) {
// Show/hide filters menu item.
var text = (track.filters_div.is(":visible") ? "Hide filters" : "Show filters");
track_dropdown[text] = function() {
// Toggle filtering div, reset filters, and remake menu.
track.filters_visible = (track.filters_div.is(":visible"));
if (track.filters_visible) {
track.filters_manager.reset_filters();
}
track.filters_div.toggle();
track.make_name_popup_menu();
};
}
//
// Show/hide tool option.
//
if (track.tool) {
// Show/hide dynamic tool menu item.
var text = (track.dynamic_tool_div.is(":visible") ? "Hide tool" : "Show tool");
track_dropdown[text] = function() {
// Set track name, toggle tool div, and remake menu.
if (!track.dynamic_tool_div.is(":visible")) {
track.update_name(track.name + track.tool_region_and_parameters_str());
}
else {
menu_option_text = "Show dynamic tool";
track.revert_name();
}
track.dynamic_tool_div.toggle();
track.make_name_popup_menu();
};
}
//
// List chrom/contigs with data option.
//
if (track.valid_chroms) {
track_dropdown["List chrom/contigs with data"] = function() {
show_modal("Chrom/contigs with data", "
", { "Close": function() { hide_modal(); } });
};
}
//
// Remove option.
//
// Need to either remove track from view or from parent.
var parent_obj = view;
var no_tracks_fn = function() { $("#no-tracks").show(); };
if (this.parent_track) {
// Track is child track.
parent_obj = this.parent_track;
no_tracks_fn = function() {};
}
track_dropdown.Remove = function() {
parent_obj.remove_track(track);
if (parent_obj.num_tracks === 0) {
no_tracks_fn();
}
};
make_popupmenu(track.name_div, track_dropdown);
},
/**
* Draw track. It is possible to force a redraw rather than use cached tiles and/or clear old
* tiles after drawing new tiles.
*/
draw: function(force, clear_after) {
// Cannot draw without dataset_id; dataset_id may not be set if track dynamically created
// and is waiting for dataset.
if (!this.dataset_id) { return; }
var low = this.view.low,
high = this.view.high,
range = high - low,
width = this.view.container.width(),
// w_scale units are pixels per base.
w_scale = width / range,
resolution = this.view.resolution,
parent_element = $("
"),
gen_key = function(width, w_scale, tile_index) {
return width + '_' + w_scale + '_' + tile_index;
};
if (!clear_after) { this.content_div.children().remove(); }
this.content_div.append( parent_element );
this.max_height = 0;
// Index of first tile that overlaps visible region
var tile_index = Math.floor( low / resolution / DENSITY );
// A list of tiles drawn/retrieved.
var drawn_tiles = [];
var tile_count = 0
while ( ( tile_index * DENSITY * resolution ) < high ) {
// Check in cache
var key = gen_key(width, w_scale, tile_index);
var cached = this.tile_cache.get(key);
var tile_low = tile_index * DENSITY * this.view.resolution;
var tile_high = tile_low + DENSITY * this.view.resolution;
if ( !force && cached ) {
drawn_tiles[drawn_tiles.length] = cached;
this.show_tile(cached, parent_element, tile_low, w_scale);
} else {
this.delayed_draw(force, key, tile_index, resolution, parent_element, w_scale, drawn_tiles);
}
tile_index += 1;
tile_count++;
}
//
// Post-draw actions:
//
var track = this;
var intervalId = setInterval(function() {
if (drawn_tiles.length === tile_count) {
// All tiles have been drawn.
clearInterval(intervalId);
// Clear tiles?
if (clear_after) {
// Clear out track content in order to show the most recent content.
// Most recent content is the div with children (tiles) most recently appended to track.
// However, do not delete recently-appended empty content as calls to draw() may still be active
// and using these divs.
var track_content = track.content_div.children();
var remove = false;
for (var i = track_content.length-1, len = 0; i >= len; i--) {
var child = $(track_content[i]);
if (remove) {
child.remove();
}
else if (child.children().length !== 0) {
// Found most recent content with tiles: set remove to start removing old elements.
remove = true;
}
}
}
//
// If mode is Histogram and tiles do not share max, redraw tiles as necessary using new max.
//
// HACK: use track type b/c LineTrack histograms are different; what's needed is different
// post-draw actions for different line tracks.
if (track.track_type == "FeatureTrack" && track.mode == "Histogram") {
// Get global max.
var global_max = -1;
for (var i = 0; i < drawn_tiles.length; i++) {
var cur_max = drawn_tiles[i].max_val;
if (cur_max > global_max) {
global_max = cur_max;
}
}
for (var i = 0; i < drawn_tiles.length; i++) {
if (drawn_tiles[i].max_val !== global_max) {
var tile = drawn_tiles[i];
tile.canvas.remove();
track.delayed_draw(true, gen_key(width, w_scale, tile.index), tile.index,
tile.resolution, parent_element, w_scale, [], { max: global_max });
}
}
}
//
// Update filter attributes, UI.
//
// Update filtering UI.
if (track.filters_manager) {
var filters = track.filters_manager.filters;
for (var f = 0; f < filters.length; f++) {
filters[f].update_ui_elt();
}
// Determine if filters are available; this is based on the example feature.
var filters_available = false;
if (track.example_feature) {
for (var f = 0; f < filters.length; f++) {
if (filters[f].applies_to(track.example_feature)) {
filters_available = true;
break;
}
}
}
// If filter availability changed, hide filter div if necessary and update menu.
if (track.filters_available !== filters_available) {
track.filters_available = filters_available;
if (!track.filters_available) {
track.filters_div.hide();
}
track.make_name_popup_menu();
}
}
// Store initial canvas in case we need to use it for overview
/* This is completely broken, just saves the first tile it sees
regardless of if it should be the overview
if (!track.initial_canvas && !window.G_vmlCanvasManager) {
track.initial_canvas = $(tile_element).clone();
var src_ctx = tile_element.get(0).getContext("2d");
var tgt_ctx = track.initial_canvas.get(0).getContext("2d");
var data = src_ctx.getImageData(0, 0, src_ctx.canvas.width, src_ctx.canvas.height);
tgt_ctx.putImageData(data, 0, 0);
track.set_overview();
}
*/
}
}, 50);
//
// Draw child tracks.
//
for (var i = 0; i < this.child_tracks.length; i++) {
this.child_tracks[i].draw(force, clear_after);
}
},
delayed_draw: function(force, key, tile_index, resolution, parent_element, w_scale, drawn_tiles, more_tile_data) {
var track = this,
tile_low = tile_index * DENSITY * resolution,
tile_high = tile_low + DENSITY * resolution;
// Helper method.
var draw_and_show_tile = function(id, result, resolution, tile_index, parent_element, w_scale, seq_data) {
// DEBUG: this is still called too many times when moving slowly,
// console.log( "draw_and_show_tile", resolution, tile_index, w_scale );
var tile = track.draw_tile(result, resolution, tile_index, w_scale, seq_data);
track.tile_cache.set(key, tile);
// Don't show if no tile
if (tile === undefined) {
return;
}
track.show_tile(tile, parent_element, tile_low, w_scale);
drawn_tiles[drawn_tiles.length] = tile;
};
// Put a 50ms delay on drawing so that if the user scrolls fast, we don't load extra data
var id = setTimeout(function() {
if (tile_low <= track.view.high && tile_high >= track.view.low) {
// Show/draw tile: check cache for tile; if tile not in cache, draw it.
var tile = (force ? undefined : track.tile_cache.get(key));
if (tile) {
track.show_tile(tile, parent_element, tile_low, w_scale);
drawn_tiles[drawn_tiles.length] = tile;
}
else {
//
// Really draw tile: get data, seq data if available, and draw tile.
//
$.when(track.data_cache.get_data(view.chrom, tile_low, tile_high, track.mode,
resolution, track.data_url_extra_params)).then(function(tile_data) {
extend(tile_data, more_tile_data);
// If sequence data needed, get that and draw. Otherwise draw.
if (view.reference_track && w_scale > view.canvas_manager.char_width_px) {
$.when(view.reference_track.data_cache.get_data(view.chrom, tile_low, tile_high,
track.mode, resolution,
view.reference_track.data_url_extra_params)).then(function(seq_data) {
draw_and_show_tile(id, tile_data, resolution, tile_index, parent_element, w_scale, seq_data);
});
}
else {
draw_and_show_tile(id, tile_data, resolution, tile_index, parent_element, w_scale);
}
});
}
}
}, 50);
},
/**
* Show track tile and perform associated actions.
*/
show_tile: function(tile, parent_element, tile_low, w_scale) {
// Readability.
var track = this;
//
// Show tile element.
//
// Position tile element, recalculate left position at display time
var range = this.view.high - this.view.low,
left = (tile_low - this.view.low) * w_scale;
if (this.left_offset) {
left -= this.left_offset;
}
var tile_element = tile.canvas;
tile_element.css({ position: 'absolute', top: 0, left: left, height: '' });
parent_element.append(tile_element);
// Set track height.
track.max_height = Math.max(track.max_height, tile_element.height());
track.content_div.css("height", track.max_height + "px");
parent_element.children().css("height", track.max_height + "px");
},
// Set track as the overview track in the visualization.
set_overview: function() {
var view = this.view;
if (this.initial_canvas && this.is_overview) {
view.overview_close.show();
view.overview_viewport.append(this.initial_canvas);
view.overview_highlight.show().height(this.initial_canvas.height());
view.overview_viewport.height(this.initial_canvas.height() + view.overview_box.height());
}
$(window).trigger("resize");
},
/**
* Utility function that creates a label string describing the region and parameters of a track's tool.
*/
tool_region_and_parameters_str: function(chrom, low, high) {
// Region is chrom:low-high or 'all.'
var
track = this,
region = (chrom !== undefined && low !== undefined && high !== undefined ?
chrom + ":" + low + "-" + high : "all");
return " - region=[" + region + "], parameters=[" + track.tool.get_param_values().join(", ") + "]";
},
/**
* Add a child track to this track.
*/
add_track: function(child_track) {
child_track.track_id = this.track_id + "_" + this.child_tracks.length;
child_track.container_div.attr('id', 'track_' + child_track.track_id);
this.child_tracks_container.append(child_track.container_div);
sortable( child_track.container_div, '.child-track-icon' );
if (!$(this.child_tracks_container).is(":visible")) {
this.child_tracks_container.show();
}
this.child_tracks.push(child_track);
this.view.has_changes = true;
},
/**
* Remove a child track from this track.
*/
remove_track: function(child_track) {
child_track.container_div.fadeOut('slow', function() { $(this).remove(); });
}
});
var LabelTrack = function (view, parent_element) {
this.track_type = "LabelTrack";
this.hidden = true;
Track.call( this, null, view, parent_element );
this.container_div.addClass( "label-track" );
};
extend( LabelTrack.prototype, Track.prototype, {
draw: function() {
var view = this.view,
range = view.high - view.low,
tickDistance = Math.floor( Math.pow( 10, Math.floor( Math.log( range ) / Math.log( 10 ) ) ) ),
position = Math.floor( view.low / tickDistance ) * tickDistance,
width = this.view.container.width(),
new_div = $("
");
while ( position < view.high ) {
var screenPosition = ( position - view.low ) / range * width;
new_div.append( $("
").css( {
position: "absolute",
// Reduce by one to account for border
left: screenPosition - 1
}));
position += tickDistance;
}
this.content_div.children( ":first" ).remove();
this.content_div.append( new_div );
}
});
var ReferenceTrack = function (view) {
this.track_type = "ReferenceTrack";
this.hidden = true;
Track.call(this, null, view, view.top_labeltrack);
TiledTrack.call(this);
view.reference_track = this;
this.left_offset = 200;
this.height_px = 12;
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: view.dbkey};
this.data_cache = new ReferenceTrackDataManager(CACHED_DATA, this, false);
this.tile_cache = new Cache(CACHED_TILES_LINE);
};
extend(ReferenceTrack.prototype, TiledTrack.prototype, {
/**
* Draw ReferenceTrack tile.
*/
draw_tile: function(seq, resolution, tile_index, w_scale) {
var track = this,
tile_length = DENSITY * resolution;
if (w_scale > this.view.canvas_manager.char_width_px) {
if (seq === null) {
track.content_div.css("height", "0px");
return;
}
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 = ctx.canvas.manager.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 new Tile(tile_index, resolution, canvas);
}
this.content_div.css("height", "0px");
}
});
var LineTrack = function (name, view, hda_ldda, dataset_id, prefs) {
var track = this;
this.track_type = "LineTrack";
this.display_modes = ["Histogram", "Line", "Filled", "Intensity"];
this.mode = "Histogram";
Track.call( this, name, view, view.viewport_container );
TiledTrack.call( this );
this.min_height_px = 16;
this.max_height_px = 400;
this.height_px = 80;
this.hda_ldda = hda_ldda;
this.dataset_id = dataset_id;
this.original_dataset_id = dataset_id;
this.data_cache = new DataManager(CACHED_DATA, this);
this.tile_cache = new Cache(CACHED_TILES_LINE);
// Define track configuration
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: prefs,
onchange: function() {
track.vertical_range = track.prefs.max_value - track.prefs.min_value;
// Update the y-axis
$('#linetrack_' + track.track_id + '_minval').text(track.prefs.min_value);
$('#linetrack_' + track.track_id + '_maxval').text(track.prefs.max_value);
track.tile_cache.clear();
track.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 () {
// 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 = $( "
" )
// Control shows on hover over track, stays while dragging
$(track.container_div).hover( function() {
in_handle = true;
drag_control.show();
}, function() {
in_handle = false;
if ( ! in_drag ) { drag_control.hide(); }
});
// Update height and force redraw of current view while dragging,
// clear cache to force redraw of other tiles.
drag_control.hide().bind( "dragstart", function( e, d ) {
in_drag = true;
d.original_height = $(track.content_div).height();
}).bind( "drag", function( e, d ) {
var new_height = Math.min( Math.max( d.original_height + d.deltaY, track.min_height_px ), track.max_height_px );
$(track.content_div).css( 'height', new_height );
track.height_px = new_height;
track.draw( true );
}).bind( "dragend", function( e, d ) {
track.tile_cache.clear();
in_drag = false;
if ( ! in_handle ) { drag_control.hide(); }
track.track_config.values.height = track.height_px;
}).appendTo( track.container_div );
},
predraw_init: function() {
var track = this,
track_id = track.view.tracks.indexOf(track);
track.vertical_range = undefined;
return $.getJSON( track.data_url, { stats: true, chrom: track.view.chrom, low: null, high: null,
hda_ldda: track.hda_ldda, dataset_id: track.dataset_id }, function(result) {
track.container_div.addClass( "line-track" );
var data = result.data;
if ( isNaN(parseFloat(track.prefs.min_value)) || isNaN(parseFloat(track.prefs.max_value)) ) {
track.prefs.min_value = data.min;
track.prefs.max_value = data.max;
// Update the config
$('#track_' + track_id + '_minval').val(track.prefs.min_value);
$('#track_' + track_id + '_maxval').val(track.prefs.max_value);
}
track.vertical_range = track.prefs.max_value - track.prefs.min_value;
track.total_frequency = data.total_frequency;
// Draw y-axis labels if necessary
track.container_div.find(".yaxislabel").remove();
var min_label = $("
").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_minval').text(round_1000(track.prefs.min_value));
var max_label = $("
").addClass('yaxislabel').attr("id", 'linetrack_' + track_id + '_maxval').text(round_1000(track.prefs.max_value));
max_label.css({ position: "absolute", top: "24px", left: "10px" });
max_label.prependTo(track.container_div);
min_label.css({ position: "absolute", bottom: "2px", left: "10px" });
min_label.prependTo(track.container_div);
});
},
/**
* Draw LineTrack tile.
*/
draw_tile: function(result, resolution, tile_index, w_scale) {
if (this.vertical_range === undefined) {
return;
}
var tile_low = tile_index * DENSITY * resolution,
tile_length = DENSITY * 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;
// Paint line onto full canvas
var ctx = canvas.getContext("2d");
var painter = new painters.LinePainter( result.data, tile_low, tile_low + tile_length,
this.prefs, this.mode );
painter.draw( ctx, width, height );
return new Tile(tile_length, resolution, canvas);
}
});
var FeatureTrack = function(name, view, hda_ldda, dataset_id, prefs, filters, tool, parent_track) {
//
// Preinitialization: do things that need to be done before calling Track and TiledTrack
// initialization code.
//
var track = this;
this.track_type = "FeatureTrack";
this.display_modes = ["Auto", "Histogram", "Dense", "Squish", "Pack"];
// Define and restore track configuration.
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: prefs,
onchange: function() {
track.tile_cache.clear();
track.draw();
}
});
this.prefs = this.track_config.values;
//
// Initialization.
//
Track.call(this, name, view, view.viewport_container);
TiledTrack.call(this, filters, tool, parent_track);
this.height_px = 0;
this.container_div.addClass( "feature-track" );
this.hda_ldda = hda_ldda;
this.dataset_id = dataset_id;
this.original_dataset_id = dataset_id;
this.show_labels_scale = 0.001;
this.showing_details = false;
this.summary_draw_height = 30;
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 = painters.LinkedFeaturePainter;
};
extend(FeatureTrack.prototype, TiledTrack.prototype, {
update_auto_mode: function( mode ) {
if ( this.mode == "Auto" ) {
if ( mode == "no_detail" ) {
mode = "feature spans";
} else if ( mode == "summary_tree" ) {
mode = "coverage histogram";
}
this.mode_div.text( "Auto (" + mode + ")" );
}
},
/**
* Place features in slots for drawing (i.e. pack features).
* this.inc_slots[level] is created in this method. this.inc_slots[level]
* is a dictionary of slotted features; key is feature uid, value is a dictionary
* with keys 'slot' and 'text'.
* 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 dummy_context = this.view.canvas_manager.dummy_context,
inc_slots = this.inc_slots[level];
if (!inc_slots || (inc_slots.mode !== mode)) {
inc_slots = new (slotting.FeatureSlotter)( level, mode === "Pack", MAX_FEATURE_DEPTH, function ( x ) { return dummy_context.measureText( x ) } );
inc_slots.mode = mode;
this.inc_slots[level] = inc_slots;
}
return inc_slots.slot_features( features );
},
/**
* Given feature data, returns summary tree data. Feature data must be sorted by start
* position. Return value is a dict with keys 'data', 'delta' (bin size) and 'max.' Data
* is a two-item list; first item is bin start, second is bin's count.
*/
get_summary_tree_data: function(data, low, high, num_bins) {
if (num_bins > high - low) {
num_bins = high - low;
}
var bin_size = Math.floor((high - low)/num_bins),
bins = [],
max_count = 0;
/*
// For debugging:
for (var i = 0; i < data.length; i++)
console.log("\t", data[i][1], data[i][2], data[i][3]);
*/
//
// Loop through bins, counting data for each interval.
//
var data_index_start = 0,
data_index = 0,
data_interval,
bin_index = 0,
bin_interval = [],
cur_bin,
overlap;
// Set bin interval.
var set_bin_interval = function(interval, low, bin_index, bin_size) {
interval[0] = low + bin_index * bin_size;
interval[1] = low + (bin_index + 1) * bin_size;
};
// Loop through bins, data to compute bin counts. Only compute bin counts as long
// as there is data.
while (bin_index < num_bins && data_index_start !== data.length) {
// Find next bin that has data.
var bin_has_data = false;
for (; bin_index < num_bins && !bin_has_data; bin_index++) {
set_bin_interval(bin_interval, low, bin_index, bin_size);
// Loop through data and break if data found that goes in bin.
for (data_index = data_index_start; data_index < data.length; data_index++) {
data_interval = data[data_index].slice(1, 3);
if (is_overlap(data_interval, bin_interval)) {
bin_has_data = true;
break;
}
}
// Break from bin loop if this bin has data.
if (bin_has_data) {
break;
}
}
// Set start index to current data, which is the first to overlap with this bin
// and perhaps with later bins.
data_start_index = data_index;
// Count intervals that overlap with bin.
bins[bins.length] = cur_bin = [bin_interval[0], 0];
for (; data_index < data.length; data_index++) {
data_interval = data[data_index].slice(1, 3);
if (is_overlap(data_interval, bin_interval)) {
cur_bin[1]++;
}
else { break; }
}
// Update max count.
if (cur_bin[1] > max_count) {
max_count = cur_bin[1];
}
// Go to next bin.
bin_index++;
}
return {max: max_count, delta: bin_size, data: bins};
},
/**
* Draw FeatureTrack tile.
*/
draw_tile: function(result, resolution, tile_index, 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 {
// 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) ) {
if ( this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH ) {
mode = "Squish";
} else {
mode = "Pack";
}
}
this.update_auto_mode( mode );
}
// Drawing the summary tree (feature coverage histogram)
if (mode === "summary_tree" || mode === "Histogram") {
required_height = this.summary_draw_height;
// 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 = $("
").addClass('yaxislabel');
max_label.text( result.max );
max_label.css({ position: "absolute", top: "24px", left: "10px", color: this.prefs.label_color });
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 + SUMMARY_TREE_TOP_PADDING;
// Get summary tree data if necessary and set max if there is one.
if (result.dataset_type != "summary_tree") {
var st_data = this.get_summary_tree_data(result.data, tile_low, tile_high, 200);
if (result.max) {
st_data.max = result.max;
}
result = st_data;
}
// Paint summary tree into canvas
var painter = new painters.SummaryTreePainter(result, tile_low, tile_high, this.prefs);
var ctx = canvas.getContext("2d");
// Deal with left_offset by translating
ctx.translate(left_offset, SUMMARY_TREE_TOP_PADDING);
painter.draw(ctx, width, required_height);
return new SummaryTreeTile(tile_index, resolution, canvas, result.max);
}
// Start dealing with row-by-row tracks
// If working with a mode where slotting is necessary, 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 ) {
var filters = this.filters_manager.filters;
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 = filters.length; f < flen; f++) {
filter = filters[f];
filter.update_attrs(feature);
if (!filter.keep(feature)) {
hide_feature = true;
break;
}
}
if (!hide_feature) {
filtered.push( feature );
}
}
}
// 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();
canvas.width = width + left_offset;
canvas.height = required_height;
// 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 = ctx.canvas.manager.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) {
ctx.fillStyle = "red";
ctx.textAlign = "left";
var old_base = ctx.textBaseline;
ctx.textBaseline = "top";
ctx.fillRect(left_offset, 0, canvas.width - left_offset, 1);
ctx.fillText(result.message, left_offset, 2);
ctx.textBaseline = old_base;
// If there's no data, return.
if (!result.data) {
return new Tile(tile_index, resolution, canvas, required_height);
}
}
// 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 new FeatureTrackTile(tile_index, resolution, 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 = painters.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 = painters.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();
}
});
// Exports
exports.View = View;
exports.LineTrack = LineTrack;
exports.FeatureTrack = FeatureTrack;
exports.ReadTrack = ReadTrack;
// End trackster_module encapsulation
};
// ---- To be extracted ------------------------------------------------------
// ---- Feature Packing ----
// Encapsulation
var slotting_module = function(require, exports) {
var extend = require('class').extend;
// HACK: LABEL_SPACING is currently duplicated between here and painters
var LABEL_SPACING = 2,
PACK_SPACING = 5;
/**
* 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.
*/
exports.FeatureSlotter = function ( w_scale, include_label, max_rows, measureText ) {
this.slots = {};
this.start_end_dct = {};
this.w_scale = w_scale;
this.include_label = include_label;
this.max_rows = max_rows;
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
*/
extend( exports.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, max_rows = this.max_rows;
// 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_rows; 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;
}
});
// End slotting_module encapsulation
};
// ---- Painters ----
var painters_module = function(require, exports){
var extend = require('class').extend;
/**
* Draw a dashed line on a canvas using filled rectangles. This function is based on:
* http://vetruvet.blogspot.com/2010/10/drawing-dashed-lines-on-html5-canvas.html
* However, that approach uses lines, which don't seem to render as well, so use
* rectangles instead.
*/
var dashedLine = function(ctx, x1, y1, x2, y2, dashLen) {
if (dashLen === undefined) { dashLen = 4; }
var dX = x2 - x1;
var dY = y2 - y1;
var dashes = Math.floor(Math.sqrt(dX * dX + dY * dY) / dashLen);
var dashX = dX / dashes;
var dashY = dY / dashes;
var q;
for (q = 0; q < dashes; q++, x1 += dashX, y1 += dashY) {
if (q % 2 !== 0) {
continue;
}
ctx.fillRect(x1, y1, dashLen, 1);
}
};
/**
* Draw an isosceles triangle that points down.
*/
var drawDownwardEquilateralTriangle = function(ctx, down_vertex_x, down_vertex_y, side_len) {
// Compute other two points of triangle.
var
x1 = down_vertex_x - side_len/2,
x2 = down_vertex_x + side_len/2,
y = down_vertex_y - Math.sqrt( side_len*3/2 );
// Draw and fill.
ctx.beginPath();
ctx.moveTo(x1, y);
ctx.lineTo(x2, y);
ctx.lineTo(down_vertex_x, down_vertex_y);
ctx.lineTo(x1, y);
ctx.strokeStyle = this.fillStyle;
ctx.fill();
ctx.stroke();
ctx.closePath();
};
/**
* Base class for painters
*
* -- Mode and prefs are both optional
*/
var Painter = function( data, view_start, view_end, prefs, mode ) {
// Data and data properties
this.data = data;
// View
this.view_start = view_start;
this.view_end = view_end;
// Drawing prefs
this.prefs = extend( {}, this.default_prefs, prefs );
this.mode = mode;
}
Painter.prototype.default_prefs = {};
/**
* SummaryTreePainter, a histogram showing number of intervals in a region
*/
var SummaryTreePainter = function( data, view_start, view_end, prefs, mode ) {
Painter.call( this, data, view_start, view_end, prefs, mode );
}
SummaryTreePainter.prototype.default_prefs = { show_counts: false };
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.data, delta = this.data.delta, max = this.data.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;
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
if (y !== 0 && y_px < 1) { y_px = 1; }
ctx.fillStyle = this.prefs.block_color;
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.prefs.show_counts && (ctx.measureText(y).width + text_padding_req_x) < delta_x_px) {
ctx.fillStyle = this.prefs.label_color;
ctx.textAlign = "center";
ctx.fillText(y, x + (delta_x_px/2), 10);
}
}
ctx.restore();
}
var LinePainter = function( data, view_start, view_end, prefs, mode ) {
Painter.call( this, data, view_start, view_end, prefs, mode );
if ( this.prefs.min_value === undefined ) {
var min_value = Infinity;
for (var i = 0, len = this.data.length; i < len; i++) {
min_value = Math.min( min_value, this.data[i][1] );
}
this.prefs.min_value = min_value;
}
if ( this.prefs.max_value === undefined ) {
var max_value = -Infinity;
for (var i = 0, len = this.data.length; i < len; i++) {
max_value = Math.max( max_value, this.data[i][1] );
}
this.prefs.max_value = max_value;
}
};
LinePainter.prototype.default_prefs = { min_value: undefined, max_value: undefined, mode: "Histogram", color: "#000", overflow_color: "#F66" };
LinePainter.prototype.draw = function( ctx, width, height ) {
var
in_path = false,
min_value = this.prefs.min_value,
max_value = this.prefs.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 );
// Horizontal line to denote x-axis
if ( mode !== "Intensity" ) {
ctx.fillStyle = "#aaa";
ctx.fillRect( 0, y_zero, width, 1 );
}
ctx.beginPath();
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++) {
ctx.fillStyle = this.prefs.color;
x_scaled = Math.round((data[i][0] - view_start) * w_scale);
y = data[i][1];
var top_overflow = false, bot_overflow = false;
if (y === null) {
if (in_path && mode === "Filled") {
ctx.lineTo(x_scaled, height_px);
}
in_path = false;
continue;
}
if (y < min_value) {
bot_overflow = true;
y = min_value;
} else if (y > max_value) {
top_overflow = true;
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);
}
}
}
// Draw lines at boundaries if overflowing min or max
ctx.fillStyle = this.prefs.overflow_color;
if (top_overflow || bot_overflow) {
var overflow_x;
if (mode === "Histogram" || mode === "Intensity") {
overflow_x = delta_x_px;
} else { // Line and Filled, which are points
x_scaled -= 2; // Move it over to the left so it's centered on the point
overflow_x = 4;
}
if (top_overflow) {
ctx.fillRect(x_scaled, 0, overflow_x, 3);
}
if (bot_overflow) {
ctx.fillRect(x_scaled, height_px - 3, overflow_x, 3);
}
}
ctx.fillStyle = this.prefs.color;
}
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 ) {
Painter.call( this, data, view_start, view_end, prefs, mode );
}
FeaturePainter.prototype.default_prefs = { block_color: "#FFF", connector_color: "#FFF" };
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") {
required_height = rows_required * y_scale;
}
// Pad bottom by half a row, at least 5 px
return required_height + Math.max( Math.round( y_scale / 2 ), 5 );
},
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 ( ( feature_start < view_end && feature_end > view_start ) && (this.mode == "Dense" || slot !== null)) {
this.draw_element(ctx, this.mode, feature, slot, view_start, view_end, w_scale, y_scale,
width );
}
}
ctx.restore();
}
});
// Constants specific to feature tracks moved here (HACKING, these should
// basically all be configuration options)
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 = 3,
SQUISH_FEATURE_HEIGHT = 3,
PACK_FEATURE_HEIGHT = 9,
LABEL_SPACING = 2,
CONNECTOR_COLOR = "#ccc";
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 === "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],
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" ? 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);
}
*/
if ( mode == "Dense" ) {
slot = 1;
}
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, NO_DETAIL_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" || mode === "Dense" ) {
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 = ctx.canvas.manager.get_pattern( 'right_strand_inv' );
} else if (feature.strand === "-") {
ctx.fillStyle = ctx.canvas.manager.get_pattern( '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" || mode === "Dense") {
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 = ctx.canvas.manager.get_pattern( 'right_strand' );
} else if (feature_strand === "-") {
ctx.fillStyle = ctx.canvas.manager.get_pattern( '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) ),
block_end = Math.ceil( Math.min(width, Math.max((block[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.
// - No thick is sometimes encoded as thick_start == thick_end, so don't draw in that case
if (thick_start !== undefined && feature_te > feature_ts && !(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);
if ( feature_blocks.length == 1 && mode == "Pack") {
// Exactly one block means we have no introns, but do have a distinct "thick" region,
// draw arrows over it if in pack mode
if (feature_strand === "+") {
ctx.fillStyle = ctx.canvas.manager.get_pattern( 'right_strand_inv' );
} else if (feature_strand === "-") {
ctx.fillStyle = ctx.canvas.manager.get_pattern( 'left_strand_inv' );
}
// If region is wide enough in pixels, pad a bit
if ( block_thick_start + 14 < block_thick_end ) {
block_thick_start += 2;
block_thick_end -= 2;
}
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: assumption here that the entire view starts at 0
if (tile_low === 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],
feature_end = feature[2],
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 = (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_low === 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;
};
ReadPainter.prototype.default_prefs = extend( {}, FeaturePainter.prototype.default_prefs, { show_insertions: false } );
extend( ReadPainter.prototype, FeaturePainter.prototype, {
/**
* Returns y_scale based on mode.
*/
get_row_height: function() {
var y_scale, mode = this.mode;
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.prefs.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,
char_width_px = ctx.canvas.manager.char_width_px;
// 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) );
// Make sure that read is drawn even if it too small to be rendered officially; in this case,
// read is drawn at 1px.
// TODO: need to ensure that s_start, s_end are calcuated the same for both slotting
// and drawing.
if (s_start === s_end) {
s_end += 1;
}
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.
if (is_overlap([seq_start, seq_start + cig_len], tile_region)) {
// 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 - 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.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;
}
}
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, 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, y_center + 4, s_end - s_start, SQUISH_FEATURE_HEIGHT);
}
}
seq_offset += cig_len;
base_offset += cig_len;
break;
case "N": // Skipped bases.
ctx.fillStyle = CONNECTOR_COLOR;
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 - 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 insert_x_coord = s_start - gap;
if (is_overlap([seq_start, seq_start + cig_len], tile_region)) {
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.prefs.show_insertions) {
//
// Show inserted sequence above, centered on insertion point.
//
// Draw sequence.
// X center is offset + start -
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";
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 - (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.save();
ctx.font = "bold " + ctx.font;
ctx.fillText(data[0], data[1], data[2]);
ctx.restore();
}
else if (type == "triangle") {
drawDownwardEquilateralTriangle(ctx, 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" ? 0 : (0 + 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 > ctx.canvas.manager.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;
dashedLine(ctx, 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);
}
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 + LABEL_SPACING - gap, y_center + 8);
} else {
ctx.textAlign = "right";
ctx.fillText(feature_name, f_start - LABEL_SPACING - gap, y_center + 8);
}
ctx.fillStyle = block_color;
}
}
});
exports.SummaryTreePainter = SummaryTreePainter;
exports.LinePainter = LinePainter;
exports.LinkedFeaturePainter = LinkedFeaturePainter;
exports.ReadPainter = ReadPainter;
exports.VariantPainter = VariantPainter;
// End painters_module encapsulation
};
// Finally, compose and export trackster module exports into globals
// These will be replaced with require statements in CommonJS
(function(target){
// Faking up a CommonJS like loader here, each module gets a require and
// and exports. We avoid resolution problems by just ordering carefully.
var modules = {};
// Provide a require function
var require = function( name ) {
return modules[name];
};
// Run module will run the module_function provided and store in the
// require dict using key
var run_module = function( key, module_function ) {
var exports = {};
module_function( require, exports );
modules[key] = exports;
};
// Run all modules
run_module( 'class', class_module );
run_module( 'slotting', slotting_module );
run_module( 'painters', painters_module );
run_module( 'trackster', trackster_module );
// Export trackster stuff
for ( key in modules.trackster ) {
target[key] = modules.trackster[key];
}
})(window);