A small freedom area.

Map for xkcd No:1110

Fri 21 Sep 2012

fun, prog

Three times a week, Randall Munroe draws a new comic entry on xkcd.com. This Wednesday, the comic was quite unusual and pretty awesome: xkcd #1110. Basically, it's a giant map browsable with a limited field of view. I played with it for something like 12 seconds, and then decided to create a map so I wouldn't miss anything. Oh, just like pokanalysis...

This article is pretty accessible from a technical point of view, and I know a lot of other people did that as well, but I was willing to share the method I used anyway.

Download the images

First, we need to grab all the images to process them locally.

We're going to use several scripts, so we need a place for some common code, let's say in a xkcdcommon.py script:

class XKCD1110:

    def __init__(self):
        self.size = [14, 48, 25, 33]
        self.w = self.size[1] + self.size[3]
        self.h = self.size[0] + self.size[2]

    def tile_name(self, x, y):
        x -= self.size[3]
        y -= self.size[0]
        return (('%d%c' % (y + 1, 's')) if y >= 0 else ('%d%c' % (-y, 'n'))) + \
               (('%d%c' % (x + 1, 'e')) if x >= 0 else ('%d%c' % (-x, 'w')))

This is a direct mapping of the code in 1110.js.

We now know the map has width=48+33=81 and height=14+25=39. We also have a function to map the [x;y] position to the segmented image. Each image has a size of 2048x2048. This is a pretty huge map we have there.

Since there is no listing of the images available, we will have to brute-force the download (if someone has another trick I'm interested). Here is the img-url-list.py script to print the full list of the potential images on stdout:

#!/usr/bin/env python

from xkcdcommon import XKCD1110

xkcd = XKCD1110()
for y in range(xkcd.h):
    for x in range(xkcd.w):
        print "http://imgs.xkcd.com/clickdrag/%s.png" % xkcd.tile_name(x, y)

And we can download them in a pic/ directory with the following download-pic.sh shell script:

#!/bin/sh

mkdir -p pic
for url in `./img-url-list.py`; do
    wget -c "$url" -P pic
    sleep .5
done

To simplify the reconstruction of the map, we might want to use a padding tile, let's generate one in a special color (to better mark the difference with the other tiles):

convert -size 2048x2048 canvas:khaki pic/ref.png

Make the small tiles

The map is really large and we want to construct a preview, so we need a reduce-images.sh script to reduce each downloaded images in pic/:

#!/bin/sh

[ $# -ne 1 ] && printf "usage: $0 size\n" && exit 0

mkdir -p small
cd pic
for f in *.png; do
    convert $f -resize ${1}x${1} ../small/$f
done

This will fill the small/ directory with the reduced images (2048x2048 to 64x64 if the run script parameter is 64).

Complete map image output

Now for our first output, we can construct a full picture of the reduced images using again ImageMagick. Here is a output-image.py python script to generate the appropriate convert command line to stack all the images into one:

#!/usr/bin/env python

import os.path
from xkcdcommon import XKCD1110

xkcd = XKCD1110()

cmd = 'convert'
for y in range(xkcd.h):
    cmd += ' ('
    for x in range(xkcd.w):
        tile = 'small/' + xkcd.tile_name(x, y) + '.png'
        if not os.path.isfile(tile):
            tile = 'small/ref.png'
        cmd += ' %s' % tile
    cmd += ' +append )'
cmd += ' -append xkcd.png'
print cmd

Pretty easy to use:

$(./output-image.py)

And here is the xkcd.png output:

xkcd #1110 full map

Complete map in HTML

Now it would be interesting to be able to zoom on a given part, so here is a output-html.py script to get a HTML output with clickable images:

#!/usr/bin/env python

import sys, os.path
from xkcdcommon import XKCD1110

xkcd = XKCD1110()

if len(sys.argv) != 2:
    print 'usage: %s size' % sys.argv[0]
    sys.exit(0)

size = int(sys.argv[1])

print '''<!doctype html><html><head>
<title>XKCD #1110 map</title>
<style>a{padding:0;margin:0;position:absolute;}</style>
</head><body>'''
for y in range(xkcd.h):
    for x in range(xkcd.w):
        tile = xkcd.tile_name(x, y) + '.png'
        if not os.path.isfile('pic/' + tile):
            tile = 'ref.png'
        print '<a href="pic/%s" style="left:%dpx; top:%dpx;"><img src="small/%s" alt="" /></a>' % (tile, x*size, y*size, tile)
print '</body></html>'

That script needs the sizes of the small pictures. Assuming we resized the images with ./resize-images.sh 64, the HTML page can be generated with:

./output-html.py 64 > xkcd1110.html

The output of the page is viewable here: interactive xkcd #1110 full map.

That's all folks.

index | article raw