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 { EventBase } from '../events/EventBase'; import { URLRequest } from '../net/URLRequest'; import { ParserBase } from '../parsers/ParserBase'; import { AssetBase } from './AssetBase'; import { AssetLibraryIterator } from './AssetLibraryIterator'; import { ConflictPrecedence } from './ConflictPrecedence'; import { ConflictStrategy } from './ConflictStrategy'; import { ConflictStrategyBase } from './ConflictStrategyBase'; import { IAsset } from './IAsset'; import { IAssetAdapter } from './IAssetAdapter'; import { Loader } from './Loader'; import { LoaderContext } from './LoaderContext'; /** * 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 _onAssetRenameDelegate: (event: AssetEvent) => void; private _onAssetConflictResolvedDelegate: (event: AssetEvent) => void; private _onLoaderStartDelegate: (event: LoaderEvent) => void; private _onLoaderCompleteDelegate: (event: LoaderEvent) => void; private _onTextureSizeErrorDelegate: (event: LoaderEvent) => void; private _onAssetCompleteDelegate: (event: AssetEvent) => void; private _onLoadErrorDelegate: (event: URLLoaderEvent) => void; private _onParseErrorDelegate: (event: ParserEvent) => void; private _errorDelegateSelector: {[index: string]: ((event: EventBase) => void)}; /** * 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._onLoaderStartDelegate = (event: LoaderEvent) => this._onLoaderStart(event); this._onLoaderCompleteDelegate = (event: LoaderEvent) => this._onLoaderComplete(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); this._errorDelegateSelector = { [URLLoaderEvent.LOAD_ERROR]: this._onLoadErrorDelegate, [ParserEvent.PARSE_ERROR]: this._onParseErrorDelegate }; } /** * Special addEventListener case for URLLoaderEvent.LOAD_ERROR * and ype == ParserEvent.PARSE_ERROR * * @param type * @param listener */ public addEventListener(type: string, listener: (event: EventBase) => void): void { if (type == URLLoaderEvent.LOAD_ERROR || type == ParserEvent.PARSE_ERROR) for (let i: number; i < this._loaderSessions.length; i++) this._loaderSessions[i].addEventListener(type, this._errorDelegateSelector[type]); super.addEventListener(type, listener); } /** * Special removeEventListener case for * URLLoaderEvent.LOAD_ERROR and ype == * ParserEvent.PARSE_ERROR * * @param type * @param listener */ public removeEventListener(type: string, listener: (event: EventBase) => void): void { if (type == URLLoaderEvent.LOAD_ERROR || type == ParserEvent.PARSE_ERROR) for (let i: number; i < this._loaderSessions.length; i++) this._loaderSessions[i].removeEventListener(type, this._errorDelegateSelector[type]); super.removeEventListener(type, listener); } /** * 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 { const loader: Loader = new Loader(); this._loaderSessions.push(loader); loader.addEventListener(LoaderEvent.LOADER_START, this._onLoaderStartDelegate); loader.addEventListener(LoaderEvent.LOADER_COMPLETE, this._onLoaderCompleteDelegate); loader.addEventListener(AssetEvent.TEXTURE_SIZE_ERROR, this._onTextureSizeErrorDelegate); loader.addEventListener(AssetEvent.ASSET_COMPLETE, this._onAssetCompleteDelegate); if (this.hasEventListener(URLLoaderEvent.LOAD_ERROR)) loader.addEventListener(URLLoaderEvent.LOAD_ERROR, this._onLoadErrorDelegate); if (this.hasEventListener(ParserEvent.PARSE_ERROR)) loader.addEventListener(ParserEvent.PARSE_ERROR, this._onParseErrorDelegate); return loader; } public stopLoader(loader: Loader): void { const index: number = this._loaderSessions.indexOf(loader); if (index == -1) throw new Error('loader is not an active session'); this._loaderSessions.splice(index, 1); this._killLoaderSession(loader); } /** * */ public getAsset(name: string, ns: string = null): IAssetAdapter { if (this._assetDictDirty) this.rehashAssetDict(); if (ns == null) ns = AssetBase.DEFAULT_NAMESPACE; if (!this._assetDictionary.hasOwnProperty(ns)) return null; return this._assetDictionary[ns][name.toString().toLowerCase()]; } 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: IAssetAdapter): void { // Bail if asset has already been added. if (this._assets.indexOf(asset) >= 0) return; const old: IAssetAdapter = this.getAsset(asset.adaptee.name, asset.adaptee.assetNamespace); const ns: string = asset.adaptee.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.adaptee.name.toString().toLowerCase()] = asset; asset.adaptee.addEventListener(AssetEvent.RENAME, this._onAssetRenameDelegate); asset.adaptee.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: IAssetAdapter, dispose: boolean = true): void { this.removeAssetFromDict(asset); asset.adaptee.removeEventListener(AssetEvent.RENAME, this._onAssetRenameDelegate); asset.adaptee.removeEventListener(AssetEvent.ASSET_CONFLICT_RESOLVED, this._onAssetConflictResolvedDelegate); const idx: number = 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): IAssetAdapter { const asset: IAssetAdapter = 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) { let asset: IAssetAdapter; const len: number = this._assets.length; for (let 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 { let idx: number = 0; let asset: IAssetAdapter; // Empty the assets vector after having stored a copy of it. // The copy will be filled with all assets which weren't removed. const old_assets: IAssetAdapter[] = this._assets.concat(); this._assets.length = 0; if (ns == null) ns = AssetBase.DEFAULT_NAMESPACE; const len: number = old_assets.length; for (let 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.adaptee.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: IAssetAdapter, autoRemoveEmptyNamespace: boolean = true): void { if (this._assetDictDirty) this.rehashAssetDict(); const assetNamespace: string = asset.adaptee.assetNamespace; if (this._assetDictionary.hasOwnProperty(assetNamespace)) { if (this._assetDictionary[assetNamespace].hasOwnProperty(asset.adaptee.name.toString().toLowerCase())) delete this._assetDictionary[assetNamespace][asset.adaptee.name.toString().toLowerCase()]; if (autoRemoveEmptyNamespace && !Object.keys(this._assetDictionary[assetNamespace]).length) delete this._assetDictionary[assetNamespace]; } } public stop(): void { const len: number = this._loaderSessions.length; for (let i: number = 0; i < len; i++) this._killLoaderSession(this._loaderSessions[i]); this._loaderSessions = new Array(); } private rehashAssetDict(): void { let asset: IAssetAdapter; let assetNamespace: string; this._assetDictionary = {}; const len: number = this._assets.length; for (let c: number = 0; c < len; c++) { asset = this._assets[c]; assetNamespace = asset.adaptee.assetNamespace; if (!this._assetDictionary.hasOwnProperty(assetNamespace)) this._assetDictionary[assetNamespace] = {}; this._assetDictionary[assetNamespace][asset.adaptee.name.toString().toLowerCase()] = asset; } this._assetDictDirty = false; } /** * Called when a an error occurs during loading. */ private _onLoadError(event: URLLoaderEvent): void { this.dispatchEvent(event); } /** * Called when a an error occurs during parsing. */ private _onParseError(event: ParserEvent): void { this.dispatchEvent(event); } private _onAssetComplete(event: AssetEvent): void { this.addAsset(event.asset.adapter); this.dispatchEvent(event); } private _onTextureSizeError(event: LoaderEvent): void { this.dispatchEvent(event); } private _onLoaderStart(event: LoaderEvent): void { this.dispatchEvent(event); } /** * Called when the resource and all of its dependencies was retrieved. */ private _onLoaderComplete(event: LoaderEvent): void { this.stopLoader(event.target); this.dispatchEvent(event); } private _killLoaderSession(loader: Loader): void { loader.removeEventListener(LoaderEvent.LOADER_START, this._onLoaderStartDelegate); loader.removeEventListener(LoaderEvent.LOADER_COMPLETE, this._onLoaderCompleteDelegate); loader.removeEventListener(AssetEvent.TEXTURE_SIZE_ERROR, this._onTextureSizeErrorDelegate); loader.removeEventListener(AssetEvent.ASSET_COMPLETE, this._onAssetCompleteDelegate); if (this.hasEventListener(URLLoaderEvent.LOAD_ERROR)) loader.removeEventListener(URLLoaderEvent.LOAD_ERROR, this._onLoadErrorDelegate); if (this.hasEventListener(ParserEvent.PARSE_ERROR)) loader.removeEventListener(ParserEvent.PARSE_ERROR, this._onParseErrorDelegate); loader.stop(); } private _onAssetRename(event: AssetEvent): void { const asset: IAssetAdapter = ( event.target).adapter;// TODO: was ev.currentTarget - watch this var const old: IAssetAdapter = this.getAsset(asset.adaptee.assetNamespace, asset.adaptee.name); if (old != null) { this._strategy.resolveConflict(asset, old, this._assetDictionary[asset.adaptee.assetNamespace], this._strategyPreference); } else { const dict: Object = this._assetDictionary[event.asset.assetNamespace]; if (dict == null) return; dict[event.prevName] = null; dict[event.asset.name.toString().toLowerCase()] = event.asset; } } private _onAssetConflictResolved(event: AssetEvent): void { this.dispatchEvent(event.clone()); } }