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

Last change on this file was 15045, checked in by sward, 11 years ago

Use XAPI 0.6 for gazeteer.

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