TileMapServiceImageryProvider.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import Cartesian2 from "../Core/Cartesian2.js";
  2. import Cartographic from "../Core/Cartographic.js";
  3. import defaultValue from "../Core/defaultValue.js";
  4. import defined from "../Core/defined.js";
  5. import DeveloperError from "../Core/DeveloperError.js";
  6. import GeographicProjection from "../Core/GeographicProjection.js";
  7. import GeographicTilingScheme from "../Core/GeographicTilingScheme.js";
  8. import Rectangle from "../Core/Rectangle.js";
  9. import Resource from "../Core/Resource.js";
  10. import RuntimeError from "../Core/RuntimeError.js";
  11. import TileProviderError from "../Core/TileProviderError.js";
  12. import WebMercatorTilingScheme from "../Core/WebMercatorTilingScheme.js";
  13. import when from "../ThirdParty/when.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. * var 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. var deferred = when.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. var resource;
  88. var that = this;
  89. when(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. .otherwise(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. .otherwise(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. var swTile = tilingScheme.positionToTileXY(
  144. Rectangle.southwest(rectangle),
  145. minimumLevel
  146. );
  147. var neTile = tilingScheme.positionToTileXY(
  148. Rectangle.northeast(rectangle),
  149. minimumLevel
  150. );
  151. var 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. var tileFormatRegex = /tileformat/i;
  160. var tileSetRegex = /tileset/i;
  161. var tileSetsRegex = /tilesets/i;
  162. var bboxRegex = /boundingbox/i;
  163. var format, bbox, tilesets;
  164. var tilesetsList = []; //list of TileSets
  165. var xmlResource = this._xmlResource;
  166. var metadataError = this._metadataError;
  167. var deferred = this._deferred;
  168. var requestMetadata = this._requestMetadata;
  169. // Allowing options properties (already copied to that) to override XML values
  170. // Iterate XML Document nodes for properties
  171. var nodeList = xml.childNodes[0].childNodes;
  172. for (var 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. var tileSetNodes = nodeList.item(i).childNodes;
  178. // Iterate the nodes to find all TileSets
  179. for (var 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. var message;
  190. if (!defined(tilesets) || !defined(bbox)) {
  191. message =
  192. "Unable to find expected tilesets or bbox attributes in " +
  193. xmlResource.url +
  194. ".";
  195. metadataError = TileProviderError.handleError(
  196. metadataError,
  197. this,
  198. this.errorEvent,
  199. message,
  200. undefined,
  201. undefined,
  202. undefined,
  203. requestMetadata
  204. );
  205. if (!metadataError.retry) {
  206. deferred.reject(new RuntimeError(message));
  207. }
  208. this._metadataError = metadataError;
  209. return;
  210. }
  211. var options = this._options;
  212. var fileExtension = defaultValue(
  213. options.fileExtension,
  214. format.getAttribute("extension")
  215. );
  216. var tileWidth = defaultValue(
  217. options.tileWidth,
  218. parseInt(format.getAttribute("width"), 10)
  219. );
  220. var tileHeight = defaultValue(
  221. options.tileHeight,
  222. parseInt(format.getAttribute("height"), 10)
  223. );
  224. var minimumLevel = defaultValue(
  225. options.minimumLevel,
  226. parseInt(tilesetsList[0].getAttribute("order"), 10)
  227. );
  228. var maximumLevel = defaultValue(
  229. options.maximumLevel,
  230. parseInt(tilesetsList[tilesetsList.length - 1].getAttribute("order"), 10)
  231. );
  232. var tilingSchemeName = tilesets.getAttribute("profile");
  233. var tilingScheme = options.tilingScheme;
  234. if (!defined(tilingScheme)) {
  235. if (
  236. tilingSchemeName === "geodetic" ||
  237. tilingSchemeName === "global-geodetic"
  238. ) {
  239. tilingScheme = new GeographicTilingScheme({
  240. ellipsoid: options.ellipsoid,
  241. });
  242. } else if (
  243. tilingSchemeName === "mercator" ||
  244. tilingSchemeName === "global-mercator"
  245. ) {
  246. tilingScheme = new WebMercatorTilingScheme({
  247. ellipsoid: options.ellipsoid,
  248. });
  249. } else {
  250. message =
  251. xmlResource.url +
  252. "specifies an unsupported profile attribute, " +
  253. tilingSchemeName +
  254. ".";
  255. metadataError = TileProviderError.handleError(
  256. metadataError,
  257. this,
  258. this.errorEvent,
  259. message,
  260. undefined,
  261. undefined,
  262. undefined,
  263. requestMetadata
  264. );
  265. if (!metadataError.retry) {
  266. deferred.reject(new RuntimeError(message));
  267. }
  268. this._metadataError = metadataError;
  269. return;
  270. }
  271. }
  272. // rectangle handling
  273. var rectangle = Rectangle.clone(options.rectangle);
  274. if (!defined(rectangle)) {
  275. var sw;
  276. var ne;
  277. var swXY;
  278. var neXY;
  279. // In older versions of gdal x and y values were flipped, which is why we check for an option to flip
  280. // the values here as well. Unfortunately there is no way to autodetect whether flipping is needed.
  281. var flipXY = defaultValue(options.flipXY, false);
  282. if (flipXY) {
  283. swXY = new Cartesian2(
  284. parseFloat(bbox.getAttribute("miny")),
  285. parseFloat(bbox.getAttribute("minx"))
  286. );
  287. neXY = new Cartesian2(
  288. parseFloat(bbox.getAttribute("maxy")),
  289. parseFloat(bbox.getAttribute("maxx"))
  290. );
  291. } else {
  292. swXY = new Cartesian2(
  293. parseFloat(bbox.getAttribute("minx")),
  294. parseFloat(bbox.getAttribute("miny"))
  295. );
  296. neXY = new Cartesian2(
  297. parseFloat(bbox.getAttribute("maxx")),
  298. parseFloat(bbox.getAttribute("maxy"))
  299. );
  300. }
  301. // Determine based on the profile attribute if this tileset was generated by gdal2tiles.py, which
  302. // uses 'mercator' and 'geodetic' profiles, or by a tool compliant with the TMS standard, which is
  303. // 'global-mercator' and 'global-geodetic' profiles. In the gdal2Tiles case, X and Y are always in
  304. // geodetic degrees.
  305. var isGdal2tiles =
  306. tilingSchemeName === "geodetic" || tilingSchemeName === "mercator";
  307. if (
  308. tilingScheme.projection instanceof GeographicProjection ||
  309. isGdal2tiles
  310. ) {
  311. sw = Cartographic.fromDegrees(swXY.x, swXY.y);
  312. ne = Cartographic.fromDegrees(neXY.x, neXY.y);
  313. } else {
  314. var projection = tilingScheme.projection;
  315. sw = projection.unproject(swXY);
  316. ne = projection.unproject(neXY);
  317. }
  318. rectangle = new Rectangle(
  319. sw.longitude,
  320. sw.latitude,
  321. ne.longitude,
  322. ne.latitude
  323. );
  324. }
  325. // The rectangle must not be outside the bounds allowed by the tiling scheme.
  326. rectangle = confineRectangleToTilingScheme(rectangle, tilingScheme);
  327. // clamp our minimum detail level to something that isn't going to request a ridiculous number of tiles
  328. minimumLevel = calculateSafeMinimumDetailLevel(
  329. tilingScheme,
  330. rectangle,
  331. minimumLevel
  332. );
  333. var templateResource = this._tmsResource.getDerivedResource({
  334. url: "{z}/{x}/{reverseY}." + fileExtension,
  335. });
  336. deferred.resolve({
  337. url: templateResource,
  338. tilingScheme: tilingScheme,
  339. rectangle: rectangle,
  340. tileWidth: tileWidth,
  341. tileHeight: tileHeight,
  342. minimumLevel: minimumLevel,
  343. maximumLevel: maximumLevel,
  344. tileDiscardPolicy: options.tileDiscardPolicy,
  345. credit: options.credit,
  346. });
  347. };
  348. TileMapServiceImageryProvider.prototype._metadataFailure = function (error) {
  349. // Can't load XML, still allow options and defaults
  350. var options = this._options;
  351. var fileExtension = defaultValue(options.fileExtension, "png");
  352. var tileWidth = defaultValue(options.tileWidth, 256);
  353. var tileHeight = defaultValue(options.tileHeight, 256);
  354. var maximumLevel = options.maximumLevel;
  355. var tilingScheme = defined(options.tilingScheme)
  356. ? options.tilingScheme
  357. : new WebMercatorTilingScheme({ ellipsoid: options.ellipsoid });
  358. var rectangle = defaultValue(options.rectangle, tilingScheme.rectangle);
  359. // The rectangle must not be outside the bounds allowed by the tiling scheme.
  360. rectangle = confineRectangleToTilingScheme(rectangle, tilingScheme);
  361. // make sure we use a safe minimum detail level, so we don't request a ridiculous number of tiles
  362. var minimumLevel = calculateSafeMinimumDetailLevel(
  363. tilingScheme,
  364. rectangle,
  365. options.maximumLevel
  366. );
  367. var templateResource = this._tmsResource.getDerivedResource({
  368. url: "{z}/{x}/{reverseY}." + fileExtension,
  369. });
  370. this._deferred.resolve({
  371. url: templateResource,
  372. tilingScheme: tilingScheme,
  373. rectangle: rectangle,
  374. tileWidth: tileWidth,
  375. tileHeight: tileHeight,
  376. minimumLevel: minimumLevel,
  377. maximumLevel: maximumLevel,
  378. tileDiscardPolicy: options.tileDiscardPolicy,
  379. credit: options.credit,
  380. });
  381. };
  382. export default TileMapServiceImageryProvider;