ArcGISCache.js 18 KB

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