import binarySearch from './binarySearch.js';
|
import Cartographic from './Cartographic.js';
|
import defined from './defined.js';
|
import Rectangle from './Rectangle.js';
|
|
/**
|
* Reports the availability of tiles in a {@link TilingScheme}.
|
*
|
* @alias TileAvailability
|
* @constructor
|
*
|
* @param {TilingScheme} tilingScheme The tiling scheme in which to report availability.
|
* @param {Number} maximumLevel The maximum tile level that is potentially available.
|
*/
|
function TileAvailability(tilingScheme, maximumLevel) {
|
this._tilingScheme = tilingScheme;
|
this._maximumLevel = maximumLevel;
|
|
this._rootNodes = [];
|
}
|
|
var rectangleScratch = new Rectangle();
|
|
function findNode(level, x, y, nodes) {
|
var count = nodes.length;
|
for (var i = 0; i < count; ++i) {
|
var node = nodes[i];
|
if (node.x === x && node.y === y && node.level === level) {
|
return true;
|
}
|
}
|
|
return false;
|
}
|
|
/**
|
* Marks a rectangular range of tiles in a particular level as being available. For best performance,
|
* add your ranges in order of increasing level.
|
*
|
* @param {Number} level The level.
|
* @param {Number} startX The X coordinate of the first available tiles at the level.
|
* @param {Number} startY The Y coordinate of the first available tiles at the level.
|
* @param {Number} endX The X coordinate of the last available tiles at the level.
|
* @param {Number} endY The Y coordinate of the last available tiles at the level.
|
*/
|
TileAvailability.prototype.addAvailableTileRange = function(level, startX, startY, endX, endY) {
|
var tilingScheme = this._tilingScheme;
|
|
var rootNodes = this._rootNodes;
|
if (level === 0) {
|
for (var y = startY; y <= endY; ++y) {
|
for (var x = startX; x <= endX; ++x) {
|
if (!findNode(level, x, y, rootNodes)) {
|
rootNodes.push(new QuadtreeNode(tilingScheme, undefined, 0, x, y));
|
}
|
}
|
}
|
}
|
|
tilingScheme.tileXYToRectangle(startX, startY, level, rectangleScratch);
|
var west = rectangleScratch.west;
|
var north = rectangleScratch.north;
|
|
tilingScheme.tileXYToRectangle(endX, endY, level, rectangleScratch);
|
var east = rectangleScratch.east;
|
var south = rectangleScratch.south;
|
|
var rectangleWithLevel = new RectangleWithLevel(level, west, south, east, north);
|
|
for (var i = 0; i < rootNodes.length; ++i) {
|
var rootNode = rootNodes[i];
|
if (rectanglesOverlap(rootNode.extent, rectangleWithLevel)) {
|
putRectangleInQuadtree(this._maximumLevel, rootNode, rectangleWithLevel);
|
}
|
}
|
};
|
|
/**
|
* Determines the level of the most detailed tile covering the position. This function
|
* usually completes in time logarithmic to the number of rectangles added with
|
* {@link TileAvailability#addAvailableTileRange}.
|
*
|
* @param {Cartographic} position The position for which to determine the maximum available level. The height component is ignored.
|
* @return {Number} The level of the most detailed tile covering the position.
|
* @throws {DeveloperError} If position is outside any tile according to the tiling scheme.
|
*/
|
TileAvailability.prototype.computeMaximumLevelAtPosition = function(position) {
|
// Find the root node that contains this position.
|
var node;
|
for (var nodeIndex = 0; nodeIndex < this._rootNodes.length; ++nodeIndex) {
|
var rootNode = this._rootNodes[nodeIndex];
|
if (rectangleContainsPosition(rootNode.extent, position)) {
|
node = rootNode;
|
break;
|
}
|
}
|
|
if (!defined(node)) {
|
return -1;
|
}
|
|
return findMaxLevelFromNode(undefined, node, position);
|
};
|
|
var rectanglesScratch = [];
|
var remainingToCoverByLevelScratch = [];
|
var westScratch = new Rectangle();
|
var eastScratch = new Rectangle();
|
|
/**
|
* Finds the most detailed level that is available _everywhere_ within a given rectangle. More detailed
|
* tiles may be available in parts of the rectangle, but not the whole thing. The return value of this
|
* function may be safely passed to {@link sampleTerrain} for any position within the rectangle. This function
|
* usually completes in time logarithmic to the number of rectangles added with
|
* {@link TileAvailability#addAvailableTileRange}.
|
*
|
* @param {Rectangle} rectangle The rectangle.
|
* @return {Number} The best available level for the entire rectangle.
|
*/
|
TileAvailability.prototype.computeBestAvailableLevelOverRectangle = function(rectangle) {
|
var rectangles = rectanglesScratch;
|
rectangles.length = 0;
|
|
if (rectangle.east < rectangle.west) {
|
// Rectangle crosses the IDL, make it two rectangles.
|
rectangles.push(Rectangle.fromRadians(-Math.PI, rectangle.south, rectangle.east, rectangle.north, westScratch));
|
rectangles.push(Rectangle.fromRadians(rectangle.west, rectangle.south, Math.PI, rectangle.north, eastScratch));
|
} else {
|
rectangles.push(rectangle);
|
}
|
|
var remainingToCoverByLevel = remainingToCoverByLevelScratch;
|
remainingToCoverByLevel.length = 0;
|
|
var i;
|
for (i = 0; i < this._rootNodes.length; ++i) {
|
updateCoverageWithNode(remainingToCoverByLevel, this._rootNodes[i], rectangles);
|
}
|
|
for (i = remainingToCoverByLevel.length - 1; i >= 0; --i) {
|
if (defined(remainingToCoverByLevel[i]) && remainingToCoverByLevel[i].length === 0) {
|
return i;
|
}
|
}
|
|
return 0;
|
};
|
|
var cartographicScratch = new Cartographic();
|
|
/**
|
* Determines if a particular tile is available.
|
* @param {Number} level The tile level to check.
|
* @param {Number} x The X coordinate of the tile to check.
|
* @param {Number} y The Y coordinate of the tile to check.
|
* @return {Boolean} True if the tile is available; otherwise, false.
|
*/
|
TileAvailability.prototype.isTileAvailable = function(level, x, y) {
|
// Get the center of the tile and find the maximum level at that position.
|
// Because availability is by tile, if the level is available at that point, it
|
// is sure to be available for the whole tile. We assume that if a tile at level n exists,
|
// then all its parent tiles back to level 0 exist too. This isn't really enforced
|
// anywhere, but Cesium would never load a tile for which this is not true.
|
var rectangle = this._tilingScheme.tileXYToRectangle(x, y, level, rectangleScratch);
|
Rectangle.center(rectangle, cartographicScratch);
|
return this.computeMaximumLevelAtPosition(cartographicScratch) >= level;
|
};
|
|
/**
|
* Computes a bit mask indicating which of a tile's four children exist.
|
* If a child's bit is set, a tile is available for that child. If it is cleared,
|
* the tile is not available. The bit values are as follows:
|
* <table>
|
* <tr><th>Bit Position</th><th>Bit Value</th><th>Child Tile</th></tr>
|
* <tr><td>0</td><td>1</td><td>Southwest</td></tr>
|
* <tr><td>1</td><td>2</td><td>Southeast</td></tr>
|
* <tr><td>2</td><td>4</td><td>Northwest</td></tr>
|
* <tr><td>3</td><td>8</td><td>Northeast</td></tr>
|
* </table>
|
*
|
* @param {Number} level The level of the parent tile.
|
* @param {Number} x The X coordinate of the parent tile.
|
* @param {Number} y The Y coordinate of the parent tile.
|
* @return {Number} The bit mask indicating child availability.
|
*/
|
TileAvailability.prototype.computeChildMaskForTile = function(level, x, y) {
|
var childLevel = level + 1;
|
if (childLevel >= this._maximumLevel) {
|
return 0;
|
}
|
|
var mask = 0;
|
|
mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y + 1) ? 1 : 0;
|
mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y + 1) ? 2 : 0;
|
mask |= this.isTileAvailable(childLevel, 2 * x, 2 * y) ? 4 : 0;
|
mask |= this.isTileAvailable(childLevel, 2 * x + 1, 2 * y) ? 8 : 0;
|
|
return mask;
|
};
|
|
function QuadtreeNode(tilingScheme, parent, level, x, y) {
|
this.tilingScheme = tilingScheme;
|
this.parent = parent;
|
this.level = level;
|
this.x = x;
|
this.y = y;
|
this.extent = tilingScheme.tileXYToRectangle(x, y, level);
|
|
this.rectangles = [];
|
this._sw = undefined;
|
this._se = undefined;
|
this._nw = undefined;
|
this._ne = undefined;
|
}
|
|
Object.defineProperties(QuadtreeNode.prototype, {
|
nw: {
|
get: function() {
|
if (!this._nw) {
|
this._nw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2);
|
}
|
return this._nw;
|
}
|
},
|
|
ne: {
|
get: function() {
|
if (!this._ne) {
|
this._ne = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2);
|
}
|
return this._ne;
|
}
|
},
|
|
sw: {
|
get: function() {
|
if (!this._sw) {
|
this._sw = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2, this.y * 2 + 1);
|
}
|
return this._sw;
|
}
|
},
|
|
se: {
|
get: function() {
|
if (!this._se) {
|
this._se = new QuadtreeNode(this.tilingScheme, this, this.level + 1, this.x * 2 + 1, this.y * 2 + 1);
|
}
|
return this._se;
|
}
|
}
|
});
|
|
function RectangleWithLevel(level, west, south, east, north) {
|
this.level = level;
|
this.west = west;
|
this.south = south;
|
this.east = east;
|
this.north = north;
|
}
|
|
function rectanglesOverlap(rectangle1, rectangle2) {
|
var west = Math.max(rectangle1.west, rectangle2.west);
|
var south = Math.max(rectangle1.south, rectangle2.south);
|
var east = Math.min(rectangle1.east, rectangle2.east);
|
var north = Math.min(rectangle1.north, rectangle2.north);
|
return south < north && west < east;
|
}
|
|
function putRectangleInQuadtree(maxDepth, node, rectangle) {
|
while (node.level < maxDepth) {
|
if (rectangleFullyContainsRectangle(node.nw.extent, rectangle)) {
|
node = node.nw;
|
} else if (rectangleFullyContainsRectangle(node.ne.extent, rectangle)) {
|
node = node.ne;
|
} else if (rectangleFullyContainsRectangle(node.sw.extent, rectangle)) {
|
node = node.sw;
|
} else if (rectangleFullyContainsRectangle(node.se.extent, rectangle)) {
|
node = node.se;
|
} else {
|
break;
|
}
|
}
|
|
if (node.rectangles.length === 0 || node.rectangles[node.rectangles.length - 1].level <= rectangle.level) {
|
node.rectangles.push(rectangle);
|
} else {
|
// Maintain ordering by level when inserting.
|
var index = binarySearch(node.rectangles, rectangle.level, rectangleLevelComparator);
|
if (index <= 0) {
|
index = ~index;
|
}
|
node.rectangles.splice(index, 0, rectangle);
|
}
|
}
|
|
function rectangleLevelComparator(a, b) {
|
return a.level - b;
|
}
|
|
function rectangleFullyContainsRectangle(potentialContainer, rectangleToTest) {
|
return rectangleToTest.west >= potentialContainer.west &&
|
rectangleToTest.east <= potentialContainer.east &&
|
rectangleToTest.south >= potentialContainer.south &&
|
rectangleToTest.north <= potentialContainer.north;
|
}
|
|
function rectangleContainsPosition(potentialContainer, positionToTest) {
|
return positionToTest.longitude >= potentialContainer.west &&
|
positionToTest.longitude <= potentialContainer.east &&
|
positionToTest.latitude >= potentialContainer.south &&
|
positionToTest.latitude <= potentialContainer.north;
|
}
|
|
function findMaxLevelFromNode(stopNode, node, position) {
|
var maxLevel = 0;
|
|
// Find the deepest quadtree node containing this point.
|
var found = false;
|
while (!found) {
|
var nw = node._nw && rectangleContainsPosition(node._nw.extent, position);
|
var ne = node._ne && rectangleContainsPosition(node._ne.extent, position);
|
var sw = node._sw && rectangleContainsPosition(node._sw.extent, position);
|
var se = node._se && rectangleContainsPosition(node._se.extent, position);
|
|
// The common scenario is that the point is in only one quadrant and we can simply
|
// iterate down the tree. But if the point is on a boundary between tiles, it is
|
// in multiple tiles and we need to check all of them, so use recursion.
|
if (nw + ne + sw + se > 1) {
|
if (nw) {
|
maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._nw, position));
|
}
|
if (ne) {
|
maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._ne, position));
|
}
|
if (sw) {
|
maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._sw, position));
|
}
|
if (se) {
|
maxLevel = Math.max(maxLevel, findMaxLevelFromNode(node, node._se, position));
|
}
|
break;
|
} else if (nw) {
|
node = node._nw;
|
} else if (ne) {
|
node = node._ne;
|
} else if (sw) {
|
node = node._sw;
|
} else if (se) {
|
node = node._se;
|
} else {
|
found = true;
|
}
|
}
|
|
// Work up the tree until we find a rectangle that contains this point.
|
while (node !== stopNode) {
|
var rectangles = node.rectangles;
|
|
// Rectangles are sorted by level, lowest first.
|
for (var i = rectangles.length - 1; i >= 0 && rectangles[i].level > maxLevel; --i) {
|
var rectangle = rectangles[i];
|
if (rectangleContainsPosition(rectangle, position)) {
|
maxLevel = rectangle.level;
|
}
|
}
|
|
node = node.parent;
|
}
|
|
return maxLevel;
|
}
|
|
function updateCoverageWithNode(remainingToCoverByLevel, node, rectanglesToCover) {
|
if (!node) {
|
return;
|
}
|
|
var i;
|
var anyOverlap = false;
|
for (i = 0; i < rectanglesToCover.length; ++i) {
|
anyOverlap = anyOverlap || rectanglesOverlap(node.extent, rectanglesToCover[i]);
|
}
|
|
if (!anyOverlap) {
|
// This node is not applicable to the rectangle(s).
|
return;
|
}
|
|
var rectangles = node.rectangles;
|
for (i = 0; i < rectangles.length; ++i) {
|
var rectangle = rectangles[i];
|
|
if (!remainingToCoverByLevel[rectangle.level]) {
|
remainingToCoverByLevel[rectangle.level] = rectanglesToCover;
|
}
|
|
remainingToCoverByLevel[rectangle.level] = subtractRectangle(remainingToCoverByLevel[rectangle.level], rectangle);
|
}
|
|
// Update with child nodes.
|
updateCoverageWithNode(remainingToCoverByLevel, node._nw, rectanglesToCover);
|
updateCoverageWithNode(remainingToCoverByLevel, node._ne, rectanglesToCover);
|
updateCoverageWithNode(remainingToCoverByLevel, node._sw, rectanglesToCover);
|
updateCoverageWithNode(remainingToCoverByLevel, node._se, rectanglesToCover);
|
}
|
|
function subtractRectangle(rectangleList, rectangleToSubtract) {
|
var result = [];
|
for (var i = 0; i < rectangleList.length; ++i) {
|
var rectangle = rectangleList[i];
|
if (!rectanglesOverlap(rectangle, rectangleToSubtract)) {
|
// Disjoint rectangles. Original rectangle is unmodified.
|
result.push(rectangle);
|
} else {
|
// rectangleToSubtract partially or completely overlaps rectangle.
|
if (rectangle.west < rectangleToSubtract.west) {
|
result.push(new Rectangle(rectangle.west, rectangle.south, rectangleToSubtract.west, rectangle.north));
|
}
|
if (rectangle.east > rectangleToSubtract.east) {
|
result.push(new Rectangle(rectangleToSubtract.east, rectangle.south, rectangle.east, rectangle.north));
|
}
|
if (rectangle.south < rectangleToSubtract.south) {
|
result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangle.south, Math.min(rectangleToSubtract.east, rectangle.east), rectangleToSubtract.south));
|
}
|
if (rectangle.north > rectangleToSubtract.north) {
|
result.push(new Rectangle(Math.max(rectangleToSubtract.west, rectangle.west), rectangleToSubtract.north, Math.min(rectangleToSubtract.east, rectangle.east), rectangle.north));
|
}
|
}
|
}
|
|
return result;
|
}
|
export default TileAvailability;
|