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

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

Enable overlays (basically things which move between frames, rather than
persisting like the main image. First use of overlays is just a yellow
dot to mark each person's position

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