ImageryLayerCollection.js 19 KB

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