UNPKG

mapbox-gl

Version:
344 lines (303 loc) 10.6 kB
'use strict'; var Tile = require('./tile'); var TileCoord = require('./tile_coord'); var Point = require('point-geometry'); var Cache = require('../util/mru_cache'); var util = require('../util/util'); module.exports = TilePyramid; /** * A tile pyramid is a specialized cache and datastructure * that contains tiles. It's used by sources to manage their * data. * * @param {Object} options * @param {number} options.tileSize * @param {number} options.minzoom * @param {number} options.maxzoom * @private */ function TilePyramid(options) { this.tileSize = options.tileSize; this.minzoom = options.minzoom; this.maxzoom = options.maxzoom; this.reparseOverscaled = options.reparseOverscaled; this._load = options.load; this._abort = options.abort; this._unload = options.unload; this._add = options.add; this._remove = options.remove; this._redoPlacement = options.redoPlacement; this._tiles = {}; this._cache = new Cache(options.cacheSize, function(tile) { return this._unload(tile); }.bind(this)); } TilePyramid.prototype = { /** * Confirm that every tracked tile is loaded. * @returns {boolean} whether all tiles are loaded. * @private */ loaded: function() { for (var t in this._tiles) { if (!this._tiles[t].loaded) return false; } return true; }, /** * Return all tile ids ordered with z-order, and cast to numbers * @returns {Array<number>} ids * @private */ orderedIDs: function() { return Object.keys(this._tiles) .sort(function(a, b) { return (b % 32) - (a % 32); }) .map(function(id) { return +id; }); }, renderedIDs: function() { return this.orderedIDs().filter(function(id) { return this._tiles[id].loaded && !this._coveredTiles[id]; }.bind(this)); }, reload: function() { this._cache.reset(); for (var i in this._tiles) { this._load(this._tiles[i]); } }, /** * Get a specific tile by id * @param {string|number} id tile id * @returns {Object} tile * @private */ getTile: function(id) { return this._tiles[id]; }, /** * get the zoom level adjusted for the difference in map and source tilesizes * @param {Object} transform * @returns {number} zoom level * @private */ getZoom: function(transform) { return transform.zoom + Math.log(transform.tileSize / this.tileSize) / Math.LN2; }, /** * Return a zoom level that will cover all tiles in a given transform * @param {Object} transform * @returns {number} zoom level * @private */ coveringZoomLevel: function(transform) { return Math.floor(this.getZoom(transform)); }, /** * Given a transform, return all coordinates that could cover that * transform for a covering zoom level. * @param {Object} transform * @returns {Array<Tile>} tiles * @private */ coveringTiles: function(transform) { var z = this.coveringZoomLevel(transform); var actualZ = z; if (z < this.minzoom) return []; if (z > this.maxzoom) z = this.maxzoom; var tr = transform, tileCenter = tr.locationCoordinate(tr.center)._zoomTo(z), centerPoint = new Point(tileCenter.column - 0.5, tileCenter.row - 0.5); return TileCoord.cover(z, [ tr.pointCoordinate(new Point(0, 0))._zoomTo(z), tr.pointCoordinate(new Point(tr.width, 0))._zoomTo(z), tr.pointCoordinate(new Point(tr.width, tr.height))._zoomTo(z), tr.pointCoordinate(new Point(0, tr.height))._zoomTo(z) ], this.reparseOverscaled ? actualZ : z).sort(function(a, b) { return centerPoint.dist(a) - centerPoint.dist(b); }); }, /** * Recursively find children of the given tile (up to maxCoveringZoom) that are already loaded; * adds found tiles to retain object; returns true if children completely cover the tile * * @param {Coordinate} coord * @param {number} maxCoveringZoom * @param {boolean} retain * @returns {boolean} whether the operation was complete * @private */ findLoadedChildren: function(coord, maxCoveringZoom, retain) { var complete = true; var z = coord.z; var coords = coord.children(this.maxzoom); for (var i = 0; i < coords.length; i++) { var id = coords[i].id; if (this._tiles[id] && this._tiles[id].loaded) { retain[id] = true; } else { complete = false; if (z < maxCoveringZoom) { // Go further down the hierarchy to find more unloaded children. this.findLoadedChildren(coords[i], maxCoveringZoom, retain); } } } return complete; }, /** * Find a loaded parent of the given tile (up to minCoveringZoom); * adds the found tile to retain object and returns the tile if found * * @param {Coordinate} coord * @param {number} minCoveringZoom * @param {boolean} retain * @returns {Tile} tile object * @private */ findLoadedParent: function(coord, minCoveringZoom, retain) { for (var z = coord.z - 1; z >= minCoveringZoom; z--) { coord = coord.parent(this.maxzoom); var tile = this._tiles[coord.id]; if (tile && tile.loaded) { retain[coord.id] = true; return tile; } } }, /** * Removes tiles that are outside the viewport and adds new tiles that * are inside the viewport. * @private */ update: function(used, transform, fadeDuration) { var i; var coord; var tile; // Determine the overzooming/underzooming amounts. var zoom = Math.floor(this.getZoom(transform)); var minCoveringZoom = util.clamp(zoom - 10, this.minzoom, this.maxzoom); var maxCoveringZoom = util.clamp(zoom + 1, this.minzoom, this.maxzoom); // Retain is a list of tiles that we shouldn't delete, even if they are not // the most ideal tile for the current viewport. This may include tiles like // parent or child tiles that are *already* loaded. var retain = {}; var now = new Date().getTime(); // Covered is a list of retained tiles who's areas are full covered by other, // better, retained tiles. They are not drawn separately. this._coveredTiles = {}; var required = used ? this.coveringTiles(transform) : []; for (i = 0; i < required.length; i++) { coord = required[i]; tile = this.addTile(coord); retain[coord.id] = true; if (tile.loaded) continue; // The tile we require is not yet loaded. // Retain child or parent tiles that cover the same area. if (!this.findLoadedChildren(coord, maxCoveringZoom, retain)) { this.findLoadedParent(coord, minCoveringZoom, retain); } } for (var id in retain) { coord = TileCoord.fromID(id); tile = this._tiles[id]; if (tile && tile.timeAdded > now - (fadeDuration || 0)) { // This tile is still fading in. Find tiles to cross-fade with it. if (this.findLoadedChildren(coord, maxCoveringZoom, retain)) { this._coveredTiles[id] = true; retain[id] = true; } else { this.findLoadedParent(coord, minCoveringZoom, retain); } } } // Remove the tiles we don't need anymore. var remove = util.keysDifference(this._tiles, retain); for (i = 0; i < remove.length; i++) { this.removeTile(+remove[i]); } }, /** * Add a tile, given its coordinate, to the pyramid. * @param {Coordinate} coord * @returns {Coordinate} the coordinate. * @private */ addTile: function(coord) { var tile = this._tiles[coord.id]; if (tile) return tile; var wrapped = coord.wrapped(); tile = this._tiles[wrapped.id]; if (!tile) { tile = this._cache.get(wrapped.id); if (tile && this._redoPlacement) { this._redoPlacement(tile); } } if (!tile) { var zoom = coord.z; var overscaling = zoom > this.maxzoom ? Math.pow(2, zoom - this.maxzoom) : 1; tile = new Tile(wrapped, this.tileSize * overscaling); this._load(tile); } tile.uses++; this._tiles[coord.id] = tile; this._add(tile, coord); return tile; }, /** * Remove a tile, given its id, from the pyramid * @param {string|number} id tile id * @returns {undefined} nothing * @private */ removeTile: function(id) { var tile = this._tiles[id]; if (!tile) return; tile.uses--; delete this._tiles[id]; this._remove(tile); if (tile.uses > 0) return; if (tile.loaded) { this._cache.add(tile.coord.wrapped().id, tile); } else { this._abort(tile); this._unload(tile); } }, /** * Remove all tiles from this pyramid * @private */ clearTiles: function() { for (var id in this._tiles) this.removeTile(id); this._cache.reset(); }, /** * For a given coordinate, search through our current tiles and attempt * to find a tile at that point * @param {Coordinate} coord * @returns {Object} tile * @private */ tileAt: function(coord) { var ids = this.orderedIDs(); for (var i = 0; i < ids.length; i++) { var tile = this._tiles[ids[i]]; var pos = tile.positionAt(coord, this.maxzoom); if (pos && pos.x >= 0 && pos.x < 4096 && pos.y >= 0 && pos.y < 4096) { // The click is within the viewport. There is only ever one tile in // a layer that has this property. return { tile: tile, x: pos.x, y: pos.y, scale: pos.scale }; } } } };