Source: pixi/deepzoom/image.js

pixi/deepzoom/image.js

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.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)
    }
}