Running two (or more) instances of TileCache.

Posted by – Thursday 2010-09-16

TileCache is caching system that caches a Web Map Service (WMS) at a discrete set of map resolutions. Sometimes two (or more) WMS caches working at different sets of map resolutions are needed, and a possible solution can be running two (or more) instances of TileCache.

In first section we briefly introduce WMS caches and TileCache.

In the second section, it is seen why running two instances of TileCache it is needed. We have created a dynamic map, based on the OpenLayers library, which is inserted in a web page. Depending on client display resolution, the map can be shown cropped. To avoid it, on map initialization its resolution (map units per pixel) is changed in function of client display resolution. A side effect of this solution is that in case the WMS layers are cached, we will need to run more than one instance of TileCache.

In the third section we show how to install and configure one instance of TileCache on a Linux server.

Finally, in the fourth section the previous setup is generalized to install two instances of TileCache.

1. About WMS caches and TileCache.

Rendering WMS images is a CPU intensive task, usually resulting in high load times on client side. Thus, WMS cache systems, that store and send already rendered images as response to WMS GetMap requests, are of interest for two resasons: on server side the rendering work already done by the computer is reused, and on client side load times decrease by orders of magnitude.

WMS caches are conceptually very similar to other types of caches. When a WMS GetMap request is received by a WMS cache, a cache hit or a cache miss will happen. In case of cache hit, the requested image is already rendered and stored in the WMS cache and it is sent to the requesting client. In case of cache miss, the requested image is not present in the WMS cache, so (a) the request is forwarded to the WMS server, (b) the WMS server renders the image, (c) the rendered image is sent to the WMS cache, (d) the rendered image is sent to client and (e) it is also stored in the WMS cache. Note that it is assumed that clients must always send all their WMS requests to the WMS cache, not to the WMS server.

Requested images are usually 256×256 pixels tiles. For a given map resolution and bounding box, the ‘total’ layer image is composed by arranging the requested tiles. Thus, given a bounding box and tile dimensions the space gets ’tiled’.

Another constraint is the set of map resolutions. Resolution is defined as the existing relation between the used projection system units and display pixel, typically meters or degrees per pixel [note 1]. For every cached layer, only a discrete, finite set of map resolutions can be cached. If the map resolution set were not finite, a WMS cache would require infinite storage space.

Although limiting, this discrete, finite set constraint is not always an issue. For client side applications like Google Maps or those based on the OpenLayers library, a WMS cache is enough, because by design they show maps at a discrete, finite number of zoom levels (or, equivalently, map resolutions) [note 2].

Summarizing, for our discussion there will be in total three constraints in a WMS cache: bounding box, tile dimensions and the discrete, finite set of map resolutions [note 3]. These constrains sample the space, and this sort of spatial sampling is what is stored in the WMS cache.

On April 2010 the Open Geospatial Consortium (OGC) released the Web Map Tile Service[1] standard (WMTS), version 1.0.0. Before the release of this standard, proposals like the WMS Tiling Client Recommendation[2] (WMS-C) already existed. In this post we will focus on TileCache[3], from MetaCarta Labs, an implementation of the WMS-C recommendation.

2. Same map, different map resolutions.

One of the problems web page developers face is dealing with different – and usually unknown – client display resolutions. Client’s display resolution must not be confused with map resolution.

The following pictures show the nature of the problem that can arise when the same map is show at different client display resolutions, given the same map resolution. Note that in both figures the current zoom level is the same, 0.

figure 1
figure 1

figure 2
figure 2 (cropped map image)

The pictures show a map viewer inserted in a web page. Map viewers are build with the Javascript OpenLayers library, version 2.9.1. Actually both viewers are the exactly the same, but running in two computers with different display resolutions. The geographical information shown is the municipalities boundaries – represented with red, dashed lines – of Lugo, a province of northwestern Spain. In figure 1 the whole boundaries can be seen and in figure 2 some of them are cropped.

The viewer JavaScript code is:

var WMS_SERVER = 'http://myserver/sdi-lugo?service=WMS';

var map;
var maxResolution = 200;

