UNPKG

@cesium/engine

Version:

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

1,361 lines (1,243 loc) 63.3 kB
import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Check from "../Core/Check.js"; import createWorldImageryAsync from "../Scene/createWorldImageryAsync.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import DeveloperError from "../Core/DeveloperError.js"; import Event from "../Core/Event.js"; import FeatureDetection from "../Core/FeatureDetection.js"; import GeographicProjection from "../Core/GeographicProjection.js"; import IndexDatatype from "../Core/IndexDatatype.js"; import CesiumMath from "../Core/Math.js"; import PixelFormat from "../Core/PixelFormat.js"; import Rectangle from "../Core/Rectangle.js"; import Request from "../Core/Request.js"; import RequestState from "../Core/RequestState.js"; import RequestType from "../Core/RequestType.js"; import TerrainProvider from "../Core/TerrainProvider.js"; import TileProviderError from "../Core/TileProviderError.js"; import WebMercatorProjection from "../Core/WebMercatorProjection.js"; import Buffer from "../Renderer/Buffer.js"; import BufferUsage from "../Renderer/BufferUsage.js"; import ComputeCommand from "../Renderer/ComputeCommand.js"; import ContextLimits from "../Renderer/ContextLimits.js"; import MipmapHint from "../Renderer/MipmapHint.js"; import Sampler from "../Renderer/Sampler.js"; import ShaderProgram from "../Renderer/ShaderProgram.js"; import ShaderSource from "../Renderer/ShaderSource.js"; import Texture from "../Renderer/Texture.js"; import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import TextureWrap from "../Renderer/TextureWrap.js"; import VertexArray from "../Renderer/VertexArray.js"; import ReprojectWebMercatorFS from "../Shaders/ReprojectWebMercatorFS.js"; import ReprojectWebMercatorVS from "../Shaders/ReprojectWebMercatorVS.js"; import Imagery from "./Imagery.js"; import ImageryState from "./ImageryState.js"; import SplitDirection from "./SplitDirection.js"; import TileImagery from "./TileImagery.js"; /** * @typedef {Object} ImageryLayer.ConstructorOptions * * Initialization options for the ImageryLayer constructor. * * @property {Rectangle} [rectangle=imageryProvider.rectangle] The rectangle of the layer. This rectangle * can limit the visible portion of the imagery provider. * @property {number|Function} [alpha=1.0] The alpha blending value of this layer, from 0.0 to 1.0. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the alpha is required, and it is expected to return * the alpha value to use for the tile. * @property {number|Function} [nightAlpha=1.0] The alpha blending value of this layer on the night side of the globe, from 0.0 to 1.0. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the alpha is required, and it is expected to return * the alpha value to use for the tile. This only takes effect when <code>enableLighting</code> is <code>true</code>. * @property {number|Function} [dayAlpha=1.0] The alpha blending value of this layer on the day side of the globe, from 0.0 to 1.0. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the alpha is required, and it is expected to return * the alpha value to use for the tile. This only takes effect when <code>enableLighting</code> is <code>true</code>. * @property {number|Function} [brightness=1.0] The brightness of this layer. 1.0 uses the unmodified imagery * color. Less than 1.0 makes the imagery darker while greater than 1.0 makes it brighter. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the brightness is required, and it is expected to return * the brightness value to use for the tile. The function is executed for every * frame and for every tile, so it must be fast. * @property {number|Function} [contrast=1.0] The contrast of this layer. 1.0 uses the unmodified imagery color. * Less than 1.0 reduces the contrast while greater than 1.0 increases it. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the contrast is required, and it is expected to return * the contrast value to use for the tile. The function is executed for every * frame and for every tile, so it must be fast. * @property {number|Function} [hue=0.0] The hue of this layer. 0.0 uses the unmodified imagery color. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates * of the imagery tile for which the hue is required, and it is expected to return * the hue value to use for the tile. The function is executed for every * frame and for every tile, so it must be fast. * @property {number|Function} [saturation=1.0] The saturation of this layer. 1.0 uses the unmodified imagery color. * Less than 1.0 reduces the saturation while greater than 1.0 increases it. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates * of the imagery tile for which the saturation is required, and it is expected to return * the saturation value to use for the tile. The function is executed for every * frame and for every tile, so it must be fast. * @property {number|Function} [gamma=1.0] The gamma correction to apply to this layer. 1.0 uses the unmodified imagery color. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the gamma is required, and it is expected to return * the gamma value to use for the tile. The function is executed for every * frame and for every tile, so it must be fast. * @property {SplitDirection|Function} [splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this layer. * @property {TextureMinificationFilter} [minificationFilter=TextureMinificationFilter.LINEAR] The * texture minification filter to apply to this layer. Possible values * are <code>TextureMinificationFilter.LINEAR</code> and * <code>TextureMinificationFilter.NEAREST</code>. * @property {TextureMagnificationFilter} [magnificationFilter=TextureMagnificationFilter.LINEAR] The * texture minification filter to apply to this layer. Possible values * are <code>TextureMagnificationFilter.LINEAR</code> and * <code>TextureMagnificationFilter.NEAREST</code>. * @property {boolean} [show=true] True if the layer is shown; otherwise, false. * @property {number} [maximumAnisotropy=maximum supported] The maximum anisotropy level to use * for texture filtering. If this parameter is not specified, the maximum anisotropy supported * by the WebGL stack will be used. Larger values make the imagery look better in horizon * views. * @property {number} [minimumTerrainLevel] The minimum terrain level-of-detail at which to show this imagery layer, * or undefined to show it at all levels. Level zero is the least-detailed level. * @property {number} [maximumTerrainLevel] The maximum terrain level-of-detail at which to show this imagery layer, * or undefined to show it at all levels. Level zero is the least-detailed level. * @property {Rectangle} [cutoutRectangle] Cartographic rectangle for cutting out a portion of this ImageryLayer. * @property {Color} [colorToAlpha] Color to be used as alpha. * @property {number} [colorToAlphaThreshold=0.004] Threshold for color-to-alpha. */ /** * An imagery layer that displays tiled image data from a single imagery provider * on a {@link Globe} or {@link Cesium3DTileset}. * * @alias ImageryLayer * @constructor * * @param {ImageryProvider} [imageryProvider] The imagery provider to use. * @param {ImageryLayer.ConstructorOptions} [options] An object describing initialization options * * @see {@link ImageryLayer.fromProviderAsync} for creating an imagery layer from an asynchronous imagery provider. * @see {@link ImageryLayer.fromWorldImagery} for creating an imagery layer for Cesium ion's default global base imagery layer. * @see {@link Scene#imageryLayers} for adding an imagery layer to the globe. * @see {@link Cesium3DTileset#imageryLayers} for adding an imagery layer to a 3D tileset. * * @example * // Add an OpenStreetMaps layer * const imageryLayer = new Cesium.ImageryLayer(new Cesium.OpenStreetMapImageryProvider({ * url: "https://tile.openstreetmap.org/" * })); * scene.imageryLayers.add(imageryLayer); * * @example * // Add Cesium ion's default world imagery layer * const imageryLayer = Cesium.ImageryLayer.fromWorldImagery(); * scene.imageryLayers.add(imageryLayer); * * @example * // Add a new transparent layer from Cesium ion * const imageryLayer = Cesium.ImageryLayer.fromProviderAsync(Cesium.IonImageryProvider.fromAssetId(3812)); * imageryLayer.alpha = 0.5; * scene.imageryLayers.add(imageryLayer); * * @example * // Drape Bing Maps Aerial imagery over a 3D tileset * const tileset = await Cesium.Cesium3DTileset.fromUrl( * "http://localhost:8002/tilesets/Seattle/tileset.json" * ); * scene.primitives.add(tileset); * * const imageryProvider = await Cesium.createWorldImageryAsync({ * style: Cesium.IonWorldImageryStyle.AERIAL, * }); * const imageryLayer = new ImageryLayer(imageryProvider); * tileset.imageryLayers.add(imageryLayer); */ function ImageryLayer(imageryProvider, options) { this._imageryProvider = imageryProvider; this._readyEvent = new Event(); this._errorEvent = new Event(); options = options ?? Frozen.EMPTY_OBJECT; imageryProvider = imageryProvider ?? Frozen.EMPTY_OBJECT; /** * The alpha blending value of this layer, with 0.0 representing fully transparent and * 1.0 representing fully opaque. * * @type {number} * @default 1.0 */ this.alpha = options.alpha ?? imageryProvider._defaultAlpha ?? 1.0; /** * The alpha blending value of this layer on the night side of the globe, with 0.0 representing fully transparent and * 1.0 representing fully opaque. This only takes effect when {@link Globe#enableLighting} is <code>true</code>. * * @type {number} * @default 1.0 */ this.nightAlpha = options.nightAlpha ?? imageryProvider._defaultNightAlpha ?? 1.0; /** * The alpha blending value of this layer on the day side of the globe, with 0.0 representing fully transparent and * 1.0 representing fully opaque. This only takes effect when {@link Globe#enableLighting} is <code>true</code>. * * @type {number} * @default 1.0 */ this.dayAlpha = options.dayAlpha ?? imageryProvider._defaultDayAlpha ?? 1.0; /** * The brightness of this layer. 1.0 uses the unmodified imagery color. Less than 1.0 * makes the imagery darker while greater than 1.0 makes it brighter. * * @type {number} * @default {@link ImageryLayer.DEFAULT_BRIGHTNESS} */ this.brightness = options.brightness ?? imageryProvider._defaultBrightness ?? ImageryLayer.DEFAULT_BRIGHTNESS; /** * The contrast of this layer. 1.0 uses the unmodified imagery color. Less than 1.0 reduces * the contrast while greater than 1.0 increases it. * * @type {number} * @default {@link ImageryLayer.DEFAULT_CONTRAST} */ this.contrast = options.contrast ?? imageryProvider._defaultContrast ?? ImageryLayer.DEFAULT_CONTRAST; /** * The hue of this layer in radians. 0.0 uses the unmodified imagery color. * * @type {number} * @default {@link ImageryLayer.DEFAULT_HUE} */ this.hue = options.hue ?? imageryProvider._defaultHue ?? ImageryLayer.DEFAULT_HUE; /** * The saturation of this layer. 1.0 uses the unmodified imagery color. Less than 1.0 reduces the * saturation while greater than 1.0 increases it. * * @type {number} * @default {@link ImageryLayer.DEFAULT_SATURATION} */ this.saturation = options.saturation ?? imageryProvider._defaultSaturation ?? ImageryLayer.DEFAULT_SATURATION; /** * The gamma correction to apply to this layer. 1.0 uses the unmodified imagery color. * * @type {number} * @default {@link ImageryLayer.DEFAULT_GAMMA} */ this.gamma = options.gamma ?? imageryProvider._defaultGamma ?? ImageryLayer.DEFAULT_GAMMA; /** * The {@link SplitDirection} to apply to this layer. * * @type {SplitDirection} * @default {@link ImageryLayer.DEFAULT_SPLIT} */ this.splitDirection = options.splitDirection ?? ImageryLayer.DEFAULT_SPLIT; /** * The {@link TextureMinificationFilter} to apply to this layer. * Possible values are {@link TextureMinificationFilter.LINEAR} (the default) * and {@link TextureMinificationFilter.NEAREST}. * * To take effect, this property must be set immediately after adding the imagery layer. * Once a texture is loaded it won't be possible to change the texture filter used. * * @type {TextureMinificationFilter} * @default {@link ImageryLayer.DEFAULT_MINIFICATION_FILTER} */ this.minificationFilter = options.minificationFilter ?? imageryProvider._defaultMinificationFilter ?? ImageryLayer.DEFAULT_MINIFICATION_FILTER; /** * The {@link TextureMagnificationFilter} to apply to this layer. * Possible values are {@link TextureMagnificationFilter.LINEAR} (the default) * and {@link TextureMagnificationFilter.NEAREST}. * * To take effect, this property must be set immediately after adding the imagery layer. * Once a texture is loaded it won't be possible to change the texture filter used. * * @type {TextureMagnificationFilter} * @default {@link ImageryLayer.DEFAULT_MAGNIFICATION_FILTER} */ this.magnificationFilter = options.magnificationFilter ?? imageryProvider._defaultMagnificationFilter ?? ImageryLayer.DEFAULT_MAGNIFICATION_FILTER; /** * Determines if this layer is shown. * * @type {boolean} * @default true */ this.show = options.show ?? true; this._minimumTerrainLevel = options.minimumTerrainLevel; this._maximumTerrainLevel = options.maximumTerrainLevel; this._rectangle = options.rectangle ?? Rectangle.MAX_VALUE; this._maximumAnisotropy = options.maximumAnisotropy; this._imageryCache = {}; this._skeletonPlaceholder = new TileImagery(Imagery.createPlaceholder(this)); // The value of the show property on the last update. this._show = true; // The index of this layer in the ImageryLayerCollection. this._layerIndex = -1; // true if this is the base (lowest shown) layer. this._isBaseLayer = false; this._requestImageError = undefined; this._reprojectComputeCommands = []; /** * Rectangle cutout in this layer of imagery. * * @type {Rectangle} */ this.cutoutRectangle = options.cutoutRectangle; /** * Color value that should be set to transparent. * * @type {Color} */ this.colorToAlpha = options.colorToAlpha; /** * Normalized (0-1) threshold for color-to-alpha. * * @type {number} */ this.colorToAlphaThreshold = options.colorToAlphaThreshold ?? ImageryLayer.DEFAULT_APPLY_COLOR_TO_ALPHA_THRESHOLD; } Object.defineProperties(ImageryLayer.prototype, { /** * Gets the imagery provider for this layer. This should not be called before {@link ImageryLayer#ready} returns true. * @memberof ImageryLayer.prototype * @type {ImageryProvider} * @readonly */ imageryProvider: { get: function () { return this._imageryProvider; }, }, /** * Returns true when the terrain provider has been successfully created. Otherwise, returns false. * @memberof ImageryLayer.prototype * @type {boolean} * @readonly */ ready: { get: function () { return defined(this._imageryProvider); }, }, /** * Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing * to the event, you will be notified of the error and can potentially recover from it. Event listeners * are passed an instance of the thrown error. * @memberof ImageryLayer.prototype * @type {Event<ImageryLayer.ErrorEventCallback>} * @readonly */ errorEvent: { get: function () { return this._errorEvent; }, }, /** * Gets an event that is raised when the imagery provider has been successfully created. Event listeners * are passed the created instance of {@link ImageryProvider}. * @memberof ImageryLayer.prototype * @type {Event<ImageryLayer.ReadyEventCallback>} * @readonly */ readyEvent: { get: function () { return this._readyEvent; }, }, /** * Gets the rectangle of this layer. If this rectangle is smaller than the rectangle of the * {@link ImageryProvider}, only a portion of the imagery provider is shown. * @memberof ImageryLayer.prototype * @type {Rectangle} * @readonly */ rectangle: { get: function () { return this._rectangle; }, }, }); /** * This value is used as the default brightness for the imagery layer if one is not provided during construction * or by the imagery provider. This value does not modify the brightness of the imagery. * @type {number} * @default 1.0 */ ImageryLayer.DEFAULT_BRIGHTNESS = 1.0; /** * This value is used as the default contrast for the imagery layer if one is not provided during construction * or by the imagery provider. This value does not modify the contrast of the imagery. * @type {number} * @default 1.0 */ ImageryLayer.DEFAULT_CONTRAST = 1.0; /** * This value is used as the default hue for the imagery layer if one is not provided during construction * or by the imagery provider. This value does not modify the hue of the imagery. * @type {number} * @default 0.0 */ ImageryLayer.DEFAULT_HUE = 0.0; /** * This value is used as the default saturation for the imagery layer if one is not provided during construction * or by the imagery provider. This value does not modify the saturation of the imagery. * @type {number} * @default 1.0 */ ImageryLayer.DEFAULT_SATURATION = 1.0; /** * This value is used as the default gamma for the imagery layer if one is not provided during construction * or by the imagery provider. This value does not modify the gamma of the imagery. * @type {number} * @default 1.0 */ ImageryLayer.DEFAULT_GAMMA = 1.0; /** * This value is used as the default split for the imagery layer if one is not provided during construction * or by the imagery provider. * @type {SplitDirection} * @default SplitDirection.NONE */ ImageryLayer.DEFAULT_SPLIT = SplitDirection.NONE; /** * This value is used as the default texture minification filter for the imagery layer if one is not provided * during construction or by the imagery provider. * @type {TextureMinificationFilter} * @default TextureMinificationFilter.LINEAR */ ImageryLayer.DEFAULT_MINIFICATION_FILTER = TextureMinificationFilter.LINEAR; /** * This value is used as the default texture magnification filter for the imagery layer if one is not provided * during construction or by the imagery provider. * @type {TextureMagnificationFilter} * @default TextureMagnificationFilter.LINEAR */ ImageryLayer.DEFAULT_MAGNIFICATION_FILTER = TextureMagnificationFilter.LINEAR; /** * This value is used as the default threshold for color-to-alpha if one is not provided * during construction or by the imagery provider. * @type {number} * @default 0.004 */ ImageryLayer.DEFAULT_APPLY_COLOR_TO_ALPHA_THRESHOLD = 0.004; /** * Create a new imagery layer from an asynchronous imagery provider. The layer will handle any asynchronous loads or errors, and begin rendering the imagery layer once ready. * * @param {Promise<ImageryProvider>} imageryProviderPromise A promise which resolves to a imagery provider * @param {ImageryLayer.ConstructorOptions} [options] An object describing initialization options * @returns {ImageryLayer} The created imagery layer. * * @example * // Create a new base layer * const viewer = new Cesium.Viewer("cesiumContainer", { * baseLayer: Cesium.ImageryLayer.fromProviderAsync(Cesium.IonImageryProvider.fromAssetId(3812)); * }); * * @example * // Add a new transparent layer * const imageryLayer = Cesium.ImageryLayer.fromProviderAsync(Cesium.IonImageryProvider.fromAssetId(3812)); * imageryLayer.alpha = 0.5; * viewer.imageryLayers.add(imageryLayer); * * @example * // Handle loading events * const imageryLayer = Cesium.ImageryLayer.fromProviderAsync(Cesium.IonImageryProvider.fromAssetId(3812)); * viewer.imageryLayers.add(imageryLayer); * * imageryLayer.readyEvent.addEventListener(provider => { * imageryLayer.imageryProvider.errorEvent.addEventListener(error => { * alert(`Encountered an error while loading imagery tiles! ${error}`); * }); * }); * * imageryLayer.errorEvent.addEventListener(error => { * alert(`Encountered an error while creating an imagery layer! ${error}`); * }); * * @see ImageryLayer#errorEvent * @see ImageryLayer#readyEvent * @see ImageryLayer#imageryProvider * @see ImageryLayer.fromWorldImagery */ ImageryLayer.fromProviderAsync = function (imageryProviderPromise, options) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("imageryProviderPromise", imageryProviderPromise); //>>includeEnd('debug'); const layer = new ImageryLayer(undefined, options); handlePromise(layer, Promise.resolve(imageryProviderPromise)); return layer; }; /** * @typedef {ImageryLayer.ConstructorOptions} ImageryLayer.WorldImageryConstructorOptions * * Initialization options for ImageryLayer.fromWorldImagery * * @property {IonWorldImageryStyle} [options.style=IonWorldImageryStyle] The style of base imagery, only AERIAL, AERIAL_WITH_LABELS, and ROAD are currently supported. */ /** * Create a new imagery layer for ion's default global base imagery layer, currently Bing Maps. The layer will handle any asynchronous loads or errors, and begin rendering the imagery layer once ready. * * @param {ImageryLayer.WorldImageryConstructorOptions} options An object describing initialization options * @returns {ImageryLayer} The created imagery layer. * * * @example * // Create a new base layer * const viewer = new Cesium.Viewer("cesiumContainer", { * baseLayer: Cesium.ImageryLayer.fromWorldImagery(); * }); * * @example * // Add a new transparent layer * const imageryLayer = Cesium.ImageryLayer.fromWorldImagery(); * imageryLayer.alpha = 0.5; * viewer.imageryLayers.add(imageryLayer); * * @example * // Handle loading events * const imageryLayer = Cesium.ImageryLayer.fromWorldImagery(); * viewer.imageryLayers.add(imageryLayer); * * imageryLayer.readyEvent.addEventListener(provider => { * imageryLayer.imageryProvider.errorEvent.addEventListener(error => { * alert(`Encountered an error while loading imagery tiles! ${error}`); * }); * }); * * imageryLayer.errorEvent.addEventListener(error => { * alert(`Encountered an error while creating an imagery layer! ${error}`); * }); * * @see ImageryLayer#errorEvent * @see ImageryLayer#readyEvent * @see ImageryLayer#imageryProvider */ ImageryLayer.fromWorldImagery = function (options) { options = options ?? Frozen.EMPTY_OBJECT; return ImageryLayer.fromProviderAsync( createWorldImageryAsync({ style: options.style, }), options, ); }; /** * Gets a value indicating whether this layer is the base layer in the * {@link ImageryLayerCollection}. The base layer is the one that underlies all * others. It is special in that it is treated as if it has global rectangle, even if * it actually does not, by stretching the texels at the edges over the entire * globe. * * @returns {boolean} true if this is the base layer; otherwise, false. */ ImageryLayer.prototype.isBaseLayer = function () { return this._isBaseLayer; }; /** * Returns true if this object was destroyed; otherwise, false. * <br /><br /> * If this object was destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. * * @returns {boolean} True if this object was destroyed; otherwise, false. * * @see ImageryLayer#destroy */ ImageryLayer.prototype.isDestroyed = function () { return false; }; /** * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic * release of WebGL resources, instead of relying on the garbage collector to destroy this object. * <br /><br /> * Once an object is destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore, * assign the return value (<code>undefined</code>) to the object as done in the example. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * imageryLayer = imageryLayer && imageryLayer.destroy(); * * @see ImageryLayer#isDestroyed */ ImageryLayer.prototype.destroy = function () { return destroyObject(this); }; const imageryBoundsScratch = new Rectangle(); const tileImageryBoundsScratch = new Rectangle(); const clippedRectangleScratch = new Rectangle(); const terrainRectangleScratch = new Rectangle(); /** * Computes the intersection of this layer's rectangle with the imagery provider's availability rectangle, * producing the overall bounds of imagery that can be produced by this layer. * * @returns {Rectangle} A rectangle which defines the overall bounds of imagery that can be produced by this layer. * * @example * // Zoom to an imagery layer. * const imageryRectangle = imageryLayer.getImageryRectangle(); * scene.camera.flyTo({ * destination: rectangle * }); * */ ImageryLayer.prototype.getImageryRectangle = function () { const imageryProvider = this._imageryProvider; const rectangle = this._rectangle; return Rectangle.intersection(imageryProvider.rectangle, rectangle); }; /** * Create skeletons for the imagery tiles that partially or completely overlap a given terrain * tile. * * @private * * @param {QuadtreeTile} tile The terrain tile. * @param {TerrainProvider|undefined} terrainProvider The terrain provider associated with the terrain tile. * @param {number} insertionPoint The position to insert new skeletons before in the tile's imagery list. * @returns {boolean} true if this layer overlaps any portion of the terrain tile; otherwise, false. */ ImageryLayer.prototype._createTileImagerySkeletons = function ( tile, terrainProvider, insertionPoint, ) { const surfaceTile = tile.data; if ( !defined(terrainProvider) || (defined(this._minimumTerrainLevel) && tile.level < this._minimumTerrainLevel) ) { return false; } if ( defined(this._maximumTerrainLevel) && tile.level > this._maximumTerrainLevel ) { return false; } if (!defined(insertionPoint)) { insertionPoint = surfaceTile.imagery.length; } const imageryProvider = this._imageryProvider; if (!this.ready) { // The imagery provider is not ready, so we can't create skeletons, yet. // Instead, add a placeholder so that we'll know to create // the skeletons once the provider is ready. this._skeletonPlaceholder.loadingImagery.addReference(); surfaceTile.imagery.splice(insertionPoint, 0, this._skeletonPlaceholder); return true; } // Use Web Mercator for our texture coordinate computations if this imagery layer uses // that projection and the terrain tile falls entirely inside the valid bounds of the // projection. const useWebMercatorT = imageryProvider.tilingScheme.projection instanceof WebMercatorProjection && tile.rectangle.north < WebMercatorProjection.MaximumLatitude && tile.rectangle.south > -WebMercatorProjection.MaximumLatitude; // Compute the rectangle of the imagery from this imageryProvider that overlaps // the geometry tile. The ImageryProvider and ImageryLayer both have the // opportunity to constrain the rectangle. The imagery TilingScheme's rectangle // always fully contains the ImageryProvider's rectangle. const imageryBounds = Rectangle.intersection( imageryProvider.rectangle, this._rectangle, imageryBoundsScratch, ); let rectangle = Rectangle.intersection( tile.rectangle, imageryBounds, tileImageryBoundsScratch, ); if (!defined(rectangle)) { // There is no overlap between this terrain tile and this imagery // provider. Unless this is the base layer, no skeletons need to be created. // We stretch texels at the edge of the base layer over the entire globe. if (!this.isBaseLayer()) { return false; } const baseImageryRectangle = imageryBounds; const baseTerrainRectangle = tile.rectangle; rectangle = tileImageryBoundsScratch; if (baseTerrainRectangle.south >= baseImageryRectangle.north) { rectangle.north = rectangle.south = baseImageryRectangle.north; } else if (baseTerrainRectangle.north <= baseImageryRectangle.south) { rectangle.north = rectangle.south = baseImageryRectangle.south; } else { rectangle.south = Math.max( baseTerrainRectangle.south, baseImageryRectangle.south, ); rectangle.north = Math.min( baseTerrainRectangle.north, baseImageryRectangle.north, ); } if (baseTerrainRectangle.west >= baseImageryRectangle.east) { rectangle.west = rectangle.east = baseImageryRectangle.east; } else if (baseTerrainRectangle.east <= baseImageryRectangle.west) { rectangle.west = rectangle.east = baseImageryRectangle.west; } else { rectangle.west = Math.max( baseTerrainRectangle.west, baseImageryRectangle.west, ); rectangle.east = Math.min( baseTerrainRectangle.east, baseImageryRectangle.east, ); } } let latitudeClosestToEquator = 0.0; if (rectangle.south > 0.0) { latitudeClosestToEquator = rectangle.south; } else if (rectangle.north < 0.0) { latitudeClosestToEquator = rectangle.north; } // Compute the required level in the imagery tiling scheme. // The errorRatio should really be imagerySSE / terrainSSE rather than this hard-coded value. // But first we need configurable imagery SSE and we need the rendering to be able to handle more // images attached to a terrain tile than there are available texture units. So that's for the future. const errorRatio = 1.0; const targetGeometricError = errorRatio * terrainProvider.getLevelMaximumGeometricError(tile.level); let imageryLevel = getLevelWithMaximumTexelSpacing( this, targetGeometricError, latitudeClosestToEquator, ); imageryLevel = Math.max(0, imageryLevel); const maximumLevel = imageryProvider.maximumLevel; if (imageryLevel > maximumLevel) { imageryLevel = maximumLevel; } if (defined(imageryProvider.minimumLevel)) { const minimumLevel = imageryProvider.minimumLevel; if (imageryLevel < minimumLevel) { imageryLevel = minimumLevel; } } const imageryTilingScheme = imageryProvider.tilingScheme; const northwestTileCoordinates = imageryTilingScheme.positionToTileXY( Rectangle.northwest(rectangle), imageryLevel, ); const southeastTileCoordinates = imageryTilingScheme.positionToTileXY( Rectangle.southeast(rectangle), imageryLevel, ); // 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. let veryCloseX = tile.rectangle.width / 512.0; let veryCloseY = tile.rectangle.height / 512.0; const northwestTileRectangle = imageryTilingScheme.tileXYToRectangle( northwestTileCoordinates.x, northwestTileCoordinates.y, imageryLevel, ); if ( Math.abs(northwestTileRectangle.south - tile.rectangle.north) < veryCloseY && northwestTileCoordinates.y < southeastTileCoordinates.y ) { ++northwestTileCoordinates.y; } if ( Math.abs(northwestTileRectangle.east - tile.rectangle.west) < veryCloseX && northwestTileCoordinates.x < southeastTileCoordinates.x ) { ++northwestTileCoordinates.x; } const southeastTileRectangle = imageryTilingScheme.tileXYToRectangle( southeastTileCoordinates.x, southeastTileCoordinates.y, imageryLevel, ); if ( Math.abs(southeastTileRectangle.north - tile.rectangle.south) < veryCloseY && southeastTileCoordinates.y > northwestTileCoordinates.y ) { --southeastTileCoordinates.y; } if ( Math.abs(southeastTileRectangle.west - tile.rectangle.east) < veryCloseX && southeastTileCoordinates.x > northwestTileCoordinates.x ) { --southeastTileCoordinates.x; } // Create TileImagery instances for each imagery tile overlapping this terrain tile. // We need to do all texture coordinate computations in the imagery tile's tiling scheme. const terrainRectangle = Rectangle.clone( tile.rectangle, terrainRectangleScratch, ); let imageryRectangle = imageryTilingScheme.tileXYToRectangle( northwestTileCoordinates.x, northwestTileCoordinates.y, imageryLevel, ); let clippedImageryRectangle = Rectangle.intersection( imageryRectangle, imageryBounds, clippedRectangleScratch, ); let imageryTileXYToRectangle; if (useWebMercatorT) { imageryTilingScheme.rectangleToNativeRectangle( terrainRectangle, terrainRectangle, ); imageryTilingScheme.rectangleToNativeRectangle( imageryRectangle, imageryRectangle, ); imageryTilingScheme.rectangleToNativeRectangle( clippedImageryRectangle, clippedImageryRectangle, ); imageryTilingScheme.rectangleToNativeRectangle( imageryBounds, imageryBounds, ); imageryTileXYToRectangle = imageryTilingScheme.tileXYToNativeRectangle.bind(imageryTilingScheme); veryCloseX = terrainRectangle.width / 512.0; veryCloseY = terrainRectangle.height / 512.0; } else { imageryTileXYToRectangle = imageryTilingScheme.tileXYToRectangle.bind(imageryTilingScheme); } let minU; let maxU = 0.0; let minV = 1.0; let maxV; // If this is the northern-most or western-most tile in the imagery tiling scheme, // it may not start at the northern or western edge of the terrain tile. // Calculate where it does start. if ( !this.isBaseLayer() && Math.abs(clippedImageryRectangle.west - terrainRectangle.west) >= veryCloseX ) { maxU = Math.min( 1.0, (clippedImageryRectangle.west - terrainRectangle.west) / terrainRectangle.width, ); } if ( !this.isBaseLayer() && Math.abs(clippedImageryRectangle.north - terrainRectangle.north) >= veryCloseY ) { minV = Math.max( 0.0, (clippedImageryRectangle.north - terrainRectangle.south) / terrainRectangle.height, ); } const initialMinV = minV; for ( let i = northwestTileCoordinates.x; i <= southeastTileCoordinates.x; i++ ) { minU = maxU; imageryRectangle = imageryTileXYToRectangle( i, northwestTileCoordinates.y, imageryLevel, ); clippedImageryRectangle = Rectangle.simpleIntersection( imageryRectangle, imageryBounds, clippedRectangleScratch, ); if (!defined(clippedImageryRectangle)) { continue; } maxU = Math.min( 1.0, (clippedImageryRectangle.east - terrainRectangle.west) / terrainRectangle.width, ); // If this is the eastern-most imagery tile mapped to this terrain tile, // and there are more imagery tiles to the east of this one, the maxU // should be 1.0 to make sure rounding errors don't make the last // image fall shy of the edge of the terrain tile. if ( i === southeastTileCoordinates.x && (this.isBaseLayer() || Math.abs(clippedImageryRectangle.east - terrainRectangle.east) < veryCloseX) ) { maxU = 1.0; } minV = initialMinV; for ( let j = northwestTileCoordinates.y; j <= southeastTileCoordinates.y; j++ ) { maxV = minV; imageryRectangle = imageryTileXYToRectangle(i, j, imageryLevel); clippedImageryRectangle = Rectangle.simpleIntersection( imageryRectangle, imageryBounds, clippedRectangleScratch, ); if (!defined(clippedImageryRectangle)) { continue; } minV = Math.max( 0.0, (clippedImageryRectangle.south - terrainRectangle.south) / terrainRectangle.height, ); // If this is the southern-most imagery tile mapped to this terrain tile, // and there are more imagery tiles to the south of this one, the minV // should be 0.0 to make sure rounding errors don't make the last // image fall shy of the edge of the terrain tile. if ( j === southeastTileCoordinates.y && (this.isBaseLayer() || Math.abs(clippedImageryRectangle.south - terrainRectangle.south) < veryCloseY) ) { minV = 0.0; } const texCoordsRectangle = new Cartesian4(minU, minV, maxU, maxV); const imagery = this.getImageryFromCache(i, j, imageryLevel); surfaceTile.imagery.splice( insertionPoint, 0, new TileImagery(imagery, texCoordsRectangle, useWebMercatorT), ); ++insertionPoint; } } return true; }; /** * Calculate the translation and scale for a particular {@link TileImagery} attached to a * particular terrain tile. * * @private * * @param {Tile} tile The terrain tile. * @param {TileImagery} tileImagery The imagery tile mapping. * @returns {Cartesian4} The translation and scale where X and Y are the translation and Z and W * are the scale. */ ImageryLayer.prototype._calculateTextureTranslationAndScale = function ( tile, tileImagery, ) { let imageryRectangle = tileImagery.readyImagery.rectangle; let terrainRectangle = tile.rectangle; if (tileImagery.useWebMercatorT) { const tilingScheme = tileImagery.readyImagery.imageryLayer.imageryProvider.tilingScheme; imageryRectangle = tilingScheme.rectangleToNativeRectangle( imageryRectangle, imageryBoundsScratch, ); terrainRectangle = tilingScheme.rectangleToNativeRectangle( terrainRectangle, terrainRectangleScratch, ); } const terrainWidth = terrainRectangle.width; const terrainHeight = terrainRectangle.height; const scaleX = terrainWidth / imageryRectangle.width; const scaleY = terrainHeight / imageryRectangle.height; return new Cartesian4( (scaleX * (terrainRectangle.west - imageryRectangle.west)) / terrainWidth, (scaleY * (terrainRectangle.south - imageryRectangle.south)) / terrainHeight, scaleX, scaleY, ); }; /** * Request a particular piece of imagery from the imagery provider. This method handles raising an * error event if the request fails, and retrying the request if necessary. * * @private * * @param {Imagery} imagery The imagery to request. */ ImageryLayer.prototype._requestImagery = function (imagery) { const imageryProvider = this._imageryProvider; const that = this; function success(image) { if (!defined(image)) { return failure(); } imagery.image = image; imagery.state = ImageryState.RECEIVED; imagery.request = undefined; TileProviderError.reportSuccess(that._requestImageError); } function failure(e) { if (imagery.request.state === RequestState.CANCELLED) { // Cancelled due to low priority - try again later. imagery.state = ImageryState.UNLOADED; imagery.request = undefined; return; } // Initially assume failure. An error handler may retry, in which case the state will // change to TRANSITIONING. imagery.state = ImageryState.FAILED; imagery.request = undefined; const message = `Failed to obtain image tile X: ${imagery.x} Y: ${imagery.y} Level: ${imagery.level}.`; that._requestImageError = TileProviderError.reportError( that._requestImageError, imageryProvider, imageryProvider.errorEvent, message, imagery.x, imagery.y, imagery.level, e, ); if (that._requestImageError.retry) { doRequest(); } } function doRequest() { const request = new Request({ throttle: false, throttleByServer: true, type: RequestType.IMAGERY, }); imagery.request = request; imagery.state = ImageryState.TRANSITIONING; const imagePromise = imageryProvider.requestImage( imagery.x, imagery.y, imagery.level, request, ); if (!defined(imagePromise)) { // Too many parallel requests, so postpone loading tile. imagery.state = ImageryState.UNLOADED; imagery.request = undefined; return; } if (defined(imageryProvider.getTileCredits)) { imagery.credits = imageryProvider.getTileCredits( imagery.x, imagery.y, imagery.level, ); } imagePromise .then(function (image) { success(image); }) .catch(function (e) { failure(e); }); } doRequest(); }; ImageryLayer.prototype._createTextureWebGL = function (context, imagery) { const sampler = new Sampler({ minificationFilter: this.minificationFilter, magnificationFilter: this.magnificationFilter, }); const image = imagery.image; if (defined(image.internalFormat)) { return new Texture({ context: context, pixelFormat: image.internalFormat, width: image.width, height: image.height, source: { arrayBufferView: image.bufferView, }, sampler: sampler, }); } return new Texture({ context: context, source: image, pixelFormat: this._imageryProvider.hasAlphaChannel ? PixelFormat.RGBA : PixelFormat.RGB, sampler: sampler, }); }; /** * Create a WebGL texture for a given {@link Imagery} instance. * * @private * * @param {Context} context The rendered context to use to create textures. * @param {Imagery} imagery The imagery for which to create a texture. */ ImageryLayer.prototype._createTexture = function (context, imagery) { const imageryProvider = this._imageryProvider; const image = imagery.image; // If this imagery provider has a discard policy, use it to check if this // image should be discarded. if (defined(imageryProvider.tileDiscardPolicy)) { const discardPolicy = imageryProvider.tileDiscardPolicy; if (defined(discardPolicy)) { // If the discard policy is not ready yet, transition back to the // RECEIVED state and we'll try again next time. if (!discardPolicy.isReady()) { imagery.state = ImageryState.RECEIVED; return; } // Mark discarded imagery tiles invalid. Parent imagery will be used instead. if (discardPolicy.shouldDiscardImage(image)) { imagery.state = ImageryState.INVALID; return; } } } //>>includeStart('debug', pragmas.debug); if ( this.minificationFilter !== TextureMinificationFilter.NEAREST && this.minificationFilter !== TextureMinificationFilter.LINEAR ) { throw new DeveloperError( "ImageryLayer minification filter must be NEAREST or LINEAR", ); } //>>includeEnd('debug'); // Imagery does not need to be discarded, so upload it to WebGL. const texture = this._createTextureWebGL(context, imagery); if ( imageryProvider.tilingScheme.projection instanceof WebMercatorProjection ) { imagery.textureWebMercator = texture; } else { imagery.texture = texture; } imagery.image = undefined; imagery.state = ImageryState.TEXTURE_LOADED; }; function getSamplerKey( minificationFilter, magnificationFilter, maximumAnisotropy, ) { return `${minificationFilter}:${magnificationFilter}:${maximumAnisotropy}`; } ImageryLayer.prototype._finalizeReprojectTexture = function (context, texture) { let minificationFilter = this.minificationFilter; const magnificationFilter = this.magnificationFilter; const usesLinearTextureFilter = minificationFilter === TextureMinificationFilter.LINEAR && magnificationFilter === TextureMagnificationFilter.LINEAR; // Use mipmaps if this texture has power-of-two dimensions. // In addition, mipmaps are only generated if the texture filters are both LINEAR. if ( usesLinearTextureFilter && !PixelFormat.isCompressedFormat(texture.pixelFormat) && CesiumMath.isPowerOfTwo(texture.width) && CesiumMath.isPowerOfTwo(texture.height) ) { minificationFilter = TextureMinificationFilter.LINEAR_MIPMAP_LINEAR; const maximumSupportedAnisotropy = ContextLimits.maximumTextureFilterAnisotropy; const maximumAnisotropy = Math.min( maximumSupportedAnisotropy, this._maximumAnisotropy ?? maximumSupportedAnisotropy, ); const mipmapSamplerKey = getSamplerKey( minificationFilter, magnificationFilter, maximumAnisotropy, ); let mipmapSamplers = context.cache.imageryLayerMipmapSamplers; if (!defined(mipmapSamplers)) { mipmapSamplers = {}; context.cache.imageryLayerMipmapSamplers = mipmapSamplers; } let mipmapSampler = mipmapSamplers[mipmapSamplerKey]; if (!defined(mipmapSampler)) { mipmapSampler = mipmapSamplers[mipmapSamplerKey] = new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: minificationFilter, magnificationFilter: magnificationFilter, maximumAnisotropy: maximumAnisotropy, }); } texture.generateMipmap(MipmapHint.NICEST); texture.sampler = mipmapSampler; } else { const nonMipmapSamplerKey = getSamplerKey( minificationFilter, magnificationFilter, 0, ); let nonMipmapSamplers = context.cache.imageryLayerNonMipmapSamplers; if (!defined(nonMipmapSamplers)) { nonMipmapSamplers = {}; context.cache.imageryLayerNonMipmapSamplers = nonMipmapSamplers; } let nonMipmapSampler = nonMipmapSamplers[nonMipmapSamplerKey]; if (!defined(nonMipmapSampler)) { nonMipmapSampler = nonMipmapSamplers[nonMipmapSamplerKey] = new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: minificationFilter, magnificationFilter: magnificationFilter, }); } texture.sampler = nonMipmapSampler; } }; /** * Enqueues a command re-projecting a texture to a {@link GeographicProjection} on the next update, if necessary, and generate * mipmaps for the geographic texture. * * @private * * @param {FrameState} frameState The frameState. * @param {Imagery} imagery The imagery instance to reproject. * @param {boolean}