TileMapServiceImageryProvider.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import Cartesian2 from "../Core/Cartesian2.js";
  2. import Cartographic from "../Core/Cartographic.js";
  3. import defaultValue from "../Core/defaultValue.js";
  4. import defer from "../Core/defer.js";
  5. import defined from "../Core/defined.js";
  6. import DeveloperError from "../Core/DeveloperError.js";
  7. import GeographicProjection from "../Core/GeographicProjection.js";
  8. import GeographicTilingScheme from "../Core/GeographicTilingScheme.js";
  9. import Rectangle from "../Core/Rectangle.js";
  10. import Resource from "../Core/Resource.js";
  11. import RuntimeError from "../Core/RuntimeError.js";
  12. import TileProviderError from "../Core/TileProviderError.js";
  13. import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
  14. import UrlTemplateImageryProvider from "./UrlTemplateImageryProvider.js";
  15. /**
  16. * @typedef {Object} TileMapServiceImageryProvider.ConstructorOptions
  17. *
  18. * Initialization options for the TileMapServiceImageryProvider constructor
  19. *
  20. * @property {Resource|String|Promise<Resource>|Promise<String>} [url='.'] Path to image tiles on server.
  21. * @property {String} [fileExtension='png'] The file extension for images on the server.
  22. * @property {Credit|String} [credit=''] A credit for the data source, which is displayed on the canvas.
  23. * @property {Number} [minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying
  24. * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely
  25. * to result in rendering problems.
  26. * @property {Number} [maximumLevel] The maximum level-of-detail supported by the imagery provider, or undefined if there is no limit.
  27. * @property {Rectangle} [rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image.
  28. * @property {TilingScheme} [tilingScheme] The tiling scheme specifying how the ellipsoidal
  29. * surface is broken into tiles. If this parameter is not provided, a {@link WebMercatorTilingScheme}
  30. * is used.
  31. * @property {Ellipsoid} [ellipsoid] The ellipsoid. If the tilingScheme is specified,
  32. * this parameter is ignored and the tiling scheme's ellipsoid is used instead. If neither
  33. * parameter is specified, the WGS84 ellipsoid is used.
  34. * @property {Number} [tileWidth=256] Pixel width of image tiles.
  35. * @property {Number} [tileHeight=256] Pixel height of image tiles.
  36. * @property {Boolean} [flipXY] Older versions of gdal2tiles.py flipped X and Y values in tilemapresource.xml.
  37. * Specifying this option will do the same, allowing for loading of these incorrect tilesets.
  38. */
  39. /**
  40. * An imagery provider that provides tiled imagery as generated by
  41. * {@link http://www.maptiler.org/|MapTiler}, {@link http://www.klokan.cz/projects/gdal2tiles/|GDAL2Tiles}, etc.
  42. *
  43. * @alias TileMapServiceImageryProvider
  44. * @constructor
  45. * @extends UrlTemplateImageryProvider
  46. *
  47. * @param {TileMapServiceImageryProvider.ConstructorOptions} options Object describing initialization options
  48. *
  49. * @see ArcGisMapServerImageryProvider
  50. * @see BingMapsImageryProvider
  51. * @see GoogleEarthEnterpriseMapsProvider
  52. * @see OpenStreetMapImageryProvider
  53. * @see SingleTileImageryProvider
  54. * @see WebMapServiceImageryProvider
  55. * @see WebMapTileServiceImageryProvider
  56. * @see UrlTemplateImageryProvider
  57. *
  58. * @example
  59. * const tms = new Cesium.TileMapServiceImageryProvider({
  60. * url : '../images/cesium_maptiler/Cesium_Logo_Color',
  61. * fileExtension: 'png',
  62. * maximumLevel: 4,
  63. * rectangle: new Cesium.Rectangle(
  64. * Cesium.Math.toRadians(-120.0),
  65. * Cesium.Math.toRadians(20.0),
  66. * Cesium.Math.toRadians(-60.0),
  67. * Cesium.Math.toRadians(40.0))
  68. * });
  69. */
  70. function TileMapServiceImageryProvider(options) {
  71. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  72. //>>includeStart('debug', pragmas.debug);
  73. if (!defined(options.url)) {
  74. throw new DeveloperError("options.url is required.");
  75. }
  76. //>>includeEnd('debug');
  77. const deferred = defer();
  78. UrlTemplateImageryProvider.call(this, deferred.promise);
  79. this._tmsResource = undefined;
  80. this._xmlResource = undefined;
  81. this._options = options;
  82. this._deferred = deferred;
  83. this._metadataError = undefined;
  84. this._metadataSuccess = this._metadataSuccess.bind(this);
  85. this._metadataFailure = this._metadataFailure.bind(this);
  86. this._requestMetadata = this._requestMetadata.bind(this);
  87. let resource;
  88. const that = this;
  89. Promise.resolve(options.url)
  90. .then(function (url) {
  91. resource = Resource.createIfNeeded(url);
  92. resource.appendForwardSlash();
  93. that._tmsResource = resource;
  94. that._xmlResource = resource.getDerivedResource({
  95. url: "tilemapresource.xml",
  96. });
  97. that._requestMetadata();
  98. })
  99. .catch(function (e) {
  100. deferred.reject(e);
  101. });
  102. }
  103. if (defined(Object.create)) {
  104. TileMapServiceImageryProvider.prototype = Object.create(
  105. UrlTemplateImageryProvider.prototype
  106. );
  107. TileMapServiceImageryProvider.prototype.constructor = TileMapServiceImageryProvider;
  108. }
  109. TileMapServiceImageryProvider.prototype._requestMetadata = function () {
  110. // Try to load remaining parameters from XML
  111. this._xmlResource
  112. .fetchXML()
  113. .then(this._metadataSuccess)
  114. .catch(this._metadataFailure);
  115. };
  116. /**
  117. * Mutates the properties of a given rectangle so it does not extend outside of the given tiling scheme's rectangle
  118. * @private
  119. */
  120. function confineRectangleToTilingScheme(rectangle, tilingScheme) {
  121. if (rectangle.west < tilingScheme.rectangle.west) {
  122. rectangle.west = tilingScheme.rectangle.west;
  123. }
  124. if (rectangle.east > tilingScheme.rectangle.east) {
  125. rectangle.east = tilingScheme.rectangle.east;
  126. }
  127. if (rectangle.south < tilingScheme.rectangle.south) {
  128. rectangle.south = tilingScheme.rectangle.south;
  129. }
  130. if (rectangle.north > tilingScheme.rectangle.north) {
  131. rectangle.north = tilingScheme.rectangle.north;
  132. }
  133. return rectangle;
  134. }
  135. function calculateSafeMinimumDetailLevel(
  136. tilingScheme,
  137. rectangle,
  138. minimumLevel
  139. ) {
  140. // Check the number of tiles at the minimum level. If it's more than four,
  141. // try requesting the lower levels anyway, because starting at the higher minimum
  142. // level will cause too many tiles to be downloaded and rendered.
  143. const swTile = tilingScheme.positionToTileXY(
  144. Rectangle.southwest(rectangle),
  145. minimumLevel
  146. );
  147. const neTile = tilingScheme.positionToTileXY(
  148. Rectangle.northeast(rectangle),
  149. minimumLevel
  150. );
  151. const tileCount =
  152. (Math.abs(neTile.x - swTile.x) + 1) * (Math.abs(neTile.y - swTile.y) + 1);
  153. if (tileCount > 4) {
  154. return 0;
  155. }
  156. return minimumLevel;
  157. }
  158. TileMapServiceImageryProvider.prototype._metadataSuccess = function (xml) {
  159. const tileFormatRegex = /tileformat/i;
  160. const tileSetRegex = /tileset/i;
  161. const tileSetsRegex = /tilesets/i;
  162. const bboxRegex = /boundingbox/i;
  163. let format, bbox, tilesets;
  164. const tilesetsList = []; //list of TileSets
  165. const xmlResource = this._xmlResource;
  166. let metadataError = this._metadataError;
  167. const deferred = this._deferred;
  168. const requestMetadata = this._requestMetadata;
  169. // Allowing options properties (already copied to that) to override XML values
  170. // Iterate XML Document nodes for properties
  171. const nodeList = xml.childNodes[0].childNodes;
  172. for (let i = 0; i < nodeList.length; i++) {
  173. if (tileFormatRegex.test(nodeList.item(i).nodeName)) {
  174. format = nodeList.item(i);
  175. } else if (tileSetsRegex.test(nodeList.item(i).nodeName)) {
  176. tilesets = nodeList.item(i); // Node list of TileSets
  177. const tileSetNodes = nodeList.item(i).childNodes;
  178. // Iterate the nodes to find all TileSets
  179. for (let j = 0; j < tileSetNodes.length; j++) {
  180. if (tileSetRegex.test(tileSetNodes.item(j).nodeName)) {
  181. // Add them to tilesets list
  182. tilesetsList.push(tileSetNodes.item(j));
  183. }
  184. }
  185. } else if (bboxRegex.test(nodeList.item(i).nodeName)) {
  186. bbox = nodeList.item(i);
  187. }
  188. }
  189. let message;
  190. if (!defined(tilesets) || !defined(bbox)) {
  191. message = `Unable to find expected tilesets or bbox attributes in ${xmlResource.url}.`;
  192. metadataError = TileProviderError.handleError(
  193. metadataError,
  194. this,
  195. this.errorEvent,
  196. message,
  197. undefined,
  198. undefined,
  199. undefined,
  200. requestMetadata
  201. );
  202. if (!metadataError.retry) {
  203. deferred.reject(new RuntimeError(message));
  204. }
  205. this._metadataError = metadataError;
  206. return;
  207. }
  208. const options = this._options;
  209. const fileExtension = defaultValue(
  210. options.fileExtension,
  211. format.getAttribute("extension")
  212. );
  213. const tileWidth = defaultValue(
  214. options.tileWidth,
  215. parseInt(format.getAttribute("width"), 10)
  216. );
  217. const tileHeight = defaultValue(
  218. options.tileHeight,
  219. parseInt(format.getAttribute("height"), 10)
  220. );
  221. let minimumLevel = defaultValue(
  222. options.minimumLevel,
  223. parseInt(tilesetsList[0].getAttribute("order"), 10)
  224. );
  225. const maximumLevel = defaultValue(
  226. options.maximumLevel,
  227. parseInt(tilesetsList[tilesetsList.length - 1].getAttribute("order"), 10)
  228. );
  229. const tilingSchemeName = tilesets.getAttribute("profile");
  230. let tilingScheme = options.tilingScheme;
  231. if (!defined(tilingScheme)) {
  232. if (
  233. tilingSchemeName === "geodetic" ||
  234. tilingSchemeName === "global-geodetic"
  235. ) {
  236. tilingScheme = new GeographicTilingScheme({
  237. ellipsoid: options.ellipsoid,
  238. });
  239. } else if (
  240. tilingSchemeName === "mercator" ||
  241. tilingSchemeName === "global-mercator"
  242. ) {
  243. tilingScheme = new WebMercatorTilingScheme({
  244. ellipsoid: options.ellipsoid,
  245. });
  246. } else {
  247. message = `${xmlResource.url}specifies an unsupported profile attribute, ${tilingSchemeName}.`;
  248. metadataError = TileProviderError.handleError(
  249. metadataError,
  250. this,
  251. this.errorEvent,
  252. message,
  253. undefined,
  254. undefined,
  255. undefined,
  256. requestMetadata
  257. );
  258. if (!metadataError.retry) {
  259. deferred.reject(new RuntimeError(message));
  260. }
  261. this._metadataError = metadataError;
  262. return;
  263. }
  264. }
  265. // rectangle handling
  266. let rectangle = Rectangle.clone(options.rectangle);
  267. if (!defined(rectangle)) {
  268. let sw;
  269. let ne;
  270. let swXY;
  271. let neXY;
  272. // In older versions of gdal x and y values were flipped, which is why we check for an option to flip
  273. // the values here as well. Unfortunately there is no way to autodetect whether flipping is needed.
  274. const flipXY = defaultValue(options.flipXY, false);
  275. if (flipXY) {
  276. swXY = new Cartesian2(
  277. parseFloat(bbox.getAttribute("miny")),
  278. parseFloat(bbox.getAttribute("minx"))
  279. );
  280. neXY = new Cartesian2(
  281. parseFloat(bbox.getAttribute("maxy")),
  282. parseFloat(bbox.getAttribute("maxx"))
  283. );
  284. } else {
  285. swXY = new Cartesian2(
  286. parseFloat(bbox.getAttribute("minx")),
  287. parseFloat(bbox.getAttribute("miny"))
  288. );
  289. neXY = new Cartesian2(
  290. parseFloat(bbox.getAttribute("maxx")),
  291. parseFloat(bbox.getAttribute("maxy"))
  292. );
  293. }
  294. // Determine based on the profile attribute if this tileset was generated by gdal2tiles.py, which
  295. // uses 'mercator' and 'geodetic' profiles, or by a tool compliant with the TMS standard, which is
  296. // 'global-mercator' and 'global-geodetic' profiles. In the gdal2Tiles case, X and Y are always in
  297. // geodetic degrees.
  298. const isGdal2tiles =
  299. tilingSchemeName === "geodetic" || tilingSchemeName === "mercator";
  300. if (
  301. tilingScheme.projection instanceof GeographicProjection ||
  302. isGdal2tiles
  303. ) {
  304. sw = Cartographic.fromDegrees(swXY.x, swXY.y);
  305. ne = Cartographic.fromDegrees(neXY.x, neXY.y);
  306. } else {
  307. const projection = tilingScheme.projection;
  308. sw = projection.unproject(swXY);
  309. ne = projection.unproject(neXY);
  310. }
  311. rectangle = new Rectangle(
  312. sw.longitude,
  313. sw.latitude,
  314. ne.longitude,
  315. ne.latitude
  316. );
  317. }
  318. // The rectangle must not be outside the bounds allowed by the tiling scheme.
  319. rectangle = confineRectangleToTilingScheme(rectangle, tilingScheme);
  320. // clamp our minimum detail level to something that isn't going to request a ridiculous number of tiles
  321. minimumLevel = calculateSafeMinimumDetailLevel(
  322. tilingScheme,
  323. rectangle,
  324. minimumLevel
  325. );
  326. const templateResource = this._tmsResource.getDerivedResource({
  327. url: `{z}/{x}/{reverseY}.${fileExtension}`,
  328. });
  329. deferred.resolve({
  330. url: templateResource,
  331. tilingScheme: tilingScheme,
  332. rectangle: rectangle,
  333. tileWidth: tileWidth,
  334. tileHeight: tileHeight,
  335. minimumLevel: minimumLevel,
  336. maximumLevel: maximumLevel,
  337. tileDiscardPolicy: options.tileDiscardPolicy,
  338. credit: options.credit,
  339. });
  340. };
  341. TileMapServiceImageryProvider.prototype._metadataFailure = function (error) {
  342. // Can't load XML, still allow options and defaults
  343. const options = this._options;
  344. const fileExtension = defaultValue(options.fileExtension, "png");
  345. const tileWidth = defaultValue(options.tileWidth, 256);
  346. const tileHeight = defaultValue(options.tileHeight, 256);
  347. const maximumLevel = options.maximumLevel;
  348. const tilingScheme = defined(options.tilingScheme)
  349. ? options.tilingScheme
  350. : new WebMercatorTilingScheme({ ellipsoid: options.ellipsoid });
  351. let rectangle = defaultValue(options.rectangle, tilingScheme.rectangle);
  352. // The rectangle must not be outside the bounds allowed by the tiling scheme.
  353. rectangle = confineRectangleToTilingScheme(rectangle, tilingScheme);
  354. // make sure we use a safe minimum detail level, so we don't request a ridiculous number of tiles
  355. const minimumLevel = calculateSafeMinimumDetailLevel(
  356. tilingScheme,
  357. rectangle,
  358. options.minimumLevel
  359. );
  360. const templateResource = this._tmsResource.getDerivedResource({
  361. url: `{z}/{x}/{reverseY}.${fileExtension}`,
  362. });
  363. this._deferred.resolve({
  364. url: templateResource,
  365. tilingScheme: tilingScheme,
  366. rectangle: rectangle,
  367. tileWidth: tileWidth,
  368. tileHeight: tileHeight,
  369. minimumLevel: minimumLevel,
  370. maximumLevel: maximumLevel,
  371. tileDiscardPolicy: options.tileDiscardPolicy,
  372. credit: options.credit,
  373. });
  374. };
  375. export default TileMapServiceImageryProvider;