function initMap() {
   var mapOptions = {
      'units': 'm',
      'numZoomLevels': 10,
      'projection': 'epsg:23029',
      'maxExtent': new OpenLayers.Bounds(580000, 4688000, 680000, 4850000),
      'maxResolution': maxResolution,
      'controls': []
   };
   map = new OpenLayers.Map('map', mapOptions);

   var municipalities = new OpenLayers.Layer.WMS('municipalities', WMS_SERVER, {
      'layers': 'municipalities',
      'format': 'image/png',
      'transparent': false
   }, {
      'isBaseLayer': true
   });
   map.addLayer(municipalities);

   map.addControl(new OpenLayers.Control.Navigation());
   map.addControl(new OpenLayers.Control.LayerSwitcher());
   map.addControl(new OpenLayers.Control.PanZoomBar());

   map.zoomToMaxExtent();
}

The map is initialized with the parameters grouped in the mapOptions object. The set of map resolutions is calculated from the parameters maxResolution and zoomLevels. There are ten map resolutions, which are 200, 100, 50, 25, 12.5… Note that in this sucession the nth element is the middle of the previous one [note 4]. It is also worth noting that a change in the parameter maxResolution implies changing the set of map resolutions [note 5].

In a JavaScript program, display resolution can be detected using the Screen object. Thus, on map initialization the value of the maxResolution parameter can be set depending on the display resolution returned by the Screen object. As stated before, setting the maxResolution parameter implies setting all the map resolutions. Thus, a possible solution to the problem of the cropped map image could be setting the value of the variable mapMaxResolution depending on the value of the display resolution:

var mapMaxResolution = 200;
if ((screen.width < 1280) || (screen.height < 960)) {
     mapMaxResolution = 300;
}

By increasing map resolution each pixel will show a higher portion of the map, so it could be a solution for the problem of cropped images. However, in our case WMS requests will be sent to a WMS cache - that works at a fixed set of map resolutions -, so the previous solution would not always work.

3. Installing and configuring one instance of TileCache.

Let's see how to install and configure one instance of TileCache, version 2.10. Our configuration will consist on a lighttpd HTTP server waiting for WMS requests, that are forwarded to the TileCache instance. The lighttpd HTTP server and the TileCache instance comunicate the each other via the FastCGI protocol. The environment will be a Debian Linux server. In next section, we will generalize this setup to install two (or more) instances of TileCache.

If using the package system, installing lighttpd and TileCache is straightforward. From a shell execute:

$ sudo apt-get install lighttpd
$ sudo apt-get install tilecache

Since we will use the FastCGI protocol to communicate lighttpd and TileCache, the Python packages flup and Paste are needed. Again, the installation can be accomplished using the package system:

$ sudo apt-get install python-flup python-paste

Let's see how to configure lighttpd, version 1.4.19, to forward the WMS requests to the TileCache instance via FastCGI. Obviously, lighttpd's FastCGI module must be enabled and requests sent to the WMS cache must be 'captured' to be then redirected to TileCache. This can be done by appending the following lines to the lighttpd configuration file, /etc/lighttpd/lighttpd.conf [note 6, 7, 8]:

server.modules += ("mod_setenv")
$HTTP["url"] =~ "/sdi-lugo" {
        setenv.add-environment = ("MS_MAPFILE" => "/path/to/mapfile")
}

server.modules += ("mod_fastcgi")
fastcgi.server = (
   "/sdi-lugo" => ((
      "socket" => "/tmp/mapserver-fastcgi.socket",
      "check-local" => "disable",
      "bin-path" => "/usr/lib/cgi-bin/mapserv",
      "min-procs" => 1,
      "max-procs" => 6,
      "max-load-per-proc" => 32,
      "idle-timeout" => 200
   )),
   "/wms-cached" => ((
      "socket" => "/tmp/tilecache.socket",
      "check-local" => "disable",
      "bin-path" => "/usr/lib/cgi-bin/tilecache.fcgi",
      "min-procs" => 1,
      "max-procs" => 16,
      "max-load-per-proc" => 2,
      "idle-timeout" => 20
   ))
)

