Restructured library.

This commit is contained in:
2019-03-22 12:54:57 +01:00
parent 1bc2deb4d3
commit d1efeeffa6
1912 changed files with 21424 additions and 21383 deletions
+144
View File
@@ -0,0 +1,144 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>DeepZoomImage Doctests</title>
<link rel="stylesheet" href="../../3rdparty/highlight/styles/default.css">
<link rel="stylesheet" href="../../../css/doctest.css">
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<script src="../../3rdparty/all.js"></script>
<script src="../../3rdparty/jquery.min.js"></script>
<script src="../../all.js"></script>
<script src="../all.js"></script>
<style>
#app {
display: table;
margin: 0 auto;
}
#app > * {
margin-bottom: 5px;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>DeepZoomImage</h1>
<p>
The main class of a deeply zoomable image that is represented by a hierarchy of tile layers for each zoom level. This gives
the user the impression that even huge pictures (up to gigapixel-images) can be zoomed instantaneously, since the
tiles at smaller levels are scaled immediately and overloaded by more detailed tiles at the larger level as fast
as possible.
</p>
<br />
<div id="app">
<button id="change_dpr">Change Pixel Ratio</button>
<div id="canvas_container"></div>
<div id="info"></div>
</div>
<script class="doctest">
// When an element is added, the ScatterApp wrapps it in it's own Scatter Container.
// Just as in the doctest: scatter.html
class ScatterApp extends PIXIApp {
sceneFactory() {
return new ScatterContainer(this.renderer, { showBounds: true, app: this })
}
}
let app
let state = 0
//Destroys the PIXIApp element and the corresponding canvas,
//to reinstantiate the entire application.
changePIXI()
function changePIXI() {
if (typeof app != 'undefined') {
//The parameter destroys the canvas, when destroying the app.
// Not deleting a new canvas resulted in some
// weird PIXI error.
app.destroy(true)
}
//A new canvas has to be created
//for the new view.
var canvas = document.createElement("canvas")
canvas_container.appendChild(canvas);
app = new ScatterApp({
resolution: state + 1,
//Default parameters
view: canvas,
autoResize: false,
width: 128,
height: 128,
backgroundColor: 0xFFCCCCCC
}).setup().run()
// To create a DeepZoomImage, a DeepZoomInfo has to
// be provided. It contains all the necessary informations
// for the DeepZoomImage, to behave as intended.
// (E.g. that it displays the right level of tiles for the current view distance.)
deepZoomInfo = new DeepZoomInfo(
{
"tileSize": 128,
"format": "jpg",
"overlap": 0,
"type": "map",
"height": 4096,
"width": 4096,
"path": "../assets/maps/test",
"urlTileTemplate": "{path}/{level}/{column}/{row}.{format}"
});
// Create the DeepZoomImage
deepZoomImage = new DeepZoomImage(deepZoomInfo, {
highResolution: !!state,
app
});
deepZoomImage.scatter = new DisplayObjectScatter(deepZoomImage, app.renderer, {
// Allow more flexible scaling for debugging purposes.
minScale: 0,
maxScale: 100,
// Notify the DeepZoomImage, when it's container has
// been transformed (translated, scaled, rotated, ...)
onTransform: (event) => {
deepZoomImage.transformed(event)
}
});
// Add the DeepZoomImage to the scene.
app.scene.addChild(deepZoomImage)
//Set info text.
info.innerHTML = "resolution: " + app._options.resolution +
"<br>high resolution: " + !!state;
}
// Add functionality to the button.
change_dpr.addEventListener("click", (event) => {
state = (state + 1) % 2
changePIXI()
})
</script>
</body>
</html>
+1118
View File
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>DeepZoomImage Doctests</title>
<link rel="stylesheet" href="../../3rdparty/highlight/styles/default.css">
<link rel="stylesheet" href="../../../css/doctest.css">
<script src="../../3rdparty/highlight/highlight.pack.js"></script>
<script src="../../3rdparty/all.js"></script>
<script src="../../3rdparty/jquery.min.js"></script>
<script src="../../all.js"></script>
<script src="../all.js"></script>
<style>
#app {
display: table;
margin: 0 auto;
}
#app > * {
margin-bottom: 5px;
}
</style>
</head>
<body onload="Doctest.run()">
<h1>Double DeepZoomImage</h1>
<p>
The main class of a deeply zoomable image that is represented by a hierarchy of tile layers for each zoom level. This gives
the user the impression that even huge pictures (up to gigapixel-images) can be zoomed instantaneously, since the
tiles at smaller levels are scaled immediately and overloaded by more detailed tiles at the larger level as fast
as possible.
</p>
<br />
<div id="div1" style="float: left;"></div>
<div id="div2" style="float: right;"></div>
<div style="clear: left; margin-top: 540px;" />
<script class="doctest">
// deepZoom
//--------------------
const deepZoomInfo = new DeepZoomInfo({
"tileSize": 128,
"format": "jpg",
"overlap": 0,
"type": "map",
"height": 4096,
"width": 4096,
"path": "../assets/maps/test",
"urlTileTemplate": "{path}/{level}/{column}/{row}.{format}"
})
// const deepZoomInfo = new DeepZoomInfo({
// compression: [
// "dds"
// ],
// clip: {
// minLevel: 12,
// maxLevel: 20,
// startCol: 275215,
// startRow: 181050,
// bounds: {
// min: [48.458353, 8.96484374976547],
// max: [48.5747899110263, 9.14062499976523]
// }
// },
// tileSize: 512,
// format: "png",
// overlap: 0,
// type: "map",
// height: 131072,
// width: 131072,
// path: "../../../var/tuesch/luftbild_2016_full",
// urlTileTemplate: "{path}/{level}/{row}/{column}.{format}"
// })
// app
//--------------------
window.app = new PIXIApp({
width: 400,
height: 500,
backgroundColor: 0xFFCCCCCC
}).setup().run()
div1.appendChild(app.view)
// create the ScatterContainer
//--------------------
const scatterContainer1 = new ScatterContainer(app.renderer, {showBounds: true, app: app})
app.scene.addChild(scatterContainer1)
// Create the DeepZoomImage
//--------------------
setTimeout(() => {
const deepZoomImage1 = new DeepZoomImage(deepZoomInfo, {app, world: scatterContainer1})
deepZoomImage1.scatter = new DisplayObjectScatter(deepZoomImage1, app.renderer, {
minScale: 0,
maxScale: 50,
onTransform: event => {
console.log('currentLevel', deepZoomImage1.currentLevel)
deepZoomImage1.transformed(event)
}
})
scatterContainer1.addChild(deepZoomImage1)
}, 1000)
// app2
//--------------------
const app2 = new PIXIApp({
width: 400,
height: 500,
backgroundColor: 0xFFCCCCCC
}).setup().run()
div2.appendChild(app2.view)
// create the ScatterContainer
//--------------------
const scatterContainer2 = new ScatterContainer(app2.renderer, {showBounds: true, app: app2})
app2.scene.addChild(scatterContainer2)
// Create the DeepZoomImage
//--------------------
const deepZoomImage2 = new DeepZoomImage(deepZoomInfo, {app: app2})
deepZoomImage2.scatter = new DisplayObjectScatter(deepZoomImage2, app2.renderer, {
minScale: 0,
maxScale: 100,
onTransform: (event) => {
deepZoomImage2.transformed(event)
}
})
scatterContainer2.addChild(deepZoomImage2)
</script>
<h1>DeepZoomImage in DeepZoomImage</h1>
<p>
The main class of a deeply zoomable image that is represented by a hierarchy of tile layers for each zoom level. This gives
the user the impression that even huge pictures (up to gigapixel-images) can be zoomed instantaneously, since the
tiles at smaller levels are scaled immediately and overloaded by more detailed tiles at the larger level as fast
as possible.
</p>
<br />
<div id="div3"></div>
<script class="doctest">
// app3
//--------------------
const app3 = new PIXIApp({
width: 900,
height: 500,
backgroundColor: 0xFFCCCCCC
}).setup().run()
window.app3 = app3
div3.appendChild(app3.view)
// create the ScatterContainer
//--------------------
const scatterContainer3 = new ScatterContainer(app3.renderer, {app: app3, showBounds: true, claimEvent: false, stopEvents: false})
app3.scene.addChild(scatterContainer3)
// Create the DeepZoomImage
//--------------------
const deepZoomImage3 = new DeepZoomImage(deepZoomInfo, {app: app3})
deepZoomImage3.scatter = new DisplayObjectScatter(deepZoomImage3, app3.renderer, {
minScale: 0,
maxScale: 100,
startScale: 2,
autoBringToFront: false,
onTransform: (event) => {
deepZoomImage3.transformed(event)
}
})
app3._deepZoomImage3 = deepZoomImage3
scatterContainer3.addChild(deepZoomImage3)
// Create the second DeepZoomImage
//--------------------
const border = new PIXI.Graphics()
border.beginFill(0x282828, 1)
border.drawRect(0, 0, 264, 244)
scatterContainer3.addChild(border)
const mask = new PIXI.Graphics()
mask.beginFill(0x282828, 1)
mask.drawRect(0, 0, 260, 240)
scatterContainer3.addChild(mask)
const deepZoomImage4 = new DeepZoomImage(deepZoomInfo, {app: app3})
deepZoomImage4.x = 4
deepZoomImage4.y = 4
deepZoomImage4.scatter = new DisplayObjectScatter(deepZoomImage4, app3.renderer, {
minScale: 0,
maxScale: 100,
onTransform: (event) => {
deepZoomImage4.transformed(event)
}
})
deepZoomImage4.mask = mask
app3._deepZoomImage4 = deepZoomImage4
scatterContainer3.addChild(deepZoomImage4)
</script>
</body>
</html>
+371
View File
@@ -0,0 +1,371 @@
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()
}
}
+210
View File
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en" class="has-navbar-fixed-top">
<head>
<meta charset="utf-8">
<title>DeepZoom Tests</title>
<!-- disable zooming -->
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="../../../lib/3rdparty/all.js"></script>
<script src="../../../lib/all.js"></script>
<link rel="stylesheet" href="../test/lib/bulma.css">
<link rel="stylesheet" href="../test/lib/mocha.min.css">
<link rel="stylesheet" href="../test/test.css">
<script src="../test/lib/fontawesome.js"></script>
<script src="../test/lib/chai.js"></script>
<script src="../test/lib/mocha.min.js"></script>
<script src="../test/lib/Chart.bundle.min.js"></script>
</head>
<body>
<nav class="navbar is-fixed-top has-shadow">
<div class="navbar-brand">
<a class="navbar-item" href="../../../index.html">
<img class="image is-32x32" src="../../../assets/icons/icon.png">
</a>
<div class="navbar-item">
DeepZoom Tests
</div>
</div>
</nav>
<section class="section">
<div class="columns">
<div class="column" id="main"></div>
<div class="column">
<aside class="menu">
<p class="menu-label">
All tests
</p>
<ul class="menu-list">
<li>
<a onclick="uiTestSuite.run()">Run all tests</a>
</li>
</ul>
<p class="menu-label">
Memory tests
</p>
<ul class="menu-list">
<li>
<a onclick="uiTestSuite.run('zoomInZoomOut')">Zoom in, Zoom out, Zoom in, Zoom out, ...</a>
</li>
</ul>
<p class="menu-label">Test report</p>
<div id="mocha"></div>
<div id="chart" style="position: relative; height: 400px; width: 100%;">
<canvas id="memoryChart"></canvas>
</div>
</aside>
</div>
</div>
</section>
<script>
// testFrame
//--------------------
function loadTestFrame(cb) {
// remove old iframe
const element = document.getElementById('testFrame')
if (element) {
main.removeChild(element)
}
// create new iframe
const iframe = document.createElement("iframe")
iframe.setAttribute('id', 'testFrame')
iframe.setAttribute('src', './index.html')
iframe.setAttribute('style', 'border: 2px solid gray;')
iframe.setAttribute('width', '1024')
iframe.setAttribute('height', '768')
if (cb) {
iframe.addEventListener('load', function _callback(event) {
cb.call(this, event, iframe)
iframe.removeEventListener('load', _callback, true) // execute callback only once
}, true)
}
main.appendChild(iframe)
// bind to main window (testFrame is not bound automatically)
window.testFrame = iframe
}
loadTestFrame()
// chart
//--------------------
const data = {
datasets: [{
label: 'Total HeapSize',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
pointRadius: 1
}, {
label: 'Used HeapSize',
data: [],
backgroundColor: 'rgba(255, 206, 86, 0.2)',
borderColor: 'rgba(255, 206, 86, 1)',
borderWidth: 1,
pointRadius: 1
}, {
label: 'HeapSize Limit',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255,99,132,1)',
borderWidth: 1,
pointRadius: 1
}]
}
function addChart() {
const ctx = document.getElementById('memoryChart').getContext('2d')
const chart = new Chart(ctx, {
type: 'scatter',
data,
options: {
responsive: true,
title: {
display: true,
text: 'JavaScript Memory'
},
tooltips: {
enabled: false
},
hover: {
enabled: false
},
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'Time [min]'
},
ticks: {
min: 0,
max: 10,
stepSize: 2
}
}],
yAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'HeapSize [MB]'
},
ticks: {
maxTicksLimit: 6
}
}]
}
}
})
const start = Date.now()
setInterval(() => {
data.datasets.forEach(function(dataset) {
const memory = window.performance.memory
const x = (Date.now() - start) / 1000 / 60
let y = 0
if (dataset.label === 'Total HeapSize') {
y = memory.totalJSHeapSize / 1000 / 1000
} else if (dataset.label === 'Used HeapSize') {
y = memory.usedJSHeapSize / 1000 / 1000
} else {
y = memory.jsHeapSizeLimit / 1000 / 1000
}
dataset.data.push({x, y})
});
chart.update()
}, 1000)
}
if (/chrome/i.test(navigator.userAgent)) {
addChart()
} else {
console.log('No Chrome, no Chart :-(')
}
</script>
<script src="../../../lib/bootstrap.js"></script>
<script>
Bootstrap.import('../test/testsuite.js')
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

