iwmlib/pixi/deepzoom/loader.js
2019-03-21 09:57:27 +01:00

372 lines
12 KiB
JavaScript

import { deepZoomTileCache, 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
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)
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()
}
tile = new Tile(texture, url)
this.loaded.set(url, tile)
this.tiles.tileAvailable(tile, col, row, url)
}
}
/**
* 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
if (deepZoomTileCache.has(url)) {
let tiles = deepZoomTileCache.get(url)
for (let tile of tiles.values()) {
//console.log("Reusing cached texture", tile.parent)
let texture = tile.texture
this._textureAvailable(url, col, row, texture)
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 destroyBase = !deepZoomTileCache.has(resource.url)
texture.destroy(destroyBase)
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) {
super(tiles)
let worker = this.worker = new Worker("../../lib/pixi/deepzoom/tileloader.js")
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()
}
}