Now, all HTTP requests received by lighttpd whose URL matchs the regular expression ^/wms-cached are forwarded to the TileCache (bin-path parameter) instance; the communication is done through the socket /tmp/tilecache.socket.

Let's see how to configure the TileCache instance. Its configuration file, /etc/tilecache.cfg, looks like:

[cache]
type=Disk
base=/mnt/geodata/tilecache/

[municipalities]
debug=yes
type=WMSLayer
url=http://myserver/sdi-lugo?service=WMS&transparent=true
extension=png
size=256,256
bbox=580000,4688000,680000,4850000
layers=municipalities
srs=EPSG:23029
extent_type=loose
maxResolution=200
levels=10

This cache type is DiskCache [note 9], which means that tiles are stored in the hard disk, in our case in the directory /mnt/geodata/tilecache/. The set of map resolutions this cached layer works at is given by the parameters maxResolution and levels [note 10, 11]. Any WMS request received asking for a tile whose map resolution does not belong to set map resolutions will raise an error. Note that in the url parameter, the WMS request parameter transparent has been set to true [note 12].

As seen before, with this configuration we are limited to one set of map resolutions. Actually, this is not true because we can sample the WMS layer twice, at different sets of map resolutions each time. The TileCache configuration file would be similar to:

[cache]
type=Disk
base=/mnt/geodata/tilecache/

[municipalities-200]
debug=yes
type=WMSLayer
url=http://myserver/sdi-lugo?service=WMS&transparent=true
extension=png
size=256,256
bbox=580000,4688000,680000,4850000
layers=municipalities
srs=EPSG:23029
extent_type=loose
maxResolution=200
levels=10

[municipalities-300]
debug=yes
type=WMSLayer
url=http://myserver/sdi-lugo?service=WMS&transparent=true
extension=png
size=256,256
bbox=580000,4688000,680000,4850000
layers=municipalities
srs=EPSG:23029
extent_type=loose
maxResolution=300
levels=10

In this TileCache configuration file we can see that there are two layers: 'municipalities-200' and 'municipalities-300'; final number in layer name - not mandatory - indicates the map maximum resolution. On client side, depending on display resolution the layer 'municipalities-200' or the layer 'municipalities-300' will be queried.

var WMS_SERVER = 'http://myserver/sdi-lugo?service=WMS';
var WMS_SERVER_CACHED = 'http://myserver/wms-cached';
 
var map;
var maxResolution;
 
function initMap() {
   if ((screen.width < 1280) || (screenHeight < 960)) {
      mapMaxResolution = 300;
   } else {
      mapMaxResolution = 200;
   }

   var mapOptions = {
      'units': 'm',
      'numZoomLevels': 10,
      'projection': 'epsg:23029',
      'maxExtent': new OpenLayers.Bounds(580000, 4688000, 680000, 4850000),
      'maxResolution': maxResolution,
      'controls': []
   };
   map = new OpenLayers.Map('map', mapOptions);
 
   var municipalities;
   if ((screen.width < 1280) || (screenHeight < 960)) {
      municipalities = new OpenLayers.Layer.WMS('municipalities', WMS_SERVER_CACHED, {
         'layers': 'municipalities-300',
         'format': 'image/png',
         'transparent': false
      }, {
         'isBaseLayer': true
      });
   } else {
      municipalities = new OpenLayers.Layer.WMS('municipalities', WMS_SERVER_CACHED, {
         'layers': 'municipalities-200',
         'format': 'image/png',
         'transparent': false
      }, {
         'isBaseLayer': true
      });
   }

   map.addLayer(municipalities);
 
   map.addControl(new OpenLayers.Control.Navigation());
   map.addControl(new OpenLayers.Control.LayerSwitcher());
   map.addControl(new OpenLayers.Control.PanZoomBar());
 
   map.zoomToMaxExtent();
}

This solution seems fine... provided your viewer works with a low number of layers. If you had 30 layers instead of 1, then you would have to write code to create 60 layers. And if you had 30 layers and 3 sets of map resolutions instead of 2, then you would have to write code to create 90 layers. In all cases, only 30 layers would be created by the JavaScript engine on execution time.

A more elegant and useful solution would look like this one:

