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({ rotation: 45, distance: 3 })] } this.addChild(this.tileContainer) if (deepZoomInfo.clip) { let mask = new PIXI.Graphics() mask.alpha = 0.0000001 mask.beginFill(0xff0000, 1) 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) 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.from(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) } }