package com.exanimo.net
{
import com.exanimo.events.LoadQueueEvent;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.display.Loader;
import flash.display.LoaderInfo;
import flash.net.URLLoader;
import flash.utils.clearTimeout;
import flash.utils.Dictionary;
import flash.utils.setTimeout;
/**
*
* Defines a LoadQueue, which allows you to defer the loading of objects.
*
* @langversion ActionScript 3
* @playerversion Flash 9.0.0
*
* @author Matthew Tretter (matthew@exanimo.com)
* @author Eric Eldredge
* @since 2007.10.03
*
*/
public class LoadQueue extends EventDispatcher
{
private var _currentItems:Array;
private var _isLoading:Boolean;
private var _isOpen:Boolean;
private var _loadArgsList:Dictionary;
private var _loadNextTimeoutID:uint;
private var _maxConnections:int;
private var _parent:Object;
private var _queue:Array;
/**
*
* Constructs a new LoadQueue.
*
*/
public function LoadQueue()
{
this._currentItems = [];
this._queue = [];
this._loadArgsList = new Dictionary(true);
this._maxConnections = -1;
}
//
// accessors
//
/**
*
* The number of files that this queue is allowed to load
* simultaneously. If this LoadQueue belongs to another, the default
* number of maximum connections will be as many as the parent queue
* allows. Otherwise, the default is one.
*
*/
public function get maxConnections():int
{
return this._maxConnections;
}
/**
* @private
*/
public function set maxConnections(maxConnections:int):void
{
// TODO: test this. I think it's broken.
if (maxConnections < -1)
{
throw new RangeError('LoadQueue.maxConnections must be greater than or equal to -1.');
}
this._maxConnections = maxConnections;
if (this._isLoading)
{
this._loadNext();
}
}
/**
*
* The number of files currently being loaded.
*
* @default 0
*
*/
public function get numConnections():uint
{
var numConnections:uint = 0;
var item:Object;
for (var i:uint = 0; i < this.numItems; i++)
{
item = this.getItemAt(i);
numConnections += item is LoadQueue ? item.numConnections : this._currentItems.indexOf(item) == - 1 ? 0 : 1;
}
return numConnections;
}
/**
*
* The total number of assets in this LoadQueue tree. This number is
* calculated by adding the number of non-LoadQueue items in this queue
* to the number of non-LoadQueue items in all descendant queues.
*
* @default 0
*
*/
public function get numAssets():int
{
return this._getNumAssets(this);
}
/**
*
* The number of items in the queue. This number includes child
* LoadQueues, so it may be misleading. For example, a LoadQueue may
* contain another LoadQueue which in turn contains 10 URLLoaders. The
* parent's numItems is 1.
*
* @default 0
*
* @see #numAssets
*
*/
public function get numItems():int
{
return this._queue.length;
}
/**
*
* The LoadQueue to which this one belongs.
*
* @default null
*
*/
public function get parent():Object
{
return this._parent;
}
//
// public methods
//
/**
*
* @throws ArgumentError
* thrown if item does not implement load and
* close methods
*
* @inheritDoc
*
*/
public function addItem(item:Object):void
{
this._addItemAt(item, this.numItems);
}
/**
*
* @throws ArgumentError
* thrown if item does not implement load and
* close methods
*
* @inheritDoc
*
*/
public function addItemAt(item:Object, index:int):void
{
this._addItemAt(item, index);
}
/**
*
* Stops the loading process. The close() method will be
* called on all loading items in the queue.
*
*/
public function close():void
{
this._isLoading = false;
while (this._currentItems.length)
{
var item:Object = this._currentItems.pop();
try
{
item.close();
}
catch (error:Error)
{
// The item hasn't been opened yet.
// TODO: When the item opens, close it.
}
}
clearTimeout(this._loadNextTimeoutID);
}
/**
* @inheritDoc
*/
public function getItemAt(index:int):Object
{
if (index >= this.numItems || index < 0)
{
throw new RangeError('The specified index is greater than or equal to the number of items in the LoadQueue.');
}
return this._queue[index];
}
/**
* @inheritDoc
*/
public function getItemIndex(item:Object):int
{
return this._getItemIndex(item);
}
/**
*
* Gets the load arguments for the specified loader object.
*
* @see #setLoadArguments
*
* @return
* An array containing all of the arguments that were assigned to
* this object using setLoadArguments
*
*/
public function getLoadArguments(item:Object):Array
{
return this._loadArgsList[item] ? this._loadArgsList[item].slice() : null;
}
/**
*
* Begins loading the queue.
*
*/
public function load():void
{
if (this._isLoading) return;
this._isLoading = true;
this._loadNext();
}
/**
* @inheritDoc
*/
public function removeAll():void
{
while (this.numItems)
{
this.removeItemAt(0);
}
}
/**
* @inheritDoc
*/
public function removeItem(item:Object):Object
{
var itemIndex:int = this._getItemIndex(item)
if (itemIndex == -1)
{
throw new ArgumentError('The item you are attempting to remove is not present in the LoadQueue.');
}
return this._removeItemAt(itemIndex);
}
/**
* @inheritDoc
*/
public function removeItemAt(index:int):Object
{
if (index >= this.numItems || index < 0)
{
throw new RangeError('The supplied index is out of bounds.');
}
return this._removeItemAt(index);
}
/**
* @inheritDoc
*/
public function replaceItem(newItem:Object, oldItem:Object):Object
{
return this.replaceItemAt(newItem, this.getItemIndex(oldItem));
}
/**
* @inheritDoc
*/
public function replaceItemAt(newItem:Object, index:uint):Object
{
var oldItem:Object = this.removeItemAt(index);
this.addItemAt(newItem, index);
return oldItem;
}
/**
* @inheritDoc
*/
public function setItemAt(item:Object, index:int):Object
{
// TODO: implement this method!
return {};
}
/**
*
* Sets the load arguments for the specified loader object. These
* arguments will be passed to the items's load method
* when it comes time to load the item. Typically, the load arguments
* will include a URLRequest, however it may not if the item is a
* custom loader object.
*
* @see #getLoadArguments
*
* @param item
* The loader object to set the load arguments for.
* @param ...rest
* A list of arguments to be passed to the item's load
* method.
*
*/
public function setLoadArguments(item:Object, ...loadArgs:Array):void
{
this._loadArgsList[item] = loadArgs;
}
/**
* @inheritDoc
*/
public function toArray():Array
{
return this._queue.slice();
}
//
// private methods
//
/**
*
* Common implementation for adding items. All functions that add items
* to the queue do so by calling this method.
*
*/
private function _addItemAt(item:Object, index:uint):void
{
// Make sure that the object implements load() and close().
if (!(item.hasOwnProperty('load') && (typeof item.load == 'function') && item.hasOwnProperty('close') && (typeof item.close == 'function')))
{
throw new ArgumentError('You cannot add an item to the LoadQueue unless it implements load() and close().');
}
// Make sure that the index isn't out of bounds.
if ((index < 0) || (index > this.numItems))
{
throw new RangeError('The specified index does not exist in the LoadQueue.');
}
// If you were loading when you added the item, you should resume
// loading after it's added.
var continueLoading:Boolean = this._isLoading;
// If the item is a LoadQueue, point its parent property to this
// LoadQueue. If it already has a parent, remove it from that
// first.
var lq:LoadQueue;
if ((lq = item as LoadQueue))
{
if (lq.parent && (lq.parent != this))
{
lq.parent.removeItem(lq);
}
lq._parent = this;
}
// Remove the item if it already exists in the queue.
var currentIndex:int = this._getItemIndex(item);
if (currentIndex != -1)
{
if (currentIndex == index) return;
this._removeItemAt(currentIndex, false);
}
// Add the item to the queue.
this._queue.splice(index, 0, item);
if (continueLoading)
{
this._scheduleLoadNext();
}
}
/**
*
* Removes the loaded item from the queue and calls _loadNext to
* continue the loading process. Called when an item in the queue
* finishes loading.
*
* @param e:Event
* the event that triggered the handler
*
*/
private function _completeHandler(e:Event):void
{
var item:Object = e.currentTarget is LoaderInfo ? e.currentTarget.loader : e.currentTarget;
delete this._loadArgsList[item];
this.removeItem(item);
// "Bubble" the ASSET_COMPLETE event
var tmp:LoadQueue = this;
while (tmp)
{
tmp.dispatchEvent(new LoadQueueEvent(LoadQueueEvent.ASSET_COMPLETE, false, false, item, this));
tmp = tmp._parent as LoadQueue;
}
if (!this.numItems)
{
// All the items in the queue have completed.
this._isLoading = false;
this._isOpen = false;
this.dispatchEvent(new Event(Event.COMPLETE));
}
this._loadNext();
}
/**
*
* Removes the loaded item from the queue and calls _loadNext to
* continue the loading process. Called when an item in the queue
* errors.
*
* @param e:Event
* the event that triggered the handler
*
*/
private function _errorHandler(e:Event):void
{
// TODO: Better error handling than just skipping the item!!
// TODO: This code is duplicated in _completeHandler. Centralize!
trace(e['text'] || e);
var item:Object;
try
{
item = e.currentTarget is LoaderInfo ? e.currentTarget.loader : e.currentTarget;
}
catch (error:Error)
{
// The loader could not be accessed. Search the LoadQueue.
for (var i:uint = 0; i < this.numItems; i++)
{
var tmp:Loader;
if ((tmp = this.getItemAt(i) as Loader) && (tmp.contentLoaderInfo == e.currentTarget))
{
item = tmp;
break;
}
}
}
delete this._loadArgsList[item];
this.removeItem(item);
if (!this.numItems)
{
// All the items in the queue have completed.
this._isLoading = false;
this._isOpen = false;
this.dispatchEvent(new Event(Event.COMPLETE));
}
this._loadNext();
}
/**
*
* Gets the event dispatcher that dispatches the COMPLETE event for the
* provided object. This is necessary because Loader objects don't
* dispatch their own events.
*
* @param item
* the object whose dispatcher to get
*
* @return IEventDispatcher
* the IEventDispatcher that will dispatch a COMPLETE event when the
* supplied object has finished loading.
*
*/
private function _getDispatcher(item:Object):IEventDispatcher
{
return (item is Loader ? item.contentLoaderInfo : item) as IEventDispatcher;
}
/**
*
*
*/
private function _getItemIndex(item:Object):int
{
return this._queue.indexOf(item);
}
/**
*
*
*
*/
private function _getNumAssets(lq:LoadQueue):uint
{
var numAssets:int = 0;
for (var i:uint = 0; i < lq.numItems; i++)
{
var item:Object = lq.getItemAt(i);
numAssets += item is LoadQueue ? this._getNumAssets(item as LoadQueue) : 1;
}
return numAssets;
}
/**
*
* Returns the number of connections that are currently allowed.
*
*/
private function _getNumAllowedConnections():uint
{
var numAllowedConnections:uint;
if (!this.parent)
{
numAllowedConnections = Math.max(0, (this.maxConnections == -1 ? 1 : this.maxConnections) - this.numConnections);
}
else
{
var parentsNumAllowedConnections:uint = this.parent._getNumAllowedConnections();
numAllowedConnections = Math.min((this.maxConnections == -1 ? uint.MAX_VALUE : this.maxConnections), parentsNumAllowedConnections);
}
return numAllowedConnections;
}
/**
*
* Loads the next asset in the tree.
*
*/
private function _loadNext():void
{
// Make sure this queue is loading before trying to load the next
// loader. (This method may be called on a non-loading queue if the
// loader is also in another, loading queue)
if (!this._isLoading) return;
for each (var item:Object in this._queue)
{
if (!this._getNumAllowedConnections())
{
break;
}
var doLoad:Boolean = item is LoadQueue;
if (this._currentItems.indexOf(item) == -1)
{
this._currentItems.push(item);
doLoad = true;
}
if (doLoad)
{
// Add listeners
var dispatcher:IEventDispatcher = this._getDispatcher(item);
dispatcher.addEventListener(Event.OPEN, this._openHandler, false, int.MAX_VALUE, true);
dispatcher.addEventListener(Event.COMPLETE, this._completeHandler, false, int.MAX_VALUE, true);
dispatcher.addEventListener(IOErrorEvent.IO_ERROR, this._errorHandler, false, int.MAX_VALUE, true);
dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, this._errorHandler, false, int.MAX_VALUE, true);
// Load the item.
var args:Array = this.getLoadArguments(item) || [];
args.unshift(item);
this.loadItemNow.apply(null, args);
if (!this._isOpen)
{
// Dispatch an OPEN event on the LoadQueue.
this._isOpen = true;
this.dispatchEvent(new Event(Event.OPEN));
}
// "Bubble" the ASSET_OPEN event
var tmp:LoadQueue = this;
while (tmp)
{
tmp.dispatchEvent(new LoadQueueEvent(LoadQueueEvent.ASSET_OPEN, false, false, item, this));
tmp = tmp._parent as LoadQueue;
}
}
}
}
/**
*
* Dispatches an OPEN event when the first item of each loading process
* fires its OPEN event.
*
* @param e
* the event that triggered the handler
*
*/
private function _openHandler(e:Event):void
{
// We re-add the COMPLETE listener in the open handler so that
// externally added COMPLETE listeners are called in the correct
// order. (We must ALSO add it in _addItemAt so that things that
// COMPLETE events are still caught for items that don't dispatch
// OPEN events.)
e.currentTarget.addEventListener(Event.COMPLETE, this._completeHandler, false, int.MAX_VALUE, true);
}
/**
*
* Common implementation for removing items. All functions that remove
* items do so by calling this function.
*
*/
private function _removeItemAt(index:int, continueLoading:Boolean = true):Object
{
// Remove the item from the queue.
var item:Object = this.getItemAt(index);
continueLoading = continueLoading && this._isLoading;
var dispatcher:IEventDispatcher = this._getDispatcher(item);
this._queue.splice(index, 1);
// Remove the item from the list of current items.
var itemIndex:int = this._currentItems.indexOf(item);
if (itemIndex != -1)
{
this._currentItems.splice(itemIndex, 1);
}
// If the item is a LoadQueue, null its parent property.
var lq:LoadQueue;
if ((lq = item as LoadQueue))
{
lq._parent = null;
}
dispatcher.removeEventListener(Event.COMPLETE, this._completeHandler);
dispatcher.removeEventListener(Event.OPEN, this._openHandler);
dispatcher.removeEventListener(IOErrorEvent.IO_ERROR, this._errorHandler);
dispatcher.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, this._errorHandler);
if (continueLoading)
{
this._loadNext();
}
// "Bubble" the ASSET_REMOVED event
var tmp:LoadQueue = this;
while (tmp)
{
tmp.dispatchEvent(new LoadQueueEvent(LoadQueueEvent.ASSET_REMOVED, false, false, item, this));
tmp = tmp._parent as LoadQueue;
}
return item;
}
/**
*
* Schedules the next item int he queue for loading. This is done to
* give the user time to set the load arguments.
*
*/
private function _scheduleLoadNext():void
{
if (!this._isLoading) return;
this._loadNextTimeoutID = setTimeout(this._loadNext, 0);
}
//
// protected methods
//
/**
*
* @private
* Loads an item.
*
*/
protected function loadItemNow(item:Object, ...loadArgs:Array):void
{
item.load.apply(null, loadArgs);
}
}
}