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

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

minor bugfixes

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