ArcGISTiledElevationTerrainProvider.js 20 KB

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