// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- /* * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Mobile Browser. * * The Initial Developer of the Original Code is * Mozilla Corporation. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Roy Frostig * Stuart Parmenter * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #define BEGIN_FOREACH_IN_RECT(rect, tilecache, tile) \ { \ let __starti = (rect).left >> kTileExponentWidth; \ let __endi = (rect).right >> kTileExponentWidth; \ let __startj = (rect).top >> kTileExponentHeight; \ let __endj = (rect).bottom >> kTileExponentHeight; \ \ let tile = null; \ let __i, __j; \ for (__j = __startj; __j <= __endj; ++__j) { \ for (__i = __starti; __i <= __endi; ++__i) { \ tile = (tilecache).getTile(__i, __j, false, null); \ if (tile) { #define END_FOREACH_IN_RECT \ } \ } \ } \ } #define BEGIN_FORCREATE_IN_RECT(rect, tilecache, tile) \ { \ let __starti = (rect).left >> kTileExponentWidth; \ let __endi = (rect).right >> kTileExponentWidth; \ let __startj = (rect).top >> kTileExponentHeight; \ let __endj = (rect).bottom >> kTileExponentHeight; \ \ let tile = null; \ let __i, __j; \ for (__j = __startj; __j <= __endj; ++__j) { \ for (__i = __starti; __i <= __endi; ++__i) { \ tile = (tilecache).getTile(__i, __j, true, null); \ if (tile) { \ #define END_FORCREATE_IN_RECT \ } \ } \ } \ } #define FOREACH_IN_RECT(rect, tilecache, fn, thisObj) \ BEGIN_FOREACH_IN_RECT(rect, tilecache, tile) \ (fn).call((thisObj), tile); \ END_FOREACH_IN_RECT #define FORCREATE_IN_RECT(rect, tilecache, fn, thisObj) \ BEGIN_FORCREATE_IN_RECT(rect, tilecache, tile) \ (fn).call((thisObj), tile); \ END_FORCREATE_IN_RECT const kXHTMLNamespaceURI = "http://www.w3.org/1999/xhtml"; // base-2 exponent for width, height of a single tile. const kTileExponentWidth = 9; const kTileExponentHeight = 9; const kTileWidth = Math.pow(2, kTileExponentWidth); // 2^9 = 512 const kTileHeight = Math.pow(2, kTileExponentHeight); // 2^9 = 512 const kTileCrawlTimeCap = 100; // millis const kTileCrawlComeAgain = 0; // millis /** * The Tile Manager! * * @param appendTile The function the tile manager should call in order to * "display" a tile (e.g. append it to the DOM). The argument to this * function is a TileManager.Tile object. * @param removeTile The function the tile manager should call in order to * "undisplay" a tile (e.g. remove it from the DOM). The argument to this * function is a TileManager.Tile object. * @param fakeWidth The width of the widest possible visible rectangle, e.g. * the width of the screen. This is used in setting the zoomLevel. */ function TileManager(appendTile, removeTile, browserView, cacheSize) { /* backref to the BrowserView object that owns us */ this._browserView = browserView; /* callbacks to append / remove a tile to / from the parent */ this._appendTile = appendTile; this._removeTile = removeTile; /* tile cache holds tile objects and pools them under a given capacity */ let self = this; this._tileCache = new TileManager.TileCache(function(tile) { self._removeTileSafe(tile); }, -1, -1, cacheSize); /* Rectangle within the viewport that is visible to the user. It is "critical" * in the sense that it must be rendered as soon as it becomes dirty, and tiles * within this rectangle should not be evicted for use elsewhere. */ this._criticalRect = new Rect(0, 0, 0, 0); /* timeout of the non-visible-tiles-crawler to cache renders from the browser */ this._idleTileCrawlerTimeout = 0; /* object that keeps state on our current prefetch crawl */ this._crawler = null; /* remember these values to reduce the recenterEvictionQueue cost */ this._ctr = new Point(0, 0); } TileManager.prototype = { /** * Entry point by which the BrowserView informs of changes to the viewport or * critical rect. */ viewportChangeHandler: function viewportChangeHandler(viewportRect, criticalRect, boundsSizeChanged, dirtyAll) { let tc = this._tileCache; let iBoundOld = tc.iBound; let jBoundOld = tc.jBound; let iBound = tc.iBound = Math.ceil(viewportRect.right / kTileWidth) - 1; let jBound = tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight) - 1; if (criticalRect.isEmpty() || !criticalRect.equals(this._criticalRect)) { this.beginCriticalMove(criticalRect); this.endCriticalMove(criticalRect, !(dirtyAll || boundsSizeChanged)); } if (dirtyAll) { this.dirtyRects([viewportRect.clone()], true); } else if (boundsSizeChanged) { // // This is a special case. The bounds size changed, but we are // told that not everything is dirty (so mayhap content grew or // shrank vertically or horizontally). We might have old tiles // around in those areas just due to the fact that they haven't // been yet evicted, so we patrol the new regions in search of // any such leftover tiles and mark those we find as dirty. // // The two dirty rects below mark dirty any renegade tiles in // the newly annexed grid regions as per the following diagram // of the "new" viewport. // // +------------+------+ // |old | A | // |viewport | | // | | | // | | | // | | | // +------------+ | // | B | | // | | | // +------------+------+ // // The first rectangle covers annexed region A, the second // rectangle covers annexed region B. // // XXXrf If the tiles are large, then we are creating some // redundant work here by invalidating the entire tile that // the old viewport boundary crossed (note markDirty() being // called with no rectangle parameter). The rectangular area // within the tile lying beyond the old boundary is certainly // dirty, but not the area before. Moreover, since we mark // dirty entire tiles that may cross into the old viewport, // they might, in particular, cross into the critical rect // (which is anyhwere in the old viewport), so we call a // criticalRectPaint() for such cleanup. We do all this more // or less because we don't have much of a notion of "the old // viewport" here except for in the sense that we know the // index bounds on the tilecache grid from before (and the new // index bounds now). // let t, l, b, r, rect; let rects = []; if (iBoundOld <= iBound) { l = iBoundOld * kTileWidth; t = 0; r = (iBound + 1) * kTileWidth; b = (jBound + 1) * kTileHeight; rect = new Rect(l, t, r - l, b - t); rect.restrictTo(viewportRect); if (!rect.isEmpty()) rects.push(rect); } if (jBoundOld <= jBound) { l = 0; t = jBoundOld * kTileHeight; r = (iBound + 1) * kTileWidth; b = (jBound + 1) * kTileHeight; rect = new Rect(l, t, r - l, b - t); rect.restrictTo(viewportRect); if (!rect.isEmpty()) rects.push(rect); } this.dirtyRects(rects, true); } }, dirtyRects: function dirtyRects(rects, doCriticalRender) { let criticalIsDirty = false; let criticalRect = this._criticalRect; let tc = this._tileCache; let crawler = this._crawler; for (let i = 0, len = rects.length; i < len; ++i) { let rect = rects[i]; BEGIN_FOREACH_IN_RECT(rect, tc, tile) if (!tile.boundRect.intersects(criticalRect)) this._removeTileSafe(tile); else criticalIsDirty = true; let intersection = tile.boundRect.intersect(rects[i]); tile.markDirty(intersection); if (crawler) crawler.enqueue(tile.i, tile.j); END_FOREACH_IN_RECT } if (criticalIsDirty && doCriticalRender) this.criticalRectPaint(); }, criticalRectPaint: function criticalRectPaint() { let cr = this._criticalRect; //let start = Date.now(); if (!cr.isEmpty()) { let ctr = cr.center().map(Math.round); if (!this._ctr.equals(ctr)) { this._ctr.set(ctr); this.recenterEvictionQueue(ctr); } //let start = Date.now(); this._renderAppendHoldRect(cr); //dump(" render, append, hold: " + (Date.now() - start) + "\n"); } //dump(" paint: " + (Date.now() - start) + "\n"); }, beginCriticalMove: function beginCriticalMove(destCriticalRect) { if (!destCriticalRect.isEmpty()) { let tc = this._tileCache; BEGIN_FOREACH_IN_RECT(destCriticalRect, tc, tile) if (!tile.isDirty()) this._appendTileSafe(tile); END_FOREACH_IN_RECT } }, endCriticalMove: function endCriticalMove(destCriticalRect, doCriticalPaint) { let tc = this._tileCache; let cr = this._criticalRect; //let start = Date.now(); if (!cr.isEmpty()) { BEGIN_FOREACH_IN_RECT(cr, tc, tile) tc.releaseTile(tile); END_FOREACH_IN_RECT } //dump(" release: " + (Date.now() - start) + "\n"); //start = Date.now(); // XXX the conjunction with doCriticalPaint may cause tiles to disappear // (be evicted) during a (relatively slow) move as no tiles will be "held" // until a critical paint is requested. Also, while we have this // && doCriticalPaint then we don't need this loop altogether, as // criticalRectPaint will hold everything for us (called below) //if (destCriticalRect && doCriticalPaint) { // BEGIN_FOREACH_IN_RECT(destCriticalRect, tc, tile) // tc.holdTile(tile); // END_FOREACH_IN_RECT //} //dump(" hold: " + (Date.now() - start) + "\n"); cr.copyFrom(destCriticalRect); if (doCriticalPaint) this.criticalRectPaint(); }, restartPrefetchCrawl: function restartPrefetchCrawl(startRectOrQueue) { if (startRectOrQueue instanceof Array) { this._crawler = new TileManager.CrawlIterator(this._tileCache); if (startRectOrQueue) { for (let k = 0, len = startRectOrQueue.length; k < len; ++k) this._crawler.enqueue(startRectOrQueue[k].i, startRectOrQueue[k].j); } } else { let cr = this._criticalRect; this._crawler = new TileManager.CrawlIterator(this._tileCache, startRectOrQueue || (cr ? cr.clone() : null)); } if (!this._idleTileCrawlerTimeout) this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, kTileCrawlComeAgain, this); }, stopPrefetchCrawl: function stopPrefetchCrawl(skipRecenter) { if (this._idleTileCrawlerTimeout) clearTimeout(this._idleTileCrawlerTimeout); delete this._idleTileCrawlerTimeout; this._crawler = null; if (!skipRecenter) { let cr = this._criticalRect; if (!cr.isEmpty()) { let ctr = cr.center().map(Math.round); this.recenterEvictionQueue(ctr); } } }, recenterEvictionQueue: function recenterEvictionQueue(ctr) { if (this._crawler) { this.restartPrefetchCrawl(); } let ctri = ctr.x >> kTileExponentWidth; let ctrj = ctr.y >> kTileExponentHeight; this._tileCache.sortEvictionQueue(function evictFarTiles(a, b) { let dista = Math.max(Math.abs(a.i - ctri), Math.abs(a.j - ctrj)); let distb = Math.max(Math.abs(b.i - ctri), Math.abs(b.j - ctrj)); return dista - distb; }); }, /** * Render a rect to the canvas under the given scale. We attempt to avoid a * drawWindow() by copying the image (via drawImage()) from cached tiles, we * may have. If we find that we're missing a necessary tile, we fall back on * drawWindow() directly to the destination canvas. */ renderRectToCanvas: function renderRectToCanvas(srcRect, destCanvas, scalex, scaley) { let tc = this._tileCache; let ctx = destCanvas.getContext("2d"); let completed = (function breakableLoop() { BEGIN_FOREACH_IN_RECT(srcRect, tc, tile) if (tile.isDirty()) return false; let dx = Math.round(scalex * (tile.boundRect.left - srcRect.left)); let dy = Math.round(scaley * (tile.boundRect.top - srcRect.top)); ctx.drawImage(tile._canvas, dx, dy, Math.round(scalex * kTileWidth), Math.round(scaley * kTileHeight)); END_FOREACH_IN_RECT return true; })(); if (!completed) { let bv = this._browserView; bv.viewportToBrowserRect(srcRect); ctx.save(); bv.browserToViewportCanvasContext(ctx); ctx.scale(scalex, scaley); ctx.drawWindow(bv._contentWindow, 0, 0, srcRect.width, srcRect.height, "white", (ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET)); ctx.restore(); } }, _renderTile: function _renderTile(tile) { if (tile.isDirty()) { /* let ctx = tile._canvas.getContext("2d"); ctx.save(); ctx.fillStyle = "rgba(0,255,0,.5)"; ctx.translate(-tile.boundRect.left, -tile.boundRect.top); ctx.fillRect(tile._dirtyTileCanvasRect.left, tile._dirtyTileCanvasRect.top, tile._dirtyTileCanvasRect.width, tile._dirtyTileCanvasRect.height); ctx.restore(); window.setTimeout(function(bv) { tile.render(bv); }, 1000, this._browserView); */ tile.render(this._browserView); } }, _appendTileSafe: function _appendTileSafe(tile) { if (!tile.appended) { this._appendTile(tile); tile.appended = true; } }, _removeTileSafe: function _removeTileSafe(tile) { if (tile.appended) { this._removeTile(tile); tile.appended = false; } }, _renderAppendHoldRect: function _renderAppendHoldRect(rect) { let tc = this._tileCache; BEGIN_FORCREATE_IN_RECT(rect, tc, tile) if (tile.isDirty()) this._renderTile(tile); this._appendTileSafe(tile); this._tileCache.holdTile(tile); END_FORCREATE_IN_RECT }, _idleTileCrawler: function _idleTileCrawler(self) { if (!self) self = this; let start = Date.now(); let comeAgain = true; while ((Date.now() - start) <= kTileCrawlTimeCap) { let tile = self._crawler.next(); if (!tile) { comeAgain = false; break; } if (tile.isDirty()) { self._renderTile(tile); } } if (comeAgain) { self._idleTileCrawlerTimeout = setTimeout(self._idleTileCrawler, kTileCrawlComeAgain, self); } else { self.stopPrefetchCrawl(); } } }; /** * The tile cache used by the tile manager to hold and index all * tiles. Also responsible for pooling tiles and maintaining the * number of tiles under given capacity. * * @param onBeforeTileDetach callback set by the TileManager to call before * we must "detach" a tile from a tileholder due to needing it elsewhere or * having to discard it on capacity decrease * @param capacity the initial capacity of the tile cache, i.e. the max number * of tiles the cache can have allocated */ TileManager.TileCache = function TileCache(onBeforeTileDetach, iBound, jBound, capacity) { if (arguments.length <= 3 || capacity < 0) capacity = Infinity; // We track all pooled tiles in a 2D array (row, column) ordered as // they "appear on screen". The array is a grid that functions for // storage of the tiles and as a lookup map. Each array entry is // a reference to the tile occupying that space ("tileholder"). Entries // are not unique, so a tile could be referenced by multiple array entries, // i.e. a tile could "span" many tile placeholders (e.g. if we merge // neighbouring tiles). this._tileMap = []; // holds the same tiles that _tileMap holds, but as contiguous array // elements, for pooling tiles for reuse under finite capacity this._tilePool = []; this._pos = -1; this._capacity = capacity; this._onBeforeTileDetach = onBeforeTileDetach; this.iBound = iBound; this.jBound = jBound; }; TileManager.TileCache.prototype = { get size() { return this._tilePool.length; }, /** * The default tile comparison function used to order the tile pool such that * tiles most eligible for eviction have higher index. * * Unless reset, this is a comparison function that will compare free tiles * as greater than all non-free tiles. Useful, for instance, to shrink * the tile pool when capacity is lowered, as we want to remove all tiles * at the new cap and beyond, favoring removal of free tiles first. */ evictionCmp: function evictionCmp(a, b) { if (a.free == b.free) return (a.j == b.j) ? b.i - a.i : b.j - a.j; return (a.free) ? 1 : -1; }, lookup: function lookup(i, j) { let tile = null; if (this._tileMap[i]) tile = this._tileMap[i][j] || null; return tile; }, getCapacity: function getCapacity() { return this._capacity; }, setCapacity: function setCapacity(newCap, skipEvictionQueueSort) { if (newCap < 0) throw "Cannot set a negative tile cache capacity"; if (newCap == Infinity) { this._capacity = Infinity; return; } else if (this._capacity == Infinity) { // pretend we had a finite capacity all along and proceed normally this._capacity = this._tilePool.length; } if (newCap < this._capacity) { // This case is obnoxious. We're decreasing our capacity which means // we may have to get rid of tiles. Note that "throwing out" means the // cache won't keep them, and they'll get GC'ed as soon as all other // refholders let go of their refs to the tile. if (!skipEvictionQueueSort) this.sortEvictionQueue(); let rem = this._tilePool.splice(newCap); for (let k = 0, len = rem.length; k < len; ++k) this._detachTile(rem[k].i, rem[k].j); } this._capacity = newCap; }, inBounds: function inBounds(i, j) { return (0 <= i && 0 <= j && i <= this.iBound && j <= this.jBound); }, sortEvictionQueue: function sortEvictionQueue(cmp) { let pool = this._tilePool; pool.sort(cmp ? cmp : this.evictionCmp); this._pos = pool.length - 1; }, /** * Get a tile by its indices * * @param i Column * @param j Row * @param create Flag true if the tile should be created in case there is no * tile at (i, j) * @param reuseCondition Boolean-valued function to restrict conditions under * which an old tile may be reused for creating this one. This can happen if * the cache has reached its capacity and must reuse existing tiles in order to * create this one. The function is given a Tile object as its argument and * returns true if the tile is OK for reuse. This argument has no effect if the * create argument is false. */ getTile: function getTile(i, j, create, evictionGuard) { if (!this.inBounds(i, j)) return null; let tile = this.lookup(i, j); if (!tile && create) { tile = this._createTile(i, j, evictionGuard); if (tile) tile.markDirty(); } return tile; }, /** * Look up (possibly creating) a tile from its viewport coordinates. * * @param x * @param y * @param create Flag true if the tile should be created in case it doesn't * already exist at the tileholder corresponding to (x, y) */ tileFromPoint: function tileFromPoint(x, y, create) { let i = x >> kTileExponentWidth; let j = y >> kTileExponentHeight; return this.getTile(i, j, create); }, /** * Hold a tile (i.e. mark it non-free). */ holdTile: function holdTile(tile) { if (tile) tile.free = false; }, /** * Release a tile (i.e. mark it free). */ releaseTile: function releaseTile(tile) { if (tile) tile.free = true; }, _detachTile: function _detachTile(i, j) { let tile = this.lookup(i, j); if (tile) { if (this._onBeforeTileDetach) this._onBeforeTileDetach(tile); this.releaseTile(tile); delete this._tileMap[i][j]; } return tile; }, /** * Pluck tile from its current tileMap position and drop it in position * given by (i, j). */ _reassignTile: function _reassignTile(tile, i, j) { this._detachTile(tile.i, tile.j); // detach tile.init(i, j); // re-init this._tileMap[i][j] = tile; // attach return tile; }, _evictTile: function _evictTile(evictionGuard) { let k = this._pos; let pool = this._tilePool; let victim = null; for (; k >= 0; --k) { if (pool[k].free && ( !evictionGuard || evictionGuard(pool[k]) )) { victim = pool[k]; --k; break; } } this._pos = k; return victim; }, _createTile: function _createTile(i, j, evictionGuard) { if (!this._tileMap[i]) this._tileMap[i] = []; let tile = null; if (this._tilePool.length < this._capacity) { // either capacity is tile = new TileManager.Tile(i, j); // infinite, or we have this._tileMap[i][j] = tile; // room to allocate more this._tilePool.push(tile); } else { tile = this._evictTile(evictionGuard); if (tile) this._reassignTile(tile, i, j); } return tile; } }; /** * A tile is a little object with an DOM element that it used for * caching renders from drawWindow(). * * Supports the dirtying of only a subrectangle of the bounding rectangle, as * well as just dirtying the entire tile. */ TileManager.Tile = function Tile(i, j) { this._canvas = document.createElementNS(kXHTMLNamespaceURI, "canvas"); this._canvas.setAttribute("width", String(kTileWidth)); this._canvas.setAttribute("height", String(kTileHeight)); this._canvas.setAttribute("moz-opaque", "true"); this.init(i, j); // defines more properties, cf below }; TileManager.Tile.prototype = { init: function init(i, j) { if (!this.boundRect) this.boundRect = new Rect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight); else this.boundRect.setRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight); /* indices of this tile in the tile cache's tile grid map */ this.i = i; // row this.j = j; // column /* flag used by TileManager to avoid re-appending tiles that have already * been appended */ this.appended = false; /* flag used by the TileCache to mark tiles as ineligible for eviction, * usually because they are fixed in some critical position */ this.free = true; /* flags true if we need to repaint our own local canvas */ this._dirtyTileCanvas = false; /* keep a dirty rectangle (i.e. only part of the tile is dirty) */ this._dirtyTileCanvasRect = new Rect(0, 0, 0, 0); }, get x() { return this.boundRect.left; }, get y() { return this.boundRect.top; }, /** * Get the DOM element with this tile's cached paint. */ getContentImage: function getContentImage() { return this._canvas; }, isDirty: function isDirty() { return this._dirtyTileCanvas; }, /** * This will mark dirty at least everything in dirtyRect (which must be * specified in canvas coordinates). If dirtyRect is not given then * the entire tile is marked dirty (i.e. the whole tile needs to be rendered * on next render). */ markDirty: function markDirty(dirtyRect) { if (!dirtyRect) { this._dirtyTileCanvasRect.copyFrom(this.boundRect); } else { this._dirtyTileCanvasRect.expandToContain(dirtyRect.intersect(this.boundRect)).expandToIntegers(); } // XXX if, after the above, the dirty rectangle takes up a large enough // portion of the boundRect, we should probably just mark the entire tile // dirty and fastpath for future calls. if (!this._dirtyTileCanvasRect.isEmpty()) this._dirtyTileCanvas = true; }, unmarkDirty: function unmarkDirty() { this._dirtyTileCanvasRect.setRect(0, 0, 0, 0); this._dirtyTileCanvas = false; }, /** * Actually draw the browser content into the dirty region of this tile. This * requires us to actually draw with nsIDOMCanvasRenderingContext2D:: * drawWindow(), which we expect to be a heavy operation. * * You likely want to check if the tile isDirty() before asking it * to render, as this will cause the entire tile to re-render in the * case that it is not dirty. */ render: function render(browserView) { if (!this.isDirty()) this.markDirty(); let rect = this._dirtyTileCanvasRect; let x = rect.left - this.boundRect.left; let y = rect.top - this.boundRect.top; browserView.viewportToBrowserRect(rect); let ctx = this._canvas.getContext("2d"); ctx.save(); ctx.translate(x, y); browserView.browserToViewportCanvasContext(ctx); ctx.drawWindow(browserView._contentWindow, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, "white", (ctx.DRAWWINDOW_DRAW_CARET)); ctx.restore(); this.unmarkDirty(); }, /** * Standard toString prints "Tile(, )", but if `more' flags true * then some extra information is printed. */ toString: function toString(more) { if (more) { return 'Tile(' + this.i + ', ' + this.j + ', ' + 'dirty=' + this.isDirty() + ', ' + 'boundRect=' + this.boundRect.toString() + ')'; } return 'Tile(' + this.i + ', ' + this.j + ')'; } }; /** * A CrawlIterator is in charge of creating and returning subsequent tiles "crawled" * over as we render tiles lazily. * * Currently the CrawlIterator is built to expand a rectangle iteratively and return * subsequent tiles that intersect the boundary of the rectangle. Each expansion of * the rectangle is one unit of tile dimensions in each direction. This is repeated * until all tiles from elsewhere have been reused (assuming the cache has finite * capacity) in this crawl, so that we don't start reusing tiles from the beginning * of our crawl. Afterward, the CrawlIterator enters a state where it operates as a * FIFO queue, and calls to next() simply dequeue elements, which must be added with * enqueue(). * * @param tileCache The TileCache over whose tiles this CrawlIterator will crawl * @param startRect [optional] The rectangle that we grow in the first (rectangle * expansion) iteration state. */ TileManager.CrawlIterator = function CrawlIterator(tileCache, startRect) { this._tileCache = tileCache; this._stepRect = startRect; // used to remember tiles that we've reused during this crawl this._visited = {}; // filters the tiles we've already reused once from being considered victims // for reuse when we ask the tile cache to create a new tile let visited = this._visited; this._notVisited = function(tile) { return !visited[tile.toString()]; }; // a generator that generates tile indices corresponding to tiles intersecting // the boundary of an expanding rectangle this._crawlIndices = !startRect ? null : (function indicesGenerator(rect, tc) { let outOfBounds = false; while (!outOfBounds) { rect.left -= kTileWidth; // expand the rect rect.right += kTileWidth; rect.top -= kTileHeight; rect.bottom += kTileHeight; let starti = rect.left >> kTileExponentWidth; let endi = rect.right >> kTileExponentWidth; let startj = rect.top >> kTileExponentHeight; let endj = rect.bottom >> kTileExponentHeight; let i, j; outOfBounds = true; // top, bottom rect borders for each (let y in [rect.top, rect.bottom]) { j = y >> kTileExponentHeight; for (i = starti; i <= endi; ++i) { if (tc.inBounds(i, j)) { outOfBounds = false; yield [i, j]; } } } // left, right rect borders for each (let x in [rect.left, rect.right]) { i = x >> kTileExponentWidth; for (j = startj; j <= endj; ++j) { if (tc.inBounds(i, j)) { outOfBounds = false; yield [i, j]; } } } } })(this._stepRect, this._tileCache), // instantiate the generator // after we finish the rectangle iteration state, we enter the FIFO queue state this._queueState = !startRect; this._queue = []; // used to prevent tiles from being enqueued twice --- "patience, we'll get to // it in a moment" this._enqueued = {}; }; TileManager.CrawlIterator.prototype = { __iterator__: function() { while (true) { let tile = this.next(); if (!tile) break; yield tile; } }, becomeQueue: function becomeQueue() { this._queueState = true; }, unbecomeQueue: function unbecomeQueue() { this._queueState = false; }, next: function next() { if (this._queueState) return this.dequeue(); let tile = null; if (this._crawlIndices) { try { let [i, j] = this._crawlIndices.next(); tile = this._tileCache.getTile(i, j, true, this._notVisited); } catch (e) { if (!(e instanceof StopIteration)) throw e; } } if (tile) { this._visited[tile.toString()] = true; } else { this.becomeQueue(); return this.next(); } return tile; }, dequeue: function dequeue() { let tile = null; do { let idx = this._queue.shift(); if (!idx) return null; delete this._enqueued[idx]; let [i, j] = this._unstrIndices(idx); tile = this._tileCache.getTile(i, j, false); } while (!tile); return tile; }, enqueue: function enqueue(i, j) { let idx = this._strIndices(i, j); if (!this._enqueued[idx]) { this._queue.push(idx); this._enqueued[idx] = true; } }, _strIndices: function _strIndices(i, j) { return i + "," + j; }, _unstrIndices: function _unstrIndices(str) { return str.split(','); } };