Modest Maps

Python ws-compose and ws-pinwin Examples

ws-compose is an HTTP interface (written in Python) to the Modest Maps libraries for fetching and compositing map tiles in to a single static image.

ws-pinwin does everything that ws-compose does but also allows you to plot one or more (Flickr-style “pinwin”) markers at specific coordinates on the map.

Being HTTP interfaces, using either is as simple as constucting a URL, with the correct parmaters, and then retrieving it with either a GET or POST request. Examples of all of the variations are available in ws-examples.php script which we’ll walk through with comments below.

The setup

First, create a simple class to handle the details like what address and port your ws-compose server is running on. It will also hide the actual HTTP request and response by wrapping everything in a single fetch method that takes a hash of arguments and returns an image object.

class ModestMaps {
        
    function ModestMaps ($host='127.0.0.1', $port='9999'){
        $this->host = $host;
        $this->port = $port;
    }

    function fetch (&$args){
        $url = $this->build_url($args);

        $opts = array('http' => array('method'=>"GET", 'timeout' => 300));
        $ctx = stream_context_create($opts);

        $data  = file_get_contents($url, false, $ctx);
        return imagecreatefromstring($data);
    }

    function build_url (&$args){

        $params = array();

        foreach ($args as $key => $value){
            
            if (! is_array($value)){
                $value = array($value);
            }

            foreach ($value as $v){
                $enc_value = urlencode($v);
                $params[] = "$key=$enc_value";
            }
        }

        return "http://{$this->host}:{$this->port}?" . implode("&", $params);
    }
}

Next create some simple wrapper functions for calling the different servers and testing the various inputs and outputs. This isn’t necessary to actually use ws-compose, or ws-pinwin, but tests without feedback are pretty boring it turns out.

function test_ws_compose($label, &$args){
    $mm = new ModestMaps('127.0.0.1', '9998');
    return run_test($mm, $label, $args);
}

function test_ws_pinwin($label, &$args){
    $mm = new ModestMaps('127.0.0.1', '9999');
    return run_test($mm, $label, $args);
}

function run_test ($mm, $label, &$args){

    echo "test :  $label\n";
    
    foreach ($args as $key => $value){
        $v = (is_array($value)) ? implode(";", $value) : $value;
        echo "$key : $v\n";
    }

    $url = $mm->build_url($args);
    $res = ($mm->fetch($args)) ? "OK" : "FAIL";

    echo "fetch : {$url}\n";
    echo "result : {$res}\n";
    echo "\n";
}

ws-compose.py

The simplest map you can create is one centered on a specific latitude and longitude, constrained by a combination of the zoom, height and width arguments.

$args  = array(
           'provider' => 'GOOGLE_ROAD',
           'method' => 'center',
           'latitude' => '45.521375561025756',
           'longitude' => '-73.57049345970154',
           'zoom' => 15,
           'height' => 500,
           'width' => 500
           );

test_ws_compose("simple map centered by lat/lon", $args);
http://127.0.0.1:9998?provider=GOOGLE_ROAD&latitude=45.521375561025756&longitude= \
    -73.57049345970154&zoom=15&height=500&width=500

Instead of centering the map on a given point you can also define a bounding box. Bounding box maps are further limited by the value of the method argument. The first option is “extent” which will tell ModestMaps to calculate the map’s final zoom level relative to the final image’s dimensions in order to fit the entirety of the bounding box.

For example, if you specify a very large bounding box and a small-ish image ModestMaps will almost certainly zoom out to the state or country level.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'extent',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'height' => 1024,
          'width' => 1024,
          );

test_ws_compose("simple map by bbox + extent", $args);
http://127.0.0.1:9998?provider=GOOGLE_ROAD&method=extent&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&height=1024&width=1024

The second option is “bbox” which instead determines the final image’s dimension based on the zoom argument. ModestMaps will fetch all of the tiles necessary to render the bounding box and that zoom level, and doesn’t concern itself with the dimensions of the final image. It may be very large.

The image’s final width and height are reported back in the x-wscompose-image-width and x-wscompose-image-height HTTP headers respectively.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'bbox',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'zoom' => 15,
          );

test_ws_compose("simple map by bbox + zoom", $args);
http://127.0.0.1:9998?provider=GOOGLE_ROAD&method=bbox&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&zoom=15

ws-pinwin.py

ws-pinwin allows you add Flickr-style pinwin markers to your map. ModestMaps takes care of sorting out where on the map the marker should fall, converting latitudes and longitudes in to equivalent x and y coordinates.

Each marker is defined by a string containing the following comma-separated items : a unique label to identify the marker; the latitude where the marker should be placed; the longitude where the marker should be placed.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'bbox',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'zoom' => 14,
          'marker' => 'roy,45.521375561025756,-73.57049345970154',
          );

