ArcGISTiledElevationTerrainProvider.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. import Cartesian2 from "./Cartesian2.js";
  2. import Credit from "./Credit.js";
  3. import defaultValue from "./defaultValue.js";
  4. import defined from "./defined.js";
  5. import DeveloperError from "./DeveloperError.js";
  6. import Ellipsoid from "./Ellipsoid.js";
  7. import Event from "./Event.js";
  8. import GeographicTilingScheme from "./GeographicTilingScheme.js";
  9. import HeightmapEncoding from "./HeightmapEncoding.js";
  10. import HeightmapTerrainData from "./HeightmapTerrainData.js";
  11. import Rectangle from "./Rectangle.js";
  12. import Request from "./Request.js";
  13. import RequestState from "./RequestState.js";
  14. import RequestType from "./RequestType.js";
  15. import Resource from "./Resource.js";
  16. import RuntimeError from "./RuntimeError.js";
  17. import TerrainProvider from "./TerrainProvider.js";
  18. import TileAvailability from "./TileAvailability.js";
  19. import TileProviderError from "./TileProviderError.js";
  20. import WebMercatorTilingScheme from "./WebMercatorTilingScheme.js";
  21. const ALL_CHILDREN = 15;
  22. /**
  23. * A {@link TerrainProvider} that produces terrain geometry by tessellating height maps
  24. * retrieved from Elevation Tiles of an an ArcGIS ImageService.
  25. *
  26. * @alias ArcGISTiledElevationTerrainProvider
  27. * @constructor
  28. *
  29. * @param {Object} options Object with the following properties:
  30. * @param {Resource|String|Promise<Resource>|Promise<String>} options.url The URL of the ArcGIS ImageServer service.
  31. * @param {String} [options.token] The authorization token to use to connect to the service.
  32. * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If the tilingScheme is specified,
  33. * this parameter is ignored and the tiling scheme's ellipsoid is used instead.
  34. * If neither parameter is specified, the WGS84 ellipsoid is used.
  35. *
  36. * @example
  37. * const terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({
  38. * url : 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer',
  39. * token : 'KED1aF_I4UzXOHy3BnhwyBHU4l5oY6rO6walkmHoYqGp4XyIWUd5YZUC1ZrLAzvV40pR6gBXQayh0eFA8m6vPg..'
  40. * });
  41. * viewer.terrainProvider = terrainProvider;
  42. *
  43. * @see TerrainProvider
  44. */
  45. function ArcGISTiledElevationTerrainProvider(options) {
  46. //>>includeStart('debug', pragmas.debug);
  47. if (!defined(options) || !defined(options.url)) {
  48. throw new DeveloperError("options.url is required.");
  49. }
  50. //>>includeEnd('debug');
  51. this._resource = undefined;
  52. this._credit = undefined;
  53. this._tilingScheme = undefined;
  54. this._levelZeroMaximumGeometricError = undefined;
  55. this._maxLevel = undefined;
  56. this._terrainDataStructure = undefined;
  57. this._ready = false;
  58. this._width = undefined;
  59. this._height = undefined;
  60. this._encoding = undefined;
  61. const token = options.token;
  62. this._hasAvailability = false;
  63. this._tilesAvailable = undefined;
  64. this._tilesAvailablityLoaded = undefined;
  65. this._availableCache = {};
  66. const that = this;
  67. const ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
  68. this._readyPromise = Promise.resolve(options.url)
  69. .then(function (url) {
  70. let resource = Resource.createIfNeeded(url);
  71. resource.appendForwardSlash();
  72. if (defined(token)) {
  73. resource = resource.getDerivedResource({
  74. queryParameters: {
  75. token: token,
  76. },
  77. });
  78. }
  79. that._resource = resource;
  80. const metadataResource = resource.getDerivedResource({
  81. queryParameters: {
  82. f: "pjson",
  83. },
  84. });
  85. return metadataResource.fetchJson();
  86. })
  87. .then(function (metadata) {
  88. const copyrightText = metadata.copyrightText;
  89. if (defined(copyrightText)) {
  90. that._credit = new Credit(copyrightText);
  91. }
  92. const spatialReference = metadata.spatialReference;
  93. const wkid = defaultValue(
  94. spatialReference.latestWkid,
  95. spatialReference.wkid
  96. );
  97. const extent = metadata.extent;
  98. const tilingSchemeOptions = {
  99. ellipsoid: ellipsoid,
  100. };
  101. if (wkid === 4326) {
  102. tilingSchemeOptions.rectangle = Rectangle.fromDegrees(
  103. extent.xmin,
  104. extent.ymin,
  105. extent.xmax,
  106. extent.ymax
  107. );
  108. that._tilingScheme = new GeographicTilingScheme(tilingSchemeOptions);
  109. } else if (wkid === 3857) {
  110. tilingSchemeOptions.rectangleSouthwestInMeters = new Cartesian2(
  111. extent.xmin,
  112. extent.ymin
  113. );
  114. tilingSchemeOptions.rectangleNortheastInMeters = new Cartesian2(
  115. extent.xmax,
  116. extent.ymax
  117. );
  118. that._tilingScheme = new WebMercatorTilingScheme(tilingSchemeOptions);
  119. } else {
  120. return Promise.reject(new RuntimeError("Invalid spatial reference"));
  121. }
  122. const tileInfo = metadata.tileInfo;
  123. if (!defined(tileInfo)) {
  124. return Promise.reject(new RuntimeError("tileInfo is required"));
  125. }
  126. that._width = tileInfo.rows + 1;
  127. that._height = tileInfo.cols + 1;
  128. that._encoding =
  129. tileInfo.format === "LERC"
  130. ? HeightmapEncoding.LERC
  131. : HeightmapEncoding.NONE;
  132. that._lodCount = tileInfo.lods.length - 1;
  133. const hasAvailability = (that._hasAvailability =
  134. metadata.capabilities.indexOf("Tilemap") !== -1);
  135. if (hasAvailability) {
  136. that._tilesAvailable = new TileAvailability(
  137. that._tilingScheme,
  138. that._lodCount
  139. );
  140. that._tilesAvailable.addAvailableTileRange(
  141. 0,
  142. 0,
  143. 0,
  144. that._tilingScheme.getNumberOfXTilesAtLevel(0),
  145. that._tilingScheme.getNumberOfYTilesAtLevel(0)
  146. );
  147. that._tilesAvailablityLoaded = new TileAvailability(
  148. that._tilingScheme,
  149. that._lodCount
  150. );
  151. }
  152. that._levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(
  153. that._tilingScheme.ellipsoid,
  154. that._width,
  155. that._tilingScheme.getNumberOfXTilesAtLevel(0)
  156. );
  157. if (metadata.bandCount > 1) {
  158. console.log(
  159. "ArcGISTiledElevationTerrainProvider: Terrain data has more than 1 band. Using the first one."
  160. );
  161. }
  162. that._terrainDataStructure = {
  163. elementMultiplier: 1.0,
  164. lowestEncodedHeight: metadata.minValues[0],
  165. highestEncodedHeight: metadata.maxValues[0],
  166. };
  167. that._ready = true;
  168. return true;
  169. })
  170. .catch(function (error) {
  171. const message = `An error occurred while accessing ${that._resource.url}.`;
  172. TileProviderError.handleError(undefined, that, that._errorEvent, message);
  173. return Promise.reject(error);
  174. });
  175. this._errorEvent = new Event();
  176. }
  177. Object.defineProperties(ArcGISTiledElevationTerrainProvider.prototype, {
  178. /**
  179. * Gets an event that is raised when the terrain provider encounters an asynchronous error. By subscribing
  180. * to the event, you will be notified of the error and can potentially recover from it. Event listeners
  181. * are passed an instance of {@link TileProviderError}.
  182. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  183. * @type {Event}
  184. * @readonly
  185. */
  186. errorEvent: {
  187. get: function () {
  188. return this._errorEvent;
  189. },
  190. },
  191. /**
  192. * Gets the credit to display when this terrain provider is active. Typically this is used to credit
  193. * the source of the terrain. This function should not be called before {@link ArcGISTiledElevationTerrainProvider#ready} returns true.
  194. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  195. * @type {Credit}
  196. * @readonly
  197. */
  198. credit: {
  199. get: function () {
  200. //>>includeStart('debug', pragmas.debug);
  201. if (!this.ready) {
  202. throw new DeveloperError(
  203. "credit must not be called before ready returns true."
  204. );
  205. }
  206. //>>includeEnd('debug');
  207. return this._credit;
  208. },
  209. },
  210. /**
  211. * Gets the tiling scheme used by this provider. This function should
  212. * not be called before {@link ArcGISTiledElevationTerrainProvider#ready} returns true.
  213. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  214. * @type {GeographicTilingScheme}
  215. * @readonly
  216. */
  217. tilingScheme: {
  218. get: function () {
  219. //>>includeStart('debug', pragmas.debug);
  220. if (!this.ready) {
  221. throw new DeveloperError(
  222. "tilingScheme must not be called before ready returns true."
  223. );
  224. }
  225. //>>includeEnd('debug');
  226. return this._tilingScheme;
  227. },
  228. },
  229. /**
  230. * Gets a value indicating whether or not the provider is ready for use.
  231. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  232. * @type {Boolean}
  233. * @readonly
  234. */
  235. ready: {
  236. get: function () {
  237. return this._ready;
  238. },
  239. },
  240. /**
  241. * Gets a promise that resolves to true when the provider is ready for use.
  242. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  243. * @type {Promise.<Boolean>}
  244. * @readonly
  245. */
  246. readyPromise: {
  247. get: function () {
  248. return this._readyPromise;
  249. },
  250. },
  251. /**
  252. * Gets a value indicating whether or not the provider includes a water mask. The water mask
  253. * indicates which areas of the globe are water rather than land, so they can be rendered
  254. * as a reflective surface with animated waves. This function should not be
  255. * called before {@link ArcGISTiledElevationTerrainProvider#ready} returns true.
  256. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  257. * @type {Boolean}
  258. * @readonly
  259. */
  260. hasWaterMask: {
  261. get: function () {
  262. return false;
  263. },
  264. },
  265. /**
  266. * Gets a value indicating whether or not the requested tiles include vertex normals.
  267. * This function should not be called before {@link ArcGISTiledElevationTerrainProvider#ready} returns true.
  268. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  269. * @type {Boolean}
  270. * @readonly
  271. */
  272. hasVertexNormals: {
  273. get: function () {
  274. return false;
  275. },
  276. },
  277. /**
  278. * Gets an object that can be used to determine availability of terrain from this provider, such as
  279. * at points and in rectangles. This function should not be called before
  280. * {@link TerrainProvider#ready} returns true. This property may be undefined if availability
  281. * information is not available.
  282. * @memberof ArcGISTiledElevationTerrainProvider.prototype
  283. * @type {TileAvailability}
  284. * @readonly
  285. */
  286. availability: {
  287. get: function () {
  288. //>>includeStart('debug', pragmas.debug)
  289. if (!this._ready) {
  290. throw new DeveloperError(
  291. "availability must not be called before the terrain provider is ready."
  292. );
  293. }
  294. //>>includeEnd('debug');
  295. return this._tilesAvailable;
  296. },
  297. },
  298. });
  299. /**
  300. * Requests the geometry for a given tile. This function should not be called before
  301. * {@link ArcGISTiledElevationTerrainProvider#ready} returns true. The result includes terrain
  302. * data and indicates that all child tiles are available.
  303. *
  304. * @param {Number} x The X coordinate of the tile for which to request geometry.
  305. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  306. * @param {Number} level The level of the tile for which to request geometry.
  307. * @param {Request} [request] The request object. Intended for internal use only.
  308. * @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
  309. * returns undefined instead of a promise, it is an indication that too many requests are already
  310. * pending and the request will be retried later.
  311. */
  312. ArcGISTiledElevationTerrainProvider.prototype.requestTileGeometry = function (
  313. x,
  314. y,
  315. level,
  316. request
  317. ) {
  318. //>>includeStart('debug', pragmas.debug)
  319. if (!this._ready) {
  320. throw new DeveloperError(
  321. "requestTileGeometry must not be called before the terrain provider is ready."
  322. );
  323. }
  324. //>>includeEnd('debug');
  325. const tileResource = this._resource.getDerivedResource({
  326. url: `tile/${level}/${y}/${x}`,
  327. request: request,
  328. });
  329. const hasAvailability = this._hasAvailability;
  330. let availabilityPromise = Promise.resolve(true);
  331. let availabilityRequest;
  332. if (
  333. hasAvailability &&
  334. !defined(isTileAvailable(this, level + 1, x * 2, y * 2))
  335. ) {
  336. // We need to load child availability
  337. const availabilityResult = requestAvailability(
  338. this,
  339. level + 1,
  340. x * 2,
  341. y * 2
  342. );
  343. availabilityPromise = availabilityResult.promise;
  344. availabilityRequest = availabilityResult.request;
  345. }
  346. const promise = tileResource.fetchArrayBuffer();
  347. if (!defined(promise) || !defined(availabilityPromise)) {
  348. return undefined;
  349. }
  350. const that = this;
  351. const tilesAvailable = this._tilesAvailable;
  352. return Promise.all([promise, availabilityPromise])
  353. .then(function (result) {
  354. return new HeightmapTerrainData({
  355. buffer: result[0],
  356. width: that._width,
  357. height: that._height,
  358. childTileMask: hasAvailability
  359. ? tilesAvailable.computeChildMaskForTile(level, x, y)
  360. : ALL_CHILDREN,
  361. structure: that._terrainDataStructure,
  362. encoding: that._encoding,
  363. });
  364. })
  365. .catch(function (error) {
  366. if (
  367. defined(availabilityRequest) &&
  368. availabilityRequest.state === RequestState.CANCELLED
  369. ) {
  370. request.cancel();
  371. // Don't reject the promise till the request is actually cancelled
  372. // Otherwise it will think the request failed, but it didn't.
  373. return request.deferred.promise.finally(function () {
  374. request.state = RequestState.CANCELLED;
  375. return Promise.reject(error);
  376. });
  377. }
  378. return Promise.reject(error);
  379. });
  380. };
  381. function isTileAvailable(that, level, x, y) {
  382. if (!that._hasAvailability) {
  383. return undefined;
  384. }
  385. const tilesAvailablityLoaded = that._tilesAvailablityLoaded;
  386. const tilesAvailable = that._tilesAvailable;
  387. if (level > that._lodCount) {
  388. return false;
  389. }
  390. // Check if tiles are known to be available
  391. if (tilesAvailable.isTileAvailable(level, x, y)) {
  392. return true;
  393. }
  394. // or to not be available
  395. if (tilesAvailablityLoaded.isTileAvailable(level, x, y)) {
  396. return false;
  397. }
  398. return undefined;
  399. }
  400. /**
  401. * Gets the maximum geometric error allowed in a tile at a given level.
  402. *
  403. * @param {Number} level The tile level for which to get the maximum geometric error.
  404. * @returns {Number} The maximum geometric error.
  405. */
  406. ArcGISTiledElevationTerrainProvider.prototype.getLevelMaximumGeometricError = function (
  407. level
  408. ) {
  409. //>>includeStart('debug', pragmas.debug);
  410. if (!this.ready) {
  411. throw new DeveloperError(
  412. "getLevelMaximumGeometricError must not be called before ready returns true."
  413. );
  414. }
  415. //>>includeEnd('debug');
  416. return this._levelZeroMaximumGeometricError / (1 << level);
  417. };
  418. /**
  419. * Determines whether data for a tile is available to be loaded.
  420. *
  421. * @param {Number} x The X coordinate of the tile for which to request geometry.
  422. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  423. * @param {Number} level The level of the tile for which to request geometry.
  424. * @returns {Boolean|undefined} Undefined if not supported, otherwise true or false.
  425. */
  426. ArcGISTiledElevationTerrainProvider.prototype.getTileDataAvailable = function (
  427. x,
  428. y,
  429. level
  430. ) {
  431. if (!this._hasAvailability) {
  432. return undefined;
  433. }
  434. const result = isTileAvailable(this, level, x, y);
  435. if (defined(result)) {
  436. return result;
  437. }
  438. requestAvailability(this, level, x, y);
  439. return undefined;
  440. };
  441. /**
  442. * Makes sure we load availability data for a tile
  443. *
  444. * @param {Number} x The X coordinate of the tile for which to request geometry.
  445. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  446. * @param {Number} level The level of the tile for which to request geometry.
  447. * @returns {undefined} This provider does not support loading availability.
  448. */
  449. ArcGISTiledElevationTerrainProvider.prototype.loadTileDataAvailability = function (
  450. x,
  451. y,
  452. level
  453. ) {
  454. return undefined;
  455. };
  456. function findRange(origin, width, height, data) {
  457. const endCol = width - 1;
  458. const endRow = height - 1;
  459. const value = data[origin.y * width + origin.x];
  460. const endingIndices = [];
  461. const range = {
  462. startX: origin.x,
  463. startY: origin.y,
  464. endX: 0,
  465. endY: 0,
  466. };
  467. const corner = new Cartesian2(origin.x + 1, origin.y + 1);
  468. let doneX = false;
  469. let doneY = false;
  470. while (!(doneX && doneY)) {
  471. // We want to use the original value when checking Y,
  472. // so get it before it possibly gets incremented
  473. let endX = corner.x;
  474. // If we no longer move in the Y direction we need to check the corner tile in X pass
  475. const endY = doneY ? corner.y + 1 : corner.y;
  476. // Check X range
  477. if (!doneX) {
  478. for (let y = origin.y; y < endY; ++y) {
  479. if (data[y * width + corner.x] !== value) {
  480. doneX = true;
  481. break;
  482. }
  483. }
  484. if (doneX) {
  485. endingIndices.push(new Cartesian2(corner.x, origin.y));
  486. // Use the last good column so we can continue with Y
  487. --corner.x;
  488. --endX;
  489. range.endX = corner.x;
  490. } else if (corner.x === endCol) {
  491. range.endX = corner.x;
  492. doneX = true;
  493. } else {
  494. ++corner.x;
  495. }
  496. }
  497. // Check Y range - The corner tile is checked here
  498. if (!doneY) {
  499. const col = corner.y * width;
  500. for (let x = origin.x; x <= endX; ++x) {
  501. if (data[col + x] !== value) {
  502. doneY = true;
  503. break;
  504. }
  505. }
  506. if (doneY) {
  507. endingIndices.push(new Cartesian2(origin.x, corner.y));
  508. // Use the last good row so we can continue with X
  509. --corner.y;
  510. range.endY = corner.y;
  511. } else if (corner.y === endRow) {
  512. range.endY = corner.y;
  513. doneY = true;
  514. } else {
  515. ++corner.y;
  516. }
  517. }
  518. }
  519. return {
  520. endingIndices: endingIndices,
  521. range: range,
  522. value: value,
  523. };
  524. }
  525. function computeAvailability(x, y, width, height, data) {
  526. const ranges = [];
  527. const singleValue = data.every(function (val) {
  528. return val === data[0];
  529. });
  530. if (singleValue) {
  531. if (data[0] === 1) {
  532. ranges.push({
  533. startX: x,
  534. startY: y,
  535. endX: x + width - 1,
  536. endY: y + height - 1,
  537. });
  538. }
  539. return ranges;
  540. }
  541. let positions = [new Cartesian2(0, 0)];
  542. while (positions.length > 0) {
  543. const origin = positions.pop();
  544. const result = findRange(origin, width, height, data);
  545. if (result.value === 1) {
  546. // Convert range into the array into global tile coordinates
  547. const range = result.range;
  548. range.startX += x;
  549. range.endX += x;
  550. range.startY += y;
  551. range.endY += y;
  552. ranges.push(range);
  553. }
  554. const endingIndices = result.endingIndices;
  555. if (endingIndices.length > 0) {
  556. positions = positions.concat(endingIndices);
  557. }
  558. }
  559. return ranges;
  560. }
  561. function requestAvailability(that, level, x, y) {
  562. if (!that._hasAvailability) {
  563. return {};
  564. }
  565. // Fetch 128x128 availability list, so we make the minimum amount of requests
  566. const xOffset = Math.floor(x / 128) * 128;
  567. const yOffset = Math.floor(y / 128) * 128;
  568. const dim = Math.min(1 << level, 128);
  569. const url = `tilemap/${level}/${yOffset}/${xOffset}/${dim}/${dim}`;
  570. const availableCache = that._availableCache;
  571. if (defined(availableCache[url])) {
  572. return availableCache[url];
  573. }
  574. const request = new Request({
  575. throttle: false,
  576. throttleByServer: true,
  577. type: RequestType.TERRAIN,
  578. });
  579. const tilemapResource = that._resource.getDerivedResource({
  580. url: url,
  581. request: request,
  582. });
  583. let promise = tilemapResource.fetchJson();
  584. if (!defined(promise)) {
  585. return {};
  586. }
  587. promise = promise.then(function (result) {
  588. const available = computeAvailability(
  589. xOffset,
  590. yOffset,
  591. dim,
  592. dim,
  593. result.data
  594. );
  595. // Mark whole area as having availability loaded
  596. that._tilesAvailablityLoaded.addAvailableTileRange(
  597. level,
  598. xOffset,
  599. yOffset,
  600. xOffset + dim,
  601. yOffset + dim
  602. );
  603. const tilesAvailable = that._tilesAvailable;
  604. for (let i = 0; i < available.length; ++i) {
  605. const range = available[i];
  606. tilesAvailable.addAvailableTileRange(
  607. level,
  608. range.startX,
  609. range.startY,
  610. range.endX,
  611. range.endY
  612. );
  613. }
  614. // Conveniently return availability of original tile
  615. return isTileAvailable(that, level, x, y);
  616. });
  617. availableCache[url] = {
  618. promise: promise,
  619. request: request,
  620. };
  621. promise = promise.finally(function (result) {
  622. delete availableCache[url];
  623. return result;
  624. });
  625. return {
  626. promise: promise,
  627. request: request,
  628. };
  629. }
  630. export default ArcGISTiledElevationTerrainProvider;