import {URLRequest} from "../net/URLRequest";
import {AssetLibrary} from "../library/AssetLibrary";
import {AssetLibraryIterator} from "../library/AssetLibraryIterator";
import {Loader} from "../library/Loader";
import {LoaderContext} from "../library/LoaderContext";
import {ConflictPrecedence} from "../library/ConflictPrecedence";
import {ConflictStrategy} from "../library/ConflictStrategy";
import {ConflictStrategyBase} from "../library/ConflictStrategyBase";
import {AssetBase} from "../library/AssetBase";
import {IAsset} from "../library/IAsset";
import {ErrorBase} from "../errors/ErrorBase";
import {AssetEvent} from "../events/AssetEvent";
import {URLLoaderEvent} from "../events/URLLoaderEvent";
import {LoaderEvent} from "../events/LoaderEvent";
import {EventDispatcher} from "../events/EventDispatcher";
import {ParserEvent} from "../events/ParserEvent";
import {ParserBase} from "../parsers/ParserBase";
/**
* AssetLibraryBundle enforces a multiton pattern and is not intended to be instanced directly.
* Its purpose is to create a container for 3D data management, both before and after parsing.
* If you are interested in creating multiple library bundles, please use the getInstance() method.
*/
export class AssetLibraryBundle extends EventDispatcher
{
public static _iInstances:Object = new Object();
private _loaderSessions:Array;
private _strategy:ConflictStrategyBase;
private _strategyPreference:string;
private _assets:Array;
private _assetDictionary:Object;
private _assetDictDirty:boolean;
private _loaderSessionsGarbage:Array = new Array();
private _gcTimeoutIID;
private _onAssetRenameDelegate:(event:AssetEvent) => void;
private _onAssetConflictResolvedDelegate:(event:AssetEvent) => void;
private _onResourceCompleteDelegate:(event:LoaderEvent) => void;
private _onTextureSizeErrorDelegate:(event:LoaderEvent) => void;
private _onAssetCompleteDelegate:(event:AssetEvent) => void;
private _onLoadErrorDelegate:(event:URLLoaderEvent) => boolean;
private _onParseErrorDelegate:(event:ParserEvent) => boolean;
/**
* Creates a new AssetLibraryBundle object.
*
* @param me A multiton enforcer for the AssetLibraryBundle ensuring it cannnot be instanced.
*/
constructor()
{
super();
this._assets = new Array();//new Vector.;
this._assetDictionary = new Object();
this._loaderSessions = new Array();
this.conflictStrategy = ConflictStrategy.IGNORE.create();
this.conflictPrecedence = ConflictPrecedence.FAVOR_NEW;
this._onAssetRenameDelegate = (event:AssetEvent) => this.onAssetRename(event);
this._onAssetConflictResolvedDelegate = (event:AssetEvent) => this.onAssetConflictResolved(event);
this._onResourceCompleteDelegate = (event:LoaderEvent) => this.onResourceComplete(event);
this._onTextureSizeErrorDelegate = (event:LoaderEvent) => this.onTextureSizeError(event);
this._onAssetCompleteDelegate = (event:AssetEvent) => this.onAssetComplete(event);
this._onLoadErrorDelegate = (event:URLLoaderEvent) => this.onLoadError(event);
this._onParseErrorDelegate = (event:ParserEvent) => this.onParseError(event);
}
/**
* Returns an AssetLibraryBundle instance. If no key is given, returns the default bundle instance (which is
* similar to using the AssetLibraryBundle as a singleton.) To keep several separated library bundles,
* pass a string key to this method to define which bundle should be returned. This is
* referred to as using the AssetLibrary as a multiton.
*
* @param key Defines which multiton instance should be returned.
* @return An instance of the asset library
*/
public static getInstance(key:string = 'default'):AssetLibraryBundle
{
if (!key)
key = 'default';
if (!AssetLibraryBundle._iInstances.hasOwnProperty(key))
AssetLibraryBundle._iInstances[key] = new AssetLibraryBundle();
return AssetLibraryBundle._iInstances[key];
}
/**
*
*/
public enableParser(parserClass:Object):void
{
Loader.enableParser(parserClass);
}
/**
*
*/
public enableParsers(parserClasses:Object[]):void
{
Loader.enableParsers(parserClasses);
}
/**
* Defines which strategy should be used for resolving naming conflicts, when two library
* assets are given the same name. By default, ConflictStrategy.APPEND_NUM_SUFFIX
* is used which means that a numeric suffix is appended to one of the assets. The
* conflictPrecedence property defines which of the two conflicting assets will
* be renamed.
*
* @see naming.ConflictStrategy
* @see AssetLibrary.conflictPrecedence
*/
public get conflictStrategy():ConflictStrategyBase
{
return this._strategy;
}
public set conflictStrategy(val:ConflictStrategyBase)
{
if (!val)
throw new ErrorBase('namingStrategy must not be null. To ignore naming, use AssetLibrary.IGNORE');
this._strategy = val.create();
}
/**
* Defines which asset should have precedence when resolving a naming conflict between
* two assets of which one has just been renamed by the user or by a parser. By default
* ConflictPrecedence.FAVOR_NEW is used, meaning that the newly renamed
* asset will keep it's new name while the older asset gets renamed to not conflict.
*
* This property is ignored for conflict strategies that do not actually rename an
* asset automatically, such as ConflictStrategy.IGNORE and ConflictStrategy.THROW_ERROR.
*
* @see away.library.ConflictPrecedence
* @see away.library.ConflictStrategy
*/
public get conflictPrecedence():string
{
return this._strategyPreference;
}
public set conflictPrecedence(val:string)
{
this._strategyPreference = val;
}
/**
* Create an AssetLibraryIterator instance that can be used to iterate over the assets
* in this asset library instance. The iterator can filter assets on asset type and/or
* namespace. A "null" filter value means no filter of that type is used.
*
* @param assetTypeFilter Asset type to filter on (from the AssetType enum class.) Use
* null to not filter on asset type.
* @param namespaceFilter Namespace to filter on. Use null to not filter on namespace.
* @param filterFunc Callback function to use when deciding whether an asset should be
* included in the iteration or not. This needs to be a function that takes a single
* parameter of type IAsset and returns a boolean where true means it should be included.
*
* @see away.library.AssetType
*/
public createIterator(assetTypeFilter:string = null, namespaceFilter:string = null, filterFunc = null):AssetLibraryIterator
{
return new AssetLibraryIterator(this._assets, assetTypeFilter, namespaceFilter, filterFunc);
}
/**
* Loads a file and (optionally) all of its dependencies.
*
* @param req The URLRequest object containing the URL of the file to be loaded.
* @param context An optional context object providing additional parameters for loading
* @param ns An optional namespace string under which the file is to be loaded, allowing the differentiation of two resources with identical assets
* @param parser An optional parser object for translating the loaded data into a usable resource. If not provided, Loader will attempt to auto-detect the file type.
* @return A handle to the retrieved resource.
*/
public load(req:URLRequest, context:LoaderContext = null, ns:string = null, parser:ParserBase = null):void
{
this.getLoader().load(req, context, ns, parser);
}
/**
* Loads a resource from existing data in memory.
*
* @param data The data object containing all resource information.
* @param context An optional context object providing additional parameters for loading
* @param ns An optional namespace string under which the file is to be loaded, allowing the differentiation of two resources with identical assets
* @param parser An optional parser object for translating the loaded data into a usable resource. If not provided, Loader will attempt to auto-detect the file type.
* @return A handle to the retrieved resource.
*/
public loadData(data:any, context:LoaderContext = null, ns:string = null, parser:ParserBase = null):void
{
this.getLoader().loadData(data, '', context, ns, parser);
}
public getLoader():Loader
{
var loader:Loader = new Loader();
this._loaderSessions.push(loader);
loader.addEventListener(LoaderEvent.LOAD_COMPLETE, this._onResourceCompleteDelegate);
loader.addEventListener(AssetEvent.TEXTURE_SIZE_ERROR, this._onTextureSizeErrorDelegate);
loader.addEventListener(AssetEvent.ASSET_COMPLETE, this._onAssetCompleteDelegate);
// Error are handled separately (see documentation for addErrorHandler)
loader._iAddErrorHandler(this._onLoadErrorDelegate);
loader._iAddParseErrorHandler(this._onParseErrorDelegate);
return loader;
}
public disposeLoader(loader:Loader):void
{
var index:number = this._loaderSessions.indexOf(loader);
this._loaderSessions.splice(index, 1);
// Add loader to a garbage array - for a collection sweep and kill
this._loaderSessionsGarbage.push(loader);
this._gcTimeoutIID = setTimeout(() => {this.loaderSessionGC()}, 100);
}
/**
*
*/
public getAsset(name:string, ns:string = null):IAsset
{
if (this._assetDictDirty)
this.rehashAssetDict();
if (ns == null)
ns = AssetBase.DEFAULT_NAMESPACE;
if (!this._assetDictionary.hasOwnProperty(ns))
return null;
return this._assetDictionary[ns][name];
}
public getAllAssets():Array
{
return this._assets;
}
/**
* Adds an asset to the asset library, first making sure that it's name is unique
* using the method defined by the conflictStrategy and
* conflictPrecedence properties.
*/
public addAsset(asset:IAsset):void
{
var ns:string;
var old:IAsset;
// Bail if asset has already been added.
if (this._assets.indexOf(asset) >= 0)
return;
old = this.getAsset(asset.name, asset.assetNamespace);
ns = asset.assetNamespace || AssetBase.DEFAULT_NAMESPACE;
if (old != null)
this._strategy.resolveConflict(asset, old, this._assetDictionary[ns], this._strategyPreference);
//create unique-id (for now this is used in AwayBuilder only
//asset.id = IDUtil.createUID();
// Add it
this._assets.push(asset);
if (!this._assetDictionary.hasOwnProperty(ns))
this._assetDictionary[ns] = new Object();
this._assetDictionary[ns][asset.name] = asset;
asset.addEventListener(AssetEvent.RENAME, this._onAssetRenameDelegate);
asset.addEventListener(AssetEvent.ASSET_CONFLICT_RESOLVED, this._onAssetConflictResolvedDelegate);
}
/**
* Removes an asset from the library, and optionally disposes that asset by calling
* it's disposeAsset() method (which for most assets is implemented as a default
* version of that type's dispose() method.
*
* @param asset The asset which should be removed from this library.
* @param dispose Defines whether the assets should also be disposed.
*/
public removeAsset(asset:IAsset, dispose:boolean = true):void
{
var idx:number;
this.removeAssetFromDict(asset);
asset.removeEventListener(AssetEvent.RENAME, this._onAssetRenameDelegate);
asset.removeEventListener(AssetEvent.ASSET_CONFLICT_RESOLVED, this._onAssetConflictResolvedDelegate);
idx = this._assets.indexOf(asset);
if (idx >= 0)
this._assets.splice(idx, 1);
if (dispose)
asset.dispose();
}
/**
* Removes an asset which is specified using name and namespace.
*
* @param name The name of the asset to be removed.
* @param ns The namespace to which the desired asset belongs.
* @param dispose Defines whether the assets should also be disposed.
*
* @see away.library.AssetLibrary.removeAsset()
*/
public removeAssetByName(name:string, ns:string = null, dispose:boolean = true):IAsset
{
var asset:IAsset = this.getAsset(name, ns);
if (asset)
this.removeAsset(asset, dispose);
return asset;
}
/**
* Removes all assets from the asset library, optionally disposing them as they
* are removed.
*
* @param dispose Defines whether the assets should also be disposed.
*/
public removeAllAssets(dispose:boolean = true):void
{
if (dispose) {
var asset:IAsset;
var len:number = this._assets.length;
for (var c:number = 0; c < len; c++) {
asset = this._assets[c];
asset.dispose();
}
}
this._assets.length = 0;
this.rehashAssetDict();
}
/**
* Removes all assets belonging to a particular namespace (null for default)
* from the asset library, and optionall disposes them by calling their
* disposeAsset() method.
*
* @param ns The namespace from which all assets should be removed.
* @param dispose Defines whether the assets should also be disposed.
*
* @see away.library.AssetLibrary.removeAsset()
*/
public removeNamespaceAssets(ns:string = null, dispose:boolean = true):void
{
var idx:number = 0;
var asset:IAsset;
var old_assets:IAsset[];
// Empty the assets vector after having stored a copy of it.
// The copy will be filled with all assets which weren't removed.
old_assets = this._assets.concat();
this._assets.length = 0;
if (ns == null)
ns = AssetBase.DEFAULT_NAMESPACE;
var len:number = old_assets.length;
for (var d:number = 0; d < len; d++) {
asset = old_assets[d];
// Remove from dict if in the supplied namespace. If not,
// transfer over to the new vector.
if (asset.assetNamespace == ns) {
if (dispose)
asset.dispose();
// Remove asset from dictionary, but don't try to auto-remove
// the namespace, which will trigger an unnecessarily expensive
// test that is not needed since we know that the namespace
// will be empty when loop finishes.
this.removeAssetFromDict(asset, false);
} else {
this._assets[idx++] = asset;
}
}
/*
for each (asset in old_assets) {
// Remove from dict if in the supplied namespace. If not,
// transfer over to the new vector.
if (asset.assetNamespace == ns) {
if (dispose)
asset.dispose();
// Remove asset from dictionary, but don't try to auto-remove
// the namespace, which will trigger an unnecessarily expensive
// test that is not needed since we know that the namespace
// will be empty when loop finishes.
removeAssetFromDict(asset, false);
} else
_assets[idx++] = asset;
}
*/
// Remove empty namespace
if (this._assetDictionary.hasOwnProperty(ns))
delete this._assetDictionary[ns];
}
private removeAssetFromDict(asset:IAsset, autoRemoveEmptyNamespace:boolean = true):void
{
if (this._assetDictDirty)
this.rehashAssetDict();
if (this._assetDictionary.hasOwnProperty(asset.assetNamespace)) {
if (this._assetDictionary[asset.assetNamespace].hasOwnProperty(asset.name))
delete this._assetDictionary[asset.assetNamespace][asset.name];
if (autoRemoveEmptyNamespace) {
var key:string;
var empty:boolean = true;
for (key in this._assetDictionary[asset.assetNamespace]) {
empty = false;
break;
}
if (empty)
delete this._assetDictionary[asset.assetNamespace];
}
}
}
public stopAllLoaders():void
{
var len:number = this._loaderSessions.length;
for (var i:number = 0; i < len; i++)
this.killloaderSession(this._loaderSessions[i]);
this._loaderSessions = new Array();
}
private rehashAssetDict():void
{
var asset:IAsset;
this._assetDictionary = {};
var len:number = this._assets.length;
for (var c:number = 0; c < len; c++) {
asset = this._assets[c];
if (!this._assetDictionary.hasOwnProperty(asset.assetNamespace))
this._assetDictionary[asset.assetNamespace] = {};
this._assetDictionary[asset.assetNamespace][asset.name] = asset;
}
this._assetDictDirty = false;
}
/**
* Called when a an error occurs during loading.
*/
private onLoadError(event:URLLoaderEvent):boolean
{
if (this.hasEventListener(URLLoaderEvent.LOAD_ERROR)) {
this.dispatchEvent(event);
return true;
} else {
return false;
}
}
/**
* Called when a an error occurs during parsing.
*/
private onParseError(event:ParserEvent):boolean
{
if (this.hasEventListener(ParserEvent.PARSE_ERROR)) {
this.dispatchEvent(event);
return true;
} else {
return false;
}
}
private onAssetComplete(event:AssetEvent):void
{
// Only add asset to library the first time.
if (event.type == AssetEvent.ASSET_COMPLETE)
this.addAsset(event.asset);
this.dispatchEvent(event);
}
private onTextureSizeError(event:LoaderEvent):void
{
this.dispatchEvent(event);
}
/**
* Called when the resource and all of its dependencies was retrieved.
*/
private onResourceComplete(event:LoaderEvent):void
{
var loader:Loader = event.target;
this.dispatchEvent(event);
this.disposeLoader(loader);
}
private loaderSessionGC():void
{
var loader:Loader;
while (this._loaderSessionsGarbage.length > 0) {
loader = this._loaderSessionsGarbage.pop();
this.killloaderSession(loader);
}
clearTimeout(this._gcTimeoutIID);
this._gcTimeoutIID = null;
}
private killloaderSession(loader:Loader):void
{
loader.removeEventListener(LoaderEvent.LOAD_COMPLETE, this._onResourceCompleteDelegate);
loader.removeEventListener(AssetEvent.TEXTURE_SIZE_ERROR, this._onTextureSizeErrorDelegate);
loader.removeEventListener(AssetEvent.ASSET_COMPLETE, this._onAssetCompleteDelegate);
loader.stop();
}
private onAssetRename(event:AssetEvent):void
{
var asset:IAsset = event.target;// TODO: was ev.currentTarget - watch this var
var old:IAsset = this.getAsset(asset.assetNamespace, asset.name);
if (old != null) {
this._strategy.resolveConflict(asset, old, this._assetDictionary[asset.assetNamespace], this._strategyPreference);
} else {
var dict:Object = this._assetDictionary[event.asset.assetNamespace];
if (dict == null)
return;
dict[event.prevName] = null;
dict[event.asset.name] = event.asset;
}
}
private onAssetConflictResolved(event:AssetEvent):void
{
this.dispatchEvent(event.clone());
}
}