1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 SC.IMAGE_ABORTED_ERROR = SC.$error("SC.Image.AbortedError", "Image", -100) ; 9 10 SC.IMAGE_FAILED_ERROR = SC.$error("SC.Image.FailedError", "Image", -101) ; 11 12 /** 13 @class 14 15 The image queue can be used to control the order of loading images. 16 17 Images queues are necessary because browsers impose strict limits on the 18 number of concurrent connections that can be open at any one time to any one 19 host. By controlling the order and timing of your loads using this image 20 queue, you can improve the percieved performance of your application by 21 ensuring the images you need most load first. 22 23 Note that if you use the SC.ImageView class, it will use this image queue 24 for you automatically. 25 26 ## Loading Images 27 28 When you need to display an image, simply call the loadImage() method with 29 the URL of the image, along with a target/method callback. The signature of 30 your callback should be: 31 32 imageDidLoad: function(imageUrl, imageOrError) { 33 //... 34 } 35 36 The "imageOrError" parameter will contain either an image object or an error 37 object if the image could not be loaded for some reason. If you receive an 38 error object, it will be one of SC.IMAGE_ABORTED_ERROR or 39 SC.IMAGE_FAILED_ERROR. 40 41 You can also optionally specify that the image should be loaded in the 42 background. Background images are loaded with a lower priority than 43 foreground images. 44 45 ## Aborting Image Loads 46 47 If you request an image load but then no longer require the image for some 48 reason, you should notify the imageQueue by calling the releaseImage() 49 method. Pass the URL, target and method that you included in your original 50 loadImage() request. 51 52 If you have requested an image before, you should always call releaseImage() 53 when you are finished with it, even if the image has already loaded. This 54 will allow the imageQueue to properly manage its own internal resources. 55 56 This method may remove the image from the queue of images that need or load 57 or it may abort an image load in progress to make room for other images. If 58 the image is already loaded, this method will have no effect. 59 60 ## Reloading an Image 61 62 If you have already loaded an image, the imageQueue will avoid loading the 63 image again. However, if you need to force the imageQueue to reload the 64 image for some reason, you can do so by calling reloadImage(), passing the 65 URL. 66 67 This will cause the image queue to attempt to load the image again the next 68 time you call loadImage on it. 69 70 @extends SC.Object 71 @since SproutCore 1.0 72 */ 73 SC.imageQueue = SC.Object.create(/** @scope SC.imageQueue.prototype */ { 74 75 /** 76 The maximum number of images that can load from a single hostname at any 77 one time. For most browsers 4 is a reasonable number, though you may 78 tweak this on a browser-by-browser basis. 79 */ 80 loadLimit: 4, 81 82 /** 83 The number of currently active requests on the queue. 84 */ 85 activeRequests: 0, 86 87 /** 88 Loads an image from the server, calling your target/method when complete. 89 90 You should always pass at least a URL and optionally a target/method. If 91 you do not pass the target/method, the image will be loaded in background 92 priority. Usually, however, you will want to pass a callback to be 93 notified when the image has loaded. Your callback should have a signature 94 like: 95 96 imageDidLoad: function(imageUrl, imageOrError) { .. } 97 98 If you do pass a target/method you can optionally also choose to load the 99 image either in the foreground or in the background. The imageQueue 100 prioritizes foreground images over background images. This does not impact 101 how many images load at one time. 102 103 @param {String} url 104 @param {Object} target 105 @param {String|Function} method 106 @param {Boolean} isBackgroundFlag 107 @returns {SC.imageQueue} receiver 108 */ 109 loadImage: function(url, target, method, isBackgroundFlag) { 110 // normalize params 111 var type = SC.typeOf(target); 112 if (SC.none(method) && SC.typeOf(target)===SC.T_FUNCTION) { 113 target = null; method = target ; 114 } 115 if (SC.typeOf(method) === SC.T_STRING) { 116 method = target[method]; 117 } 118 // if no callback is passed, assume background image. otherwise, assume 119 // foreground image. 120 if (SC.none(isBackgroundFlag)) { 121 isBackgroundFlag = SC.none(target) && SC.none(method); 122 } 123 124 // get image entry in queue. If entry is loaded, just invoke callback 125 // and quit. 126 var entry = this._imageEntryFor(url) ; 127 if (entry.status === this.IMAGE_LOADED) { 128 if (method) method.call(target || entry.image, entry.url, entry.image); 129 130 // otherwise, add to list of callbacks and queue image. 131 } else { 132 if (target || method) this._addCallback(entry, target, method); 133 entry.retainCount++; // increment retain count, regardless of callback 134 this._scheduleImageEntry(entry, isBackgroundFlag); 135 } 136 }, 137 138 /** 139 Invoke this method when you are finished with an image URL. If you 140 passed a target/method, you should also pass it here to remove it from 141 the list of callbacks. 142 143 @param {String} url 144 @param {Object} target 145 @param {String|Function} method 146 @returns {SC.imageQueue} receiver 147 */ 148 releaseImage: function(url, target, method) { 149 // get entry. if there is no entry, just return as there is nothing to 150 // do. 151 var entry = this._imageEntryFor(url, NO) ; 152 if (!entry) return this ; 153 154 // there is an entry, decrement the retain count. If <=0, delete! 155 if (--entry.retainCount <= 0) { 156 this._deleteEntry(entry); 157 158 // if >0, just remove target/method if passed 159 } else if (target || method) { 160 // normalize 161 var type = SC.typeOf(target); 162 if (SC.none(method) && SC.typeOf(target)===SC.T_FUNCTION) { 163 target = null; method = target ; 164 } 165 if (SC.typeOf(method) === SC.T_STRING) { 166 method = target[method]; 167 } 168 169 // and remove 170 this._removeCallback(entry, target, method) ; 171 } 172 }, 173 174 /** 175 Forces the image to reload the next time you try to load it. 176 */ 177 reloadImage: function(url) { 178 var entry = this._imageEntryFor(url, NO); 179 if (entry && entry.status===this.IMAGE_LOADED) { 180 entry.status = this.IMAGE_WAITING; 181 } 182 }, 183 184 /** 185 Initiates a load of the next image in the image queue. Normally you will 186 not need to call this method yourself as it will be initiated 187 automatically when the queue becomes active. 188 */ 189 loadNextImage: function() { 190 var entry = null, queue; 191 192 // only run if we don't have too many active request... 193 if (this.get('activeRequests')>=this.get('loadLimit')) return; 194 195 // first look in foreground queue 196 queue = this._foregroundQueue ; 197 while(queue.length>0 && !entry) entry = queue.shift(); 198 199 // then look in background queue 200 if (!entry) { 201 queue = this._backgroundQueue ; 202 while(queue.length>0 && !entry) entry = queue.shift(); 203 } 204 this.set('isLoading', !!entry); // update isLoading... 205 206 // if we have an entry, then initiate an image load with the proper 207 // callbacks. 208 if (entry) { 209 // var img = (entry.image = new Image()) ; 210 var img = entry.image ; 211 if(!img) return; 212 213 // Using bind here instead of setting onabort/onerror/onload directly 214 // fixes an issue with images having 0 width and height 215 $(img).bind('abort', this._imageDidAbort); 216 $(img).bind('error', this._imageDidError); 217 $(img).bind('load', this._imageDidLoad); 218 img.src = entry.url ; 219 220 // add to loading queue. 221 this._loading.push(entry) ; 222 223 // increment active requests and start next request until queue is empty 224 // or until load limit is reached. 225 this.incrementProperty('activeRequests'); 226 this.loadNextImage(); 227 } 228 }, 229 230 // .......................................................... 231 // SUPPORT METHODS 232 // 233 234 /** @private Find or create an entry for the URL. */ 235 _imageEntryFor: function(url, createIfNeeded) { 236 if (createIfNeeded === undefined) createIfNeeded = YES; 237 var entry = this._images[url] ; 238 if (!entry && createIfNeeded) { 239 var img = new Image() ; 240 entry = this._images[url] = { 241 url: url, status: this.IMAGE_WAITING, callbacks: [], retainCount: 0, image: img 242 }; 243 img.entry = entry ; // provide a link back to the image 244 } else if (entry && entry.image === null) { 245 // Ensure that if we retrieve an entry that it has an associated Image, 246 // since failed/aborted images will have had their image property nulled. 247 entry.image = new Image(); 248 entry.image.entry = entry; 249 } 250 return entry ; 251 }, 252 253 /** @private deletes an entry from the image queue, descheduling also */ 254 _deleteEntry: function(entry) { 255 this._unscheduleImageEntry(entry) ; 256 delete this._images[entry.url]; 257 }, 258 259 /** @private 260 Add a callback to the image entry. First search the callbacks to make 261 sure this is only added once. 262 */ 263 _addCallback: function(entry, target, method) { 264 var callbacks = entry.callbacks; 265 266 // try to find in existing array 267 var handler = callbacks.find(function(x) { 268 return x[0]===target && x[1]===method; 269 }, this); 270 271 // not found, add... 272 if (!handler) callbacks.push([target, method]); 273 callbacks = null; // avoid memory leaks 274 return this ; 275 }, 276 277 /** @private 278 Removes a callback from the image entry. Removing a callback just nulls 279 out that position in the array. It will be skipped when executing. 280 */ 281 _removeCallback: function(entry, target, method) { 282 var callbacks = entry.callbacks ; 283 callbacks.forEach(function(x, idx) { 284 if (x[0]===target && x[1]===method) callbacks[idx] = null; 285 }, this); 286 callbacks = null; // avoid memory leaks 287 return this ; 288 }, 289 290 /** @private 291 Adds an entry to the foreground or background queue to load. If the 292 loader is not already running, start it as well. If the entry is in the 293 queue, but it is in the background queue, possibly move it to the 294 foreground queue. 295 */ 296 _scheduleImageEntry: function(entry, isBackgroundFlag) { 297 298 var background = this._backgroundQueue ; 299 var foreground = this._foregroundQueue ; 300 301 // if entry is loaded, nothing to do... 302 if (entry.status === this.IMAGE_LOADED) return this; 303 304 // if image is already in background queue, but now needs to be 305 // foreground, simply remove from background queue.... 306 if ((entry.status===this.IMAGE_QUEUED) && !isBackgroundFlag && entry.isBackground) { 307 background[background.indexOf(entry)] = null ; 308 entry.status = this.IMAGE_WAITING ; 309 } 310 311 // if image is not in queue already, add to queue. 312 if (entry.status!==this.IMAGE_QUEUED) { 313 var queue = (isBackgroundFlag) ? background : foreground ; 314 queue.push(entry); 315 entry.status = this.IMAGE_QUEUED ; 316 entry.isBackground = isBackgroundFlag ; 317 } 318 319 // if the image loader is not already running, start it... 320 if (!this.isLoading) this.invokeLater(this.loadNextImage, 100); 321 this.set('isLoading', YES); 322 323 return this ; // done! 324 }, 325 326 /** @private 327 Removes an entry from the foreground or background queue. 328 */ 329 _unscheduleImageEntry: function(entry) { 330 // if entry is not queued, do nothing 331 if (entry.status !== this.IMAGE_QUEUED) return this ; 332 333 var queue = entry.isBackground ? this._backgroundQueue : this._foregroundQueue ; 334 queue[queue.indexOf(entry)] = null; 335 336 // if entry is loading, abort it also. Call local abort method in-case 337 // browser decides not to follow up. 338 if (this._loading.indexOf(entry) >= 0) { 339 // In some cases queue.image is undefined. Is it ever defined? 340 if (queue.image) queue.image.abort(); 341 this.imageStatusDidChange(entry, this.ABORTED); 342 } 343 344 return this ; 345 }, 346 347 /** @private invoked by Image(). Note that this is the image instance */ 348 _imageDidAbort: function() { 349 SC.run(function() { 350 SC.imageQueue.imageStatusDidChange(this.entry, SC.imageQueue.ABORTED); 351 }, this); 352 }, 353 354 _imageDidError: function() { 355 SC.run(function() { 356 SC.imageQueue.imageStatusDidChange(this.entry, SC.imageQueue.ERROR); 357 }, this); 358 }, 359 360 _imageDidLoad: function() { 361 SC.run(function() { 362 SC.imageQueue.imageStatusDidChange(this.entry, SC.imageQueue.LOADED); 363 }, this); 364 }, 365 366 /** @private called whenever the image loading status changes. Notifies 367 items in the queue and then cleans up the entry. 368 */ 369 imageStatusDidChange: function(entry, status) { 370 if (!entry) return; // nothing to do... 371 372 var url = entry.url ; 373 374 // notify handlers. 375 var value ; 376 switch(status) { 377 case this.LOADED: 378 value = entry.image; 379 break; 380 case this.ABORTED: 381 value = SC.IMAGE_ABORTED_ERROR; 382 break; 383 case this.ERROR: 384 value = SC.IMAGE_FAILED_ERROR ; 385 break; 386 default: 387 value = SC.IMAGE_FAILED_ERROR ; 388 break; 389 } 390 entry.callbacks.forEach(function(x){ 391 var target = x[0], method = x[1]; 392 method.call(target, url, value); 393 },this); 394 395 // now clear callbacks so they aren't called again. 396 entry.callbacks = []; 397 398 // finally, if the image loaded OK, then set the status. Otherwise 399 // set it to waiting so that further attempts will load again 400 entry.status = (status === this.LOADED) ? this.IMAGE_LOADED : this.IMAGE_WAITING ; 401 402 // now cleanup image... 403 var image = entry.image ; 404 if (image) { 405 image.onload = image.onerror = image.onabort = null ; // no more notices 406 if (status !== this.LOADED) entry.image = null; 407 } 408 409 // remove from loading queue and periodically compact 410 this._loading[this._loading.indexOf(entry)]=null; 411 if (this._loading.length > this.loadLimit*2) { 412 this._loading = this._loading.compact(); 413 } 414 415 this.decrementProperty('activeRequests'); 416 this.loadNextImage() ; 417 }, 418 419 init: function() { 420 sc_super(); 421 this._images = {}; 422 this._loading = [] ; 423 this._foregroundQueue = []; 424 this._backgroundQueue = []; 425 }, 426 427 IMAGE_LOADED: "loaded", 428 IMAGE_QUEUED: "queued", 429 IMAGE_WAITING: "waiting", 430 431 ABORTED: 'aborted', 432 ERROR: 'error', 433 LOADED: 'loaded' 434 }); 435