Modest Maps

Adding images to ws-pinwin markers

This is an example of how to use the ws-pinwin HTTP server to request a map for a location, rendered with a “pinwin” style marker, on to which a source image can be pasted. It builds on topics already covered in the ws-compose and ws-pinwin tutorial.

to make this...
flickrimg
...into this
280

Much of the code below has also been wrapped up the in wscompose/client.py library but since the goal is to understand what’s going on underneath the hood we’ll walk through all of the steps explicitly in both Python, on the left, and PHP, on the right. If you prefer Perl, you can refer to code written for the Net::Flickr::Geo::ModestMaps package, available on the CPAN.

First, load the libraries we’ll need to use.

# python

import httplib
import urllib
import re
import string
import Image
import StringIO
# php

# nothing except php5 with the curl and gd extensions

For the sake of brevity, we’ll assume that we know the URL and lat/lon coordinates of the image.

# python

im_url = 'http://farm1.static.flickr.com/29/52139344_21e210d829.jpg'
lat = 37.507653
lon = -122.339994
# php

$im_url = 'http://farm1.static.flickr.com/29/52139344_21e210d829.jpg';
$lat = 37.507653;
$lon = -122.339994;

Next, fetch the image and for its dimensions.

# python

try :
    im_data = urllib.urlopen(im_url).read()
    im_obj = Image.open(StringIO.StringIO(im_data))
except Exception, e :
    raise e

(image_width, image_height) = im_obj.size
# php

$im_data = file_get_contents($im_url);

if (! $im_data){
   echo "failed to retrieve '$im_url'";
   exit;
}

$im = imagecreatefromstring($im_data);
	
if (! $im){
    echo "failed to create image from '$im_url'";
    exit;
}

$im_width = imagesx($im);
$im_height = imagesy($im);

Now that we have the basic information, build the arguments that will be passed to Modest Maps. In this case we asked for a map centered on the source image’s latitude and longitude and zoomed in to "street" level with a pinwin-style marker large enough to paste the image in to.

Markers are defined by passing one or more marker arguments, each of which is a string composed of the following comma-separated values :

In this example we also ask Modest Maps to build the map image using tiles from Microsoft and to filter the final composited version with an Atkinson dithering before sending it back.

# python

marker_args = ('example', lat, lon, im_width, im_height)
marker_args = map(str, marker_args)
marker_args = ",".join(marker_args)

mm_args = {
    'filter' : 'atkinson',
    'provider' : 'MICROSOFT_AERIAL',
    'method' : 'center',
    'latitude' : lat,
    'longitude' : lon,
    'zoom' : 17,
    'height' : 1239,
    'width' : 1771,
    'marker' : marker_args,
    }

mm_params = urllib.urlencode(mm_args)

mm_url = '127.0.0.1:9999'
mm_endpoint = "/?%s" % mm_params
# php

$marker_args = array(
	   'example',
        $lat,
        $lon,
        $im_width,
        $im_height
);

$marker_args = implode(",", $marker_args);
	
$mm_args = array(
		 'filter=' . 'atkinson',
		 'provider=' . 'MICROSOFT_AERIAL',
		 'method=' . 'center',
		 'latitude=' . urlencode($lat),
		 'longitude=' . urlencode($lon),
		 'zoom=' . 17,
		 'height=' . 1239,
		 'width=' . 1771,
		 'marker=' . urlencode($marker_args),
		 );
	
$map_url = '127.0.0.1:9999/?' . implode("&", $mm_args);

Fetch the map image. Note that if the ws-pinwin server encounters a problem it will return error details in the x-errorcode and x-errormessage HTTP headers.

# python

try :
    conn = httplib.HTTPConnection(mm_url)
    conn.request("GET", mm_endpoint)
    res = conn.getresponse()
except Exception, e :
    raise e

if res.status != 200 :

    if res.status == 500 :
        code = res.getheader('x-errorcode')
        msg = res.getheader('x-errormessage')
        errmsg = "(%s) %s" % (code, msg)
        raise Exception, errmsg
    else :
        raise Exception, res.message   

conn.close()
# php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $map_url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$res = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close ($ch);

if ($err){
    print "failed to retrieve '{$map_url}' : {$err}";
    exit;
}

if ($status != 200){

    list($head, $data) = explode("\r\n\r\n", $res, 2);
    $details = array();

    foreach (explode("\r\n", $head) as $ln){

        list($k, $v) = explode(":", $ln, 2);
        $k = trim($k);
        $v = trim($v);

        if (preg_match("/^x-error/i", $k)){
            $k = str_replace("x-", "", strtolower($k));
            $details[$k] = strtolower($v);
        }
    }

    echo "({$details['errorcode']}) {$details['errormessage']}";
    exit;
}

Iterate over the HTTP headers looking for ones that begin with x-wscompose. These will contain information about the image like height and width as well as the x and y coordinates where our original image can pasted into. Metadata is further broken down into marker and minor classifactions. For example, all the markers for a map would begin with marker-.

The label assigned to the marker in this example is “example” so its data would be returned in the x-wscompose-marker-example header.

# python

meta = {}

re_xheader = re.compile(r"^x-wscompose-", re.IGNORECASE)

for key, value in res.getheaders() :
        
    if re_xheader.match(key) :
            
        parts = key.split("-")
        parts = map(string.lower, parts)
        
        major = parts[2]
        minor = parts[3]
        
        if not meta.has_key(major) :
            meta[major] = {}
                
        meta[major][minor] = value
# php

list($head, $data) = explode("\r\n\r\n", $res, 2);
$map = imagecreatefromstring($data);

$meta = array();

foreach (explode("\r\n", $head) as $ln){

	list($k, $v) = explode(":", $ln, 2);
	$k = trim($k);
	$v = trim($v);

	if (preg_match("/^x-wscompose-/i", $k)){

		$parts = explode("-", $k);
			
		$major = strtolower($parts[2]);
		$minor = strtolower($parts[3]);
			
		if (! isset($meta[$major])){
			$meta[$major] = array();
		}
			
		$meta[$major][$minor] = $v;
	}
}

Most headers are self-explanatory; markers are a little more complicated. The string after x-wscompose-marker- is the label assigned to the marker when the API call was made. The value is a comma separated list interpreted as follows :

Pick out the x and y offset that we can use as the top left corner where we will paste the original image on to the pinwin marker.

# python

coords = map(int, meta['marker']['example'].split(","))
            
x_off = coords[2]
y_off = coords[3]
# php

$coords = explode(",", $meta['marker']['example']);
	
$x_off = $coords[2];
$y_off = $coords[3];

(Finally) paste the source image in to the map and display it!

# python

map_data = res.read()
map_obj = Image.open(StringIO.StringIO(map_data))

map_obj.paste(image_obj, (x_off, y_off))
map_obj.show()
# php

imagecopy($map, $im, $x_off, $y_off, 0, 0, $im_width, $im_height);
imagepng($map);

Profit!