var WMS_SERVER = 'http://myserver/sdi-lugo?service=WMS';
var WMS_SERVER_CACHED;
 
var map;
var maxResolution;
 
function initMap() {
   if ((screen.width < 1280) || (screenHeight < 960)) {
      mapMaxResolution = 300;
      WMS_SERVER_CACHED = 'http://myserver/wms-cached-300';
   } else {
      mapMaxResolution = 200;
      WMS_SERVER_CACHED = 'http://myserver/wms-cached-200';
   }

   var mapOptions = {
      'units': 'm',
      'numZoomLevels': 10,
      'projection': 'epsg:23029',
      'maxExtent': new OpenLayers.Bounds(580000, 4688000, 680000, 4850000),
      'maxResolution': maxResolution,
      'controls': []
   };
   map = new OpenLayers.Map('map', mapOptions);
 
   var municipalities = new OpenLayers.Layer.WMS('municipalities', WMS_SERVER_CACHED, {
      'layers': 'municipalities-300',
      'format': 'image/png',
      'transparent': false
   }, {
      'isBaseLayer': true
   });

   map.addLayer(municipalities);
 
   map.addControl(new OpenLayers.Control.Navigation());
   map.addControl(new OpenLayers.Control.LayerSwitcher());
   map.addControl(new OpenLayers.Control.PanZoomBar());
 
   map.zoomToMaxExtent();
}

As it can be seen, a simple change in the URL pointing to the queried TileCache instance (variable WMS_CACHED) is enough. This solution requires two running instances of TileCache, each serving a set of map resolutions. In next section we will se how to install two instances of TileCache.

4. Installing and configuring two instances of TileCache.

The installation of TileCache seen in the previous section is systemwide. Thus, if we want to run two (or more) instances of TileCache, a not-systemwide method is needed.

Another way of installing TileCache is as simple as downloading the source distribution and unpacking it. This installation is not systemwide. In our case, we have to unpack the source distribution twice, and we do it in the directories /var/www/tilecache-max-map-res-200 and /var/www/tilecache-max-map-res-300. Since the lighttpd HTTP server run as www-data:www-data (its owner is user www-data, which belongs to the group www-data), it is advisable setting the owner and group of these directories and its whole contents to www-data:www-data.

On the other hand, we will continue to use the FastCGI protocol to communicate the instances of TileCache and the lighttpd HTTP server. Thus, the Python packages python-flup and python-paste remain necessary.

Now there will be two TileCache configuration files: /var/www/tilecache-max-map-res-200/tilecache.cfg and /var/www/tilecache-max-map-res-300/tilecache.cfg. Both will be identical but the directories where the tiles are stored (base parameter) and, for each layer, the set of map resolutions (in our case, the maxResolution parameter).

The contents of the configuration file /var/www/tlecache-max-map-res-200/tilecache.cfg will look like:

[cache]
type=Disk
base=/mnt/geodata/tilecache/max-map-res-200/

[municipalities]
debug=yes
type=WMSLayer
url=http://myserver/sdi-lugo?service=WMS&transparent=true
extension=png
size=256,256
bbox=580000,4688000,680000,4850000
layers=municipalities
srs=EPSG:23029
extent_type=loose
maxResolution=200
levels=10

The contents of the configuration file /var/www/tlecache-max-map-res-300/tilecache.cfg will look like:

[cache]
type=Disk
base=/mnt/geodata/tilecache/max-map-res-300/

[municipalities]
debug=yes
type=WMSLayer
url=http://myserver/sdi-lugo?service=WMS&transparent=true
extension=png
size=256,256
bbox=580000,4688000,680000,4850000
layers=municipalities
srs=EPSG:23029
extent_type=loose
maxResolution=300
levels=10

The configuration file of the lighttpd HTTP server needs some modifications. Now there will be two regular expressions filtering the requests received by the HTTP server: ^/wms-cached-200 and ^/wms-cached-300. The lighttpd configuration file is modified in this fashion:

server.modules += ("mod_setenv")
$HTTP["url"] =~ "/sdi-lugo" {
        setenv.add-environment = ("MS_MAPFILE" => "/path/to/mapfile")
}

