// -*- 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. */ 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 = new TileManager.CrawlIterator(this._tileCache, new Rect(0, 0, 0, 0)); /* remember if critical rect was changed so that we only do hard work one time */ this._lastCriticalRect = new Rect(0, 0, 0, 0); /* if true, fetch offscreen dirty tiles in the "background" */ this._prefetch = false; /* create one Image of the checkerboard to be reused */ this._checkerboard = new Image(); this._checkerboard.src = "chrome://browser/content/checkerboard.png"; } 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.criticalMove(criticalRect, !(dirtyAll || boundsSizeChanged)); } else { // The critical rect hasn't changed, but there are possibly more tiles lounging about offscreen, // waiting to be rendered. Make sure crawler and eviction knows about them. this.recenterCrawler(); } 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); } }, /** * Erase everything in these rects. Useful for tiles that are outside of the viewport rect but * still visible. */ clearRects: function clearRects(rects) { let criticalIsDirty = false; let criticalRect = this._criticalRect; let tc = this._tileCache; let rect; for (let i = rects.length - 1; i >= 0; --i) { rect = rects[i]; BEGIN_FOREACH_IN_RECT(rect, tc, tile) tile.clear(rect); END_FOREACH_IN_RECT } if (criticalIsDirty && doCriticalRender) this.criticalRectPaint(); }, dirtyRects: function dirtyRects(rects, doCriticalRender) { let outsideIsDirty = false; 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)) { // Dirty tile outside of viewport. Just remove and redraw later. this._removeTileSafe(tile); crawler.enqueue(tile.i, tile.j); outsideIsDirty = true; } else { criticalIsDirty = true; } tile.markDirty(rects[i]); END_FOREACH_IN_RECT } if (criticalIsDirty && doCriticalRender) this.criticalRectPaint(); if (outsideIsDirty) this.restartPrefetchCrawl(); }, criticalRectPaint: function criticalRectPaint() { let cr = this._criticalRect; let lastCr = this._lastCriticalRect; if (!lastCr.isEmpty()) { // This is the first paint since the last critical move. let tc = this._tileCache; BEGIN_FOREACH_IN_RECT(lastCr, tc, tile) tc.releaseTile(tile); END_FOREACH_IN_RECT lastCr.setRect(0, 0, 0, 0); if (!cr.isEmpty()) this.recenterEvictionQueue(cr.center().map(Math.round)); this.recenterCrawler(); } if (!cr.isEmpty()) this._renderAppendHoldRect(cr); }, criticalMove: function criticalMove(destCriticalRect, doCriticalPaint) { let cr = this._criticalRect; let lastCr = this._lastCriticalRect; if (lastCr.isEmpty() && !cr.equals(destCriticalRect)) lastCr.copyFrom(cr); cr.copyFrom(destCriticalRect); if (doCriticalPaint) this.criticalRectPaint(); }, setPrefetch: function setPrefetch(prefetch) { if (prefetch != this._prefetch) { this._prefetch = prefetch; if (prefetch) this.restartPrefetchCrawl(); else this.stopPrefetchCrawl(); } }, restartPrefetchCrawl: function restartPrefetchCrawl() { if (this._prefetch && !this._idleTileCrawlerTimeout) this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, kTileCrawlComeAgain, this); }, stopPrefetchCrawl: function stopPrefetchCrawl() { if (this._idleTileCrawlerTimeout) clearTimeout(this._idleTileCrawlerTimeout); delete this._idleTileCrawlerTimeout; }, recenterEvictionQueue: function recenterEvictionQueue(ctr) { 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; }); }, /** Crawler will recalculate the tiles it is supposed to fetch in the background. */ recenterCrawler: function recenterCrawler() { let cr = this._criticalRect; this._crawler.recenter(cr.clone()); this.restartPrefetchCrawl(); }, /** * 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, drawMissing) { let tc = this._tileCache; let ctx = destCanvas.getContext("2d"); drawMissing = drawMissing == false ? false : true; if (!drawMissing) { let pat = ctx.createPattern(this._checkerboard, "repeat"); ctx.fillStyle = pat; ctx.fillRect(0, 0, destCanvas.width, destCanvas.height); } ctx.save(); ctx.scale(scalex, scaley); ctx.translate(-srcRect.left, -srcRect.top); let completed = (function breakableLoop() { BEGIN_FOREACH_IN_RECT(srcRect, tc, tile) if (drawMissing && tile.isDirty()) { return false; } else if (!tile.isDirty()) { ctx.drawImage(tile._canvas, tile.boundRect.left, tile.boundRect.top, kTileWidth, kTileHeight); } END_FOREACH_IN_RECT return true; })(); ctx.restore(); if (!completed) { let bv = this._browserView; bv.viewportToBrowserRect(srcRect); ctx.save(); bv.browserToViewportCanvasContext(ctx); ctx.scale(scalex, scaley); ctx.drawWindow(bv._contentWindow, srcRect.left, srcRect.top, srcRect.width, srcRect.height, "white", (ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET)); ctx.restore(); } }, _showTile: function _showTile(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); } this._appendTileSafe(tile); }, _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; // XXX this can evict crawl tiles that might just be rerendered later. It would be nice if // getTile understood which tiles had priority so that we don't waste time. BEGIN_FORCREATE_IN_RECT(rect, tc, tile) this._showTile(tile); this._tileCache.holdTile(tile); END_FORCREATE_IN_RECT }, _idleTileCrawler: function _idleTileCrawler(self) { if (!self) self = this; let start = Date.now(); let comeAgain = true; let tile; while ((Date.now() - start) <= kTileCrawlTimeCap) { tile = self._crawler.next(); if (!tile) { comeAgain = false; break; } self._showTile(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; }, 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; let tile; for (; k >= 0; --k) { tile = pool[k]; if (!this.inBounds(tile.i, tile.j) || tile.free && ( !evictionGuard || evictionGuard(tile) )) { victim = tile; --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; }, /** Clear region in rect. */ clear: function clear(rect) { let boundRect = this.boundRect; let region = rect.intersect(boundRect).expandToIntegers().translate(-boundRect.left, -boundRect.top); let ctx = this._canvas.getContext("2d"); ctx.fillStyle = "white"; ctx.fillRect(region.left, region.top, region.right - region.left, region.bottom - region.top); }, /** * 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); // We expand the rect to working around a gfx issue (bug 534054) ctx.drawWindow(browserView._contentWindow, rect.left , rect.top, rect.right - rect.left + 1, rect.bottom - rect.top + 1, "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 tc The TileCache over whose tiles this CrawlIterator will crawl * @param rect The rectangle that we grow in the first (rectangle * expansion) * iteration state. If empty, doesn't crawl. */ TileManager.CrawlIterator = function CrawlIterator(tc, rect) { this._tileCache = tc; this.recenter(rect); }; TileManager.CrawlIterator.prototype = { _generateCrawlQueue: function _generateCrawlQueue(rect) { function add(i, j) { if (tc.inBounds(i, j)) { outOfBounds = false; result.push([i, j]); --index; return true; } return false; } let tc = this._tileCache; let capacity = tc.getCapacity(); let result = []; let index = capacity; let outOfBounds; let counter; let starti = rect.left >> kTileExponentWidth; let endi = rect.right >> kTileExponentWidth; let startj = rect.top >> kTileExponentHeight; let endj = rect.bottom >> kTileExponentHeight; let i, j; while (!outOfBounds) { starti -= 1; endi += 1; startj -= 1; endj += 1; outOfBounds = true; // top, bottom rect borders for each (j in [startj, endj]) { for (counter = 1, i = Math.floor((starti + endi) / 2); i >= starti && i <= endi;) { if (add(i, j) && index == 0) return result; i += counter; counter = -counter + (counter > 0 ? -1 : 1); } } // left, right rect borders for each (i in [starti, endi]) { counter = 1; for (counter = 1, j = Math.floor((startj + endj) / 2); j >= startj && j <= endj;) { if (add(i, j) && index == 0) return result; j += counter; counter = -counter + (counter > 0 ? -1 : 1); } } } return result; }, recenter: function recenter(rect) { // Queue should not be very big, so put first priorities last in order to pop quickly. this._crawlQueue = rect.isEmpty() ? [] : this._generateCrawlQueue(rect).reverse(); // 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.i + "," + tile.j]; }; // after we finish the rectangle iteration state, we enter the FILO queue state // no need to remember old dirty tiles, if it's important we'll get to it anyways this._queue = []; // use a dictionary to prevent tiles from being enqueued twice --- "patience, we'll get to // it in a moment" this._enqueued = {}; }, next: function next() { // Priority for next goes to the crawl queue, dirty tiles afterwards. Since dirty // tile queue does not really have a necessary order, pop off the top. let coords = this._crawlQueue.pop() || this.dequeue(); let tile = null; if (coords) { let [i, j] = coords; // getTile will create a tile only if there are any left in our capacity that have not been // visited already by the crawler. tile = this._tileCache.getTile(i, j, true, this._notVisited); if (tile) { this._visited[this._strIndices(i, j)] = true; } else { tile = this.next(); } } return tile; }, dequeue: function dequeue() { if (this._queue.length) { let [i, j] = this._queue.pop(); this._enqueued[this._strIndices(i, j)] = false; return [i, j]; } else { return null; } }, enqueue: function enqueue(i, j) { let index = this._strIndices(i, j); let enqueued = this._enqueued; if (!enqueued[index]) { this._queue.push([i, j]); enqueued[index] = true; } }, _strIndices: function _strIndices(i, j) { return i + "," + j; }, };