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

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

Slower fade (3 of each frame rather than doing extra
calculations...)

File size: 15.5 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( "mpeg2video")
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 calculate(self, radius):
224    """Calculate (somehow*) the extents of the map"""
225    self.calculateCentre()
226    self.calculateExtents(radius)
227    # then use that to calculate extents
228    self.proj = Projection(
229      self.lat + self.sdLat,
230      self.lon + self.sdLon,
231      self.lat - self.sdLat,
232      self.lon - self.sdLon)
233    self.proj.debug()
234  def calculateCentre(self):
235    """Calculate the centre point of the map"""
236    sumLat = 0
237    sumLon = 0
238    for x in self.points.values():
239      for y in x:
240        sumLat = sumLat + y[0]
241        sumLon = sumLon + y[1]
242    self.lat = sumLat / self.countPoints
243    self.lon = sumLon / self.countPoints
244  def calculateExtents(self,radius):
245    """Calculate the width and height of the map"""
246    c = 40000.0 # circumference of earth, km
247    self.sdLat = (radius / (c / M_PI)) / deg2rad
248    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
249    pass
250  def createImage(self,fullwidth,width1,height,surface,surface2):
251    """Supply a cairo drawing surface for the maps"""
252    self.fullwidth = fullwidth
253    self.width = width1
254    self.height = height
255    self.proj.setOutput(width1,height)
256    self.extents = [0,0,width1,height]
257    self.surface = surface
258    self.surface2 = surface2
259    self.drawBorder()
260    self.keyY = self.height - 20
261    #self.filenameFormat = "gfx/img_%05d.png"
262    self.video = videoThingy(fullwidth, height)
263  def drawBorder(self):
264    """Draw a border around the 'map' portion of the image"""
265    ctx = cairo.Context(surface)
266    ctx.set_source_rgb(1.0,1.0,1.0)
267    ctx.rectangle(0,0,self.fullwidth,self.height)
268    ctx.fill()
269    border=5
270    ctx = cairo.Context(surface)
271    ctx.set_source_rgb(0,0,0)
272    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
273    ctx.fill()
274    self.extents = [border,border,self.width-border, self.height-border]
275  def drawKey(self, ctx, colour, name):
276    """Add a label showing which colour represents which tracklog"""
277    x = self.width + 10
278    y = self.keyY
279    ctx.arc(x, y, 4, 0, 2*M_PI)
280    ctx.fill()
281    ctx.move_to(x + 10, y+4)
282    ctx.set_source_rgb(0,0,0)
283    ctx.show_text(name)
284    ctx.stroke()
285    self.keyY = self.keyY - 20
286    pass
287  def inImage(self,x,y):
288    """Test whether an x,y coordinate is within the map drawing area"""
289    if(x < self.extents[0] or y < self.extents[1]):
290      return(0)
291    if(x > self.extents[2] or y > self.extents[3]):
292      return(0)
293    return(1)
294  def drawTracklogs(self, pointsize):
295    """Draw all tracklogs from memory onto the map"""
296   
297    # Get a list of timestamps in the GPX
298    timeList = self.validTimes.keys()
299    timeList.sort()
300   
301    # Setup drawing
302    self.palette = Palette(self.count)
303    ctx = cairo.Context(surface)
304    # CAIRO_LINE_CAP_ROUND
305    ctx.set_line_cap(cairo.LINE_CAP_ROUND)
306   
307    # Divide time into frames
308    secondsPerFrame = 60
309    frameTimes = range(int(timeList[0]), int(timeList[-1]), secondsPerFrame)
310    numFrames = len(frameTimes)
311    lastT = 0
312    count = 1
313    pointsDrawn = 0
314    currentPositions = {}
315    # For each timeslot (not all of these will become frames in the video)
316    for t in frameTimes:
317      self.palette.reset()
318      pointsDrawnThisTimestep = 0
319      # For each file
320      for name,a in self.points.items():
321        colour = self.palette.get()
322        ctx.set_source_rgb(colour[0],colour[1],colour[2])
323        # For each trackpoint which occurs within this timeslot
324        for b in a:
325          if(b[2] <= t and b[2] > lastT):
326            x = self.proj.xpos(b[1])
327            y = self.proj.ypos(b[0])
328            if(self.inImage(x,y)):
329              if(1):
330                ctx.move_to(x,y)
331                ctx.line_to(x,y)
332                ctx.stroke()
333              else:
334                ctx.arc(x, y, pointsize, 0, 2*M_PI)
335              currentPositions[name] = (x,y)
336              ctx.fill()
337              pointsDrawnThisTimestep = pointsDrawnThisTimestep + 1
338              pointsDrawn = pointsDrawn + 1
339        # On the first frame, draw a key for each track
340        # (this remains on the image when subsequent trackpoints are drawn)
341        if(count == 1):
342          self.drawKey(ctx, colour, name)
343      # If anything changed in this frame, then add it to the video
344      if(pointsDrawnThisTimestep > 0):
345        ctx2 = cairo.Context(surface2)
346        ctx2.set_source_surface(surface, 0, 0);
347        ctx2.set_operator(cairo.OPERATOR_SOURCE);
348        ctx2.paint();
349        ctx2.set_source_rgb(1.0, 1.0, 0.0)
350        for name,pos in currentPositions.items():
351          ctx2.arc(pos[0], pos[1], pointsize*5, 0, 2*M_PI)
352          ctx2.fill()
353        self.video.addFrame(surface2)
354      print "t: %03.1f%%, %d points" % (100.0 * count / numFrames, pointsDrawnThisTimestep)
355      count = count + 1
356      lastT = t
357    self.pause(self.surface2, 50)
358    self.fadeToMapImage()
359
360  def drawCities(self, gazeteer):
361    Cities = CityPlotter(self.surface, self.extents)
362    Cities.drawCities(self.proj, gazeteer)
363 
364  def pause(self, surface, frames):
365    print "Pausing..."
366    self.video.addFrame(surface, frames)
367   
368  def fadeToMapImage(self):
369    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)
370    print "Downloading map: "
371    print URL
372    sock = urllib.urlopen(URL)
373    out = open("map.png","w")
374    out.write(sock.read())
375    out.close()
376    sock.close
377   
378    mapSurface = cairo.ImageSurface.create_from_png("map.png")
379   
380    w = mapSurface.get_width()
381    h = mapSurface.get_height()
382    print "Size %d x %d" % (w,h)
383 
384    for alphaPercent in range(0, 101, 1):
385      alpha = float(alphaPercent) / 100.0
386      print "Fading map: %1.0f%%" % alphaPercent
387     
388      # Copy
389      ctx2 = cairo.Context(surface2)
390      ctx2.set_source_surface(surface, 0, 0);
391      ctx2.set_operator(cairo.OPERATOR_SOURCE);
392      ctx2.paint();
393   
394      # Overlay
395      ctx2.set_source_surface(mapSurface, 0, 0);
396      ctx2.paint_with_alpha(alpha)
397
398      self.video.addFrame(surface2, 3)
399     
400    self.pause(self.surface2, 200)
401
402  def drawTitle(self):
403    self.drawBorder()
404    ctx = cairo.Context(self.surface)
405    page = TitlePage(0.5 * self.width, self.height, ctx)
406    page.text("OpenStreetMap", 55, 0.28)
407    page.text("Surrey Hills Mapping Party", 30, 0.46)
408    page.text("October 2006", 30, 0.52)
409    page.text("Creative Commons CC-BY-SA 2.0", 25, 0.85)
410   
411    print "Title..."
412    self.video.addFrame(self.surface, 70)
413
414  def drawCredits(self):
415    self.drawBorder()
416    ctx = cairo.Context(self.surface)
417    page = TitlePage(0.5 * self.width, self.height, ctx)
418    page.text("www.OpenStreetMap.org", 30, 0.35)
419    page.text("Creative Commons CC-BY-SA 2.0", 25, 0.85)
420    print "Credits..."
421    self.video.addFrame(self.surface, 70)
422
423class TitlePage():
424  def __init__(self,xc,height,context):
425    self.context = context
426    self.height = height
427    self.xc = xc
428    self.context.set_source_rgb(1.0, 1.0, 1.0)
429    self.context.select_font_face( \
430      "FreeSerif", 
431      cairo.FONT_SLANT_NORMAL, 
432      cairo.FONT_WEIGHT_BOLD)
433
434  def text(self,text,size, yp):
435    self.context.set_font_size(size)
436    x_bearing, y_bearing, width, height = \
437      self.context.text_extents(text)[:4]
438    self.context.move_to( \
439      self.xc - width / 2 - x_bearing, 
440      yp * self.height - height / 2 - y_bearing)
441    self.context.show_text(text)   
442
443# Handle command-line options
444opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","pointsize=","gazeteer="])
445# Defauts:
446directory = "./"
447size = 600
448radius = 10 # km
449pointsize = 1 # mm
450gazeteer = "osmxapi" # can change to a filename
451# Options:
452for o, a in opts:
453  if o in ("-h", "--help"):
454    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -p [point size] -g [gazeteer file]"
455    sys.exit()
456  if o in ("-d", "--dir"):
457    directory = a
458  if o in ("-s", "--size"):
459    size = int(a)
460  if o in ("-r", "--radius"):
461    radius = float(a)
462  if o in ("-p", "--pointsize"):
463    pointsize = float(a)
464  if o in ("-g", "--gazeteer"):
465    gazeteer = a
466
467TracklogPlotter = TracklogInfo()
468print "Loading data"
469TracklogPlotter.walkDir(directory)
470print "Calculating extents"
471TracklogPlotter.calculate(radius)
472if(not TracklogPlotter.valid):
473  print "Couldn't calculate extents"
474  sys.exit()
475width = size
476height = size
477fullwidth = width + 120
478print "Creating image %d x %d px" % (fullwidth,height)
479
480surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
481surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
482TracklogPlotter.createImage(fullwidth, width, height, surface, surface2)
483
484TracklogPlotter.drawTitle()
485
486print "Plotting tracklogs"
487TracklogPlotter.drawBorder()
488TracklogPlotter.drawCities(gazeteer)
489
490TracklogPlotter.drawTracklogs(pointsize)
491
492TracklogPlotter.drawCredits()
493
494TracklogPlotter.finish()
Note: See TracBrowser for help on using the repository browser.