source: subversion/applications/rendering/party/video.py @ 5030

Last change on this file since 5030 was 5030, checked in by ojw, 13 years ago

Make executable with shebang

File size: 11.6 KB
Line 
1#!/usr/bin/env python
2# GPX Party renderer
3# Takes a directory structure full of GPX file tracklogs,
4# and renders them all to a series of PNG images that can
5# be encoded into a video
6#
7# Copyright 2007, Oliver White
8# Licensed as GNU GPL version3 or at your option any later version
9import cairo
10import math
11import sys
12import getopt
13import colorsys
14import urllib
15from xml.sax import saxutils
16from xml.dom import minidom
17from UserDict import UserDict
18from time import *
19from xml.sax import make_parser
20import os
21from os.path import join, getsize
22deg2rad = 0.0174532925
23M_PI = 3.1415926535
24
25class Palette:
26  def __init__(self, size):
27    """Create a palette of visually-unique colours.
28    size = how many colours to create"""
29    size = max(size,1.0)
30    self.dh = 1.0/(size + 1.0)
31    self.s = 0.8
32    self.v = 0.9
33    self.reset()
34  def reset(self):
35    self.h = 0.0
36  def get(self):
37    """Get the next available colour"""
38    colour = colorsys.hsv_to_rgb(self.h, self.s, self.v)
39    self.h = self.h + self.dh
40    if(self.h > 1.0):
41      self.h = 0.0
42    return(colour)
43
44class CityPlotter(saxutils.DefaultHandler):
45  def __init__(self,surface,extents):
46    self.extents = extents
47    self.surface = surface
48  def drawCities(self,proj,gazeteer):
49    """Load and plot city locations"""
50    if gazeteer == "none" or gazeteer == "":
51      return
52    self.loadCities(proj, gazeteer)
53    self.renderCities(proj)
54  def loadCities(self,proj,gazeteer):
55    """Load city data from the network (osmxapi), or from a file"""
56    self.attr = {}
57    self.cities = []
58    parser = make_parser()
59    parser.setContentHandler(self)
60    if gazeteer == "osmxapi":
61      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)
62      print "Downloading gazeteer"
63      sock = urllib.urlopen(URL)
64      parser.parse(sock)
65      sock.close
66    else:
67      print "Loading gazeteer"
68      parser.parse(gazeteer)
69    print " - Loaded %d placenames" % len(self.cities)
70  def startElement(self, name, attrs):
71    """Store node positions and tag values"""
72    if(name == 'node'):
73      self.attr['lat'] = float(attrs.get('lat', None))
74      self.attr['lon'] = float(attrs.get('lon', None))
75    if(name == 'tag'):
76      self.attr[attrs.get('k', None)] = attrs.get('v', None)
77  def endElement(self, name):
78    """When each node is completely read, store it and reset the tag list for the next node"""
79    if(name == 'node'):
80      if self.attr.get('place') in ("city","town","village"):
81        self.cities.append(self.attr)
82      self.attr = {}
83  def listCities(self):
84    """Dumps list of city locations to stdout"""
85    for c in self.cities:
86      print "%s: %f,%f %s" % (c.get('name',''), c.get('lat'), c.get('lon'), c.get('place',''))
87  def renderCities(self,proj):
88    """Draws cities onto the map"""
89    for c in self.cities:
90      ctx = cairo.Context(surface)
91      ctx.set_source_rgb(1.0,1.0,1.0)
92      x = proj.xpos(c.get('lon'))
93      y = proj.ypos(c.get('lat'))
94      ctx.move_to(x, y)
95      ctx.show_text(c.get('name',''))
96      ctx.stroke()
97
98class Projection:
99  def __init__(self,N,E,S,W):
100    self.N = N
101    self.E = E
102    self.S = S
103    self.W = W
104    self.dLat = N - S
105    self.dLon = E - W
106    self.ratio = self.dLon / self.dLat
107  def setOutput(self,width,height):
108    self.width = width
109    self.height = height
110  def xpos(self,lon):
111    return(self.width * (lon - self.W) / self.dLon)
112  def ypos(self,lat):
113    return(self.height * (1 - (lat - self.S) / self.dLat))
114  def debug(self):
115    """Display the map extents"""
116    print " - Lat %f to %f, Long %f to %f" % (self.S,self.N,self.W,self.E)
117    print " - Ratio: %f" % self.ratio
118   
119class TracklogInfo(saxutils.DefaultHandler):
120  def __init__(self):
121    self.count = 0
122    self.countPoints = 0
123    self.points = {}
124    self.currentFile = ''
125    self.validTimes = {}
126  def walkDir(self, directory):
127    """Load a directory-structure full of GPX files into memory"""
128    for root, dirs, files in os.walk(directory):
129      for file in files:
130        if(file.endswith(".gpx")):
131          print " * %s" % file
132          self.currentFile = file
133          fullFilename = join(root, file)
134          self.points[self.currentFile] = []
135          self.inTime = 0
136          self.inTrackpoint = 0
137          parser = make_parser()
138          parser.setContentHandler(self)
139          parser.parse(fullFilename)
140          self.count = self.count + 1
141    self.currentFile = ''
142    if(self.countPoints == 0):
143      print "No GPX files found"
144      sys.exit()
145    print "Read %d points in %d files" % (self.countPoints,self.count)
146  def startElement(self, name, attrs):
147    """Handle tracklog points found in the GPX files"""
148    if(name == 'trkpt'):
149      self.pLat = float(attrs.get('lat', None))
150      self.pLon = float(attrs.get('lon', None))
151      self.inTrackpoint = 1
152    if(name == 'time'):
153      if(self.inTrackpoint == 1):
154        self.inTime = 1
155        self.timeText = ''
156  def endElement(self,name):
157    if(name == 'time' and self.inTime):
158      self.inTime = 0
159      # Parses <time>2006-10-28T10:06:03Z</time>
160      time = mktime(strptime(self.timeText, "%Y-%m-%dT%H:%M:%SZ"))
161      self.validTimes[time] = 1;
162      self.points[self.currentFile].append((self.pLat,self.pLon,time));
163      self.countPoints = self.countPoints + 1
164  def characters(self, content):
165    if(self.inTime == 1):
166      self.timeText = self.timeText + content
167   
168  def valid(self):
169    """Test whether the lat/long extents of the map are sane"""
170    if(self.ratio == 0.0):
171      return(0)
172    if(self.dLat <= 0.0 or self.dLon <= 0):
173      return(0)
174    return(1)
175  def calculate(self, radius):
176    """Calculate (somehow*) the extents of the map"""
177    self.calculateCentre()
178    self.calculateExtents(radius)
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 calculateCentre(self):
187    """Calculate the centre point of the map"""
188    sumLat = 0
189    sumLon = 0
190    for x in self.points.values():
191      for y in x:
192        sumLat = sumLat + y[0]
193        sumLon = sumLon + y[1]
194    self.lat = sumLat / self.countPoints
195    self.lon = sumLon / self.countPoints
196  def calculateExtents(self,radius):
197    """Calculate the width and height of the map"""
198    c = 40000.0 # circumference of earth, km
199    self.sdLat = (radius / (c / M_PI)) / deg2rad
200    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
201    pass
202  def createImage(self,fullwidth,width1,height,surface,surface2):
203    """Supply a cairo drawing surface for the maps"""
204    self.fullwidth = fullwidth
205    self.width = width1
206    self.height = height
207    self.proj.setOutput(width1,height)
208    self.extents = [0,0,width1,height]
209    self.surface = surface
210    self.surface2 = surface2
211    self.drawBorder()
212    self.keyY = self.height - 20
213  def drawBorder(self):
214    """Draw a border around the 'map' portion of the image"""
215    ctx = cairo.Context(surface)
216    ctx.set_source_rgb(1.0,1.0,1.0)
217    ctx.rectangle(0,0,self.fullwidth,self.height)
218    ctx.fill()
219    border=5
220    ctx = cairo.Context(surface)
221    ctx.set_source_rgb(0,0,0)
222    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
223    ctx.fill()
224    self.extents = [border,border,self.width-border, self.height-border]
225  def drawKey(self, ctx, colour, name):
226    """Add a label showing which colour represents which tracklog"""
227    x = self.width + 10
228    y = self.keyY
229    ctx.arc(x, y, 4, 0, 2*M_PI)
230    ctx.fill()
231    ctx.move_to(x + 10, y+4)
232    ctx.set_source_rgb(0,0,0)
233    ctx.show_text(name)
234    ctx.stroke()
235    self.keyY = self.keyY - 20
236    pass
237  def inImage(self,x,y):
238    """Test whether an x,y coordinate is within the map drawing area"""
239    if(x < self.extents[0] or y < self.extents[1]):
240      return(0)
241    if(x > self.extents[2] or y > self.extents[3]):
242      return(0)
243    return(1)
244  def drawTracklogs(self, pointsize):
245    """Draw all tracklogs from memory onto the map"""
246   
247    # Get a list of timestamps in the GPX
248    timeList = self.validTimes.keys()
249    timeList.sort()
250   
251    # Setup drawing
252    self.palette = Palette(self.count)
253    ctx = cairo.Context(surface)
254    # CAIRO_LINE_CAP_ROUND
255    ctx.set_line_cap(cairo.LINE_CAP_ROUND)
256   
257    # Divide time into frames
258    secondsPerFrame = 50
259    frameTimes = range(int(timeList[0]), int(timeList[-1]), secondsPerFrame)
260    numFrames = len(frameTimes)
261    lastT = 0
262    frame = 1
263    count = 1
264    pointsDrawn = 0
265    currentPositions = {}
266    # For each timeslot (not all of these will become frames in the video)
267    for t in frameTimes:
268      self.palette.reset()
269      pointsDrawnThisTimestep = 0
270      # For each file
271      for name,a in self.points.items():
272        colour = self.palette.get()
273        ctx.set_source_rgb(colour[0],colour[1],colour[2])
274        # For each trackpoint which occurs within this timeslot
275        for b in a:
276          if(b[2] <= t and b[2] > lastT):
277            x = self.proj.xpos(b[1])
278            y = self.proj.ypos(b[0])
279            if(self.inImage(x,y)):
280              if(1):
281                ctx.move_to(x,y)
282                ctx.line_to(x,y)
283                ctx.stroke()
284              else:
285                ctx.arc(x, y, pointsize, 0, 2*M_PI)
286              currentPositions[name] = (x,y)
287              ctx.fill()
288              pointsDrawnThisTimestep = pointsDrawnThisTimestep + 1
289              pointsDrawn = pointsDrawn + 1
290        # On the first frame, draw a key for each track
291        # (this remains on the image when subsequent trackpoints are drawn)
292        if(count == 1):
293          self.drawKey(ctx, colour, name)
294      # If anything changed in this frame, then add it to the video
295      if(pointsDrawnThisTimestep > 0):
296        ctx2 = cairo.Context(surface2)
297        ctx2.set_source_surface(surface, 0, 0);
298        ctx2.set_operator(cairo.OPERATOR_SOURCE);
299        ctx2.paint();
300        ctx2.set_source_rgb(1.0, 1.0, 0.0)
301        for name,pos in currentPositions.items():
302          ctx2.arc(pos[0], pos[1], pointsize*5, 0, 2*M_PI)
303          ctx2.fill()
304        filename = "gfx/img_%05d.png" % frame
305        frame = frame + 1
306        surface2.write_to_png(filename)
307      print "t: %03.1f%%, %d points" % (100.0 * count / numFrames, pointsDrawnThisTimestep)
308      count = count + 1
309      lastT = t
310
311  def drawCities(self, gazeteer):
312    Cities = CityPlotter(self.surface, self.extents)
313    Cities.drawCities(self.proj, gazeteer)
314
315# Handle command-line options
316opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","pointsize=","gazeteer="])
317# Defauts:
318directory = "./"
319size = 600
320radius = 10 # km
321pointsize = 1 # mm
322gazeteer = "osmxapi" # can change to a filename
323# Options:
324for o, a in opts:
325  if o in ("-h", "--help"):
326    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -p [point size] -g [gazeteer file]"
327    sys.exit()
328  if o in ("-d", "--dir"):
329    directory = a
330  if o in ("-s", "--size"):
331    size = int(a)
332  if o in ("-r", "--radius"):
333    radius = float(a)
334  if o in ("-p", "--pointsize"):
335    pointsize = float(a)
336  if o in ("-g", "--gazeteer"):
337    gazeteer = a
338
339TracklogPlotter = TracklogInfo()
340print "Loading data"
341TracklogPlotter.walkDir(directory)
342print "Calculating extents"
343TracklogPlotter.calculate(radius)
344if(not TracklogPlotter.valid):
345  print "Couldn't calculate extents"
346  sys.exit()
347width = size
348height = size
349fullwidth = width + 120
350print "Creating image %d x %d px" % (fullwidth,height)
351
352surface = cairo.ImageSurface(cairo.FORMAT_RGB24, fullwidth, height)
353surface2 = cairo.ImageSurface(cairo.FORMAT_RGB24, fullwidth, height)
354TracklogPlotter.createImage(fullwidth, width, height, surface, surface2)
355
356print "Plotting tracklogs"
357TracklogPlotter.drawCities(gazeteer)
358TracklogPlotter.drawTracklogs(pointsize)
359
Note: See TracBrowser for help on using the repository browser.