gecko/mobile/chrome/content/TileManager.js.in

946 lines
30 KiB
JavaScript

// -*- 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 <rfrostig@mozilla.com>
* Stuart Parmenter <stuart@mozilla.com>
*
* 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 = 7;
const kTileExponentHeight = 7;
const kTileWidth = Math.pow(2, kTileExponentWidth); // 2^7 = 128
const kTileHeight = Math.pow(2, kTileExponentHeight); // 2^7 = 128
const kTileLazyRoundTimeCap = 500; // millis
const kInitialCacheCapacity = 200;
/**
* 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) {
/* 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, kInitialCacheCapacity);
/* 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 = null;
/* timeout of the non-visible-tiles-crawler to cache renders from the browser */
this._idleTileCrawlerTimeout = 0;
/* object that keeps state on our current lazyload crawl */
this._crawler = null;
}
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;
tc.iBound = Math.ceil(viewportRect.right / kTileWidth);
tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight);
if (!criticalRect || !criticalRect.equals(this._criticalRect)) {
this.beginCriticalMove(criticalRect);
this.endCriticalMove(criticalRect, !boundsSizeChanged);
}
if (boundsSizeChanged) {
// TODO fastpath if !dirtyAll
this.dirtyRects([viewportRect.clone()], 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 (!criticalRect || !tile.boundRect.intersects(criticalRect))
this._removeTileSafe(tile);
else
criticalIsDirty = true;
// XXX we may want to pass rect through to make markDirty only mark the
// intersection of tile's boundRect with rect dirty (e.g. if we have big
// tiles)
tile.markDirty();
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) {
let [ctrx, ctry] = cr.centerRounded();
//dump('-----> centering at ' + ctrx + ',' + ctry + '\n');
this.recenterEvictionQueue(ctrx, ctry);
//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) {
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) {
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");
if (cr && destCriticalRect)
cr.copyFrom(destCriticalRect);
else
this._criticalRect = cr = destCriticalRect;
if (doCriticalPaint)
this.criticalRectPaint();
},
restartLazyCrawl: function restartLazyCrawl(startRectOrQueue) {
if (!startRectOrQueue || startRectOrQueue instanceof Array) {
this._crawler = new TileManager.CrawlIterator(this._tileCache);
if (startRectOrQueue) {
let len = startRectOrQueue.length;
for (let k = 0; k < len; ++k)
this._crawler.enqueue(startRectOrQueue[k].i, startRectOrQueue[k].j);
}
} else {
this._crawler = new TileManager.CrawlIterator(this._tileCache, startRectOrQueue);
}
if (!this._idleTileCrawlerTimeout)
this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, 2000, this);
},
stopLazyCrawl: function stopLazyCrawl() {
if (this._idleTileCrawlerTimeout)
clearTimeout(this._idleTileCrawlerTimeout);
delete this._idleTileCrawlerTimeout;
this._crawler = null;
let cr = this._criticalRect;
if (cr) {
let [ctrx, ctry] = cr.centerRounded();
this.recenterEvictionQueue(ctrx, ctry);
}
},
recenterEvictionQueue: function recenterEvictionQueue(ctrx, ctry) {
let ctri = ctrx >> kTileExponentWidth;
let ctrj = ctry >> kTileExponentHeight;
//dump(' that is, ' + ctri + ',' + ctrj + '\n');
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;
});
},
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())
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;
dump('crawl pass.\n');
let itered = 0, rendered = 0;
let start = Date.now();
let comeAgain = true;
while ((Date.now() - start) <= kTileLazyRoundTimeCap) {
let tile = self._crawler.next();
if (!tile) {
comeAgain = false;
break;
}
if (tile.isDirty()) {
self._renderTile(tile);
++rendered;
}
++itered;
}
dump('crawl itered:' + itered + ' rendered:' + rendered + '\n');
if (comeAgain) {
self._idleTileCrawlerTimeout = setTimeout(self._idleTileCrawler, 2000, self);
} else {
self.stopLazyCrawl();
dump('crawl end\n');
}
}
};
/**
* 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 <html:canvas> 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 wsRect(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 = null;
},
get x() { return this.boundRect.left; },
get y() { return this.boundRect.top; },
/**
* Get the <html:canvas> 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) {
if (!this._dirtyTileCanvasRect)
this._dirtyTileCanvasRect = this.boundRect.clone();
else
this._dirtyTileCanvasRect.copyFrom(this.boundRect);
} else { // XXX this case is not very well-tested
if (!this._dirtyTileCanvasRect)
this._dirtyTileCanvasRect = dirtyRect.intersect(this.boundRect);
else if (dirtyRect.intersects(this.boundRect))
this._dirtyTileCanvasRect.expandToContain(dirtyRect.intersect(this.boundRect));
}
// 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)
this._dirtyTileCanvas = true;
},
unmarkDirty: function unmarkDirty() {
this._dirtyTileCanvasRect = null;
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 sourceContent = browserView._contentWindow;
let ctx = this._canvas.getContext("2d");
ctx.save();
browserView.browserToViewportCanvasContext(ctx);
ctx.translate(x, y);
ctx.drawWindow(sourceContent,
rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
"white",
(ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET));
ctx.restore();
this.unmarkDirty();
},
/**
* Standard toString prints "Tile(<row>, <column>)", 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) {
// expand rect
rect.left -= kTileWidth;
rect.right += kTileWidth;
rect.top -= kTileHeight;
rect.bottom += kTileHeight;
let dx = (rect.right % kTileWidth) - (rect.left % kTileWidth);
let dy = (rect.bottom % kTileHeight) - (rect.top % kTileHeight);
outOfBounds = true;
// top, bottom borders
for each (let y in [rect.top, rect.bottom]) {
for (let x = rect.left; x <= rect.right - dx; x += kTileWidth) {
let i = x >> kTileExponentWidth;
let j = y >> kTileExponentHeight;
if (tc.inBounds(i, j)) {
outOfBounds = false;
yield [i, j];
}
}
}
// left, right borders
for each (let x in [rect.left, rect.right]) {
for (let y = rect.top; y <= rect.bottom - dy; y += kTileHeight) {
let i = x >> kTileExponentWidth;
let j = y >> kTileExponentHeight;
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(',');
}
};