ArcGISCache.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for
  2. * full list of contributors). Published under the 2-clause BSD license.
  3. * See license.txt in the OpenLayers distribution or repository for the
  4. * full text of the license. */
  5. /**
  6. * @requires OpenLayers/Layer/XYZ.js
  7. */
  8. /**
  9. * Class: OpenLayers.Layer.ArcGISCache
  10. * Layer for accessing cached map tiles from an ArcGIS Server style mapcache.
  11. * Tile must already be cached for this layer to access it. This does not require
  12. * ArcGIS Server itself.
  13. *
  14. * A few attempts have been made at this kind of layer before. See
  15. * http://trac.osgeo.org/openlayers/ticket/1967
  16. * and
  17. * http://trac.osgeo.org/openlayers/browser/sandbox/tschaub/arcgiscache/lib/OpenLayers/Layer/ArcGISCache.js
  18. *
  19. * Typically the problem encountered is that the tiles seem to "jump around".
  20. * This is due to the fact that the actual max extent for the tiles on AGS layers
  21. * changes at each zoom level due to the way these caches are constructed.
  22. * We have attempted to use the resolutions, tile size, and tile origin
  23. * from the cache meta data to make the appropriate changes to the max extent
  24. * of the tile to compensate for this behavior. This must be done as zoom levels change
  25. * and before tiles are requested, which is why methods from base classes are overridden.
  26. *
  27. * For reference, you can access mapcache meta data in two ways. For accessing a
  28. * mapcache through ArcGIS Server, you can simply go to the landing page for the
  29. * layer. (ie. http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer)
  30. * For accessing it directly through HTTP, there should always be a conf.xml file
  31. * in the root directory.
  32. * (ie. http://serverx.esri.com/arcgiscache/DG_County_roads_yesA_backgroundDark/Layers/conf.xml)
  33. *
  34. *Inherits from:
  35. * - <OpenLayers.Layer.XYZ>
  36. */
  37. OpenLayers.Layer.ArcGISCache = OpenLayers.Class(OpenLayers.Layer.XYZ, {
  38. /**
  39. * APIProperty: url
  40. * {String | Array} The base URL for the layer cache. You can also
  41. * provide a list of URL strings for the layer if your cache is
  42. * available from multiple origins. This must be set before the layer
  43. * is drawn.
  44. */
  45. url: null,
  46. /**
  47. * APIProperty: tileOrigin
  48. * {<OpenLayers.LonLat>} The location of the tile origin for the cache.
  49. * An ArcGIS cache has it's origin at the upper-left (lowest x value
  50. * and highest y value of the coordinate system). The units for the
  51. * tile origin should be the same as the units for the cached data.
  52. */
  53. tileOrigin: null,
  54. /**
  55. * APIProperty: tileSize
  56. * {<OpenLayers.Size>} This size of each tile. Defaults to 256 by 256 pixels.
  57. */
  58. tileSize: new OpenLayers.Size(256, 256),
  59. /**
  60. * APIProperty: useAGS
  61. * {Boolean} Indicates if we are going to be accessing the ArcGIS Server (AGS)
  62. * cache via an AGS MapServer or directly through HTTP. When accessing via
  63. * AGS the path structure uses a standard z/y/x structure. But AGS actually
  64. * stores the tile images on disk using a hex based folder structure that looks
  65. * like "http://example.com/mylayer/L00/R00000000/C00000000.png". Learn more
  66. * about this here:
  67. * http://blogs.esri.com/Support/blogs/mappingcenter/archive/2010/08/20/Checking-Your-Local-Cache-Folders.aspx
  68. * Defaults to true;
  69. */
  70. useArcGISServer: true,
  71. /**
  72. * APIProperty: type
  73. * {String} Image type for the layer. This becomes the filename extension
  74. * in tile requests. Default is "png" (generating a url like
  75. * "http://example.com/mylayer/L00/R00000000/C00000000.png").
  76. */
  77. type: 'png',
  78. /**
  79. * APIProperty: useScales
  80. * {Boolean} Optional override to indicate that the layer should use 'scale' information
  81. * returned from the server capabilities object instead of 'resolution' information.
  82. * This can be important if your tile server uses an unusual DPI for the tiles.
  83. */
  84. useScales: false,
  85. /**
  86. * APIProperty: overrideDPI
  87. * {Boolean} Optional override to change the OpenLayers.DOTS_PER_INCH setting based
  88. * on the tile information in the server capabilities object. This can be useful
  89. * if your server has a non-standard DPI setting on its tiles, and you're only using
  90. * tiles with that DPI. This value is used while OpenLayers is calculating resolution
  91. * using scales, and is not necessary if you have resolution information. (This is
  92. * typically the case) Regardless, this setting can be useful, but is dangerous
  93. * because it will impact other layers while calculating resolution. Only use this
  94. * if you know what you are doing. (See OpenLayers.Util.getResolutionFromScale)
  95. */
  96. overrideDPI: false,
  97. /**
  98. * Constructor: OpenLayers.Layer.ArcGISCache
  99. * Creates a new instance of this class
  100. *
  101. * Parameters:
  102. * name - {String}
  103. * url - {String}
  104. * options - {Object} extra layer options
  105. */
  106. initialize: function(name, url, options) {
  107. OpenLayers.Layer.XYZ.prototype.initialize.apply(this, arguments);
  108. if (this.resolutions) {
  109. this.serverResolutions = this.resolutions;
  110. this.maxExtent = this.getMaxExtentForResolution(this.resolutions[0]);
  111. }
  112. // this block steps through translating the values from the server layer JSON
  113. // capabilities object into values that we can use. This is also a helpful
  114. // reference when configuring this layer directly.
  115. if (this.layerInfo) {
  116. // alias the object
  117. var info = this.layerInfo;
  118. // build our extents
  119. var startingTileExtent = new OpenLayers.Bounds(
  120. info.fullExtent.xmin,
  121. info.fullExtent.ymin,
  122. info.fullExtent.xmax,
  123. info.fullExtent.ymax
  124. );
  125. // set our projection based on the given spatial reference.
  126. // esri uses slightly different IDs, so this may not be comprehensive
  127. this.projection = 'EPSG:' + info.spatialReference.wkid;
  128. this.sphericalMercator = (info.spatialReference.wkid == 102100);
  129. // convert esri units into openlayers units (basic feet or meters only)
  130. this.units = (info.units == "esriFeet") ? 'ft' : 'm';
  131. // optional extended section based on whether or not the server returned
  132. // specific tile information
  133. if (!!info.tileInfo) {
  134. // either set the tiles based on rows/columns, or specific width/height
  135. this.tileSize = new OpenLayers.Size(
  136. info.tileInfo.width || info.tileInfo.cols,
  137. info.tileInfo.height || info.tileInfo.rows
  138. );
  139. // this must be set when manually configuring this layer
  140. this.tileOrigin = new OpenLayers.LonLat(
  141. info.tileInfo.origin.x,
  142. info.tileInfo.origin.y
  143. );
  144. var upperLeft = new OpenLayers.Geometry.Point(
  145. startingTileExtent.left,
  146. startingTileExtent.top
  147. );
  148. var bottomRight = new OpenLayers.Geometry.Point(
  149. startingTileExtent.right,
  150. startingTileExtent.bottom
  151. );
  152. if (this.useScales) {
  153. this.scales = [];
  154. } else {
  155. this.resolutions = [];
  156. }
  157. this.lods = [];
  158. for(var key in info.tileInfo.lods) {
  159. if (info.tileInfo.lods.hasOwnProperty(key)) {
  160. var lod = info.tileInfo.lods[key];
  161. if (this.useScales) {
  162. this.scales.push(lod.scale);
  163. } else {
  164. this.resolutions.push(lod.resolution);
  165. }
  166. var start = this.getContainingTileCoords(upperLeft, lod.resolution);
  167. lod.startTileCol = start.x;
  168. lod.startTileRow = start.y;
  169. var end = this.getContainingTileCoords(bottomRight, lod.resolution);
  170. lod.endTileCol = end.x;
  171. lod.endTileRow = end.y;
  172. this.lods.push(lod);
  173. }
  174. }
  175. this.maxExtent = this.calculateMaxExtentWithLOD(this.lods[0]);
  176. this.serverResolutions = this.resolutions;
  177. if (this.overrideDPI && info.tileInfo.dpi) {
  178. // see comment above for 'overrideDPI'
  179. OpenLayers.DOTS_PER_INCH = info.tileInfo.dpi;
  180. }
  181. }
  182. }
  183. },
  184. /**
  185. * Method: getContainingTileCoords
  186. * Calculates the x/y pixel corresponding to the position of the tile
  187. * that contains the given point and for the for the given resolution.
  188. *
  189. * Parameters:
  190. * point - {<OpenLayers.Geometry.Point>}
  191. * res - {Float} The resolution for which to compute the extent.
  192. *
  193. * Returns:
  194. * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position
  195. * of the upper left tile for the given resolution.
  196. */
  197. getContainingTileCoords: function(point, res) {
  198. return new OpenLayers.Pixel(
  199. Math.max(Math.floor((point.x - this.tileOrigin.lon) / (this.tileSize.w * res)),0),
  200. Math.max(Math.floor((this.tileOrigin.lat - point.y) / (this.tileSize.h * res)),0)
  201. );
  202. },
  203. /**
  204. * Method: calculateMaxExtentWithLOD
  205. * Given a Level of Detail object from the server, this function
  206. * calculates the actual max extent
  207. *
  208. * Parameters:
  209. * lod - {Object} a Level of Detail Object from the server capabilities object
  210. representing a particular zoom level
  211. *
  212. * Returns:
  213. * {<OpenLayers.Bounds>} The actual extent of the tiles for the given zoom level
  214. */
  215. calculateMaxExtentWithLOD: function(lod) {
  216. // the max extent we're provided with just overlaps some tiles
  217. // our real extent is the bounds of all the tiles we touch
  218. var numTileCols = (lod.endTileCol - lod.startTileCol) + 1;
  219. var numTileRows = (lod.endTileRow - lod.startTileRow) + 1;
  220. var minX = this.tileOrigin.lon + (lod.startTileCol * this.tileSize.w * lod.resolution);
  221. var maxX = minX + (numTileCols * this.tileSize.w * lod.resolution);
  222. var maxY = this.tileOrigin.lat - (lod.startTileRow * this.tileSize.h * lod.resolution);
  223. var minY = maxY - (numTileRows * this.tileSize.h * lod.resolution);
  224. return new OpenLayers.Bounds(minX, minY, maxX, maxY);
  225. },
  226. /**
  227. * Method: calculateMaxExtentWithExtent
  228. * Given a 'suggested' max extent from the server, this function uses
  229. * information about the actual tile sizes to determine the actual
  230. * extent of the layer.
  231. *
  232. * Parameters:
  233. * extent - {<OpenLayers.Bounds>} The 'suggested' extent for the layer
  234. * res - {Float} The resolution for which to compute the extent.
  235. *
  236. * Returns:
  237. * {<OpenLayers.Bounds>} The actual extent of the tiles for the given zoom level
  238. */
  239. calculateMaxExtentWithExtent: function(extent, res) {
  240. var upperLeft = new OpenLayers.Geometry.Point(extent.left, extent.top);
  241. var bottomRight = new OpenLayers.Geometry.Point(extent.right, extent.bottom);
  242. var start = this.getContainingTileCoords(upperLeft, res);
  243. var end = this.getContainingTileCoords(bottomRight, res);
  244. var lod = {
  245. resolution: res,
  246. startTileCol: start.x,
  247. startTileRow: start.y,
  248. endTileCol: end.x,
  249. endTileRow: end.y
  250. };
  251. return this.calculateMaxExtentWithLOD(lod);
  252. },
  253. /**
  254. * Method: getUpperLeftTileCoord
  255. * Calculates the x/y pixel corresponding to the position
  256. * of the upper left tile for the given resolution.
  257. *
  258. * Parameters:
  259. * res - {Float} The resolution for which to compute the extent.
  260. *
  261. * Returns:
  262. * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position
  263. * of the upper left tile for the given resolution.
  264. */
  265. getUpperLeftTileCoord: function(res) {
  266. var upperLeft = new OpenLayers.Geometry.Point(
  267. this.maxExtent.left,
  268. this.maxExtent.top);
  269. return this.getContainingTileCoords(upperLeft, res);
  270. },
  271. /**
  272. * Method: getLowerRightTileCoord
  273. * Calculates the x/y pixel corresponding to the position
  274. * of the lower right tile for the given resolution.
  275. *
  276. * Parameters:
  277. * res - {Float} The resolution for which to compute the extent.
  278. *
  279. * Returns:
  280. * {<OpenLayers.Pixel>} The x/y pixel corresponding to the position
  281. * of the lower right tile for the given resolution.
  282. */
  283. getLowerRightTileCoord: function(res) {
  284. var bottomRight = new OpenLayers.Geometry.Point(
  285. this.maxExtent.right,
  286. this.maxExtent.bottom);
  287. return this.getContainingTileCoords(bottomRight, res);
  288. },
  289. /**
  290. * Method: getMaxExtentForResolution
  291. * Since the max extent of a set of tiles can change from zoom level
  292. * to zoom level, we need to be able to calculate that max extent
  293. * for a given resolution.
  294. *
  295. * Parameters:
  296. * res - {Float} The resolution for which to compute the extent.
  297. *
  298. * Returns:
  299. * {<OpenLayers.Bounds>} The extent for this resolution
  300. */
  301. getMaxExtentForResolution: function(res) {
  302. var start = this.getUpperLeftTileCoord(res);
  303. var end = this.getLowerRightTileCoord(res);
  304. var numTileCols = (end.x - start.x) + 1;
  305. var numTileRows = (end.y - start.y) + 1;
  306. var minX = this.tileOrigin.lon + (start.x * this.tileSize.w * res);
  307. var maxX = minX + (numTileCols * this.tileSize.w * res);
  308. var maxY = this.tileOrigin.lat - (start.y * this.tileSize.h * res);
  309. var minY = maxY - (numTileRows * this.tileSize.h * res);
  310. return new OpenLayers.Bounds(minX, minY, maxX, maxY);
  311. },
  312. /**
  313. * APIMethod: clone
  314. * Returns an exact clone of this OpenLayers.Layer.ArcGISCache
  315. *
  316. * Parameters:
  317. * [obj] - {Object} optional object to assign the cloned instance to.
  318. *
  319. * Returns:
  320. * {<OpenLayers.Layer.ArcGISCache>} clone of this instance
  321. */
  322. clone: function (obj) {
  323. if (obj == null) {
  324. obj = new OpenLayers.Layer.ArcGISCache(this.name, this.url, this.options);
  325. }
  326. return OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]);
  327. },
  328. /**
  329. * Method: initGriddedTiles
  330. *
  331. * Parameters:
  332. * bounds - {<OpenLayers.Bounds>}
  333. */
  334. initGriddedTiles: function(bounds) {
  335. delete this._tileOrigin;
  336. OpenLayers.Layer.XYZ.prototype.initGriddedTiles.apply(this, arguments);
  337. },
  338. /**
  339. * Method: getMaxExtent
  340. * Get this layer's maximum extent.
  341. *
  342. * Returns:
  343. * {<OpenLayers.Bounds>}
  344. */
  345. getMaxExtent: function() {
  346. var resolution = this.map.getResolution();
  347. return this.maxExtent = this.getMaxExtentForResolution(resolution);
  348. },
  349. /**
  350. * Method: getTileOrigin
  351. * Determine the origin for aligning the grid of tiles.
  352. * The origin will be derived from the layer's <maxExtent> property.
  353. *
  354. * Returns:
  355. * {<OpenLayers.LonLat>} The tile origin.
  356. */
  357. getTileOrigin: function() {
  358. if (!this._tileOrigin) {
  359. var extent = this.getMaxExtent();
  360. this._tileOrigin = new OpenLayers.LonLat(extent.left, extent.bottom);
  361. }
  362. return this._tileOrigin;
  363. },
  364. /**
  365. * Method: getURL
  366. * Determine the URL for a tile given the tile bounds. This is should support
  367. * urls that access tiles through an ArcGIS Server MapServer or directly through
  368. * the hex folder structure using HTTP. Just be sure to set the useArcGISServer
  369. * property appropriately! This is basically the same as
  370. * 'OpenLayers.Layer.TMS.getURL', but with the addition of hex addressing,
  371. * and tile rounding.
  372. *
  373. * Parameters:
  374. * bounds - {<OpenLayers.Bounds>}
  375. *
  376. * Returns:
  377. * {String} The URL for a tile based on given bounds.
  378. */
  379. getURL: function (bounds) {
  380. var res = this.getResolution();
  381. // tile center
  382. var originTileX = (this.tileOrigin.lon + (res * this.tileSize.w/2));
  383. var originTileY = (this.tileOrigin.lat - (res * this.tileSize.h/2));
  384. var center = bounds.getCenterLonLat();
  385. var point = { x: center.lon, y: center.lat };
  386. var x = (Math.round(Math.abs((center.lon - originTileX) / (res * this.tileSize.w))));
  387. var y = (Math.round(Math.abs((originTileY - center.lat) / (res * this.tileSize.h))));
  388. var z = this.map.getZoom();
  389. // this prevents us from getting pink tiles (non-existant tiles)
  390. if (this.lods) {
  391. var lod = this.lods[this.map.getZoom()];
  392. if ((x < lod.startTileCol || x > lod.endTileCol)
  393. || (y < lod.startTileRow || y > lod.endTileRow)) {
  394. return null;
  395. }
  396. }
  397. else {
  398. var start = this.getUpperLeftTileCoord(res);
  399. var end = this.getLowerRightTileCoord(res);
  400. if ((x < start.x || x >= end.x)
  401. || (y < start.y || y >= end.y)) {
  402. return null;
  403. }
  404. }
  405. // Construct the url string
  406. var url = this.url;
  407. var s = '' + x + y + z;
  408. if (OpenLayers.Util.isArray(url)) {
  409. url = this.selectUrl(s, url);
  410. }
  411. // Accessing tiles through ArcGIS Server uses a different path
  412. // structure than direct access via the folder structure.
  413. if (this.useArcGISServer) {
  414. // AGS MapServers have pretty url access to tiles
  415. url = url + '/tile/${z}/${y}/${x}';
  416. } else {
  417. // The tile images are stored using hex values on disk.
  418. x = 'C' + OpenLayers.Number.zeroPad(x, 8, 16);
  419. y = 'R' + OpenLayers.Number.zeroPad(y, 8, 16);
  420. z = 'L' + OpenLayers.Number.zeroPad(z, 2, 10);
  421. url = url + '/${z}/${y}/${x}.' + this.type;
  422. }
  423. // Write the values into our formatted url
  424. url = OpenLayers.String.format(url, {'x': x, 'y': y, 'z': z});
  425. return OpenLayers.Util.urlAppend(
  426. url, OpenLayers.Util.getParameterString(this.params)
  427. );
  428. },
  429. CLASS_NAME: 'OpenLayers.Layer.ArcGISCache'
  430. });