import BoundingRectangle from "../Core/BoundingRectangle.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Cartographic from "../Core/Cartographic.js"; import Check from "../Core/Check.js"; import Color from "../Core/Color.js"; import createGuid from "../Core/createGuid.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js"; import Matrix4 from "../Core/Matrix4.js"; import NearFarScalar from "../Core/NearFarScalar.js"; import Resource from "../Core/Resource.js"; import HeightReference from "./HeightReference.js"; import HorizontalOrigin from "./HorizontalOrigin.js"; import SceneMode from "./SceneMode.js"; import SceneTransforms from "./SceneTransforms.js"; import VerticalOrigin from "./VerticalOrigin.js"; /** *
x
increases from
* left to right, and y
increases from top to bottom.
* default ![]() |
* b.pixeloffset = new Cartesian2(50, 25); ![]() |
*
x
points towards the viewer's right, y
points up, and
* z
points into the screen. Eye coordinates use the same scale as world and model coordinates,
* which is typically meters.
* ![]() |
* ![]() |
*
b.eyeOffset = new Cartesian3(0.0, 8000000.0, 0.0);
1.0
does not change the size of the billboard; a scale greater than
* 1.0
enlarges the billboard; a positive scale less than 1.0
shrinks
* the billboard.
* 0.5
, 1.0
,
* and 2.0
.
* 0.0
makes the billboard transparent, and 1.0
makes the billboard opaque.
* default ![]() |
* alpha : 0.5 ![]() |
*
value
's red
, green
,
* blue
, and alpha
properties as shown in Example 1. These components range from 0.0
* (no intensity) to 1.0
(full intensity).
* @memberof Billboard.prototype
* @type {Color}
*
* @example
* // Example 1. Assign yellow.
* b.color = Cesium.Color.YELLOW;
*
* @example
* // Example 2. Make a billboard 50% translucent.
* b.color = new Cesium.Color(1.0, 1.0, 1.0, 0.5);
*/
color: {
get: function () {
return this._color;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const color = this._color;
if (!Color.equals(color, value)) {
Color.clone(value, color);
makeDirty(this, COLOR_INDEX);
}
},
},
/**
* Gets or sets the rotation angle in radians.
* @memberof Billboard.prototype
* @type {number}
*/
rotation: {
get: function () {
return this._rotation;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("value", value);
//>>includeEnd('debug');
if (this._rotation !== value) {
this._rotation = value;
makeDirty(this, ROTATION_INDEX);
}
},
},
/**
* Gets or sets the aligned axis in world space. The aligned axis is the unit vector that the billboard up vector points towards.
* The default is the zero vector, which means the billboard is aligned to the screen up vector.
* @memberof Billboard.prototype
* @type {Cartesian3}
* @example
* // Example 1.
* // Have the billboard up vector point north
* billboard.alignedAxis = Cesium.Cartesian3.UNIT_Z;
*
* @example
* // Example 2.
* // Have the billboard point east.
* billboard.alignedAxis = Cesium.Cartesian3.UNIT_Z;
* billboard.rotation = -Cesium.Math.PI_OVER_TWO;
*
* @example
* // Example 3.
* // Reset the aligned axis
* billboard.alignedAxis = Cesium.Cartesian3.ZERO;
*/
alignedAxis: {
get: function () {
return this._alignedAxis;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("value", value);
//>>includeEnd('debug');
const alignedAxis = this._alignedAxis;
if (!Cartesian3.equals(alignedAxis, value)) {
Cartesian3.clone(value, alignedAxis);
makeDirty(this, ALIGNED_AXIS_INDEX);
}
},
},
/**
* Gets or sets a width for the billboard. If undefined, the image width will be used.
* @memberof Billboard.prototype
* @type {number}
*/
width: {
get: function () {
return defaultValue(this._width, this._imageWidth);
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
if (defined(value)) {
Check.typeOf.number("value", value);
}
//>>includeEnd('debug');
if (this._width !== value) {
this._width = value;
makeDirty(this, IMAGE_INDEX_INDEX);
}
},
},
/**
* Gets or sets a height for the billboard. If undefined, the image height will be used.
* @memberof Billboard.prototype
* @type {number}
*/
height: {
get: function () {
return defaultValue(this._height, this._imageHeight);
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
if (defined(value)) {
Check.typeOf.number("value", value);
}
//>>includeEnd('debug');
if (this._height !== value) {
this._height = value;
makeDirty(this, IMAGE_INDEX_INDEX);
}
},
},
/**
* Gets or sets if the billboard size is in meters or pixels. true
to size the billboard in meters;
* otherwise, the size is in pixels.
* @memberof Billboard.prototype
* @type {boolean}
* @default false
*/
sizeInMeters: {
get: function () {
return this._sizeInMeters;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("value", value);
//>>includeEnd('debug');
if (this._sizeInMeters !== value) {
this._sizeInMeters = value;
makeDirty(this, COLOR_INDEX);
}
},
},
/**
* Gets or sets the condition specifying at what distance from the camera that this billboard will be displayed.
* @memberof Billboard.prototype
* @type {DistanceDisplayCondition}
* @default undefined
*/
distanceDisplayCondition: {
get: function () {
return this._distanceDisplayCondition;
},
set: function (value) {
if (
!DistanceDisplayCondition.equals(value, this._distanceDisplayCondition)
) {
//>>includeStart('debug', pragmas.debug);
if (defined(value)) {
Check.typeOf.object("value", value);
if (value.far <= value.near) {
throw new DeveloperError(
"far distance must be greater than near distance."
);
}
}
//>>includeEnd('debug');
this._distanceDisplayCondition = DistanceDisplayCondition.clone(
value,
this._distanceDisplayCondition
);
makeDirty(this, DISTANCE_DISPLAY_CONDITION);
}
},
},
/**
* Gets or sets the distance from the camera at which to disable the depth test to, for example, prevent clipping against terrain.
* When set to zero, the depth test is always applied. When set to Number.POSITIVE_INFINITY, the depth test is never applied.
* @memberof Billboard.prototype
* @type {number}
*/
disableDepthTestDistance: {
get: function () {
return this._disableDepthTestDistance;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
if (defined(value)) {
Check.typeOf.number("value", value);
if (value < 0.0) {
throw new DeveloperError(
"disableDepthTestDistance must be greater than or equal to 0.0."
);
}
}
//>>includeEnd('debug');
if (this._disableDepthTestDistance !== value) {
this._disableDepthTestDistance = value;
makeDirty(this, DISABLE_DEPTH_DISTANCE);
}
},
},
/**
* Gets or sets the user-defined object returned when the billboard is picked.
* @memberof Billboard.prototype
* @type {object}
*/
id: {
get: function () {
return this._id;
},
set: function (value) {
this._id = value;
if (defined(this._pickId)) {
this._pickId.object.id = value;
}
},
},
/**
* The primitive to return when picking this billboard.
* @memberof Billboard.prototype
* @private
*/
pickPrimitive: {
get: function () {
return this._pickPrimitive;
},
set: function (value) {
this._pickPrimitive = value;
if (defined(this._pickId)) {
this._pickId.object.primitive = value;
}
},
},
/**
* @private
*/
pickId: {
get: function () {
return this._pickId;
},
},
/**
* * Gets or sets the image to be used for this billboard. If a texture has already been created for the * given image, the existing texture is used. *
** This property can be set to a loaded Image, a URL which will be loaded as an Image automatically, * a canvas, or another billboard's image property (from the same billboard collection). *
* * @memberof Billboard.prototype * @type {string} * @example * // load an image from a URL * b.image = 'some/image/url.png'; * * // assuming b1 and b2 are billboards in the same billboard collection, * // use the same image for both billboards. * b2.image = b1.image; */ image: { get: function () { return this._imageId; }, set: function (value) { if (!defined(value)) { this._imageIndex = -1; this._imageSubRegion = undefined; this._imageId = undefined; this._image = undefined; this._imageIndexPromise = undefined; makeDirty(this, IMAGE_INDEX_INDEX); } else if (typeof value === "string") { this.setImage(value, value); } else if (value instanceof Resource) { this.setImage(value.url, value); } else if (defined(value.src)) { this.setImage(value.src, value); } else { this.setImage(createGuid(), value); } }, }, /** * Whentrue
, this billboard is ready to render, i.e., the image
* has been downloaded and the WebGL resources are created.
*
* @memberof Billboard.prototype
*
* @type {boolean}
* @readonly
*
* @default false
*/
ready: {
get: function () {
return this._imageIndex !== -1;
},
},
/**
* Keeps track of the position of the billboard based on the height reference.
* @memberof Billboard.prototype
* @type {Cartesian3}
* @private
*/
_clampedPosition: {
get: function () {
return this._actualClampedPosition;
},
set: function (value) {
this._actualClampedPosition = Cartesian3.clone(
value,
this._actualClampedPosition
);
makeDirty(this, POSITION_INDEX);
},
},
/**
* Determines whether or not this billboard will be shown or hidden because it was clustered.
* @memberof Billboard.prototype
* @type {boolean}
* @private
*/
clusterShow: {
get: function () {
return this._clusterShow;
},
set: function (value) {
if (this._clusterShow !== value) {
this._clusterShow = value;
makeDirty(this, SHOW_INDEX);
}
},
},
/**
* The outline color of this Billboard. Effective only for SDF billboards like Label glyphs.
* @memberof Billboard.prototype
* @type {Color}
* @private
*/
outlineColor: {
get: function () {
return this._outlineColor;
},
set: function (value) {
//>>includeStart('debug', pragmas.debug);
if (!defined(value)) {
throw new DeveloperError("value is required.");
}
//>>includeEnd('debug');
const outlineColor = this._outlineColor;
if (!Color.equals(outlineColor, value)) {
Color.clone(value, outlineColor);
makeDirty(this, SDF_INDEX);
}
},
},
/**
* The outline width of this Billboard in pixels. Effective only for SDF billboards like Label glyphs.
* @memberof Billboard.prototype
* @type {number}
* @private
*/
outlineWidth: {
get: function () {
return this._outlineWidth;
},
set: function (value) {
if (this._outlineWidth !== value) {
this._outlineWidth = value;
makeDirty(this, SDF_INDEX);
}
},
},
});
Billboard.prototype.getPickId = function (context) {
if (!defined(this._pickId)) {
this._pickId = context.createPickId({
primitive: this._pickPrimitive,
collection: this._collection,
id: this._id,
});
}
return this._pickId;
};
Billboard.prototype._updateClamping = function () {
Billboard._updateClamping(this._billboardCollection, this);
};
const scratchCartographic = new Cartographic();
const scratchPosition = new Cartesian3();
Billboard._updateClamping = function (collection, owner) {
const scene = collection._scene;
if (!defined(scene) || !defined(scene.globe)) {
//>>includeStart('debug', pragmas.debug);
if (owner._heightReference !== HeightReference.NONE) {
throw new DeveloperError(
"Height reference is not supported without a scene and globe."
);
}
//>>includeEnd('debug');
return;
}
const globe = scene.globe;
const ellipsoid = globe.ellipsoid;
const surface = globe._surface;
const mode = scene.frameState.mode;
const modeChanged = mode !== owner._mode;
owner._mode = mode;
if (
(owner._heightReference === HeightReference.NONE || modeChanged) &&
defined(owner._removeCallbackFunc)
) {
owner._removeCallbackFunc();
owner._removeCallbackFunc = undefined;
owner._clampedPosition = undefined;
}
if (
owner._heightReference === HeightReference.NONE ||
!defined(owner._position)
) {
return;
}
const position = ellipsoid.cartesianToCartographic(owner._position);
if (!defined(position)) {
owner._actualClampedPosition = undefined;
return;
}
if (defined(owner._removeCallbackFunc)) {
owner._removeCallbackFunc();
}
function updateFunction(clampedPosition) {
if (owner._heightReference === HeightReference.RELATIVE_TO_GROUND) {
if (owner._mode === SceneMode.SCENE3D) {
const clampedCart = ellipsoid.cartesianToCartographic(
clampedPosition,
scratchCartographic
);
clampedCart.height += position.height;
ellipsoid.cartographicToCartesian(clampedCart, clampedPosition);
} else {
clampedPosition.x += position.height;
}
}
owner._clampedPosition = Cartesian3.clone(
clampedPosition,
owner._clampedPosition
);
}
owner._removeCallbackFunc = surface.updateHeight(position, updateFunction);
Cartographic.clone(position, scratchCartographic);
const height = globe.getHeight(position);
if (defined(height)) {
scratchCartographic.height = height;
}
ellipsoid.cartographicToCartesian(scratchCartographic, scratchPosition);
updateFunction(scratchPosition);
};
Billboard.prototype._loadImage = function () {
const atlas = this._billboardCollection._textureAtlas;
const imageId = this._imageId;
const image = this._image;
const imageSubRegion = this._imageSubRegion;
let imageIndexPromise;
const that = this;
function completeImageLoad(index) {
if (
that._imageId !== imageId ||
that._image !== image ||
!BoundingRectangle.equals(that._imageSubRegion, imageSubRegion)
) {
// another load occurred before this one finished, ignore the index
return;
}
// fill in imageWidth and imageHeight
const textureCoordinates = atlas.textureCoordinates[index];
that._imageWidth = atlas.texture.width * textureCoordinates.width;
that._imageHeight = atlas.texture.height * textureCoordinates.height;
that._imageIndex = index;
that._ready = true;
that._image = undefined;
that._imageIndexPromise = undefined;
makeDirty(that, IMAGE_INDEX_INDEX);
}
if (defined(image)) {
// No need to wait on imageIndexPromise since these have already been added to the atlas
const index = atlas.getImageIndex(imageId);
if (defined(index)) {
completeImageLoad(index);
return;
}
imageIndexPromise = atlas.addImage(imageId, image);
}
if (defined(imageSubRegion)) {
imageIndexPromise = atlas.addSubRegion(imageId, imageSubRegion);
}
this._imageIndexPromise = imageIndexPromise;
if (!defined(imageIndexPromise)) {
return;
}
imageIndexPromise.then(completeImageLoad).catch(function (error) {
console.error(`Error loading image for billboard: ${error}`);
that._imageIndexPromise = undefined;
});
};
/**
* * Sets the image to be used for this billboard. If a texture has already been created for the * given id, the existing texture is used. *
** This function is useful for dynamically creating textures that are shared across many billboards. * Only the first billboard will actually call the function and create the texture, while subsequent * billboards created with the same id will simply re-use the existing texture. *
** To load an image from a URL, setting the {@link Billboard#image} property is more convenient. *
* * @param {string} id The id of the image. This can be any string that uniquely identifies the image. * @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Billboard.CreateImageCallback} image The image to load. This parameter * can either be a loaded Image or Canvas, a URL which will be loaded as an Image automatically, * or a function which will be called to create the image if it hasn't been loaded already. * @example * // create a billboard image dynamically * function drawImage(id) { * // create and draw an image using a canvas * const canvas = document.createElement('canvas'); * const context2D = canvas.getContext('2d'); * // ... draw image * return canvas; * } * // drawImage will be called to create the texture * b.setImage('myImage', drawImage); * * // subsequent billboards created in the same collection using the same id will use the existing * // texture, without the need to create the canvas or draw the image * b2.setImage('myImage', drawImage); */ Billboard.prototype.setImage = function (id, image) { //>>includeStart('debug', pragmas.debug); if (!defined(id)) { throw new DeveloperError("id is required."); } if (!defined(image)) { throw new DeveloperError("image is required."); } //>>includeEnd('debug'); if (this._imageId === id) { return; } this._imageIndex = -1; this._imageSubRegion = undefined; this._imageId = id; this._image = image; if (defined(this._billboardCollection._textureAtlas)) { this._loadImage(); } }; /** * Uses a sub-region of the image with the given id as the image for this billboard, * measured in pixels from the bottom-left. * * @param {string} id The id of the image to use. * @param {BoundingRectangle} subRegion The sub-region of the image. * * @exception {RuntimeError} image with id must be in the atlas */ Billboard.prototype.setImageSubRegion = function (id, subRegion) { //>>includeStart('debug', pragmas.debug); if (!defined(id)) { throw new DeveloperError("id is required."); } if (!defined(subRegion)) { throw new DeveloperError("subRegion is required."); } //>>includeEnd('debug'); if ( this._imageId === id && BoundingRectangle.equals(this._imageSubRegion, subRegion) ) { return; } this._imageIndex = -1; this._imageId = id; this._imageSubRegion = BoundingRectangle.clone(subRegion); if (defined(this._billboardCollection._textureAtlas)) { this._loadImage(); } }; Billboard.prototype._setTranslate = function (value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError("value is required."); } //>>includeEnd('debug'); const translate = this._translate; if (!Cartesian2.equals(translate, value)) { Cartesian2.clone(value, translate); makeDirty(this, PIXEL_OFFSET_INDEX); } }; Billboard.prototype._getActualPosition = function () { return defined(this._clampedPosition) ? this._clampedPosition : this._actualPosition; }; Billboard.prototype._setActualPosition = function (value) { if (!defined(this._clampedPosition)) { Cartesian3.clone(value, this._actualPosition); } makeDirty(this, POSITION_INDEX); }; const tempCartesian3 = new Cartesian4(); Billboard._computeActualPosition = function ( billboard, position, frameState, modelMatrix ) { if (defined(billboard._clampedPosition)) { if (frameState.mode !== billboard._mode) { billboard._updateClamping(); } return billboard._clampedPosition; } else if (frameState.mode === SceneMode.SCENE3D) { return position; } Matrix4.multiplyByPoint(modelMatrix, position, tempCartesian3); return SceneTransforms.computeActualWgs84Position(frameState, tempCartesian3); }; const scratchCartesian3 = new Cartesian3(); // This function is basically a stripped-down JavaScript version of BillboardCollectionVS.glsl Billboard._computeScreenSpacePosition = function ( modelMatrix, position, eyeOffset, pixelOffset, scene, result ) { // Model to world coordinates const positionWorld = Matrix4.multiplyByPoint( modelMatrix, position, scratchCartesian3 ); // World to window coordinates const positionWC = SceneTransforms.wgs84WithEyeOffsetToWindowCoordinates( scene, positionWorld, eyeOffset, result ); if (!defined(positionWC)) { return undefined; } // Apply pixel offset Cartesian2.add(positionWC, pixelOffset, positionWC); return positionWC; }; const scratchPixelOffset = new Cartesian2(0.0, 0.0); /** * Computes the screen-space position of the billboard's origin, taking into account eye and pixel offsets. * The screen space origin is the top, left corner of the canvas;x
increases from
* left to right, and y
increases from top to bottom.
*
* @param {Scene} scene The scene.
* @param {Cartesian2} [result] The object onto which to store the result.
* @returns {Cartesian2} The screen-space position of the billboard.
*
* @exception {DeveloperError} Billboard must be in a collection.
*
* @example
* console.log(b.computeScreenSpacePosition(scene).toString());
*
* @see Billboard#eyeOffset
* @see Billboard#pixelOffset
*/
Billboard.prototype.computeScreenSpacePosition = function (scene, result) {
const billboardCollection = this._billboardCollection;
if (!defined(result)) {
result = new Cartesian2();
}
//>>includeStart('debug', pragmas.debug);
if (!defined(billboardCollection)) {
throw new DeveloperError(
"Billboard must be in a collection. Was it removed?"
);
}
if (!defined(scene)) {
throw new DeveloperError("scene is required.");
}
//>>includeEnd('debug');
// pixel offset for screen space computation is the pixelOffset + screen space translate
Cartesian2.clone(this._pixelOffset, scratchPixelOffset);
Cartesian2.add(scratchPixelOffset, this._translate, scratchPixelOffset);
let modelMatrix = billboardCollection.modelMatrix;
let position = this._position;
if (defined(this._clampedPosition)) {
position = this._clampedPosition;
if (scene.mode !== SceneMode.SCENE3D) {
// position needs to be in world coordinates
const projection = scene.mapProjection;
const ellipsoid = projection.ellipsoid;
const cart = projection.unproject(position, scratchCartographic);
position = ellipsoid.cartographicToCartesian(cart, scratchCartesian3);
modelMatrix = Matrix4.IDENTITY;
}
}
const windowCoordinates = Billboard._computeScreenSpacePosition(
modelMatrix,
position,
this._eyeOffset,
scratchPixelOffset,
scene,
result
);
return windowCoordinates;
};
/**
* Gets a billboard's screen space bounding box centered around screenSpacePosition.
* @param {Billboard} billboard The billboard to get the screen space bounding box for.
* @param {Cartesian2} screenSpacePosition The screen space center of the label.
* @param {BoundingRectangle} [result] The object onto which to store the result.
* @returns {BoundingRectangle} The screen space bounding box.
*
* @private
*/
Billboard.getScreenSpaceBoundingBox = function (
billboard,
screenSpacePosition,
result
) {
let width = billboard.width;
let height = billboard.height;
const scale = billboard.scale;
width *= scale;
height *= scale;
let x = screenSpacePosition.x;
if (billboard.horizontalOrigin === HorizontalOrigin.RIGHT) {
x -= width;
} else if (billboard.horizontalOrigin === HorizontalOrigin.CENTER) {
x -= width * 0.5;
}
let y = screenSpacePosition.y;
if (
billboard.verticalOrigin === VerticalOrigin.BOTTOM ||
billboard.verticalOrigin === VerticalOrigin.BASELINE
) {
y -= height;
} else if (billboard.verticalOrigin === VerticalOrigin.CENTER) {
y -= height * 0.5;
}
if (!defined(result)) {
result = new BoundingRectangle();
}
result.x = x;
result.y = y;
result.width = width;
result.height = height;
return result;
};
/**
* Determines if this billboard equals another billboard. Billboards are equal if all their properties
* are equal. Billboards in different collections can be equal.
*
* @param {Billboard} other The billboard to compare for equality.
* @returns {boolean} true
if the billboards are equal; otherwise, false
.
*/
Billboard.prototype.equals = function (other) {
return (
this === other ||
(defined(other) &&
this._id === other._id &&
Cartesian3.equals(this._position, other._position) &&
this._imageId === other._imageId &&
this._show === other._show &&
this._scale === other._scale &&
this._verticalOrigin === other._verticalOrigin &&
this._horizontalOrigin === other._horizontalOrigin &&
this._heightReference === other._heightReference &&
BoundingRectangle.equals(this._imageSubRegion, other._imageSubRegion) &&
Color.equals(this._color, other._color) &&
Cartesian2.equals(this._pixelOffset, other._pixelOffset) &&
Cartesian2.equals(this._translate, other._translate) &&
Cartesian3.equals(this._eyeOffset, other._eyeOffset) &&
NearFarScalar.equals(this._scaleByDistance, other._scaleByDistance) &&
NearFarScalar.equals(
this._translucencyByDistance,
other._translucencyByDistance
) &&
NearFarScalar.equals(
this._pixelOffsetScaleByDistance,
other._pixelOffsetScaleByDistance
) &&
DistanceDisplayCondition.equals(
this._distanceDisplayCondition,
other._distanceDisplayCondition
) &&
this._disableDepthTestDistance === other._disableDepthTestDistance)
);
};
Billboard.prototype._destroy = function () {
if (defined(this._customData)) {
this._billboardCollection._scene.globe._surface.removeTileCustomData(
this._customData
);
this._customData = undefined;
}
if (defined(this._removeCallbackFunc)) {
this._removeCallbackFunc();
this._removeCallbackFunc = undefined;
}
this.image = undefined;
this._pickId = this._pickId && this._pickId.destroy();
this._billboardCollection = undefined;
};
/**
* A function that creates an image.
* @callback Billboard.CreateImageCallback
* @param {string} id The identifier of the image to load.
* @returns {HTMLImageElement|HTMLCanvasElement|Promise