Last active
April 26, 2024 13:44
-
-
Save brandon-rhodes/6455705 to your computer and use it in GitHub Desktop.
Script that builds a Google Earth overlay that projects the map of Tolkien’s Middle-earth atop modern Europe at the correct position and scale. Once the .kmz overlay file has been generated, simple open it using the File menu in Google Earth.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Project a map of Middle-earth on modern Europe. | |
Builds an `overlay.kmz` file in the current directory which should be | |
opened with Google Earth. | |
""" | |
import os | |
import urllib2 | |
import zipfile | |
from math import cos, radians | |
from PIL import Image, ImageDraw, ImageFont | |
# An image and where it comes from. | |
filename = 'gmiddleearth2.jpg' | |
url = 'http://www.anarda.net/tolkien/dibujos/mapas/' + filename | |
# Values taken from the map. | |
x_rivendell = 696 # x of Rivendell | |
x_hobbiton = 360 # x of Hobbiton | |
y_hobbiton = 437 # y of Hobbiton | |
x_scale0 = 128 # x of left edge of "Miles" scale | |
x_scale300 = 363 # x of right edge of "Miles" scale | |
# Google search for "oxford england lat lon". | |
lat_hobbiton = 51.7519 # Oxford latitude | |
lon_hobbiton = -1.2578 # Oxford longitude | |
# Since we approximate the Earth as a sphere, instead of a full WGS84 | |
# ellipsoid, this value involves some fudging: it has been adjusted | |
# until it makes the Middle-earth map scale, when project on the Google | |
# Earth globe, exactly 300 miles in length. | |
miles_per_degree = 67.7 # per degree of latitude | |
# Per the hypotheses of Lalaith, the map's scale only applies to the | |
# central meridian that runs vertically through Rivendell, and along | |
# lines of latitude. See: http://lalaith.vpsurf.de/Tolkien/Grid.html | |
miles_per_pixel = 300.0 / (x_scale300 - x_scale0) | |
latitude_per_pixel = miles_per_pixel / miles_per_degree | |
# And, finally, the code. | |
def download_and_open_image(): | |
"""Returns an open Image containing the Middle-earth map.""" | |
if not os.path.exists(filename): | |
data = urllib2.urlopen(url).read() | |
with open(filename, 'wb') as f: | |
f.write(data) | |
return Image.open(filename) | |
def latitude(y): | |
"""Return the latitude, in radians, of a given `y` map image coordinate.""" | |
return lat_hobbiton + (y_hobbiton - y) * latitude_per_pixel | |
def mag(y): | |
"""How much do we have to expand each row of the image horizontally?""" | |
return 1. / cos(radians(latitude(y))) | |
def scale_map(): | |
"""Return the Middle-earth map, scaled to project on a Google Earth.""" | |
im = download_and_open_image() | |
xsize, ysize = im.size | |
# Find the location of the FreeSans font by asking the "locate" command, | |
# rather than hard-coding its location in a string constant. | |
font_path = os.popen('locate -n1 /FreeSans.ttf').read().strip() | |
# Remove the right margin to place Rivendell exactly in the center, | |
# since Lalaith believes that to be the map's "central meridian". | |
xsize = 2 * x_rivendell | |
im = im.crop((0, 0, xsize, ysize)) | |
# Draw red lines of latitude at five-degree increments, labeled in | |
# blue in a simple sans-serif font, to make it easy to verify that | |
# the image is placed correctly on the Google Earth globe. | |
font = ImageFont.truetype(font_path, 24) | |
draw = ImageDraw.Draw(im) | |
for lat in range(30, 61, 5): | |
y = y_hobbiton + (lat_hobbiton - lat) / latitude_per_pixel | |
draw.line((0, y, xsize, y), fill=(255,0,0)) | |
draw.text((xsize - 50, y), u'%sN' % lat, font=font, fill=(0,0,255)) | |
del draw | |
# How big will the whole image be? The top row, at y=0, will be widest. | |
xfinal = int(xsize * mag(0)) | |
# Build the mesh that stretches each line of pixels in the original | |
# image into a wider line of pixels in the resulting image. | |
middle = x_rivendell * mag(0) # x-coordinate on which to center | |
data = [] | |
for y in range(0, ysize): | |
m = mag(y) | |
source_quad = (0, y-0.5, 0, y+0.5, | |
xsize, y+0.5, xsize, y-0.5) # the whole row of pixels | |
dest_bbox = (int(middle - m * x_rivendell), y, | |
int(middle + m * x_rivendell), y + 1) # a wider row | |
data.append((dest_bbox, source_quad)) | |
# Do the transform and save the result. | |
im = im.transform((xfinal, ysize), Image.MESH, data, Image.BICUBIC) | |
return im, xfinal, xsize, ysize | |
def build_overlay(): | |
"""Build the whole overlay, with the KML and image inside of it.""" | |
im, xfinal, xsize, ysize = scale_map() | |
im.save('middle-earth-scaled.jpg') | |
rivendell_hobbiton_miles = (x_rivendell - x_hobbiton) * miles_per_pixel | |
lon_rivendell = lon_hobbiton + ( | |
rivendell_hobbiton_miles / miles_per_degree * mag(y_hobbiton)) | |
with zipfile.ZipFile('middle-earth-overlay.kmz', 'w') as z: | |
z.writestr('doc.kml', kml.format( | |
path='files/middle-earth-scaled.jpg', | |
lat_north=latitude(0), | |
lat_south=latitude(ysize), | |
lon_east=lon_rivendell + xsize / 2.0 * latitude_per_pixel * mag(0), | |
lon_west=lon_rivendell - xsize / 2.0 * latitude_per_pixel * mag(0), | |
)) | |
z.write('middle-earth-scaled.jpg', 'files/middle-earth-scaled.jpg') | |
# KML Mad Libs. | |
kml = """\ | |
<?xml version="1.0" encoding="UTF-8"?> | |
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"> | |
<GroundOverlay> | |
<name>Map of Middle-earth</name> | |
<color>aaffffff</color> | |
<Icon> | |
<href>{path}</href> | |
<viewBoundScale>0.75</viewBoundScale> | |
</Icon> | |
<LatLonBox> | |
<north>{lat_north}</north> | |
<south>{lat_south}</south> | |
<east>{lon_east}</east> | |
<west>{lon_west}</west> | |
</LatLonBox> | |
</GroundOverlay> | |
</kml> | |
""" | |
if __name__ == '__main__': | |
build_overlay() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment