Ways to make feature tile layers interactive

Most maps do not just consist of a static base map. Usually you want to visualize some additional data on top of that base map. This additional data, for example POIs, tracks or boundaries, should be presented in a way that the user can interact with the data.

If your data does not exceed a certain size, the browser does a great job in rendering the data and taking care of the interaction. Just download the feature geometries to the client using GeoJSON, KML, or some other vector format, and use SVG or canvas to render the geometries.

But if you try to map all deaths in the Iraq war, show election data in a choropleth map or display a large number of buildings, then rendering your features inside the browser does not work that well any more. For several reasons: First, it will take some time to transfer the whole vector data to the client. Then, rendering gets slow and your map does not feel that responsive any more when panning or zooming.

So at this point you will do the same as you (or someone else) did with your base map: You will render your geometries on the server-side and serve the data as image tiles. But with plain images we are loosing interactivity! We still want to be able to click on features and we want visual feedback when hovering a feature, so that we know where to click! So, that is what this post is about: How do you preserve interactivity when using feature tiles?

In the following we will take a look at three different web mapping applications that use tiles for displaying features and we will compare the different approaches taken to make these tile layers interactive.

Google Fusion Tables

Google Fusion Tables is a service that lets you upload large sets of tabular data. The data can then be visualized using different kinds of charts, but you can also show your data on a map.

For example, this is a table with all counties of the USA. To get a map with all counties, you just have to hit Visualize » Map. The counties are rendered as red dots that you can click on. If you zoom in a bit further, the counties will be shown as polygons.

Image tiles

Now let’s try to understand how it works! The features are rendered on transparent tiles that are fetched just like the tiles of the base map. For example this is the URL for tile (3, 5) at zoom-level 4:

http://mt0.google.com/mapslt?
  lyrs=ft:210217
   |s:select col4 from 210217
   |h:false|uit:AIGcsfM_tVdc0DJ2ucYf0FrsPlxv22KwIA
   |t:1307554407
  &x=3&y=5&z=4
  &w=256&h=256
  &source=maps_api

Line 2 (lyrs=ft:210217) contains the ID of the table, the query that selects the geometries is in line 3 (select col4 from 210217). The information about which tile is requested is in line 6 (x=3&y=5&z=4).

Interactive markers

So much about the tile images. But how does the browser know on which areas of the images you can click on? At the beginning, when the map is displayed for the first time, the Google Map client library just fetches the tile images. But once you start hovering the map with your mouse pointer, the client will dynamically request position data for the tile that you are just hovering. For example a request can look like this:

http://mt0.google.com/vt/ft
  ?lyrs=ft:210217
   |s:select col4 from 210217
   |h:false|uit:AIGcsfNfL6pLSANzTVnbSlwncVdo-FccYA
   |t:1307632793
  &las=tuvt
  &z=3
  &source=maps_api
  &callback=_xdc_._bgopuz14u

The request looks similar to the tile URL, but the tile coordinates (for example: x=3&y=5) are not passed in clear text. I guess the coordinates are either encoded in line 6 (las=tuvt) or in line 9 (callback=_xdc_._bgopuz14u). The response for this request was:

_xdc_._bgopuz14u && _xdc_._bgopuz14u([
 {
  id:"tuvt",
  base:[295698432,668991488],
  zrange:[3,3],
  layer:"ft:210217
    |h:false
    |s:select col4 from 210217
    |t:1307632793
    |uit:AIGcsfNfL6pLSANzTVnbSlwncVdo-FccYA",
  features:[
   {
    id:"4:6113019567",
    a:[0,0],
    bb:[-4,-4,4,4]
   },
   {
     id:"4:6113167168",
     a:[-25165824,-2097152],
     bb:[-4,-4,4,4]
   },
   ...
  ]
}])

If the tile contains features, they are listed in the features attribute (line 11). Every feature has an identifier (id:”4:6113019567″), a position (a:[0,0]) and a bounding box in pixels relative to the position (bb:[-4,-4,4,4]) that marks the area you can click on. The feature position is probably relative to the tile position (line 4: base:[295698432,668991488]). And I am not really sure what unit Google is using for the coordinates, pixel coordinates or some real world coordinate system?

Interactive polygons

So the clickable area for points features is a rectangle, which works fine for small symbols. But for polygons and lines a rectangle would be too imprecise. That is why the response for a tile containing polygons or lines looks a bit different:

_xdc_._1kgooi20ar && _xdc_._1kgooi20ar([{
  id:"tuvuuv",
  zrange:[5,5],
  layer:"ft:210217
   |h:false
   |s:select col4 from 210217
   |t:1307550131|uit:AIGcsfOMgmq2ezQxiAgCamH-DAbYFlDFfA",
  features:
   [
    {id:"4:6112682473"},
    {id:"4:6112987488"},
    ...
   ],
  raster:"?A?A??ED?ED?ED?ED? ... D@B@wD@AEGD?D@"
}])

You still have the feature list, but now only the feature identifiers are given. Instead of feature positions you have this weird looking string in line 14 (raster:”..”). This string contains information about which feature was rendered to which pixel. At first, this sounds like a lot of data, but there are not many responses larger than 10 KB (gzipped). Unfortunately there is no information available about how this works in detail, but our next application uses a very  similar approach.

