sampleTerrain.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import Check from "./Check.js";
  2. import defined from "./defined.js";
  3. /**
  4. * Initiates a terrain height query for an array of {@link Cartographic} positions by
  5. * requesting tiles from a terrain provider, sampling, and interpolating. The interpolation
  6. * matches the triangles used to render the terrain at the specified level. The query
  7. * happens asynchronously, so this function returns a promise that is resolved when
  8. * the query completes. Each point height is modified in place. If a height can not be
  9. * determined because no terrain data is available for the specified level at that location,
  10. * or another error occurs, the height is set to undefined. As is typical of the
  11. * {@link Cartographic} type, the supplied height is a height above the reference ellipsoid
  12. * (such as {@link Ellipsoid.WGS84}) rather than an altitude above mean sea level. In other
  13. * words, it will not necessarily be 0.0 if sampled in the ocean. This function needs the
  14. * terrain level of detail as input, if you need to get the altitude of the terrain as precisely
  15. * as possible (i.e. with maximum level of detail) use {@link sampleTerrainMostDetailed}.
  16. *
  17. * @function sampleTerrain
  18. *
  19. * @param {TerrainProvider} terrainProvider The terrain provider from which to query heights.
  20. * @param {number} level The terrain level-of-detail from which to query terrain heights.
  21. * @param {Cartographic[]} positions The positions to update with terrain heights.
  22. * @returns {Promise<Cartographic[]>} A promise that resolves to the provided list of positions when terrain the query has completed.
  23. *
  24. * @see sampleTerrainMostDetailed
  25. *
  26. * @example
  27. * // Query the terrain height of two Cartographic positions
  28. * const terrainProvider = await Cesium.createWorldTerrainAsync();
  29. * const positions = [
  30. * Cesium.Cartographic.fromDegrees(86.925145, 27.988257),
  31. * Cesium.Cartographic.fromDegrees(87.0, 28.0)
  32. * ];
  33. * const updatedPositions = await Cesium.sampleTerrain(terrainProvider, 11, positions);
  34. * // positions[0].height and positions[1].height have been updated.
  35. * // updatedPositions is just a reference to positions.
  36. */
  37. async function sampleTerrain(terrainProvider, level, positions) {
  38. //>>includeStart('debug', pragmas.debug);
  39. Check.typeOf.object("terrainProvider", terrainProvider);
  40. Check.typeOf.number("level", level);
  41. Check.defined("positions", positions);
  42. //>>includeEnd('debug');
  43. // readyPromise has been deprecated; This is here for backwards compatibility
  44. await terrainProvider._readyPromise;
  45. return doSampling(terrainProvider, level, positions);
  46. }
  47. /**
  48. * @param {object[]} tileRequests The mutated list of requests, the first one will be attempted
  49. * @param {Array<Promise<void>>} results The list to put the result promises into
  50. * @returns {boolean} true if the request was made, and we are okay to attempt the next item immediately,
  51. * or false if we were throttled and should wait awhile before retrying.
  52. *
  53. * @private
  54. */
  55. function attemptConsumeNextQueueItem(tileRequests, results) {
  56. const tileRequest = tileRequests[0];
  57. const requestPromise = tileRequest.terrainProvider.requestTileGeometry(
  58. tileRequest.x,
  59. tileRequest.y,
  60. tileRequest.level
  61. );
  62. if (!requestPromise) {
  63. // getting back undefined instead of a promise indicates we should retry a bit later
  64. return false;
  65. }
  66. const promise = requestPromise
  67. .then(createInterpolateFunction(tileRequest))
  68. .catch(createMarkFailedFunction(tileRequest));
  69. // remove the request we've just done from the queue
  70. // and add its promise result to the result list
  71. tileRequests.shift();
  72. results.push(promise);
  73. // indicate we should synchronously attempt the next request as well
  74. return true;
  75. }
  76. /**
  77. * Wrap window.setTimeout in a Promise
  78. * @param {number} ms
  79. * @private
  80. */
  81. function delay(ms) {
  82. return new Promise(function (res) {
  83. setTimeout(res, ms);
  84. });
  85. }
  86. /**
  87. * Recursively consumes all the tileRequests until the list has been emptied
  88. * and a Promise of each result has been put into the results list
  89. * @param {object[]} tileRequests The list of requests desired to be made
  90. * @param {Array<Promise<void>>} results The list to put all the result promises into
  91. * @returns {Promise<void>} A promise which resolves once all requests have been started
  92. *
  93. * @private
  94. */
  95. function drainTileRequestQueue(tileRequests, results) {
  96. // nothing left to do
  97. if (!tileRequests.length) {
  98. return Promise.resolve();
  99. }
  100. // consume an item from the queue, which will
  101. // mutate the request and result lists, and return true if we should
  102. // immediately attempt to consume the next item as well
  103. const success = attemptConsumeNextQueueItem(tileRequests, results);
  104. if (success) {
  105. return drainTileRequestQueue(tileRequests, results);
  106. }
  107. // wait a small fixed amount of time first, before retrying the same request again
  108. return delay(100).then(() => {
  109. return drainTileRequestQueue(tileRequests, results);
  110. });
  111. }
  112. function doSampling(terrainProvider, level, positions) {
  113. const tilingScheme = terrainProvider.tilingScheme;
  114. let i;
  115. // Sort points into a set of tiles
  116. const tileRequests = []; // Result will be an Array as it's easier to work with
  117. const tileRequestSet = {}; // A unique set
  118. for (i = 0; i < positions.length; ++i) {
  119. const xy = tilingScheme.positionToTileXY(positions[i], level);
  120. if (!defined(xy)) {
  121. continue;
  122. }
  123. const key = xy.toString();
  124. if (!tileRequestSet.hasOwnProperty(key)) {
  125. // When tile is requested for the first time
  126. const value = {
  127. x: xy.x,
  128. y: xy.y,
  129. level: level,
  130. tilingScheme: tilingScheme,
  131. terrainProvider: terrainProvider,
  132. positions: [],
  133. };
  134. tileRequestSet[key] = value;
  135. tileRequests.push(value);
  136. }
  137. // Now append to array of points for the tile
  138. tileRequestSet[key].positions.push(positions[i]);
  139. }
  140. // create our list of result promises to be filled
  141. const tilePromises = [];
  142. return drainTileRequestQueue(tileRequests, tilePromises).then(function () {
  143. // now all the required requests have been started
  144. // we just wait for them all to finish
  145. return Promise.all(tilePromises).then(function () {
  146. return positions;
  147. });
  148. });
  149. }
  150. /**
  151. * Calls {@link TerrainData#interpolateHeight} on a given {@link TerrainData} for a given {@link Cartographic} and
  152. * will assign the height property if the return value is not undefined.
  153. *
  154. * If the return value is false; it's suggesting that you should call {@link TerrainData#createMesh} first.
  155. * @param {Cartographic} position The position to interpolate for and assign the height value to
  156. * @param {TerrainData} terrainData
  157. * @param {Rectangle} rectangle
  158. * @returns {boolean} If the height was actually interpolated and assigned
  159. * @private
  160. */
  161. function interpolateAndAssignHeight(position, terrainData, rectangle) {
  162. const height = terrainData.interpolateHeight(
  163. rectangle,
  164. position.longitude,
  165. position.latitude
  166. );
  167. if (height === undefined) {
  168. // if height comes back as undefined, it may implicitly mean the terrain data
  169. // requires us to call TerrainData.createMesh() first (ArcGIS requires this in particular)
  170. // so we'll return false and do that next!
  171. return false;
  172. }
  173. position.height = height;
  174. return true;
  175. }
  176. function createInterpolateFunction(tileRequest) {
  177. const tilePositions = tileRequest.positions;
  178. const rectangle = tileRequest.tilingScheme.tileXYToRectangle(
  179. tileRequest.x,
  180. tileRequest.y,
  181. tileRequest.level
  182. );
  183. return function (terrainData) {
  184. let isMeshRequired = false;
  185. for (let i = 0; i < tilePositions.length; ++i) {
  186. const position = tilePositions[i];
  187. const isHeightAssigned = interpolateAndAssignHeight(
  188. position,
  189. terrainData,
  190. rectangle
  191. );
  192. // we've found a position which returned undefined - hinting to us
  193. // that we probably need to create a mesh for this terrain data.
  194. // so break out of this loop and create the mesh - then we'll interpolate all the heights again
  195. if (!isHeightAssigned) {
  196. isMeshRequired = true;
  197. break;
  198. }
  199. }
  200. if (!isMeshRequired) {
  201. // all position heights were interpolated - we don't need the mesh
  202. return Promise.resolve();
  203. }
  204. // create the mesh - and interpolate all the positions again
  205. // note: terrain exaggeration is not passed in - we are only interested in the raw data
  206. return terrainData
  207. .createMesh({
  208. tilingScheme: tileRequest.tilingScheme,
  209. x: tileRequest.x,
  210. y: tileRequest.y,
  211. level: tileRequest.level,
  212. // don't throttle this mesh creation because we've asked to sample these points;
  213. // so sample them! We don't care how many tiles that is!
  214. throttle: false,
  215. })
  216. .then(function () {
  217. // mesh has been created - so go through every position (maybe again)
  218. // and re-interpolate the heights - presumably using the mesh this time
  219. for (let i = 0; i < tilePositions.length; ++i) {
  220. const position = tilePositions[i];
  221. // if it doesn't work this time - that's fine, we tried.
  222. interpolateAndAssignHeight(position, terrainData, rectangle);
  223. }
  224. });
  225. };
  226. }
  227. function createMarkFailedFunction(tileRequest) {
  228. const tilePositions = tileRequest.positions;
  229. return function () {
  230. for (let i = 0; i < tilePositions.length; ++i) {
  231. const position = tilePositions[i];
  232. position.height = undefined;
  233. }
  234. };
  235. }
  236. export default sampleTerrain;