GoogleEarthEnterpriseTerrainProvider.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. import Credit from "./Credit.js";
  2. import defaultValue from "./defaultValue.js";
  3. import defined from "./defined.js";
  4. import DeveloperError from "./DeveloperError.js";
  5. import Event from "./Event.js";
  6. import GeographicTilingScheme from "./GeographicTilingScheme.js";
  7. import GoogleEarthEnterpriseMetadata from "./GoogleEarthEnterpriseMetadata.js";
  8. import GoogleEarthEnterpriseTerrainData from "./GoogleEarthEnterpriseTerrainData.js";
  9. import HeightmapTerrainData from "./HeightmapTerrainData.js";
  10. import JulianDate from "./JulianDate.js";
  11. import CesiumMath from "./Math.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 TaskProcessor from "./TaskProcessor.js";
  19. import TileProviderError from "./TileProviderError.js";
  20. const TerrainState = {
  21. UNKNOWN: 0,
  22. NONE: 1,
  23. SELF: 2,
  24. PARENT: 3,
  25. };
  26. const julianDateScratch = new JulianDate();
  27. function TerrainCache() {
  28. this._terrainCache = {};
  29. this._lastTidy = JulianDate.now();
  30. }
  31. TerrainCache.prototype.add = function (quadKey, buffer) {
  32. this._terrainCache[quadKey] = {
  33. buffer: buffer,
  34. timestamp: JulianDate.now(),
  35. };
  36. };
  37. TerrainCache.prototype.get = function (quadKey) {
  38. const terrainCache = this._terrainCache;
  39. const result = terrainCache[quadKey];
  40. if (defined(result)) {
  41. delete this._terrainCache[quadKey];
  42. return result.buffer;
  43. }
  44. };
  45. TerrainCache.prototype.tidy = function () {
  46. JulianDate.now(julianDateScratch);
  47. if (JulianDate.secondsDifference(julianDateScratch, this._lastTidy) > 10) {
  48. const terrainCache = this._terrainCache;
  49. const keys = Object.keys(terrainCache);
  50. const count = keys.length;
  51. for (let i = 0; i < count; ++i) {
  52. const k = keys[i];
  53. const e = terrainCache[k];
  54. if (JulianDate.secondsDifference(julianDateScratch, e.timestamp) > 10) {
  55. delete terrainCache[k];
  56. }
  57. }
  58. JulianDate.clone(julianDateScratch, this._lastTidy);
  59. }
  60. };
  61. /**
  62. * Provides tiled terrain using the Google Earth Enterprise REST API.
  63. *
  64. * @alias GoogleEarthEnterpriseTerrainProvider
  65. * @constructor
  66. *
  67. * @param {Object} options Object with the following properties:
  68. * @param {Resource|String} options.url The url of the Google Earth Enterprise server hosting the imagery.
  69. * @param {GoogleEarthEnterpriseMetadata} options.metadata A metadata object that can be used to share metadata requests with a GoogleEarthEnterpriseImageryProvider.
  70. * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used.
  71. * @param {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas.
  72. *
  73. * @see GoogleEarthEnterpriseImageryProvider
  74. * @see CesiumTerrainProvider
  75. *
  76. * @example
  77. * const geeMetadata = new GoogleEarthEnterpriseMetadata('http://www.earthenterprise.org/3d');
  78. * const gee = new Cesium.GoogleEarthEnterpriseTerrainProvider({
  79. * metadata : geeMetadata
  80. * });
  81. *
  82. * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing}
  83. */
  84. function GoogleEarthEnterpriseTerrainProvider(options) {
  85. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  86. //>>includeStart('debug', pragmas.debug);
  87. if (!(defined(options.url) || defined(options.metadata))) {
  88. throw new DeveloperError("options.url or options.metadata is required.");
  89. }
  90. //>>includeEnd('debug');
  91. let metadata;
  92. if (defined(options.metadata)) {
  93. metadata = options.metadata;
  94. } else {
  95. const resource = Resource.createIfNeeded(options.url);
  96. metadata = new GoogleEarthEnterpriseMetadata(resource);
  97. }
  98. this._metadata = metadata;
  99. this._tilingScheme = new GeographicTilingScheme({
  100. numberOfLevelZeroTilesX: 2,
  101. numberOfLevelZeroTilesY: 2,
  102. rectangle: new Rectangle(
  103. -CesiumMath.PI,
  104. -CesiumMath.PI,
  105. CesiumMath.PI,
  106. CesiumMath.PI
  107. ),
  108. ellipsoid: options.ellipsoid,
  109. });
  110. let credit = options.credit;
  111. if (typeof credit === "string") {
  112. credit = new Credit(credit);
  113. }
  114. this._credit = credit;
  115. // Pulled from Google's documentation
  116. this._levelZeroMaximumGeometricError = 40075.16;
  117. this._terrainCache = new TerrainCache();
  118. this._terrainPromises = {};
  119. this._terrainRequests = {};
  120. this._errorEvent = new Event();
  121. this._ready = false;
  122. const that = this;
  123. let metadataError;
  124. this._readyPromise = metadata.readyPromise
  125. .then(function (result) {
  126. if (!metadata.terrainPresent) {
  127. const e = new RuntimeError(
  128. `The server ${metadata.url} doesn't have terrain`
  129. );
  130. metadataError = TileProviderError.handleError(
  131. metadataError,
  132. that,
  133. that._errorEvent,
  134. e.message,
  135. undefined,
  136. undefined,
  137. undefined,
  138. e
  139. );
  140. return Promise.reject(e);
  141. }
  142. TileProviderError.handleSuccess(metadataError);
  143. that._ready = result;
  144. return result;
  145. })
  146. .catch(function (e) {
  147. metadataError = TileProviderError.handleError(
  148. metadataError,
  149. that,
  150. that._errorEvent,
  151. e.message,
  152. undefined,
  153. undefined,
  154. undefined,
  155. e
  156. );
  157. return Promise.reject(e);
  158. });
  159. }
  160. Object.defineProperties(GoogleEarthEnterpriseTerrainProvider.prototype, {
  161. /**
  162. * Gets the name of the Google Earth Enterprise server url hosting the imagery.
  163. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  164. * @type {String}
  165. * @readonly
  166. */
  167. url: {
  168. get: function () {
  169. return this._metadata.url;
  170. },
  171. },
  172. /**
  173. * Gets the proxy used by this provider.
  174. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  175. * @type {Proxy}
  176. * @readonly
  177. */
  178. proxy: {
  179. get: function () {
  180. return this._metadata.proxy;
  181. },
  182. },
  183. /**
  184. * Gets the tiling scheme used by this provider. This function should
  185. * not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true.
  186. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  187. * @type {TilingScheme}
  188. * @readonly
  189. */
  190. tilingScheme: {
  191. get: function () {
  192. //>>includeStart('debug', pragmas.debug);
  193. if (!this._ready) {
  194. throw new DeveloperError(
  195. "tilingScheme must not be called before the imagery provider is ready."
  196. );
  197. }
  198. //>>includeEnd('debug');
  199. return this._tilingScheme;
  200. },
  201. },
  202. /**
  203. * Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing
  204. * to the event, you will be notified of the error and can potentially recover from it. Event listeners
  205. * are passed an instance of {@link TileProviderError}.
  206. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  207. * @type {Event}
  208. * @readonly
  209. */
  210. errorEvent: {
  211. get: function () {
  212. return this._errorEvent;
  213. },
  214. },
  215. /**
  216. * Gets a value indicating whether or not the provider is ready for use.
  217. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  218. * @type {Boolean}
  219. * @readonly
  220. */
  221. ready: {
  222. get: function () {
  223. return this._ready;
  224. },
  225. },
  226. /**
  227. * Gets a promise that resolves to true when the provider is ready for use.
  228. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  229. * @type {Promise.<Boolean>}
  230. * @readonly
  231. */
  232. readyPromise: {
  233. get: function () {
  234. return this._readyPromise;
  235. },
  236. },
  237. /**
  238. * Gets the credit to display when this terrain provider is active. Typically this is used to credit
  239. * the source of the terrain. This function should not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true.
  240. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  241. * @type {Credit}
  242. * @readonly
  243. */
  244. credit: {
  245. get: function () {
  246. return this._credit;
  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 GoogleEarthEnterpriseTerrainProvider#ready} returns true.
  254. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  255. * @type {Boolean}
  256. * @readonly
  257. */
  258. hasWaterMask: {
  259. get: function () {
  260. return false;
  261. },
  262. },
  263. /**
  264. * Gets a value indicating whether or not the requested tiles include vertex normals.
  265. * This function should not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true.
  266. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  267. * @type {Boolean}
  268. * @readonly
  269. */
  270. hasVertexNormals: {
  271. get: function () {
  272. return false;
  273. },
  274. },
  275. /**
  276. * Gets an object that can be used to determine availability of terrain from this provider, such as
  277. * at points and in rectangles. This function should not be called before
  278. * {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. This property may be undefined if availability
  279. * information is not available.
  280. * @memberof GoogleEarthEnterpriseTerrainProvider.prototype
  281. * @type {TileAvailability}
  282. * @readonly
  283. */
  284. availability: {
  285. get: function () {
  286. return undefined;
  287. },
  288. },
  289. });
  290. const taskProcessor = new TaskProcessor("decodeGoogleEarthEnterprisePacket");
  291. // If the tile has its own terrain, then you can just use its child bitmask. If it was requested using it's parent
  292. // then you need to check all of its children to see if they have terrain.
  293. function computeChildMask(quadKey, info, metadata) {
  294. let childMask = info.getChildBitmask();
  295. if (info.terrainState === TerrainState.PARENT) {
  296. childMask = 0;
  297. for (let i = 0; i < 4; ++i) {
  298. const child = metadata.getTileInformationFromQuadKey(
  299. quadKey + i.toString()
  300. );
  301. if (defined(child) && child.hasTerrain()) {
  302. childMask |= 1 << i;
  303. }
  304. }
  305. }
  306. return childMask;
  307. }
  308. /**
  309. * Requests the geometry for a given tile. This function should not be called before
  310. * {@link GoogleEarthEnterpriseTerrainProvider#ready} returns true. The result must include terrain data and
  311. * may optionally include a water mask and an indication of which child tiles are available.
  312. *
  313. * @param {Number} x The X coordinate of the tile for which to request geometry.
  314. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  315. * @param {Number} level The level of the tile for which to request geometry.
  316. * @param {Request} [request] The request object. Intended for internal use only.
  317. * @returns {Promise.<TerrainData>|undefined} A promise for the requested geometry. If this method
  318. * returns undefined instead of a promise, it is an indication that too many requests are already
  319. * pending and the request will be retried later.
  320. *
  321. * @exception {DeveloperError} This function must not be called before {@link GoogleEarthEnterpriseTerrainProvider#ready}
  322. * returns true.
  323. */
  324. GoogleEarthEnterpriseTerrainProvider.prototype.requestTileGeometry = function (
  325. x,
  326. y,
  327. level,
  328. request
  329. ) {
  330. //>>includeStart('debug', pragmas.debug)
  331. if (!this._ready) {
  332. throw new DeveloperError(
  333. "requestTileGeometry must not be called before the terrain provider is ready."
  334. );
  335. }
  336. //>>includeEnd('debug');
  337. const quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level);
  338. const terrainCache = this._terrainCache;
  339. const metadata = this._metadata;
  340. const info = metadata.getTileInformationFromQuadKey(quadKey);
  341. // Check if this tile is even possibly available
  342. if (!defined(info)) {
  343. return Promise.reject(new RuntimeError("Terrain tile doesn't exist"));
  344. }
  345. let terrainState = info.terrainState;
  346. if (!defined(terrainState)) {
  347. // First time we have tried to load this tile, so set terrain state to UNKNOWN
  348. terrainState = info.terrainState = TerrainState.UNKNOWN;
  349. }
  350. // If its in the cache, return it
  351. const buffer = terrainCache.get(quadKey);
  352. if (defined(buffer)) {
  353. const credit = metadata.providers[info.terrainProvider];
  354. return Promise.resolve(
  355. new GoogleEarthEnterpriseTerrainData({
  356. buffer: buffer,
  357. childTileMask: computeChildMask(quadKey, info, metadata),
  358. credits: defined(credit) ? [credit] : undefined,
  359. negativeAltitudeExponentBias: metadata.negativeAltitudeExponentBias,
  360. negativeElevationThreshold: metadata.negativeAltitudeThreshold,
  361. })
  362. );
  363. }
  364. // Clean up the cache
  365. terrainCache.tidy();
  366. // We have a tile, check to see if no ancestors have terrain or that we know for sure it doesn't
  367. if (!info.ancestorHasTerrain) {
  368. // We haven't reached a level with terrain, so return the ellipsoid
  369. return Promise.resolve(
  370. new HeightmapTerrainData({
  371. buffer: new Uint8Array(16 * 16),
  372. width: 16,
  373. height: 16,
  374. })
  375. );
  376. } else if (terrainState === TerrainState.NONE) {
  377. // Already have info and there isn't any terrain here
  378. return Promise.reject(new RuntimeError("Terrain tile doesn't exist"));
  379. }
  380. // Figure out where we are getting the terrain and what version
  381. let parentInfo;
  382. let q = quadKey;
  383. let terrainVersion = -1;
  384. switch (terrainState) {
  385. case TerrainState.SELF: // We have terrain and have retrieved it before
  386. terrainVersion = info.terrainVersion;
  387. break;
  388. case TerrainState.PARENT: // We have terrain in our parent
  389. q = q.substring(0, q.length - 1);
  390. parentInfo = metadata.getTileInformationFromQuadKey(q);
  391. terrainVersion = parentInfo.terrainVersion;
  392. break;
  393. case TerrainState.UNKNOWN: // We haven't tried to retrieve terrain yet
  394. if (info.hasTerrain()) {
  395. terrainVersion = info.terrainVersion; // We should have terrain
  396. } else {
  397. q = q.substring(0, q.length - 1);
  398. parentInfo = metadata.getTileInformationFromQuadKey(q);
  399. if (defined(parentInfo) && parentInfo.hasTerrain()) {
  400. terrainVersion = parentInfo.terrainVersion; // Try checking in the parent
  401. }
  402. }
  403. break;
  404. }
  405. // We can't figure out where to get the terrain
  406. if (terrainVersion < 0) {
  407. return Promise.reject(new RuntimeError("Terrain tile doesn't exist"));
  408. }
  409. // Load that terrain
  410. const terrainPromises = this._terrainPromises;
  411. const terrainRequests = this._terrainRequests;
  412. let sharedPromise;
  413. let sharedRequest;
  414. if (defined(terrainPromises[q])) {
  415. // Already being loaded possibly from another child, so return existing promise
  416. sharedPromise = terrainPromises[q];
  417. sharedRequest = terrainRequests[q];
  418. } else {
  419. // Create new request for terrain
  420. sharedRequest = request;
  421. const requestPromise = buildTerrainResource(
  422. this,
  423. q,
  424. terrainVersion,
  425. sharedRequest
  426. ).fetchArrayBuffer();
  427. if (!defined(requestPromise)) {
  428. return undefined; // Throttled
  429. }
  430. sharedPromise = requestPromise.then(function (terrain) {
  431. if (defined(terrain)) {
  432. return taskProcessor
  433. .scheduleTask(
  434. {
  435. buffer: terrain,
  436. type: "Terrain",
  437. key: metadata.key,
  438. },
  439. [terrain]
  440. )
  441. .then(function (terrainTiles) {
  442. // Add requested tile and mark it as SELF
  443. const requestedInfo = metadata.getTileInformationFromQuadKey(q);
  444. requestedInfo.terrainState = TerrainState.SELF;
  445. terrainCache.add(q, terrainTiles[0]);
  446. const provider = requestedInfo.terrainProvider;
  447. // Add children to cache
  448. const count = terrainTiles.length - 1;
  449. for (let j = 0; j < count; ++j) {
  450. const childKey = q + j.toString();
  451. const child = metadata.getTileInformationFromQuadKey(childKey);
  452. if (defined(child)) {
  453. terrainCache.add(childKey, terrainTiles[j + 1]);
  454. child.terrainState = TerrainState.PARENT;
  455. if (child.terrainProvider === 0) {
  456. child.terrainProvider = provider;
  457. }
  458. }
  459. }
  460. });
  461. }
  462. return Promise.reject(new RuntimeError("Failed to load terrain."));
  463. });
  464. terrainPromises[q] = sharedPromise; // Store promise without delete from terrainPromises
  465. terrainRequests[q] = sharedRequest;
  466. // Set promise so we remove from terrainPromises just one time
  467. sharedPromise = sharedPromise.finally(function () {
  468. delete terrainPromises[q];
  469. delete terrainRequests[q];
  470. });
  471. }
  472. return sharedPromise
  473. .then(function () {
  474. const buffer = terrainCache.get(quadKey);
  475. if (defined(buffer)) {
  476. const credit = metadata.providers[info.terrainProvider];
  477. return new GoogleEarthEnterpriseTerrainData({
  478. buffer: buffer,
  479. childTileMask: computeChildMask(quadKey, info, metadata),
  480. credits: defined(credit) ? [credit] : undefined,
  481. negativeAltitudeExponentBias: metadata.negativeAltitudeExponentBias,
  482. negativeElevationThreshold: metadata.negativeAltitudeThreshold,
  483. });
  484. }
  485. return Promise.reject(new RuntimeError("Failed to load terrain."));
  486. })
  487. .catch(function (error) {
  488. if (sharedRequest.state === RequestState.CANCELLED) {
  489. request.state = sharedRequest.state;
  490. return Promise.reject(error);
  491. }
  492. info.terrainState = TerrainState.NONE;
  493. return Promise.reject(error);
  494. });
  495. };
  496. /**
  497. * Gets the maximum geometric error allowed in a tile at a given level.
  498. *
  499. * @param {Number} level The tile level for which to get the maximum geometric error.
  500. * @returns {Number} The maximum geometric error.
  501. */
  502. GoogleEarthEnterpriseTerrainProvider.prototype.getLevelMaximumGeometricError = function (
  503. level
  504. ) {
  505. return this._levelZeroMaximumGeometricError / (1 << level);
  506. };
  507. /**
  508. * Determines whether data for a tile is available to be loaded.
  509. *
  510. * @param {Number} x The X coordinate of the tile for which to request geometry.
  511. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  512. * @param {Number} level The level of the tile for which to request geometry.
  513. * @returns {Boolean|undefined} Undefined if not supported, otherwise true or false.
  514. */
  515. GoogleEarthEnterpriseTerrainProvider.prototype.getTileDataAvailable = function (
  516. x,
  517. y,
  518. level
  519. ) {
  520. const metadata = this._metadata;
  521. let quadKey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level);
  522. const info = metadata.getTileInformation(x, y, level);
  523. if (info === null) {
  524. return false;
  525. }
  526. if (defined(info)) {
  527. if (!info.ancestorHasTerrain) {
  528. return true; // We'll just return the ellipsoid
  529. }
  530. const terrainState = info.terrainState;
  531. if (terrainState === TerrainState.NONE) {
  532. return false; // Terrain is not available
  533. }
  534. if (!defined(terrainState) || terrainState === TerrainState.UNKNOWN) {
  535. info.terrainState = TerrainState.UNKNOWN;
  536. if (!info.hasTerrain()) {
  537. quadKey = quadKey.substring(0, quadKey.length - 1);
  538. const parentInfo = metadata.getTileInformationFromQuadKey(quadKey);
  539. if (!defined(parentInfo) || !parentInfo.hasTerrain()) {
  540. return false;
  541. }
  542. }
  543. }
  544. return true;
  545. }
  546. if (metadata.isValid(quadKey)) {
  547. // We will need this tile, so request metadata and return false for now
  548. const request = new Request({
  549. throttle: false,
  550. throttleByServer: true,
  551. type: RequestType.TERRAIN,
  552. });
  553. metadata.populateSubtree(x, y, level, request);
  554. }
  555. return false;
  556. };
  557. /**
  558. * Makes sure we load availability data for a tile
  559. *
  560. * @param {Number} x The X coordinate of the tile for which to request geometry.
  561. * @param {Number} y The Y coordinate of the tile for which to request geometry.
  562. * @param {Number} level The level of the tile for which to request geometry.
  563. * @returns {undefined}
  564. */
  565. GoogleEarthEnterpriseTerrainProvider.prototype.loadTileDataAvailability = function (
  566. x,
  567. y,
  568. level
  569. ) {
  570. return undefined;
  571. };
  572. //
  573. // Functions to handle imagery packets
  574. //
  575. function buildTerrainResource(terrainProvider, quadKey, version, request) {
  576. version = defined(version) && version > 0 ? version : 1;
  577. return terrainProvider._metadata.resource.getDerivedResource({
  578. url: `flatfile?f1c-0${quadKey}-t.${version.toString()}`,
  579. request: request,
  580. });
  581. }
  582. export default GoogleEarthEnterpriseTerrainProvider;