Многочисленные мелкие доработки, связанные с изменением отрисовки треков и загрузкой...
[pyrungps.git] / pygpx.py
1 # coding: UTF-8
2 #------------------------
3 #  Работа с GPX-файлами
4 #------------------------
5 from lxml import etree
6 import os
7 import math
8 import datetime
9 from dateutil.parser import parse
10 from pprint import pprint
11
12 def deg2rad(deg):
13 # Преобразование из градусов в радианы.
14     return deg / (180 / math.pi)
15
16 tagprefix = { '1.0': '{http://www.topografix.com/GPX/1/0}', '1.1': '{http://www.topografix.com/GPX/1/1}'}
17
18 def StripPrefix(tag,version):
19   nstag=tagprefix[version]
20   return tag.replace(nstag,'')
21   
22 def formattime(dt):
23   return dt.strftime("%Y-%m-%dT%H:%M:%S")+"."+str(int(dt.microsecond/1000)).zfill(3)+"Z"
24
25 class Link:
26     # Ссылка. Может присутствовать в точке, треке, файле.
27     def __init__(self, node, version):
28         self.version = version
29         self.href = node.get("href")
30         self.text = None
31         self.type = None
32         if node is not None:
33           for child in node:
34             if child.tag == tagprefix[version] + "text":
35               self.text = child.text
36             elif child.tag == tagprefix[version] + "type":
37               self.type = child.text
38             else:
39               raise ValueError("Неизвестный тип узла: '%s' " % child.tag)  
40
41     def write(self,root):
42       linknode = etree.SubElement(root,"link");
43       linknode.set("href",self.href)
44       if self.text:
45         etree.SubElement(linknode,"text").text = self.text
46       if self.type:
47         etree.SubElement(linknode,"type").text = self.type
48
49 class GPXTrackPt:
50     # Точка с координатами. Может присутствовать в последовательности точек в треке, маршруте, а также в списке путевых точек.# 
51
52     def __init__(self, node, version):
53         # Извлекаем данные из GPX-тэгов.# 
54         self.version = version
55         # Сначала обязательные.# 
56         if node is not None:
57           self.lat = float(node.get("lat"))
58           self.lon = float(node.get("lon"))
59         self.elevation = None
60         self.time = None
61         self.speed = None
62         self.link = None
63         self.additional_info = {}
64         # Потом необязательные.# 
65         if node is not None:
66           for child in node:
67             if child.tag == tagprefix[version] + "time":
68                 self.time = parse(child.text)
69             elif child.tag == tagprefix[version] + "ele":
70                 self.elevation = float(child.text)
71             # Стандартом скорость не предусмотрена, но де-факто в Run.GPS используется.# 
72             elif child.tag == tagprefix[version] + "speed":
73                 self.speed = float(child.text)
74             elif child.tag == tagprefix[version] + "link":
75                 self.link = Link(child,version)
76             elif StripPrefix(child.tag,version) in ['magvar','geoidheight','name','cmt','desc',
77                 'src','sym','type','fix','sat','hdop','vdop','pdop','ageofdgpsdata','dgpsid']:
78                 self.additional_info[StripPrefix(child.tag,version)]=child.text
79             elif child.tag == tagprefix[version] + "extensions":
80                 pass
81             else:
82                 raise ValueError("Неизвестный тип узла: '%s'" % child.tag)
83   
84         for i in self.additional_info.keys():
85           val = self.additional_info[i];
86           if val:
87             if not val.strip(' \n\t'):
88               del self.additional_info[i]
89
90
91     def write(self,root,tag ="wpt"):
92       wptnode = etree.SubElement(root,tag)
93       wptnode.set("lat",repr(self.lat));
94       wptnode.set("lon",repr(self.lon));
95       if self.elevation:
96         etree.SubElement(wptnode,"ele").text = repr(self.elevation)
97       if self.time:
98         etree.SubElement(wptnode,"time").text = formattime(self.time)
99       if self.speed:
100         etree.SubElement(wptnode,"speed").text = repr(self.speed)
101       if self.link:
102         self.link.write(wptnode)
103       for i in self.additional_info.keys():
104         etree.SubElement(wptnode,i).text = self.additional_info[i]
105
106     def distance(self, other):
107         # Расстояние между двумя точками на глобусе.# 
108         try:
109             # http://www.platoscave.net/blog/2009/oct/5/calculate-distance-latitude-longitude-python/
110
111             radius = 6378700.0 # meters
112
113             lat1, lon1 = self.lat, self.lon
114             lat2, lon2 = other.lat, other.lon
115
116             dlat = math.radians(lat2-lat1)
117             dlon = math.radians(lon2-lon1)
118             a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1)) \
119                 * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2)
120             c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
121             d = radius * c
122
123         except ValueError, e:
124             raise ValueError(e)
125         return d
126
127     def duration(self, other):
128         # Время между двумя точками.# 
129         return other.time - self.time
130
131 class GPXRoute:
132         # Маршрут.# 
133
134     def __init__(self, node, version):
135         self.name = None
136         self.version = version
137         self.rtepts = []
138         self.additional_info = {}
139         self.link = None
140         if node is not None:
141           for child in node:
142             if child.tag == tagprefix[version] + "name":
143               self.name = child.text
144             elif child.tag == tagprefix[version] + "link":
145               self.link = Link(child,version)
146             elif StripPrefix(child.tag,version) in [ "cmt", "desc", "src", "number", "type" ]:
147               self.additional_info[StripPrefix(child.tag,version)]=child.text
148             elif child.tag == tagprefix[version] + "rtept":
149               self.rtepts.append(GPXTrackPt(child, version))
150             elif child.tag == tagprefix[version] + "extensions":
151               pass
152             else:
153               raise ValueError("Неизвестный тип узла <%s>" % child.tag)
154         for i in self.additional_info.keys():
155           val = self.additional_info[i];
156           if val:
157             if not val.strip(' \n\t'):
158               del self.additional_info[i]
159
160     def write(self,root):
161       rtenode = etree.SubElement(root,"rte")
162       if self.name:
163         etree.SubElement(rtenode,"name").text = self.name
164       if self.link:
165         self.link.write(rtenode)
166       for i in self.additional_info.keys():
167         print "storing %s " % ( i )
168         etree.SubElement(rtenode,i).text = self.additional_info[i]
169       for i in self.rtepts:
170         i.write(rtenode,"rtept") 
171         
172 class GPXTrackSeg:
173     # Один сегмент трека.# 
174
175     def __init__(self, node, version):
176         self.version = version
177         self.trkpts = []
178         self.elevation_gain = 0.0
179         self.elevation_loss = 0.0
180         if node is not None:
181           for child in node:
182             if child.tag == tagprefix[version] + "trkpt":
183               self.trkpts.append(GPXTrackPt(child, self.version))
184             else:
185               raise ValueError("Неизвестный тип узла <%s>" % node.nodeName)
186         self._get_elevation()
187
188     def write(self,root):
189       trksegnode = etree.SubElement(root,"trkseg")
190       for i in self.trkpts:
191         i.write(trksegnode,"trkpt")      
192
193     def _get_elevation(self):
194
195         elev_data = []
196         for pt in self.trkpts:
197           if pt.elevation:
198             elev_data.append(pt.elevation)
199
200         gain = 0.0
201         loss = 0.0
202         last_elevation = None
203
204         window_size = 5
205         i = 0
206         moving_averages = []
207
208         while i < len(elev_data) - window_size + 1:
209           this_window = elev_data[i : i + window_size]
210           window_average = sum(this_window) / window_size
211           moving_averages.append(window_average)
212           i += 1
213           
214         if len(moving_averages)>2:
215           elev_data = moving_averages
216
217         for pt in elev_data:    
218           if last_elevation is not None:
219             try:
220               if pt > last_elevation:
221                   gain += pt - last_elevation
222               else:
223                   loss += last_elevation - pt
224             except:
225               pass      
226           last_elevation=pt    
227
228         self.elevation_gain = gain
229         self.elevation_loss = loss
230
231     def distance(self):
232         # Расчет длины сегмента.# 
233         _length = 0.0
234         last_pt = None
235         for pt in self.trkpts:
236           if last_pt is not None:
237             _length += last_pt.distance(pt)
238           last_pt = pt
239         return _length
240
241     def filtered_distance(self,max_speed):
242         # Расчет длины сегмента с фильтрацией предположительно сбойных участков.# 
243         _length = 0.0
244         last_pt = None
245         for pt in self.trkpts:
246           if last_pt is not None:
247             _delta = last_pt.distance(pt)
248             _time_delta = pt.time - last_pt.time
249             _time_delta_s = _time_delta.days*86400 + _time_delta.seconds + _time_delta.microseconds/1000000
250             if _time_delta_s > 0:
251                 if _delta/_time_delta_s < max_speed:
252                     _length += _delta
253             else:
254                 _length += _delta
255           last_pt = pt      
256         return _length
257
258     def duration(self):
259         # Расчет продолжительности сегмента.# 
260         return self.trkpts[0].duration(self.trkpts[-1])
261
262     def bound_box(self):
263           # Обрамляющий прямоугольник для сегмента.# 
264       minlat =360
265       maxlat = -360
266       minlon = 360
267       maxlon = -360
268       for pt in self.trkpts:
269         if pt.lat<minlat:
270           minlat=pt.lat
271         if pt.lat>maxlat:
272           maxlat=pt.lat
273         if pt.lon<minlon:
274           minlon=pt.lon
275         if pt.lon>maxlon:
276           maxlon=pt.lon
277       return ((minlat,minlon),(maxlat,maxlon))      
278
279 class GPXTrack:
280     # Трек.# 
281
282     def __init__(self, node, version):
283         # Создаем трек из GPX-данных.# 
284
285         self.version = version
286         self.trksegs = []
287         self.sport= None
288         self.additional_info = {}
289         self.name = None
290         self.link = None
291   
292         if node is not None:
293           for child in node:
294             if child.tag == tagprefix[version] + "name":
295               self.name = child.text
296             elif child.tag == tagprefix[version] + "trkseg":
297               if len(child) > 0:
298                 self.trksegs.append(GPXTrackSeg(child, self.version))
299             elif child.tag == tagprefix[version] + "link":
300               self.link = Link(child,version)
301             elif StripPrefix(child.tag,version) in [ "number", "desc", "cmt", "src", "type" ]:
302               self.additional_info[StripPrefix(child.tag,version)] = child.text
303             elif child.tag == tagprefix[version] + "extensions":
304               for ext in child:
305                 # Из расширений извлекаем вид спорта - специфично для Run.GPS.# 
306                 if ext.tag == tagprefix[version] + "sport":
307                   self.sport = ext.text
308   
309         for i in self.additional_info.keys():
310           val = self.additional_info[i];
311           if val:
312             if not val.strip(' \n\t'):
313               del self.additional_info[i]
314
315     def write(self,root):
316       trknode = etree.SubElement(root,"trk")
317       if self.name:
318         etree.SubElement(trknode,"name").text = self.name
319       if self.sport:
320         etree.SubElement(etree.SubElement(trknode,"extensions"),"sport").text=self.sport  
321       if self.link:
322         self.link.write(trknode)
323       for i in self.additional_info.keys():
324         etree.SubElement(trknode,i).text = self.additional_info[i]
325       for i in self.trksegs:
326         i.write(trknode)      
327
328     def elevation_gain(self):
329                 # Набор высоты по треку.# 
330         return sum([trkseg.elevation_gain for trkseg in self.trksegs])
331
332     def elevation_loss(self):
333                 # Сброс высоты по треку.# 
334         return sum([trkseg.elevation_loss for trkseg in self.trksegs])
335
336     def distance(self):
337         # Длина трека.# 
338         try:
339             return sum([trkseg.distance() for trkseg in self.trksegs])
340         except IndexError:
341             print "Пустой сегмент трека, расчет длины невозможен."
342
343     def filtered_distance(self,max_speed=100):
344         # Длина трека с фильтрацией сбойных участков.# 
345         try:
346             return sum([trkseg.filtered_distance(max_speed=max_speed) for trkseg in self.trksegs])
347         except IndexError:
348             print "Пустой сегмент трека, расчет длины невозможен."
349
350     def duration(self):
351         # Продолжительность трека, не включая паузы между сегментами.# 
352         dur = datetime.timedelta(0)
353         for trkseg in self.trksegs:
354             try:
355               dur += trkseg.duration()
356             except:
357               pass  
358         return dur
359
360     def full_duration(self):
361         # Продолжительность трека, включая паузы между сегментами.# 
362         try:
363           return self.start().duration(self.end())
364         except:
365           return None
366
367     def start(self):
368         # Стартовая точка.# 
369         try:
370             return self.trksegs[0].trkpts[0]
371         except IndexError:
372             return None
373
374     def end(self):
375         # Финишная точка.# 
376         try:
377             return self.trksegs[-1].trkpts[-1]
378         except IndexError:
379             return None
380
381     def start_time(self):
382         # Время старта.# 
383         try:
384             return self.start().time
385         except:
386             return None
387
388     def end_time(self):
389         # Время финиша.# 
390         try:
391             return self.end().time
392         except:
393             return None    
394
395     def bound_box(self):
396       # Обрамляющий прямоугольник для всего трека.# 
397       minlat =360
398       maxlat = -360
399       minlon = 360
400       maxlon = -360
401       for trkseg in self.trksegs:
402         for pt in trkseg.trkpts:
403           if pt.lat<minlat:
404             minlat=pt.lat
405           if pt.lat>maxlat:
406             maxlat=pt.lat
407           if pt.lon<minlon:
408             minlon=pt.lon
409           if pt.lon>maxlon:
410             maxlon=pt.lon
411       return ((minlat,minlon),(maxlat,maxlon))      
412
413
414 class GPX:
415     # Работа с GPX-документами.# 
416
417     def __init__(self):
418                 # Создание пустого GPX-объекта# 
419         PATH = os.path.dirname(__file__)
420         self.creator = None
421         self.time = None
422         self.tracks = []
423         self.routes = []
424         self.waypoints = []
425         self.version = ""
426         self.author = None
427         self.name = None
428         self.link = None
429         self.copyright = None
430         self.meta = {}
431
432     def ReadTree(self,tree):
433                 # Загрузка из дерева etree.# 
434         root = tree
435
436         # Test if this is a GPX file or not and if it's version 1.1
437         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
438             self.version = "1.1"
439         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
440             self.version = "1.0"
441         elif root.get("version") not in [ "1.1", "1.0"]:
442             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
443         else:
444             raise ValueError("Неизвестный формат дерева")
445         # attempt to validate the xml file against the schema
446
447         # initalize the GPX document for parsing.
448         self._init_version(root)
449         
450
451     def ReadFile(self, fd):
452         # Загрузка из файла.# 
453         gpx_doc = etree.parse(fd)
454         root = gpx_doc.getroot()
455
456         # Test if this is a GPX file or not and if it's version 1.1
457         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
458             self.version = "1.1"
459         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
460             self.version = "1.0"
461         elif root.get("version") not in [ "1.1", "1.0"]:
462             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
463         else:
464             raise ValueError("Неизвестный формат файла")
465         # initalize the GPX document for parsing.
466         self._init_version(root)
467
468     def _init_version(self,root):
469         # Инициализация объекта.# 
470         self.creator = root.get("creator")
471         if root is not None:
472           for child in root:
473             if child.tag == tagprefix[self.version] + "author":
474               self.author = child.text
475             elif child.tag == tagprefix[self.version] + "trk":
476               self.tracks.append(GPXTrack(child, self.version))
477             elif child.tag == tagprefix[self.version] + "rte":
478               self.routes.append(GPXRoute(child, self.version))
479             elif child.tag == tagprefix[self.version] + "wpt":
480               self.waypoints.append(GPXTrackPt(child, self.version))
481             elif child.tag == tagprefix[self.version] + "metadata":
482                 for meta in child:
483                     if meta.tag == tagprefix[self.version] + "author":
484                       self.author = meta.text
485                     elif meta.tag == tagprefix[self.version] + "name":
486                       self.name = meta.text  
487                     elif meta.tag == tagprefix[self.version] + "bounds":
488                       # Границы пропускаем - их будем вычислять сами заново
489                       pass
490                     elif meta.tag == tagprefix[self.version] + "copyright":
491                       self.copyright = meta.text
492                     elif meta.tag == tagprefix[self.version] + "link":
493                       self.link = Link(meta,self.version) 
494                     else:
495                       self.meta[StripPrefix(meta.tag,self.version)]=meta.text
496                         
497         for i in self.meta.keys():
498           val = self.meta[i];
499           if val:
500             if not val.strip(' \n\t'):
501               del self.meta[i]
502
503     def elevation_gain(self):
504                 # Суммарный набор высоты.# 
505         return sum([track.elevation_gain() for track in self.tracks])
506
507     def elevation_loss(self):
508                 # Суммарная потеря высоты.# 
509         return sum([track.elevation_loss() for track in self.tracks])
510
511     def distance(self):
512         # Суммарная дистанция.# 
513         try:
514             return sum([track.distance() for track in self.tracks])
515         except IndexError:
516             print "Пустой файл!"
517
518     def filtered_distance(self):
519         # Суммарная дистанция с фильтрацией сбойных данных.# 
520         try:
521             return sum([track.filtered_distance() for track in self.tracks])
522         except IndexError:
523             print "Пустой файл!"
524
525     def duration(self):
526         # Суммарная продолжительность, не включая паузы.# 
527         dur = datetime.timedelta(0)
528         for track in self.tracks:
529             dur += track.duration()
530         return dur
531
532     def full_duration(self):
533         # Суммарная продолжительность, включая паузы# 
534         if hasattr(self.start(), 'duration'):
535             return self.start().duration(self.end())
536         else:
537             return None
538
539     def start(self):
540         # Точка старта.# 
541         return self.tracks[0].start()
542
543     def end(self):
544         # Точка финиша.# 
545         return self.tracks[-1].end()
546
547     def start_time(self):
548         # Время старта.# 
549         if self.start():
550             return self.start().time
551         else:
552             return None
553
554     def end_time(self):
555         # Время финиша.# 
556         return self.end().time
557
558     def bound_box(self):
559       # Обрамляющий прямоугольник для всех треков в файле.#  
560       minlat =360
561       maxlat = -360
562       minlon = 360
563       maxlon = -360
564       for track in self.tracks:
565         for trkseg in track.trksegs:
566           for pt in trkseg.trkpts:
567             if pt.lat<minlat:
568               minlat=pt.lat
569             if pt.lat>maxlat:
570               maxlat=pt.lat
571             if pt.lon<minlon:
572               minlon=pt.lon
573             if pt.lon>maxlon:
574               maxlon=pt.lon
575       return ((minlat,minlon),(maxlat,maxlon))      
576
577     def FixNames(self,linkname):
578       pprint(linkname)
579       pprint(type(linkname))
580       if type(linkname) is unicode:
581         goodname=linkname
582       else:  
583         goodname = linkname.decode('UTF-8')
584       badname = goodname.encode('ascii','replace')
585       if self.name and self.name.startswith(badname):
586         self.name=self.name.replace(badname,goodname)
587       for i in self.tracks:
588         if i.name and i.name.startswith(badname):
589           i.name = i.name.replace(badname,goodname)
590
591     def ProcessTrackSegs(self,seconds_gap=120):
592       for tr in self.tracks:
593         # Объединяем все точки трека в единую последовательность
594         all_trackpoints = []
595         for trseg in tr.trksegs:
596           all_trackpoints.extend(trseg.trkpts)
597         new_segs = []
598         current_seg = None
599         prev_pt = None
600         for pt in all_trackpoints:
601           if not current_seg:
602           # Начало нового сегмента
603             current_seg = GPXTrackSeg(None,self.version)
604             current_seg.trkpts.append(pt)
605           else: 
606             delta = pt.time - prev_pt.time
607             if delta.days > 0 or delta.seconds > seconds_gap:
608               # Нашли место разрыва
609               new_segs.append(current_seg)
610               current_seg = None  
611             else:
612               current_seg.trkpts.append(pt)  
613           prev_pt=pt   
614         # Если есть, что закинуть в хвост - пишем
615         if current_seg is not None:
616           new_segs.append(current_seg)  
617         # Обработали
618         for trs in new_segs:
619           trs._get_elevation()
620         tr.trksegs = new_segs
621         
622
623     def XMLTree(self):
624       root = etree.Element("gpx", attrib ={"creator": "pygpx",
625                                   "version": "1.1",
626                                   "xmlns": "http://www.topografix.com/GPX/1/1"}); 
627
628       # meta subtree
629
630       meta = etree.SubElement( root, "metadata" );
631       if self.name:
632         metarecord = etree.SubElement( meta, "name" )
633         metarecord.text = self.name
634       if self.author:
635         metarecord = etree.SubElement( meta, "author" )
636         metarecord.text = self.author
637       if self.copyright:
638         metarecord = etree.SubElement( meta, "copyright" )
639         metarecord.text = self.copyright
640       if self.link:  
641         self.link.write(meta)
642       for metatag in self.meta.iterkeys():
643         metarecord = etree.SubElement( meta, metatag )
644         metarecord.text = self.meta[metatag]
645
646       # wpts subtree
647
648       for i in self.waypoints:
649         i.write(root)      
650
651       # routes subtree
652       
653       for i in self.routes:
654         i.write(root)
655
656       # tracks subtree
657       
658       for i in self.tracks:
659         i.write(root)
660         
661       return root
662