Svgworld
Svgworld is a little project to plot a map of the world onto the surface of a globe, rendering the results using Scalable Vector Graphics (SVG). Plotting an entire world map onto a 2D surface tends to introduce some awkward distortion, so a 3D plot has some advantages. Of course, only part of the world can be properly viewed in the 3D projection, depending on the orientation chosen.
Running svgworld.py with no arguments writes a map centered on latitude=0, longitude=0 to stdout, which can be viewed in SVG capable browsers (FF, Chrome, Opera, Safari)
python svgworld.py > world.svg
Open world.svg (World map centered on lat=0,lon=0)
svgworld.py can also be configured to center on a particular country, and write output to a file, for example, to write the SVG map to output file centered on Cameroon:
python svgworld.py -c Cameroon > cameroon.svg
Open cameroon.svg (World map centered on Cameroon)
One of the useful things about SVG is that you can use the browser's zoom facility to zoom into small details on the map. This isn't possible with plain images.
Python Code
Projecting the countries of the world onto a 3D globe is accomplished in this project using the following python scripts:
- pysvg.py A simple library for constructing svg documents
- orthographic.py Routines for performing orthographic projection
- globe.py Creates a globe using SVG, allows regions to be added
- svgworld.py Plots country data onto a globe to obtain a 3D world map
pysvg
A library of simple routines for constructing a scalable vector graphics (SVG) document. SVG is the vector graphics equivalent of HTML. pysvg allows text, circle and polygon objects to an svgdoc document.
pysvg.pyimport sys
# base class for SVG objects, holding style information and handing rendering
class svgstyled(object):
id = 0
def __init__(self,tag):
self.tag = tag
self.style = {}
self.attrs = {}
self.content = ''
self.id = "sv"+str(svgstyled.id)
svgstyled.id += 1
# add a style
def addStyle(self,name,value):
self.style[name] = value
return self
# add an SVG attribute
def addAttr(self,name,value):
self.attrs[name] = value
return self
# set the XML content of the element
def setContent(self,content):
self.content = content
return self
# render all attributes to a string
def getAttrs(self):
s = ''
for name in self.attrs:
s += ' %s="%s"'%(name,str(self.attrs[name]))
s += self.getStyleAttr()
return s
# render the style attribute
def getStyleAttr(self):
keys = self.style.keys()
s = ''
if len(keys):
s = ' style="'
for k in keys:
s += k + ":" + str(self.style[k])+";"
s += '" '
return s
# render the object as an SVG string
def svg(self):
s = '<' + self.tag
s += self.getAttrs()
if self.content != '':
s += '>'
s += self.content
s += '</' + self.tag + '>\n'
else:
s += '/>\n'
return s
# get a unique identifier string for this object
def getId(self):
return self.id
# represent a section of text as an SVG object
class text(svgstyled):
def __init__(self,x,y,txt,hide=False,id=None):
svgstyled.__init__(self,"text")
self.addAttr("x",x).addAttr("y",y).setContent(txt)
if id:
self.addAttr("id",id)
if hide:
self.addStyle("display","none")
# represent a circle as an SVG object
class circle(svgstyled):
def __init__(self,x,y,r,col):
svgstyled.__init__(self,'circle')
self.addAttr("cx",x).addAttr("cy",y).addAttr("r",r).addAttr("fill",col)
# represent a polygon as an SVG object
class polygon(svgstyled):
def __init__(self,points,fill,stroke,stroke_width,tooltip_id=None):
svgstyled.__init__(self,"path")
s = 'M'
sep = ''
for (x,y) in points:
s += sep
sep = ' '
s += str(x)+","+str(y)
s += 'Z'
self.addAttr("d",s).addAttr("id",self.id).addAttr("fill","rgb(%d,%d,%d)"%fill)
self.addAttr("stroke-width",stroke_width).addAttr("stroke","rgb(%d,%d,%d)"%stroke)
if tooltip_id:
self.addAttr("class",tooltip_id)
self.addAttr("onmouseover","showToolTip(evt,'%s',true)"%tooltip_id)
self.addAttr("onmouseout","showToolTip(evt,'%s',false)"%tooltip_id)
class svgdoc(object):
# construct a document with a given width and height, and javascript to embed
def __init__(self,width,height,jscode):
self.objects = []
self.width = width
self.height = height
self.jscode = jscode
# add an object to the document (obj inherits from svgstyled)
def add(self,obj):
self.objects.append(obj)
# render the document as SVG and return as string
def render(self):
# add the XML header
txt = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="%(width)d" height="%(height)d" version="1.1">"""%{ 'height':self.height, 'width':self.width }
# add the javascript
if self.jscode != '':
txt += '<script type="text/ecmascript">\n<![CDATA[\n' + self.jscode + '\n// ]]>\n</script>'
# add the objects
for o in self.objects:
txt += o.svg()
txt += '</svg>'
return txt
orthographic
The class is initialized with the longitude and latitude specifying the center of the view, and the radius (in pixels). The plot method converts a list of (latitude,longitude) pairs into a list of (x,y) points to be plotted onto the surface of a globe. If a region is completely invisble in the chosen orientation, no value is returned. If a region is partly visible, any invisible points are moved to the horizon.
The maths behind orthographic projection is covered in this page on the Wolfram Mathworld website.
orthographic.pyimport math
import sys
class globeplotter(object):
def __init__(self,latitude0,longitude0,r):
self.latitude0 = latitude0
self.elevation0 = self.to_elevation(latitude0)
self.longitude0 = longitude0
self.azimuth0 = self.to_azimuth(longitude0)
self.r = r
def to_elevation(self,latitude):
return ((latitude + 90.0) / 180.0) * math.pi - math.pi/2
def to_azimuth(self,longitude):
return ((longitude + 180.0) / 360.0) * math.pi*2 - math.pi
def plot(self,region):
points = []
ignore = True
for (latitude,longitude) in region:
elevation = self.to_elevation(latitude)
azimuth = self.to_azimuth(longitude)
# work out if the point is visible
cosc = math.sin(elevation)*math.sin(self.elevation0)+math.cos(self.elevation0)*math.cos(elevation)*math.cos(azimuth-self.azimuth0)
if cosc >= 0.0:
# this point is visible, so do not ignore this region
ignore = False
# orthographic projection
xo = self.r*math.cos(elevation)*math.sin(azimuth-self.azimuth0)
yo = -self.r*(math.cos(self.elevation0)*math.sin(elevation)-math.sin(self.elevation0)*math.cos(elevation)*math.cos(azimuth-self.azimuth0))
x = self.r + xo
y = self.r + yo
if cosc < 0:
# this point is on the far side of the globe. Truncate it to lie on the rim.
theta = math.atan2(yo,xo)
x1 = self.r + self.r * math.cos(theta)
y1 = self.r + self.r * math.sin(theta)
points.append((x1,y1))
else:
points.append((x,y))
if ignore:
return None
return points