1056 lines
35 KiB
JavaScript
Executable File
1056 lines
35 KiB
JavaScript
Executable File
import { Capabilities } from '../../capabilities.js'
|
|
import { Points } from '../../utils.js'
|
|
import Tile from './tile.js'
|
|
import { Tiles } from './tiles.js'
|
|
|
|
function isEven(n) {
|
|
return n % 2 == 0
|
|
}
|
|
|
|
/**
|
|
* A utility class that holds information typically provided by DZI files, i.e.
|
|
* height and width of the overall image, overlap, and image type.
|
|
*
|
|
* @constructor
|
|
* @param {obj} attrs - A JSON-Object holding the listed keys and values
|
|
* @example
|
|
* {
|
|
* "tileSize": 1024,
|
|
* "format": "jpeg",
|
|
* "overlap": 1,
|
|
* "height": 4794,
|
|
* "width": 4095,
|
|
* "clip": { "minLevel": 12, "maxLevel": 20, "startCol": 301436, "startRow": 354060 },
|
|
* // optional: minLevel and maxLevel define the level bounds
|
|
* // startCol: first col at maxLevel
|
|
* // startRow: first row at maxLevel
|
|
* "path": "var/Vermeer/Vermeer_files",
|
|
* "type": "dzi", // optional: dzi (default) or map
|
|
* "urlTileTemplate": "{path}/{level}/{column}/{row}.{format}"
|
|
* // optional: {path}/{level}/{column}_{row}.{format} (default) or
|
|
* // a template String with the format of the URL
|
|
* }
|
|
*/
|
|
export class DeepZoomInfo {
|
|
constructor(attrs) {
|
|
for (let key in attrs) {
|
|
this[key] = attrs[key]
|
|
}
|
|
this.maxLevel = 0 // The highest level number, typically corresponds to the
|
|
// number in the file system for the folder with tiles
|
|
this.clip = this.clip || null // e.g. { level: 12, col: 301436, row: 354060 }
|
|
this.type = this.type || 'dzi'
|
|
this.urlTileTemplate = this.urlTileTemplate || '{path}/{level}/{column}_{row}.{format}'
|
|
this.setupDimensions()
|
|
}
|
|
|
|
/* Computes the needed number of layers from the width and height
|
|
* of the image. Note that this includes the level 0, i.e. 0 ... 4
|
|
* means that 5 levels exist.
|
|
**/
|
|
numLevels() {
|
|
let maxDimension = Math.max(this.width, this.height)
|
|
let boundary = this.type === 'dzi' ? 1 : this.tileSize
|
|
let numLevels = 0
|
|
while (maxDimension >= boundary) {
|
|
maxDimension /= 2
|
|
numLevels++
|
|
}
|
|
return numLevels
|
|
}
|
|
|
|
/** Computes the scale at the given level.
|
|
* @param {number} level - The level of the wanted layer
|
|
* @returns {number} scale
|
|
**/
|
|
getScale(level) {
|
|
let scale = 1
|
|
if (this.type === 'dzi') {
|
|
scale = Math.pow(0.5, this.maxLevel - level + 1)
|
|
} else {
|
|
scale = Math.pow(0.5, this.maxLevel - level)
|
|
}
|
|
return scale
|
|
}
|
|
|
|
/** Computes the scaled width and height of the given level.
|
|
* @param {number} level - The level of the wanted layer
|
|
* @returns {array} size - The width and height
|
|
**/
|
|
getDimensions(level) {
|
|
let scale = this.getScale(level)
|
|
let w = Math.ceil(this.width * scale)
|
|
let h = Math.ceil(this.height * scale)
|
|
return [w, h]
|
|
}
|
|
|
|
/** Computes the number of cols and rows of tiles.
|
|
* @param {number} level - The level of the wanted layer
|
|
* @returns {array} size - The cols and rows
|
|
**/
|
|
getNumTiles(level) {
|
|
let dim = this.getDimensions(level)
|
|
let cols = Math.ceil(dim[0] / this.tileSize)
|
|
let rows = Math.ceil(dim[1] / this.tileSize)
|
|
if (this.clip) {
|
|
let rest = this.rests[level]
|
|
if (rest) {
|
|
if (rest.restCol) {
|
|
cols += 1
|
|
}
|
|
if (rest.restRows) {
|
|
rows += 1
|
|
}
|
|
}
|
|
}
|
|
return [cols, rows]
|
|
}
|
|
|
|
setupDimensions(loadBaseImage = false) {
|
|
/** Setup instance variables and load the base image, i.e. the largest
|
|
image that can be represented as a single tile.
|
|
@private
|
|
**/
|
|
let ww = this.width
|
|
let hh = this.height
|
|
let scale = 1.0
|
|
let level = 0
|
|
let single = 0
|
|
const tsize = this.tileSize
|
|
|
|
if (this.clip) {
|
|
this.baseLevel = this.clip.minLevel
|
|
this.maxLevel = this.clip.maxLevel
|
|
this.baseImage = null
|
|
this.size = this.getDimensions(this.baseLevel)
|
|
this.offsets = {}
|
|
this.rests = {}
|
|
let startCol = this.clip.startCol
|
|
let startRow = this.clip.startRow
|
|
let floatStartCol = startCol
|
|
let floatStartRow = startRow
|
|
for (let i = this.maxLevel; i >= this.baseLevel; i--) {
|
|
this.offsets[i] = { startCol, startRow }
|
|
let restCol = floatStartCol % 1
|
|
let restRow = floatStartRow % 1
|
|
this.rests[i] = { restCol, restRow }
|
|
startCol = Math.floor(startCol / 2)
|
|
startRow = Math.floor(startRow / 2)
|
|
floatStartCol /= 2
|
|
floatStartRow /= 2
|
|
}
|
|
} else {
|
|
const boundary = this.type === 'dzi' ? 1.0 : tsize
|
|
while (ww > boundary && hh > boundary) {
|
|
if (ww >= tsize && hh >= tsize) {
|
|
single += 1
|
|
}
|
|
scale = scale / 2.0
|
|
ww = Math.ceil(this.width * scale)
|
|
hh = Math.ceil(this.height * scale)
|
|
level += 1
|
|
}
|
|
this.baseLevel = level - single
|
|
this.maxLevel = this.numLevels() - 1
|
|
this.baseURL = this.urlForTile(this.baseLevel, 0, 0, false)
|
|
|
|
if (loadBaseImage) {
|
|
this.imageForURL(this.baseURL, e => {
|
|
this.size = [e.target.naturalWidth, e.target.naturalHeight]
|
|
this.baseImage = e.target
|
|
})
|
|
} else {
|
|
this.baseImage = null
|
|
this.size = this.getDimensions(this.baseLevel)
|
|
}
|
|
}
|
|
}
|
|
|
|
get maxLoadableLevel() {
|
|
if (this.clip) {
|
|
return this.maxLevel
|
|
}
|
|
return this.type === 'dzi' ? this.maxLevel : this.maxLevel
|
|
}
|
|
|
|
/** Computes the url for the given level, column and and row.
|
|
* @param {number} level - The level of the wanted layer
|
|
* @param {number} column - The column of the tile
|
|
* @param {number} row - The row of the tile
|
|
* @returns {string} url
|
|
**/
|
|
urlForTile(level, column, row, compressed = true) {
|
|
let format = this.format
|
|
if (compressed && this.compression) {
|
|
let supported = Capabilities.isIOS ? 'pvr' : 'dds'
|
|
if (this.compression.indexOf(supported) >= 0) {
|
|
format = supported
|
|
}
|
|
}
|
|
if (this.clip) {
|
|
let offset = this.offsets[level]
|
|
if (offset) {
|
|
let { startCol, startRow } = offset
|
|
column += startCol
|
|
row += startRow
|
|
}
|
|
}
|
|
let url = this.urlTileTemplate
|
|
.replace(/\{path\}/g, this.path)
|
|
.replace(/\{level\}/g, level)
|
|
.replace(/\{row\}/g, row)
|
|
.replace(/\{column\}/g, column)
|
|
.replace(/\{format\}/g, format)
|
|
return url
|
|
}
|
|
|
|
/** Loads the image for the given url and executes a callback function
|
|
on completion.
|
|
* @param {string} url - The url of the tile
|
|
* @param {function} complete - The callback function
|
|
* @returns {Image} obj
|
|
**/
|
|
imageForURL(url, complete) {
|
|
let img = new Image()
|
|
img.onload = complete.bind(this)
|
|
img.src = url
|
|
return img
|
|
}
|
|
|
|
/** Computes the columns and rows as well as scaled width and height.
|
|
* @param {number} level - The level of the wanted layer
|
|
* @returns {array} [cols, rows, width, height]
|
|
**/
|
|
dimensions(level) {
|
|
let dim = this.getDimensions(level)
|
|
let tiles = this.getNumTiles(level)
|
|
return [tiles[0], tiles[1], dim[0], dim[1]]
|
|
}
|
|
|
|
test() {
|
|
//console.log("w=" + this.width + " h=" + this.height + " maxlevel=" + this.maxLevel + " base=" + this.baseLevel)
|
|
for (let i = 0; i <= this.maxLevel; i++) {
|
|
console.log(' ' + i + ' -> ' + this.getScale(i) + ' [' + this.dimensions(i) + ']')
|
|
}
|
|
console.log(this.urlForTile(this.baseLevel, 0, 0))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A utility class that describes a quad tree of tiles. Each tile on a given
|
|
* level has up to four corresponding tiles on the next level. A TileQuadNode
|
|
* uses the attributes nw (i.e. northwest), ne, sw, se to link to the
|
|
* quad nodes on the next level. The previous attributes links to the quad
|
|
* one level below that holds the given quad as nw, ne, sw, or se.
|
|
* We use this node class because we need a representation of tiles that are
|
|
* needed but not loaded yet to compute tiles which can be abandoned to reduce
|
|
* the memory pressure.
|
|
*
|
|
* @constructor
|
|
* @param {level} Number - The level the quad node belongs to
|
|
* @param {col} Number - The col of the quad
|
|
* @param {row} Number - The level the quad node belongs to
|
|
* @param {url} String - The level the quad node belongs to
|
|
*/
|
|
class TileQuadNode {
|
|
constructor(level, col, row, url) {
|
|
this.level = level
|
|
this.col = col
|
|
this.row = row
|
|
this.url = url
|
|
this.nw = null
|
|
this.ne = null
|
|
this.sw = null
|
|
this.se = null
|
|
this.previous = null
|
|
}
|
|
|
|
/** Return True if this node has no successors and can be used as
|
|
an indicator of tiles to free.
|
|
**/
|
|
noQuads() {
|
|
if (this.previous === null) return false
|
|
return this.nw === null && this.ne === null && this.sw === null && this.se === null
|
|
}
|
|
|
|
/** Unlink the given quad node
|
|
|
|
* @param {node} TileQuadNode - The TileQuadNode to remove
|
|
**/
|
|
unlink(node) {
|
|
if (this.nw === node) this.nw = null
|
|
if (this.ne === node) this.ne = null
|
|
if (this.sw === node) this.sw = null
|
|
if (this.se === node) this.se = null
|
|
}
|
|
|
|
/** Link this quad node to the given previous node. Use the north
|
|
* and west flags to address nw, ne, sw, and se.
|
|
|
|
* @param {node} TileQuadNode - The TileQuadNode to remove
|
|
* @param {north} Boolean - Link to north (true) or south (false)
|
|
* @param {west} Boolean - Link to west (true) or east (false)
|
|
**/
|
|
link(north, west, previous) {
|
|
this.previous = previous
|
|
if (north) {
|
|
if (west) {
|
|
previous.nw = this
|
|
} else {
|
|
previous.ne = this
|
|
}
|
|
} else {
|
|
if (west) {
|
|
previous.sw = this
|
|
} else {
|
|
previous.se = this
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
|
|
* @constructor
|
|
* @param {DeepZoomInfo} deepZoomInfo - Information extracted from a JSON-Object
|
|
*/
|
|
export class DeepZoomImage extends PIXI.Container {
|
|
constructor(
|
|
deepZoomInfo,
|
|
{
|
|
debug = false,
|
|
shadow = false,
|
|
center = false,
|
|
world = null, // Defines the world bounds the images lives in
|
|
highResolution = true,
|
|
autoLoadTiles = true,
|
|
useWorker = '',
|
|
minimumLevel = 0,
|
|
alpha = 1,
|
|
app = window.app
|
|
} = {}
|
|
) {
|
|
super()
|
|
this.app = app
|
|
this.debug = debug
|
|
this.shadow = shadow
|
|
this.world = world
|
|
this.useWorker = useWorker
|
|
this.resolution = highResolution ? Math.round(window.devicePixelRatio) : 1
|
|
this.alpha = alpha
|
|
this.fastLoads = 0
|
|
this.active = true
|
|
this.autoLoadTiles = autoLoadTiles
|
|
this.minimumLevel = minimumLevel
|
|
this.quadTrees = new Map() // url as keys, TileQuadNodes as values
|
|
this.setup(deepZoomInfo, center)
|
|
}
|
|
|
|
get point() {
|
|
if (this._point == null) {
|
|
let graphics = new PIXI.Graphics()
|
|
graphics.lineStyle(2, 0x00ff00)
|
|
graphics.drawCircle(0, 0, 2)
|
|
graphics.interactive = false
|
|
this._point = graphics
|
|
}
|
|
return this._point
|
|
}
|
|
|
|
/** Reads the DeepZoomInfo object and initializes all tile layers.
|
|
* Called by the constructor.
|
|
* Creates the sprite for the loaded texture and add the sprite to the tile
|
|
* layer.
|
|
* @param {Object} deepZoomInfo - the DeepZoomInfo instance
|
|
* @param {boolean} center - If true ensures that the pivot is set to the center
|
|
**/
|
|
setup(deepZoomInfo, center) {
|
|
this.info = deepZoomInfo
|
|
this.interactive = true
|
|
this.tileLayers = {}
|
|
|
|
this._foreground = null
|
|
this.tileContainer = new PIXI.Container()
|
|
this.tileContainer.interactive = false
|
|
|
|
let [w, h] = this.baseSize
|
|
if (this.shadow) {
|
|
this.filters = [new PIXI.filters.DropShadowFilter(45, 3)]
|
|
}
|
|
this.addChild(this.tileContainer)
|
|
|
|
if (deepZoomInfo.clip) {
|
|
let mask = new PIXI.Graphics()
|
|
mask.beginFill(1, 1)
|
|
mask.drawRect(0, 0, w, h)
|
|
mask.endFill()
|
|
this.mask = mask
|
|
mask.alpha = 0
|
|
this.addChild(mask)
|
|
this.minimumLevel = deepZoomInfo.baseLevel
|
|
}
|
|
this.currentLevel = Math.max(this.minimumLevel, deepZoomInfo.baseLevel)
|
|
//console.log("autoLoadTiles", this.autoLoadTiles)
|
|
if (this.autoLoadTiles) {
|
|
this.setupTiles(center)
|
|
}
|
|
}
|
|
|
|
/** Default setup method for tiles. Loads all tiles of the current level.
|
|
Can be overwritten in subclasses.
|
|
@param {boolean} center - If true ensures that the pivot is set to the center
|
|
**/
|
|
setupTiles(center = false) {
|
|
// First load background tile
|
|
let tiles = this.ensureAllTiles(this.currentLevel)
|
|
if (center) {
|
|
this.pivot.set(w / 2, h / 2)
|
|
}
|
|
let scaleLevel = this.levelForScale(1)
|
|
this.ensureAllTiles(scaleLevel)
|
|
}
|
|
|
|
removeTileQuadNode(level, col, row, url) {
|
|
if (this.quadTrees.has(url)) {
|
|
let quad = this.quadTrees.get(url)
|
|
this.tileQuadRemoved(quad)
|
|
this.quadTrees.delete(url)
|
|
}
|
|
}
|
|
|
|
addTileQuadNode(level, col, row, url) {
|
|
if (this.quadTrees.has(url)) return this.quadTrees.get(url)
|
|
let quad = new TileQuadNode(level, col, row, url)
|
|
this.quadTrees.set(url, quad)
|
|
this.tileQuadAdded(quad)
|
|
return quad
|
|
}
|
|
|
|
tileQuadRemoved(quad) {
|
|
let below = quad.previous
|
|
// if (this.debug) console.log("tileQuadRemoved", quad)
|
|
if (below) {
|
|
below.unlink(quad)
|
|
if (below.noQuads()) {
|
|
if (this.debug) console.log('Removed tile below')
|
|
let levelBelow = quad.level - 1
|
|
if (levelBelow < this.minimumLevel) return
|
|
let c = Math.floor(quad.col / 2)
|
|
let r = Math.floor(quad.row / 2)
|
|
let urlBelow = this.info.urlForTile(levelBelow, c, r)
|
|
if (this.quadTrees.has(urlBelow)) {
|
|
this.removeTileQuadNode(levelBelow, c, r, urlBelow)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tileQuadAdded(quad) {
|
|
let levelBelow = quad.level - 1
|
|
if (levelBelow < this.minimumLevel) return
|
|
//if (this.debug) console.log("tileQuadAdded", quad)
|
|
let c = Math.floor(quad.col / 2)
|
|
let r = Math.floor(quad.row / 2)
|
|
let urlBelow = this.info.urlForTile(levelBelow, c, r)
|
|
let below = null
|
|
if (!this.quadTrees.has(urlBelow)) {
|
|
below = this.addTileQuadNode(levelBelow, c, r, urlBelow)
|
|
quad.link(isEven(quad.row), isEven(quad.col), below)
|
|
}
|
|
}
|
|
|
|
/** Returns the tile layer level that corresponds to the given scale.
|
|
* @param {number} scale - the scale factor
|
|
**/
|
|
levelForScale(scale) {
|
|
let level = Math.round(Math.log2(scale * this.resolution)) // Math.floor(Math.log2(event.scale))+1
|
|
let newLevel = this.info.baseLevel + Math.max(level, 0)
|
|
return Math.min(newLevel, this.info.maxLoadableLevel)
|
|
}
|
|
|
|
/** Returns the tile layer level that corresponds to the given scale.
|
|
* @param {number} scale - the scale factor
|
|
**/
|
|
levelAndAlphaForScale(scale) {
|
|
let value = Math.log2(scale * this.resolution)
|
|
let level = Math.round(value)
|
|
let newLevel = this.info.baseLevel + Math.max(level, 0)
|
|
|
|
return { level: Math.min(newLevel, this.info.maxLoadableLevel), alpha: value - level }
|
|
}
|
|
|
|
/** Adds a tile layer to the DeepZoomImage.
|
|
* @param {string} key - the access key
|
|
* @param {Tiles} tiles - the tile layer object
|
|
**/
|
|
addTiles(key, tiles) {
|
|
if (key in this.tileLayers) {
|
|
console.warn('Tiles already availabl', key)
|
|
}
|
|
this.tileContainer.addChild(tiles)
|
|
this.tileLayers[key] = tiles
|
|
}
|
|
|
|
destroyTiles(key) {
|
|
let tiles = this.tileLayers[key]
|
|
this.tileContainer.removeChild(tiles)
|
|
tiles.destroy()
|
|
delete this.tileLayers[key]
|
|
}
|
|
|
|
/** Getter for PIXI.Container foreground layer.
|
|
* Adds a PIXI.Container if necessary.
|
|
**/
|
|
get foreground() {
|
|
if (this._foreground == null) {
|
|
this._foreground = new PIXI.Container()
|
|
this.addChild(this._foreground)
|
|
}
|
|
return this._foreground
|
|
}
|
|
|
|
/** Getter for the DeepZoomInfo base level size.
|
|
**/
|
|
get baseSize() {
|
|
return this.info.getDimensions(this.info.baseLevel)
|
|
}
|
|
|
|
/** Getter for the current scaled size in pixels.
|
|
**/
|
|
get pixelSize() {
|
|
let [w, h] = this.baseSize
|
|
return [w * this.scale.x, h * this.scale.y]
|
|
}
|
|
|
|
/** Getter for the max scale factor.
|
|
**/
|
|
get maxScale() {
|
|
let delta = this.info.maxLevel - this.info.baseLevel
|
|
return (Math.pow(2, delta) / this.resolution) * 2
|
|
}
|
|
|
|
/** Getter for the current width.
|
|
**/
|
|
get width() {
|
|
return this.pixelSize[0]
|
|
}
|
|
|
|
/** Getter for the current height.
|
|
**/
|
|
get height() {
|
|
return this.pixelSize[1]
|
|
}
|
|
|
|
/* Overrides PIXI.Container.hitArea()
|
|
* Allows to optimize the hit testing. Container with hit areas are directly
|
|
* hit tested without consideration of children.
|
|
*/
|
|
get hitArea() {
|
|
// Defining the hitArea resulted hitting the scatter in masked area
|
|
// when a mask was used (@Tüsch[submaps]). Removing the hitArea() altogether
|
|
// broke the interaction in other projects (@googleart).
|
|
// Fix: When masked, the hitArea is ignored by returning null.
|
|
// TODO: test if childs are hittested, without setting interactiveChildren.
|
|
// Opel, 03-05-2018
|
|
if (this.mask) {
|
|
return null
|
|
}
|
|
return this
|
|
}
|
|
|
|
/* Overrides PIXI.Container.contains()
|
|
* Allows to optimize the hit testing.
|
|
*/
|
|
contains(x, y) {
|
|
let [w, h] = this.baseSize
|
|
return x >= 0 && x <= w && y >= 0 && y <= h
|
|
}
|
|
|
|
/** Overrides PIXI.Container._calculateBounds()
|
|
* Only considers the base size and reduces the calculation to a single
|
|
* rect.
|
|
*/
|
|
_calculateBounds() {
|
|
let [w, h] = this.baseSize
|
|
this._bounds.addFrame(this.transform, 0, 0, w, h)
|
|
}
|
|
|
|
/** Overrides PIXI.Container.calculateBounds()
|
|
* Skips the children and only considers the deep zoom base size. Calls
|
|
* the also overwritten _calculateBounds method.
|
|
*/
|
|
calculateBounds() {
|
|
this._bounds.clear()
|
|
this._calculateBounds()
|
|
this._lastBoundsID = this._boundsID
|
|
}
|
|
|
|
/** Returns a single sprite that can be used a thumbnail representation of
|
|
* large images.
|
|
* @return {Sprite} sprite - A sprite with a single tile texture
|
|
*/
|
|
thumbnail() {
|
|
return new PIXI.Sprite.fromImage(this.info.baseURL)
|
|
}
|
|
|
|
/** Returns a list of all tiles of a given level.
|
|
* @param {Tiles} tiles - the grid of tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
* @return {Array[]} - An array of [url, col, row] arrays
|
|
**/
|
|
allTiles(tiles, level) {
|
|
let result = []
|
|
for (let col = 0; col < tiles.cols; col++) {
|
|
for (let row = 0; row < tiles.rows; row++) {
|
|
let url = this.info.urlForTile(level, col, row)
|
|
result.push([url, col, row])
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
worldBounds() {
|
|
let viewBounds = this.app.scene.bounds || this.app.scene.getBounds()
|
|
// Using getBounds extends visible scope after loading tiles and leads
|
|
// to excessive loading. So we prefer bounds over getBounds()
|
|
if (this.world != null) {
|
|
let bounds = this.world.bounds
|
|
let x = Math.max(-bounds.width, bounds.x)
|
|
let y = Math.max(-bounds.height, bounds.y)
|
|
let width = Math.min(viewBounds.width, bounds.width)
|
|
let height = Math.min(viewBounds.height, bounds.height)
|
|
//console.log("worldBounds new", { x, y, width, height })
|
|
return { x, y, width, height }
|
|
}
|
|
//console.log("worldBounds old", viewBounds)
|
|
return viewBounds
|
|
}
|
|
|
|
/** Loads all tiles that are needed to fill the app bounds.
|
|
* @param {Tiles} tiles - the grid of tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
* @param {boolean} debug
|
|
* @return {Array[]} - An array of [url, col, row] arrays
|
|
*/
|
|
neededTiles(tiles, level, debug = false) {
|
|
let needed = []
|
|
let tsize = tiles.tileSize
|
|
let worldBounds = this.worldBounds()
|
|
let maxWidth = worldBounds.width
|
|
let maxHeight = worldBounds.height
|
|
|
|
let pointInWindow = new PIXI.Point()
|
|
let worldTopLeft = new PIXI.Point(worldBounds.x, worldBounds.y)
|
|
let worldBottomRight = new PIXI.Point(worldBounds.x + maxWidth, worldBounds.y + maxHeight)
|
|
let worldCenter = new PIXI.Point(worldBounds.x + maxWidth / 2, worldBounds.y + maxHeight / 2)
|
|
let tilesCenter = tiles.toLocal(worldCenter)
|
|
|
|
let topLeft = tiles.toLocal(worldTopLeft)
|
|
let bottomRight = tiles.toLocal(worldBottomRight)
|
|
tiles._centerPoint = tilesCenter
|
|
let bounds = new PIXI.Rectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
|
|
|
|
tiles._boundsRect = bounds
|
|
|
|
/* UO: we need a toLocal call here since the transform may need an update
|
|
which is guaranteed by the toLocal method. */
|
|
let centerCol = Math.floor(tilesCenter.x / tsize)
|
|
let centerRow = Math.floor(tilesCenter.y / tsize)
|
|
|
|
// Expand because we want to test for included centers
|
|
bounds.x -= tsize / 2
|
|
bounds.y -= tsize / 2
|
|
bounds.width += tsize
|
|
bounds.height += tsize
|
|
|
|
try {
|
|
let maxTilesWidth = Math.ceil(maxWidth / tsize)
|
|
let maxTilesHeight = Math.ceil(maxHeight / tsize)
|
|
|
|
maxTilesWidth += 2
|
|
maxTilesHeight += 2
|
|
|
|
let startCol = Math.max(0, centerCol - maxTilesWidth)
|
|
let endCol = Math.min(tiles.cols, centerCol + maxTilesWidth)
|
|
|
|
let startRow = Math.max(0, centerRow - maxTilesHeight)
|
|
let endRow = Math.min(tiles.rows, centerRow + maxTilesHeight)
|
|
|
|
for (let col = startCol; col < endCol; col++) {
|
|
let cx = (col + 0.5) * tsize
|
|
for (let row = startRow; row < endRow; row++) {
|
|
let cy = (row + 0.5) * tsize
|
|
let tileCenter = new PIXI.Point(cx, cy)
|
|
if (bounds.contains(tileCenter.x, tileCenter.y)) {
|
|
let url = this.info.urlForTile(level, col, row)
|
|
needed.push([url, col, row])
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(error.message)
|
|
}
|
|
return { centerCol, centerRow, needed }
|
|
}
|
|
|
|
/** Returns all changed tiles for a given level.
|
|
* @param {Tiles} tiles - the grid of tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
* @return {object} - An object with the keys added and removed which values are [url, col, row] arrays
|
|
*/
|
|
changedTiles(tiles, level) {
|
|
if (this.debug) console.time('changedTiles')
|
|
let changed = { added: [], removed: [] }
|
|
let newNeeded = new Map()
|
|
let { centerCol, centerRow, needed } = this.neededTiles(tiles, level)
|
|
needed.forEach(d => {
|
|
let [url, col, row] = d
|
|
newNeeded.set(url, [col, row])
|
|
if (!tiles.requested.has(url)) {
|
|
changed.added.push(d)
|
|
}
|
|
})
|
|
for (let url of tiles.needed.keys()) {
|
|
if (!newNeeded.has(url)) {
|
|
let [col, row] = tiles.needed.get(url)
|
|
changed.removed.push([url, col, row])
|
|
}
|
|
}
|
|
tiles.needed = newNeeded
|
|
if (this.debug) console.log(newNeeded)
|
|
if (this.debug) console.timeEnd('changedTiles')
|
|
return { centerCol, centerRow, changed }
|
|
}
|
|
|
|
/** Populates all tiles for a given level.
|
|
* @param {Tiles} tiles - the grid of tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
*/
|
|
populateAllTiles(tiles, level) {
|
|
let all = this.allTiles(tiles, level)
|
|
for (let [url, col, row] of all) {
|
|
this.addTileQuadNode(level, col, row, url)
|
|
}
|
|
tiles.loadTiles(all, false, 0, 0)
|
|
}
|
|
|
|
/** Loads all tiles that are needed to fill the browser window.
|
|
* If the optional about parameter is provided (as a point with col as x,
|
|
* and row as y attr) the tiles are sorted by the distance to this point.
|
|
*
|
|
* @param {Tiles} tiles - the grid of tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
* Optional parameter:
|
|
* @param {boolean} onlyone - if true only one tile is loaded
|
|
* @param {PIXI.Point} about - point of interaction
|
|
*/
|
|
populateTiles(tiles, level, { onlyone = false, about = null } = {}) {
|
|
if (tiles.isComplete()) return
|
|
let referenceCol = -1
|
|
let referenceRow = -1
|
|
let { centerCol, centerRow, changed } = this.changedTiles(tiles, level)
|
|
if (about != null) {
|
|
// We want to load tiles in the focus of the user first, therefore
|
|
// we sort according to the distance of the focus of interaction
|
|
let refPoint = this.toLocal(about)
|
|
let scaledTileSize = tiles.tileSize * tiles.tileScale
|
|
referenceCol = Math.floor(refPoint.x / scaledTileSize)
|
|
referenceRow = Math.floor(refPoint.y / scaledTileSize)
|
|
} else {
|
|
referenceCol = centerCol
|
|
referenceRow = centerRow
|
|
}
|
|
referenceCol = centerCol
|
|
referenceRow = centerRow
|
|
|
|
let removed = changed.removed
|
|
for (let [url, col, row] of removed) {
|
|
this.removeTileQuadNode(level, col, row, url)
|
|
}
|
|
let added = changed.added
|
|
if (added.length == 0) return
|
|
for (let [url, col, row] of added) {
|
|
this.addTileQuadNode(level, col, row, url)
|
|
}
|
|
let ref = new PIXI.Point(referenceCol, referenceRow)
|
|
// Note: The array must be sorted in a way that the nearest tiles
|
|
// are at the end of the array since the load queue uses Array.push
|
|
// Array.pop
|
|
added.sort((a, b) => {
|
|
let aa = new PIXI.Point(a[1], a[2])
|
|
let bb = new PIXI.Point(b[1], b[2])
|
|
let da = Points.distance(aa, ref)
|
|
let db = Points.distance(bb, ref)
|
|
return db - da
|
|
})
|
|
tiles.loadTiles(added, onlyone, referenceCol, referenceRow)
|
|
}
|
|
|
|
/** Private method: creates all tiles for a given level.
|
|
* @param {number} level - The zoom level of the grid
|
|
* @return {Tiles} - tiles
|
|
*/
|
|
_createTiles(key, level) {
|
|
let [cols, rows, w, h] = this.info.dimensions(level)
|
|
let increasedLevels = level - this.info.baseLevel
|
|
let invScale = Math.pow(0.5, increasedLevels)
|
|
let tiles = new Tiles(level, this, invScale, cols, rows, w, h, this.info.tileSize, this.info.overlap)
|
|
this.addTiles(key, tiles)
|
|
if (this.info.clip) {
|
|
let rest = this.info.rests[level]
|
|
if (rest) {
|
|
let x = rest.restCol * this.info.tileSize * invScale
|
|
let y = rest.restRow * this.info.tileSize * invScale
|
|
tiles.x = -x
|
|
tiles.y = -y
|
|
}
|
|
}
|
|
return tiles
|
|
}
|
|
|
|
/** Ensures that all needed tiles of a given level are loaded. Creates
|
|
* a new Tiles layer if necessary
|
|
* @param {number} level - The zoom level of the grid
|
|
* @return {Tiles} tiles
|
|
*/
|
|
ensureTiles(level, about) {
|
|
let key = level.toString()
|
|
if (key in this.tileLayers) {
|
|
let tiles = this.tileLayers[key]
|
|
this.populateTiles(tiles, level, { about: about })
|
|
return tiles
|
|
}
|
|
let tiles = this._createTiles(key, level)
|
|
this.populateTiles(tiles, level, { about: about })
|
|
//console.log("ensureTiles", level)
|
|
return tiles
|
|
}
|
|
|
|
untintTiles(level) {
|
|
let key = level.toString()
|
|
if (key in this.tileLayers) {
|
|
let tiles = this.tileLayers[key]
|
|
}
|
|
}
|
|
|
|
/** Ensures that all tiles of a given level are loaded.
|
|
* @param {number} level - The zoom level of the grid
|
|
*/
|
|
ensureAllTiles(level) {
|
|
let key = level.toString()
|
|
if (key in this.tileLayers) {
|
|
let tiles = this.tileLayers[key]
|
|
this.populateAllTiles(tiles, level)
|
|
tiles.keep = true
|
|
return
|
|
}
|
|
let tiles = this._createTiles(key, level)
|
|
this.populateAllTiles(tiles, level)
|
|
tiles.keep = true
|
|
return tiles
|
|
}
|
|
|
|
hideTilesAboveLevel(level) {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (tiles.level > level) {
|
|
tiles.visible = false
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Destroys all tiles above a given level to ensure that the memory can
|
|
* be reused.
|
|
* @param {number} level - The zoom level of the grid
|
|
*/
|
|
destroyTilesAboveLevel(level) {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (tiles.level > level && !tiles.keep) {
|
|
for (let url of tiles.available.keys()) {
|
|
let quad = this.quadTrees.get(url)
|
|
if (quad) this.removeTileQuadNode(quad)
|
|
}
|
|
this.destroyTiles(key)
|
|
}
|
|
})
|
|
}
|
|
|
|
destroyAllTiles() {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
this.destroyTiles(key)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Tint tiles in all layers that are no longer needed
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
tintObsoleteTiles() {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
tiles.untintTiles()
|
|
if (!tiles.keep) {
|
|
tiles.tintObsoleteTiles()
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Destroy tiles in all layers that are no longer needed
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
destroyUnneededTiles() {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (!tiles.keep) {
|
|
tiles.destroyUnneededTiles()
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Destroy tiles in all layers that are no longer needed
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
destroyObsoleteTiles() {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (!tiles.keep) {
|
|
tiles.destroyObsoleteTiles()
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Destroy tiles in all layers that are not part of the
|
|
* visible quadTrees
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
destroyTiles() {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (!tiles.keep) {
|
|
tiles.destroyTiles(this.quadTrees)
|
|
}
|
|
})
|
|
}
|
|
|
|
/* Tint all tiles
|
|
* @param {number} level - The zoom level of the grid
|
|
*/
|
|
tintTilesBelowLevel(level) {
|
|
Object.keys(this.tileLayers).forEach(key => {
|
|
let tiles = this.tileLayers[key]
|
|
if (tiles.level < level) {
|
|
tiles.tintTiles(this.quadTrees)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Ensure that the given tiles layer is the topmost one and visible.
|
|
* @param {*} tiles
|
|
*/
|
|
bringTilesToFront(tiles) {
|
|
this.tileContainer.addChild(tiles)
|
|
tiles.visible = true
|
|
}
|
|
|
|
/** A callback function that can be used by a Scatter view to inform
|
|
* the zoomable image that it has been moved, rotated or scaled, and should
|
|
* load tiles accordingly.
|
|
* @param {PIXI.Point} translated - the movement of the scatter
|
|
* @param {number} scale - the zoom factor
|
|
* @param {PIXI.Point} about - the anchor point of the zoom
|
|
* @param {boolean} fast - informs the callback to return as fast as possible,
|
|
* i.e. after loading a single tile
|
|
* @param {boolean} debug - log debug infos
|
|
*/
|
|
transformed(event) {
|
|
if (!this.active) {
|
|
return
|
|
}
|
|
let key = this.currentLevel.toString()
|
|
let currentTiles = this.tileLayers[key]
|
|
if (typeof currentTiles == 'undefined') {
|
|
return
|
|
}
|
|
if (event.fast) {
|
|
this.fastLoads += 1
|
|
this.populateTiles(currentTiles, this.currentLevel, {
|
|
onlyone: false,
|
|
about: event.about
|
|
})
|
|
if (this.fastLoads == 3) {
|
|
this.fastLoads = 0
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
if (event.scale == null) {
|
|
this.ensureTiles(this.currentLevel, event.about)
|
|
return
|
|
}
|
|
|
|
let level = this.levelForScale(event.scale)
|
|
let newLevel = Math.max(level, this.minimumLevel)
|
|
if (newLevel != this.currentLevel) {
|
|
if (!currentTiles.keep) {
|
|
currentTiles.loader.cancel()
|
|
}
|
|
this.hideTilesAboveLevel(newLevel)
|
|
currentTiles = this.ensureTiles(newLevel, event.about)
|
|
this.currentLevel = newLevel
|
|
} else {
|
|
this.ensureTiles(this.currentLevel, event.about)
|
|
}
|
|
this.bringTilesToFront(currentTiles)
|
|
if (this._foreground) {
|
|
this.addChild(this._foreground)
|
|
}
|
|
}
|
|
|
|
/**
|
|
*Activates the textures on the DeepZoomImage.
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
activate() {
|
|
this.active = true
|
|
this.destroyTilesAboveLevel(this.currentLevel)
|
|
this.ensureTiles(this.currentLevel, null)
|
|
//console.log("Activate Textures!", this.currentLevel)
|
|
}
|
|
|
|
/**
|
|
*Dectivates the textures on the DeepZoomImage.
|
|
*
|
|
* @memberof DeepZoomImage
|
|
*/
|
|
deactivate() {
|
|
this.active = false
|
|
this.destroyAllTiles()
|
|
this.tileContainer.destroy({ children: true })
|
|
}
|
|
|
|
throwFinished() {
|
|
//console.log("throwFinished")
|
|
let key = this.currentLevel.toString()
|
|
let currentTiles = this.tileLayers[key]
|
|
if (typeof currentTiles == 'undefined') {
|
|
return
|
|
}
|
|
this.ensureTiles(this.currentLevel)
|
|
}
|
|
}
|