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

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

Fix close() calls on sockets.

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