mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
trackster: feature tracks auto-resize to fit data, prepare for LRU cache
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
MIT LICENSE
|
||||
Copyright (c) 2007 Monsur Hossain (http://www.monsur.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// ****************************************************************************
|
||||
// CachePriority ENUM
|
||||
// An easier way to refer to the priority of a cache item
|
||||
var CachePriority = {
|
||||
Low: 1,
|
||||
Normal: 2,
|
||||
High: 4
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache constructor
|
||||
// Creates a new cache object
|
||||
// INPUT: maxSize (optional) - indicates how many items the cache can hold.
|
||||
// default is -1, which means no limit on the
|
||||
// number of items.
|
||||
function Cache(maxSize) {
|
||||
this.items = {};
|
||||
this.count = 0;
|
||||
if (maxSize == null)
|
||||
maxSize = -1;
|
||||
this.maxSize = maxSize;
|
||||
this.fillFactor = .75;
|
||||
this.purgeSize = Math.round(this.maxSize * this.fillFactor);
|
||||
|
||||
this.stats = {}
|
||||
this.stats.hits = 0;
|
||||
this.stats.misses = 0;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache.getItem
|
||||
// retrieves an item from the cache, returns null if the item doesn't exist
|
||||
// or it is expired.
|
||||
// INPUT: key - the key to load from the cache
|
||||
Cache.prototype.getItem = function(key) {
|
||||
|
||||
// retrieve the item from the cache
|
||||
var item = this.items[key];
|
||||
|
||||
if (item != null) {
|
||||
if (!this._isExpired(item)) {
|
||||
// if the item is not expired
|
||||
// update its last accessed date
|
||||
item.lastAccessed = new Date().getTime();
|
||||
} else {
|
||||
// if the item is expired, remove it from the cache
|
||||
this._removeItem(key);
|
||||
item = null;
|
||||
}
|
||||
}
|
||||
|
||||
// return the item value (if it exists), or null
|
||||
var returnVal = null;
|
||||
if (item != null) {
|
||||
returnVal = item.value;
|
||||
this.stats.hits++;
|
||||
} else {
|
||||
this.stats.misses++;
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache.setItem
|
||||
// sets an item in the cache
|
||||
// parameters: key - the key to refer to the object
|
||||
// value - the object to cache
|
||||
// options - an optional parameter described below
|
||||
// the last parameter accepts an object which controls various caching options:
|
||||
// expirationAbsolute: the datetime when the item should expire
|
||||
// expirationSliding: an integer representing the seconds since
|
||||
// the last cache access after which the item
|
||||
// should expire
|
||||
// priority: How important it is to leave this item in the cache.
|
||||
// You can use the values CachePriority.Low, .Normal, or
|
||||
// .High, or you can just use an integer. Note that
|
||||
// placing a priority on an item does not guarantee
|
||||
// it will remain in cache. It can still be purged if
|
||||
// an expiration is hit, or if the cache is full.
|
||||
// callback: A function that gets called when the item is purged
|
||||
// from cache. The key and value of the removed item
|
||||
// are passed as parameters to the callback function.
|
||||
Cache.prototype.setItem = function(key, value, options) {
|
||||
|
||||
function CacheItem(k, v, o) {
|
||||
if ((k == null) || (k == ''))
|
||||
throw new Error("key cannot be null or empty");
|
||||
this.key = k;
|
||||
this.value = v;
|
||||
if (o == null)
|
||||
o = {};
|
||||
if (o.expirationAbsolute != null)
|
||||
o.expirationAbsolute = o.expirationAbsolute.getTime();
|
||||
if (o.priority == null)
|
||||
o.priority = CachePriority.Normal;
|
||||
this.options = o;
|
||||
this.lastAccessed = new Date().getTime();
|
||||
}
|
||||
|
||||
// add a new cache item to the cache
|
||||
if (this.items[key] != null)
|
||||
this._removeItem(key);
|
||||
this._addItem(new CacheItem(key, value, options));
|
||||
|
||||
// if the cache is full, purge it
|
||||
if ((this.maxSize > 0) && (this.count > this.maxSize)) {
|
||||
this._purge();
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache.clear
|
||||
// Remove all items from the cache
|
||||
Cache.prototype.clear = function() {
|
||||
|
||||
// loop through each item in the cache and remove it
|
||||
for (var key in this.items) {
|
||||
this._removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache._purge (PRIVATE FUNCTION)
|
||||
// remove old elements from the cache
|
||||
Cache.prototype._purge = function() {
|
||||
|
||||
var tmparray = new Array();
|
||||
|
||||
// loop through the cache, expire items that should be expired
|
||||
// otherwise, add the item to an array
|
||||
for (var key in this.items) {
|
||||
var item = this.items[key];
|
||||
if (this._isExpired(item)) {
|
||||
this._removeItem(key);
|
||||
} else {
|
||||
tmparray.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (tmparray.length > this.purgeSize) {
|
||||
|
||||
// sort this array based on cache priority and the last accessed date
|
||||
tmparray = tmparray.sort(function(a, b) {
|
||||
if (a.options.priority != b.options.priority) {
|
||||
return b.options.priority - a.options.priority;
|
||||
} else {
|
||||
return b.lastAccessed - a.lastAccessed;
|
||||
}
|
||||
});
|
||||
|
||||
// remove items from the end of the array
|
||||
while (tmparray.length > this.purgeSize) {
|
||||
var ritem = tmparray.pop();
|
||||
this._removeItem(ritem.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache._addItem (PRIVATE FUNCTION)
|
||||
// add an item to the cache
|
||||
Cache.prototype._addItem = function(item) {
|
||||
this.items[item.key] = item;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache._removeItem (PRIVATE FUNCTION)
|
||||
// Remove an item from the cache, call the callback function (if necessary)
|
||||
Cache.prototype._removeItem = function(key) {
|
||||
var item = this.items[key];
|
||||
delete this.items[key];
|
||||
this.count--;
|
||||
|
||||
// if there is a callback function, call it at the end of execution
|
||||
if (item.options.callback != null) {
|
||||
var callback = function() {
|
||||
item.options.callback(item.key, item.value);
|
||||
}
|
||||
setTimeout(callback, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// Cache._isExpired (PRIVATE FUNCTION)
|
||||
// Returns true if the item should be expired based on its expiration options
|
||||
Cache.prototype._isExpired = function(item) {
|
||||
var now = new Date().getTime();
|
||||
var expired = false;
|
||||
if ((item.options.expirationAbsolute) && (item.options.expirationAbsolute < now)) {
|
||||
// if the absolute expiration has passed, expire the item
|
||||
expired = true;
|
||||
}
|
||||
if ((expired == false) && (item.options.expirationSliding)) {
|
||||
// if the sliding expiration has passed, expire the item
|
||||
var lastAccess = item.lastAccessed + (item.options.expirationSliding * 1000);
|
||||
if (lastAccess < now) {
|
||||
expired = true;
|
||||
}
|
||||
}
|
||||
return expired;
|
||||
}
|
||||
|
||||
Cache.prototype.toHtmlString = function() {
|
||||
var returnStr = this.count + " item(s) in cache<br /><ul>";
|
||||
for (var key in this.items) {
|
||||
var item = this.items[key];
|
||||
returnStr = returnStr + "<li>" + item.key.toString() + " = " + item.value.toString() + "</li>";
|
||||
}
|
||||
returnStr = returnStr + "</ul>";
|
||||
return returnStr;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
var CachePriority={Low:1,Normal:2,High:4};function Cache(a){this.items={};this.count=0;if(a==null){a=-1}this.maxSize=a;this.fillFactor=0.75;this.purgeSize=Math.round(this.maxSize*this.fillFactor);this.stats={};this.stats.hits=0;this.stats.misses=0}Cache.prototype.getItem=function(a){var c=this.items[a];if(c!=null){if(!this._isExpired(c)){c.lastAccessed=new Date().getTime()}else{this._removeItem(a);c=null}}var b=null;if(c!=null){b=c.value;this.stats.hits++}else{this.stats.misses++}return b};Cache.prototype.setItem=function(c,d,b){function a(f,e,g){if((f==null)||(f=="")){throw new Error("key cannot be null or empty")}this.key=f;this.value=e;if(g==null){g={}}if(g.expirationAbsolute!=null){g.expirationAbsolute=g.expirationAbsolute.getTime()}if(g.priority==null){g.priority=CachePriority.Normal}this.options=g;this.lastAccessed=new Date().getTime()}if(this.items[c]!=null){this._removeItem(c)}this._addItem(new a(c,d,b));if((this.maxSize>0)&&(this.count>this.maxSize)){this._purge()}};Cache.prototype.clear=function(){for(var a in this.items){this._removeItem(a)}};Cache.prototype._purge=function(){var d=new Array();for(var a in this.items){var b=this.items[a];if(this._isExpired(b)){this._removeItem(a)}else{d.push(b)}}if(d.length>this.purgeSize){d=d.sort(function(f,e){if(f.options.priority!=e.options.priority){return e.options.priority-f.options.priority}else{return e.lastAccessed-f.lastAccessed}});while(d.length>this.purgeSize){var c=d.pop();this._removeItem(c.key)}}};Cache.prototype._addItem=function(a){this.items[a.key]=a;this.count++};Cache.prototype._removeItem=function(a){var b=this.items[a];delete this.items[a];this.count--;if(b.options.callback!=null){var c=function(){b.options.callback(b.key,b.value)};setTimeout(c,0)}};Cache.prototype._isExpired=function(c){var a=new Date().getTime();var b=false;if((c.options.expirationAbsolute)&&(c.options.expirationAbsolute<a)){b=true}if((b==false)&&(c.options.expirationSliding)){var d=c.lastAccessed+(c.options.expirationSliding*1000);if(d<a){b=true}}return b};Cache.prototype.toHtmlString=function(){var b=this.count+" item(s) in cache<br /><ul>";for(var a in this.items){var c=this.items[a];b=b+"<li>"+c.key.toString()+" = "+c.value.toString()+"</li>"}b=b+"</ul>";return b};
|
||||
File diff suppressed because one or more lines are too long
+39
-29
@@ -2,7 +2,9 @@
|
||||
2009, James Taylor, Kanwei Li
|
||||
*/
|
||||
|
||||
var DENSITY = 1000;
|
||||
var DENSITY = 1000,
|
||||
DATA_ERROR = "There was an error in indexing this dataset.",
|
||||
DATA_NONE = "No data for this chrom/contig.";
|
||||
|
||||
var DataCache = function( type, track ) {
|
||||
this.type = type;
|
||||
@@ -239,18 +241,16 @@ $.extend( LineTrack.prototype, TiledTrack.prototype, {
|
||||
var track = this;
|
||||
$.getJSON( data_url, { stats: true, track_type: track.track_type, chrom: track.view.chrom,
|
||||
low: null, high: null, dataset_id: track.dataset_id }, function ( data ) {
|
||||
if (data) {
|
||||
if (data == "error") {
|
||||
track.content_div.addClass("error").text("There was an error in indexing this dataset.");
|
||||
} else if (data == "no data") {
|
||||
// console.log(track.content_div);
|
||||
track.content_div.addClass("nodata").text("No data for this chrom/contig.");
|
||||
} else {
|
||||
track.min_value = data['min'];
|
||||
track.max_value = data['max'];
|
||||
track.vertical_range = track.max_value - track.min_value;
|
||||
track.view.redraw();
|
||||
}
|
||||
if (!data || data == "error") {
|
||||
track.content_div.addClass("error").text(DATA_ERROR);
|
||||
} else if (data == "no data") {
|
||||
// console.log(track.content_div);
|
||||
track.content_div.addClass("nodata").text(DATA_NONE);
|
||||
} else {
|
||||
track.min_value = data['min'];
|
||||
track.max_value = data['max'];
|
||||
track.vertical_range = track.max_value - track.min_value;
|
||||
track.view.redraw();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -321,27 +321,28 @@ var FeatureTrack = function ( name, dataset_id, height ) {
|
||||
this.zo_slots = {};
|
||||
this.show_labels_scale = 0.01;
|
||||
this.showing_labels = false;
|
||||
this.vertical_gap = 10;
|
||||
};
|
||||
$.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
|
||||
calc_slots: function( include_labels ) {
|
||||
// console.log("num vals: " + this.values.length);
|
||||
var end_ary = new Array();
|
||||
var end_ary = [];
|
||||
var scale = this.container_div.width() / (this.view.high - this.view.low);
|
||||
// console.log(scale);
|
||||
if (include_labels) this.zi_slots = new Object();
|
||||
// console.log(scale, this.view.high, this.view.low);
|
||||
if (include_labels) this.zi_slots = {};
|
||||
var dummy_canvas = $("<canvas></canvas>").get(0).getContext("2d");
|
||||
for (var i in this.values) {
|
||||
|
||||
feature = this.values[i];
|
||||
f_start = Math.floor( Math.max(this.view.max_low, (feature.start - this.view.max_low) * scale) );
|
||||
var feature = this.values[i];
|
||||
var f_start = Math.floor( Math.max(this.view.max_low, (feature.start - this.view.max_low) * scale) );
|
||||
if (include_labels) {
|
||||
f_start -= dummy_canvas.measureText(feature.name).width;
|
||||
f_start -= 10; // Spacing between text and line
|
||||
}
|
||||
|
||||
f_end = Math.ceil( Math.min(this.view.max_high, (feature.end - this.view.max_low) * scale) );
|
||||
var f_end = Math.ceil( Math.min(this.view.max_high, (feature.end - this.view.max_low) * scale) );
|
||||
// if (include_labels) { console.log(f_start, f_end); }
|
||||
j = 0;
|
||||
var j = 0;
|
||||
while (true) {
|
||||
if (end_ary[j] == undefined || end_ary[j] < f_start) {
|
||||
end_ary[j] = f_end;
|
||||
@@ -355,17 +356,26 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
this.height_px = end_ary.length * this.vertical_gap + 15;
|
||||
this.content_div.css( "height", this.height_px + "px" );
|
||||
},
|
||||
|
||||
init: function() {
|
||||
var track = this;
|
||||
$.getJSON( data_url, { track_type: track.track_type, low: track.view.max_low, high: track.view.max_high,
|
||||
dataset_id: track.dataset_id, chrom: track.view.chrom }, function ( data ) {
|
||||
track.values = data;
|
||||
track.calc_slots();
|
||||
track.slots = track.zo_slots;
|
||||
// console.log(track.zo_slots);
|
||||
track.draw();
|
||||
if (data == "error") {
|
||||
track.content_div.addClass("error").text(DATA_ERROR);
|
||||
} else if (data.length == 0 || data == "no data") {
|
||||
// console.log(track.content_div);
|
||||
track.content_div.addClass("nodata").text(DATA_NONE);
|
||||
} else {
|
||||
track.values = data;
|
||||
track.calc_slots();
|
||||
track.slots = track.zo_slots;
|
||||
// console.log(track.zo_slots);
|
||||
track.draw();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -391,7 +401,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
range = view.high - view.low,
|
||||
width = Math.ceil( tile_length * w_scale ),
|
||||
slots = new Array(),
|
||||
height = 200,
|
||||
height = this.height_px,
|
||||
new_canvas = $("<canvas class='tile'></canvas>");
|
||||
|
||||
new_canvas.css({
|
||||
@@ -412,7 +422,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
f_end = Math.ceil( Math.min(width, (feature.end - tile_low) * w_scale) );
|
||||
// console.log(feature.start, feature.end, f_start, f_end, j);
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(f_start, this.slots[feature.name] * 10 + 5, f_end - f_start, 1);
|
||||
ctx.fillRect(f_start, this.slots[feature.name] * this.vertical_gap + 5, f_end - f_start, 1);
|
||||
|
||||
if (this.showing_labels && ctx.fillText) {
|
||||
ctx.font = "10px monospace";
|
||||
@@ -435,7 +445,7 @@ $.extend( FeatureTrack.prototype, TiledTrack.prototype, {
|
||||
if (exon_start && block_start >= exon_start && block_end <= exon_end) {
|
||||
thickness = 5, y_start = 3;
|
||||
}
|
||||
ctx.fillRect(exon_start, this.slots[feature.name] * 10 + y_start, block_end - block_start, thickness);
|
||||
ctx.fillRect(exon_start, this.slots[feature.name] * this.vertical_gap + y_start, block_end - block_start, thickness);
|
||||
// console.log(block_start, block_end);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ body {
|
||||
.track-content.error {
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
background-color: #600;
|
||||
background-color: #ECB4AF;
|
||||
}
|
||||
.track-content.nodata {
|
||||
text-align: center;
|
||||
|
||||
Reference in New Issue
Block a user