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

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

Use line ends rather than arc() to do points

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