Source: asset/asset-registry.js

Object.assign(pc, function () {
    /**
     * @constructor
     * @name pc.AssetRegistry
     * @classdesc Container for all assets that are available to this application
     * @description Create an instance of an AssetRegistry.
     * Note: PlayCanvas scripts are provided with an AssetRegistry instance as 'app.assets'.
     * @param {pc.ResourceLoader} loader The ResourceLoader used to load the asset files.
     * @property {String} prefix A URL prefix that will be added to all asset loading requests.
     */
    var AssetRegistry = function (loader) {
        this._loader = loader;

        this._assets = []; // list of all assets
        this._cache = {}; // index for looking up assets by id
        this._names = {}; // index for looking up assets by name
        this._tags = new pc.TagsCache('_id'); // index for looking up by tags
        this._urls = {}; // index for looking up assets by url

        this.prefix = null;

        pc.events.attach(this);
    };

    /**
     * @event
     * @name pc.AssetRegistry#load
     * @description Fired when an asset completes loading
     * @param {pc.Asset} asset The asset that has just loaded
     * @example
     * app.assets.on("load", function (asset) {
     *     console.log("asset loaded: " + asset.name);
     * });
     */

    /**
     * @event
     * @name pc.AssetRegistry#load:[id]
     * @description Fired when an asset completes loading
     * @param {pc.Asset} asset The asset that has just loaded
     * @example
     * var id = 123456;
     * var asset = app.assets.get(id);
     * app.assets.on("load:" + id, function (asset) {
     *     console.log("asset loaded: " + asset.name);
     * });
     * app.assets.load(asset);
     */

    /**
     * @event
     * @name pc.AssetRegistry#load:url:[url]
     * @description Fired when an asset completes loading
     * @param {pc.Asset} asset The asset that has just loaded
     * @example
     * var id = 123456;
     * var asset = app.assets.get(id);
     * app.assets.on("load:url:" + asset.file.url, function (asset) {
     *     console.log("asset loaded: " + asset.name);
     * });
     * app.assets.load(asset);
     */

    /**
     * @event
     * @name pc.AssetRegistry#add
     * @description Fired when an asset is added to the registry
     * @param {pc.Asset} asset The asset that was added
     * @example
     * app.assets.on("add", function (asset) {
     *     console.log("New asset added: " + asset.name);
     * });
     */

    /**
     * @event
     * @name pc.AssetRegistry#add:[id]
     * @description Fired when an asset is added to the registry
     * @param {pc.Asset} asset The asset that was added
     * @example
     * var id = 123456;
     * app.assets.on("add:" + id, function (asset) {
     *     console.log("Asset 123456 loaded");
     * });
     */

    /**
     * @event
     * @name pc.AssetRegistry#add:url:[url]
     * @description Fired when an asset is added to the registry
     * @param {pc.Asset} asset The asset that was added
     */

    /**
     * @event
     * @name pc.AssetRegistry#remove
     * @description Fired when an asset is removed from the registry
     * @param {pc.Asset} asset The asset that was removed
     * @example
     * app.assets.on("remove", function (aseet) {
     *     console.log("Asset removed: " + asset.name);
     * });
     */

    /**
     * @event
     * @name pc.AssetRegistry#remove:[id]
     * @description Fired when an asset is removed from the registry
     * @param {pc.Asset} asset The asset that was removed
     * @example
     * var id = 123456;
     * app.assets.on("remove:" + id, function (asset) {
     *     console.log("Asset removed: " + asset.name);
     * });
     */

    /**
     * @event
     * @name pc.AssetRegistry#remove:url:[url]
     * @description Fired when an asset is removed from the registry
     * @param {pc.Asset} asset The asset that was removed
     */

    /**
     * @event
     * @name pc.AssetRegistry#error
     * @description Fired when an error occurs during asset loading
     * @param {String} err The error message
     * @param {pc.Asset} asset The asset that generated the error
     * @example
     * var id = 123456;
     * var asset = app.assets.get(id);
     * app.assets.on("error", function (err, asset) {
     *     console.error(err);
     * });
     * app.assets.load(asset);
     */

    /**
     * @event
     * @name pc.AssetRegistry#error:[id]
     * @description Fired when an error occurs during asset loading
     * @param {pc.Asset} asset The asset that generated the error
     * @example
     * var id = 123456;
     * var asset = app.assets.get(id);
     * app.assets.on("error:" + id, function (err, asset) {
     *     console.error(err);
     * });
     * app.assets.load(asset);
     */

    Object.assign(AssetRegistry.prototype, {
        /**
         * @function
         * @name pc.AssetRegistry#list
         * @description Create a filtered list of assets from the registry
         * @param {Object} filters Properties to filter on, currently supports: 'preload: true|false'
         * @returns {pc.Asset[]} The filtered list of assets.
         */
        list: function (filters) {
            filters = filters || {};
            return this._assets.filter(function (asset) {
                var include = true;
                if (filters.preload !== undefined) {
                    include = (asset.preload === filters.preload);
                }
                return include;
            });
        },

        /**
         * @function
         * @name pc.AssetRegistry#add
         * @description Add an asset to the registry
         * @param {pc.Asset} asset The asset to add
         * @example
         * var asset = new pc.Asset("My Asset", "texture", {url: "../path/to/image.jpg"});
         * app.assets.add(asset);
         */
        add: function (asset) {
            var index = this._assets.push(asset) - 1;
            var url;

            // id cache
            this._cache[asset.id] = index;
            if (!this._names[asset.name])
                this._names[asset.name] = [];

            // name cache
            this._names[asset.name].push(index);
            if (asset.file) {
                url = asset.file.url;
                this._urls[url] = index;
            }
            asset.registry = this;

            // tags cache
            this._tags.addItem(asset);
            asset.tags.on('add', this._onTagAdd, this);
            asset.tags.on('remove', this._onTagRemove, this);

            this.fire("add", asset);
            this.fire("add:" + asset.id, asset);
            if (url)
                this.fire("add:url:" + url, asset);

            if (asset.preload)
                this.load(asset);
        },

        /**
         * @function
         * @name pc.AssetRegistry#remove
         * @description Remove an asset from the registry
         * @param {pc.Asset} asset The asset to remove
         * @returns {Boolean} True if the asset was successfully removed and false otherwise
         * @example
         * var asset = app.assets.get(100);
         * app.assets.remove(asset);
         */
        remove: function (asset) {
            var idx = this._cache[asset.id];
            var url = asset.file ? asset.file.url : null;

            if (idx !== undefined) {
                // remove from list
                this._assets.splice(idx, 1);

                // remove id -> index cache
                delete this._cache[asset.id];

                // name cache needs to be completely rebuilt
                this._names = {};

                // urls cache needs to be completely rebuilt
                this._urls = [];

                // update id cache and rebuild name cache
                for (var i = 0, l = this._assets.length; i < l; i++) {
                    var a = this._assets[i];

                    this._cache[a.id] = i;
                    if (!this._names[a.name]) {
                        this._names[a.name] = [];
                    }
                    this._names[a.name].push(i);

                    if (a.file) {
                        this._urls[a.file.url] = i;
                    }
                }

                // tags cache
                this._tags.removeItem(asset);
                asset.tags.off('add', this._onTagAdd, this);
                asset.tags.off('remove', this._onTagRemove, this);

                asset.fire("remove", asset);
                this.fire("remove", asset);
                this.fire("remove:" + asset.id, asset);
                if (url)
                    this.fire("remove:url:" + url, asset);

                return true;
            }

            // asset not in registry
            return false;
        },

        /**
         * @function
         * @name pc.AssetRegistry#get
         * @description Retrieve an asset from the registry by its id field
         * @param {Number} id the id of the asset to get
         * @returns {pc.Asset} The asset
         * @example
         * var asset = app.assets.get(100);
         */
        get: function (id) {
            var idx = this._cache[id];
            return this._assets[idx];
        },

        /**
         * @function
         * @name pc.AssetRegistry#getByUrl
         * @description Retrieve an asset from the registry by it's file's URL field
         * @param {String} url The url of the asset to get
         * @returns {pc.Asset} The asset
         * @example
         * var asset = app.assets.getByUrl("../path/to/image.jpg");
         */
        getByUrl: function (url) {
            var idx = this._urls[url];
            return this._assets[idx];
        },

        /**
         * @function
         * @name pc.AssetRegistry#load
         * @description Load the asset's file from a remote source. Listen for "load" events on the asset to find out when it is loaded
         * @param {pc.Asset} asset The asset to load
         * @example
         * // load some assets
         * var toload = [app.assets.find("My Asset"), app.assets.find("Another Asset")]
         * var count = 0;
         * for (var i = 0; i < toload.length; i++) {
         *     var asset = toload[i];
         *     asset.ready(function (asset) {
         *         count++;
         *         if (count === toload.length) {
         *             // done
         *         }
         *     });
         *     app.assets.load(asset)
         * }
         */
        load: function (asset) {
            if (asset.loading)
                return;

            var self = this;

            // do nothing if asset is already loaded
            // note: lots of code calls assets.load() assuming this check is present
            // don't remove it without updating calls to assets.load() with checks for the asset.loaded state
            if (asset.loaded) {
                if (asset.type === 'cubemap')
                    self._loader.patch(asset, this);
                return;
            }

            var load = !!asset.file;

            var file = asset.getPreferredFile();

            var _load = function () {
                var url = asset.getFileUrl();

                asset.loading = true;

                self._loader.load(url, asset.type, function (err, resource, extra) {
                    asset.loaded = true;
                    asset.loading = false;

                    if (err) {
                        self.fire("error", err, asset);
                        self.fire("error:" + asset.id, err, asset);
                        asset.fire("error", err, asset);
                        return;
                    }
                    if (resource instanceof Array) {
                        asset.resources = resource;
                    } else {
                        asset.resource = resource;
                    }

                    if (!pc.script.legacy && asset.type === 'script') {
                        var loader = self._loader.getHandler('script');

                        if (loader._cache[asset.id] && loader._cache[asset.id].parentNode === document.head) {
                            // remove old element
                            document.head.removeChild(loader._cache[asset.id]);
                        }

                        loader._cache[asset.id] = extra;
                    }

                    self._loader.patch(asset, self);

                    self.fire("load", asset);
                    self.fire("load:" + asset.id, asset);
                    if (file && file.url)
                        self.fire("load:url:" + file.url, asset);
                    asset.fire("load", asset);
                }, asset);
            };

            var _open = function () {
                var resource = self._loader.open(asset.type, asset.data);
                if (resource instanceof Array) {
                    asset.resources = resource;
                } else {
                    asset.resource = resource;
                }
                asset.loaded = true;

                self._loader.patch(asset, self);

                self.fire("load", asset);
                self.fire("load:" + asset.id, asset);
                if (file && file.url)
                    self.fire("load:url:" + file.url, asset);
                asset.fire("load", asset);
            };

            // check for special case for cubemaps
            if (file && asset.type === "cubemap") {
                load = false;
                // loading prefiltered cubemap data
                var url = asset.getFileUrl();

                this._loader.load(url, "texture", function (err, texture) {
                    if (!err) {
                        // Fudging an asset so that we can apply texture settings from the cubemap to the DDS texture
                        self._loader.patch({
                            resource: texture,
                            type: "texture",
                            data: asset.data
                        }, self);

                        // store in asset data
                        asset._dds = texture;
                        _open();
                    } else {
                        self.fire("error", err, asset);
                        self.fire("error:" + asset.id, err, asset);
                        asset.fire("error", err, asset);
                    }
                });
            }

            if (!file) {
                _open();
            } else if (load) {
                this.fire("load:start", asset);
                this.fire("load:" + asset.id + ":start", asset);

                _load();
            }
        },

        /**
         * @function
         * @name pc.AssetRegistry#loadFromUrl
         * @description Use this to load and create an asset if you don't have assets created. Usually you would only use this
         * if you are not integrated with the PlayCanvas Editor
         * @param {String} url The url to load
         * @param {String} type The type of asset to load
         * @param {Function} callback Function called when asset is loaded, passed (err, asset), where err is null if no errors were encountered
         * @example
         * app.assets.loadFromUrl("../path/to/texture.jpg", "texture", function (err, asset) {
         *     var texture = asset.resource;
         * });
         */
        loadFromUrl: function (url, type, callback) {
            var self = this;

            var name = pc.path.getBasename(url);

            var file = {
                url: url
            };
            var data = {};

            var asset = self.getByUrl(url);
            if (!asset) {
                asset = new pc.Asset(name, type, file, data);
                self.add(asset);
            }

            if (type === 'model') {
                self._loadModel(asset, callback);
                return;
            }

            asset.once("load", function (loadedAsset) {
                callback(null, loadedAsset);
            });
            asset.once("error", function (err) {
                callback(err);
            });
            self.load(asset);
        },

        // private method used for engine-only loading of model data
        _loadModel: function (asset, callback) {
            var self = this;

            var url = asset.getFileUrl();
            var dir = pc.path.getDirectory(url);
            var basename = pc.path.getBasename(url);
            var ext = pc.path.getExtension(url);

            var _loadAsset = function (assetToLoad) {
                asset.once("load", function (loadedAsset) {
                    callback(null, loadedAsset);
                });
                asset.once("error", function (err) {
                    callback(err);
                });
                self.load(assetToLoad);
            };

            if (ext === '.json') {
                // playcanvas model format supports material mapping file
                var mappingUrl = pc.path.join(dir, basename.replace(".json", ".mapping.json"));
                this._loader.load(mappingUrl, 'json', function (err, data) {
                    if (err) {
                        asset.data = { mapping: [] };
                        _loadAsset(asset);
                        return;
                    }

                    self._loadMaterials(dir, data, function (e, materials) {
                        asset.data = data;
                        _loadAsset(asset);
                    });
                });
            } else {
                // other model format (e.g. obj)
                _loadAsset(asset);
            }

        },

        // private method used for engine-only loading of model data
        _loadMaterials: function (dir, mapping, callback) {
            var self = this;
            var i;
            var count = mapping.mapping.length;
            var materials = [];

            var done = function (err, loadedMaterials) {
                self._loadTextures(loadedMaterials, function (e, textures) {
                    callback(null, loadedMaterials);
                });
            };

            if (count === 0) {
                callback(null, materials);
            }

            var onLoadAsset = function (err, asset) {
                materials.push(asset);
                count--;
                if (count === 0)
                    done(null, materials);
            };

            for (i = 0; i < mapping.mapping.length; i++) {
                var path = mapping.mapping[i].path;
                if (path) {
                    path = pc.path.join(dir, path);
                    self.loadFromUrl(path, "material", onLoadAsset);
                } else {
                    count--;
                }
            }
        },

        // private method used for engine-only loading of model data
        _loadTextures: function (materialAssets, callback) {
            var self = this;
            var i;
            var used = {}; // prevent duplicate urls
            var urls = [];
            var textures = [];
            var count = 0;
            for (i = 0; i < materialAssets.length; i++) {
                var materialData = materialAssets[i].data;

                if (materialData.mappingFormat !== 'path') {
                    console.warn('Skipping: ' + materialAssets[i].name + ', material files must be mappingFormat: "path" to be loaded from URL');
                    continue;
                }

                var url = materialAssets[i].getFileUrl();
                var dir = pc.path.getDirectory(url);
                var textureUrl;

                for (var pi = 0; pi < pc.StandardMaterial.TEXTURE_PARAMETERS.length; pi++) {
                    var paramName = pc.StandardMaterial.TEXTURE_PARAMETERS[pi];

                    if (materialData[paramName]) {
                        var texturePath = materialData[paramName];
                        textureUrl = pc.path.join(dir, texturePath);
                        if (!used[textureUrl]) {
                            used[textureUrl] = true;
                            urls.push(textureUrl);
                            count++;
                        }
                    }
                }
            }

            if (!count) {
                callback(null, textures);
                return;
            }

            var onLoadAsset = function (err, texture) {
                textures.push(texture);
                count--;

                if (err) console.error(err);

                if (count === 0)
                    callback(null, textures);
            };

            for (i = 0; i < urls.length; i++)
                self.loadFromUrl(urls[i], "texture", onLoadAsset);
        },

        /**
         * @function
         * @name pc.AssetRegistry#findAll
         * @description Return all Assets with the specified name and type found in the registry
         * @param {String} name The name of the Assets to find
         * @param {String} [type] The type of the Assets to find
         * @returns {pc.Asset[]} A list of all Assets found
         * @example
         * var assets = app.assets.findAll("myTextureAsset", "texture");
         * console.log("Found " + assets.length + " assets called " + name);
         */
        findAll: function (name, type) {
            var self = this;
            var idxs = this._names[name];
            if (idxs) {
                var assets = idxs.map(function (idx) {
                    return self._assets[idx];
                });

                if (type) {
                    return assets.filter(function (asset) {
                        return (asset.type === type);
                    });
                }

                return assets;
            }

            return [];
        },

        _onTagAdd: function (tag, asset) {
            this._tags.add(tag, asset);
        },

        _onTagRemove: function (tag, asset) {
            this._tags.remove(tag, asset);
        },

        /**
         * @function
         * @name pc.AssetRegistry#findByTag
         * @description Return all Assets that satisfy the search query.
         * Query can be simply a string, or comma separated strings,
         * to have inclusive results of assets that match at least one query.
         * A query that consists of an array of tags can be used to match assets that have each tag of array
         * @param {String} tag Name of a tag or array of tags
         * @returns {pc.Asset[]} A list of all Assets matched query
         * @example
         * var assets = app.assets.findByTag("level-1");
         * // returns all assets that tagged by `level-1`
         * @example
         * var assets = app.assets.findByTag("level-1", "level-2");
         * // returns all assets that tagged by `level-1` OR `level-2`
         * @example
         * var assets = app.assets.findByTag([ "level-1", "monster" ]);
         * // returns all assets that tagged by `level-1` AND `monster`
         * @example
         * var assets = app.assets.findByTag([ "level-1", "monster" ], [ "level-2", "monster" ]);
         * // returns all assets that tagged by (`level-1` AND `monster`) OR (`level-2` AND `monster`)
         */
        findByTag: function () {
            return this._tags.find(arguments);
        },

        /**
         * @function
         * @name pc.AssetRegistry#filter
         * @description Return all Assets that satisfy filter callback
         * @param {Function} callback The callback function that is used to filter assets, return `true` to include asset to result list
         * @returns {pc.Asset[]} A list of all Assets found
         * @example
         * var assets = app.assets.filter(function(asset) {
         *     return asset.name.indexOf('monster') !== -1;
         * });
         * console.log("Found " + assets.length + " assets, where names contains 'monster'");
         */
        filter: function (callback) {
            var items = [];
            for (var i = 0, len = this._assets.length; i < len; i++) {
                if (callback(this._assets[i]))
                    items.push(this._assets[i]);
            }
            return items;
        },

        /**
         * @function
         * @name pc.AssetRegistry#find
         * @description Return the first Asset with the specified name and type found in the registry
         * @param {String} name The name of the Asset to find
         * @param {String} [type] The type of the Asset to find
         * @returns {pc.Asset} A single Asset or null if no Asset is found
         * @example
         * var asset = app.assets.find("myTextureAsset", "texture");
         */
        find: function (name, type) {
            var asset = this.findAll(name, type);
            return asset ? asset[0] : null;
        },

        // backwards compatibility
        getAssetById: function (id) {
            console.warn("DEPRECATED: getAssetById() use get() instead");
            return this.get(id);
        }
    });

    return {
        AssetRegistry: AssetRegistry
    };
}());