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

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

very first attempt at fading-out to the rendered map (warning: not
pretty code)

File size: 12.8 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
22deg2rad = 0.0174532925
23M_PI = 3.1415926535
24
25class Palette:
26  def __init__(self, size):
27    """Create a palette of visually-unique colours.
28    size = how many colours to create"""
29    size = max(size,1.0)
30    self.dh = 1.0/(size + 1.0)
31    self.s = 0.8
32    self.v = 0.9
33    self.reset()
34  def reset(self):
35    self.h = 0.0
36  def get(self):
37    """Get the next available colour"""
38    colour = colorsys.hsv_to_rgb(self.h, self.s, self.v)
39    self.h = self.h + self.dh
40    if(self.h > 1.0):
41      self.h = 0.0
42    return(colour)
43
44class CityPlotter(saxutils.DefaultHandler):
45  def __init__(self,surface,extents):
46    self.extents = extents
47    self.surface = surface
48  def drawCities(self,proj,gazeteer):
49    """Load and plot city locations"""
50    if gazeteer == "none" or gazeteer == "":
51      return
52    self.loadCities(proj, gazeteer)
53    self.renderCities(proj)
54  def loadCities(self,proj,gazeteer):
55    """Load city data from the network (osmxapi), or from a file"""
56    self.attr = {}
57    self.cities = []
58    parser = make_parser()
59    parser.setContentHandler(self)
60    if gazeteer == "osmxapi":
61      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)
62      print "Downloading gazeteer"
63      sock = urllib.urlopen(URL)
64      parser.parse(sock)
65      sock.close
66    else:
67      print "Loading gazeteer"
68      parser.parse(gazeteer)
69    print " - Loaded %d placenames" % len(self.cities)
70  def startElement(self, name, attrs):
71    """Store node positions and tag values"""
72    if(name == 'node'):
73      self.attr['lat'] = float(attrs.get('lat', None))
74      self.attr['lon'] = float(attrs.get('lon', None))
75    if(name == 'tag'):
76      self.attr[attrs.get('k', None)] = attrs.get('v', None)
77  def endElement(self, name):
78    """When each node is completely read, store it and reset the tag list for the next node"""
79    if(name == 'node'):
80      if self.attr.get('place') in ("city","town","village"):
81        self.cities.append(self.attr)
82      self.attr = {}
83  def listCities(self):
84    """Dumps list of city locations to stdout"""
85    for c in self.cities:
86      print "%s: %f,%f %s" % (c.get('name',''), c.get('lat'), c.get('lon'), c.get('place',''))
87  def renderCities(self,proj):
88    """Draws cities onto the map"""
89    for c in self.cities:
90      ctx = cairo.Context(surface)
91      ctx.set_source_rgb(1.0,1.0,1.0)
92      x = proj.xpos(c.get('lon'))
93      y = proj.ypos(c.get('lat'))
94      ctx.move_to(x, y)
95      ctx.show_text(c.get('name',''))
96      ctx.stroke()
97
98class Projection:
99  def __init__(self,N,E,S,W):
100    self.N = N
101    self.E = E
102    self.S = S
103    self.W = W
104    self.dLat = N - S
105    self.dLon = E - W
106    self.ratio = self.dLon / self.dLat
107  def setOutput(self,width,height):
108    self.width = width
109    self.height = height
110  def xpos(self,lon):
111    return(self.width * (lon - self.W) / self.dLon)
112  def ypos(self,lat):
113    return(self.height * (1 - (lat - self.S) / self.dLat))
114  def debug(self):
115    """Display the map extents"""
116    print " - Lat %f to %f, Long %f to %f" % (self.S,self.N,self.W,self.E)
117    print " - Ratio: %f" % self.ratio
118   
119class TracklogInfo(saxutils.DefaultHandler):
120  def __init__(self):
121    self.count = 0
122    self.countPoints = 0
123    self.points = {}
124    self.currentFile = ''
125    self.validTimes = {}
126  def walkDir(self, directory):
127    """Load a directory-structure full of GPX files into memory"""
128    for root, dirs, files in os.walk(directory):
129      for file in files:
130        if(file.endswith(".gpx")):
131          print " * %s" % file
132          self.currentFile = file
133          fullFilename = join(root, file)
134          self.points[self.currentFile] = []
135          self.inTime = 0
136          self.inTrackpoint = 0
137          parser = make_parser()
138          parser.setContentHandler(self)
139          parser.parse(fullFilename)
140          self.count = self.count + 1
141    self.currentFile = ''
142    if(self.countPoints == 0):
143      print "No GPX files found"
144      sys.exit()
145    print "Read %d points in %d files" % (self.countPoints,self.count)
146  def startElement(self, name, attrs):
147    """Handle tracklog points found in the GPX files"""
148    if(name == 'trkpt'):
149      self.pLat = float(attrs.get('lat', None))
150      self.pLon = float(attrs.get('lon', None))
151      self.inTrackpoint = 1
152    if(name == 'time'):
153      if(self.inTrackpoint == 1):
154        self.inTime = 1
155        self.timeText = ''
156  def endElement(self,name):
157    if(name == 'time' and self.inTime):
158      self.inTime = 0
159      # Parses <time>2006-10-28T10:06:03Z</time>
160      time = mktime(strptime(self.timeText, "%Y-%m-%dT%H:%M:%SZ"))
161      self.validTimes[time] = 1;
162      self.points[self.currentFile].append((self.pLat,self.pLon,time));
163      self.countPoints = self.countPoints + 1
164  def characters(self, content):
165    if(self.inTime == 1):
166      self.timeText = self.timeText + content
167   
168  def valid(self):
169    """Test whether the lat/long extents of the map are sane"""
170    if(self.ratio == 0.0):
171      return(0)
172    if(self.dLat <= 0.0 or self.dLon <= 0):
173      return(0)
174    return(1)
175  def calculate(self, radius):
176    """Calculate (somehow*) the extents of the map"""
177    self.calculateCentre()
178    self.calculateExtents(radius)
179    # then use that to calculate extents
180    self.proj = Projection(
181      self.lat + self.sdLat,
182      self.lon + self.sdLon,
183      self.lat - self.sdLat,
184      self.lon - self.sdLon)
185    self.proj.debug()
186  def calculateCentre(self):
187    """Calculate the centre point of the map"""
188    sumLat = 0
189    sumLon = 0
190    for x in self.points.values():
191      for y in x:
192        sumLat = sumLat + y[0]
193        sumLon = sumLon + y[1]
194    self.lat = sumLat / self.countPoints
195    self.lon = sumLon / self.countPoints
196  def calculateExtents(self,radius):
197    """Calculate the width and height of the map"""
198    c = 40000.0 # circumference of earth, km
199    self.sdLat = (radius / (c / M_PI)) / deg2rad
200    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
201    pass
202  def createImage(self,fullwidth,width1,height,surface,surface2):
203    """Supply a cairo drawing surface for the maps"""
204    self.fullwidth = fullwidth
205    self.width = width1
206    self.height = height
207    self.proj.setOutput(width1,height)
208    self.extents = [0,0,width1,height]
209    self.surface = surface
210    self.surface2 = surface2
211    self.drawBorder()
212    self.keyY = self.height - 20
213  def drawBorder(self):
214    """Draw a border around the 'map' portion of the image"""
215    ctx = cairo.Context(surface)
216    ctx.set_source_rgb(1.0,1.0,1.0)
217    ctx.rectangle(0,0,self.fullwidth,self.height)
218    ctx.fill()
219    border=5
220    ctx = cairo.Context(surface)
221    ctx.set_source_rgb(0,0,0)
222    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
223    ctx.fill()
224    self.extents = [border,border,self.width-border, self.height-border]
225  def drawKey(self, ctx, colour, name):
226    """Add a label showing which colour represents which tracklog"""
227    x = self.width + 10
228    y = self.keyY
229    ctx.arc(x, y, 4, 0, 2*M_PI)
230    ctx.fill()
231    ctx.move_to(x + 10, y+4)
232    ctx.set_source_rgb(0,0,0)
233    ctx.show_text(name)
234    ctx.stroke()
235    self.keyY = self.keyY - 20
236    pass
237  def inImage(self,x,y):
238    """Test whether an x,y coordinate is within the map drawing area"""
239    if(x < self.extents[0] or y < self.extents[1]):
240      return(0)
241    if(x > self.extents[2] or y > self.extents[3]):
242      return(0)
243    return(1)
244  def drawTracklogs(self, pointsize):
245    """Draw all tracklogs from memory onto the map"""
246   
247    # Get a list of timestamps in the GPX
248    timeList = self.validTimes.keys()
249    timeList.sort()
250   
251    # Setup drawing
252    self.palette = Palette(self.count)
253    ctx = cairo.Context(surface)
254    # CAIRO_LINE_CAP_ROUND
255    ctx.set_line_cap(cairo.LINE_CAP_ROUND)
256   
257    # Divide time into frames
258    secondsPerFrame = 500
259    frameTimes = range(int(timeList[0]), int(timeList[-1]), secondsPerFrame)
260    numFrames = len(frameTimes)
261    lastT = 0
262    frame = 1
263    count = 1
264    pointsDrawn = 0
265    currentPositions = {}
266    # For each timeslot (not all of these will become frames in the video)
267    for t in frameTimes:
268      self.palette.reset()
269      pointsDrawnThisTimestep = 0
270      # For each file
271      for name,a in self.points.items():
272        colour = self.palette.get()
273        ctx.set_source_rgb(colour[0],colour[1],colour[2])
274        # For each trackpoint which occurs within this timeslot
275        for b in a:
276          if(b[2] <= t and b[2] > lastT):
277            x = self.proj.xpos(b[1])
278            y = self.proj.ypos(b[0])
279            if(self.inImage(x,y)):
280              if(1):
281                ctx.move_to(x,y)
282                ctx.line_to(x,y)
283                ctx.stroke()
284              else:
285                ctx.arc(x, y, pointsize, 0, 2*M_PI)
286              currentPositions[name] = (x,y)
287              ctx.fill()
288              pointsDrawnThisTimestep = pointsDrawnThisTimestep + 1
289              pointsDrawn = pointsDrawn + 1
290        # On the first frame, draw a key for each track
291        # (this remains on the image when subsequent trackpoints are drawn)
292        if(count == 1):
293          self.drawKey(ctx, colour, name)
294      # If anything changed in this frame, then add it to the video
295      if(pointsDrawnThisTimestep > 0):
296        ctx2 = cairo.Context(surface2)
297        ctx2.set_source_surface(surface, 0, 0);
298        ctx2.set_operator(cairo.OPERATOR_SOURCE);
299        ctx2.paint();
300        ctx2.set_source_rgb(1.0, 1.0, 0.0)
301        for name,pos in currentPositions.items():
302          ctx2.arc(pos[0], pos[1], pointsize*5, 0, 2*M_PI)
303          ctx2.fill()
304        filename = "gfx/img_%05d.png" % frame
305        frame = frame + 1
306        surface2.write_to_png(filename)
307      print "t: %03.1f%%, %d points" % (100.0 * count / numFrames, pointsDrawnThisTimestep)
308      count = count + 1
309      lastT = t
310    self.fadeToMapImage("gfx/img_%05d.png", frame)
311
312  def drawCities(self, gazeteer):
313    Cities = CityPlotter(self.surface, self.extents)
314    Cities.drawCities(self.proj, gazeteer)
315 
316  def fadeToMapImage(self, filenameFormat, frame):
317    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)
318    print "Downloading map: ", URL
319    sock = urllib.urlopen(URL)
320    out = open("map.png","w")
321    out.write(sock.read())
322    out.close()
323    sock.close
324   
325    mapSurface = cairo.ImageSurface.create_from_png("map.png")
326   
327    w = mapSurface.get_width()
328    h = mapSurface.get_height()
329    print "Size %d x %d" % (w,h)
330 
331    for alphaPercent in range(0, 101, 1):
332      alpha = float(alphaPercent) / 100.0
333      print "Fading map: %1.0f%%" % alphaPercent
334     
335      # Copy
336      ctx2 = cairo.Context(surface2)
337      ctx2.set_source_surface(surface, 0, 0);
338      ctx2.set_operator(cairo.OPERATOR_SOURCE);
339      ctx2.paint();
340   
341      # Overlay
342      ctx2.set_source_surface(mapSurface, 0, 0);
343      ctx2.paint_with_alpha(alpha)
344     
345      filename = filenameFormat % frame
346      self.surface2.write_to_png(filename)
347      frame = frame + 1
348
349
350# Handle command-line options
351opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","pointsize=","gazeteer="])
352# Defauts:
353directory = "./"
354size = 600
355radius = 10 # km
356pointsize = 1 # mm
357gazeteer = "osmxapi" # can change to a filename
358# Options:
359for o, a in opts:
360  if o in ("-h", "--help"):
361    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -p [point size] -g [gazeteer file]"
362    sys.exit()
363  if o in ("-d", "--dir"):
364    directory = a
365  if o in ("-s", "--size"):
366    size = int(a)
367  if o in ("-r", "--radius"):
368    radius = float(a)
369  if o in ("-p", "--pointsize"):
370    pointsize = float(a)
371  if o in ("-g", "--gazeteer"):
372    gazeteer = a
373
374TracklogPlotter = TracklogInfo()
375print "Loading data"
376TracklogPlotter.walkDir(directory)
377print "Calculating extents"
378TracklogPlotter.calculate(radius)
379if(not TracklogPlotter.valid):
380  print "Couldn't calculate extents"
381  sys.exit()
382width = size
383height = size
384fullwidth = width + 120
385print "Creating image %d x %d px" % (fullwidth,height)
386
387surface = cairo.ImageSurface(cairo.FORMAT_RGB24, fullwidth, height)
388surface2 = cairo.ImageSurface(cairo.FORMAT_RGB24, fullwidth, height)
389TracklogPlotter.createImage(fullwidth, width, height, surface, surface2)
390
391print "Plotting tracklogs"
392TracklogPlotter.drawCities(gazeteer)
393
394TracklogPlotter.drawTracklogs(pointsize)
395
396
Note: See TracBrowser for help on using the repository browser.