ImageryLayerCollection.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import defaultValue from "../Core/defaultValue.js";
  2. import defined from "../Core/defined.js";
  3. import destroyObject from "../Core/destroyObject.js";
  4. import DeveloperError from "../Core/DeveloperError.js";
  5. import Event from "../Core/Event.js";
  6. import CesiumMath from "../Core/Math.js";
  7. import Rectangle from "../Core/Rectangle.js";
  8. import ImageryLayer from "./ImageryLayer.js";
  9. /**
  10. * An ordered collection of imagery layers.
  11. *
  12. * @alias ImageryLayerCollection
  13. * @constructor
  14. *
  15. * @demo {@link https://sandcastle.cesium.com/index.html?src=Imagery%20Adjustment.html|Cesium Sandcastle Imagery Adjustment Demo}
  16. * @demo {@link https://sandcastle.cesium.com/index.html?src=Imagery%20Layers%20Manipulation.html|Cesium Sandcastle Imagery Manipulation Demo}
  17. */
  18. function ImageryLayerCollection() {
  19. this._layers = [];
  20. /**
  21. * An event that is raised when a layer is added to the collection. Event handlers are passed the layer that
  22. * was added and the index at which it was added.
  23. * @type {Event}
  24. * @default Event()
  25. */
  26. this.layerAdded = new Event();
  27. /**
  28. * An event that is raised when a layer is removed from the collection. Event handlers are passed the layer that
  29. * was removed and the index from which it was removed.
  30. * @type {Event}
  31. * @default Event()
  32. */
  33. this.layerRemoved = new Event();
  34. /**
  35. * An event that is raised when a layer changes position in the collection. Event handlers are passed the layer that
  36. * was moved, its new index after the move, and its old index prior to the move.
  37. * @type {Event}
  38. * @default Event()
  39. */
  40. this.layerMoved = new Event();
  41. /**
  42. * An event that is raised when a layer is shown or hidden by setting the
  43. * {@link ImageryLayer#show} property. Event handlers are passed a reference to this layer,
  44. * the index of the layer in the collection, and a flag that is true if the layer is now
  45. * shown or false if it is now hidden.
  46. *
  47. * @type {Event}
  48. * @default Event()
  49. */
  50. this.layerShownOrHidden = new Event();
  51. }
  52. Object.defineProperties(ImageryLayerCollection.prototype, {
  53. /**
  54. * Gets the number of layers in this collection.
  55. * @memberof ImageryLayerCollection.prototype
  56. * @type {Number}
  57. */
  58. length: {
  59. get: function () {
  60. return this._layers.length;
  61. },
  62. },
  63. });
  64. /**
  65. * Adds a layer to the collection.
  66. *
  67. * @param {ImageryLayer} layer the layer to add.
  68. * @param {Number} [index] the index to add the layer at. If omitted, the layer will
  69. * be added on top of all existing layers.
  70. *
  71. * @exception {DeveloperError} index, if supplied, must be greater than or equal to zero and less than or equal to the number of the layers.
  72. */
  73. ImageryLayerCollection.prototype.add = function (layer, index) {
  74. const hasIndex = defined(index);
  75. //>>includeStart('debug', pragmas.debug);
  76. if (!defined(layer)) {
  77. throw new DeveloperError("layer is required.");
  78. }
  79. if (hasIndex) {
  80. if (index < 0) {
  81. throw new DeveloperError("index must be greater than or equal to zero.");
  82. } else if (index > this._layers.length) {
  83. throw new DeveloperError(
  84. "index must be less than or equal to the number of layers."
  85. );
  86. }
  87. }
  88. //>>includeEnd('debug');
  89. if (!hasIndex) {
  90. index = this._layers.length;
  91. this._layers.push(layer);
  92. } else {
  93. this._layers.splice(index, 0, layer);
  94. }
  95. this._update();
  96. this.layerAdded.raiseEvent(layer, index);
  97. };
  98. /**
  99. * Creates a new layer using the given ImageryProvider and adds it to the collection.
  100. *
  101. * @param {ImageryProvider} imageryProvider the imagery provider to create a new layer for.
  102. * @param {Number} [index] the index to add the layer at. If omitted, the layer will
  103. * added on top of all existing layers.
  104. * @returns {ImageryLayer} The newly created layer.
  105. */
  106. ImageryLayerCollection.prototype.addImageryProvider = function (
  107. imageryProvider,
  108. index
  109. ) {
  110. //>>includeStart('debug', pragmas.debug);
  111. if (!defined(imageryProvider)) {
  112. throw new DeveloperError("imageryProvider is required.");
  113. }
  114. //>>includeEnd('debug');
  115. const layer = new ImageryLayer(imageryProvider);
  116. this.add(layer, index);
  117. return layer;
  118. };
  119. /**
  120. * Removes a layer from this collection, if present.
  121. *
  122. * @param {ImageryLayer} layer The layer to remove.
  123. * @param {Boolean} [destroy=true] whether to destroy the layers in addition to removing them.
  124. * @returns {Boolean} true if the layer was in the collection and was removed,
  125. * false if the layer was not in the collection.
  126. */
  127. ImageryLayerCollection.prototype.remove = function (layer, destroy) {
  128. destroy = defaultValue(destroy, true);
  129. const index = this._layers.indexOf(layer);
  130. if (index !== -1) {
  131. this._layers.splice(index, 1);
  132. this._update();
  133. this.layerRemoved.raiseEvent(layer, index);
  134. if (destroy) {
  135. layer.destroy();
  136. }
  137. return true;
  138. }
  139. return false;
  140. };
  141. /**
  142. * Removes all layers from this collection.
  143. *
  144. * @param {Boolean} [destroy=true] whether to destroy the layers in addition to removing them.
  145. */
  146. ImageryLayerCollection.prototype.removeAll = function (destroy) {
  147. destroy = defaultValue(destroy, true);
  148. const layers = this._layers;
  149. for (let i = 0, len = layers.length; i < len; i++) {
  150. const layer = layers[i];
  151. this.layerRemoved.raiseEvent(layer, i);
  152. if (destroy) {
  153. layer.destroy();
  154. }
  155. }
  156. this._layers = [];
  157. };
  158. /**
  159. * Checks to see if the collection contains a given layer.
  160. *
  161. * @param {ImageryLayer} layer the layer to check for.
  162. *
  163. * @returns {Boolean} true if the collection contains the layer, false otherwise.
  164. */
  165. ImageryLayerCollection.prototype.contains = function (layer) {
  166. return this.indexOf(layer) !== -1;
  167. };
  168. /**
  169. * Determines the index of a given layer in the collection.
  170. *
  171. * @param {ImageryLayer} layer The layer to find the index of.
  172. *
  173. * @returns {Number} The index of the layer in the collection, or -1 if the layer does not exist in the collection.
  174. */
  175. ImageryLayerCollection.prototype.indexOf = function (layer) {
  176. return this._layers.indexOf(layer);
  177. };
  178. /**
  179. * Gets a layer by index from the collection.
  180. *
  181. * @param {Number} index the index to retrieve.
  182. *
  183. * @returns {ImageryLayer} The imagery layer at the given index.
  184. */
  185. ImageryLayerCollection.prototype.get = function (index) {
  186. //>>includeStart('debug', pragmas.debug);
  187. if (!defined(index)) {
  188. throw new DeveloperError("index is required.", "index");
  189. }
  190. //>>includeEnd('debug');
  191. return this._layers[index];
  192. };
  193. function getLayerIndex(layers, layer) {
  194. //>>includeStart('debug', pragmas.debug);
  195. if (!defined(layer)) {
  196. throw new DeveloperError("layer is required.");
  197. }
  198. //>>includeEnd('debug');
  199. const index = layers.indexOf(layer);
  200. //>>includeStart('debug', pragmas.debug);
  201. if (index === -1) {
  202. throw new DeveloperError("layer is not in this collection.");
  203. }
  204. //>>includeEnd('debug');
  205. return index;
  206. }
  207. function swapLayers(collection, i, j) {
  208. const arr = collection._layers;
  209. i = CesiumMath.clamp(i, 0, arr.length - 1);
  210. j = CesiumMath.clamp(j, 0, arr.length - 1);
  211. if (i === j) {
  212. return;
  213. }
  214. const temp = arr[i];
  215. arr[i] = arr[j];
  216. arr[j] = temp;
  217. collection._update();
  218. collection.layerMoved.raiseEvent(temp, j, i);
  219. }
  220. /**
  221. * Raises a layer up one position in the collection.
  222. *
  223. * @param {ImageryLayer} layer the layer to move.
  224. *
  225. * @exception {DeveloperError} layer is not in this collection.
  226. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  227. */
  228. ImageryLayerCollection.prototype.raise = function (layer) {
  229. const index = getLayerIndex(this._layers, layer);
  230. swapLayers(this, index, index + 1);
  231. };
  232. /**
  233. * Lowers a layer down one position in the collection.
  234. *
  235. * @param {ImageryLayer} layer the layer to move.
  236. *
  237. * @exception {DeveloperError} layer is not in this collection.
  238. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  239. */
  240. ImageryLayerCollection.prototype.lower = function (layer) {
  241. const index = getLayerIndex(this._layers, layer);
  242. swapLayers(this, index, index - 1);
  243. };
  244. /**
  245. * Raises a layer to the top of the collection.
  246. *
  247. * @param {ImageryLayer} layer the layer to move.
  248. *
  249. * @exception {DeveloperError} layer is not in this collection.
  250. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  251. */
  252. ImageryLayerCollection.prototype.raiseToTop = function (layer) {
  253. const index = getLayerIndex(this._layers, layer);
  254. if (index === this._layers.length - 1) {
  255. return;
  256. }
  257. this._layers.splice(index, 1);
  258. this._layers.push(layer);
  259. this._update();
  260. this.layerMoved.raiseEvent(layer, this._layers.length - 1, index);
  261. };
  262. /**
  263. * Lowers a layer to the bottom of the collection.
  264. *
  265. * @param {ImageryLayer} layer the layer to move.
  266. *
  267. * @exception {DeveloperError} layer is not in this collection.
  268. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  269. */
  270. ImageryLayerCollection.prototype.lowerToBottom = function (layer) {
  271. const index = getLayerIndex(this._layers, layer);
  272. if (index === 0) {
  273. return;
  274. }
  275. this._layers.splice(index, 1);
  276. this._layers.splice(0, 0, layer);
  277. this._update();
  278. this.layerMoved.raiseEvent(layer, 0, index);
  279. };
  280. const applicableRectangleScratch = new Rectangle();
  281. function pickImageryHelper(scene, pickedLocation, pickFeatures, callback) {
  282. // Find the terrain tile containing the picked location.
  283. const tilesToRender = scene.globe._surface._tilesToRender;
  284. let pickedTile;
  285. for (
  286. let textureIndex = 0;
  287. !defined(pickedTile) && textureIndex < tilesToRender.length;
  288. ++textureIndex
  289. ) {
  290. const tile = tilesToRender[textureIndex];
  291. if (Rectangle.contains(tile.rectangle, pickedLocation)) {
  292. pickedTile = tile;
  293. }
  294. }
  295. if (!defined(pickedTile)) {
  296. return;
  297. }
  298. // Pick against all attached imagery tiles containing the pickedLocation.
  299. const imageryTiles = pickedTile.data.imagery;
  300. for (let i = imageryTiles.length - 1; i >= 0; --i) {
  301. const terrainImagery = imageryTiles[i];
  302. const imagery = terrainImagery.readyImagery;
  303. if (!defined(imagery)) {
  304. continue;
  305. }
  306. const provider = imagery.imageryLayer.imageryProvider;
  307. if (pickFeatures && !defined(provider.pickFeatures)) {
  308. continue;
  309. }
  310. if (!Rectangle.contains(imagery.rectangle, pickedLocation)) {
  311. continue;
  312. }
  313. // If this imagery came from a parent, it may not be applicable to its entire rectangle.
  314. // Check the textureCoordinateRectangle.
  315. const applicableRectangle = applicableRectangleScratch;
  316. const epsilon = 1 / 1024; // 1/4 of a pixel in a typical 256x256 tile.
  317. applicableRectangle.west = CesiumMath.lerp(
  318. pickedTile.rectangle.west,
  319. pickedTile.rectangle.east,
  320. terrainImagery.textureCoordinateRectangle.x - epsilon
  321. );
  322. applicableRectangle.east = CesiumMath.lerp(
  323. pickedTile.rectangle.west,
  324. pickedTile.rectangle.east,
  325. terrainImagery.textureCoordinateRectangle.z + epsilon
  326. );
  327. applicableRectangle.south = CesiumMath.lerp(
  328. pickedTile.rectangle.south,
  329. pickedTile.rectangle.north,
  330. terrainImagery.textureCoordinateRectangle.y - epsilon
  331. );
  332. applicableRectangle.north = CesiumMath.lerp(
  333. pickedTile.rectangle.south,
  334. pickedTile.rectangle.north,
  335. terrainImagery.textureCoordinateRectangle.w + epsilon
  336. );
  337. if (!Rectangle.contains(applicableRectangle, pickedLocation)) {
  338. continue;
  339. }
  340. callback(imagery);
  341. }
  342. }
  343. /**
  344. * Determines the imagery layers that are intersected by a pick ray. To compute a pick ray from a
  345. * location on the screen, use {@link Camera.getPickRay}.
  346. *
  347. * @param {Ray} ray The ray to test for intersection.
  348. * @param {Scene} scene The scene.
  349. * @return {ImageryLayer[]|undefined} An array that includes all of
  350. * the layers that are intersected by a given pick ray. Undefined if
  351. * no layers are selected.
  352. *
  353. */
  354. ImageryLayerCollection.prototype.pickImageryLayers = function (ray, scene) {
  355. // Find the picked location on the globe.
  356. const pickedPosition = scene.globe.pick(ray, scene);
  357. if (!defined(pickedPosition)) {
  358. return;
  359. }
  360. const pickedLocation = scene.globe.ellipsoid.cartesianToCartographic(
  361. pickedPosition
  362. );
  363. const imageryLayers = [];
  364. pickImageryHelper(scene, pickedLocation, false, function (imagery) {
  365. imageryLayers.push(imagery.imageryLayer);
  366. });
  367. if (imageryLayers.length === 0) {
  368. return undefined;
  369. }
  370. return imageryLayers;
  371. };
  372. /**
  373. * Asynchronously determines the imagery layer features that are intersected by a pick ray. The intersected imagery
  374. * layer features are found by invoking {@link ImageryProvider#pickFeatures} for each imagery layer tile intersected
  375. * by the pick ray. To compute a pick ray from a location on the screen, use {@link Camera.getPickRay}.
  376. *
  377. * @param {Ray} ray The ray to test for intersection.
  378. * @param {Scene} scene The scene.
  379. * @return {Promise.<ImageryLayerFeatureInfo[]>|undefined} A promise that resolves to an array of features intersected by the pick ray.
  380. * If it can be quickly determined that no features are intersected (for example,
  381. * because no active imagery providers support {@link ImageryProvider#pickFeatures}
  382. * or because the pick ray does not intersect the surface), this function will
  383. * return undefined.
  384. *
  385. * @example
  386. * const pickRay = viewer.camera.getPickRay(windowPosition);
  387. * const featuresPromise = viewer.imageryLayers.pickImageryLayerFeatures(pickRay, viewer.scene);
  388. * if (!Cesium.defined(featuresPromise)) {
  389. * console.log('No features picked.');
  390. * } else {
  391. * Promise.resolve(featuresPromise).then(function(features) {
  392. * // This function is called asynchronously when the list if picked features is available.
  393. * console.log('Number of features: ' + features.length);
  394. * if (features.length > 0) {
  395. * console.log('First feature name: ' + features[0].name);
  396. * }
  397. * });
  398. * }
  399. */
  400. ImageryLayerCollection.prototype.pickImageryLayerFeatures = function (
  401. ray,
  402. scene
  403. ) {
  404. // Find the picked location on the globe.
  405. const pickedPosition = scene.globe.pick(ray, scene);
  406. if (!defined(pickedPosition)) {
  407. return;
  408. }
  409. const pickedLocation = scene.globe.ellipsoid.cartesianToCartographic(
  410. pickedPosition
  411. );
  412. const promises = [];
  413. const imageryLayers = [];
  414. pickImageryHelper(scene, pickedLocation, true, function (imagery) {
  415. const provider = imagery.imageryLayer.imageryProvider;
  416. const promise = provider.pickFeatures(
  417. imagery.x,
  418. imagery.y,
  419. imagery.level,
  420. pickedLocation.longitude,
  421. pickedLocation.latitude
  422. );
  423. if (defined(promise)) {
  424. promises.push(promise);
  425. imageryLayers.push(imagery.imageryLayer);
  426. }
  427. });
  428. if (promises.length === 0) {
  429. return undefined;
  430. }
  431. return Promise.all(promises).then(function (results) {
  432. const features = [];
  433. for (let resultIndex = 0; resultIndex < results.length; ++resultIndex) {
  434. const result = results[resultIndex];
  435. const image = imageryLayers[resultIndex];
  436. if (defined(result) && result.length > 0) {
  437. for (
  438. let featureIndex = 0;
  439. featureIndex < result.length;
  440. ++featureIndex
  441. ) {
  442. const feature = result[featureIndex];
  443. feature.imageryLayer = image;
  444. // For features without a position, use the picked location.
  445. if (!defined(feature.position)) {
  446. feature.position = pickedLocation;
  447. }
  448. features.push(feature);
  449. }
  450. }
  451. }
  452. return features;
  453. });
  454. };
  455. /**
  456. * Updates frame state to execute any queued texture re-projections.
  457. *
  458. * @private
  459. *
  460. * @param {FrameState} frameState The frameState.
  461. */
  462. ImageryLayerCollection.prototype.queueReprojectionCommands = function (
  463. frameState
  464. ) {
  465. const layers = this._layers;
  466. for (let i = 0, len = layers.length; i < len; ++i) {
  467. layers[i].queueReprojectionCommands(frameState);
  468. }
  469. };
  470. /**
  471. * Cancels re-projection commands queued for the next frame.
  472. *
  473. * @private
  474. */
  475. ImageryLayerCollection.prototype.cancelReprojections = function () {
  476. const layers = this._layers;
  477. for (let i = 0, len = layers.length; i < len; ++i) {
  478. layers[i].cancelReprojections();
  479. }
  480. };
  481. /**
  482. * Returns true if this object was destroyed; otherwise, false.
  483. * <br /><br />
  484. * If this object was destroyed, it should not be used; calling any function other than
  485. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  486. *
  487. * @returns {Boolean} true if this object was destroyed; otherwise, false.
  488. *
  489. * @see ImageryLayerCollection#destroy
  490. */
  491. ImageryLayerCollection.prototype.isDestroyed = function () {
  492. return false;
  493. };
  494. /**
  495. * Destroys the WebGL resources held by all layers in this collection. Explicitly destroying this
  496. * object allows for deterministic release of WebGL resources, instead of relying on the garbage
  497. * collector.
  498. * <br /><br />
  499. * Once this object is destroyed, it should not be used; calling any function other than
  500. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  501. * assign the return value (<code>undefined</code>) to the object as done in the example.
  502. *
  503. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  504. *
  505. *
  506. * @example
  507. * layerCollection = layerCollection && layerCollection.destroy();
  508. *
  509. * @see ImageryLayerCollection#isDestroyed
  510. */
  511. ImageryLayerCollection.prototype.destroy = function () {
  512. this.removeAll(true);
  513. return destroyObject(this);
  514. };
  515. ImageryLayerCollection.prototype._update = function () {
  516. let isBaseLayer = true;
  517. const layers = this._layers;
  518. let layersShownOrHidden;
  519. let layer;
  520. let i, len;
  521. for (i = 0, len = layers.length; i < len; ++i) {
  522. layer = layers[i];
  523. layer._layerIndex = i;
  524. if (layer.show) {
  525. layer._isBaseLayer = isBaseLayer;
  526. isBaseLayer = false;
  527. } else {
  528. layer._isBaseLayer = false;
  529. }
  530. if (layer.show !== layer._show) {
  531. if (defined(layer._show)) {
  532. if (!defined(layersShownOrHidden)) {
  533. layersShownOrHidden = [];
  534. }
  535. layersShownOrHidden.push(layer);
  536. }
  537. layer._show = layer.show;
  538. }
  539. }
  540. if (defined(layersShownOrHidden)) {
  541. for (i = 0, len = layersShownOrHidden.length; i < len; ++i) {
  542. layer = layersShownOrHidden[i];
  543. this.layerShownOrHidden.raiseEvent(layer, layer._layerIndex, layer.show);
  544. }
  545. }
  546. };
  547. export default ImageryLayerCollection;