This commit is contained in:
Dannon Baker
2015-02-10 09:51:33 -05:00
8 changed files with 100 additions and 493 deletions
@@ -150,6 +150,9 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** fn to call when the collection is created (scoped to this) */
this.oncreate = attributes.oncreate;
/** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
this.autoscrollDist = attributes.autoscrollDist || 24;
/** is the unpaired panel shown? */
this.unpairedPanelHidden = false;
/** is the paired panel shown? */
@@ -873,7 +876,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
filterChoice( '_1', '_2' ),
filterChoice( '_R1', '_R2' ),
'</div>'
].join(''), {}));
].join(''))({}));
return this.$( selector ).popover({
container : '.collection-creator',
@@ -1025,7 +1028,6 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*)*/
_showAlert : function( message, alertClass ){
console.debug( 'alert:', message, alertClass );
alertClass = alertClass || 'alert-danger';
this.$( '.main-help' ).hide();
this.$( '.header .alert' ).attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
@@ -1305,8 +1307,10 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** rename a pair when the pair name is clicked */
_clickPairName : function( ev ){
ev.stopPropagation();
var $control = $( ev.currentTarget ),
pair = this.paired[ $control.parent().parent().index() / 2 ],
var $name = $( ev.currentTarget ),
$pair = $name.parent().parent(),
index = $pair.index( '.dataset.paired' ),
pair = this.paired[ index ],
response = prompt( 'Enter a new name for the pair:', pair.name );
if( response ){
pair.name = response;
@@ -1314,7 +1318,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
// when adding/removing extensions
//TODO: kinda hacky
pair.customizedName = true;
$control.text( pair.name );
$name.text( pair.name );
}
},
@@ -1322,7 +1326,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
_clickUnpair : function( ev ){
//if( !ev.currentTarget ){ return true; }
//TODO: this is a hack bc each paired rev now has two elems (dataset, button)
var pairIndex = Math.floor( $( ev.currentTarget ).index() / 2 );
var pairIndex = Math.floor( $( ev.currentTarget ).index( '.unpair-btn' ) );
//this.debug( 'pair:', pairIndex );
//TODO: animate
this._unpair( this.paired[ pairIndex ] );
@@ -1340,27 +1344,41 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
//console.debug( '_dragoverPairedColumns:', ev );
ev.preventDefault();
var $list = this.$( '.paired-columns .column-datasets' ),
offset = $list.offset();
var $list = this.$( '.paired-columns .column-datasets' );
this._checkForAutoscroll( $list, ev.originalEvent.clientY );
//console.debug( ev.originalEvent.clientX, ev.originalEvent.clientY );
var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY );
//console.debug( ev.originalEvent.clientX - offset.left, ev.originalEvent.clientY - offset.top );
$( '.paired-drop-placeholder' ).remove();
var $placeholder = $( '<div class="paired-drop-placeholder"></div>')
var $placeholder = $( '<div class="paired-drop-placeholder"></div>' );
if( !$nearest.size() ){
$list.append( $placeholder );
} else {
$nearest.before( $placeholder );
}
},
/** If the mouse is near enough to the list's top or bottom, scroll the list */
_checkForAutoscroll : function( $element, y ){
var AUTOSCROLL_SPEED = 2;
var offset = $element.offset(),
scrollTop = $element.scrollTop(),
upperDist = y - offset.top,
lowerDist = ( offset.top + $element.outerHeight() ) - y;
//console.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist );
if( upperDist >= 0 && upperDist < this.autoscrollDist ){
$element.scrollTop( scrollTop - AUTOSCROLL_SPEED );
} else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){
$element.scrollTop( scrollTop + AUTOSCROLL_SPEED );
}
},
/** get the nearest *previous* paired dataset PairView based on the mouse's Y coordinate.
* If the y is at the end of the list, return an empty jQuery object.
*/
_getNearestPairedDatasetLi : function( y ){
var WIGGLE = 4,
lis = this.$( '.paired-columns .column-datasets li' ).toArray();
//TODO: better way?
for( var i=0; i<lis.length; i++ ){
var $li = $( lis[i] ),
top = $li.offset().top,
@@ -1,228 +0,0 @@
//==============================================================================
/*
TODO:
?? superclass dataloader, subclass lazydataloader??
*/
//==============================================================================
/**
* Object to progressively load JSON data from a REST url, delaying some time between loading chunks
*
* Will load size amount of data every delay ms, starting at start and ending at total.
*
* NOTE: Data from ajax loading is aggregated in a list, with one element for each ajax response.
* It's up to the calling code to combine the results in a meaningful, correct way.
*
* example:
* var loader = new scatterplot.LazyDataLoader({
* //logger : console,
* url : ( apiDatasetsURL + '/' + hda.id + '?data_type=raw_data'
* + '&columns=[10,14]' ),
* total : hda.metadata_data_lines,
* size : 500,
*
* initialize : function( config ){
* // ... do some stuff
* },
*
* buildUrl : function( start, size ){
* // change the formation of start, size in query string
* return loader.url + '&' + jQuery.param({
* start_val: start,
* max_vals: size
* });
* },
* });
*
* // you can use events
* $( loader ).bind( 'error', function( event, xhr, status, error ){
* alert( loader + ' ERROR:' + status + '\n' + error );
* // bail out...
* });
* $( loader ).bind( 'loaded.new', function( event, response ){
* console.info( 'new data available:', event, response );
* // ... do stuff with new data
* });
* $( loader ).bind( 'complete', function( event, allDataArray, total ){
* console.info( 'final load complete:', event, allDataArray, total );
* // ... do stuff with all data
* });
*
* // ...or use a callback called when all data is loaded
* loader.load( function( dataArray ){ console.debug( 'FINISHED!', x, y, z ); } );
*/
function LazyDataLoader( config ){
// for now assume:
// get, async, and params sent via url query string
// we want json
// we know the size of the data on the server beforehand
var loader = this,
// events to trigger when new or all data has been loaded
// new batches of data (including last). Will be sent: the ajax response data, start value, and size
LOADED_NEW_EVENT = 'loaded.new',
// all data has been loaded: the final loader's data array and the total
LOADED_ALL_EVENT = 'complete';
// error from ajax
ERROR_EVENT = 'error';
jQuery.extend( loader, LoggableMixin );
jQuery.extend( loader, {
//NOTE: the next two need to be sent in config (required)
// total size of data on server
total : undefined,
// url of service to get the data
url : undefined,
// holds the interval id for the current load delay
currentIntervalId : undefined,
// each load call will add an element to this array
// it's the responsibility of the code using this to combine them properly
data : [],
// ms btwn recursive loads
delay : 4000,
// starting line, element, whatever
start : 0,
// size to fetch per load
size : 4000,
// loader init func: extends loader with config and calls config.init if there
//@param {object} config : object containing variables to override (or additional)
initialize : function( config ){
jQuery.extend( loader, config );
// call the custom initialize function if any
// only dangerous if the user tries LazyDataLoader.prototype.init
if( config.hasOwnProperty( 'initialize' ) ){
config.initialize.call( loader, config );
}
this.log( this + ' initialized:', loader );
},
// returns query string formatted start and size (for the next fetch) appended to the loader.url
//OVERRIDE: to change how params are passed, param names, etc.
//@param {int} start : the line/row/datum indicating where in the dataset the next load begins
//@param {int} size : the number of lines/rows/data to get on the next load
buildUrl : function( start, size ){
// currently VERY SPECIFIC to using data_providers.py start_val, max_vals params
return this.url + '&' + jQuery.param({
start_val: start,
max_vals: size
});
},
//OVERRIDE: to handle ajax errors differently
ajaxErrorFn : function( xhr, status, error ){
console.error( 'ERROR fetching data:', error );
},
// converters passed to the jQuery ajax call for data type parsing
//OVERRIDE: to provide custom parsing
converters : {
'* text' : window.String,
'text html' : true,
'text xml' : jQuery.parseXML,
'text json' : jQuery.parseJSON
},
// interface to begin load (and first recursive call)
//@param {Function} callback : function to execute when all data is loaded. callback is passed loader.data
load : function( callback ){
this.log( this + '.load' );
// ensure necessary stuff
if( !loader.url ){ throw( loader + ' requires a url' ); }
if( this.total === null ){
this.log( '\t total is null (will load all)' );
} else {
this.log( '\t total:', this.total );
}
//if( !loader.total ){ throw( loader + ' requires a total (total size of the data)' ); }
//FIRST RECURSION: start
var startingSize = loader.size;
if( ( loader.total !== null ) && ( loader.total < loader.size ) ){
startingSize = loader.total;
}
loader.log( loader + '\t beginning recursion' );
loadHelper( loader.start, startingSize );
//FIRST, SUBSEQUENT RECURSION function
function loadHelper( start, size ){
loader.log( loader + '.loadHelper, start:', start, 'size:', size );
var url = loader.buildUrl( start, size );
loader.log( '\t url:', url );
jQuery.ajax({
url : loader.buildUrl( start, size ),
converters : loader.converters,
dataType : 'json',
error : function( xhr, status, error ){
loader.log( '\t ajax error, status:', status, 'error:', error );
if( loader.currentIntervalId ){
clearInterval( loader.currentIntervalId );
}
$( loader ).trigger( ERROR_EVENT, [ status, error ] );
loader.ajaxErrorFn( xhr, status, error );
},
success : function( response ){
loader.log( '\t ajax success, response:', response, 'next:', next, 'remainder:', remainder );
if( response !== null ){
// store the response as is in a new element
//TODO:?? store start, size as well?
loader.data.push( response );
//TODO: these might not be the best way to split this up
// fire the first load event (if this is the first batch) AND partial
$( loader ).trigger( LOADED_NEW_EVENT, [ response, start, size ] );
//RECURSION:
var next = start + size,
remainder = loader.size;
if( loader.total !== null ){
remainder = Math.min( loader.total - next, loader.size );
}
loader.log( '\t next recursion, start:', next, 'size:', remainder );
// if we haven't gotten everything yet, so set up for next recursive call and set the timer
if( loader.total === null || remainder > 0 ){
loader.currentIntervalId = setTimeout(
function(){ loadHelper( next, remainder ); },
loader.delay
);
loader.log( '\t currentIntervalId:', loader.currentIntervalId );
// otherwise (base-case), don't do anything
} else {
loadFinished();
}
} else { //response === null --> base-case, server sez nuthin left
loadFinished();
}
}
});
}
//HANDLE BASE-CASE, LAST RECURSION
function loadFinished(){
loader.log( loader + '.loadHelper, has finished:', loader.data );
$( loader ).trigger( LOADED_ALL_EVENT, [ loader.data, loader.total ] );
if( callback ){ callback( loader.data ); }
}
},
toString : function(){ return 'LazyDataLoader'; }
});
loader.initialize( config );
return loader;
}
//==============================================================================
+18 -3
View File
@@ -14,6 +14,7 @@ from datetime import datetime
from galaxy.exceptions import ObjectNotFound
from galaxy.util import umask_fix_perms, string_as_bool
from galaxy.util import string_as_bool
from galaxy.util.directory_hash import directory_hash_id
from galaxy.util.sleeper import Sleeper
from .s3_multipart_upload import multipart_upload
@@ -77,6 +78,7 @@ class S3ObjectStore(ObjectStore):
b_xml = config_xml.findall('bucket')[0]
self.bucket = b_xml.get('name')
self.use_rr = string_as_bool(b_xml.get('use_reduced_redundancy', "False"))
self.max_chunk_size = int(b_xml.get('max_chunk_size', 250))
cn_xml = config_xml.findall('connection')
if not cn_xml:
cn_xml = {}
@@ -84,11 +86,24 @@ class S3ObjectStore(ObjectStore):
cn_xml = cn_xml[0]
self.host = cn_xml.get('host', None)
self.port = int(cn_xml.get('port', 6000))
self.is_secure = cn_xml.get('is_secure', True)
self.multipart = string_as_bool(cn_xml.get('multipart', 'True'))
self.is_secure = string_as_bool(cn_xml.get('is_secure', 'True'))
self.conn_path = cn_xml.get('conn_path', '/')
c_xml = config_xml.findall('cache')[0]
self.cache_size = float(c_xml.get('size', -1))
self.cache_path = c_xml.get('path')
# for multipart upload
self.s3server = {'access_key': self.access_key,
'secret_key': self.secret_key,
'is_secure': self.is_secure,
'max_chunk_size': self.max_chunk_size,
'host': self.host,
'port': self.port,
'use_rr': self.use_rr,
'conn_path': self.conn_path}
except Exception:
# Toss it back up after logging, we can't continue loading at this point.
log.exception("Malformed ObjectStore Configuration XML -- unable to continue")
@@ -328,14 +343,14 @@ class S3ObjectStore(ObjectStore):
start_time = datetime.now()
log.debug("Pushing cache file '%s' of size %s bytes to key '%s'" % (source_file, os.path.getsize(source_file), rel_path))
mb_size = os.path.getsize(source_file) / 1e6
if mb_size < 10 or type(self) == SwiftObjectStore:
if mb_size < 10 or (not self.multipart):
self.transfer_progress = 0 # Reset transfer progress counter
key.set_contents_from_filename(source_file,
reduced_redundancy=self.use_rr,
cb=self._transfer_cb,
num_cb=10)
else:
multipart_upload(self.bucket, key.name, source_file, mb_size, self.access_key, self.secret_key, use_rr=self.use_rr)
multipart_upload(self.s3server, self.bucket, key.name, source_file, mb_size)
end_time = datetime.now()
log.debug("Pushed cache file '%s' to key '%s' (%s bytes transfered in %s sec)" % (source_file, rel_path, os.path.getsize(source_file), end_time - start_time))
return True
+23 -10
View File
@@ -34,13 +34,23 @@ def map_wrap(f):
return wrapper
def mp_from_ids(mp_id, mp_keyname, mp_bucketname, aws_access_key_id, aws_secret_access_key):
def mp_from_ids(s3server, mp_id, mp_keyname, mp_bucketname):
"""Get the multipart upload from the bucket and multipart IDs.
This allows us to reconstitute a connection to the upload
from within multiprocessing functions.
"""
conn = S3Connection(aws_access_key_id, aws_secret_access_key)
if s3server['host']:
conn = boto.connect_s3(aws_access_key_id=s3server['access_key'],
aws_secret_access_key=s3server['secret_key'],
is_secure=s3server['is_secure'],
host=s3server['host'],
port=s3server['port'],
calling_format=boto.s3.connection.OrdinaryCallingFormat(),
path=s3server['conn_path'])
else:
conn = S3Connection(s3server['access_key'], s3server['secret_key'])
bucket = conn.lookup(mp_bucketname)
mp = boto.s3.multipart.MultiPartUpload(bucket)
mp.key_name = mp_keyname
@@ -49,33 +59,36 @@ def mp_from_ids(mp_id, mp_keyname, mp_bucketname, aws_access_key_id, aws_secret_
@map_wrap
def transfer_part(mp_id, mp_keyname, mp_bucketname, i, part, aws_access_key_id, aws_secret_access_key):
def transfer_part(s3server, mp_id, mp_keyname, mp_bucketname, i, part):
"""Transfer a part of a multipart upload. Designed to be run in parallel.
"""
mp = mp_from_ids(mp_id, mp_keyname, mp_bucketname, aws_access_key_id, aws_secret_access_key)
mp = mp_from_ids(s3server, mp_id, mp_keyname, mp_bucketname)
with open(part) as t_handle:
mp.upload_part_from_file(t_handle, i + 1)
os.remove(part)
def multipart_upload(bucket, s3_key_name, tarball, mb_size, aws_access_key_id, aws_secret_access_key, use_rr=True):
def multipart_upload(s3server, bucket, s3_key_name, tarball, mb_size):
"""Upload large files using Amazon's multipart upload functionality.
"""
cores = multiprocessing.cpu_count()
def split_file(in_file, mb_size, split_num=5):
prefix = os.path.join(os.path.dirname(in_file),
"%sS3PART" % (os.path.basename(s3_key_name)))
# Split chunks so they are 5MB < chunk < 250MB
split_size = int(max(min(mb_size / (split_num * 2.0), 250), 5))
"%sS3PART" % (os.path.basename(s3_key_name)))
max_chunk = s3server['max_chunk_size']
# Split chunks so they are 5MB < chunk < 250MB(max_chunk_size)
split_size = int(max(min(mb_size / (split_num * 2.0), max_chunk), 5))
if not os.path.exists("%saa" % prefix):
cl = ["split", "-b%sm" % split_size, in_file, prefix]
subprocess.check_call(cl)
return sorted(glob.glob("%s*" % prefix))
mp = bucket.initiate_multipart_upload(s3_key_name, reduced_redundancy=use_rr)
mp = bucket.initiate_multipart_upload(s3_key_name,
reduced_redundancy=s3server['use_rr'])
with multimap(cores) as pmap:
for _ in pmap(transfer_part, ((mp.id, mp.key_name, mp.bucket_name, i, part, aws_access_key_id, aws_secret_access_key)
for _ in pmap(transfer_part, ((s3server, mp.id, mp.key_name, mp.bucket_name, i, part)
for (i, part) in
enumerate(split_file(tarball, mb_size, cores)))):
pass
@@ -150,6 +150,9 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** fn to call when the collection is created (scoped to this) */
this.oncreate = attributes.oncreate;
/** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
this.autoscrollDist = attributes.autoscrollDist || 24;
/** is the unpaired panel shown? */
this.unpairedPanelHidden = false;
/** is the paired panel shown? */
@@ -873,7 +876,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
filterChoice( '_1', '_2' ),
filterChoice( '_R1', '_R2' ),
'</div>'
].join(''), {}));
].join(''))({}));
return this.$( selector ).popover({
container : '.collection-creator',
@@ -1025,7 +1028,6 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*)*/
_showAlert : function( message, alertClass ){
console.debug( 'alert:', message, alertClass );
alertClass = alertClass || 'alert-danger';
this.$( '.main-help' ).hide();
this.$( '.header .alert' ).attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
@@ -1305,8 +1307,10 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
/** rename a pair when the pair name is clicked */
_clickPairName : function( ev ){
ev.stopPropagation();
var $control = $( ev.currentTarget ),
pair = this.paired[ $control.parent().parent().index() / 2 ],
var $name = $( ev.currentTarget ),
$pair = $name.parent().parent(),
index = $pair.index( '.dataset.paired' ),
pair = this.paired[ index ],
response = prompt( 'Enter a new name for the pair:', pair.name );
if( response ){
pair.name = response;
@@ -1314,7 +1318,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
// when adding/removing extensions
//TODO: kinda hacky
pair.customizedName = true;
$control.text( pair.name );
$name.text( pair.name );
}
},
@@ -1322,7 +1326,7 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
_clickUnpair : function( ev ){
//if( !ev.currentTarget ){ return true; }
//TODO: this is a hack bc each paired rev now has two elems (dataset, button)
var pairIndex = Math.floor( $( ev.currentTarget ).index() / 2 );
var pairIndex = Math.floor( $( ev.currentTarget ).index( '.unpair-btn' ) );
//this.debug( 'pair:', pairIndex );
//TODO: animate
this._unpair( this.paired[ pairIndex ] );
@@ -1340,27 +1344,41 @@ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).exte
//console.debug( '_dragoverPairedColumns:', ev );
ev.preventDefault();
var $list = this.$( '.paired-columns .column-datasets' ),
offset = $list.offset();
var $list = this.$( '.paired-columns .column-datasets' );
this._checkForAutoscroll( $list, ev.originalEvent.clientY );
//console.debug( ev.originalEvent.clientX, ev.originalEvent.clientY );
var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY );
//console.debug( ev.originalEvent.clientX - offset.left, ev.originalEvent.clientY - offset.top );
$( '.paired-drop-placeholder' ).remove();
var $placeholder = $( '<div class="paired-drop-placeholder"></div>')
var $placeholder = $( '<div class="paired-drop-placeholder"></div>' );
if( !$nearest.size() ){
$list.append( $placeholder );
} else {
$nearest.before( $placeholder );
}
},
/** If the mouse is near enough to the list's top or bottom, scroll the list */
_checkForAutoscroll : function( $element, y ){
var AUTOSCROLL_SPEED = 2;
var offset = $element.offset(),
scrollTop = $element.scrollTop(),
upperDist = y - offset.top,
lowerDist = ( offset.top + $element.outerHeight() ) - y;
//console.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist );
if( upperDist >= 0 && upperDist < this.autoscrollDist ){
$element.scrollTop( scrollTop - AUTOSCROLL_SPEED );
} else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){
$element.scrollTop( scrollTop + AUTOSCROLL_SPEED );
}
},
/** get the nearest *previous* paired dataset PairView based on the mouse's Y coordinate.
* If the y is at the end of the list, return an empty jQuery object.
*/
_getNearestPairedDatasetLi : function( y ){
var WIGGLE = 4,
lis = this.$( '.paired-columns .column-datasets li' ).toArray();
//TODO: better way?
for( var i=0; i<lis.length; i++ ){
var $li = $( lis[i] ),
top = $li.offset().top,
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
function LazyDataLoader(c){var a=this,d="loaded.new",b="complete";ERROR_EVENT="error";jQuery.extend(a,LoggableMixin);jQuery.extend(a,{total:undefined,url:undefined,currentIntervalId:undefined,data:[],delay:4000,start:0,size:4000,initialize:function(e){jQuery.extend(a,e);if(e.hasOwnProperty("initialize")){e.initialize.call(a,e)}this.log(this+" initialized:",a)},buildUrl:function(f,e){return this.url+"&"+jQuery.param({start_val:f,max_vals:e})},ajaxErrorFn:function(g,e,f){console.error("ERROR fetching data:",f)},converters:{"* text":window.String,"text html":true,"text xml":jQuery.parseXML,"text json":jQuery.parseJSON},load:function(h){this.log(this+".load");if(!a.url){throw (a+" requires a url")}if(this.total===null){this.log("\t total is null (will load all)")}else{this.log("\t total:",this.total)}var g=a.size;if((a.total!==null)&&(a.total<a.size)){g=a.total}a.log(a+"\t beginning recursion");f(a.start,g);function f(k,j){a.log(a+".loadHelper, start:",k,"size:",j);var i=a.buildUrl(k,j);a.log("\t url:",i);jQuery.ajax({url:a.buildUrl(k,j),converters:a.converters,dataType:"json",error:function(n,l,m){a.log("\t ajax error, status:",l,"error:",m);if(a.currentIntervalId){clearInterval(a.currentIntervalId)}$(a).trigger(ERROR_EVENT,[l,m]);a.ajaxErrorFn(n,l,m)},success:function(l){a.log("\t ajax success, response:",l,"next:",m,"remainder:",n);if(l!==null){a.data.push(l);$(a).trigger(d,[l,k,j]);var m=k+j,n=a.size;if(a.total!==null){n=Math.min(a.total-m,a.size)}a.log("\t next recursion, start:",m,"size:",n);if(a.total===null||n>0){a.currentIntervalId=setTimeout(function(){f(m,n)},a.delay);a.log("\t currentIntervalId:",a.currentIntervalId)}else{e()}}else{e()}}})}function e(){a.log(a+".loadHelper, has finished:",a.data);$(a).trigger(b,[a.data,a.total]);if(h){h(a.data)}}},toString:function(){return"LazyDataLoader"}});a.initialize(c);return a};
-228
View File
@@ -1,228 +0,0 @@
//==============================================================================
/*
TODO:
?? superclass dataloader, subclass lazydataloader??
*/
//==============================================================================
/**
* Object to progressively load JSON data from a REST url, delaying some time between loading chunks
*
* Will load size amount of data every delay ms, starting at start and ending at total.
*
* NOTE: Data from ajax loading is aggregated in a list, with one element for each ajax response.
* It's up to the calling code to combine the results in a meaningful, correct way.
*
* example:
* var loader = new scatterplot.LazyDataLoader({
* //logger : console,
* url : ( apiDatasetsURL + '/' + hda.id + '?data_type=raw_data'
* + '&columns=[10,14]' ),
* total : hda.metadata_data_lines,
* size : 500,
*
* initialize : function( config ){
* // ... do some stuff
* },
*
* buildUrl : function( start, size ){
* // change the formation of start, size in query string
* return loader.url + '&' + jQuery.param({
* start_val: start,
* max_vals: size
* });
* },
* });
*
* // you can use events
* $( loader ).bind( 'error', function( event, xhr, status, error ){
* alert( loader + ' ERROR:' + status + '\n' + error );
* // bail out...
* });
* $( loader ).bind( 'loaded.new', function( event, response ){
* console.info( 'new data available:', event, response );
* // ... do stuff with new data
* });
* $( loader ).bind( 'complete', function( event, allDataArray, total ){
* console.info( 'final load complete:', event, allDataArray, total );
* // ... do stuff with all data
* });
*
* // ...or use a callback called when all data is loaded
* loader.load( function( dataArray ){ console.debug( 'FINISHED!', x, y, z ); } );
*/
function LazyDataLoader( config ){
// for now assume:
// get, async, and params sent via url query string
// we want json
// we know the size of the data on the server beforehand
var loader = this,
// events to trigger when new or all data has been loaded
// new batches of data (including last). Will be sent: the ajax response data, start value, and size
LOADED_NEW_EVENT = 'loaded.new',
// all data has been loaded: the final loader's data array and the total
LOADED_ALL_EVENT = 'complete';
// error from ajax
ERROR_EVENT = 'error';
jQuery.extend( loader, LoggableMixin );
jQuery.extend( loader, {
//NOTE: the next two need to be sent in config (required)
// total size of data on server
total : undefined,
// url of service to get the data
url : undefined,
// holds the interval id for the current load delay
currentIntervalId : undefined,
// each load call will add an element to this array
// it's the responsibility of the code using this to combine them properly
data : [],
// ms btwn recursive loads
delay : 4000,
// starting line, element, whatever
start : 0,
// size to fetch per load
size : 4000,
// loader init func: extends loader with config and calls config.init if there
//@param {object} config : object containing variables to override (or additional)
initialize : function( config ){
jQuery.extend( loader, config );
// call the custom initialize function if any
// only dangerous if the user tries LazyDataLoader.prototype.init
if( config.hasOwnProperty( 'initialize' ) ){
config.initialize.call( loader, config );
}
this.log( this + ' initialized:', loader );
},
// returns query string formatted start and size (for the next fetch) appended to the loader.url
//OVERRIDE: to change how params are passed, param names, etc.
//@param {int} start : the line/row/datum indicating where in the dataset the next load begins
//@param {int} size : the number of lines/rows/data to get on the next load
buildUrl : function( start, size ){
// currently VERY SPECIFIC to using data_providers.py start_val, max_vals params
return this.url + '&' + jQuery.param({
start_val: start,
max_vals: size
});
},
//OVERRIDE: to handle ajax errors differently
ajaxErrorFn : function( xhr, status, error ){
console.error( 'ERROR fetching data:', error );
},
// converters passed to the jQuery ajax call for data type parsing
//OVERRIDE: to provide custom parsing
converters : {
'* text' : window.String,
'text html' : true,
'text xml' : jQuery.parseXML,
'text json' : jQuery.parseJSON
},
// interface to begin load (and first recursive call)
//@param {Function} callback : function to execute when all data is loaded. callback is passed loader.data
load : function( callback ){
this.log( this + '.load' );
// ensure necessary stuff
if( !loader.url ){ throw( loader + ' requires a url' ); }
if( this.total === null ){
this.log( '\t total is null (will load all)' );
} else {
this.log( '\t total:', this.total );
}
//if( !loader.total ){ throw( loader + ' requires a total (total size of the data)' ); }
//FIRST RECURSION: start
var startingSize = loader.size;
if( ( loader.total !== null ) && ( loader.total < loader.size ) ){
startingSize = loader.total;
}
loader.log( loader + '\t beginning recursion' );
loadHelper( loader.start, startingSize );
//FIRST, SUBSEQUENT RECURSION function
function loadHelper( start, size ){
loader.log( loader + '.loadHelper, start:', start, 'size:', size );
var url = loader.buildUrl( start, size );
loader.log( '\t url:', url );
jQuery.ajax({
url : loader.buildUrl( start, size ),
converters : loader.converters,
dataType : 'json',
error : function( xhr, status, error ){
loader.log( '\t ajax error, status:', status, 'error:', error );
if( loader.currentIntervalId ){
clearInterval( loader.currentIntervalId );
}
$( loader ).trigger( ERROR_EVENT, [ status, error ] );
loader.ajaxErrorFn( xhr, status, error );
},
success : function( response ){
loader.log( '\t ajax success, response:', response, 'next:', next, 'remainder:', remainder );
if( response !== null ){
// store the response as is in a new element
//TODO:?? store start, size as well?
loader.data.push( response );
//TODO: these might not be the best way to split this up
// fire the first load event (if this is the first batch) AND partial
$( loader ).trigger( LOADED_NEW_EVENT, [ response, start, size ] );
//RECURSION:
var next = start + size,
remainder = loader.size;
if( loader.total !== null ){
remainder = Math.min( loader.total - next, loader.size );
}
loader.log( '\t next recursion, start:', next, 'size:', remainder );
// if we haven't gotten everything yet, so set up for next recursive call and set the timer
if( loader.total === null || remainder > 0 ){
loader.currentIntervalId = setTimeout(
function(){ loadHelper( next, remainder ); },
loader.delay
);
loader.log( '\t currentIntervalId:', loader.currentIntervalId );
// otherwise (base-case), don't do anything
} else {
loadFinished();
}
} else { //response === null --> base-case, server sez nuthin left
loadFinished();
}
}
});
}
//HANDLE BASE-CASE, LAST RECURSION
function loadFinished(){
loader.log( loader + '.loadHelper, has finished:', loader.data );
$( loader ).trigger( LOADED_ALL_EVENT, [ loader.data, loader.total ] );
if( callback ){ callback( loader.data ); }
}
},
toString : function(){ return 'LazyDataLoader'; }
});
loader.initialize( config );
return loader;
}
//==============================================================================