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

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

Brackets not required in a class definition if it doesn't inherit

File size: 15.9 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
22import pymedia.video.vcodec as vcodec
23import pygame
24import array
25
26deg2rad = 0.0174532925
27M_PI = 3.1415926535
28
29class videoThingy:
30  def __init__(self, width, height):
31    params= { \
32      'type': 0,
33      'gop_size': 12,
34      'frame_rate_base': 125,
35      'max_b_frames': 0,
36      'height': height,
37      'width': width,
38      'frame_rate': 2997,
39      'deinterlace': 0,
40      'bitrate': 2700000,
41      'id': vcodec.getCodecID( "mpeg1video")
42    }
43    self.width = width
44    self.height = height
45    filename = "out_m.mpg"
46    print "Outputting to %s at %d x %d" %( filename,width,height)
47    self.fw= open(filename, 'wb' )
48    self.e= vcodec.Encoder( params )
49 
50  def addFrame(self, surface, repeat = 1):
51    # surface = cairo.ImageSurface.create_from_png("map.png")
52    buf = surface.get_data_as_rgba()
53    w = surface.get_width()
54    h = surface.get_height()
55    #s= pygame.image.load("map.png")
56   
57    s= pygame.image.frombuffer(buf, (self.width,self.height), "RGBA")
58    ss= pygame.image.tostring(s, "RGB")
59
60    bmpFrame= vcodec.VFrame( vcodec.formats.PIX_FMT_RGB24, s.get_size(), (ss,None,None))
61    yuvFrame= bmpFrame.convert( vcodec.formats.PIX_FMT_YUV420P )
62   
63    for i in range(repeat):
64      d= self.e.encode( yuvFrame )
65      self.fw.write( d.data )
66     
67  def finish(self):
68    self.fw.close()
69
70class Palette:
71  def __init__(self, size):
72    """Create a palette of visually-unique colours.
73    size = how many colours to create"""
74    size = max(size,1.0)
75    self.dh = 1.0/(size + 1.0)
76    self.s = 0.8
77    self.v = 0.9
78    self.reset()
79  def reset(self):
80    self.h = 0.0
81  def get(self):
82    """Get the next available colour"""
83    colour = colorsys.hsv_to_rgb(self.h, self.s, self.v)
84    self.h = self.h + self.dh
85    if(self.h > 1.0):
86      self.h = 0.0
87    return(colour)
88
89class CityPlotter(saxutils.DefaultHandler):
90  def __init__(self,surface,extents):
91    self.extents = extents
92    self.surface = surface
93  def drawCities(self,proj,gazeteer):
94    """Load and plot city locations"""
95    if gazeteer == "none" or gazeteer == "":
96      return
97    self.loadCities(proj, gazeteer)
98    self.renderCities(proj)
99  def loadCities(self,proj,gazeteer):
100    """Load city data from the network (osmxapi), or from a file"""
101    self.attr = {}
102    self.cities = []
103    parser = make_parser()
104    parser.setContentHandler(self)
105    if gazeteer == "osmxapi":
106      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)
107      print "Downloading gazeteer"
108      sock = urllib.urlopen(URL)
109      parser.parse(sock)
110      sock.close
111    else:
112      print "Loading gazeteer"
113      parser.parse(gazeteer)
114    print " - Loaded %d placenames" % len(self.cities)
115  def startElement(self, name, attrs):
116    """Store node positions and tag values"""
117    if(name == 'node'):
118      self.attr['lat'] = float(attrs.get('lat', None))
119      self.attr['lon'] = float(attrs.get('lon', None))
120    if(name == 'tag'):
121      self.attr[attrs.get('k', None)] = attrs.get('v', None)
122  def endElement(self, name):
123    """When each node is completely read, store it and reset the tag list for the next node"""
124    if(name == 'node'):
125      if self.attr.get('place') in ("city","town","village"):
126        self.cities.append(self.attr)
127      self.attr = {}
128  def listCities(self):
129    """Dumps list of city locations to stdout"""
130    for c in self.cities:
131      print "%s: %f,%f %s" % (c.get('name',''), c.get('lat'), c.get('lon'), c.get('place',''))
132  def renderCities(self,proj):
133    """Draws cities onto the map"""
134    for c in self.cities:
135      ctx = cairo.Context(surface)
136      ctx.set_source_rgb(1.0,1.0,1.0)
137      x = proj.xpos(c.get('lon'))
138      y = proj.ypos(c.get('lat'))
139      ctx.move_to(x, y)
140      ctx.show_text(c.get('name',''))
141      ctx.stroke()
142
143class Projection:
144  def __init__(self,N,E,S,W):
145    self.N = N
146    self.E = E
147    self.S = S
148    self.W = W
149    self.dLat = N - S
150    self.dLon = E - W
151    self.ratio = self.dLon / self.dLat
152  def setOutput(self,width,height):
153    self.width = width
154    self.height = height
155  def xpos(self,lon):
156    return(self.width * (lon - self.W) / self.dLon)
157  def ypos(self,lat):
158    return(self.height * (1 - (lat - self.S) / self.dLat))
159  def debug(self):
160    """Display the map extents"""
161    print " - Lat %f to %f, Long %f to %f" % (self.S,self.N,self.W,self.E)
162    print " - Ratio: %f" % self.ratio
163   
164class TracklogInfo(saxutils.DefaultHandler):
165  def __init__(self):
166    self.count = 0
167    self.countPoints = 0
168    self.points = {}
169    self.currentFile = ''
170    self.validTimes = {}
171    self.frame = 1
172  def finish(self):
173    self.video.finish()
174  def walkDir(self, directory):
175    """Load a directory-structure full of GPX files into memory"""
176    for root, dirs, files in os.walk(directory):
177      for file in files:
178        if(file.endswith(".gpx")):
179          print " * %s" % file
180          self.currentFile = file
181          fullFilename = join(root, file)
182          self.points[self.currentFile] = []
183          self.inTime = 0
184          self.inTrackpoint = 0
185          parser = make_parser()
186          parser.setContentHandler(self)
187          parser.parse(fullFilename)
188          self.count = self.count + 1
189    self.currentFile = ''
190    if(self.countPoints == 0):
191      print "No GPX files found"
192      sys.exit()
193    print "Read %d points in %d files" % (self.countPoints,self.count)
194  def startElement(self, name, attrs):
195    """Handle tracklog points found in the GPX files"""
196    if(name == 'trkpt'):
197      self.pLat = float(attrs.get('lat', None))
198      self.pLon = float(attrs.get('lon', None))
199      self.inTrackpoint = 1
200    if(name == 'time'):
201      if(self.inTrackpoint == 1):
202        self.inTime = 1
203        self.timeText = ''
204  def endElement(self,name):
205    if(name == 'time' and self.inTime):
206      self.inTime = 0
207      # Parses <time>2006-10-28T10:06:03Z</time>
208      time = mktime(strptime(self.timeText, "%Y-%m-%dT%H:%M:%SZ"))
209      self.validTimes[time] = 1;
210      self.points[self.currentFile].append((self.pLat,self.pLon,time));
211      self.countPoints = self.countPoints + 1
212  def characters(self, content):
213    if(self.inTime == 1):
214      self.timeText = self.timeText + content
215   
216  def valid(self):
217    """Test whether the lat/long extents of the map are sane"""
218    if(self.ratio == 0.0):
219      return(0)
220    if(self.dLat <= 0.0 or self.dLon <= 0):
221      return(0)
222    return(1)
223  def setCentre(self,lat,lon):
224    self.lat = lat
225    self.lon = lon
226    print "Centred on %f, %f" % (lat,lon)
227  def calculateCentre(self):
228    """Calculate the centre point of the map"""
229    sumLat = 0
230    sumLon = 0
231    for x in self.points.values():
232      for y in x:
233        sumLat = sumLat + y[0]
234        sumLon = sumLon + y[1]
235    self.setCentre(sumLat / self.countPoints, sumLon / self.countPoints)
236  def calculateExtents(self,radius):
237    """Calculate the width and height of the map"""
238    c = 40000.0 # circumference of earth, km
239    self.sdLat = (radius / (c / M_PI)) / deg2rad
240    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
241    self.proj = Projection(
242      self.lat + self.sdLat,
243      self.lon + self.sdLon,
244      self.lat - self.sdLat,
245      self.lon - self.sdLon)
246    self.proj.debug()
247  def createImage(self,fullwidth,width1,height,surface,surface2):
248    """Supply a cairo drawing surface for the maps"""
249    self.fullwidth = fullwidth
250    self.width = width1
251    self.height = height
252    self.proj.setOutput(width1,height)
253    self.extents = [0,0,width1,height]
254    self.surface = surface
255    self.surface2 = surface2
256    self.drawBorder()
257    self.keyY = self.height - 20
258    #self.filenameFormat = "gfx/img_%05d.png"
259    self.video = videoThingy(fullwidth, height)
260  def drawBorder(self):
261    """Draw a border around the 'map' portion of the image"""
262    ctx = cairo.Context(surface)
263    ctx.set_source_rgb(1.0,1.0,1.0)
264    ctx.rectangle(0,0,self.fullwidth,self.height)
265    ctx.fill()
266    border=5
267    ctx = cairo.Context(surface)
268    ctx.set_source_rgb(0,0,0)
269    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
270    ctx.fill()
271    self.extents = [border,border,self.width-border, self.height-border]
272  def drawKey(self, ctx, colour, name):
273    """Add a label showing which colour represents which tracklog"""
274    x = self.width + 10
275    y = self.keyY
276    ctx.arc(x, y, 4, 0, 2*M_PI)
277    ctx.fill()
278    ctx.move_to(x + 10, y+4)
279    ctx.set_source_rgb(0,0,0)
280    ctx.show_text(name)
281    ctx.stroke()
282    self.keyY = self.keyY - 20
283    pass
284  def inImage(self,x,y):
285    """Test whether an x,y coordinate is within the map drawing area"""
286    if(x < self.extents[0] or y < self.extents[1]):
287      return(0)
288    if(x > self.extents[2] or y > self.extents[3]):
289      return(0)
290    return(1)
291  def drawTracklogs(self):
292    """Draw all tracklogs from memory onto the map"""
293   
294    # Get a list of timestamps in the GPX
295    timeList = self.validTimes.keys()
296    timeList.sort()
297   
298    # Setup drawing
299    self.palette = Palette(self.count)
300    ctx = cairo.Context(surface)
301    ctx.set_line_cap(cairo.LINE_CAP_ROUND)
302   
303    # Divide time into frames
304    secondsPerFrame = 60
305    frameTimes = range(int(timeList[0]), int(timeList[-1]), secondsPerFrame)
306    numFrames = len(frameTimes)
307    lastT = 0
308    count = 1
309    pointsDrawn = 0
310    currentPositions = {}
311    # For each timeslot (not all of these will become frames in the video)
312    for t in frameTimes:
313      somethingToDraw = 0
314      if lastT == 0:
315        somethingToDraw = 1
316      else:
317        for ti in range(lastT+1, t+1):
318          if self.validTimes.has_key(ti):
319            somethingToDraw = 1
320
321      if somethingToDraw:
322        self.palette.reset()
323        pointsDrawnThisTimestep = 0
324        # For each file
325        for name,a in self.points.items():
326          colour = self.palette.get()
327          ctx.set_source_rgb(colour[0],colour[1],colour[2])
328          # For each trackpoint which occurs within this timeslot
329          for b in a:
330            if(b[2] <= t and b[2] > lastT):
331              x = self.proj.xpos(b[1])
332              y = self.proj.ypos(b[0])
333              if(self.inImage(x,y)):
334                if(1):
335                  ctx.move_to(x,y)
336                  ctx.line_to(x,y)
337                  ctx.stroke()
338                else:
339                  ctx.arc(x, y, pointsize, 0, 2*M_PI)
340                currentPositions[name] = (x,y)
341                ctx.fill()
342                pointsDrawnThisTimestep = pointsDrawnThisTimestep + 1
343                pointsDrawn = pointsDrawn + 1
344          # On the first frame, draw a key for each track
345          # (this remains on the image when subsequent trackpoints are drawn)
346          if(count == 1):
347            self.drawKey(ctx, colour, name)
348        # If anything changed in this frame, then add it to the video
349        if(pointsDrawnThisTimestep > 0):
350          ctx2 = cairo.Context(surface2)
351          ctx2.set_source_surface(surface, 0, 0);
352          ctx2.set_operator(cairo.OPERATOR_SOURCE);
353          ctx2.paint();
354          ctx2.set_source_rgb(1.0, 1.0, 0.0)
355          for name,pos in currentPositions.items():
356            ctx2.arc(pos[0], pos[1], pointsize*5, 0, 2*M_PI)
357            ctx2.fill()
358          self.video.addFrame(surface2)
359          print "t: %03.1f%%, %d points" % (100.0 * count / numFrames, pointsDrawnThisTimestep)
360      lastT = t
361      count = count + 1
362    self.pause(self.surface2, 50)
363    self.fadeToMapImage()
364
365  def drawCities(self, gazeteer):
366    Cities = CityPlotter(self.surface, self.extents)
367    Cities.drawCities(self.proj, gazeteer)
368 
369  def pause(self, surface, frames):
370    print "Pausing..."
371    self.video.addFrame(surface, frames)
372   
373  def fadeToMapImage(self):
374    URL =  "http://dev.openstreetmap.org/~ojw/bbox/?W=%f&S=%f&E=%f&N=%f&width=%d&height=%d" % (self.proj.W, self.proj.S, self.proj.E, self.proj.N, self.width, self.height)
375    print "Downloading map: "
376    print URL
377    sock = urllib.urlopen(URL)
378    out = open("map.png","w")
379    out.write(sock.read())
380    out.close()
381    sock.close
382   
383    mapSurface = cairo.ImageSurface.create_from_png("map.png")
384   
385    w = mapSurface.get_width()
386    h = mapSurface.get_height()
387    print "Size %d x %d" % (w,h)
388 
389    for alphaPercent in range(0, 101, 1):
390      alpha = float(alphaPercent) / 100.0
391      print "Fading map: %1.0f%%" % alphaPercent
392     
393      # Copy
394      ctx2 = cairo.Context(surface2)
395      ctx2.set_source_surface(surface, 0, 0);
396      ctx2.set_operator(cairo.OPERATOR_SOURCE);
397      ctx2.paint();
398   
399      # Overlay
400      ctx2.set_source_surface(mapSurface, 0, 0);
401      ctx2.paint_with_alpha(alpha)
402
403      self.video.addFrame(surface2, 3)
404     
405    self.pause(self.surface2, 200)
406
407  def drawTitle(self):
408    self.drawBorder()
409    ctx = cairo.Context(self.surface)
410    page = TitlePage(0.5 * self.width, self.height, ctx)
411    page.text("OpenStreetMap", 55, 0.28)
412    page.text("Surrey Hills Mapping Party", 30, 0.46)
413    page.text("October 2006", 30, 0.52)
414    page.text("Creative Commons CC-BY-SA 2.0", 25, 0.85)
415   
416    print "Title..."
417    self.video.addFrame(self.surface, 70)
418
419  def drawCredits(self):
420    self.drawBorder()
421    ctx = cairo.Context(self.surface)
422    page = TitlePage(0.5 * self.width, self.height, ctx)
423    page.text("www.OpenStreetMap.org", 30, 0.35)
424    page.text("Creative Commons CC-BY-SA 2.0", 25, 0.85)
425    print "Credits..."
426    self.video.addFrame(self.surface, 70)
427
428class TitlePage:
429  def __init__(self,xc,height,context):
430    self.context = context
431    self.height = height
432    self.xc = xc
433    self.context.set_source_rgb(1.0, 1.0, 1.0)
434    self.context.select_font_face( \
435      "FreeSerif", 
436      cairo.FONT_SLANT_NORMAL, 
437      cairo.FONT_WEIGHT_BOLD)
438
439  def text(self,text,size, yp):
440    self.context.set_font_size(size)
441    x_bearing, y_bearing, width, height = \
442      self.context.text_extents(text)[:4]
443    self.context.move_to( \
444      self.xc - width / 2 - x_bearing, 
445      yp * self.height - height / 2 - y_bearing)
446    self.context.show_text(text)   
447
448# Handle command-line options
449opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","gazeteer=","pos="])
450# Defauts:
451directory = "./"
452size = 600
453radius = 10 # km
454pointsize = 1 # mm
455specifyCentre = 0
456gazeteer = "osmxapi" # can change to a filename
457# Options:
458for o, a in opts:
459  if o in ("-h", "--help"):
460    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -g [gazeteer file] --pos=[lat],[lon]"
461    sys.exit()
462  if o in ("-d", "--dir"):
463    directory = a
464  if o in ("-s", "--size"):
465    size = int(a)
466  if o in ("-r", "--radius"):
467    radius = float(a)
468  if o in ("-g", "--gazeteer"):
469    gazeteer = a
470  if o in ("--pos"):
471    lat, lon = [float(x) for x in a.split(",")]
472    specifyCentre = 1
473    print "Lat: %f\nLon: %f" % (lat,lon)
474
475TracklogPlotter = TracklogInfo()
476print "Loading data"
477TracklogPlotter.walkDir(directory)
478
479print "Calculating extents"
480if specifyCentre:
481  TracklogPlotter.setCentre(lat,lon)
482else:
483  TracklogPlotter.calculateCentre()
484TracklogPlotter.calculateExtents(radius)
485 
486if(not TracklogPlotter.valid):
487  print "Couldn't calculate extents"
488  sys.exit()
489
490width = size
491height = size
492fullwidth = width + 120
493print "Creating image %d x %d px" % (fullwidth,height)
494
495surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
496surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
497TracklogPlotter.createImage(fullwidth, width, height, surface, surface2)
498
499TracklogPlotter.drawTitle()
500
501print "Plotting tracklogs"
502TracklogPlotter.drawBorder()
503TracklogPlotter.drawCities(gazeteer)
504
505TracklogPlotter.drawTracklogs()
506
507TracklogPlotter.drawCredits()
508
509TracklogPlotter.finish()
Note: See TracBrowser for help on using the repository browser.