/*global define*/
define([
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
'../Core/destroyObject',
'../Core/DeveloperError',
'../Core/Event',
'../Core/Math',
'../Core/Rectangle',
'../ThirdParty/when',
'./ImageryLayer'
], function(
defaultValue,
defined,
defineProperties,
destroyObject,
DeveloperError,
Event,
CesiumMath,
Rectangle,
when,
ImageryLayer) {
"use strict";
/**
* An ordered collection of imagery layers.
*
* @alias ImageryLayerCollection
* @constructor
*
* @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Imagery%20Adjustment.html|Cesium Sandcastle Imagery Adjustment Demo}
* @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Imagery%20Layers%20Manipulation.html|Cesium Sandcastle Imagery Manipulation Demo}
*/
var ImageryLayerCollection = function ImageryLayerCollection() {
this._layers = [];
/**
* An event that is raised when a layer is added to the collection. Event handlers are passed the layer that
* was added and the index at which it was added.
* @type {Event}
* @default Event()
*/
this.layerAdded = new Event();
/**
* An event that is raised when a layer is removed from the collection. Event handlers are passed the layer that
* was removed and the index from which it was removed.
* @type {Event}
* @default Event()
*/
this.layerRemoved = new Event();
/**
* An event that is raised when a layer changes position in the collection. Event handlers are passed the layer that
* was moved, its new index after the move, and its old index prior to the move.
* @type {Event}
* @default Event()
*/
this.layerMoved = new Event();
/**
* An event that is raised when a layer is shown or hidden by setting the
* {@link ImageryLayer#show} property. Event handlers are passed a reference to this layer,
* the index of the layer in the collection, and a flag that is true if the layer is now
* shown or false if it is now hidden.
*
* @type {Event}
* @default Event()
*/
this.layerShownOrHidden = new Event();
};
defineProperties(ImageryLayerCollection.prototype, {
/**
* Gets the number of layers in this collection.
* @memberof ImageryLayerCollection.prototype
* @type {Number}
*/
length : {
get : function() {
return this._layers.length;
}
}
});
/**
* Adds a layer to the collection.
*
* @param {ImageryLayer} layer the layer to add.
* @param {Number} [index] the index to add the layer at. If omitted, the layer will
* added on top of all existing layers.
*
* @exception {DeveloperError} index, if supplied, must be greater than or equal to zero and less than or equal to the number of the layers.
*/
ImageryLayerCollection.prototype.add = function(layer, index) {
var hasIndex = defined(index);
//>>includeStart('debug', pragmas.debug);
if (!defined(layer)) {
throw new DeveloperError('layer is required.');
}
if (hasIndex) {
if (index < 0) {
throw new DeveloperError('index must be greater than or equal to zero.');
} else if (index > this._layers.length) {
throw new DeveloperError('index must be less than or equal to the number of layers.');
}
}
//>>includeEnd('debug');
if (!hasIndex) {
index = this._layers.length;
this._layers.push(layer);
} else {
this._layers.splice(index, 0, layer);
}
this._update();
this.layerAdded.raiseEvent(layer, index);
};
/**
* Creates a new layer using the given ImageryProvider and adds it to the collection.
*
* @param {ImageryProvider} imageryProvider the imagery provider to create a new layer for.
* @param {Number} [index] the index to add the layer at. If omitted, the layer will
* added on top of all existing layers.
* @returns {ImageryLayer} The newly created layer.
*/
ImageryLayerCollection.prototype.addImageryProvider = function(imageryProvider, index) {
//>>includeStart('debug', pragmas.debug);
if (!defined(imageryProvider)) {
throw new DeveloperError('imageryProvider is required.');
}
//>>includeEnd('debug');
var layer = new ImageryLayer(imageryProvider);
this.add(layer, index);
return layer;
};
/**
* Removes a layer from this collection, if present.
*
* @param {ImageryLayer} layer The layer to remove.
* @param {Boolean} [destroy=true] whether to destroy the layers in addition to removing them.
* @returns {Boolean} true if the layer was in the collection and was removed,
* false if the layer was not in the collection.
*/
ImageryLayerCollection.prototype.remove = function(layer, destroy) {
destroy = defaultValue(destroy, true);
var index = this._layers.indexOf(layer);
if (index !== -1) {
this._layers.splice(index, 1);
this._update();
this.layerRemoved.raiseEvent(layer, index);
if (destroy) {
layer.destroy();
}
return true;
}
return false;
};
/**
* Removes all layers from this collection.
*
* @param {Boolean} [destroy=true] whether to destroy the layers in addition to removing them.
*/
ImageryLayerCollection.prototype.removeAll = function(destroy) {
destroy = defaultValue(destroy, true);
var layers = this._layers;
for (var i = 0, len = layers.length; i < len; i++) {
var layer = layers[i];
this.layerRemoved.raiseEvent(layer, i);
if (destroy) {
layer.destroy();
}
}
this._layers = [];
};
/**
* Checks to see if the collection contains a given layer.
*
* @param {ImageryLayer} layer the layer to check for.
*
* @returns {Boolean} true if the collection contains the layer, false otherwise.
*/
ImageryLayerCollection.prototype.contains = function(layer) {
return this.indexOf(layer) !== -1;
};
/**
* Determines the index of a given layer in the collection.
*
* @param {ImageryLayer} layer The layer to find the index of.
*
* @returns {Number} The index of the layer in the collection, or -1 if the layer does not exist in the collection.
*/
ImageryLayerCollection.prototype.indexOf = function(layer) {
return this._layers.indexOf(layer);
};
/**
* Gets a layer by index from the collection.
*
* @param {Number} index the index to retrieve.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
ImageryLayerCollection.prototype.get = function(index) {
//>>includeStart('debug', pragmas.debug);
if (!defined(index)) {
throw new DeveloperError('index is required.', 'index');
}
//>>includeEnd('debug');
return this._layers[index];
};
function getLayerIndex(layers, layer) {
//>>includeStart('debug', pragmas.debug);
if (!defined(layer)) {
throw new DeveloperError('layer is required.');
}
//>>includeEnd('debug');
var index = layers.indexOf(layer);
//>>includeStart('debug', pragmas.debug);
if (index === -1) {
throw new DeveloperError('layer is not in this collection.');
}
//>>includeEnd('debug');
return index;
}
function swapLayers(collection, i, j) {
var arr = collection._layers;
i = CesiumMath.clamp(i, 0, arr.length - 1);
j = CesiumMath.clamp(j, 0, arr.length - 1);
if (i === j) {
return;
}
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
collection._update();
collection.layerMoved.raiseEvent(temp, j, i);
}
/**
* Raises a layer up one position in the collection.
*
* @param {ImageryLayer} layer the layer to move.
*
* @exception {DeveloperError} layer is not in this collection.
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
ImageryLayerCollection.prototype.raise = function(layer) {
var index = getLayerIndex(this._layers, layer);
swapLayers(this, index, index + 1);
};
/**
* Lowers a layer down one position in the collection.
*
* @param {ImageryLayer} layer the layer to move.
*
* @exception {DeveloperError} layer is not in this collection.
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
ImageryLayerCollection.prototype.lower = function(layer) {
var index = getLayerIndex(this._layers, layer);
swapLayers(this, index, index - 1);
};
/**
* Raises a layer to the top of the collection.
*
* @param {ImageryLayer} layer the layer to move.
*
* @exception {DeveloperError} layer is not in this collection.
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
ImageryLayerCollection.prototype.raiseToTop = function(layer) {
var index = getLayerIndex(this._layers, layer);
if (index === this._layers.length - 1) {
return;
}
this._layers.splice(index, 1);
this._layers.push(layer);
this._update();
this.layerMoved.raiseEvent(layer, this._layers.length - 1, index);
};
/**
* Lowers a layer to the bottom of the collection.
*
* @param {ImageryLayer} layer the layer to move.
*
* @exception {DeveloperError} layer is not in this collection.
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
ImageryLayerCollection.prototype.lowerToBottom = function(layer) {
var index = getLayerIndex(this._layers, layer);
if (index === 0) {
return;
}
this._layers.splice(index, 1);
this._layers.splice(0, 0, layer);
this._update();
this.layerMoved.raiseEvent(layer, 0, index);
};
/**
* Asynchronously determines the imagery layer features that are intersected by a pick ray. The intersected imagery
* layer features are found by invoking {@link ImageryProvider#pickFeatures} for each imagery layer tile intersected
* by the pick ray. To compute a pick ray from a location on the screen, use {@link Camera.getPickRay}.
*
* @param {Ray} ray The ray to test for intersection.
* @param {Scene} scene The scene.
* @return {Promise|ImageryLayerFeatureInfo[]} A promise that resolves to an array of features intersected by the pick ray.
* If it can be quickly determined that no features are intersected (for example,
* because no active imagery providers support {@link ImageryProvider#pickFeatures}
* or because the pick ray does not intersect the surface), this function will
* return undefined.
*
* @example
* var pickRay = viewer.camera.getPickRay(windowPosition);
* var featuresPromise = viewer.imageryLayers.pickImageryLayerFeatures(pickRay, viewer.scene);
* if (!Cesium.defined(featuresPromise)) {
* console.log('No features picked.');
* } else {
* Cesium.when(featuresPromise, function(features) {
* // This function is called asynchronously when the list if picked features is available.
* console.log('Number of features: ' + features.length);
* if (features.length > 0) {
* console.log('First feature name: ' + features[0].name);
* }
* });
* });
* }
*/
ImageryLayerCollection.prototype.pickImageryLayerFeatures = function(ray, scene) {
// Find the picked location on the globe.
var pickedPosition = scene.globe.pick(ray, scene);
if (!defined(pickedPosition)) {
return undefined;
}
var pickedLocation = scene.globe.ellipsoid.cartesianToCartographic(pickedPosition);
// Find the terrain tile containing the picked location.
var tilesToRender = scene.globe._surface._tilesToRender;
var length = tilesToRender.length;
var pickedTile;
for (var textureIndex = 0; !defined(pickedTile) && textureIndex < tilesToRender.length; ++textureIndex) {
var tile = tilesToRender[textureIndex];
if (Rectangle.contains(tile.rectangle, pickedLocation)) {
pickedTile = tile;
}
}
if (!defined(pickedTile)) {
return undefined;
}
// Pick against all attached imagery tiles containing the pickedLocation.
var tileExtent = pickedTile.rectangle;
var imageryTiles = pickedTile.data.imagery;
var promises = [];
for (var i = imageryTiles.length - 1; i >= 0; --i) {
var terrainImagery = imageryTiles[i];
var imagery = terrainImagery.readyImagery;
if (!defined(imagery)) {
continue;
}
var provider = imagery.imageryLayer.imageryProvider;
if (!defined(provider.pickFeatures)) {
continue;
}
var promise = provider.pickFeatures(imagery.x, imagery.y, imagery.level, pickedLocation.longitude, pickedLocation.latitude);
if (!defined(promise)) {
continue;
}
promises.push(promise);
}
if (promises.length === 0) {
return undefined;
}
return when.all(promises, function(results) {
var features = [];
for (var resultIndex = 0; resultIndex < results.length; ++resultIndex) {
var result = results[resultIndex];
if (defined(result) && result.length > 0) {
for (var featureIndex = 0; featureIndex < result.length; ++featureIndex) {
var feature = result[featureIndex];
// For features without a position, use the picked location.
if (!defined(feature.position)) {
feature.position = pickedLocation;
}
features.push(feature);
}
}
}
return features;
});
};
/**
* Returns true if this object was destroyed; otherwise, false.
*
* If this object was destroyed, it should not be used; calling any function other than
* isDestroyed
will result in a {@link DeveloperError} exception.
*
* @returns {Boolean} true if this object was destroyed; otherwise, false.
*
* @see ImageryLayerCollection#destroy
*/
ImageryLayerCollection.prototype.isDestroyed = function() {
return false;
};
/**
* Destroys the WebGL resources held by all layers in this collection. Explicitly destroying this
* object allows for deterministic release of WebGL resources, instead of relying on the garbage
* collector.
*
* Once this object is destroyed, it should not be used; calling any function other than
* isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined
) to the object as done in the example.
*
* @returns {undefined}
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see ImageryLayerCollection#isDestroyed
*
* @example
* layerCollection = layerCollection && layerCollection.destroy();
*/
ImageryLayerCollection.prototype.destroy = function() {
this.removeAll(true);
return destroyObject(this);
};
ImageryLayerCollection.prototype._update = function() {
var isBaseLayer = true;
var layers = this._layers;
var layersShownOrHidden;
var layer;
for (var i = 0, len = layers.length; i < len; ++i) {
layer = layers[i];
layer._layerIndex = i;
if (layer.show) {
layer._isBaseLayer = isBaseLayer;
isBaseLayer = false;
} else {
layer._isBaseLayer = false;
}
if (layer.show !== layer._show) {
if (defined(layer._show)) {
if (!defined(layersShownOrHidden)) {
layersShownOrHidden = [];
}
layersShownOrHidden.push(layer);
}
layer._show = layer.show;
}
}
if (defined(layersShownOrHidden)) {
for (i = 0, len = layersShownOrHidden.length; i < len; ++i) {
layer = layersShownOrHidden[i];
this.layerShownOrHidden.raiseEvent(layer, layer._layerIndex, layer.show);
}
}
};
return ImageryLayerCollection;
});