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