В Ubuntu 22.04 сломали проекции в Mapnik, приходится использовать альтернативный...
[pyrungps.git] / pyrungps / 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         # 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         return d
123
124     def duration(self, other):
125         # Время между двумя точками.# 
126         return other.time - self.time
127
128 class GPXRoute:
129         # Маршрут.# 
130
131     def __init__(self, node, version):
132         self.name = None
133         self.version = version
134         self.rtepts = []
135         self.additional_info = {}
136         self.link = None
137         if node is not None:
138           for child in node:
139             if child.tag == tagprefix[version] + "name":
140               self.name = child.text
141             elif child.tag == tagprefix[version] + "link":
142               self.link = Link(child,version)
143             elif StripPrefix(child.tag,version) in [ "cmt", "desc", "src", "number", "type" ]:
144               self.additional_info[StripPrefix(child.tag,version)]=child.text
145             elif child.tag == tagprefix[version] + "rtept":
146               self.rtepts.append(GPXTrackPt(child, version))
147             elif child.tag == tagprefix[version] + "extensions":
148               pass
149             else:
150               raise ValueError("Неизвестный тип узла <%s>" % child.tag)
151         for i in self.additional_info.keys():
152           val = self.additional_info[i];
153           if val:
154             if not val.strip(' \n\t'):
155               del self.additional_info[i]
156
157     def write(self,root):
158       rtenode = etree.SubElement(root,"rte")
159       if self.name:
160         etree.SubElement(rtenode,"name").text = self.name
161       if self.link:
162         self.link.write(rtenode)
163       for i in self.additional_info.keys():
164         print("storing %s " % ( i ))
165         etree.SubElement(rtenode,i).text = self.additional_info[i]
166       for i in self.rtepts:
167         i.write(rtenode,"rtept") 
168         
169 class GPXTrackSeg:
170     # Один сегмент трека.# 
171
172     def __init__(self, node, version):
173         self.version = version
174         self.trkpts = []
175         self.elevation_gain = 0.0
176         self.elevation_loss = 0.0
177         if node is not None:
178           for child in node:
179             if child.tag == tagprefix[version] + "trkpt":
180               self.trkpts.append(GPXTrackPt(child, self.version))
181             else:
182               raise ValueError("Неизвестный тип узла <%s>" % node.nodeName)
183         self._get_elevation()
184
185     def write(self,root):
186       trksegnode = etree.SubElement(root,"trkseg")
187       for i in self.trkpts:
188         i.write(trksegnode,"trkpt")      
189
190     def _get_elevation(self):
191
192         elev_data = []
193         for pt in self.trkpts:
194           if pt.elevation:
195             elev_data.append(pt.elevation)
196
197         gain = 0.0
198         loss = 0.0
199         last_elevation = None
200
201         window_size = 5
202         i = 0
203         moving_averages = []
204
205         while i < len(elev_data) - window_size + 1:
206           this_window = elev_data[i : i + window_size]
207           window_average = sum(this_window) / window_size
208           moving_averages.append(window_average)
209           i += 1
210           
211         if len(moving_averages)>2:
212           elev_data = moving_averages
213
214         for pt in elev_data:    
215           if last_elevation is not None:
216             try:
217               if pt > last_elevation:
218                   gain += pt - last_elevation
219               else:
220                   loss += last_elevation - pt
221             except:
222               pass      
223           last_elevation=pt    
224
225         self.elevation_gain = gain
226         self.elevation_loss = loss
227
228     def distance(self):
229         # Расчет длины сегмента.# 
230         _length = 0.0
231         last_pt = None
232         for pt in self.trkpts:
233           if last_pt is not None:
234             _length += last_pt.distance(pt)
235           last_pt = pt
236         return _length
237
238     def filtered_distance(self,max_speed):
239         # Расчет длины сегмента с фильтрацией предположительно сбойных участков.# 
240         _length = 0.0
241         last_pt = None
242         for pt in self.trkpts:
243           if last_pt is not None:
244             _delta = last_pt.distance(pt)
245             _time_delta = pt.time - last_pt.time
246             _time_delta_s = _time_delta.days*86400 + _time_delta.seconds + _time_delta.microseconds/1000000
247             if _time_delta_s > 0:
248                 if _delta/_time_delta_s < max_speed:
249                     _length += _delta
250             else:
251                 _length += _delta
252           last_pt = pt      
253         return _length
254
255     def duration(self):
256         # Расчет продолжительности сегмента.# 
257         return self.trkpts[0].duration(self.trkpts[-1])
258
259     def bound_box(self):
260           # Обрамляющий прямоугольник для сегмента.# 
261       minlat =360
262       maxlat = -360
263       minlon = 360
264       maxlon = -360
265       for pt in self.trkpts:
266         if pt.lat<minlat:
267           minlat=pt.lat
268         if pt.lat>maxlat:
269           maxlat=pt.lat
270         if pt.lon<minlon:
271           minlon=pt.lon
272         if pt.lon>maxlon:
273           maxlon=pt.lon
274       return ((minlat,minlon),(maxlat,maxlon))      
275
276 class GPXTrack:
277     # Трек.# 
278
279     def __init__(self, node, version):
280         # Создаем трек из GPX-данных.# 
281
282         self.version = version
283         self.trksegs = []
284         self.sport= None
285         self.additional_info = {}
286         self.name = None
287         self.link = None
288   
289         if node is not None:
290           for child in node:
291             if child.tag == tagprefix[version] + "name":
292               self.name = child.text
293             elif child.tag == tagprefix[version] + "trkseg":
294               if len(child) > 0:
295                 self.trksegs.append(GPXTrackSeg(child, self.version))
296             elif child.tag == tagprefix[version] + "link":
297               self.link = Link(child,version)
298             elif StripPrefix(child.tag,version) in [ "number", "desc", "cmt", "src", "type" ]:
299               self.additional_info[StripPrefix(child.tag,version)] = child.text
300             elif child.tag == tagprefix[version] + "extensions":
301               for ext in child:
302                 # Из расширений извлекаем вид спорта - специфично для Run.GPS.# 
303                 if ext.tag == tagprefix[version] + "sport":
304                   self.sport = ext.text
305   
306         for i in self.additional_info.keys():
307           val = self.additional_info[i];
308           if val:
309             if not val.strip(' \n\t'):
310               del self.additional_info[i]
311
312     def write(self,root):
313       trknode = etree.SubElement(root,"trk")
314       if self.name:
315         etree.SubElement(trknode,"name").text = self.name
316       if self.sport:
317         etree.SubElement(etree.SubElement(trknode,"extensions"),"sport").text=self.sport  
318       if self.link:
319         self.link.write(trknode)
320       for i in self.additional_info.keys():
321         etree.SubElement(trknode,i).text = self.additional_info[i]
322       for i in self.trksegs:
323         i.write(trknode)      
324
325     def elevation_gain(self):
326                 # Набор высоты по треку.# 
327         return sum([trkseg.elevation_gain for trkseg in self.trksegs])
328
329     def elevation_loss(self):
330                 # Сброс высоты по треку.# 
331         return sum([trkseg.elevation_loss for trkseg in self.trksegs])
332
333     def distance(self):
334         # Длина трека.# 
335         try:
336             return sum([trkseg.distance() for trkseg in self.trksegs])
337         except IndexError:
338             print("Пустой сегмент трека, расчет длины невозможен.")
339
340     def filtered_distance(self,max_speed=100):
341         # Длина трека с фильтрацией сбойных участков.# 
342         try:
343             return sum([trkseg.filtered_distance(max_speed=max_speed) for trkseg in self.trksegs])
344         except IndexError:
345             print("Пустой сегмент трека, расчет длины невозможен.")
346
347     def duration(self):
348         # Продолжительность трека, не включая паузы между сегментами.# 
349         dur = datetime.timedelta(0)
350         for trkseg in self.trksegs:
351             try:
352               dur += trkseg.duration()
353             except:
354               pass  
355         return dur
356
357     def full_duration(self):
358         # Продолжительность трека, включая паузы между сегментами.# 
359         try:
360           return self.start().duration(self.end())
361         except:
362           return None
363
364     def start(self):
365         # Стартовая точка.# 
366         try:
367             return self.trksegs[0].trkpts[0]
368         except IndexError:
369             return None
370
371     def end(self):
372         # Финишная точка.# 
373         try:
374             return self.trksegs[-1].trkpts[-1]
375         except IndexError:
376             return None
377
378     def start_time(self):
379         # Время старта.# 
380         try:
381             return self.start().time
382         except:
383             return None
384
385     def end_time(self):
386         # Время финиша.# 
387         try:
388             return self.end().time
389         except:
390             return None    
391
392     def bound_box(self):
393       # Обрамляющий прямоугольник для всего трека.# 
394       minlat =360
395       maxlat = -360
396       minlon = 360
397       maxlon = -360
398       for trkseg in self.trksegs:
399         for pt in trkseg.trkpts:
400           if pt.lat<minlat:
401             minlat=pt.lat
402           if pt.lat>maxlat:
403             maxlat=pt.lat
404           if pt.lon<minlon:
405             minlon=pt.lon
406           if pt.lon>maxlon:
407             maxlon=pt.lon
408       return ((minlat,minlon),(maxlat,maxlon))      
409
410
411 class GPX:
412     # Работа с GPX-документами.# 
413
414     def __init__(self):
415                 # Создание пустого GPX-объекта# 
416         PATH = os.path.dirname(__file__)
417         self.creator = None
418         self.time = None
419         self.tracks = []
420         self.routes = []
421         self.waypoints = []
422         self.version = ""
423         self.author = None
424         self.name = None
425         self.link = None
426         self.copyright = None
427         self.meta = {}
428
429     def ReadTree(self,tree):
430                 # Загрузка из дерева etree.# 
431         root = tree
432
433         # Test if this is a GPX file or not and if it's version 1.1
434         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
435             self.version = "1.1"
436         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
437             self.version = "1.0"
438         elif root.get("version") not in [ "1.1", "1.0"]:
439             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
440         else:
441             raise ValueError("Неизвестный формат дерева")
442         # attempt to validate the xml file against the schema
443
444         # initalize the GPX document for parsing.
445         self._init_version(root)
446         
447
448     def ReadFile(self, fd):
449         # Загрузка из файла.# 
450         gpx_doc = etree.parse(fd)
451         root = gpx_doc.getroot()
452
453         # Test if this is a GPX file or not and if it's version 1.1
454         if root.tag == "{http://www.topografix.com/GPX/1/1}gpx" and root.get("version") == "1.1":
455             self.version = "1.1"
456         elif root.tag == "{http://www.topografix.com/GPX/1/0}gpx" and root.get("version") == "1.0":
457             self.version = "1.0"
458         elif root.get("version") not in [ "1.1", "1.0"]:
459             raise ValueError("Версия формата %s не поддерживается. Используйте GPX 1.1." % root.get("version"))
460         else:
461             raise ValueError("Неизвестный формат файла")
462         # initalize the GPX document for parsing.
463         self._init_version(root)
464
465     def _init_version(self,root):
466         # Инициализация объекта.# 
467         self.creator = root.get("creator")
468         if root is not None:
469           for child in root:
470             if child.tag == tagprefix[self.version] + "author":
471               self.author = child.text
472             elif child.tag == tagprefix[self.version] + "trk":
473               self.tracks.append(GPXTrack(child, self.version))
474             elif child.tag == tagprefix[self.version] + "rte":
475               self.routes.append(GPXRoute(child, self.version))
476             elif child.tag == tagprefix[self.version] + "wpt":
477               self.waypoints.append(GPXTrackPt(child, self.version))
478             elif child.tag == tagprefix[self.version] + "metadata":
479                 for meta in child:
480                     if meta.tag == tagprefix[self.version] + "author":
481                       self.author = meta.text
482                     elif meta.tag == tagprefix[self.version] + "name":
483                       self.name = meta.text  
484                     elif meta.tag == tagprefix[self.version] + "bounds":
485                       # Границы пропускаем - их будем вычислять сами заново
486                       pass
487                     elif meta.tag == tagprefix[self.version] + "copyright":
488                       self.copyright = meta.text
489                     elif meta.tag == tagprefix[self.version] + "link":
490                       self.link = Link(meta,self.version) 
491                     else:
492                       self.meta[StripPrefix(meta.tag,self.version)]=meta.text
493                         
494         for i in self.meta.keys():
495           val = self.meta[i];
496           if val:
497             if not val.strip(' \n\t'):
498               del self.meta[i]
499
500     def elevation_gain(self):
501                 # Суммарный набор высоты.# 
502         return sum([track.elevation_gain() for track in self.tracks])
503
504     def elevation_loss(self):
505                 # Суммарная потеря высоты.# 
506         return sum([track.elevation_loss() for track in self.tracks])
507
508     def distance(self):
509         # Суммарная дистанция.# 
510         try:
511             return sum([track.distance() for track in self.tracks])
512         except IndexError:
513             print("Пустой файл!")
514
515     def filtered_distance(self):
516         # Суммарная дистанция с фильтрацией сбойных данных.# 
517         try:
518             return sum([track.filtered_distance() for track in self.tracks])
519         except IndexError:
520             print("Пустой файл!")
521
522     def duration(self):
523         # Суммарная продолжительность, не включая паузы.# 
524         dur = datetime.timedelta(0)
525         for track in self.tracks:
526             dur += track.duration()
527         return dur
528
529     def full_duration(self):
530         # Суммарная продолжительность, включая паузы# 
531         if hasattr(self.start(), 'duration'):
532             return self.start().duration(self.end())
533         else:
534             return None
535
536     def start(self):
537         # Точка старта.# 
538         return self.tracks[0].start()
539
540     def end(self):
541         # Точка финиша.# 
542         return self.tracks[-1].end()
543
544     def start_time(self):
545         # Время старта.# 
546         if self.start():
547             return self.start().time
548         else:
549             return None
550
551     def end_time(self):
552         # Время финиша.# 
553         return self.end().time
554
555     def bound_box(self):
556       # Обрамляющий прямоугольник для всех треков в файле.#  
557       minlat =360
558       maxlat = -360
559       minlon = 360
560       maxlon = -360
561       for track in self.tracks:
562         for trkseg in track.trksegs:
563           for pt in trkseg.trkpts:
564             if pt.lat<minlat:
565               minlat=pt.lat
566             if pt.lat>maxlat:
567               maxlat=pt.lat
568             if pt.lon<minlon:
569               minlon=pt.lon
570             if pt.lon>maxlon:
571               maxlon=pt.lon
572       return ((minlat,minlon),(maxlat,maxlon))      
573
574     def FixNames(self,linkname):
575       pprint(linkname)
576       pprint(type(linkname))
577       goodname = linkname
578       badname = goodname.encode('ascii','replace')
579       if self.name and self.name.startswith(badname):
580         self.name=self.name.replace(badname,goodname)
581       for i in self.tracks:
582         if i.name and i.name.startswith(badname):
583           i.name = i.name.replace(badname,goodname)
584
585     def ProcessTrackSegs(self,seconds_gap=120):
586       for tr in self.tracks:
587         # Объединяем все точки трека в единую последовательность
588         all_trackpoints = []
589         for trseg in tr.trksegs:
590           all_trackpoints.extend(trseg.trkpts)
591         new_segs = []
592         current_seg = None
593         prev_pt = None
594         for pt in all_trackpoints:
595           if not current_seg:
596           # Начало нового сегмента
597             current_seg = GPXTrackSeg(None,self.version)
598             current_seg.trkpts.append(pt)
599           else: 
600             delta = pt.time - prev_pt.time
601             if delta.days > 0 or delta.seconds > seconds_gap:
602               # Нашли место разрыва
603               new_segs.append(current_seg)
604               current_seg = None  
605             else:
606               current_seg.trkpts.append(pt)  
607           prev_pt=pt   
608         # Если есть, что закинуть в хвост - пишем
609         if current_seg is not None:
610           new_segs.append(current_seg)  
611         # Обработали
612         for trs in new_segs:
613           trs._get_elevation()
614         tr.trksegs = new_segs
615         
616
617     def XMLTree(self):
618       root = etree.Element("gpx", attrib ={"creator": "pygpx",
619                                   "version": "1.1",
620                                   "xmlns": "http://www.topografix.com/GPX/1/1"}); 
621
622       # meta subtree
623
624       meta = etree.SubElement( root, "metadata" );
625       if self.name:
626         metarecord = etree.SubElement( meta, "name" )
627         metarecord.text = self.name
628       if self.author:
629         metarecord = etree.SubElement( meta, "author" )
630         metarecord.text = self.author
631       if self.copyright:
632         metarecord = etree.SubElement( meta, "copyright" )
633         metarecord.text = self.copyright
634       if self.link:  
635         self.link.write(meta)
636       for metatag in self.meta.keys():
637         metarecord = etree.SubElement( meta, metatag )
638         metarecord.text = self.meta[metatag]
639
640       # wpts subtree
641
642       for i in self.waypoints:
643         i.write(root)      
644
645       # routes subtree
646       
647       for i in self.routes:
648         i.write(root)
649
650       # tracks subtree
651       
652       for i in self.tracks:
653         i.write(root)
654         
655       return root
656