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

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.py
import 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.py
import 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
     

globe

Provides routines for plotting a globe, adding a blue background (the sea), some javascript for displaying tooltips, and a method to add regions described by a country name and a set of regions which belong to the country, each region consisting of a list of longitude-latitude pairs describing the borders.

globe.py
import math
import sys

from pysvg import text,circle,polygon,svgdoc
from orthographic import globeplotter

jscode = """function showToolTip(evt,tid,show) {
    var elem = document.getElementById(tid);
    elem.style.display = show ? 'block' : 'none';
   
    var x = evt.pageX;
    var y = evt.pageY;
    
    elem.setAttribute("x",String(x));
    elem.setAttribute("y",String(y));
    
    var elems = document.getElementsByClassName(tid);
    for(i=0;i<elems.length;i++) {
        elems[i].setAttribute("stroke-width",show ? "1" : "0"); 
    }   
}
"""

class pyglobe(object):
    
    def __init__(self,lat,lon,r,title):
        self.regions = []
        self.r = r
        self.plot = globeplotter(lat,lon,r)
        self.tooltips = []
        self.title = title
        self.starty = 75

    def addRegion(self,region,fill,classname):
        points = self.plot.plot(region)
        
        if points and len(points) > 1:
            adjpoints = []
            for (x,y) in points:
                adjpoints.append((x,y+self.starty))        
            self.regions.append(polygon(adjpoints,fill,(255,0,0),0,classname))

    def addTooltip(self,label,classname):
        self.tooltips.append(text(0,0,label,True,classname))


    def render(self,outpath):

        svg = svgdoc(200+int(self.r*2),self.starty+int(self.r*2),jscode)

        # add the title
        title = text(40,40,self.title)
        title.addStyle("font-size","24px")
        svg.add(title)

        # add a blue circle representing the worlds oceans, as a background
        svg.add(circle(self.r,self.r+self.starty,self.r,"rgb(128,128,255)"))

        # add country data (polygons for visible regions)
        for region in self.regions:
            svg.add(region)
        for tooltip in self.tooltips:
            svg.add(tooltip)

        # write the svg contents to file
        if outpath:
            f = open(outpath,"w+")
            f.write(svg.render())
            f.close()
        else:
            print svg.render()

svgworld

Reads country data from a json file (countries.json) derived from the TM_WORLD_BORDERS-0.1.ZIP dataset (http://geocommons.com/overlays/18458) and constructs a globe object. For each country, adds the regions that make up that country to the globe.

svgworld.py
import json
import sys
from globe import pyglobe
           
from random import randint
           
if __name__ == '__main__':

    from optparse import OptionParser
    parser = OptionParser()

    parser.add_option("-c","--country",dest="country",type="string",help="center on the specified country")
    parser.add_option("-o","--outputpath",dest="outpath",help="specify output file path")

    (options,args) = parser.parse_args()

    latlong = (0,0)
   
    data = open("countries.json","r+")
    
    countries = []
    for line in data:
        countries.append(json.loads(line.strip()))
       
 
    for country in countries: 
        lat = float(country["info"]["LAT"])
        lon = float(country["info"]["LON"])
        name = country["info"]["NAME"]
        if options.country and options.country == name:
            latlong = (lat,lon)
       

    g = pyglobe(latlong[0],latlong[1],400,"World Map (centered on %.0f lat, %.0f lon)"%latlong)
            
    cid = 1
    for country in countries:
        classname = "c"+str(cid)
        parts = country["parts"]
        name = country["info"]["NAME"]
        fill=(128+randint(0,127),128+randint(0,127),128+randint(0,127))   
        
        for part in parts:
            l = []
            points = parts[part]
            for point in points:
                l.append((point[1],point[0]))               
            g.addRegion(l,fill,classname)

        g.addTooltip(name,classname)
        cid += 1
    
    g.render(options.outpath)
 
Anti-spam check
Leave a comment