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

Revision 12242, 8.8 KB checked in by sward, 5 years ago (diff)

Fix close() calls on sockets.

Line 
1# GPX Party renderer
2# Takes a directory structure full of GPX file tracklogs,
3# and renders them all to a single image
4#
5# Copyright 2007, Oliver White
6# Licensed as GNU GPL version3 or at your option any later version
7#
8# Usage: python render.py
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 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.h = 0.0
30    self.dh = 1.0/(size + 1.0)
31    self.s = 0.8
32    self.v = 0.9
33  def get(self):
34    """Get the next available colour"""
35    colour = colorsys.hsv_to_rgb(self.h, self.s, self.v)
36    self.h = self.h + self.dh
37    if(self.h > 1.0):
38      self.h = 0.0
39    return(colour)
40
41class CityPlotter(handler.ContentHandler):
42  def __init__(self,surface,extents):
43    self.extents = extents
44    self.surface = surface
45  def drawCities(self,proj,gazeteer):
46    """Load and plot city locations"""
47    if gazeteer == "none" or gazeteer == "":
48      return
49    self.loadCities(proj, gazeteer)
50    self.renderCities(proj)
51  def loadCities(self,proj,gazeteer):
52    """Load city data from the network (osmxapi), or from a file"""
53    self.attr = {}
54    self.cities = []
55    parser = make_parser()
56    parser.setContentHandler(self)
57    if gazeteer == "osmxapi":
58      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)
59      print "Downloading gazeteer"
60      sock = urllib.urlopen(URL)
61      parser.parse(sock)
62      sock.close()
63    else:
64      print "Loading gazeteer"
65      parser.parse(gazeteer)
66    print " - Loaded %d placenames" % len(self.cities)
67  def startElement(self, name, attrs):
68    """Store node positions and tag values"""
69    if(name == 'node'):
70      self.attr['lat'] = float(attrs.get('lat', None))
71      self.attr['lon'] = float(attrs.get('lon', None))
72    if(name == 'tag'):
73      self.attr[attrs.get('k', None)] = attrs.get('v', None)
74  def endElement(self, name):
75    """When each node is completely read, store it and reset the tag list for the next node"""
76    if(name == 'node'):
77      if self.attr.get('place') in ("city","town","village"):
78        self.cities.append(self.attr)
79      self.attr = {}
80  def listCities(self):
81    """Dumps list of city locations to stdout"""
82    for c in self.cities:
83      print "%s: %f,%f %s" % (c.get('name',''), c.get('lat'), c.get('lon'), c.get('place',''))
84  def renderCities(self,proj):
85    """Draws cities onto the map"""
86    for c in self.cities:
87      ctx = cairo.Context(surface)
88      ctx.set_source_rgb(1.0,1.0,1.0)
89      x = proj.xpos(c.get('lon'))
90      y = proj.ypos(c.get('lat'))
91      ctx.move_to(x, y)
92      ctx.show_text(c.get('name',''))
93      ctx.stroke()
94
95class Projection:
96  def __init__(self,N,E,S,W):
97    self.N = N
98    self.E = E
99    self.S = S
100    self.W = W
101    self.dLat = N - S
102    self.dLon = E - W
103    self.ratio = self.dLon / self.dLat
104  def setOutput(self,width,height):
105    self.width = width
106    self.height = height
107  def xpos(self,lon):
108    return(self.width * (lon - self.W) / self.dLon)
109  def ypos(self,lat):
110    return(self.height * (1 - (lat - self.S) / self.dLat))
111  def debug(self):
112    """Display the map extents"""
113    print " - Lat %f to %f, Long %f to %f" % (self.S,self.N,self.W,self.E)
114    print " - Ratio: %f" % self.ratio
115   
116class TracklogInfo(handler.ContentHandler):
117  def __init__(self):
118    self.count = 0
119    self.countPoints = 0
120    self.points = {}
121    self.currentFile = ''
122  def walkDir(self, directory):
123    """Load a directory-structure full of GPX files into memory"""
124    for root, dirs, files in os.walk(directory):
125      for file in files:
126        if(file.endswith(".gpx")):
127          print " * %s" % file
128          self.currentFile = file
129          fullFilename = join(root, file)
130          self.points[self.currentFile] = []
131          parser = make_parser()
132          parser.setContentHandler(self)
133          parser.parse(fullFilename)
134          self.count = self.count + 1
135    self.currentFile = ''
136    if(self.countPoints == 0):
137      print "No GPX files found"
138      sys.exit()
139    print "Read %d points in %d files" % (self.countPoints,self.count)
140  def startElement(self, name, attrs):
141    """Handle tracklog points found in the GPX files"""
142    if(name == 'trkpt'):
143      lat = float(attrs.get('lat', None))
144      lon = float(attrs.get('lon', None))
145      self.points[self.currentFile].append((lat,lon));
146      self.countPoints = self.countPoints + 1
147  def valid(self):
148    """Test whether the lat/long extents of the map are sane"""
149    if(self.ratio == 0.0):
150      return(0)
151    if(self.dLat <= 0.0 or self.dLon <= 0):
152      return(0)
153    return(1)
154  def calculate(self, radius):
155    """Calculate (somehow*) the extents of the map"""
156    self.calculateCentre()
157    self.calculateExtents(radius)
158    # then use that to calculate extents
159    self.proj = Projection(
160      self.lat + self.sdLat,
161      self.lon + self.sdLon,
162      self.lat - self.sdLat,
163      self.lon - self.sdLon)
164    self.proj.debug()
165  def calculateCentre(self):
166    """Calculate the centre point of the map"""
167    sumLat = 0
168    sumLon = 0
169    for x in self.points.values():
170      for y in x:
171        sumLat = sumLat + y[0]
172        sumLon = sumLon + y[1]
173    self.lat = sumLat / self.countPoints
174    self.lon = sumLon / self.countPoints
175  def calculateExtents(self,radius):
176    """Calculate the width and height of the map"""
177    c = 40000.0 # circumference of earth, km
178    self.sdLat = (radius / (c / M_PI)) / deg2rad
179    self.sdLon = self.sdLat / math.cos(self.lat * deg2rad)
180    pass
181  def createImage(self,width1,height,surface):
182    """Supply a cairo drawing surface for the maps"""
183    self.width = width1
184    self.height = height
185    self.proj.setOutput(width1,height)
186    self.extents = [0,0,width1,height]
187    self.surface = surface
188    self.drawBorder()
189    self.keyY = self.height - 20
190  def drawBorder(self):
191    """Draw a border around the 'map' portion of the image"""
192    border=5
193    ctx = cairo.Context(surface)
194    ctx.set_source_rgb(0,0,0)
195    ctx.rectangle(border,border,self.width-2*border, self.height-2*border)
196    ctx.fill()
197    self.extents = [border,border,self.width-border, self.height-border]
198  def drawKey(self, ctx, colour, name):
199    """Add a label showing which colour represents which tracklog"""
200    x = self.width + 10
201    y = self.keyY
202    ctx.arc(x, y, 4, 0, 2*M_PI)
203    ctx.fill()
204    ctx.move_to(x + 10, y+4)
205    ctx.set_source_rgb(0,0,0)
206    ctx.show_text(name)
207    ctx.stroke()
208    self.keyY = self.keyY - 20
209    pass
210  def inImage(self,x,y):
211    """Test whether an x,y coordinate is within the map drawing area"""
212    if(x < self.extents[0] or y < self.extents[1]):
213      return(0)
214    if(x > self.extents[2] or y > self.extents[3]):
215      return(0)
216    return(1)
217  def drawTracklogs(self, pointsize):
218    """Draw all tracklogs from memory onto the map"""
219    self.palette = Palette(self.count)
220    ctx = cairo.Context(surface)
221    for name,a in self.points.items():
222      colour = self.palette.get()
223      ctx.set_source_rgb(colour[0],colour[1],colour[2])
224      for b in a:
225        x = self.proj.xpos(b[1])
226        y = self.proj.ypos(b[0])
227        if(self.inImage(x,y)):
228          ctx.arc(x, y, pointsize, 0, 2*M_PI)
229          ctx.fill()
230      self.drawKey(ctx, colour, name)
231  def drawCities(self, gazeteer):
232    Cities = CityPlotter(self.surface, self.extents)
233    Cities.drawCities(self.proj, gazeteer)
234
235# Handle command-line options
236opts, args = getopt.getopt(sys.argv[1:], "hs:d:r:p:g:", ["help", "size=", "dir=", "radius=","pointsize=","gazeteer="])
237# Defauts:
238directory = "./"
239size = 600
240radius = 10 # km
241pointsize = 1 # mm
242gazeteer = "osmxapi" # can change to a filename
243# Options:
244for o, a in opts:
245  if o in ("-h", "--help"):
246    print "Usage: render.py -d [directory] -s [size,pixel] -r [radius,km] -p [point size] -g [gazeteer file]"
247    sys.exit()
248  if o in ("-d", "--dir"):
249    directory = a
250  if o in ("-s", "--size"):
251    size = int(a)
252  if o in ("-r", "--radius"):
253    radius = float(a)
254  if o in ("-p", "--pointsize"):
255    pointsize = float(a)
256  if o in ("-g", "--gazeteer"):
257    gazeteer = a
258
259TracklogPlotter = TracklogInfo()
260print "Loading data"
261TracklogPlotter.walkDir(directory)
262print "Calculating extents"
263TracklogPlotter.calculate(radius)
264if(not TracklogPlotter.valid):
265  print "Couldn't calculate extents"
266  sys.exit()
267width = size
268height = size
269fullwidth = width + 120
270print "Creating image %d x %d px" % (fullwidth,height)
271
272surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, fullwidth, height)
273TracklogPlotter.createImage(width, height, surface)
274
275print "Plotting tracklogs"
276TracklogPlotter.drawTracklogs(pointsize)
277TracklogPlotter.drawCities(gazeteer)
278
279surface.write_to_png("output.png")
280
Note: See TracBrowser for help on using the repository browser.