Mapbox: TileMill

Our next example is the tile server TileMill based on node.js and developed by DevelopmentSeed. First, let’s take a look at this beautiful map made with TileMill using OpenLayers as mapping client:

Like Google Fusion Tables, TileMill uses two different files for feature tiles: the tile image and a JSON file containing information that makes the image interactive. The JSON file is also loaded on-the-fly when you hover a tile. But unlike Google Fusion Tables, TileMill is an open-source project (GitHub: mapbox/tilemill), so that we can actually see how it works.

UTFGrid

For TileMill the format of these JSON files is specified in the MBTiles specification (MBTiles: UTFGrid). For example this is the JSON file for tile (7,10) at zoom level 4 (the strings in line 3, 5 and 6 were shortened):

grid({
  "grid":[
    "                !!!!!!!!!!!!!!!    ",
    ...
    "         )))))))))(((((((((((((((((",
    "          )))))))(((((((((((((((((("],
  "keys":[
    "",
    "GBR",
    "IRL",
    "IMN",
    "FRA",
    "GGY",
    "JEY",
    "ESP",
    "PRT"],
  "data":{
    "ESP":{"ABBREV":"Sp.", ...},
    "FRA":{"ABBREV":"Fr.", ...},
    ...}
});

The JSON string contains three main attributes: grid (line 2), keys (line 7) and data (line 17). The attribute keys contains a list of all feature identifiers and the optional attribute data can be used to provide additional information for a feature (for example the country name).

The attribute grid is a bit more interesting. It is an array of 64 strings where as each string contains 64 characters. So we have got an 64×64 grid of characters. If we map this grid to our 256×256 pixel tile, we see that each character stores the data for 4×4 pixels. So a character contains the encoded feature identifier, if a feature was rendered to the corresponding 4×4 pixel area.

The following graphic shows the original tile and a screen-shot of the grid array displayed in a text editor and resized to match the 256×256 pixel tile. As you can see, the shapes look very similar.

A cell of the grid does not directly contain one of the identifiers given in the keys list. To keep the amount of data low, the identifier is encoded. For example, let’s assume the user clicks on Spain, somewhere in the lower right corner of the tile. To get the feature identifier we have to do the following:

  • We do a lookup in the grid using the pixel coordinates and get the character “(” (for example in line 6).
  • Now we take a look at the UTF table and see that the character “(” equals the codepoint value 40.
  • According to the specification (UTFGrid: Encoding IDs) we have to subtract 32 and 1 (because 40 >= 35), so we get: 40 – 32 – 1 = 7
  • The number 7 is the index of the feature identifier in the keys array. key[7] is “ESP” (line 15), so we have found Spain which is correct.

DevelopmentSeed luckily provides extensions for the mapping clients OpenLayers, Modest Maps, Leaflet and the Google Maps API, so that you do not have to care about the encoding and loading the JSON files. The library containing these extensions is called Wax (the file gridutil.js reads the grid).

To generate the JSON files, TileMill uses the Mapnik grid_renderer (via node-mapnik: the file js_grid_utils.grid2utf writes the grid). The tiles can be served using TileStream. More implementations of the MBTiles specification can be found here.

Update: Presenting on Map Interactivity Without Flash at Where 2.0 and How Interactivity Works with UTFGrid give a good explanation about UTFGrid.

Switzerland Mobility

Our last example is the map on Switzerland Mobile, which was developed by Camptocamp using MapFish and OpenLayers.

The map consists of ~50 POI and line layers that can be switched on and off arbitrarily. The different layers are rendered on the same tile so that the browser does not have to deal with keeping all tiles of all layers properly lined up when panning.

The layers are interactive: Your mouse cursor turns into a pointer when you hover a track or POI and you can click on these features.

How does it work? There is no additional JSON file for every tile like in the first two examples. A different approach is taken: When you hover the map, the client constantly keeps asking the server if there are any features nearby the position your mouse cursor is pointing to. If a new request is made before a response was received for an old request, the old request is aborted, as you can see in the following screen-shot:

If there are features within a certain distance to the mouse position, the server just returns “got something here“. This tells the client to change the cursor style to a pointer. If you click on the map, a new request is made which directly returns the HTML code to be displayed in a map pop-up.

Often there is not only one biking or hiking track at the mouse position, so all hits are displayed in the pop-up (as shown in the above screen-shot). With the current UTFGrid specification you could only get one hit, because it only allows to store one feature identifier per grid cell. But the two different approaches could be combined: You could use the grid to decide whether your mouse cursor is hovering a feature, and then, when clicking somewhere, you would make a new request. This would reduce the number of server requests and you still would get all hits.

Wrap-up

The UTFGrid specification is a promising technique to make feature tile layers interactive. The size of the JSON file containing the grid is reasonable through run-length encoding and and gzipping. Additionally Wax makes sure that only those grids are requested that are actually needed.

Wouldn’t this make a good Web Map Feature Tile Service specification (WMFTS), combining WFS and WMTS?

Advertisements

Comments are closed.

%d bloggers like this: