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

Last change on this file since 12243 was 12243, checked in by sward, 12 years ago

Fix call to TracklogPlotter?.valid() and move actual checks to the Projection
class. Return failure status on exit.

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