test_ws_pinwin("pinwin map by bbox + zoom w/ marker", $args);
http://127.0.0.1:9999?provider=GOOGLE_ROAD&method=bbox&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&zoom=14&marker=roy%2C45.521375561025756%2C-73.57049345970154

The default size for markers is 75 x 75 pixels. To change the width and height of a pinwin, simply append the new values (also as comma-separated values) to the string definition for that marker.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'bbox',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'zoom' => 14,
          'marker' => 'roy,45.521375561025756,-73.57049345970154,500,180',
          );

test_ws_pinwin("pinwin map by bbox + zoom w/ marker + custom pinwin", $args);
http://127.0.0.1:9999?provider=GOOGLE_ROAD&method=bbox&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&zoom=14&marker=roy%2C45.521375561025756%2C-73.57049345970154%2C \
    500%2C180

If a marker is close enough to the edge of the final map image it may extend beyond the image’s borders. By passing the bleed parameter you can ask ModestMaps to expand the height and width of the final image accordingly, adding enough of a plain white background to ensure that the marker is not cropped.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'bbox',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'zoom' => 14,
          'marker' => 'roy,45.521375561025756,-73.57049345970154,500,180',
          'bleed' => 1,
          );

test_ws_pinwin("pinwin map by bbox + zoom w/ marker + custom pinwin and bleed", $args);
http://127.0.0.1:9999?provider=GOOGLE_ROAD&method=bbox&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&zoom=14&marker=roy%2C45.521375561025756%2C-73.57049345970154%2C \
    500%2C180&bleed=1

You can create as many markers as you want, within reason, by passing the ws-pinwin server multiple marker parameters. In this example, we simply pass an array of marker definitions to our wrapper function which sorts out the URL parameters.

Overlapping markers are automatically repositioned, with precedence given the front-most marker; the marker positioned furthest along the image’s y-axis. The height of the anchor belonging to the marker that is overlapped will be adjusted until no part of its “canvas” is covered by the marker in front of it. This process is repeated until no markers overlap.

There are limits to this process and a large number of markers densely clustered can result in ridiculously huge images or cause the ws-pinwin server to run out of memory.

$args = array(
          'provider' => 'GOOGLE_ROAD',
          'method' => 'bbox',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'zoom' => 14,
          'marker' => array(
                'roy,45.521375561025756,-73.57049345970154,500,180',
                'mileend,45.525825499457,-73.5989034175872,600,180',
                'cherrier,45.51978191639917,-73.56947422027588',
                ),
          'bleed' => 1,
          );

test_ws_pinwin("pinwin map by bbox + zoom w/ multiple markers and bleed", $args);
http://127.0.0.1:9999/?provider=GOOGLE_ROAD&method=bbox&bbox=45.482882%2C-73.619899%2C \
    45.532687%2C-73.547801&zoom=14&marker=roy%2C45.521375561025756%2C-73.57049345970154%2C \
    500%2C180&marker=mileend%2C45.525825499457%2C-73.5989034175872%2C600%2C180&marker= \
    cherrier%2C45.51978191639917%2C-73.56947422027588&bleed=1

Finally, ws-pinwin supports the ability to define filters which are applied to a composited map image prior to the addition of any markers. At the moment, there is only one filter : “atkinson” which will apply a dithered, halftone, effect to the map.

The atkinson filter takes a long time. For big maps, it takes even longer.

$args = array(
          'provider' => 'MICROSOFT_AERIAL',
          'bbox' => '45.482882,-73.619899,45.532687,-73.547801',
          'filter' => 'atkinson',
          'height' => 1024,
          'width' => 2048,
          'method' => 'extent',
          'marker' => array(
                'roy,45.521375561025756,-73.57049345970154,500,180',
                'mileend,45.525825499457,-73.5989034175872,600,180',
                'cherrier,45.51978191639917,-73.56947422027588',
                ),
          'bleed' => 1,
          );

test_ws_pinwin("pinwin map by extent w/ multiple markers, filtering and bleed", $args);
http://127.0.0.1:9999/?provider=GOOGLE_ROAD&method=extent&bbox=45.482882%2C \
    -73.619899%2C45.532687%2C-73.547801&height=1024&width=2048&marker=roy \
    %2C45.521375561025756%2C-73.57049345970154%2C500%2C180&marker=mileend \
    %2C45.525825499457%2C-73.5989034175872%2C600%2C180&marker=cherrier \
    %2C45.51978191639917%2C-73.56947422027588&bleed=1&filter=atkinson

But wait! There’s more!!

It’s nice to generate a map with lots of empty pinwins but there still needs to be a way to add content to those markers. For example :

Paris, 2007

This is convered in detail in the advanced examples section : Adding images to markers.