UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

489 lines (455 loc) 16.3 kB
import defined from "../../Core/defined.js"; import Rectangle from "../../Core/Rectangle.js"; import CartesianRectangle from "./CartesianRectangle.js"; const imageryBoundsScratch = new Rectangle(); const overlappedRectangleScratch = new Rectangle(); const clippedRectangleScratch = new Rectangle(); const nativeInputRectangleScratch = new Rectangle(); const nativeImageryBoundsScratch = new Rectangle(); const nativeClippedImageryBoundsScratch = new Rectangle(); /** * A class containing information about a piece of imagery. * * This represents the result of computing the imagery tiles that * are covered by a given <code>Rectangle</code> (and which part * of that imagery is covered, in terms of texture coordinates). * * This class represents a plain structure, without member functions. * Instances are created with the <code>createImageryCoverages</code> * function. * * The instances are used by the <code>ModelPrimitiveImagery</code>, to * represent the imagery tiles that are covered by the cartographic * bounding rectangle of the primitive positions. * * Implementation note for ImageryCoverage: * * Some of the static functions in this class have been extracted from * <code>ImageryLayer.prototype._createTileImagerySkeletons</code> * See https://github.com/CesiumGS/cesium/blob/5eaa2280f495d8f300d9e1f0497118c97aec54c8/packages/engine/Source/Scene/ImageryLayer.js#L700 * An instance of this class roughly corresponds to the <code>TileImagery</code> * that is created there. * * @private */ class ImageryCoverage { /** * Creates a new instance. * * @param {number} x x-coordinate of the imagery tile * @param {number} y y-coordinate of the imagery tile * @param {number} level level of the imagery tile * @param {CartesianRectangle} textureCoordinateRectangle The texture coordinate * rectangle from the imagery tile that is covered * @param {Imagery} imagery The imagery */ constructor(x, y, level, textureCoordinateRectangle, imagery) { this._x = x; this._y = y; this._level = level; this._textureCoordinateRectangle = textureCoordinateRectangle; this._imagery = imagery; } /** * The x-coordinate of the imagery tile, typically correlated with longitude * * @type {number} * @readonly */ get x() { return this._x; } /** * The y-coordinate of the imagery tile, typically correlated with latitude * * @type {number} * @readonly */ get y() { return this._y; } /** * The level of the imagery tile * * @type {number} * @readonly */ get level() { return this._level; } /** * The texture coordinate range that is covered from the * imagery tile. * * This is a <code>CartesianRectangle</code> that contains the * (minU, minV, maxU, maxV) coordinate range. * * Clients may not modify the returned instance. * * @type {CartesianRectangle} * @readonly */ get textureCoordinateRectangle() { return this._textureCoordinateRectangle; } /** * Returns the imagery * * @type {Imagery} * @readonly */ get imagery() { return this._imagery; } /** * Computes the <code>ImageryCoverage</code> objects that describe the imagery * tiles and the respective texture coordinates that are covered by the given * input rectangle in the given imagery data. * * The given imagery level will be clamped if necessary, to be in the valid * range for the imagery provider of the given imagery layer. * * @param {Rectangle} inputRectangle The input rectangle (e.g. tile bounds) * @param {ImageryLayer} imageryLayer The imagery layer * @param {number} inputImageryLevel The level for which the imagery coverage * should be computed. * @returns {ImageryCoverage[]} The objects describing the covered imagery * and the respective texture coordinates */ static createImageryCoverages( inputRectangle, imageryLayer, inputImageryLevel, ) { if (!imageryLayer.show) { return []; } const imageryProvider = imageryLayer.imageryProvider; const imageryLevel = ImageryCoverage._clampImageryLevel( imageryProvider, inputImageryLevel, ); // Compute the range, in integer coordinates, of imagery // tiles that are covered by the input rectangle const imageryBounds = Rectangle.intersection( imageryProvider.rectangle, imageryLayer.rectangle, imageryBoundsScratch, ); const imageryTilingScheme = imageryProvider.tilingScheme; const imageryRange = ImageryCoverage._computeImageryRange( inputRectangle, imageryBounds, imageryTilingScheme, imageryLevel, ); // Convert the input rectangle and the imagery bounds into // the native coordinate system of the tiling scheme const nativeInputRectangle = nativeInputRectangleScratch; imageryTilingScheme.rectangleToNativeRectangle( inputRectangle, nativeInputRectangle, ); const nativeImageryBounds = nativeImageryBoundsScratch; imageryTilingScheme.rectangleToNativeRectangle( imageryBounds, nativeImageryBounds, ); // A function that returns an imagery rectangle, based on (x, y, level), // clipped to the imagery bounds (or undefined if there is no intersection // between the imagery rectangle and the bounds) const computeClippedImageryRectangle = (x, y, level) => { const localImageryRectangle = imageryTilingScheme.tileXYToRectangle( x, y, level, ); const localClippedImageryRectangle = Rectangle.intersection( localImageryRectangle, imageryBounds, clippedRectangleScratch, ); if (!defined(localClippedImageryRectangle)) { return undefined; } const nativeClippedImageryBounds = nativeClippedImageryBoundsScratch; imageryTilingScheme.rectangleToNativeRectangle( localClippedImageryRectangle, nativeClippedImageryBounds, ); return nativeClippedImageryBounds; }; const imageryCoverages = ImageryCoverage._computeImageryCoverages( imageryLayer, imageryRange, imageryLevel, nativeInputRectangle, computeClippedImageryRectangle, ); return imageryCoverages; } /** * Validate the given imagery level against the constraints of the * given imagery provider. * * This will clamp the given level to be in the range * <code>[minimumLevel, maximumLevel)</code> that is * defined by the given imagery provider (and cut off * any fractional part that the input may have) * * @param {ImageryProvider} imageryProvider The imagery provider * @param {number} imageryLevel The imagery level * @returns {number} The validated level */ static _clampImageryLevel(imageryProvider, imageryLevel) { const minimumLevel = imageryProvider.minimumLevel ?? 0; const maximumLevel = imageryProvider.maximumLevel ?? Number.POSITIVE_INFINITY; const clampedImageryLevel = Math.min( maximumLevel - 1, Math.max(minimumLevel, imageryLevel), ); const validImageryLevel = Math.floor(clampedImageryLevel); return validImageryLevel; } /** * Compute the rectangle describing the range of imagery that is covered * with the given rectangle. * * This will compute a rectangle with integer coordinates that describe * the X/Y coordinates of the imagery that is overlapped by the given * input rectangle, based on the given imagery rectangle. * * Extracted from _createTileImagerySkeletons. * * @param {Rectangle} inputRectangle The input rectangle * @param {Rectangle} imageryBounds The imagery bounds * @param {TilingScheme} imageryTilingScheme The tiling scheme * @param {number} imageryLevel The imagery level * @returns {CartesianRectangle} The rectangle */ static _computeImageryRange( inputRectangle, imageryBounds, imageryTilingScheme, imageryLevel, ) { const overlappedRectangle = ImageryCoverage._computeOverlappedRectangle( inputRectangle, imageryBounds, ); const northwestTileCoordinates = imageryTilingScheme.positionToTileXY( Rectangle.northwest(overlappedRectangle), imageryLevel, ); const southeastTileCoordinates = imageryTilingScheme.positionToTileXY( Rectangle.southeast(overlappedRectangle), imageryLevel, ); const result = new CartesianRectangle(); result.minX = northwestTileCoordinates.x; result.minY = northwestTileCoordinates.y; result.maxX = southeastTileCoordinates.x; result.maxY = southeastTileCoordinates.y; // As extracted from _createTileImagerySkeletons: // If the southeast corner of the rectangle lies very close to the north or west side // of the southeast tile, we don't actually need the southernmost or easternmost // tiles. // Similarly, if the northwest corner of the rectangle lies very close to the south or east side // of the northwest tile, we don't actually need the northernmost or westernmost tiles. // We define "very close" as being within 1/512 of the width of the tile. const veryCloseX = inputRectangle.width / 512.0; const veryCloseY = inputRectangle.height / 512.0; const northwestTileRectangle = imageryTilingScheme.tileXYToRectangle( result.minX, result.minY, imageryLevel, ); const deltaNorth = Math.abs( northwestTileRectangle.south - inputRectangle.north, ); if (deltaNorth < veryCloseY && result.minY < result.maxY) { ++result.minY; } const deltaWest = Math.abs( northwestTileRectangle.east - inputRectangle.west, ); if (deltaWest < veryCloseX && result.minX < result.maxX) { ++result.minX; } const southeastTileRectangle = imageryTilingScheme.tileXYToRectangle( result.maxX, result.maxY, imageryLevel, ); const deltaSouth = Math.abs( southeastTileRectangle.north - inputRectangle.south, ); if (deltaSouth < veryCloseY && result.maxY > result.minY) { --result.maxY; } const deltaEast = Math.abs( southeastTileRectangle.west - inputRectangle.east, ); if (deltaEast < veryCloseX && result.maxX > result.minX) { --result.maxX; } return result; } /** * Clamp the given input rectangle to the given clamp rectangle. * * If the input rectangle is completely above/below or left/right * of the clamp rectangle, then the north/south or east/east * if the clamp rectangle will be used in the result. * * @param {Rectangle} input The input rectangle * @param {Rectangle} clamp The clamping rectangle * @param {Rectangle} [result] The result * @returns {Rectangle} The result */ static _clampRectangle(input, clamp, result) { if (!defined(result)) { result = new Rectangle(); } if (input.south >= clamp.north) { result.north = result.south = clamp.north; } else if (input.north <= clamp.south) { result.north = result.south = clamp.south; } else { result.south = Math.max(input.south, clamp.south); result.north = Math.min(input.north, clamp.north); } if (input.west >= clamp.east) { result.west = result.east = clamp.east; } else if (input.east <= clamp.west) { result.west = result.east = clamp.west; } else { result.west = Math.max(input.west, clamp.west); result.east = Math.min(input.east, clamp.east); } return result; } /** * Compute overlap between the given input rectangle, and the given * bounds that have been obtained from the imagery provider. * * @param {Rectangle} inputRectangle The input * @param {Rectangle} imageryBounds The imagery bounds * @returns {Rectangle} The rectangle */ static _computeOverlappedRectangle(inputRectangle, imageryBounds) { const overlappedRectangle = Rectangle.intersection( inputRectangle, imageryBounds, overlappedRectangleScratch, ); if (defined(overlappedRectangle)) { return overlappedRectangle; } return ImageryCoverage._clampRectangle( inputRectangle, imageryBounds, overlappedRectangleScratch, ); } /** * Computes the <code>ImageryCoverage</code> objects that describe the imagery and * the texture coordinates that are contained in the given range of * imagery tile coordinates, referring to the given input rectangle. * * @param {ImageryLayer} imageryLayer The imagery layer * @param {CartesianRectangle} imageryRange The range of imagery tile coordinates * @param {number} imageryLevel The imagery level * @param {Rectangle} nativeInputRectangle The input rectangle, in coordinates * that are native for the tiling scheme * @param {Function} computeClippedImageryRectangle A function that returns * an imagery rectangle, based on (x, y, level), clipped to the imagery bounds * (or undefined if there is no intersection between the imagery rectangle * and the bounds) * @returns {ImageryCoverage[]} The objects describing the covered imagery * and the respective texture coordinates */ static _computeImageryCoverages( imageryLayer, imageryRange, imageryLevel, nativeInputRectangle, computeClippedImageryRectangle, ) { const imageryCoverages = []; for (let i = imageryRange.minX; i <= imageryRange.maxX; i++) { const clippedImageryRectangleU = computeClippedImageryRectangle( i, imageryRange.maxY, imageryLevel, ); if (!defined(clippedImageryRectangleU)) { continue; } for (let j = imageryRange.minY; j <= imageryRange.maxY; j++) { const clippedImageryRectangleV = computeClippedImageryRectangle( i, j, imageryLevel, ); if (!defined(clippedImageryRectangleV)) { continue; } const textureCoordinateRectangle = ImageryCoverage._localizeToCartesianRectangle( clippedImageryRectangleV, nativeInputRectangle, undefined, ); // Note: The getImageryFromCache function will create the whole "chain" // of ancestor imageries, up to the root, and increases the reference // counter for each of them, even though it is not called // getImageryFromCacheAndCreateAllAncestorsAndAddReferences. // There is currently no way to have a single imagery, because // somewhere in TileImagery, the parent is assumed to be present. const imagery = imageryLayer.getImageryFromCache(i, j, imageryLevel); const imageryCoverage = new ImageryCoverage( i, j, imageryLevel, textureCoordinateRectangle, imagery, ); imageryCoverages.push(imageryCoverage); } } return imageryCoverages; } /** * Compute the coordinates of the first rectangle relative to the * second rectangle. * * The result will describe the bounds of the first rectangle * in coordinates that are relative to the (south,west) and * (width, height) of the second rectangle. This is suitable * for describing the texture coordinates of the first * rectangle within the second one. * * The result will be stored in the given result parameter, or * in a new rectangle if the result was undefined. * * @param {Rectangle} rectangleA The first rectangle * @param {Rectangle} rectangleB The second rectangle * @param {CartesianRectangle} [result] The result * @returns {CartesianRectangle} The result */ static _localizeToCartesianRectangle(rectangleA, rectangleB, result) { if (!defined(result)) { result = new CartesianRectangle(); } const invX = 1.0 / rectangleB.width; const invY = 1.0 / rectangleB.height; result.minX = (rectangleA.west - rectangleB.west) * invX; result.minY = (rectangleA.south - rectangleB.south) * invY; result.maxX = (rectangleA.east - rectangleB.west) * invX; result.maxY = (rectangleA.north - rectangleB.south) * invY; return result; } } export default ImageryCoverage;