server.modules += ("mod_fastcgi")
fastcgi.server = (
   "/sdi-lugo" => ((
      "socket" => "/tmp/mapserver-fastcgi.socket",
      "check-local" => "disable",
      "bin-path" => "/usr/lib/cgi-bin/mapserv",
      "min-procs" => 1,
      "max-procs" => 6,
      "max-load-per-proc" => 32,
      "idle-timeout" => 200
   )),
   "/wms-cached-200" => ((
      "socket" => "/tmp/tilecache-200.socket",
      "check-local" => "disable",
      "bin-path" => "/var/www/tilecache-max-map-res-200/tilecache.fcgi",
      "min-procs" => 1,
      "max-procs" => 16,
      "max-load-per-proc" => 2,
      "idle-timeout" => 20
   )),
   "/wms-cached-300" => ((
      "socket" => "/tmp/tilecache-300.socket",
      "check-local" => "disable",
      "bin-path" => "/var/www/tilecache-max-map-res-300/tilecache.fcgi",
      "min-procs" => 1,
      "max-procs" => 16,
      "max-load-per-proc" => 2,
      "idle-timeout" => 20
   ))
)


notes

[note 1] This is the usual definition of resolution, which can be refined by defining horizonal (x axis units per pixel) and vertical (y axis units per pixel) resolutions. In most cases horizontal and vertical resolution are equal, so we use the 'simplified' definition.

[note 2] OpenLayers based applications by default show maps at a finite, discrete set of zoom levels. Building map viewers showing maps at fractional zoom levels is also possible, feature that "allows zooming to an arbitrary level (between the min and max resolutions)". An example is available at http://openlayers.org/dev/examples/fractional-zoom.html.

[note 3] A instance of TileCache can cache the same WMS layer at different sets of map resolutions provided they have different names, say 'municipalities-boundaries-maxRes200' and 'municipalities-boundaries-maxRes300'. TileCache will treat them as different layers, because is the same WMS layer, 'municipalities-boundaries', but 'sampled' at different sets of map resolutions.

[note 4] Actually this kind of sucession is what I have observed. However in the OpenLayers library documentation I have not found anything saying that map resolutions are always calculated this way; it is said that if "not set in the layer map constructor, it wil it will be set based on other resolution related properties (maxExtent, maxResolution, maxScale, etc.)".

[note 5] Another way of setting the map resolutions is using the resolutions parameters, a "list of map resolutions (map units per pixel) in descending order". This list of map resolutions can follow a pattern different from maxMapRes, maxMapRes / 2, maxMapRes / 4, maxMapRes / 8... Note that the number of zoom levels will be equal to the number of elements the map resolutions list has.

[note 6] A better configuration practice is appending these lines to the file /etc/lighttpd/conf-available/10-fastcgi.conf and create a symbolic soft link to this file in the directory /etc/lighttpd/conf-enabled/.

[note 7] In the configuration shown, the WMS service is provided by an instance of MapServer, which also communicates with the lighttpd HTTP server using the FastCGI protocol.

[note 8] MapServer has a number of environment variables that control its "behavior or specify the location of some resources". One of them is MS_MAPFILE, the "Default mapfile to use if the map=... URL parameter is not provided". This variable can be set by the lighttpd HTTP server by using its setenv module, which allow us to avoid appending the map parameter to the WMS server URL.

[note 9] Other types of TileCache caches are Memcached, Google Disk and Amazon S3.

[note 10] Another way of setting map resolutions is using the resolutions parameter, a "comma seperate separated list of resolutions you want the TileCache instance to support".

[note 11] Map resolutions given on map initialization on client side (mapOptions) and in the TileCache configuration file must match.

[note 12] The url has been appended the transparent parameter set to true. We want the municipilities layer to be transparent, because it can be stacked on top of layers like a ortophoto. If this parameter is not present and set to true, TileCache will ask the WMS server for a non transparent tile.


references

[1] http://www.opengeospatial.org/standards/wmts
[2] http://wiki.osgeo.org/index.php/WMS_Tiling_Client_Recommendation
[3] http://www.tilecache.org/

0 Comments on Running two (or more) instances of TileCache.

Closed