OpenLayers ArcGIS Cache Example (MapServer Access)
+
+
+ arcgis, arcgiscache, cache, tms
+
+
+
+ Demonstrates the basic initialization of the ArcGIS Cache layer using a prebuilt configuration, and standard tile access.
+
+
+
+
+
+
This example demonstrates using the ArcGISCache layer for
+ accessing ESRI's ArcGIS Server (AGS) Map Cache tiles through
+ an AGS MapServer. Toggle the visibility of the AGS layer to
+ demonstrate how the two maps are lined up correctly.
+
+
Notes on this layer
+
A few attempts have been made at this kind of layer before. See
+ here and
+ here.
+ A problem the users encounter is that the tiles seem to "jump around".
+ This is due to the fact that the max extent for the cached layer actually
+ changes at each zoom level due to the way these caches are constructed.
+ We have attempted to use the resolutions, tile size, and tile origin
+ from the cache meta data to make the appropriate changes to the max extent
+ of the tile to compensate for this behavior.
+ You will need to know:
+
+
Max Extent: The max extent of the layer
+
Resolutions: An array of resolutions, one for each zoom level
+
Tile Origin: The location of the tile origin for the cache in the upper left.
+
Tile Size: The size of each tile in the cache. Commonly 256 x 256
+
+
diff --git a/examples/arcgiscache_direct.html b/examples/arcgiscache_direct.html
new file mode 100644
index 0000000000..06d1bd6583
--- /dev/null
+++ b/examples/arcgiscache_direct.html
@@ -0,0 +1,106 @@
+
+
+ ArcGIS Server Map Cache Example (Direct Access)
+
+
+
+
+
+
+
+
ArcGIS Server Map Cache Example (Direct Access)
+
+
+
+
+
+ Demonstrates the basic initialization of the ArcGIS Cache layer using a prebuilt configuration, and direct tile access from a file store.
+
+
+
+
+
+
This example demonstrates using the ArcGISCache layer for
+ accessing ESRI's ArcGIS Server (AGS) Map Cache tiles directly
+ via the folder structure and HTTP. Toggle the visibility of the AGS layer to
+ demonstrate how the two maps are lined up correctly.
+
+
Notes on this Layer
+
It's important that you set the correct values in your layer, and these
+ values will differ between tile sets. You can find these values for your
+ layer in conf.xml at the root of your cache.
+ (ie. http://serverx.esri.com/arcgiscache/dgaerials/Layers/conf.xml)
+
+
For fused map caches this is often http:ServerName/arcgiscache/MapServiceName/Layers
+ For individual layer caches this is often http:ServerName/arcgiscache/LayerName/Layers
OpenLayers ArcGIS Cache Example (Autoconfigure with JSONP)
+
+
+ arcgis, arcgiscache, cache, tms, jsonp
+
+
+
+ Demonstrates the basic initialization of the ArcGIS Cache layer by using the server capabilities object.
+
+
+
+
+
+
This example demonstrates using the ArcGISCache layer for
+ accessing ESRI's ArcGIS Server (AGS) Map Cache tiles normally through
+ a live AGS MapServer. Toggle the visibility of the overlay to
+ demonstrate how the two layers are lined up correctly.
+
+
Notes on this Layer
+
+ This method automatically configures the layer using the capabilities object
+ generated by the server itself. This page shows how to construct the url for the server capabilities object,
+ retrieve it using JSONP (and jQuery), and pass it in during construction. Note that in this case,
+ the layer is constructed before the map. This approach greatly simplifies the
+ configuration of your map, and works best when all your tiles / overlays are similarly laid out.
+ If you are using a live AGS map server for your layer, it can be helpful to check your
+ server configuration using this technique before trying one of the other examples for this layer.
+
+
+
diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js
index 3bc4e611d3..34bf4d5937 100644
--- a/lib/OpenLayers.js
+++ b/lib/OpenLayers.js
@@ -155,6 +155,7 @@
"OpenLayers/Layer/TMS.js",
"OpenLayers/Layer/TileCache.js",
"OpenLayers/Layer/Zoomify.js",
+ "OpenLayers/Layer/ArcGISCache.js",
"OpenLayers/Popup/Anchored.js",
"OpenLayers/Popup/AnchoredBubble.js",
"OpenLayers/Popup/Framed.js",
diff --git a/lib/OpenLayers/Layer/ArcGISCache.js b/lib/OpenLayers/Layer/ArcGISCache.js
new file mode 100644
index 0000000000..18c14b5359
--- /dev/null
+++ b/lib/OpenLayers/Layer/ArcGISCache.js
@@ -0,0 +1,475 @@
+/**
+ * @requires OpenLayers/Layer/XYZ.js
+ */
+
+/**
+ * Class: OpenLayers.Layer.ArcGISCache
+ * Layer for accessing cached map tiles from an ArcGIS Server style mapcache.
+ * Tile must already be cached for this layer to access it. This does not require
+ * ArcGIS Server itself.
+ *
+ * A few attempts have been made at this kind of layer before. See
+ * http://trac.osgeo.org/openlayers/ticket/1967
+ * and
+ * http://trac.osgeo.org/openlayers/browser/sandbox/tschaub/arcgiscache/lib/OpenLayers/Layer/ArcGISCache.js
+ *
+ * Typically the problem encountered is that the tiles seem to "jump around".
+ * This is due to the fact that the actual max extent for the tiles on AGS layers
+ * changes at each zoom level due to the way these caches are constructed.
+ * We have attempted to use the resolutions, tile size, and tile origin
+ * from the cache meta data to make the appropriate changes to the max extent
+ * of the tile to compensate for this behavior. This must be done as zoom levels change
+ * and before tiles are requested, which is why methods from base classes are overridden.
+ *
+ * For reference, you can access mapcache meta data in two ways. For accessing a
+ * mapcache through ArcGIS Server, you can simply go to the landing page for the
+ * layer. (ie. http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer)
+ * For accessing it directly through HTTP, there should always be a conf.xml file
+ * in the root directory.
+ * (ie. http://serverx.esri.com/arcgiscache/DG_County_roads_yesA_backgroundDark/Layers/conf.xml)
+ *
+ *Inherits from:
+ * -
+ */
+OpenLayers.Layer.ArcGISCache = OpenLayers.Class(OpenLayers.Layer.XYZ, {
+
+ /**
+ * APIProperty: url
+ * {String | Array} The base URL for the layer cache. You can also
+ * provide a list of URL strings for the layer if your cache is
+ * available from multiple origins. This must be set before the layer
+ * is drawn.
+ */
+ url: null,
+
+ /**
+ * APIProperty: tileOrigin
+ * {} The location of the tile origin for the cache.
+ * An ArcGIS cache has it's origin at the upper-left (lowest x value
+ * and highest y value of the coordinate system). The units for the
+ * tile origin should be the same as the units for the cached data.
+ */
+ tileOrigin: null,
+
+ /**
+ * APIProperty: tileSize
+ * {} This size of each tile. Defaults to 256 by 256 pixels.
+ */
+ tileSize: new OpenLayers.Size(256, 256),
+
+ /**
+ * APIProperty: useAGS
+ * {Boolean} Indicates if we are going to be accessing the ArcGIS Server (AGS)
+ * cache via an AGS MapServer or directly through HTTP. When accessing via
+ * AGS the path structure uses a standard z/y/x structure. But AGS actually
+ * stores the tile images on disk using a hex based folder structure that looks
+ * like "http://example.com/mylayer/L00/R00000000/C00000000.png". Learn more
+ * about this here:
+ * http://blogs.esri.com/Support/blogs/mappingcenter/archive/2010/08/20/Checking-Your-Local-Cache-Folders.aspx
+ * Defaults to true;
+ */
+ useArcGISServer: true,
+
+ /**
+ * APIProperty: type
+ * {String} Image type for the layer. This becomes the filename extension
+ * in tile requests. Default is "png" (generating a url like
+ * "http://example.com/mylayer/L00/R00000000/C00000000.png").
+ */
+ type: 'png',
+
+ /**
+ * APIProperty: useScales
+ * {Boolean} Optional override to indicate that the layer should use 'scale' information
+ * returned from the server capabilities object instead of 'resolution' information.
+ * This can be important if your tile server uses an unusual DPI for the tiles.
+ */
+ useScales: false,
+
+ /**
+ * APIProperty: overrideDPI
+ * {Boolean} Optional override to change the OpenLayers.DOTS_PER_INCH setting based
+ * on the tile information in the server capabilities object. This can be useful
+ * if your server has a non-standard DPI setting on its tiles, and you're only using
+ * tiles with that DPI. This value is used while OpenLayers is calculating resolution
+ * using scales, and is not necessary if you have resolution information. (This is
+ * typically the case) Regardless, this setting can be useful, but is dangerous
+ * because it will impact other layers while calculating resolution. Only use this
+ * if you know what you are doing. (See OpenLayers.Util.getResolutionFromScale)
+ */
+ overrideDPI: false,
+
+ /**
+ * Constructor: OpenLayers.Layer.ArcGISCache
+ * Creates a new instance of this class
+ *
+ * Parameters:
+ * name - {String}
+ * url - {String}
+ * options - {Object} extra layer options
+ */
+ initialize: function(name, url, options) {
+ OpenLayers.Layer.XYZ.prototype.initialize.apply(this, arguments);
+
+ if (this.resolutions) {
+ this.serverResolutions = this.resolutions;
+ this.maxExtent = this.getMaxExtentForResolution(this.resolutions[0]);
+ }
+
+ // this block steps through translating the values from the server layer JSON
+ // capabilities object into values that we can use. This is also a helpful
+ // reference when configuring this layer directly.
+ if (this.layerInfo) {
+ // alias the object
+ var info = this.layerInfo;
+
+ // build our extents
+ var startingTileExtent = new OpenLayers.Bounds(
+ info.fullExtent.xmin,
+ info.fullExtent.ymin,
+ info.fullExtent.xmax,
+ info.fullExtent.ymax
+ );
+
+ // set our projection based on the given spatial reference.
+ // esri uses slightly different IDs, so this may not be comprehensive
+ this.projection = 'EPSG:' + info.spatialReference.wkid;
+ this.sphericalMercator = (info.spatialReference.wkid == 102100);
+
+ // convert esri units into openlayers units (basic feet or meters only)
+ this.units = (info.units == "esriFeet") ? 'ft' : 'm';
+
+ // optional extended section based on whether or not the server returned
+ // specific tile information
+ if (!!info.tileInfo) {
+ // either set the tiles based on rows/columns, or specific width/height
+ this.tileSize = new OpenLayers.Size(
+ info.tileInfo.width || info.tileInfo.cols,
+ info.tileInfo.height || info.tileInfo.rows
+ );
+
+ // this must be set when manually configuring this layer
+ this.tileOrigin = new OpenLayers.LonLat(
+ info.tileInfo.origin.x,
+ info.tileInfo.origin.y
+ );
+
+ var upperLeft = new OpenLayers.Geometry.Point(
+ startingTileExtent.left,
+ startingTileExtent.top
+ );
+
+ var bottomRight = new OpenLayers.Geometry.Point(
+ startingTileExtent.right,
+ startingTileExtent.bottom
+ );
+
+ if (this.useScales) {
+ this.scales = [];
+ } else {
+ this.resolutions = [];
+ }
+
+ this.lods = [];
+ for(var key in info.tileInfo.lods) {
+ var lod = info.tileInfo.lods[key];
+ if (this.useScales) {
+ this.scales.push(lod.scale);
+ } else {
+ this.resolutions.push(lod.resolution);
+ }
+
+ var start = this.getContainingTileCoords(upperLeft, lod.resolution);
+ lod.startTileCol = start.x;
+ lod.startTileRow = start.y;
+
+ var end = this.getContainingTileCoords(bottomRight, lod.resolution);
+ lod.endTileCol = end.x;
+ lod.endTileRow = end.y;
+ this.lods.push(lod);
+ }
+
+ this.maxExtent = this.calculateMaxExtentWithLOD(this.lods[0]);
+ this.serverResolutions = this.resolutions;
+ if (this.overrideDPI && info.tileInfo.dpi) {
+ // see comment above for 'overrideDPI'
+ OpenLayers.DOTS_PER_INCH = info.tileInfo.dpi;
+ }
+ }
+ }
+ },
+
+ /**
+ * Method: getContainingTileCoords
+ * Calculates the x/y pixel corresponding to the position of the tile
+ * that contains the given point and for the for the given resolution.
+ *
+ * Parameters:
+ * point - {}
+ * res - {Float} The resolution for which to compute the extent.
+ *
+ * Returns:
+ * {} The x/y pixel corresponding to the position
+ * of the upper left tile for the given resolution.
+ */
+ getContainingTileCoords: function(point, res) {
+ return new OpenLayers.Pixel(
+ Math.floor((point.x - this.tileOrigin.lon) / (this.tileSize.w * res)),
+ Math.floor((this.tileOrigin.lat - point.y) / (this.tileSize.h * res))
+ );
+ },
+
+ /**
+ * Method: calculateMaxExtentWithLOD
+ * Given a Level of Detail object from the server, this function
+ * calculates the actual max extent
+ *
+ * Parameters:
+ * lod - {Object} a Level of Detail Object from the server capabilities object
+ representing a particular zoom level
+ *
+ * Returns:
+ * {} The actual extent of the tiles for the given zoom level
+ */
+ calculateMaxExtentWithLOD: function(lod) {
+ // the max extent we're provided with just overlaps some tiles
+ // our real extent is the bounds of all the tiles we touch
+
+ var numTileCols = (lod.endTileCol - lod.startTileCol) + 1;
+ var numTileRows = (lod.endTileRow - lod.startTileRow) + 1;
+
+ var minX = this.tileOrigin.lon + (lod.startTileCol * this.tileSize.w * lod.resolution);
+ var maxX = minX + (numTileCols * this.tileSize.w * lod.resolution);
+
+ var maxY = this.tileOrigin.lat - (lod.startTileRow * this.tileSize.h * lod.resolution);
+ var minY = maxY - (numTileRows * this.tileSize.h * lod.resolution);
+ return new OpenLayers.Bounds(minX, minY, maxX, maxY);
+ },
+
+ /**
+ * Method: calculateMaxExtentWithExtent
+ * Given a 'suggested' max extent from the server, this function uses
+ * information about the actual tile sizes to determine the actual
+ * extent of the layer.
+ *
+ * Parameters:
+ * extent - {} The 'suggested' extent for the layer
+ * res - {Float} The resolution for which to compute the extent.
+ *
+ * Returns:
+ * {} The actual extent of the tiles for the given zoom level
+ */
+ calculateMaxExtentWithExtent: function(extent, res) {
+ var upperLeft = new OpenLayers.Geometry.Point(extent.left, extent.top);
+ var bottomRight = new OpenLayers.Geometry.Point(extent.right, extent.bottom);
+ var start = this.getContainingTileCoords(upperLeft, res);
+ var end = this.getContainingTileCoords(bottomRight, res);
+ var lod = {
+ resolution: res,
+ startTileCol: start.x,
+ startTileRow: start.y,
+ endTileCol: end.x,
+ endTileRow: end.y
+ };
+ return calculateMaxExtentWithLOD(lod);
+ },
+
+ /**
+ * Method: getUpperLeftTileCoord
+ * Calculates the x/y pixel corresponding to the position
+ * of the upper left tile for the given resolution.
+ *
+ * Parameters:
+ * res - {Float} The resolution for which to compute the extent.
+ *
+ * Returns:
+ * {} The x/y pixel corresponding to the position
+ * of the upper left tile for the given resolution.
+ */
+ getUpperLeftTileCoord: function(res) {
+ var upperLeft = new OpenLayers.Geometry.Point(
+ this.maxExtent.left,
+ this.maxExtent.top);
+ return this.getContainingTileCoords(upperLeft, res);
+ },
+
+ /**
+ * Method: getLowerRightTileCoord
+ * Calculates the x/y pixel corresponding to the position
+ * of the lower right tile for the given resolution.
+ *
+ * Parameters:
+ * res - {Float} The resolution for which to compute the extent.
+ *
+ * Returns:
+ * {} The x/y pixel corresponding to the position
+ * of the lower right tile for the given resolution.
+ */
+ getLowerRightTileCoord: function(res) {
+ var bottomRight = new OpenLayers.Geometry.Point(
+ this.maxExtent.right,
+ this.maxExtent.bottom);
+ return this.getContainingTileCoords(bottomRight, res);
+ },
+
+ /**
+ * Method: getMaxExtentForResolution
+ * Since the max extent of a set of tiles can change from zoom level
+ * to zoom level, we need to be able to calculate that max extent
+ * for a given resolution.
+ *
+ * Parameters:
+ * res - {Float} The resolution for which to compute the extent.
+ *
+ * Returns:
+ * {} The extent for this resolution
+ */
+ getMaxExtentForResolution: function(res) {
+ var start = this.getUpperLeftTileCoord(res);
+ var end = this.getLowerRightTileCoord(res);
+
+ var numTileCols = (end.x - start.x) + 1;
+ var numTileRows = (end.y - start.y) + 1;
+
+ var minX = this.tileOrigin.lon + (start.x * this.tileSize.w * res);
+ var maxX = minX + (numTileCols * this.tileSize.w * res);
+
+ var maxY = this.tileOrigin.lat - (start.y * this.tileSize.h * res);
+ var minY = maxY - (numTileRows * this.tileSize.h * res);
+ return new OpenLayers.Bounds(minX, minY, maxX, maxY);
+ },
+
+ /**
+ * APIMethod: clone
+ * Returns an exact clone of this OpenLayers.Layer.ArcGISCache
+ *
+ * Parameters:
+ * [obj] - {Object} optional object to assign the cloned instance to.
+ *
+ * Returns:
+ * {} clone of this instance
+ */
+ clone: function (obj) {
+ if (obj == null) {
+ obj = new OpenLayers.Layer.ArcGISCache(this.name, this.url, this.options);
+ }
+ return OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]);
+ },
+
+ /**
+ * Method: getMaxExtent
+ * Get this layer's maximum extent.
+ *
+ * Returns:
+ * {OpenLayers.Bounds}
+ */
+ getMaxExtent: function() {
+ var resolution = this.map.getResolution();
+ return this.maxExtent = this.getMaxExtentForResolution(resolution);
+ },
+
+ /**
+ * Method: getTileOrigin
+ * Determine the origin for aligning the grid of tiles.
+ * The origin will be derived from the layer's property.
+ *
+ * Returns:
+ * {} The tile origin.
+ */
+ getTileOrigin: function() {
+ var extent = this.getMaxExtent();
+ return new OpenLayers.LonLat(extent.left, extent.bottom);
+ },
+
+ /**
+ * Method: getURL
+ * Determine the URL for a tile given the tile bounds. This is should support
+ * urls that access tiles through an ArcGIS Server MapServer or directly through
+ * the hex folder structure using HTTP. Just be sure to set the useArcGISServer
+ * property appropriately! This is basically the same as
+ * 'OpenLayers.Layer.TMS.getURL', but with the addition of hex addressing,
+ * and tile rounding.
+ *
+ * Parameters:
+ * bounds - {}
+ *
+ * Returns:
+ * {String} The URL for a tile based on given bounds.
+ */
+ getURL: function (bounds) {
+ var res = this.getResolution();
+
+ // tile center
+ var originTileX = (this.tileOrigin.lon + (res * this.tileSize.w/2));
+ var originTileY = (this.tileOrigin.lat - (res * this.tileSize.h/2));
+
+ var center = bounds.getCenterLonLat();
+ var point = { x: center.lon, y: center.lat };
+ var x = (Math.round(Math.abs((center.lon - originTileX) / (res * this.tileSize.w))));
+ var y = (Math.round(Math.abs((originTileY - center.lat) / (res * this.tileSize.h))));
+ var z = this.map.getZoom();
+
+ // this prevents us from getting pink tiles (non-existant tiles)
+ if (this.lods) {
+ var lod = this.lods[this.map.getZoom()];
+ if ((x < lod.startTileCol || x > lod.endTileCol)
+ || (y < lod.startTileRow || y > lod.endTileRow)) {
+ return null;
+ }
+ }
+ else {
+ var start = this.getUpperLeftTileCoord(res);
+ var end = this.getLowerRightTileCoord(res);
+ if ((x < start.x || x >= end.x)
+ || (y < start.y || y >= end.y)) {
+ return null;
+ }
+ }
+
+ // Construct the url string
+ var url = this.url;
+ var s = '' + x + y + z;
+
+ if (url instanceof Array) {
+ url = this.selectUrl(s, url);
+ }
+
+ // Accessing tiles through ArcGIS Server uses a different path
+ // structure than direct access via the folder structure.
+ if (this.useArcGISServer) {
+ // AGS MapServers have pretty url access to tiles
+ url = url + '/tile/${z}/${y}/${x}';
+ } else {
+ // The tile images are stored using hex values on disk.
+ x = 'C' + this.zeroPad(x, 8, 16);
+ y = 'R' + this.zeroPad(y, 8, 16);
+ z = 'L' + this.zeroPad(z, 2, 16);
+ url = url + '/${z}/${y}/${x}.' + this.type;
+ }
+
+ // Write the values into our formatted url
+ url = OpenLayers.String.format(url, {'x': x, 'y': y, 'z': z});
+
+ return url;
+ },
+
+ /**
+ * Method: zeroPad
+ * Create a zero padded string optionally with a radix for casting numbers.
+ *
+ * Parameters:
+ * num - {Number} The number to be zero padded.
+ * len - {Number} The length of the string to be returned.
+ * radix - {Number} An integer between 2 and 36 specifying the base to use
+ * for representing numeric values.
+ */
+ zeroPad: function(num, len, radix) {
+ var str = num.toString(radix || 10);
+ while (str.length < len) {
+ str = "0" + str;
+ }
+ return str;
+ },
+
+ CLASS_NAME: 'OpenLayers.Layer.ArcGISCache'
+});
diff --git a/tests/Layer/ArcGISCache.html b/tests/Layer/ArcGISCache.html
new file mode 100644
index 0000000000..20cb4ae558
--- /dev/null
+++ b/tests/Layer/ArcGISCache.html
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Layer/ArcGISCache.json b/tests/Layer/ArcGISCache.json
new file mode 100644
index 0000000000..79dffa8b89
--- /dev/null
+++ b/tests/Layer/ArcGISCache.json
@@ -0,0 +1,334 @@
+var capabilitiesObject = {
+ "currentVersion" : 10.01,
+ "serviceDescription" : "This map is designed to be used as a base map by GIS professionals and as a reference map by anyone. The base map includes administrative boundaries, cities, water features, physiographic features, parks, landmarks, highways, roads, railways, airports, and buildings overlaid on land cover and shaded relief imagery for added context. The map was compiled from a variety of best available sources from several data providers, including the U.S. Geological Survey, Food and Agriculture Organization of the United Nations, National Park Service, Tele Atlas, AND, and ESRI. The base map currently provides coverage for the world down to a scale of ~1:1m and coverage for the continental United States and Hawaii to a scale of ~1:20k. The base map also includes detailed maps for selected cities in the United States including Portland, Oregon and Philadephia, Pennsylvania. The base map was designed and developed by ESRI based on the topographic map templates that are available through the ArcGIS Resource Centers. For more information on this map, visit us \u003ca href=\"http://goto.arcgisonline.com/maps/World_Topo_Map \" target=\"_new\"\u003eonline\u003c/a\u003e.",
+ "mapName" : "Layers",
+ "description" : "This map is designed to be used as a base map by GIS professionals and as a reference map by anyone. The base map includes administrative boundaries, cities, water features, physiographic features, parks, landmarks, highways, roads, railways, airports, and buildings overlaid on land cover and shaded relief imagery for added context. The map was compiled from a variety of best available sources from several data providers, including the U.S. Geological Survey, Food and Agriculture Organization of the United Nations, National Park Service, Tele Atlas, AND, and ESRI. The base map currently provides coverage for the world down to a scale of ~1:1m and coverage for the continental United States and Hawaii to a scale of ~1:20k. The base map also includes detailed maps for selected cities in the United States including Portland, Oregon and Philadephia, Pennsylvania. The base map was designed and developed by ESRI based on the topographic map templates that are available through the ArcGIS Resource Centers. For more information on this map, visit us online at http://goto.arcgisonline.com/maps/World_Topo_Map",
+ "copyrightText" : "Sources: USGS, FAO, NPS, EPA, ESRI, DeLorme, TANA, other suppliers",
+ "layers" : [
+ {
+ "id" : 0,
+ "name" : "Topographic Info",
+ "parentLayerId" : -1,
+ "defaultVisibility" : true,
+ "subLayerIds" : [1, 2, 3, 4],
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 1,
+ "name" : "Elevation (m)",
+ "parentLayerId" : 0,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 2,
+ "name" : "Elevation (ft)",
+ "parentLayerId" : 0,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 3,
+ "name" : "Slope",
+ "parentLayerId" : 0,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 4,
+ "name" : "Aspect",
+ "parentLayerId" : 0,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 5,
+ "name" : "Places Info",
+ "parentLayerId" : -1,
+ "defaultVisibility" : true,
+ "subLayerIds" : [6, 7, 8, 9],
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 6,
+ "name" : "Place Names (Country Level)",
+ "parentLayerId" : 5,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 80000000
+ },
+ {
+ "id" : 7,
+ "name" : "Place Names (State Level)",
+ "parentLayerId" : 5,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 80000001,
+ "maxScale" : 1500000
+ },
+ {
+ "id" : 8,
+ "name" : "Place Names (County Level)",
+ "parentLayerId" : 5,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 1500001,
+ "maxScale" : 400000
+ },
+ {
+ "id" : 9,
+ "name" : "Place Names (City Level)",
+ "parentLayerId" : 5,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 399999,
+ "maxScale" : 0
+ },
+ {
+ "id" : 10,
+ "name" : "Scale Descriptions",
+ "parentLayerId" : -1,
+ "defaultVisibility" : true,
+ "subLayerIds" : [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
+ "minScale" : 0,
+ "maxScale" : 0
+ },
+ {
+ "id" : 11,
+ "name" : "Level 15 ~1:18K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 25000,
+ "maxScale" : 15001
+ },
+ {
+ "id" : 12,
+ "name" : "Level 14 ~1:36K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 50000,
+ "maxScale" : 25001
+ },
+ {
+ "id" : 13,
+ "name" : "Level 13 ~1:72K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 100000,
+ "maxScale" : 50001
+ },
+ {
+ "id" : 14,
+ "name" : "Level 12 ~1:144K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 288000,
+ "maxScale" : 100000
+ },
+ {
+ "id" : 15,
+ "name" : "Level 11 ~1:288K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 575000,
+ "maxScale" : 288000
+ },
+ {
+ "id" : 16,
+ "name" : "Level 10 ~1:577K",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 1150000,
+ "maxScale" : 575000
+ },
+ {
+ "id" : 17,
+ "name" : "Level 9 ~1:1.15M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 2200000,
+ "maxScale" : 1150000
+ },
+ {
+ "id" : 18,
+ "name" : "Level 8 ~1:2.3M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 4500000,
+ "maxScale" : 2200000
+ },
+ {
+ "id" : 19,
+ "name" : "Level 7 ~1:4.5M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 9000000,
+ "maxScale" : 4500000
+ },
+ {
+ "id" : 20,
+ "name" : "Level 6 ~1:9.2M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 18000000,
+ "maxScale" : 9000000
+ },
+ {
+ "id" : 21,
+ "name" : "Level 5 ~1:18M ",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 36000000,
+ "maxScale" : 18000000
+ },
+ {
+ "id" : 22,
+ "name" : "Level 4 ~1:36M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 72000000,
+ "maxScale" : 36000000
+ },
+ {
+ "id" : 23,
+ "name" : "Level 3 ~1:72M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 75500000,
+ "maxScale" : 70000000
+ },
+ {
+ "id" : 24,
+ "name" : "Level 2 ~1:147M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 290000000,
+ "maxScale" : 147000000
+ },
+ {
+ "id" : 25,
+ "name" : "Level 1 ~1:292M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 295000000,
+ "maxScale" : 150000000
+ },
+ {
+ "id" : 26,
+ "name" : "Level 0 ~1:584M",
+ "parentLayerId" : 10,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 295000000
+ },
+ {
+ "id" : 27,
+ "name" : "Citations",
+ "parentLayerId" : -1,
+ "defaultVisibility" : true,
+ "subLayerIds" : null,
+ "minScale" : 0,
+ "maxScale" : 0
+ }
+ ],
+ "tables" : [
+
+ ],
+ "spatialReference" : {
+ "wkid" : 102100
+ },
+ "singleFusedMapCache" : true,
+ "tileInfo" : {
+ "rows" : 256,
+ "cols" : 256,
+ "dpi" : 96,
+ "format" : "JPEG",
+ "compressionQuality" : 90,
+ "origin" : {
+ "x" : -20037508.342787,
+ "y" : 20037508.342787
+ },
+ "spatialReference" : {
+ "wkid" : 102100
+ },
+ "lods" : [
+ {"level" : 0, "resolution" : 156543.033928, "scale" : 591657527.591555},
+ {"level" : 1, "resolution" : 78271.5169639999, "scale" : 295828763.795777},
+ {"level" : 2, "resolution" : 39135.7584820001, "scale" : 147914381.897889},
+ {"level" : 3, "resolution" : 19567.8792409999, "scale" : 73957190.948944},
+ {"level" : 4, "resolution" : 9783.93962049996, "scale" : 36978595.474472},
+ {"level" : 5, "resolution" : 4891.96981024998, "scale" : 18489297.737236},
+ {"level" : 6, "resolution" : 2445.98490512499, "scale" : 9244648.868618},
+ {"level" : 7, "resolution" : 1222.99245256249, "scale" : 4622324.434309},
+ {"level" : 8, "resolution" : 611.49622628138, "scale" : 2311162.217155},
+ {"level" : 9, "resolution" : 305.748113140558, "scale" : 1155581.108577},
+ {"level" : 10, "resolution" : 152.874056570411, "scale" : 577790.554289},
+ {"level" : 11, "resolution" : 76.4370282850732, "scale" : 288895.277144},
+ {"level" : 12, "resolution" : 38.2185141425366, "scale" : 144447.638572},
+ {"level" : 13, "resolution" : 19.1092570712683, "scale" : 72223.819286},
+ {"level" : 14, "resolution" : 9.55462853563415, "scale" : 36111.909643},
+ {"level" : 15, "resolution" : 4.77731426794937, "scale" : 18055.954822},
+ {"level" : 16, "resolution" : 2.38865713397468, "scale" : 9027.977411},
+ {"level" : 17, "resolution" : 1.19432856685505, "scale" : 4513.988705},
+ {"level" : 18, "resolution" : 0.597164283559817, "scale" : 2256.994353},
+ {"level" : 19, "resolution" : 0.298582141647617, "scale" : 1128.497176}
+ ]
+ },
+ "initialExtent" : {
+ "xmin" : -45223792.233066,
+ "ymin" : -22882589.2065154,
+ "xmax" : 45223792.233066,
+ "ymax" : 22882589.2065155,
+ "spatialReference" : {
+ "wkid" : 102100
+ }
+ },
+ "fullExtent" : {
+ "xmin" : -20037507.0671618,
+ "ymin" : -19971868.8804086,
+ "xmax" : 20037507.0671618,
+ "ymax" : 19971868.8804086,
+ "spatialReference" : {
+ "wkid" : 102100
+ }
+ },
+ "units" : "esriMeters",
+ "supportedImageFormatTypes" : "PNG24,PNG,JPG,DIB,TIFF,EMF,PS,PDF,GIF,SVG,SVGZ,AI,BMP",
+ "documentInfo" : {
+ "Title" : "World Topo Map",
+ "Author" : "ESRI",
+ "Comments" : "",
+ "Subject" : "",
+ "Category" : "",
+ "Keywords" : "",
+ "Credits" : ""
+ },
+ "capabilities" : "Map,Query,Data"
+};
\ No newline at end of file
diff --git a/tests/list-tests.html b/tests/list-tests.html
index 6aa13ecfa6..27b24f3658 100644
--- a/tests/list-tests.html
+++ b/tests/list-tests.html
@@ -133,6 +133,7 @@