]> ToastFreeware Gitweb - philipp/winterrodeln/wradmin.git/blob - wradmin/lib/wrgpx.py
Changed encoding to UTF-8.
[philipp/winterrodeln/wradmin.git] / wradmin / lib / wrgpx.py
1 #!/usr/bin/python2.6
2 """Library to handle WRGPX files.
3 A WRGPX file (WR stands for Winterrodeln) is a normal GPX file
4 that follows several conventions so that it can be rendered to
5 a map automatically."""
6
7 import mapnik2 as mapnik
8 import datetime
9 import os
10 from lxml import etree
11 import numpy as np
12 import matplotlib
13 matplotlib.use('Agg')
14 import matplotlib.pyplot as plt
15 import io
16 from pylons.i18n.translation import _
17
18
19
20
21 class MetaData:
22     def __init__(self):
23         self.name = None           # name as string or None
24         self.desc = None           # description as string or None
25         self.author = None         # author as string or None
26         self.author_email = None   # email as string or None
27         self.copyright_year = None # year of copyright as int or None
28         self.license = None        # link to license
29         self.link = None           # link as string or None
30         self.time = None           # datetime class or None
31         self.minlat = None         # float coordinate or None
32         self.minlon = None         # float coordinate or None
33         self.maxlat = None         # float coordinate or None
34         self.maxlon = None         # float coordinate or None
35
36
37 class Waypoint:
38     def __init__(self):
39         self.lat = None            # float coordinate or None
40         self.lon = None            # float coordinate or None
41         self.ele = None            # height as float value or None
42         self.name = None           # name as string or None
43         self.type = None           # type as string or None
44     def __repr__(self):
45         return "Waypoint %s" % str(self.name)
46
47
48 class Track:
49     def __init__(self):
50         self.name = None           # name as string or None
51         self.type = None           # type as string or None
52         self.points = []           # one point is a tuple (lat, lon, ele) or (lat, lon, None)
53     def __repr__(self):
54         return "Track %s" % str(self.name)
55
56
57 class WrGpx:
58     def __init__(self):
59         self.metadata = MetaData()
60         self.waypoints = []
61         self.tracks = []
62     def __repr__(self):
63         return "WrGpx (%d waypoints; %d tracks)" % (len(self.waypoints), len(self.tracks))
64
65
66 class Hint:
67     def __init__(self, message, level, line_nr=None):
68         "level ... 0 informal information; 1 ... suggestion for improvement; 2 ... warning; 3 ... error"
69         self.message = message
70         self.level = level
71         self.line_nr = line_nr
72     def __repr__(self):
73         return "Hint: %s" % self.message
74
75
76 def parse_wrgpx(filename=None, string=None):
77     """Parses a WRGPX file and returns a tuple of a data structure representing the content of the GPX file
78     and a list of Hint classes. If the file is not well-formed or not an GPX 1.1 file, an etree.XMLSyntaxError exception is raised."""
79     hints = []
80     wrgpx = WrGpx()
81
82     ns = '{http://www.topografix.com/GPX/1/1}'
83
84     def parse_wpt(element):
85         waypoint = Waypoint()
86         waypoint.lat = float(element.attrib['lat'])
87         waypoint.lon = float(element.attrib['lon'])
88         for el in element:
89             if el.tag == ns+'ele': waypoint.ele = float(el.text)
90             elif el.tag == ns+'name': waypoint.name = el.text
91             elif el.tag == ns+'desc': waypoint.desc = el.text
92             elif el.tag == ns+'link': waypoint.link = el.attrib['href']
93             elif el.tag == ns+'type': waypoint.type = el.text
94         return waypoint
95
96     schema = etree.XMLSchema(file=os.path.dirname(os.path.realpath(__file__)) + os.sep + 'gpx_1_1.xsd')
97     parser = etree.XMLParser(schema=schema)
98     if not string is None: root = etree.fromstring(string, parser)
99     else: 
100         # xml = etree.parse(open(filename, 'rb'), parser)
101         xml = etree.parse(filename, parser)
102         root = xml.getroot()
103     assert root.tag == ns + 'gpx' # Nothing else should be possible after validation
104     
105     wrgpx_has_metadata = False
106     for element in root:
107         # Metadata
108         if element.tag == ns+'metadata':
109             wrgpx_has_metadata = True
110             for el in element:
111                 if el.tag == ns+'name': wrgpx.metadata.name = el.text
112                 elif el.tag == ns+'desc': wrgpx.metadata.desc = el.text
113                 elif el.tag == ns+'author':
114                     for e in el:
115                         if e.tag == ns+'name': wrgpx.metadata.author = e.text
116                         elif e.tag == ns+'email':
117                             wrgpx.metadata.author_email = e.attrib['id'] + '@' + e.attrib['domain']
118                 elif el.tag == ns+'copyright':
119                     for e in el:
120                         if e.tag == ns+'year': wrgpx.metadata.copyright_year = int(e.text)
121                         elif e.tag == ns+'license': wrgpx.metadata.license = e.text
122                 elif el.tag == ns+'link': wrgpx.metadata.link = el.attrib['href']
123                 elif el.tag == ns+'time':
124                     wrgpx.metadata.time = datetime.datetime.strptime(el.text, '%Y-%m-%dT%H:%M:%SZ') # '2008-11-10T20:44:52Z'
125                 elif el.tag == ns+'bounds':
126                     wrgpx.metadata.minlat = float(el.attrib['minlat'])
127                     wrgpx.metadata.minlon = float(el.attrib['minlon'])
128                     wrgpx.metadata.maxlat = float(el.attrib['maxlat'])
129                     wrgpx.metadata.maxlon = float(el.attrib['maxlon'])
130                 elif el.tag == ns+'extensions': hints.append(Hint("XML element extensions within XML element metadata is not used by WRGPX.", 2))
131
132         # Waypoints
133         elif element.tag == ns+'wpt':
134             waypoint = parse_wpt(element)
135             wrgpx.waypoints.append(waypoint)
136             if waypoint.ele is None: hints.append(Hint(_("Elevation of waypoint '%s' should be given.") % str(waypoint), 1))
137             if waypoint.name is None: hints.append(Hint(_("Name of waypoint '%s' must be given.") % str(waypoint), 2))
138             if waypoint.type is None: hints.append(Hint(_("Type of waypoint '%s' must be given (Gasthaus, Bushaltestelle, ...)") % str(waypoint), 2))
139             if not waypoint.type in ['Gasthaus', 'Parkplatz', 'Bushaltestelle']: hints.append(Hint(_("Type of the waypoint '%s' has to be one of (Gasthaus, Bushaltestelle, ...)") % str(waypoint), 2))
140
141         # Routes
142         elif element.tag == ns+'rte':
143             hints.append(Hint(_("GPX file has a 'rte' element. This is not used in WRGPX."), 2))
144
145         # Tracks
146         elif element.tag == ns+'trk':
147             track = Track()
148             for el in element:
149                 if el.tag == ns+'name': track.name = el.text
150                 elif el.tag == ns+'desc': track.desc = el.text
151                 elif el.tag == ns+'link': track.link = el.attrib['href']
152                 elif el.tag == ns+'type': track.type = el.text
153                 elif el.tag == ns+'trkseg': 
154                     no_ele = 0
155                     for e in el:
156                         if e.tag == ns+'trkpt':
157                             trkpt = parse_wpt(e)
158                             track.points.append((trkpt.lat, trkpt.lon, trkpt.ele))
159                             if trkpt.ele is None: no_ele += 1
160                         elif e.tag == ns+'extensions': hints.append(Hint(_("XML element extensions within XML element trkpt is not used by WRGPX."), 2))
161                     if no_ele > 0: hints.append(Hint(_("%d of %d track points have no elevation (%s).") % (no_ele, len(track.points), str(track)), 1))
162             if len(track.points) == 0: hints.append(Hint(_("track '%s' is empty.") % str(track), 2))
163             wrgpx.tracks.append(track)
164
165         # Extensions
166         elif element.tag == ns+'extensions':
167             hints.append(Hint(_("GPX file has a 'extensions' element. This is not used in WRGPX."), 2))
168
169     if not wrgpx_has_metadata: hints.append(Hint(_("GPX file has a no metadata (like author, ...)."), 2))
170     return (wrgpx, hints)
171
172
173 def height_profile(wrgpx):
174     "Create a elevation profile for the wrgpx class. Raises a RuntimError in case of an error, otherwise returns the PNG file as string."
175     proj_utm32 = mapnik.Projection("+proj=utm +zone=32 +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
176     bahntracks = [t for t in wrgpx.tracks if t.type=='Rodelbahn' and len(t.points) > 0]
177     if len(bahntracks) == 0: raise RuntimeError(_("There is no 'Rodelbahn' type track in the GPX file."))
178
179     # Descending order and check
180     for track in bahntracks:
181         if track.points[0][2] is None or track.points[-1][2] is None: raise RuntimeError(_('Track has no elevation data'))
182         if track.points[0][2] < track.points[-1][2]: track.points[:] = track.points[::-1]
183     bahntracks.sort(lambda x, y: -1 if x.points[0][2] > y.points[0][2] else 1)
184     points = []
185     for track in bahntracks: points.extend(track.points)
186
187     points = np.array(points)
188     for i in range(len(points)):
189         c = mapnik.Coord(points[i][1], points[i][0])
190         c = c.forward(proj_utm32)
191         points[i][0] = c.y
192         points[i][1] = c.x
193
194     # Calculate profile
195     s = np.sqrt(np.diff(points[:, 0]) ** 2 + np.diff(points[:, 1]) ** 2)
196     x = np.hstack((0, np.cumsum(s)))
197     h = points[:, 2]
198
199     # Plot
200     axxmin = 0
201     axxmax = x[-1]
202     axymin = np.floor_divide(min(h)-50, 100)*100
203     axymax = np.floor_divide(max(h)+150, 100)*100
204     f = plt.figure()
205     plt.fill(np.hstack((x, x[-1], x[0])), np.hstack((h, 0, 0)), fc='#014e9a', ec='none')
206     plt.plot(x, h, color='black')
207     plt.axis([axxmin, axxmax, axymin, axymax])
208     plt.grid()
209     plt.xlabel('Strecke in m [%d m Gesamtstrecke]' % round(x[-1], -1))
210     plt.ylabel('Höhe in m [%d m Differenz]' % round(h[0]-h[-1]))
211     plt.title('%s - Höhenprofil' % wrgpx.metadata.name)
212
213     width_px = 500
214     imgdata = io.StringIO()
215     f.savefig(imgdata, dpi=width_px/plt.gcf().get_figwidth())
216     s = imgdata.getvalue()
217     imgdata.close()
218     return s