iwmlib/lib/pixi/deepzoom/image.js

1056 lines
35 KiB
JavaScript
Raw Permalink Normal View History

2019-03-21 09:57:27 +01:00
import { Capabilities } from '../../capabilities.js'
import { Points } from '../../utils.js'
import Tile from './tile.js'
2019-03-21 09:57:27 +01:00
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'
2019-08-06 15:34:57 +02:00
this.urlTileTemplate = this.urlTileTemplate || '{path}/{level}/{column}_{row}.{format}'
2019-03-21 09:57:27 +01:00
this.setupDimensions()
}
/* Computes the needed number of layers from the width and height
2019-08-06 15:34:57 +02:00
* of the image. Note that this includes the level 0, i.e. 0 ... 4
* means that 5 levels exist.
**/
2019-03-21 09:57:27 +01:00
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++) {
2019-08-06 15:34:57 +02:00
console.log(' ' + i + ' -> ' + this.getScale(i) + ' [' + this.dimensions(i) + ']')
2019-03-21 09:57:27 +01:00
}
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
2019-08-06 15:34:57 +02:00
return this.nw === null && this.ne === null && this.sw === null && this.se === null
2019-03-21 09:57:27 +01:00
}
/** 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,
2019-08-06 15:34:57 +02:00
world = null, // Defines the world bounds the images lives in
2019-03-21 09:57:27 +01:00
highResolution = true,
autoLoadTiles = true,
2019-07-04 09:17:43 +02:00
useWorker = '',
2019-03-21 09:57:27 +01:00
minimumLevel = 0,
alpha = 1,
app = window.app
} = {}
) {
super()
this.app = app
this.debug = debug
this.shadow = shadow
this.world = world
2019-07-04 09:17:43 +02:00
this.useWorker = useWorker
2019-08-06 15:34:57 +02:00
this.resolution = highResolution ? Math.round(window.devicePixelRatio) : 1
2019-03-21 09:57:27 +01:00
this.alpha = alpha
this.fastLoads = 0
this.active = true
2019-03-21 09:57:27 +01:00
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) {
2019-09-20 09:54:32 +02:00
this.filters = [new PIXI.filters.DropShadowFilter({ rotation: 45, distance: 3 })]
2019-03-21 09:57:27 +01:00
}
this.addChild(this.tileContainer)
if (deepZoomInfo.clip) {
let mask = new PIXI.Graphics()
2019-10-02 09:27:12 +02:00
mask.alpha = 0.0000001
2019-10-01 15:40:55 +02:00
mask.beginFill(0xff0000, 1)
2019-03-21 09:57:27 +01:00
mask.drawRect(0, 0, w, h)
mask.endFill()
this.mask = mask
this.addChild(mask)
this.minimumLevel = deepZoomInfo.baseLevel
}
this.currentLevel = Math.max(this.minimumLevel, deepZoomInfo.baseLevel)
//console.log("autoLoadTiles", this.autoLoadTiles)
2019-03-21 09:57:27 +01:00
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
2019-08-06 15:34:57 +02:00
return (Math.pow(2, delta) / this.resolution) * 2
2019-03-21 09:57:27 +01:00
}
/** 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() {
2019-09-10 16:34:59 +02:00
return new PIXI.Sprite.from(this.info.baseURL)
2019-03-21 09:57:27 +01:00
}
/** 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() {
2019-08-06 15:34:57 +02:00
let viewBounds = this.app.scene.bounds || this.app.scene.getBounds()
2019-03-21 09:57:27 +01:00
// Using getBounds extends visible scope after loading tiles and leads
// to excessive loading. So we prefer bounds over getBounds()
2019-03-21 09:57:27 +01:00
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
2019-08-06 15:34:57 +02:00
let bounds = new PIXI.Rectangle(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
2019-03-21 09:57:27 +01:00
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 } = {}) {
2019-08-06 15:34:57 +02:00
if (tiles.isComplete()) return
2019-03-21 09:57:27 +01:00
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)
2019-08-06 15:34:57 +02:00
} else {
2019-03-21 09:57:27 +01:00
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)
2019-08-06 15:34:57 +02:00
let tiles = new Tiles(level, this, invScale, cols, rows, w, h, this.info.tileSize, this.info.overlap)
2019-03-21 09:57:27 +01:00
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
2019-08-06 15:34:57 +02:00
* @param {number} level - The zoom level of the grid
*/
2019-03-21 09:57:27 +01:00
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.
2019-08-06 15:34:57 +02:00
* @param {*} tiles
2019-03-21 09:57:27 +01:00
*/
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
}
2019-03-21 09:57:27 +01:00
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
2019-08-06 15:34:57 +02:00
} else {
2019-03-21 09:57:27 +01:00
return
}
}
if (event.scale == null) {
this.ensureTiles(this.currentLevel, event.about)
return
}
2019-03-21 09:57:27 +01:00
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)
}
}
/**
2019-08-06 15:34:57 +02:00
*Activates the textures on the DeepZoomImage.
*
* @memberof DeepZoomImage
*/
2019-03-21 09:57:27 +01:00
activate() {
this.active = true
2019-03-21 09:57:27 +01:00
this.destroyTilesAboveLevel(this.currentLevel)
this.ensureTiles(this.currentLevel, null)
//console.log("Activate Textures!", this.currentLevel)
}
/**
2019-08-06 15:34:57 +02:00
*Dectivates the textures on the DeepZoomImage.
*
* @memberof DeepZoomImage
*/
2019-03-21 09:57:27 +01:00
deactivate() {
this.active = false
2019-03-21 09:57:27 +01:00
this.destroyAllTiles()
this.tileContainer.destroy({ children: true })
}
throwFinished() {
//console.log("throwFinished")
2019-03-21 09:57:27 +01:00
let key = this.currentLevel.toString()
let currentTiles = this.tileLayers[key]
if (typeof currentTiles == 'undefined') {
return
}
this.ensureTiles(this.currentLevel)
}
}