Source: layer/WmtsLayer.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 WmtsLayer
  30. */
  31. define([
  32. '../util/AbsentResourceList',
  33. '../error/ArgumentError',
  34. '../util/Logger',
  35. '../geom/Matrix',
  36. '../geom/Sector',
  37. '../layer/Layer',
  38. '../cache/MemoryCache',
  39. '../render/Texture',
  40. '../util/WmsUrlBuilder',
  41. '../layer/WmtsLayerTile',
  42. '../util/WWMath',
  43. '../util/WWUtil'
  44. ],
  45. function (AbsentResourceList,
  46. ArgumentError,
  47. Logger,
  48. Matrix,
  49. Sector,
  50. Layer,
  51. MemoryCache,
  52. Texture,
  53. WmsUrlBuilder,
  54. WmtsLayerTile,
  55. WWMath,
  56. WWUtil) {
  57. "use strict";
  58. // TODO: Test Mercator layers.
  59. // TODO: Support tile matrix limits.
  60. // TODO: Extensibility for other projections.
  61. // TODO: Finish parsing capabilities document (ServiceIdentification and ServiceProvider).
  62. // TODO: Time dimensions.
  63. /**
  64. * Constructs a WMTS image layer.
  65. * @alias WmtsLayer
  66. * @constructor
  67. * @augments Layer
  68. * @classdesc Displays a WMTS image layer.
  69. * @param {{}} config Specifies configuration information for the layer. Must contain the following
  70. * properties:
  71. * <ul>
  72. * <li>identifier: {String} The layer name.</li>
  73. * <li>service: {String} The URL of the WMTS server</li>
  74. * <li>format: {String} The mime type of the image format to request, e.g., image/png.</li>
  75. * <li>tileMatrixSet: {{}} The tile matrix set to use for this layer.</li>
  76. * <li>style: {String} The style to use for this layer.</li>
  77. * <li>title: {String} The display name for this layer.</li>
  78. * </ul>
  79. * @param {String} timeString The time parameter passed to the WMTS server when imagery is requested. May be
  80. * null, in which case no time parameter is passed to the server.
  81. * @throws {ArgumentError} If the specified layer capabilities reference is null or undefined.
  82. */
  83. var WmtsLayer = function (config, timeString) {
  84. if (!config) {
  85. throw new ArgumentError(
  86. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "constructor",
  87. "No layer configuration specified."));
  88. }
  89. Layer.call(this, "WMTS Layer");
  90. /**
  91. * The WMTS layer identifier of this layer.
  92. * @type {String}
  93. * @readonly
  94. */
  95. this.layerIdentifier = config.identifier;
  96. /**
  97. * The style identifier specified to this layer's constructor.
  98. * @type {String}
  99. * @readonly
  100. */
  101. this.styleIdentifier = config.style;
  102. /**
  103. * The time string passed to this layer's constructor.
  104. * @type {String}
  105. * @readonly
  106. */
  107. this.timeString = timeString;
  108. /**
  109. * The image format specified to this layer's constructor.
  110. * @type {String}
  111. * @readonly
  112. */
  113. this.imageFormat = config.format;
  114. /**
  115. * The url specified to this layer's constructor.
  116. * @type {String}
  117. * @readonly
  118. */
  119. this.resourceUrl = config.resourceUrl;
  120. this.serviceUrl = config.service;
  121. /**
  122. * The tileMatrixSet specified to this layer's constructor.
  123. * @type {String}
  124. * @readonly
  125. */
  126. this.tileMatrixSet = config.tileMatrixSet;
  127. // Internal. Intentionally not documented.
  128. this.lasTtMVP = Matrix.fromIdentity();
  129. // Determine the layer's sector if possible. Mandatory for EPSG:4326 tile matrix sets. (Others compute
  130. // it from tile Matrix Set metadata.)
  131. // Sometimes BBOX defined in Matrix and not in Layer
  132. if (!config.wgs84BoundingBox && !config.boundingBox) {
  133. if (this.tileMatrixSet.boundingBox) {
  134. this.sector = new Sector(
  135. config.tileMatrixSet.boundingBox.lowerCorner[1],
  136. config.tileMatrixSet.boundingBox.upperCorner[1],
  137. config.tileMatrixSet.boundingBox.lowerCorner[0],
  138. config.tileMatrixSet.boundingBox.upperCorner[0]);
  139. } else {
  140. // Throw an exception if there is no bounding box.
  141. throw new ArgumentError(
  142. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "constructor",
  143. "No bounding box was specified in the layer or tile matrix set capabilities."));
  144. }
  145. } else if (config.wgs84BoundingBox) {
  146. this.sector = config.wgs84BoundingBox.getSector();
  147. } else if (this.tileMatrixSet.boundingBox &&
  148. WmtsLayer.isEpsg4326Crs(this.tileMatrixSet.boundingBox.crs)) {
  149. this.sector = new Sector(
  150. this.tileMatrixSet.boundingBox.lowerCorner[1],
  151. this.tileMatrixSet.boundingBox.upperCorner[1],
  152. this.tileMatrixSet.boundingBox.lowerCorner[0],
  153. this.tileMatrixSet.boundingBox.upperCorner[0]);
  154. } else if (WmtsLayer.isEpsg4326Crs(this.tileMatrixSet.supportedCRS)) {
  155. // Throw an exception if there is no 4326 bounding box.
  156. throw new ArgumentError(
  157. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "constructor",
  158. "No EPSG:4326 bounding box was specified in the layer or tile matrix set capabilities."));
  159. }
  160. // Check if the provided TileMatrixSet tile subdivision is compatible
  161. if (!WmtsLayer.isTileSubdivisionCompatible(this.tileMatrixSet)) {
  162. throw new ArgumentError(
  163. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "constructor",
  164. "TileMatrixSet level division not compatible."));
  165. }
  166. // Check if the provided TileMatrixSet coordinate system is compatible
  167. var crs = this.tileMatrixSet.supportedCRS;
  168. var supportedCrs = WmtsLayer.isEpsg3857Crs(crs) || WmtsLayer.isEpsg4326Crs(crs) || WmtsLayer.isOGCCrs84(crs);
  169. if (!supportedCrs) {
  170. throw new ArgumentError(
  171. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "constructor",
  172. "Provided CRS is not compatible."));
  173. }
  174. // Form a unique string to identify cache entries.
  175. this.cachePath = (this.resourceUrl || this.serviceUrl) +
  176. this.layerIdentifier + this.styleIdentifier + this.tileMatrixSet.identifier;
  177. if (timeString) {
  178. this.cachePath = this.cachePath + timeString;
  179. }
  180. /**
  181. * The displayName specified to this layer's constructor.
  182. * @type {String}
  183. * @readonly
  184. */
  185. this.displayName = config.title;
  186. this.currentTiles = [];
  187. this.currentTilesInvalid = true;
  188. this.tileCache = new MemoryCache(1000, 850); // Allocate a cache that accommodates 1,000 tiles.
  189. this.currentRetrievals = [];
  190. this.absentResourceList = new AbsentResourceList(3, 50e3);
  191. this.pickEnabled = false;
  192. /**
  193. * Controls the level of detail switching for this layer. The next highest resolution level is
  194. * used when an image's texel size is greater than this number of pixels, up to the maximum resolution
  195. * of this layer.
  196. * @type {Number}
  197. * @default 1.75
  198. */
  199. this.detailControl = 1.75;
  200. /**
  201. * Controls how many concurrent tile requests that are allowed for this layer.
  202. * @type {Number}
  203. * @default WorldWind.configuration.layerRetrievalQueueSize;
  204. */
  205. this.retrievalQueueSize = WorldWind.configuration.layerRetrievalQueueSize;
  206. };
  207. /**
  208. * Determines if the tile subdivision of the provided TileMatrixSet is compatible with WebWorldWind.
  209. * @param tileMatrixSet
  210. * @returns {boolean} true if this tile subdivision will work with WebWorldWind
  211. * @throws {ArgumentError} If the provided TileMatrixSet is null or empty
  212. */
  213. WmtsLayer.isTileSubdivisionCompatible = function (tileMatrixSet) {
  214. if (!tileMatrixSet || !tileMatrixSet.tileMatrix || tileMatrixSet.tileMatrix.length < 1) {
  215. throw new ArgumentError(
  216. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "isTileSubdivisionCompatible",
  217. "Empty tile matrix set"));
  218. }
  219. var matrixHeightRatio, matrixWidthRatio, tileMatrix, previousTileMatrix = tileMatrixSet.tileMatrix[0];
  220. for (var i = 1, len = tileMatrixSet.tileMatrix.length; i < len; i++) {
  221. tileMatrix = tileMatrixSet.tileMatrix[i];
  222. matrixHeightRatio = tileMatrix.matrixHeight / previousTileMatrix.matrixHeight;
  223. matrixWidthRatio = tileMatrix.matrixWidth / previousTileMatrix.matrixWidth;
  224. previousTileMatrix = tileMatrix;
  225. if (matrixHeightRatio !== 2 || matrixWidthRatio !== 2) {
  226. return false;
  227. }
  228. }
  229. return true;
  230. };
  231. /**
  232. * Constructs a tile matrix set object.
  233. * @param {{}} params Specifies parameters for the tile matrix set. Must contain the following
  234. * properties:
  235. * <ul>
  236. * <li>matrixSet: {String} The matrix name.</li>
  237. * <li>prefix: {Boolean} It represents if the identifier of the matrix must be prefixed by the matrix name.</li>
  238. * <li>projection: {String} The projection of the tiles.</li>
  239. * <li>topLeftCorner: {Array} The coordinates of the top left corner.</li>
  240. * <li>extent: {Array} The boundinx box for this matrix.</li>
  241. * <li>resolutions: {Array} The resolutions array.</li>
  242. * <li>matrixSet: {Number} The tile size.</li>
  243. * </ul>
  244. * @throws {ArgumentError} If the specified params.matrixSet is null or undefined. The name of the matrix to
  245. * use for this layer.
  246. * @throws {ArgumentError} If the specified params.prefix is null or undefined. It represents if the
  247. * identifier of the matrix must be prefixed by the matrix name
  248. * @throws {ArgumentError} If the specified params.projection is null or undefined.
  249. * @throws {ArgumentError} If the specified params.extent is null or undefined.
  250. * @throws {ArgumentError} If the specified params.resolutions is null or undefined.
  251. * @throws {ArgumentError} If the specified params.tileSize is null or undefined.
  252. * @throws {ArgumentError} If the specified params.topLeftCorner is null or undefined.
  253. */
  254. WmtsLayer.createTileMatrixSet = function (params) {
  255. if (!params.matrixSet) { // matrixSet
  256. throw new ArgumentError(
  257. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  258. "No matrixSet provided."));
  259. }
  260. if (!params.projection) { // projection
  261. throw new ArgumentError(
  262. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  263. "No projection provided."));
  264. }
  265. if (!params.extent || params.extent.length != 4) { // extent
  266. throw new ArgumentError(
  267. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  268. "No extent provided."));
  269. }
  270. // Define the boundingBox
  271. var boundingBox = {
  272. lowerCorner: [params.extent[0], params.extent[1]],
  273. upperCorner: [params.extent[2], params.extent[3]]
  274. };
  275. // Resolutions
  276. if (!params.resolutions) {
  277. throw new ArgumentError(
  278. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  279. "No resolutions provided."));
  280. }
  281. // Tile size
  282. if (!params.tileSize) {
  283. throw new ArgumentError(
  284. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  285. "No tile size provided."));
  286. }
  287. // Top left corner
  288. if (!params.topLeftCorner || params.topLeftCorner.length != 2) {
  289. throw new ArgumentError(
  290. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  291. "No extent provided."));
  292. }
  293. // Prefix
  294. if (params.prefix === undefined) {
  295. throw new ArgumentError(
  296. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  297. "Prefix not provided."));
  298. }
  299. // Check if the projection is supported
  300. if (!(WmtsLayer.isEpsg4326Crs(params.projection) || WmtsLayer.isOGCCrs84(params.projection) || WmtsLayer.isEpsg3857Crs(params.projection))) {
  301. throw new ArgumentError(
  302. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "createTileMatrixSet",
  303. "Projection provided not supported."));
  304. }
  305. var tileMatrixSet = [],
  306. scale;
  307. // Construct the tileMatrixSet
  308. for (var i = 0; i < params.resolutions.length; i++) {
  309. // Compute the scaleDenominator
  310. if (WmtsLayer.isEpsg4326Crs(params.projection) || WmtsLayer.isOGCCrs84(params.projection)) {
  311. scale = params.resolutions[i] * 6378137.0 * 2.0 * Math.PI / 360 / 0.00028;
  312. } else if (WmtsLayer.isEpsg3857Crs(params.projection)) {
  313. scale = params.resolutions[i] / 0.00028;
  314. }
  315. // Compute the matrix width / height
  316. var unitWidth = params.tileSize * params.resolutions[i];
  317. var unitHeight = params.tileSize * params.resolutions[i];
  318. var matrixWidth = Math.ceil((params.extent[2] - params.extent[0] - 0.01 * unitWidth) / unitWidth);
  319. var matrixHeight = Math.ceil((params.extent[3] - params.extent[1] - 0.01 * unitHeight) / unitHeight);
  320. // Define the tile matrix
  321. var tileMatrix = {
  322. identifier: params.prefix ? params.matrixSet + ":" + i : i,
  323. levelNumber: i,
  324. matrixHeight: matrixHeight,
  325. matrixWidth: matrixWidth,
  326. tileHeight: params.tileSize,
  327. tileWidth: params.tileSize,
  328. topLeftCorner: params.topLeftCorner,
  329. scaleDenominator: scale
  330. };
  331. tileMatrixSet.push(tileMatrix);
  332. }
  333. return {
  334. identifier: params.matrixSet,
  335. supportedCRS: params.projection,
  336. boundingBox: boundingBox,
  337. tileMatrix: tileMatrixSet
  338. };
  339. };
  340. /**
  341. * Forms a configuration object for a specified {@link WmtsLayerCapabilities} layer description. The
  342. * configuration object created and returned is suitable for passing to the WmtsLayer constructor.
  343. * <p>
  344. * This method also parses any time dimensions associated with the layer and returns them in the
  345. * configuration object's "timeSequences" property. This property is a mixed array of Date objects
  346. * and {@link PeriodicTimeSequence} objects describing the dimensions found.
  347. * @param wmtsLayerCapabilities {WmtsLayerCapabilities} The WMTS layer capabilities to create a configuration for.
  348. * @param style {string} The style to apply for this layer. May be null, in which case the first style recognized is used.
  349. * @param matrixSet {string} The matrix to use for this layer. May be null, in which case the first tileMatrixSet recognized is used.
  350. * @param imageFormat {string} The image format to use with this layer. May be null, in which case the first image format recognized is used.
  351. * @returns {{}} A configuration object.
  352. * @throws {ArgumentError} If the specified WMTS layer capabilities is null or undefined.
  353. */
  354. WmtsLayer.formLayerConfiguration = function (wmtsLayerCapabilities, style, matrixSet, imageFormat) {
  355. var config = {};
  356. /**
  357. * The WMTS layer identifier of this layer.
  358. * @type {String}
  359. * @readonly
  360. */
  361. config.identifier = wmtsLayerCapabilities.identifier;
  362. // Validate that the specified image format exists, or determine one if not specified.
  363. if (imageFormat) {
  364. var formatIdentifierFound = false;
  365. for (var i = 0; i < wmtsLayerCapabilities.format.length; i++) {
  366. if (wmtsLayerCapabilities.format[i] === imageFormat) {
  367. formatIdentifierFound = true;
  368. config.format = wmtsLayerCapabilities.format[i];
  369. break;
  370. }
  371. }
  372. if (!formatIdentifierFound) {
  373. Logger.logMessage(Logger.LEVEL_WARNING, "WmtsLayer", "formLayerConfiguration",
  374. "The specified image format is not available. Another one will be used.");
  375. config.format = null;
  376. }
  377. }
  378. if (!config.format) {
  379. if (wmtsLayerCapabilities.format.indexOf("image/png") >= 0) {
  380. config.format = "image/png";
  381. } else if (wmtsLayerCapabilities.format.indexOf("image/jpeg") >= 0) {
  382. config.format = "image/jpeg";
  383. } else if (wmtsLayerCapabilities.format.indexOf("image/tiff") >= 0) {
  384. config.format = "image/tiff";
  385. } else if (wmtsLayerCapabilities.format.indexOf("image/gif") >= 0) {
  386. config.format = "image/gif";
  387. } else {
  388. config.format = wmtsLayerCapabilities.format[0];
  389. }
  390. }
  391. if (!config.format) {
  392. throw new ArgumentError(
  393. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "formLayerConfiguration",
  394. "Layer does not provide a supported image format."));
  395. }
  396. // Configure URL
  397. if (wmtsLayerCapabilities.resourceUrl && (wmtsLayerCapabilities.resourceUrl.length >= 1)) {
  398. for (var i = 0; i < wmtsLayerCapabilities.resourceUrl.length; i++) {
  399. if (config.format === wmtsLayerCapabilities.resourceUrl[i].format) {
  400. config.resourceUrl = wmtsLayerCapabilities.resourceUrl[i].template;
  401. break;
  402. }
  403. }
  404. } else { // resource-oriented interface not supported, so use KVP interface
  405. config.service = wmtsLayerCapabilities.capabilities.getGetTileKvpAddress();
  406. if (config.service) {
  407. config.service = WmsUrlBuilder.fixGetMapString(config.service);
  408. }
  409. }
  410. if (!config.resourceUrl && !config.service) {
  411. throw new ArgumentError(
  412. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "formLayerConfiguration",
  413. "No resource URL or KVP GetTile service URL specified in WMTS capabilities."));
  414. }
  415. // Validate that the specified style identifier exists, or determine one if not specified.
  416. if (style) {
  417. var styleIdentifierFound = false;
  418. for (var i = 0; i < wmtsLayerCapabilities.style.length; i++) {
  419. if (wmtsLayerCapabilities.style[i].identifier === style) {
  420. styleIdentifierFound = true;
  421. config.style = wmtsLayerCapabilities.style[i].identifier;
  422. break;
  423. }
  424. }
  425. if (!styleIdentifierFound) {
  426. Logger.logMessage(Logger.LEVEL_WARNING, "WmtsLayer", "formLayerConfiguration",
  427. "The specified style identifier is not available. The server's default style will be used.");
  428. config.style = null;
  429. }
  430. }
  431. if (!config.style) {
  432. for (i = 0; i < wmtsLayerCapabilities.style.length; i++) {
  433. if (wmtsLayerCapabilities.style[i].isDefault) {
  434. config.style = wmtsLayerCapabilities.style[i].identifier;
  435. break;
  436. }
  437. }
  438. }
  439. if (!config.style) {
  440. Logger.logMessage(Logger.LEVEL_WARNING, "WmtsLayer", "formLayerConfiguration",
  441. "No default style available. A style will not be specified in tile requests.");
  442. }
  443. // Retrieve the supported tile matrix sets for testing against provided tile matrix set or for tile matrix
  444. // set negotiation.
  445. var supportedTileMatrixSets = wmtsLayerCapabilities.getLayerSupportedTileMatrixSets();
  446. // Validate that the specified TileMatrixSet exists and is compatible with WebWorldWind
  447. if (matrixSet) {
  448. for (var i = 0, len = supportedTileMatrixSets.length; i < len; i++) {
  449. if (supportedTileMatrixSets[i].identifier === matrixSet && WmtsLayer.isTileSubdivisionCompatible(supportedTileMatrixSets[i])) {
  450. config.tileMatrixSet = supportedTileMatrixSets[i];
  451. break;
  452. }
  453. }
  454. if (!config.tileMatrixSet) {
  455. Logger.logMessage(Logger.LEVEL_WARNING, "WmtsLayer", "formLayerConfiguration",
  456. "The specified tileMatrixSet is not available. Another one will be used.");
  457. config.tileMatrixSet = null;
  458. }
  459. }
  460. if (!config.tileMatrixSet) {
  461. // Find the tile matrix set we want to use. Prefer EPSG:4326, then EPSG:3857.
  462. var tms, tms4326 = null, tms3857 = null, tmsCRS84 = null;
  463. for (var i = 0, len = supportedTileMatrixSets.length; i < len; i++) {
  464. tms = supportedTileMatrixSets[i];
  465. // check for suitable tile division
  466. if (WmtsLayer.isTileSubdivisionCompatible(tms)) {
  467. if (WmtsLayer.isEpsg4326Crs(tms.supportedCRS)) {
  468. tms4326 = tms4326 || tms;
  469. } else if (WmtsLayer.isEpsg3857Crs(tms.supportedCRS)) {
  470. tms3857 = tms3857 || tms;
  471. } else if (WmtsLayer.isOGCCrs84(tms.supportedCRS)) {
  472. tmsCRS84 = tmsCRS84 || tms;
  473. }
  474. }
  475. }
  476. config.tileMatrixSet = tms4326 || tms3857 || tmsCRS84;
  477. }
  478. if (!config.tileMatrixSet) {
  479. throw new ArgumentError(
  480. Logger.logMessage(Logger.LEVEL_SEVERE, "WmtsLayer", "formLayerConfiguration",
  481. "No supported Tile Matrix Set could be found."));
  482. }
  483. // Configure boundingBox
  484. config.boundingBox = wmtsLayerCapabilities.boundingBox;
  485. config.wgs84BoundingBox = wmtsLayerCapabilities.wgs84BoundingBox;
  486. // Determine a default display name.
  487. if (wmtsLayerCapabilities.titles.length > 0) {
  488. config.title = wmtsLayerCapabilities.titles[0].value;
  489. } else {
  490. config.title = wmtsLayerCapabilities.identifier;
  491. }
  492. return config;
  493. };
  494. WmtsLayer.prototype = Object.create(Layer.prototype);
  495. WmtsLayer.prototype.doRender = function (dc) {
  496. if (!dc.terrain)
  497. return;
  498. if (this.currentTilesInvalid
  499. || !dc.modelviewProjection.equals(this.lasTtMVP)
  500. || dc.globeStateKey !== this.lastGlobeStateKey) {
  501. this.currentTilesInvalid = false;
  502. this.assembleTiles(dc);
  503. }
  504. this.lasTtMVP.copy(dc.modelviewProjection);
  505. this.lastGlobeStateKey = dc.globeStateKey;
  506. if (this.currentTiles.length > 0) {
  507. dc.surfaceTileRenderer.renderTiles(dc, this.currentTiles, this.opacity);
  508. dc.frameStatistics.incrementImageTileCount(this.currentTiles.length);
  509. this.inCurrentFrame = true;
  510. }
  511. };
  512. WmtsLayer.prototype.isLayerInView = function (dc) {
  513. return dc.terrain && dc.terrain.sector && dc.terrain.sector.intersects(this.sector);
  514. };
  515. WmtsLayer.prototype.isTileVisible = function (dc, tile) {
  516. if (dc.globe.projectionLimits && !tile.sector.overlaps(dc.globe.projectionLimits)) {
  517. return false;
  518. }
  519. return tile.extent.intersectsFrustum(dc.frustumInModelCoordinates);
  520. };
  521. WmtsLayer.prototype.assembleTiles = function (dc) {
  522. this.currentTiles = [];
  523. if (!this.topLevelTiles || (this.topLevelTiles.length === 0)) {
  524. this.createTopLevelTiles(dc);
  525. }
  526. for (var i = 0, len = this.topLevelTiles.length; i < len; i++) {
  527. var tile = this.topLevelTiles[i];
  528. tile.update(dc);
  529. this.currentAncestorTile = null;
  530. if (this.isTileVisible(dc, tile)) {
  531. this.addTileOrDescendants(dc, tile);
  532. }
  533. }
  534. };
  535. WmtsLayer.prototype.addTileOrDescendants = function (dc, tile) {
  536. // Check if the new sub-tile fits in TileMatrix ranges
  537. if (tile.column >= tile.tileMatrix.matrixWidth) {
  538. tile.column = tile.column - tile.tileMatrix.matrixWidth;
  539. }
  540. if (tile.column < 0) {
  541. tile.column = tile.column + tile.tileMatrix.matrixWidth;
  542. }
  543. if (this.tileMeetsRenderingCriteria(dc, tile)) {
  544. this.addTile(dc, tile);
  545. return;
  546. }
  547. var ancestorTile = null;
  548. try {
  549. if (this.isTileTextureInMemory(dc, tile) || tile.tileMatrix.levelNumber === 0) {
  550. ancestorTile = this.currentAncestorTile;
  551. this.currentAncestorTile = tile;
  552. }
  553. var nextLevel = this.tileMatrixSet.tileMatrix[tile.tileMatrix.levelNumber + 1],
  554. subTiles = tile.subdivideToCache(nextLevel, this, this.tileCache);
  555. for (var i = 0, len = subTiles.length; i < len; i++) {
  556. var child = subTiles[i];
  557. child.update(dc);
  558. if (this.sector.intersects(child.sector) && this.isTileVisible(dc, child)) {
  559. this.addTileOrDescendants(dc, child);
  560. }
  561. }
  562. } finally {
  563. if (ancestorTile) {
  564. this.currentAncestorTile = ancestorTile;
  565. }
  566. }
  567. };
  568. WmtsLayer.prototype.addTile = function (dc, tile) {
  569. tile.fallbackTile = null;
  570. var texture = dc.gpuResourceCache.resourceForKey(tile.imagePath);
  571. if (texture) {
  572. this.currentTiles.push(tile);
  573. // If the tile's texture has expired, cause it to be re-retrieved. Note that the current,
  574. // expired texture is still used until the updated one arrives.
  575. if (this.expiration && this.isTextureExpired(texture)) {
  576. this.retrieveTileImage(dc, tile);
  577. }
  578. return;
  579. }
  580. this.retrieveTileImage(dc, tile);
  581. if (this.currentAncestorTile) {
  582. if (this.isTileTextureInMemory(dc, this.currentAncestorTile)) {
  583. this.currentTiles.push(this.currentAncestorTile);
  584. }
  585. }
  586. };
  587. WmtsLayer.prototype.isTextureExpired = function (texture) {
  588. return this.expiration && (texture.creationTime.getTime() <= this.expiration.getTime());
  589. };
  590. WmtsLayer.prototype.isTileTextureInMemory = function (dc, tile) {
  591. return dc.gpuResourceCache.containsResource(tile.imagePath);
  592. };
  593. WmtsLayer.prototype.tileMeetsRenderingCriteria = function (dc, tile) {
  594. var s = this.detailControl;
  595. if (tile.sector.minLatitude >= 75 || tile.sector.maxLatitude <= -75) {
  596. s *= 1.2;
  597. }
  598. return tile.tileMatrix.levelNumber === (this.tileMatrixSet.tileMatrix.length - 1) || !tile.mustSubdivide(dc, s);
  599. };
  600. WmtsLayer.prototype.retrieveTileImage = function (dc, tile) {
  601. if (this.currentRetrievals.indexOf(tile.imagePath) < 0) {
  602. if (this.currentRetrievals.length > this.retrievalQueueSize) {
  603. return;
  604. }
  605. if (this.absentResourceList.isResourceAbsent(tile.imagePath)) {
  606. return;
  607. }
  608. var url = this.resourceUrlForTile(tile, this.imageFormat),
  609. image = new Image(),
  610. imagePath = tile.imagePath,
  611. cache = dc.gpuResourceCache,
  612. canvas = dc.currentGlContext.canvas,
  613. layer = this;
  614. if (!url) {
  615. this.currentTilesInvalid = true;
  616. return;
  617. }
  618. image.onload = function () {
  619. Logger.log(Logger.LEVEL_INFO, "Image retrieval succeeded: " + url);
  620. var texture = layer.createTexture(dc, tile, image);
  621. layer.removeFromCurrentRetrievals(imagePath);
  622. if (texture) {
  623. cache.putResource(imagePath, texture, texture.size);
  624. layer.currentTilesInvalid = true;
  625. layer.absentResourceList.unmarkResourceAbsent(imagePath);
  626. // Send an event to request a redraw.
  627. var e = document.createEvent('Event');
  628. e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
  629. canvas.dispatchEvent(e);
  630. }
  631. };
  632. image.onerror = function () {
  633. layer.removeFromCurrentRetrievals(imagePath);
  634. layer.absentResourceList.markResourceAbsent(imagePath);
  635. Logger.log(Logger.LEVEL_WARNING, "Image retrieval failed: " + url);
  636. };
  637. this.currentRetrievals.push(imagePath);
  638. image.crossOrigin = 'anonymous';
  639. image.src = url;
  640. }
  641. };
  642. WmtsLayer.prototype.resourceUrlForTile = function (tile, imageFormat) {
  643. var url;
  644. if (this.resourceUrl) {
  645. url = this.resourceUrl.replace("{Style}", this.styleIdentifier).replace("{TileMatrixSet}", this.tileMatrixSet.identifier).replace("{TileMatrix}", tile.tileMatrix.identifier).replace("{TileCol}", tile.column).replace("{TileRow}", tile.row);
  646. if (this.timeString) {
  647. url = url.replace("{Time}", this.timeString);
  648. }
  649. } else {
  650. url = this.serviceUrl + "service=WMTS&request=GetTile&version=1.0.0";
  651. url += "&Layer=" + this.layerIdentifier;
  652. if (this.styleIdentifier) {
  653. url += "&Style=" + this.styleIdentifier;
  654. }
  655. url += "&Format=" + imageFormat;
  656. if (this.timeString) {
  657. url += "&Time=" + this.timeString;
  658. }
  659. url += "&TileMatrixSet=" + this.tileMatrixSet.identifier;
  660. url += "&TileMatrix=" + tile.tileMatrix.identifier;
  661. url += "&TileRow=" + tile.row;
  662. url += "&TileCol=" + tile.column;
  663. }
  664. return url;
  665. };
  666. WmtsLayer.prototype.removeFromCurrentRetrievals = function (imagePath) {
  667. var index = this.currentRetrievals.indexOf(imagePath);
  668. if (index > -1) {
  669. this.currentRetrievals.splice(index, 1);
  670. }
  671. };
  672. WmtsLayer.prototype.createTopLevelTiles = function (dc) {
  673. var tileMatrix = this.tileMatrixSet.tileMatrix[0];
  674. this.topLevelTiles = [];
  675. for (var j = 0; j < tileMatrix.matrixHeight; j++) {
  676. for (var i = 0; i < tileMatrix.matrixWidth; i++) {
  677. this.topLevelTiles.push(this.createTile(tileMatrix, j, i));
  678. }
  679. }
  680. };
  681. WmtsLayer.prototype.createTile = function (tileMatrix, row, column) {
  682. if (WmtsLayer.isEpsg4326Crs(this.tileMatrixSet.supportedCRS)) {
  683. return this.createTile4326(tileMatrix, row, column);
  684. } else if (WmtsLayer.isEpsg3857Crs(this.tileMatrixSet.supportedCRS)) {
  685. return this.createTile3857(tileMatrix, row, column);
  686. } else if (WmtsLayer.isOGCCrs84(this.tileMatrixSet.supportedCRS)) {
  687. return this.createTileCrs84(tileMatrix, row, column);
  688. }
  689. };
  690. WmtsLayer.prototype.createTileCrs84 = function (tileMatrix, row, column) {
  691. var tileDeltaLat = this.sector.deltaLatitude() / tileMatrix.matrixHeight,
  692. tileDeltaLon = this.sector.deltaLongitude() / tileMatrix.matrixWidth,
  693. maxLat = tileMatrix.topLeftCorner[1] - row * tileDeltaLat,
  694. minLat = maxLat - tileDeltaLat,
  695. minLon = tileMatrix.topLeftCorner[0] + tileDeltaLon * column,
  696. maxLon = minLon + tileDeltaLon;
  697. var sector = new Sector(minLat, maxLat, minLon, maxLon);
  698. return this.makeTile(sector, tileMatrix, row, column);
  699. };
  700. WmtsLayer.prototype.createTile4326 = function (tileMatrix, row, column) {
  701. var tileDeltaLat = this.sector.deltaLatitude() / tileMatrix.matrixHeight,
  702. tileDeltaLon = this.sector.deltaLongitude() / tileMatrix.matrixWidth,
  703. maxLat = tileMatrix.topLeftCorner[0] - row * tileDeltaLat,
  704. minLat = maxLat - tileDeltaLat,
  705. minLon = tileMatrix.topLeftCorner[1] + tileDeltaLon * column,
  706. maxLon = minLon + tileDeltaLon;
  707. var sector = new Sector(minLat, maxLat, minLon, maxLon);
  708. return this.makeTile(sector, tileMatrix, row, column);
  709. };
  710. WmtsLayer.prototype.createTile3857 = function (tileMatrix, row, column) {
  711. if (!tileMatrix.mapWidth) {
  712. this.computeTileMatrixValues3857(tileMatrix);
  713. }
  714. var swX = WWMath.clamp(column * tileMatrix.tileWidth - 0.5, 0, tileMatrix.mapWidth),
  715. neY = WWMath.clamp(row * tileMatrix.tileHeight - 0.5, 0, tileMatrix.mapHeight),
  716. neX = WWMath.clamp(swX + (tileMatrix.tileWidth) + 0.5, 0, tileMatrix.mapWidth),
  717. swY = WWMath.clamp(neY + (tileMatrix.tileHeight) + 0.5, 0, tileMatrix.mapHeight),
  718. x, y, swLat, swLon, neLat, neLon;
  719. x = swX / tileMatrix.mapWidth;
  720. y = swY / tileMatrix.mapHeight;
  721. swLon = tileMatrix.topLeftCorner[0] + x * tileMatrix.tileMatrixDeltaX;
  722. swLat = tileMatrix.topLeftCorner[1] - y * tileMatrix.tileMatrixDeltaY;
  723. var swDegrees = WWMath.epsg3857ToEpsg4326(swLon, swLat);
  724. x = neX / tileMatrix.mapWidth;
  725. y = neY / tileMatrix.mapHeight;
  726. neLon = tileMatrix.topLeftCorner[0] + x * tileMatrix.tileMatrixDeltaX;
  727. neLat = tileMatrix.topLeftCorner[1] - y * tileMatrix.tileMatrixDeltaY;
  728. var neDegrees = WWMath.epsg3857ToEpsg4326(neLon, neLat);
  729. var sector = new Sector(swDegrees[0], neDegrees[0], swDegrees[1], neDegrees[1]);
  730. return this.makeTile(sector, tileMatrix, row, column);
  731. };
  732. WmtsLayer.prototype.computeTileMatrixValues3857 = function (tileMatrix) {
  733. var pixelSpan = tileMatrix.scaleDenominator * 0.28e-3,
  734. tileSpanX = tileMatrix.tileWidth * pixelSpan,
  735. tileSpanY = tileMatrix.tileHeight * pixelSpan,
  736. tileMatrixMaxX = tileMatrix.topLeftCorner[0] + tileSpanX * tileMatrix.matrixWidth,
  737. tileMatrixMinY = tileMatrix.topLeftCorner[1] - tileSpanY * tileMatrix.matrixHeight,
  738. bottomRightCorner = [tileMatrixMaxX, tileMatrixMinY],
  739. topLeftCorner = tileMatrix.topLeftCorner;
  740. tileMatrix.tileMatrixDeltaX = bottomRightCorner[0] - topLeftCorner[0];
  741. tileMatrix.tileMatrixDeltaY = topLeftCorner[1] - bottomRightCorner[1];
  742. tileMatrix.mapWidth = tileMatrix.tileWidth * tileMatrix.matrixWidth;
  743. tileMatrix.mapHeight = tileMatrix.tileHeight * tileMatrix.matrixHeight;
  744. };
  745. WmtsLayer.prototype.makeTile = function (sector, tileMatrix, row, column) {
  746. var path = this.cachePath + "-layer/" + tileMatrix.identifier + "/" + row + "/" + column + "."
  747. + WWUtil.suffixForMimeType(this.imageFormat);
  748. return new WmtsLayerTile(sector, tileMatrix, row, column, path);
  749. };
  750. WmtsLayer.prototype.createTexture = function (dc, tile, image) {
  751. if (WmtsLayer.isEpsg4326Crs(this.tileMatrixSet.supportedCRS)) {
  752. return new Texture(dc.currentGlContext, image);
  753. } else if (WmtsLayer.isEpsg3857Crs(this.tileMatrixSet.supportedCRS)) {
  754. return this.createTexture3857(dc, tile, image);
  755. } else if (WmtsLayer.isOGCCrs84(this.tileMatrixSet.supportedCRS)) {
  756. return new Texture(dc.currentGlContext, image);
  757. }
  758. };
  759. WmtsLayer.prototype.createTexture3857 = function (dc, tile, image) {
  760. if (!this.destCanvas) {
  761. // Create a canvas we can use when unprojecting retrieved images.
  762. this.destCanvas = document.createElement("canvas");
  763. this.destContext = this.destCanvas.getContext("2d");
  764. }
  765. var srcCanvas = dc.canvas2D,
  766. srcContext = dc.ctx2D,
  767. srcImageData,
  768. destCanvas = this.destCanvas,
  769. destContext = this.destContext,
  770. destImageData = destContext.createImageData(image.width, image.height),
  771. sector = tile.sector,
  772. tMin = WWMath.gudermannianInverse(sector.minLatitude),
  773. tMax = WWMath.gudermannianInverse(sector.maxLatitude),
  774. lat, g, srcRow, kSrc, kDest, sy, dy;
  775. srcCanvas.width = image.width;
  776. srcCanvas.height = image.height;
  777. destCanvas.width = image.width;
  778. destCanvas.height = image.height;
  779. // Draw the original image to a canvas so image data can be had for it.
  780. srcContext.drawImage(image, 0, 0, image.width, image.height);
  781. srcImageData = srcContext.getImageData(0, 0, image.width, image.height);
  782. // Unproject the retrieved image.
  783. for (var n = 0; n < 1; n++) {
  784. for (var y = 0; y < image.height; y++) {
  785. sy = 1 - y / (image.height - 1);
  786. lat = sy * sector.deltaLatitude() + sector.minLatitude;
  787. g = WWMath.gudermannianInverse(lat);
  788. dy = 1 - (g - tMin) / (tMax - tMin);
  789. dy = WWMath.clamp(dy, 0, 1);
  790. srcRow = Math.floor(dy * (image.height - 1));
  791. for (var x = 0; x < image.width; x++) {
  792. kSrc = 4 * (x + srcRow * image.width);
  793. kDest = 4 * (x + y * image.width);
  794. destImageData.data[kDest] = srcImageData.data[kSrc];
  795. destImageData.data[kDest + 1] = srcImageData.data[kSrc + 1];
  796. destImageData.data[kDest + 2] = srcImageData.data[kSrc + 2];
  797. destImageData.data[kDest + 3] = srcImageData.data[kSrc + 3];
  798. }
  799. }
  800. }
  801. destContext.putImageData(destImageData, 0, 0);
  802. return new Texture(dc.currentGlContext, destCanvas);
  803. };
  804. WmtsLayer.isEpsg4326Crs = function (crs) {
  805. return ((crs.indexOf("EPSG") >= 0) && (crs.indexOf("4326") >= 0));
  806. };
  807. WmtsLayer.isEpsg3857Crs = function (crs) {
  808. return (crs.indexOf("EPSG") >= 0)
  809. && ((crs.indexOf("3857") >= 0) || (crs.indexOf("900913") >= 0)); // 900913 is google's 3857 alias
  810. };
  811. WmtsLayer.isOGCCrs84 = function (crs) {
  812. return (crs.indexOf("OGC") >= 0) && (crs.indexOf("CRS84") >= 0);
  813. };
  814. return WmtsLayer;
  815. });