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

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

Remove pointsize option, wasn't being used

Add option to specify the centre of the image (--pos=lat,lon). If you
don't specify, it will use the mean average of the trackpoints as before

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