ArcGISTiledElevationTerrainProvider.js 23 KB

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