367 lines
12 KiB
JavaScript
367 lines
12 KiB
JavaScript
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.Loader()
|
|
this.loader.onLoad.add(this._onLoaded.bind(this))
|
|
this.loader.onError.add(this._onError.bind(this))
|
|
if (compression) {
|
|
this.loader.use(PIXI.compressedTextures.ImageParser.use)
|
|
}
|
|
}
|
|
|
|
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.from('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 = PIXI.compressedTextures.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()
|
|
}
|
|
}
|