+77
View File
@@ -0,0 +1,77 @@
export const deepZoomTileCache = new Map()
/** The current Tile implementation simply uses PIXI.Sprites.
*
* BTW: PIXI.extras.TilingSprite is not appropriate. It should be used for
* repeating patterns.
**/
export class Tile extends PIXI.Sprite {
constructor(texture, url) {
super(texture)
this.url = url
this.register(url)
}
static fromImage(imageId, crossorigin, scaleMode) {
return new Tile(PIXI.Texture.fromImage(imageId, crossorigin, scaleMode), imageId)
}
/**
* Registers the tile in the global reference counter for textures
*
* @param {*} url
* @param {boolean} [debug=false]
* @memberof Tile
*/
register(url, debug = false) {
if (deepZoomTileCache.has(url)) {
let tiles = deepZoomTileCache.get(url)
tiles.add(this)
if (debug) console.log("Tile.register", url, tiles.size)
}
else {
deepZoomTileCache.set(url, new Set([this]))
if (debug) console.log("Tile.register", url, 1)
}
}
/**
* Unregisters the rile in the global reference counter for textures
*
* @returns {number} The number of how often a texture is used.
* @memberof Tile
*/
unregister() {
let tiles = deepZoomTileCache.get(this.url)
tiles.delete(this)
if (tiles.size == 0) {
deepZoomTileCache.delete(this.url)
}
return tiles.size
}
/**
* Destroys this sprite and optionally its texture and children
*
* @param {*} options Part of the PIXI API, but ignored in the implementation
* @memberof Tile
*/
destroy(options, debug = false) {
if (this.parent != null) {
}
let count = this.unregister()
if (count <= 0) {
let opts = { children: true, texture: true, baseTexture: true }
super.destroy(opts)
if (debug) console.log("Tile.destroy", deepZoomTileCache.size, opts)
}
else {
let opts = { children: true, texture: false, baseTexture: false }
if (debug) console.log("Tile.destroy", deepZoomTileCache.size, opts)
super.destroy(opts)
}
}
}
+53
View File
@@ -0,0 +1,53 @@
let loadQueue = []
let pendingRequests = new Map()
const batchSize = 8
const debug = false
function load() {
while(loadQueue.length>0 && pendingRequests.size<batchSize) {
let tile = loadQueue.shift()
let [col, row, url] = tile
let xhr = new XMLHttpRequest()
xhr.responseType = "arraybuffer"
xhr.onload = (event) => {
pendingRequests.delete(url)
let buffer = xhr.response
postMessage({ success: true, url, col, row, buffer}, [buffer])
}
xhr.onerror = (event) => {
pendingRequests.delete(url)
let buffer = null
postMessage({ success: false, url, col, row, buffer})
}
xhr.open('GET', url, true)
xhr.send()
pendingRequests.set(url, xhr)
}
if (loadQueue.length>0)
setTimeout(load, 1000/120)
else {
if (debug) console.log("Ready")
}
}
self.onmessage = (event) => {
let msg = event.data
switch(msg.command) {
case 'load':
for(let tile of msg.tiles) {
loadQueue.push(tile)
}
load()
break
case 'abort':
loadQueue = []
for(let xhr of pendingRequests.values()) {
xhr.abort()
}
if (debug) console.log('Abort')
break
default:
console.warn('Unknown worker command: ' + msg.command)
}
}
+334
View File
@@ -0,0 +1,334 @@
import { Colors } from '../../utils.js'
import { WorkerTileLoader, PIXITileLoader } from "./loader.js"
/**
* A layer of tiles that represents a zoom level of a DeepZoomImage as a grid
* of sprites.
* @constructor
* @param {number} level - the zoom level of the tile layer
* @param {DeepZoomImage} view - the zoomable image the layer belongs to
* @param {number} scale - the scale of the tile layer
* @param {number} cols - the number of columns of the layer
* @param {number} rows - the number of rows of the layer
* @param {number} width - the width of the layer in pixel
* @param {number} height - the height of the layer in pixel
* @param {number} tileSize - the size of a single tile in pixel
* @param {number} overlap - the overlap of the tiles in pixel
* @param {number} fadeInTime - time needed to fade in tiles if TweenLite is set
**/
export class Tiles extends PIXI.Container {
constructor(
level,
view,
scale,
cols,
rows,
width,
height,
tileSize,
overlap,
fadeInTime = 0.33
) {
super()
this.debug = false
this.showGrid = false
this.view = view
this.level = level
this.cols = cols
this.rows = rows
this.pixelWidth = width
this.pixelHeight = height
this.tileSize = tileSize
this.overlap = overlap
this.needed = new Map() // url as key, [col, row] as value
this.requested = new Set()
this.available = new Map()
this.scale.set(scale, scale)
this.tileScale = scale
this.fadeInTime = fadeInTime
this.keep = false
if (this.view.preferWorker && view.info.compression.length > 0)
this.loader = new WorkerTileLoader(this)
else
this.loader = new PIXITileLoader(this, view.info.compression)
this.interactive = false
this._highlight = null
this._info = null
this._centerPoint = null
this._boundsRect = null
this.infoColor = Colors.random()
this.pprint()
this.destroyed = false
}
/** Tests whether all tiles are loaded. **/
isComplete() {
return this.cols * this.rows === this.children.length
}
/** Returns the highligh graphics layer for debugging purposes.
**/
get highlight() {
if (this._highlight == null) {
let graphics = new PIXI.Graphics()
graphics.beginFill(0xffff00, 0.1)
graphics.lineStyle(2, 0xffff00)
graphics.drawRect(1, 1, this.tileSize - 2, this.tileSize - 2)
graphics.endFill()
graphics.interactive = false
this._highlight = graphics
}
return this._highlight
}
/** Returns the highligh graphics layer for debugging purposes.
**/
get info() {
if (this._info == null) {
let graphics = new PIXI.Graphics()
graphics.lineStyle(4, 0xff0000)
graphics.interactive = false
this._info = graphics
this.addChild(this._info)
}
return this._info
}
/** Helper method pretty printing debug information. **/
pprint() {
if (this.debug)
console.log(
'Tiles level: ' +
this.level +
' scale: ' +
this.scale.x +
' cols: ' +
this.cols +
' rows: ' +
this.rows +
' w: ' +
this.pixelWidth +
' h: ' +
this.pixelHeight +
' tsize:' +
this.tileSize
)
}
/** Computes the tile position and obeys the overlap.
* @param {number} col - The column of the tile
* @param {number} row - The row of the tile
* @returns {PIXI.Point} obj
**/
tilePosition(col, row) {
let x = col * this.tileSize
let y = row * this.tileSize
let overlap = this.overlap
if (col != 0) {
x -= overlap
}
if (row != 0) {
y -= overlap
}
return new PIXI.Point(x, y)
}
/** Computes the tile size without overlap
* @param {number} col - The column of the tile
* @param {number} row - The row of the tile
* @returns {PIXI.Point} obj
**/
tileDimensions(col, row) {
let w = this.tileSize
let h = this.tileSize
let pos = this.tilePosition(col, row)
if (col == this.cols - 1) {
w = this.pixelWidth - pos.x
}
if (row == this.rows - 1) {
h = this.pixelHeight - pos.y
}
return new PIXI.Point(w, h)
}
/** Method to support debugging. Highlights the specified tile at col, row **/
highlightTile(col, row) {
if (col > -1 && row > -1 && col < this.cols && row < this.rows) {
let graphics = this.highlight
let dim = this.tileDimensions(col, row)
graphics.position = this.tilePosition(col, row)
graphics.clear()
graphics.beginFill(0xff00ff, 0.1)
graphics.lineStyle(2, 0xffff00)
graphics.drawRect(1, 1, dim.x - 2, dim.y - 2)
graphics.endFill()
this.addChild(this.highlight)
} else {
this.removeChild(this.highlight)
}
}
/** Loads the tiles for the given urls and adds the tiles as sprites.
* @param {array} urlpos - An array of URL, pos pairs
* @param {boolean} onlyone - Loads only on tile at a time if true
**/
loadTiles(urlpos, onlyone, refCol, refRow) {
if (this.showGrid) {
this.highlightTile(refCol, refRow)
}
urlpos.forEach(d => {
let [url, col, row] = d
if (this.loader.schedule(url, col, row)) {
if (onlyone) {
return this.loader.loadOneTile()
}
}
})
this.loader.loadAll()
}
/** Private method: add a red border to a tile for debugging purposes. **/
_addTileBorder(tile, col, row) {
let dim = this.tileDimensions(col, row)
let graphics = new PIXI.Graphics()
graphics.beginFill(0, 0)
graphics.lineStyle(2, 0xff0000)
graphics.drawRect(1, 1, dim.x - 2, dim.y - 2)
graphics.endFill()
tile.addChild(graphics)
}
/** Adds a tile. **/
addTile(tile, col, row, url) {
if (this.available.has(url)) {
console.warn('Trying to add available tile')
return
}
this.addChildAt(tile, 0)
this.available.set(url, tile)
if (this.destroyed) {
console.warn('Adding to destroyed tiles layer')
}
// this._addTileBorder(tile, col, row)
}
/** Called by the loader after each successfull loading of a single tile.
* Adds the sprite to the tile layer.
* @param {Object} tile - the loaded tile sprite
* @param {Number} col - the col position
* @param {Number} row - the rowposition
**/
tileAvailable(tile, col, row, url) {
let pos = this.tilePosition(col, row)
if (this.showGrid) {
this._addTileBorder(tile, col, row)
}
tile.position = pos
tile.interactive = false
if (TweenLite) {
tile.alpha = 0
TweenLite.to(tile, this.fadeInTime, { alpha: this.alpha })
}
this.addTile(tile, col, row, url)
}
/** Destroys the tiles layer and destroys the loader. Async load calls are
* cancelled.
**/
destroy() {
this.destroyed = true
this.loader.destroy()
super.destroy({ children: true }) // Calls destroyChildren
this.available.clear()
this.requested.clear()
this.needed.clear()
}
destroyTile(url, tile) {
this.loader.unschedule(url)
this.removeChild(tile)
tile.destroy()
this.available.delete(url)
}
destroyTileByUrl(url) {
if (this.available.has(url)) {
let tile = this.available.get(url)
this.destroyTile(url, tile)
}
}
/* Destroys the tiles which are not with the bounds of the app to free
* memory.
**/
destroyTiles(quadTrees) {
let count = 0
for (let [url, tile] of this.available.entries()) {
if (!quadTrees.has(url)) {
this.destroyTile(url, tile)
count += 1
}
}
if (count && this.debug)
console.log('destroyTiles', this.level, count)
}
destroyUnneededTiles() {
let count = 0
for (let [url, tile] of this.available.entries()) {
if (!this.needed.has(url)) {
this.destroyTile(url, tile)
count += 1
}
}
if (count && this.debug)
console.log('destroyUnneededTiles', this.level, count)
}
highlightInfos() {
let graphics = this.info
let color = this.infoColor
graphics.clear()
graphics.lineStyle(2, color)
for (let [col, row] of this.needed.values()) {
let dim = this.tileDimensions(col, row)
let pos = this.tilePosition(col, row)
graphics.beginFill(color, 0.2)
graphics.drawRect(pos.x + 1, pos.y + 1, dim.x - 2, dim.y - 2)
graphics.endFill()
}
let r = this._boundsRect
if (r != null) {
graphics.lineStyle(20, color)
graphics.drawRect(r.x, r.y, r.width, r.height)
graphics.moveTo(r.x, r.y)
graphics.lineTo(r.x + r.width, r.y + r.height)
graphics.moveTo(r.x, r.y + r.height)
graphics.lineTo(r.x + r.width, r.y)
}
let p = this._centerPoint
if (p != null) {
graphics.drawCircle(p.x, p.y, 20)
}
}
tintTiles(quadTrees) {
for (let [url, tile] of this.available.entries()) {
if (!quadTrees.has(url)) tile.tint = 0xff0000
}
}
untintTiles() {
for (let [url, tile] of this.available.entries()) {
tile.tint = 0xffffff
}
}
}