Переход на UTF8.
[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         root = tree
414
415         # Test if this is a GPX file or not and if it's version 1.1
416         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
417             self.version = "1.1"
418         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
419             self.version = "1.0"
420         elif root.get("version") not in [ "1.1", "1.0"]:
421             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
422         else:
423             raise ValueError("Неизвестный формат дерева")
424         # attempt to validate the xml file against the schema
425
426         # initalize the GPX document for parsing.
427         self._init_version(root)
428         
429
430     def ReadFile(self, fd):
431         # Загрузка из файла.# 
432         gpx_doc = etree.parse(fd)
433         root = gpx_doc.getroot()
434
435         # Test if this is a GPX file or not and if it's version 1.1
436         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
437             self.version = "1.1"
438         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
439             self.version = "1.0"
440         elif root.get("version") not in [ "1.1", "1.0"]:
441             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
442         else:
443             raise ValueError("Неизвестный формат файла")
444         # initalize the GPX document for parsing.
445         self._init_version(root)
446
447     def _init_version(self,root):
448         # Инициализация объекта.# 
449         self.creator = root.get("creator")
450         if root is not None:
451           for child in root:
452             if child.tag == tagprefix[self.version] + "author":
453               self.author = child.text
454             elif child.tag == tagprefix[self.version] + "trk":
455               self.tracks.append(GPXTrack(child, self.version))
456             elif child.tag == tagprefix[self.version] + "rte":
457               self.routes.append(GPXRoute(child, self.version))
458             elif child.tag == tagprefix[self.version] + "wpt":
459               self.waypoints.append(GPXTrackPt(child, self.version))
460             elif child.tag == tagprefix[self.version] + "metadata":
461                 for meta in child:
462                     if meta.tag == tagprefix[self.version] + "author":
463                       self.author = meta.text
464                     elif meta.tag == tagprefix[self.version] + "name":
465                       self.name = meta.text  
466                     elif meta.tag == tagprefix[self.version] + "bounds":
467                       # Границы пропускаем - их будем вычислять сами заново
468                       pass
469                     elif meta.tag == tagprefix[self.version] + "copyright":
470                       self.copyright = meta.text
471                     elif meta.tag == tagprefix[self.version] + "link":
472                       self.link = Link(meta,self.version) 
473                     else:
474                       self.meta[StripPrefix(meta.tag,self.version)]=meta.text
475                         
476         for i in self.meta.keys():
477           val = self.meta[i];
478           if val:
479             if not val.strip(' \n\t'):
480               del self.meta[i]
481
482     def elevation_gain(self):
483                 # Суммарный набор высоты.# 
484         return sum([track.elevation_gain() for track in self.tracks])
485
486     def elevation_loss(self):
487                 # Суммарная потеря высоты.# 
488         return sum([track.elevation_loss() for track in self.tracks])
489
490     def distance(self):
491         # Суммарная дистанция.# 
492         try:
493             return sum([track.distance() for track in self.tracks])
494         except IndexError:
495             print "Пустой файл!"
496
497     def filtered_distance(self):
498         # Суммарная дистанция с фильтрацией сбойных данных.# 
499         try:
500             return sum([track.filtered_distance() for track in self.tracks])
501         except IndexError:
502             print "Пустой файл!"
503
504     def duration(self):
505         # Суммарная продолжительность, не включая паузы.# 
506         dur = datetime.timedelta(0)
507         for track in self.tracks:
508             dur += track.duration()
509         return dur
510
511     def full_duration(self):
512         # Суммарная продолжительность, включая паузы# 
513         if hasattr(self.start(), 'duration'):
514             return self.start().duration(self.end())
515         else:
516             return None
517
518     def start(self):
519         # Точка старта.# 
520         return self.tracks[0].start()
521
522     def end(self):
523         # Точка финиша.# 
524         return self.tracks[-1].end()
525
526     def start_time(self):
527         # Время старта.# 
528         if self.start():
529             return self.start().time
530         else:
531             return None
532
533     def end_time(self):
534         # Время финиша.# 
535         return self.end().time
536
537     def bound_box(self):
538       # Обрамляющий прямоугольник для всех треков в файле.#  
539       minlat =360
540       maxlat = -360
541       minlon = 360
542       maxlon = -360
543       for track in self.tracks:
544         for trkseg in track.trksegs:
545           for pt in trkseg.trkpts:
546             if pt.lat<minlat:
547               minlat=pt.lat
548             if pt.lat>maxlat:
549               maxlat=pt.lat
550             if pt.lon<minlon:
551               minlon=pt.lon
552             if pt.lon>maxlon:
553               maxlon=pt.lon
554       return ((minlat,minlon),(maxlat,maxlon))      
555
556     def FixNames(self,linkname):
557       goodname = linkname.decode('UTF-8')
558       badname = goodname.encode('ascii','replace')
559       if self.name and self.name.startswith(badname):
560         self.name=self.name.replace(badname,goodname)
561       for i in self.tracks:
562         if i.name and i.name.startswith(badname):
563           i.name = i.name.replace(badname,goodname)
564
565     def ProcessTrackSegs(self,seconds_gap=120):
566       for tr in self.tracks:
567         # Объединяем все точки трека в единую последовательность
568         all_trackpoints = []
569         for trseg in tr.trksegs:
570           all_trackpoints.extend(trseg.trkpts)
571         new_segs = []
572         current_seg = None
573         prev_pt = None
574         for pt in all_trackpoints:
575           if not current_seg:
576           # Начало нового сегмента
577             current_seg = GPXTrackSeg(None,self.version)
578             current_seg.trkpts.append(pt)
579           else: 
580             delta = pt.time - prev_pt.time
581             if delta.days > 0 or delta.seconds > seconds_gap:
582               # Нашли место разрыва
583               new_segs.append(current_seg)
584               current_seg = None  
585             else:
586               current_seg.trkpts.append(pt)  
587           prev_pt=pt   
588         # Если есть, что закинуть в хвост - пишем
589         if current_seg is not None:
590           new_segs.append(current_seg)  
591         # Обработали
592         tr.trksegs = new_segs
593
594     def XMLTree(self):
595       root = etree.Element("gpx", attrib ={"creator": "pygpx",
596                                   "version": "1.1",
597                                   "xmlns": "http://www.topografix.com/GPX/1/1"}); 
598
599       # meta subtree
600
601       meta = etree.SubElement( root, "metadata" );
602       if self.name:
603         metarecord = etree.SubElement( meta, "name" )
604         metarecord.text = self.name
605       if self.author:
606         metarecord = etree.SubElement( meta, "author" )
607         metarecord.text = self.author
608       if self.copyright:
609         metarecord = etree.SubElement( meta, "copyright" )
610         metarecord.text = self.copyright
611       if self.link:  
612         self.link.write(meta)
613       for metatag in self.meta.iterkeys():
614         metarecord = etree.SubElement( meta, metatag )
615         metarecord.text = self.meta[metatag]
616
617       # wpts subtree
618
619       for i in self.waypoints:
620         i.write(root)      
621
622       # routes subtree
623       
624       for i in self.routes:
625         i.write(root)
626
627       # tracks subtree
628       
629       for i in self.tracks:
630         i.write(root)
631         
632       return root
633