I worked with an ESRI reseller here in the UK for a number of years and have been using ArcGIS Server and several of its APIs since their inception. I still do the odd bit of consultancy using Esri products, and recently I was asked to create an app which allowed site administrators to download individual map tiles from a cached map service. The part of that operation I wanted to cover in this post is the actual identification of those individual tiles.
Let's backtrack a bit for those who might not know what a cached map service is. When you publish a map service using ArcGIS Server (and other GIS servers), then by default it is a "dynamic map service". ArcGIS Server calculates what the user should be able to see in the current view port given the currently visible set of layers and then draws that data using the required map projection for display in the user's browser. This is computationally very expensive.
So, if you have a complex map as the basis for your web map service, with rich symbology, then the server is going to be very busy rendering the new map every time the user pans to a different location or otherwise changes the extent of the map. One particular type of map service that often performs suboptimally is the basemap. Think about it: a typical basemap consists of raster imagery and often complex, labelled vector imagery above it, and when the server has to render this (and often reproject it) on the fly, performance degrades noticably - especially when thousands of other users are trying to work with your map application simultaneously.
The best way around this problem is to cache the base map. This involves using tools to compress all the layers into one and then slice and dice the resulting image into lots of smaller images that can be sent down to the browser without any rendering or reprojection step. The browser then knows which tiles it needs to render a specific extent and just pulls those pre-generated tiles down from the web server.
That's the gist of it, although there is a lot more to caching map services than that. But that's a discussion for a future blog post. In any event, I think my client's requirement ("client" in this context referring to the organization that paid me to do this!) led to a useful way of looking "under the hood" at how these tiles are organized and delivered to the web browser.
So how to go about it?
After adding my cached map service to the map as an ArcGISTiledMapServiceLayer, the first thing I need to do is give myself a visual indication of where each of the tiles is located on the map. A bit of CSS comes to the rescue here:
#map_map-tiled .layerTile {
border: dashed 2px red;
}
#map_map-tiled .layerTile:hover {
border: dashed 2px lime;
}
This just styles each of the tiles with a dotted red border, so they are easy to see.
Having done that, I need to write some JavaScript.
The first thing I want to do is ascertain where on the map my user has clicked. I can write a click event handler for the map to do that:
var tileSelectMapClickHandler = map.on("click", lang.hitch(this, function(evt) {
mouseX = evt.screenPoint.x;
mouseY = evt.screenPoint.y;
getTile(mouseX, mouseY);
}));
Normally when we're handling the map's click event, we're interested in the map coordinates where the user clicked, rather than the screen coordinates. Luckily, the event object that gets passed into our callback contains both. We'll retrieve the screen coordinates and pass that to another function getTile() which will work out which image tile was clicked.
In getTile(), I can refer to the image tiles by using the same CSS class I used to style them above (.layerTile). I can query() all DOM nodes of that class and call their getBoundingClientRect() method. The getBoundingClientRect() method specifies the bounding rectangle of the element, in pixels, relative to the upper-left corner of the browser's client area. With this information I can check whether the point I received from the mouse click falls within these bounds. If it does, I have a match!
function getTile(mouseX, mouseY) {
// Select each map tile by its CSS class. Use hitch() to execute callback in correct scope.
query(".layerTile", tiled.getNode()).forEach(lang.hitch(this, function(node) {
// Get the screen coordinates for the tile: left is xmin, top is ymin, right is xmax, bottom is ymax
var screenTile = node.getBoundingClientRect();
// Is the mouse click within this tile?
if ((mouseX > screenTile.left) && (mouseX < screenTile.right)) {
if ((mouseY > screenTile.top) && (mouseY < screenTile.bottom)) {
// Yes: convert tile location to map coordinates
var screenTileBottomLeft = new ScreenPoint(screenTile.left, screenTile.bottom);
var screenTileTopRight = new ScreenPoint(screenTile.right, screenTile.top);
// Mark this tile as selected
selectTile(screenTileBottomLeft, screenTileTopRight, node);
}
}
}));
}
So that was the hard part of my client's requirement solved. From there I could work out the tile URL and create basic download functionality. What was more instructive from my perspective was to see how these tiles are named and organized so that the client can request them from the web server after an initial "handshake" with ArcGIS Server, leaving the GIS server free to deal with requests for the more important operational data.
If you want to have a play, I created a Plnkr for it.
I hope it saves someone out there a bit of head-scratching, or at least provides a bit of an insight into how ArcGIS Server stores cached map tiles.