import Tile from './tile.js' /** * A Tile Loader component that can be plugged into a Tiles Layer. */ export class TileLoader { constructor(tiles) { this.debug = false this.tiles = tiles this.setup() } /** Setup collections and instance vars. */ setup() { this.map = new Map() // Map {url : [ col, row]} this.loading = new Set() // Set url this.loaded = new Map() // Map {url : sprite } this.loadQueue = [] } /** Schedules a tile url for loading. The loading itself must be triggered by a call to loadOneTile or loadAll * @param {String} url - the url of the texture / tile * @param {Number} col - the tile col * @param {Number} row - the tile row **/ schedule(url, col, row) { if (this.loaded.has(url)) return false if (this.loading.has(url)) return false //Tile.schedule(url) this.map.set(url, [col, row]) this.loading.add(url) this.loadQueue.push(url) return true } unschedule(url) { if (this.loaded.has(url)) this.loaded.delete(url) if (this.loading.has(url)) this.loading.delete(url) //Tile.unschedule(url) this.loadQueue = this.loadQueue.filter(item => item != url) } /** Cancels loading by clearing the load queue **/ cancel() { this.loadQueue = [] this.loading.clear() } /** Destroys alls collections. **/ destroy() { this.setup() } /** Private method. Informs the tile layer about a texture for a given url. * Creates the sprite for the loaded texture and informs the tile layer. * @param {String} url - the url * @param {Object} texture - the loaded resource **/ _textureAvailable(url, col, row, texture) { let tile = this.loaded.get(url) if (tile != null) { console.warn("Tile already loaded") tile.unregister() } try { tile = new Tile(texture, url) this.loaded.set(url, tile) this.tiles.tileAvailable(tile, col, row, url) } catch (error) { console.warn("Tile loading error", error) } } } /** * Uses the PIXI Loader but can be replaced with othe loaders implementing * the public methods without underscore. * Calls the Tiles.tileAvailable method if the texture is available. **/ export class PIXITileLoader extends TileLoader { constructor(tiles, compression) { super(tiles) this.destroyed = false this.loader = new PIXI.loaders.Loader() this.loader.on('load', this._onLoaded.bind(this)) this.loader.on('error', this._onError.bind(this)) if (compression) { this.loader.pre(PIXI.compressedTextures.imageParser()) } } schedule(url, col, row) { // Overwritten schedule to avoid BaseTexture and Texture already loaded errors. if (this.loaded.has(url)) return false if (this.loading.has(url)) return false //Tile.schedule(url) let reusableTexture = Tile.textureAvailable(url) if (reusableTexture) { if (this.debug) console.log('Texture reusable', reusableTexture) this._textureAvailable(url, col, row, reusableTexture) return false } let texture = PIXI.utils.TextureCache[url] if (texture) { if (this.debug) console.log('Texture already loaded', texture) this._textureAvailable(url, col, row, texture) return false } let base = PIXI.utils.BaseTextureCache[url] if (base) { if (this.debug) console.log('BaseTexture already loaded', base) let texture = new PIXI.Texture(base) this._textureAvailable(url, col, row, texture) return false } return super.schedule(url, col, row) } /** Load one and only one of the scheduled tiles **/ loadOneTile() { if (this.destroyed) return this._loadOneTile() } /** Load all scheduled tiles **/ loadAll() { if (this.destroyed) return this._loadAllTiles() } /** Destroys the loader completly **/ destroy() { this.destroyed = true super.destroy() try { this.loader.reset() } catch (error) { console.warn("Error while resetting loader", error) } } _onError(loader, error) { console.warn('Cannot load', error) } /** Private method. Called by the PIXI loader after each successfull * loading of a single tile. * Creates the sprite for the loaded texture and informs the tile layer. * @param {Object} loader - the loader instance * @param {Object} resource - the loaded resource with url and texture attr **/ _onLoaded(loader, resource) { if (this.destroyed) { let texture = resource.texture let url = resource.url Tile.lateTexture(url, texture) console.warn("Received resource after destroy", texture) return } try { let [col, row] = this.map.get(resource.url) this._textureAvailable(resource.url, col, row, resource.texture) } catch (err) { console.warn("Texture unavailable: " + err.message) } } /** Private method: loads one tile from the queue. **/ _loadOneTile(retry = 1) { //console.log("_loadOneTile") if (this.destroyed) { //console.warn("_loadOneTile after destroy") return } if (this.loader.loading) { setTimeout(() => { this._loadOneTile() }, retry) return } if (this.loadQueue.length > 0) { let url = this.loadQueue.pop() this.loader.add(url, url) this.loader.load() } } /** Private method: loads all tiles from the queue in batches. Batches are helpfull to avoid loading tiles that are no longer needed because the user has already zoomed to a different level.**/ _loadAllTiles(batchSize = 8, retry = 16) { if (this.destroyed) { return } if (this.loadQueue.length > 0) { if (this.loader.loading) { //console.log("Loader busy", this.loadQueue.length) setTimeout(() => { this._loadAllTiles() }, retry) return } let i = 0 let urls = [] while (i < batchSize && this.loadQueue.length > 0) { let url = this.loadQueue.pop() if (!this.loaded.has(url)) { let resource = this.loader.resources[url] if (resource) { console.log("Resource already added", url) } else { urls.push(url) i += 1 } } } this.loader.add(urls).load(() => { this._loadAllTiles() }) } } } /** * Uses XMLHttpRequests but can be replaced with other loaders implementing * the public methods without underscore. * Calls the Tiles.tileAvailable method if the texture is available. **/ export class RequestTileLoader extends TileLoader { constructor(tiles, compression) { super(tiles) this.compression = compression } schedule(url, col, row) { this._load(url, col, row) return super.schedule(url, col, row) } _load(url, col, row, callback = null) { if (this.compression) { let xhr = new XMLHttpRequest() xhr.open('GET', url, false) xhr.responseType = 'arraybuffer' xhr.onload = e => { let CompressedImage = PIXI.compressedTextures.CompressedImage let compressed = CompressedImage.loadFromArrayBuffer( xhr.response, url ) let base = new PIXI.BaseTexture(compressed) let texture = new PIXI.Texture(base) this._textureAvailable(url, col, row, texture) if (callback) callback() } xhr.send() } else { let texture = PIXI.Texture.fromImage('assets/image.png') this._textureAvailable(url, col, row, texture) if (callback) callback() } } /** Load one and only one of the scheduled tiles **/ loadOneTile() { this._loadOneTile() } /** Load all scheduled tiles **/ loadAll() { this._loadAllTiles() } /** Private method: loads one tile from the queue. **/ _loadOneTile(retry = 1) { if (this.loadQueue.length > 0) { let url = this.loadQueue.pop() let [col, row] = this.map.get(url) this._load(url, col, row) } } /** Private method: loads all tiles from the queue in batches. Batches are helpfull to avoid loading tiles that are no longer needed because the user has already zoomed to a different level.**/ _loadAllTiles(batchSize = 8, retry = 16) { if (this.loadQueue.length > 0) { let i = 0 let urls = [] while (i < batchSize && this.loadQueue.length > 0) { let url = this.loadQueue.pop() if (this.debug) console.time(url) if (!this.loaded.has(url)) { urls.push(url) i += 1 } } let total = urls.length let count = 0 for (let url of urls) { let [col, row] = this.map.get(url) this._load(url, col, row, () => { count++ if (count == total) this._loadAllTiles() }) } } } } /** * Uses Workers but can be replaced with other loaders implementing * the public methods without underscore. * Calls the Tiles.tileAvailable method if the texture is available. **/ export class WorkerTileLoader extends TileLoader { constructor(tiles, workerPath) { super(tiles) let worker = this.worker = new Worker(workerPath) worker.onmessage = (event) => { if (event.data.success) { let { url, col, row, buffer } = event.data //console.log("WorkerTileLoader.loaded", url, buffer) let CompressedImage = PIXI.compressedTextures.CompressedImage let compressed = CompressedImage.loadFromArrayBuffer(buffer, url) let base = new PIXI.BaseTexture(compressed) let texture = new PIXI.Texture(base) this._textureAvailable(url, col, row, texture) } } } loadOne() { if (this.loadQueue.length > 0) { let url = this.loadQueue.pop() let [col, row] = this.map.get(url) let tile = [col, row, url] this.worker.postMessage({ command: "load", tiles: [tile] }) } } loadAll() { let tiles = [] while (this.loadQueue.length > 0) { let url = this.loadQueue.pop() let [col, row] = this.map.get(url) tiles.push([col, row, url]) } this.worker.postMessage({ command: "load", tiles }) } cancel() { super.cancel() this.worker.postMessage({ command: "abort" }) } destroy() { this.worker.postMessage({ command: "abort" }) this.worker.terminate() this.worker = null super.destroy() } }