Thursday, 13 November 2014

Identifying Individual Cached Tiles in the ArcGIS Server API for JavaScript

OK, I’m going to change tack slightly and talk about another web mapping technology close to my heart: Esri’s ArcGIS Server API for JavaScript.

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.

Sunday, 19 October 2014

Setting Google Maps and StreetView Locations Via a URL

I recently had a student on my Google Maps API: Get Started Pluralsight course ask how he could pass a location to a web page via the URL, and then see that location in Google StreetView. Accessing locations sent to a web page via query string parameters is something that maps developers want to do quite often, so here I describe how to go about it.

The first thing to consider is the how to encode the required location into the URL. The internet standard for doing this is to create a query string. The query string is the part of the URL that follows the name of your web page. It starts with a question mark, and then a list of key/value pairs, seperated by ampersands (&), as shown below:

 http://www.mysite.com/myapp/index.html?productid=87789&store=Oxford&instock=true  

Typically this URL is encoded by an HTML page with a form on it. When you click the submit button, the values of all the input elements are encoded into a query string and sent to a web page or server-side script that you specify. However, sometimes you'll need to format this yourself. For example, suppose you have a table in a database that contains lat/lng values. You want to be able to display this information on a web page and allow the user to click on a button on the page to see that location on the map. Or, you might be writing a mapping application which offers users the ability to bookmark favourite locations. However, regardless of the way in which the URL is formed, once your page receives these parameters, you'll need to parse them so that your application can make sense of them. Let's go back to our problem: how to display StreetView data based on a location passed via a URL. Well, let's first consider how we would do this for a map. A map requires a center point (consisting of a latitude and longitude coordinate) and a scale (represented by the "zoom level"). So our query string needs to look like this:

 http://localhost:8080/gmaps_samples/streetview1.html?lat=48.85832&lng=2.294223&zoom=18  

In our receiving page, we can access the query string portion of the URL from window.location.search. Unfortunately, that gives us the leading ? which we don't need, so we can just substring that out using window.location.search.substring(1) to get this string:

 lat=48.85832&lng=2.294223&zoom=18  

We can then use JavaScript's split() function to break up this string at each ampersand, to give us an array that looks like this:

 ["lat=48.85832", "lat=2.294223", "zoom=18"]  

We then loop through each element of this array and create a JavaScript object consisting of key/value pairs. The key is the part to the left of the = and the value is the part to the right:

 var qsParm = {};  
 function parseQueryString() {  
      var query = window.location.search.substring(1);  
      var parms = query.split('&amp;amp;');  
      for (var i = 0; i &amp;lt; parms.length; i++) {  
           var pos = parms[i].indexOf('=');  
           if (pos > 0) {  
                var key = parms[i].substring(0, pos);  
                var val = parms[i].substring(pos + 1);  
                qsParm[key] = val;  
           }  
      }  
      initializeMap(qsParm["lat"], qsParm["lng"], parseInt(qsParm["zoom"]));  
 }  

This bit of code then retrieves the lat, lng and zoom properties of our JavaScript object and passes it to the initializeMap() function that will set up our map. So the only thing we need to do is make sure that this parseQueryString() function runs when the page loads. We can use the Google Maps API convenience method google.maps.event.addDomListener() for that:

 google.maps.event.addDomListener(window, 'load', parseQueryString);  

Within our initializeMap() method we instantiate the map in the usual way, using the passed in parameters to set the mapCenter and zoom properties:

 function initializeMap(lat, lng, zoom) {  
      var mapCenter = new google.maps.LatLng(lat, lng);  
      var mapOptions = {  
           center : mapCenter,  
           zoom : zoom  
      };  
      var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);  
 }  

That will give us our map centered on and zoomed into the location of our choosing. But what about StreetView?

Well, StreetView doesn't require much extra from us. To instantiate it, we need to use a google.maps.StreetViewPanorama object instead of google.maps.Map, and a StreetViewPanoramaOptions object instead of MapOptions.

 var panoramaOptions = {  
      position : mapCenter,  
      pov : {  
           heading : 34,  
           pitch : 10  
      }  
 };  
 var panorama = new google.maps.StreetViewPanorama(document.getElementById('pano'), panoramaOptions);  

To set the correct location, we just use the same object in the panoramaOptions.position property that we used to set the centre of the map. You can see that there is also a pov property that allows us to set a heading and a pitch. These correspond to the rotation and up/down camera position respectively. I've just hardcoded these for this example, but there is no reason why you couldn't use these as query parameters too, if you wanted to have more control over your user's experience of StreetView in your application.

Finally, we add the panorama object to the web page:

 var panorama = new google.maps.StreetViewPanorama(document.getElementById('pano'), panoramaOptions);  

You can try it out here. There are a few predefined locations you can zoom into, or you can enter your own map coordinates. If you're struggling to find the coordinates for your chosen location, I have written a little helper app. Just make sure StreetView is enabled at your location. You can check StreetView coverage here.

Wednesday, 1 October 2014

Google Maps New "My Maps" Interface

Google Maps has announced the new My Maps interface (previously Google Map Engine Lite), which makes it really easy to create and share simple map, including uploading your own data files, and rendering based on columns within your data tables.

I had a quick play of this earlier and recorded the results. I hope it helps you get familiar with the new My Maps interface and encourages you to have a play!



What do you think of My Maps? Would you consider upgrading to the paid version?

Sunday, 21 September 2014

Quickly Finding the Center Point and Zoom Level in Google Maps

When I'm putting together Google Maps API applications for demos or classroom exercises, I often start off zoomed into a particular location to spare students the pain of watching my clumsy efforts to find the area I'm interested in.

