source: subversion/applications/rendering/party/render.py @ 14210

Last change on this file since 14210 was 12246, checked in by sward, 12 years ago

Remove extension from track name in the key.

File size: 9.3 KB
Line 
1# GPX Party renderer
2# Takes a directory structure full of GPX file tracklogs,
3# and renders them all to a single image
4#
5# Copyright 2007, Oliver White
6# Licensed as GNU GPL version3 or at your option any later version
7#
8# Usage: python render.py
9import cairo
10import math
11import sys
12import getopt
13import colorsys
14import urllib
15from xml.sax import handler
16from xml.dom import minidom
17from UserDict import UserDict
18from xml.sax import make_parser
19import os
20from os.path import join, getsize, splitext
21deg2rad = 0.0174532925
22M_PI = 3.1415926535
23
24class Palette:
25  def __init__(self, size):
26    """Create a palette of visually-unique colours.
27    size = how many colours to create"""
28    size = max(size,1.0)
29    self.h = 0.0
30    self.dh = 1.0/(size + 1.0)
31    self.s = 0.8
32    self.v = 0.9
33  def get(self):
34    """Get the next available colour"""
35    colour = colorsys.hsv_to_rgb(self.h, self.s, self.v)
36    self.h = self.h + self.dh
37    if(self.h > 1.0):
38      self.h = 0.0
39    return(colour)
40
41class CityPlotter(handler.ContentHandler):
42  def __init__(self,surface,extents):
43    self.extents = extents
44    self.surface = surface
45  def drawCities(self,proj,gazeteer):
46    """Load and plot city locations"""
47    if gazeteer == "none" or gazeteer == "":
48      return
49    self.loadCities(proj, gazeteer)
50    self.renderCities(proj)
51  def loadCities(self,proj,gazeteer):
52    """Load city data from the network (osmxapi), or from a file"""
53    self.attr = {}
54    self.cities = []
55    parser = make_parser()
56    parser.setContentHandler(self)
57    if gazeteer == "osmxapi":
58      URL =  "http://www.informationfreeway.org/api/0.5/node[%s][bbox=%f,%f,%f,%f]" % ("place=city|town|village", proj.W, proj.S, proj.E, proj.N)
59      print "Downloading gazeteer"
60      sock = urllib.urlopen(URL)
61      parser.parse(sock)
62      sock.close()
63    else:
64      print "Loading gazeteer"
65      parser.parse(gazeteer)
66    print " - Loaded %d placenames" % len(self.cities)
67  def startElement(self, name, attrs):
68    """Store node positions and tag values"""
69    if(name == 'node'):
70      self.attr['lat'] = float(attrs.get('lat', None))
71      self.attr['lon'] = float(attrs.get('lon', None))
72    if(name == 'tag'):
73      self.attr[attrs.get('k', None)] = attrs.get('v', None)
74  def endElement(self, name):
75    """When each node is completely read, store it and reset the tag list for the next node"""
76    if(name == 'node'):
77      if self.attr.get('place') in ("city","town","village"):
78        self.cities.append(self.attr)
79      self.attr = {}
80  def listCities(self):
81    """Dumps list of city locations to stdout"""
82    for c in self.cities:
83      print "%s: %f,%f %s" % (c.get('name',''), c.get('lat'), c.get('lon'), c.get('place',''))
84  def renderCities(self,proj):
85    """Draws cities onto the map"""
86    for c in self.cities:
87      ctx = cairo.Context(surface)
88      ctx.set_source_rgb(1.0,1.0,1.0)
89      x = proj.xpos(c.get('lon'))
90      y = proj.ypos(c.get('lat'))
91      ctx.move_to(x, y)
92      ctx.show_text(c.get('name',''))
93      ctx.stroke()
94
95class Projection:
96  def __init__(self,N,E,S,W):
97    self.N = N
98    self.E = E
99    self.S = S
100    self.W = W
101    self.dLat = N - S
102    self.dLon = E - W
103    self.ratio = self.dLon / self.dLat
104  def setOutput(self,width,height):
105    self.width = width
106    self.height = height
107  def xpos(self,lon):
108    return(self.width * (lon - self.W) / self.dLon)
109  def ypos(self,lat):
110    return(self.height * (1 - (lat - self.S) / self.dLat))
111  def debug(self):
112    """Display the map extents"""
113    print " - Lat %f to %f, Long %f to %f" % (self.S,self.N,self.W,self.E)
114    print " - Ratio: %f" % self.ratio
115  def valid(self):
116    """Test whether the lat/long extents of the map are sane"""
117    if(self.ratio == 0.0):
118      return(0)
119    if(self.dLat <= 0.0 or self.dLon <= 0):
120      return(0)
121    return(1)
122   
123class TracklogInfo(handler.ContentHandler):
124  def __init__(self):
125    self.count = 0
126    self.countPoints = 0
127    self.points = {}
128    self.currentFile = ''
129  def walkDir(self, directory):
130    """Load a directory-structure full of GPX files into memory"""
131    for root, dirs, files in os.walk(directory):
132      for file in files:
133        if(file.endswith(".gpx")):
134          print " * %s" % file
135          self.currentFile = file
136          fullFilename = join(root, file)
137          self.points[self.currentFile] = []
138          parser = make_parser()
139          parser.setContentHandler(self)
140          parser.parse(fullFilename)
141          self.count = self.count + 1
142    self.currentFile = ''
143    if(self.countPoints == 0):
144      print "No GPX files found"
145      sys.exit()
146    print "Read %d points in %d files" % (self.countPoints,self.count)
147  def startElement(self, name, attrs):
148    """Handle tracklog points found in the GPX files"""
149    if(name == 'trkpt'):
150      lat = float(attrs.get('lat', None))
151      lon = float(attrs.get('lon', None))
152      self.points[self.currentFile].append((lat,lon));
153      self.countPoints = self.countPoints + 1
154  def valid(self):
155    """Test whether the lat/long extents of the map are sane"""
156    return self.proj.valid()
157  def calculate(self, radius):
158    """Automatically calculate (somehow*) the extents of the map"""
159    self.calculateCentre()
160    self.calculateExtents(radius)
161  def setCentre(self,lat,lon):
162    self.lat = lat
163    self.lon = lon
164    print "Centred on %f, %f" % (lat,lon)
165  def calculateCentre(self):
166    """Calculate the centre point of the map"""
167    sumLat = 0
168    sumLon = 0
169    for x in self.points.values():
170      for y in x:
171        sumLat = sumLat + y[0]
172        sumLon = sumLon + y[1]
173    self.setCentre(sumLat / self.countPoints, sumLon / self.countPoints)
174  def calculateExtents(self,radius):
175    """Calculate the width and height of the map"""
176    c = 40000.0 # circumference of earth, km
177    self.sdLat = (radius / (c / M_PI)) / deg2rad
178    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
179    # then use that to calculate extents
180    self.proj = Projection(
181      self.lat + self.sdLat,
182      self.lon + self.sdLon,
183      self.lat - self.sdLat,
184      self.lon - self.sdLon)
185    self.proj.debug()
186  def createImage(self,width1,height,surface):
187    """Supply a cairo drawing surface for the maps"""
188    self.width = width1
189    self.height = height
190    self.proj.setOutput(width1,height)
191    self.extents = [0,0,width1,height]
192    self.surface = surface
193    self.drawBorder()
194    self.keyY = self.height - 20
195  def drawBorder(self):
196    """Draw a border around the 'map' portion of the image"""
197    border=5
198    ctx = cairo.Context(surface)
199    ctx.set_source_rgb(0,0,0)
200    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
201    ctx.fill()
202    self.extents = [border,border,self.width-border, self.height-border]
203  def drawKey(self, ctx, colour, name):
204    """Add a label showing which colour represents which tracklog"""
205    x = self.width + 10
206    y = self.keyY
207    ctx.arc(x, y, 4, 0, 2*M_PI)
208    ctx.fill()
209    ctx.move_to(x + 10, y+4)
210    ctx.set_source_rgb(0,0,0)
211    ctx.show_text(name)
212    ctx.stroke()
213    self.keyY = self.keyY - 20
214    pass
215  def inImage(self,x,y):
216    """Test whether an x,y coordinate is within the map drawing area"""
217    if(x < self.extents[0] or y < self.extents[1]):
218      return(0)
219    if(x > self.extents[2] or y > self.extents[3]):
220      return(0)
221    return(1)
222  def drawTracklogs(self, pointsize):
223    """Draw all tracklogs from memory onto the map"""
224    self.palette = Palette(self.count)
225    ctx = cairo.Context(surface)
226    for name,a in self.points.items():
227      colour = self.palette.get()
228      ctx.set_source_rgb(colour[0],colour[1],colour[2])
229      for b in a:
230        x = self.proj.xpos(b[1])
231        y = self.proj.ypos(b[0])
232        if(self.inImage(x,y)):
233          ctx.arc(x, y, pointsize, 0, 2*M_PI)
234          ctx.fill()
235      niceName = splitext(name)[0]
236      self.drawKey(ctx, colour, niceName)
237  def drawCities(self, gazeteer):
238    Cities = CityPlotter(self.surface, self.extents)
239    Cities.drawCities(self.proj, gazeteer)
240
241# Handle command-line options
242opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","pointsize=","gazeteer=","pos="])
243# Defauts:
244directory = "./"
245size = 600
246radius = 10 # km
247pointsize = 1 # mm
248specifyCentre = 0
249gazeteer = "osmxapi" # can change to a filename
250# Options:
251for o, a in opts:
252  if o in ("-h", "--help"):
253    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -p [point size] -g [gazeteer file]"
254    sys.exit()
255  if o in ("-d", "--dir"):
256    directory = a
257  if o in ("-s", "--size"):
258    size = int(a)
259  if o in ("-r", "--radius"):
260    radius = float(a)
261  if o in ("-p", "--pointsize"):
262    pointsize = float(a)
263  if o in ("-g", "--gazeteer"):
264    gazeteer = a
265  if o in ("--pos"):
266    lat, lon = [float(x) for x in a.split(",")]
267    specifyCentre = 1
268
269TracklogPlotter = TracklogInfo()
270print "Loading data"
271TracklogPlotter.walkDir(directory)
272print "Calculating extents"
273if specifyCentre:
274  TracklogPlotter.setCentre(lat, lon)
275  TracklogPlotter.calculateExtents(radius)
276else:
277  TracklogPlotter.calculate(radius)
278if(not TracklogPlotter.valid()):
279  print "Couldn't calculate extents"
280  sys.exit(1)
281width = size
282height = size
283fullwidth = width + 120
284print "Creating image %d x %d px" % (fullwidth,height)
285
286surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
287TracklogPlotter.createImage(width, height, surface)
288
289print "Plotting tracklogs"
290TracklogPlotter.drawTracklogs(pointsize)
291TracklogPlotter.drawCities(gazeteer)
292
293surface.write_to_png("output.png")
294
Note: See TracBrowser for help on using the repository browser.