@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
JavaScript
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}