I've got a couple of methods I use to grab the current zoom level and lat/lng of the center point. I can then put these into a mapOptions object and have the map position itself to this location automatically when the page loads.

Method number one is a little app I wrote specifically for this purpose. You can see it above and access it here.

Just zoom to the extent you want to set your map at and press the "Create MapOptions for this Location" button.

You'll be presented with a dialog box, which formats the current location into a mapOptions object ready for pasting into your app.

There's another way of getting this information though, just by using a little bit of code in your browser console. But be warned: it only works when the variable you use for the map object is global and actually called "map".

Here's the code:
 (function() {console.log("Lat:" + map.getCenter().lat().toFixed(3) + ", Lng:" + map.getCenter().lng().toFixed(3) + " Zoom Level:" + map.getZoom() );})();  

It's pretty ugly, but it does the job. Basically, this is just a self-executing function that calls getter functions on the global map variable, and outputs the display to the console:

Paste code into console and click Run
I don't want to type this out every time I use it, so I have got it configured as a keyboard shortcut using Autohotkey. (If you're on Windows and you haven't checked out AutoHotKey yet, you're doing yourself a disservice. AutoHotKey lets you automate just about any Windows task, from text expansion to changing display resolution, and I'm lost on any machine where my AutoHotKey scripts are not installed.) 

AutoHotKey Script
 :*:@@map::  
 (  
 (function() {console.log("Lat:" + map.getCenter().lat().toFixed(5) + ", Lng:" + map.getCenter().lng().toFixed(5) + " Zoom Level:" + map.getZoom() );})();  
 )  

Without delving into the syntax, this means that every time I type @@map (I had to suspend the script just to type that!), the function executes and returns the map details.

I find these methods useful on an almost daily basis. I hope you get as much mileage out of them as I do! What are your favourite Google Maps API tips?

Tuesday, 9 September 2014

Rubber Band Zooming With the Google Maps API

One of the things I really like about working with Esri’s ArcGIS Server API for JavaScript is the out of the box zoom capability, using what Esri calls “rubber band” zooming.

To zoom in, you just hold down the SHIFT key and draw a rectangle on the map using the mouse. The map then zooms to that extent. To zoom out again, hold down SHIFT and CONTROL and draw another rectangle.

So I got to thinking about what it would take to enable this functionality using the Google Maps API.

First, I needed a way to check which keys are being pressed at the time the map drawing occurs. The approach I used was to store any depressed keys in an array which I can then inspect when the user starts drawing on the map. If the SHIFT key is depressed, then my app needs to take notice, because the user plans to either zoom in (SHIFT + mouse draw), or zoom out (SHIFT + CTRL + mouse draw):

 var keys = [];  
 onkeydown = onkeyup = function(e) {  
      e = e || event; // for IE  
      keys[e.keyCode] = e.type == 'keydown';  
      if (keys[16]) {  
           drawingManager.setMap(map);  
      } else {  
           drawingManager.setMap(null);  
      }  
 }  

If the user has the SHIFT key depressed then they are about to draw an extent on the map, so I enable the Google Maps API DrawingManager. The drawing mode is set to RECTANGLE because a rectangle has a bounds property I can use for the new extent. I want everything to be keyboard-driven, so I hide the drawing control:


 var drawingManager = new google.maps.drawing.DrawingManager({  
      drawingMode: google.maps.drawing.OverlayType.RECTANGLE,  
      drawingControl: false  
 });  

When the user draws on the map, we want to see if either the SHIFT key (keyCode=16) and/or the CTRL key are pressed. (I got the key codes from this handy site.)

If the user is zooming in, then we get the bounds of the rectangle they have drawn on the map and call the map's fitBounds() method to zoom to that extent:

 google.maps.event.addListener(drawingManager, "rectanglecomplete", function(rect) {  
      // user zooming in  
      if (keys[16]) {  
           map.fitBounds(rect.bounds);  
           rect.setMap(null);  
           return;  
      }  
 });  

But what to do about zooming out? To be honest, I'm not 100% sure how Esri implement this, but for my purposes it's good enough just to recenter the map at the center of the rectangle the user has drawn and then zoom out one level:

 google.maps.event.addListener(drawingManager,"rectanglecomplete", function(rect) {  
      // user zooming out  
      if (keys[16] || keys[17]) {  
           map.setCenter(rect.getBounds().getCenter());  
           var zoom = map.getZoom();  
           map.setZoom(--zoom);  
           rect.setMap(null);  
           return;  
      }  
      // user zooming in  
      if (keys[16]) {  
           map.fitBounds(rect.bounds);  
           rect.setMap(null);  
           return;  
      }  
 });  

I check for the user zooming out first (i.e. CTRL (keycode=17) is pressed in addition to SHIFT (keycode=16)), so I can drop to the next test (SHIFT only) if CTRL is not depressed.

You can see the JS Fiddle for it here.

And that's about it. It's not perfect, and I'd want to do a bit more to it if I was using it in a production app, but it's a workable solution. If I'm unhappy about anything it's the zooming out behavior. Could I zoom out based on the size of the extent, perhaps? How would you implement this? Let me know in the comments!

Greetings!

I've been meaning to get this site up and running for a while now, but with the recent release of my very first Pluralsight course, now seemed a good time to do it.

I love maps, and I love the Internet, so putting maps on the web is just about enough to send me over the edge. In this blog I intend to cover many different aspects of web mapping, including, (but certainly not limited to), the Google Maps API and ESRI's ArcGIS API for JavaScript.

So please watch this space. I've got several posts planned over the next couple of weeks, covering all sorts of webmappingy goodness, so I hope you'll find something of interest.

Thanks for stopping by. Don't be a stranger ;)