Source: util/Tile.js

  1. /*
  2. * Copyright 2003-2006, 2009, 2017, 2020 United States Government, as represented
  3. * by the Administrator of the National Aeronautics and Space Administration.
  4. * All rights reserved.
  5. *
  6. * The NASAWorldWind/WebWorldWind platform is licensed under the Apache License,
  7. * Version 2.0 (the "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License
  9. * at http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software distributed
  12. * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
  13. * CONDITIONS OF ANY KIND, either express or implied. See the License for the
  14. * specific language governing permissions and limitations under the License.
  15. *
  16. * NASAWorldWind/WebWorldWind also contains the following 3rd party Open Source
  17. * software:
  18. *
  19. * ES6-Promise – under MIT License
  20. * libtess.js – SGI Free Software License B
  21. * Proj4 – under MIT License
  22. * JSZip – under MIT License
  23. *
  24. * A complete listing of 3rd Party software notices and licenses included in
  25. * WebWorldWind can be found in the WebWorldWind 3rd-party notices and licenses
  26. * PDF found in code directory.
  27. */
  28. /**
  29. * @exports Tile
  30. */
  31. define([
  32. '../error/ArgumentError',
  33. '../geom/BoundingBox',
  34. '../util/Logger',
  35. '../geom/Sector',
  36. '../geom/Vec3',
  37. '../util/WWUtil'
  38. ],
  39. function (ArgumentError,
  40. BoundingBox,
  41. Logger,
  42. Sector,
  43. Vec3,
  44. WWUtil) {
  45. "use strict";
  46. /**
  47. * Constructs a tile for a specified sector, level, row and column.
  48. * @alias Tile
  49. * @constructor
  50. * @classdesc Represents a tile of terrain or imagery.
  51. * Provides a base class for texture tiles used by tiled image layers and elevation tiles used by elevation models.
  52. * Applications typically do not interact with this class.
  53. * @param {Sector} sector The sector represented by this tile.
  54. * @param {Level} level This tile's level in a tile pyramid.
  55. * @param {Number} row This tile's row in the specified level in a tile pyramid.
  56. * @param {Number} column This tile's column in the specified level in a tile pyramid.
  57. * @throws {ArgumentError} If the specified sector or level is null or undefined or the row or column arguments
  58. * are less than zero.
  59. */
  60. var Tile = function (sector, level, row, column) {
  61. if (!sector) {
  62. throw new ArgumentError(
  63. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor", "missingSector"));
  64. }
  65. if (!level) {
  66. throw new ArgumentError(
  67. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
  68. "The specified level is null or undefined."));
  69. }
  70. if (row < 0 || column < 0) {
  71. throw new ArgumentError(
  72. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "constructor",
  73. "The specified row or column is less than zero."));
  74. }
  75. /**
  76. * The sector represented by this tile.
  77. * @type {Sector}
  78. * @readonly
  79. */
  80. this.sector = sector;
  81. /**
  82. * The level at which this tile lies in a tile pyramid.
  83. * @type {Number}
  84. * @readonly
  85. */
  86. this.level = level;
  87. /**
  88. * The row in this tile's level in which this tile lies in a tile pyramid.
  89. * @type {Number}
  90. * @readonly
  91. */
  92. this.row = row;
  93. /**
  94. * The column in this tile's level in which this tile lies in a tile pyramid.
  95. * @type {Number}
  96. * @readonly
  97. */
  98. this.column = column;
  99. /**
  100. * The width in pixels or cells of this tile's associated resource.
  101. * @type {Number}
  102. */
  103. this.tileWidth = level.tileWidth;
  104. /**
  105. * The height in pixels or cells of this tile's associated resource.
  106. * @type {Number}
  107. */
  108. this.tileHeight = level.tileHeight;
  109. /**
  110. * The size in radians of pixels or cells of this tile's associated resource.
  111. * @type {Number}
  112. */
  113. this.texelSize = level.texelSize;
  114. /**
  115. * A key that uniquely identifies this tile within a level set.
  116. * @type {String}
  117. * @readonly
  118. */
  119. this.tileKey = Tile.computeTileKey(level.levelNumber, row, column);
  120. /**
  121. * The Cartesian bounding box of this tile.
  122. * @type {BoundingBox}
  123. */
  124. this.extent = null;
  125. /**
  126. * The tile's local origin in model coordinates. Any model coordinate points associates with the tile
  127. * should be relative to this point.
  128. * @type {Vec3}
  129. */
  130. this.referencePoint = null;
  131. /**
  132. * This tile's opacity.
  133. * @type {Number}
  134. * @default 1
  135. */
  136. this.opacity = 1;
  137. // Internal use only. Intentionally not documented.
  138. this.samplePoints = null;
  139. // Internal use only. Intentionally not documented.
  140. this.sampleElevations = null;
  141. // Internal use only. Intentionally not documented.
  142. this.updateTimestamp = null;
  143. // Internal use only. Intentionally not documented.
  144. this.updateVerticalExaggeration = null;
  145. // Internal use only. Intentionally not documented.
  146. this.updateGlobeStateKey = null;
  147. };
  148. /**
  149. * Indicates whether this tile is equivalent to a specified tile.
  150. * @param {Tile} that The tile to check equivalence with.
  151. * @returns {boolean} true if this tile is equivalent to the specified one, false if
  152. * they are not equivalent or the specified tile is null or undefined.
  153. */
  154. Tile.prototype.isEqual = function (that) {
  155. if (!that)
  156. return false;
  157. if (!that.tileKey)
  158. return false;
  159. return this.tileKey == that.tileKey;
  160. };
  161. /**
  162. * Returns the size of this tile in bytes.
  163. * @returns {Number} The size of this tile in bytes.
  164. */
  165. Tile.prototype.size = function () {
  166. return 4 // child pointer
  167. + (4 + 32) // sector
  168. + 4 //level pointer (the level is common to the layer or tessellator so is not included here)
  169. + 8 // row and column
  170. + 8 // texel size
  171. + (4 + 32) // reference point
  172. + (4 + 676) // bounding box
  173. + 8 // min and max height
  174. + (4 + 32) // nearest point
  175. + 8; // extent timestamp and vertical exaggeration
  176. };
  177. /**
  178. * Computes an approximate distance from this tile to a specified vector.
  179. * @param {Vec3} vector The vector to compute the distance to.
  180. * @returns {number} The distance between this tile and the vector.
  181. * @throws {ArgumentError} If the specified vector is null or undefined.
  182. */
  183. Tile.prototype.distanceTo = function (vector) {
  184. if (!vector) {
  185. throw new ArgumentError(
  186. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "distanceTo", "missingVector"));
  187. }
  188. var px = vector[0], py = vector[1], pz = vector[2],
  189. dx, dy, dz,
  190. points = this.samplePoints,
  191. distance = Number.POSITIVE_INFINITY;
  192. for (var i = 0, len = points.length; i < len; i += 3) {
  193. dx = px - points[i];
  194. dy = py - points[i + 1];
  195. dz = pz - points[i + 2];
  196. distance = Math.min(distance, dx * dx + dy * dy + dz * dz); // minimum squared distance
  197. }
  198. return Math.sqrt(distance);
  199. };
  200. /**
  201. * Returns the four children formed by subdividing this tile.
  202. * @param {Level} level The level of the children.
  203. * @param {TileFactory} tileFactory The tile factory to use to create the children.
  204. * @returns {Tile[]} An array containing the four child tiles.
  205. * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
  206. */
  207. Tile.prototype.subdivide = function (level, tileFactory) {
  208. if (!level) {
  209. throw new ArgumentError(
  210. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
  211. "The specified level is null or undefined."));
  212. }
  213. if (!tileFactory) {
  214. throw new ArgumentError(
  215. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivide",
  216. "The specified tile factory is null or undefined."));
  217. }
  218. var latMin = this.sector.minLatitude,
  219. latMax = this.sector.maxLatitude,
  220. latMid = this.sector.centroidLatitude(),
  221. lonMin = this.sector.minLongitude,
  222. lonMax = this.sector.maxLongitude,
  223. lonMid = this.sector.centroidLongitude(),
  224. subRow,
  225. subCol,
  226. childSector,
  227. children = [];
  228. subRow = 2 * this.row;
  229. subCol = 2 * this.column;
  230. childSector = new Sector(latMin, latMid, lonMin, lonMid);
  231. children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  232. subRow = 2 * this.row;
  233. subCol = 2 * this.column + 1;
  234. childSector = new Sector(latMin, latMid, lonMid, lonMax);
  235. children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  236. subRow = 2 * this.row + 1;
  237. subCol = 2 * this.column;
  238. childSector = new Sector(latMid, latMax, lonMin, lonMid);
  239. children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  240. subRow = 2 * this.row + 1;
  241. subCol = 2 * this.column + 1;
  242. childSector = new Sector(latMid, latMax, lonMid, lonMax);
  243. children.push(tileFactory.createTile(childSector, level, subRow, subCol));
  244. return children;
  245. };
  246. /**
  247. * Returns the four children formed by subdividing this tile, drawing those children from a specified cache
  248. * if they exist there.
  249. * @param {Level} level The level of the children.
  250. * @param {TileFactory} tileFactory The tile factory to use to create the children.
  251. * @param {MemoryCache} cache A memory cache that may contain pre-existing child tiles. If non-null, the
  252. * cache is checked for a child collection prior to creating that tile. If one exists
  253. * in the cache it is returned rather than creating a new collection of children. If a new collection is
  254. * created, it is added to the cache.
  255. * @returns {Tile[]} An array containing the four tiles.
  256. * @throws {ArgumentError} If the specified tile factory or level is null or undefined.
  257. */
  258. Tile.prototype.subdivideToCache = function (level, tileFactory, cache) {
  259. if (!level) {
  260. throw new ArgumentError(
  261. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
  262. "The specified level is null or undefined."));
  263. }
  264. if (!tileFactory) {
  265. throw new ArgumentError(
  266. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "subdivideToCache",
  267. "The specified tile factory is null or undefined."));
  268. }
  269. var childList = cache ? cache.entryForKey(this.tileKey) : null;
  270. if (!childList) {
  271. childList = this.subdivide(level, tileFactory);
  272. if (childList && cache) {
  273. cache.putEntry(this.tileKey, childList, 4 * childList[0].size());
  274. }
  275. }
  276. return childList;
  277. };
  278. /**
  279. * Indicates whether this tile should be subdivided based on the current navigation state and a specified
  280. * detail factor.
  281. * @param {DrawContext} dc The current draw context.
  282. * @param {Number} detailFactor The detail factor to consider.
  283. * @returns {boolean} true If the tile should be subdivided, otherwise false.
  284. */
  285. Tile.prototype.mustSubdivide = function (dc, detailFactor) {
  286. // Split when the cell height (length of a texel) becomes greater than the specified fraction of the eye
  287. // distance. The fraction is specified as a power of 10. For example, a detail factor of 3 means split when
  288. // the cell height becomes more than one thousandth of the eye distance. Another way to say it is, use the
  289. // current tile if the cell height is less than the specified fraction of the eye distance.
  290. //
  291. // Note: It's tempting to instead compare a screen pixel size to the texel size, but that calculation is
  292. // window-size dependent and results in selecting an excessive number of tiles when the window is large.
  293. var cellSize = dc.globe.equatorialRadius * this.texelSize,
  294. distance = this.distanceTo(dc.eyePoint),
  295. pixelSize = dc.pixelSizeAtDistance(distance);
  296. return cellSize > Math.max(detailFactor * pixelSize, 0.5);
  297. };
  298. /**
  299. * Updates this tile's frame-dependent properties as necessary, according to the specified draw context.
  300. * <p>
  301. * The tile's frame-dependent properties, include the extent (bounding volume). These properties are dependent
  302. * on the tile's sector and the elevation values currently in memory, and change when those dependencies change.
  303. * Therefore <code>update</code> must be called once per frame before the extent and any other frame-dependent
  304. * properties are used. <code>update</code> intelligently determines when it is necessary to recompute these
  305. * properties, and does nothing if the state of all dependencies has not changed since the last call.
  306. * @param {DrawContext} dc The current draw context.
  307. */
  308. Tile.prototype.update = function (dc) {
  309. var elevationTimestamp = dc.globe.elevationTimestamp(),
  310. verticalExaggeration = dc.verticalExaggeration,
  311. globeStateKey = dc.globeStateKey;
  312. if (this.updateTimestamp != elevationTimestamp
  313. || this.updateVerticalExaggeration != verticalExaggeration
  314. || this.updateGlobeStateKey != globeStateKey) {
  315. this.doUpdate(dc);
  316. dc.frameStatistics.incrementTileUpdateCount(1);
  317. // Set the geometry extent to the globe's elevation timestamp on which the geometry is based. This
  318. // ensures that the geometry timestamp can be reliably compared to the elevation timestamp in subsequent
  319. // frames.
  320. this.updateTimestamp = elevationTimestamp;
  321. this.updateVerticalExaggeration = verticalExaggeration;
  322. this.updateGlobeStateKey = globeStateKey;
  323. }
  324. };
  325. /**
  326. * Updates this tile's frame-dependent properties according to the specified draw context.
  327. * @param {DrawContext} dc The current draw context.
  328. * @protected
  329. */
  330. Tile.prototype.doUpdate = function (dc) {
  331. // Compute the minimum and maximum world coordinate height for this tile's sector by multiplying the minimum
  332. // and maximum elevations by the scene's vertical exaggeration. This ensures that the elevations to used
  333. // build the terrain are contained by this tile's extent. Use zero if the globe as no elevations in this
  334. // tile's sector.
  335. var globe = dc.globe,
  336. verticalExaggeration = dc.verticalExaggeration,
  337. extremes = globe.minAndMaxElevationsForSector(this.sector),
  338. minHeight = extremes[0] * verticalExaggeration,
  339. maxHeight = extremes[1] * verticalExaggeration;
  340. if (minHeight === maxHeight) {
  341. minHeight = maxHeight + 10; // TODO: Determine if this is necessary.
  342. }
  343. // Compute a bounding box for this tile that contains the terrain surface in the tile's coverage area.
  344. if (!this.extent) {
  345. this.extent = new BoundingBox();
  346. }
  347. this.extent.setToSector(this.sector, globe, minHeight, maxHeight);
  348. // Compute the cartesian points for a 3x3 geographic grid. This grid captures sufficiently close sample
  349. // points in order to estimate the distance from the viewer to this tile.
  350. if (!this.samplePoints) {
  351. this.sampleElevations = new Float64Array(9);
  352. this.samplePoints = new Float64Array(3 * this.sampleElevations.length);
  353. }
  354. WWUtil.fillArray(this.sampleElevations, 0.5 * (minHeight + maxHeight));
  355. globe.computePointsForGrid(this.sector, 3, 3, this.sampleElevations, Vec3.ZERO, this.samplePoints);
  356. // Compute the reference point used as a local coordinate origin for the tile.
  357. if (!this.referencePoint) {
  358. this.referencePoint = new Vec3(0, 0, 0);
  359. }
  360. globe.computePointFromPosition(this.sector.centroidLatitude(), this.sector.centroidLongitude(), 0,
  361. this.referencePoint);
  362. };
  363. /**
  364. * Computes a key that uniquely identifies a tile within its level set.
  365. *
  366. * @param {Number} levelNumber The tile's level number in a tile pyramid.
  367. * @param {Number} row The tile's row in the specified level in a tile pyramid.
  368. * @param {Number} column The tile's column in the specified level in a tile pyramid.
  369. * @returns {String} A string key uniquely identifying a tile with the specified level, row, and column.
  370. */
  371. Tile.computeTileKey = function (levelNumber, row, column) {
  372. return levelNumber + "." + row + "." + column;
  373. };
  374. /**
  375. * Computes a row number for a tile within a level given the tile's latitude.
  376. * @param {Number} delta The level's latitudinal tile delta in degrees.
  377. * @param {Number} latitude The tile's minimum latitude.
  378. * @returns {Number} The computed row number.
  379. */
  380. Tile.computeRow = function (delta, latitude) {
  381. var row = Math.floor((latitude + 90) / delta);
  382. // If latitude is at the end of the grid, subtract 1 from the computed row to return the last row.
  383. if (latitude == 90) {
  384. row -= 1;
  385. }
  386. return row;
  387. };
  388. /**
  389. * Computes a column number for a tile within a level given the tile's longitude.
  390. * @param {Number} delta The level's longitudinal tile delta in degrees.
  391. * @param {Number} longitude The tile's minimum longitude.
  392. * @returns {Number} The computed column number.
  393. */
  394. Tile.computeColumn = function (delta, longitude) {
  395. var col = Math.floor((longitude + 180) / delta);
  396. // If longitude is at the end of the grid, subtract 1 from the computed column to return the last column.
  397. if (longitude == 180) {
  398. col -= 1;
  399. }
  400. return col;
  401. };
  402. /**
  403. * Computes the last row number for a tile within a level given the tile's maximum latitude.
  404. * @param {Number} delta The level's latitudinal tile delta in degrees.
  405. * @param {Number} maxLatitude The tile's maximum latitude in degrees.
  406. * @returns {Number} The computed row number.
  407. */
  408. Tile.computeLastRow = function (delta, maxLatitude) {
  409. var row = Math.ceil((maxLatitude + 90) / delta - 1);
  410. // If max latitude is in the first row, set the max row to 0.
  411. if (maxLatitude + 90 < delta) {
  412. row = 0;
  413. }
  414. return row;
  415. };
  416. /**
  417. * Computes the last column number for a tile within a level given the tile's maximum longitude.
  418. * @param {Number} delta The level's longitudinal tile delta in degrees.
  419. * @param {Number} maxLongitude The tile's maximum longitude in degrees.
  420. * @returns {Number} The computed column number.
  421. */
  422. Tile.computeLastColumn = function (delta, maxLongitude) {
  423. var col = Math.ceil((maxLongitude + 180) / delta - 1);
  424. // If max longitude is in the first column, set the max column to 0.
  425. if (maxLongitude + 180 < delta) {
  426. col = 0;
  427. }
  428. return col;
  429. };
  430. /**
  431. * Computes a sector spanned by a tile with the specified level number, row and column.
  432. * @param {Level} level The tile's level number.
  433. * @param {Number} row The tile's row number.
  434. * @param {Number} column The tile's column number.
  435. * @returns {Sector} The sector spanned by the tile.
  436. * @throws {ArgumentError} If the specified level is null or undefined or the row or column are less than zero.
  437. */
  438. Tile.computeSector = function (level, row, column) {
  439. if (!level) {
  440. throw new ArgumentError(
  441. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector", "missingLevel"));
  442. }
  443. if (row < 0 || column < 0) {
  444. throw new ArgumentError(
  445. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "computeSector",
  446. "The specified row or column is less than zero."));
  447. }
  448. var deltaLat = level.tileDelta.latitude,
  449. deltaLon = level.tileDelta.longitude,
  450. minLat = -90 + row * deltaLat,
  451. minLon = -180 + column * deltaLon,
  452. maxLat = minLat + deltaLat,
  453. maxLon = minLon + deltaLon;
  454. return new Sector(minLat, maxLat, minLon, maxLon);
  455. };
  456. /**
  457. * Creates all tiles for a specified level number.
  458. * @param {Level} level The level to create the tiles for.
  459. * @param {TileFactory} tileFactory The tile factory to use for creating tiles.
  460. * @param {Tile[]} result An array in which to return the results.
  461. * @throws {ArgumentError} If any argument is null or undefined.
  462. */
  463. Tile.createTilesForLevel = function (level, tileFactory, result) {
  464. if (!level) {
  465. throw new ArgumentError(
  466. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingLevel"));
  467. }
  468. if (!tileFactory) {
  469. throw new ArgumentError(
  470. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel",
  471. "The specified tile factory is null or undefined"));
  472. }
  473. if (!result) {
  474. throw new ArgumentError(
  475. Logger.logMessage(Logger.LEVEL_SEVERE, "Tile", "createTilesForLevel", "missingResult"));
  476. }
  477. var deltaLat = level.tileDelta.latitude,
  478. deltaLon = level.tileDelta.longitude,
  479. sector = level.sector,
  480. firstRow = Tile.computeRow(deltaLat, sector.minLatitude),
  481. lastRow = Tile.computeRow(deltaLat, sector.maxLatitude),
  482. firstCol = Tile.computeColumn(deltaLon, sector.minLongitude),
  483. lastCol = Tile.computeColumn(deltaLon, sector.maxLongitude),
  484. firstRowLat = -90 + firstRow * deltaLat,
  485. firstRowLon = -180 + firstCol * deltaLon,
  486. minLat = firstRowLat,
  487. minLon,
  488. maxLat,
  489. maxLon;
  490. for (var row = firstRow; row <= lastRow; row += 1) {
  491. maxLat = minLat + deltaLat;
  492. minLon = firstRowLon;
  493. for (var col = firstCol; col <= lastCol; col += 1) {
  494. maxLon = minLon + deltaLon;
  495. var tileSector = new Sector(minLat, maxLat, minLon, maxLon),
  496. tile = tileFactory.createTile(tileSector, level, row, col);
  497. result.push(tile);
  498. minLon = maxLon;
  499. }
  500. minLat = maxLat;
  501. }
  502. };
  503. return Tile;
  504. });