Python Library GEO
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gps.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. geo.gps (Geo-Positioning)
  6. =========================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This module is a submodule of :mod:`geo` and includes functions and classes for geographic issues (e.g. coordinate, area, tracks, ...).
  11. **Contentlist:**
  12. * :class:`geo.gps.area`
  13. * :class:`geo.gps.coordinate`
  14. * :class:`geo.gps.tracklist`
  15. * :class:`geo.gps.track`
  16. **Unittest:**
  17. See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation.
  18. """
  19. import calendar
  20. import math
  21. import time
  22. import xml.parsers.expat
  23. class area(object):
  24. """
  25. :param coord1: Corner Coordinate 1
  26. :type coord1: coordinate
  27. :param coord2: Corner Coordinate 2
  28. :type coord2: coordinate
  29. Class to store a geographic area and support some calculations.
  30. **Example:**
  31. .. code-block:: python
  32. >>> import geo
  33. >>> ar = geo.gps.area(...)
  34. """
  35. def __init__(self, coord1, coord2):
  36. min_lon = min(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE])
  37. max_lon = max(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE])
  38. min_lat = min(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE])
  39. max_lat = max(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE])
  40. self.coord1 = coordinate(lon=min_lon, lat=min_lat)
  41. self.coord2 = coordinate(lon=max_lon, lat=max_lat)
  42. def _max_lat(self):
  43. return self.coord2[coordinate.LATITUDE]
  44. def _max_lon(self):
  45. return self.coord2[coordinate.LONGITUDE]
  46. def _min_lat(self):
  47. return self.coord1[coordinate.LATITUDE]
  48. def _min_lon(self):
  49. return self.coord1[coordinate.LONGITUDE]
  50. def center_pos(self):
  51. """
  52. .. warning:: Needs sphinx documentation!
  53. """
  54. clon = (self.coord1[coordinate.LONGITUDE] + self.coord2[coordinate.LONGITUDE]) / 2
  55. clat = (self.coord1[coordinate.LATITUDE] + self.coord2[coordinate.LATITUDE]) / 2
  56. return coordinate(lon=clon, lat=clat)
  57. def coordinate_in_area(self, coord):
  58. """
  59. .. warning:: Needs sphinx documentation!
  60. """
  61. lon = coord[coordinate.LONGITUDE]
  62. lat = coord[coordinate.LATITUDE]
  63. return lon >= self._min_lon() and lon <= self._max_lon() and lat >= self._min_lat() and lat <= self._max_lat()
  64. def corner_coordinates(self):
  65. """
  66. .. warning:: Needs sphinx documentation!
  67. """
  68. return self.coord1, self.coord2
  69. def extend_area(self, coord):
  70. """
  71. .. warning:: Needs sphinx documentation!
  72. """
  73. if coord[coordinate.LONGITUDE] < self.coord1[coordinate.LONGITUDE]:
  74. self.coord1[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE]
  75. elif coord[coordinate.LONGITUDE] > self.coord2[coordinate.LONGITUDE]:
  76. self.coord2[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE]
  77. if coord[coordinate.LATITUDE] < self.coord1[coordinate.LATITUDE]:
  78. self.coord1[coordinate.LATITUDE] = coord[coordinate.LATITUDE]
  79. elif coord[coordinate.LATITUDE] > self.coord2[coordinate.LATITUDE]:
  80. self.coord2[coordinate.LATITUDE] = coord[coordinate.LATITUDE]
  81. def osm_map(self, map_code):
  82. """
  83. :param map_code: Map code as defined in :class:`geo.osm` (e.g. :class:`geo.osm.MAP_STANDARD`)
  84. This Method returns a :class:`geo.osm.map` instance.
  85. .. warning:: Needs sphinx documentation!
  86. """
  87. # TODO: needs to be implemented
  88. pass
  89. def __str__(self):
  90. return "%s / %s" % self.coordinates()
  91. class coordinate(dict):
  92. """
  93. :param lon: Londitude
  94. :type lon: float
  95. :param lat: Latitude
  96. :type lat: float
  97. :param height: Height
  98. :type height: float
  99. :param time: Time (Seconds since 1970)
  100. :type time: int
  101. Class to store a geographic coodinate and support some calculations.
  102. **Example:**
  103. .. code-block:: python
  104. >>> import geo
  105. >>> ab = geo.gps.coordinate(lat=49.976596,lon=9.1481443)
  106. >>> gb = geo.gps.coordinate(lat=53.6908298,lon=12.1583252)
  107. >>> ab.dist_to(gb) / 1000
  108. 462.3182843470017
  109. >>> ab.angle_to(gb) / math.pi * 180
  110. 39.02285256685333
  111. """
  112. LATITUDE = 'lat'
  113. LONGITUDE = 'lon'
  114. HIGHT = 'hight'
  115. TIME = 'time'
  116. def __init__(self, **kwargs):
  117. dict.__init__(self, **kwargs)
  118. def __str__(self):
  119. def to_string(lon_or_lat, plus_minus=('N', 'S')):
  120. degrees = int(lon_or_lat)
  121. lon_or_lat -= degrees
  122. minutes = lon_or_lat * 60
  123. pm = 0 if degrees >= 0 else 1
  124. return "%d°%.4f%s" % (abs(degrees), abs(minutes), plus_minus[pm])
  125. lon = self.get(self.LONGITUDE)
  126. lat = self.get(self.LATITUDE)
  127. if lon is not None and lat is not None:
  128. return to_string(lat) + ' ' + to_string(lon, ['E', 'W'])
  129. else:
  130. return None
  131. def angle_to(self, coord):
  132. """
  133. This Method calculates the geographic direction in radiant from this to the given coordinate.
  134. .. note:: North is 0 (turning right). That means east is :class:`math.pi`/2.
  135. :param coord: Target coordinate.
  136. :type coord: corrdinate
  137. :returns: The geographic direction in radiant.
  138. :rtype: int or float
  139. """
  140. lat1 = coord[self.LATITUDE]
  141. lon1 = coord[self.LONGITUDE]
  142. lat2 = self[self.LATITUDE]
  143. lon2 = self[self.LONGITUDE]
  144. if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None:
  145. dlon = lon1 - lon2
  146. dlat = lat1 - lat2
  147. if dlat > 0:
  148. # case (half circle north)
  149. angle = math.atan(dlon / dlat)
  150. pass
  151. elif dlat < 0:
  152. # case (half circle south)
  153. angle = math.pi + math.atan(dlon / dlat)
  154. elif dlon > 0:
  155. # case (east)
  156. angle = math.pi / 2
  157. elif dlon < 0:
  158. # case (west)
  159. angle = math.pi * 3 / 2
  160. else:
  161. # same point
  162. return None
  163. if angle < 0:
  164. angle += 2 * math.pi
  165. return angle
  166. else:
  167. return None
  168. def dist_to(self, coord):
  169. """
  170. This Method calcultes the distance from this coordinate to a given coordinate.
  171. :param coord: Target coordinate.
  172. :type coord: coordinate
  173. :return: The distance between two coordinates in meters.
  174. :rtype: int or float
  175. :raises: -
  176. """
  177. lat1 = coord[self.LATITUDE]
  178. lon1 = coord[self.LONGITUDE]
  179. lat2 = self[self.LATITUDE]
  180. lon2 = self[self.LONGITUDE]
  181. if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None:
  182. R = 6378140
  183. dLat = math.radians(lat2 - lat1)
  184. dLon = math.radians(lon2 - lon1)
  185. a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dLon / 2) * math.sin(dLon / 2)
  186. c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  187. return R * c
  188. else:
  189. return None
  190. class tracklist(list):
  191. """
  192. Class to store a a list of tracks and parse xml files created by a navigation system like Etrax Vista.
  193. **Example:**
  194. .. code-block:: python
  195. >>> import geo
  196. >>> ...
  197. """
  198. def __init__(self):
  199. list.__init__(self)
  200. self.__xml_track_on_read = None
  201. self.__xml_data_on_read = ''
  202. def __xml_start_element(self, name, attrs):
  203. self.__xml_data_on_read = ''
  204. if name == 'trk':
  205. # new track found in file
  206. self.__xml_track_on_read = track()
  207. elif name == 'trkpt':
  208. # new waypoint to append
  209. if 'lon' in attrs and 'lat' in attrs:
  210. self.__xml_track_on_read.append(coordinate(lon=float(attrs['lon']), lat=float(attrs['lat'])))
  211. def __xml_end_element(self, name):
  212. if name == 'trk':
  213. if self.__xml_track_on_read is not None and len(self.__xml_track_on_read) > 0:
  214. self.append(self.__xml_track_on_read)
  215. self.__xml_track_on_read = None
  216. elif name == 'name':
  217. if self.__xml_data_on_read != '':
  218. self.__xml_track_on_read.set_name(self.__xml_data_on_read)
  219. elif name == 'ele':
  220. c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1]
  221. c[c.HIGHT] = float(self.__xml_data_on_read)
  222. elif name == 'time':
  223. c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1]
  224. c[c.TIME] = int(calendar.timegm(time.strptime(self.__xml_data_on_read, '%Y-%m-%dT%H:%M:%SZ')))
  225. def __xml_char_data(self, data):
  226. self.__xml_data_on_read += data
  227. def load_from_file(self, xmlfilehandle):
  228. """
  229. .. warning:: Needs to be documented
  230. """
  231. # TODO: implement either usage of a filename or filehandle
  232. # parse xml-handle
  233. p = xml.parsers.expat.ParserCreate()
  234. p.StartElementHandler = self.__xml_start_element
  235. p.EndElementHandler = self.__xml_end_element
  236. p.CharacterDataHandler = self.__xml_char_data
  237. p.ParseFile(xmlfilehandle)
  238. class track(list):
  239. """
  240. Class to store a a tracks and support some calculations.
  241. **Example:**
  242. .. code-block:: python
  243. >>> import geo
  244. >>> ...
  245. """
  246. def __init__(self):
  247. self._name = None
  248. list.__init__(self)
  249. self.__init_state_variables()
  250. def __init_state_variables(self):
  251. self._area = None
  252. self._average_speed = None
  253. self._hightcharacteristic = None
  254. self._end_date = None
  255. self._optimized_track = None
  256. self._passed_hight = None
  257. self._speedcharacteristic = None
  258. self._start_date = None
  259. self._total_distance = None
  260. self._total_time = None
  261. def append(self, coord):
  262. """
  263. .. warning:: Needs to be documented
  264. """
  265. list.append(self, coord)
  266. self.__init_state_variables()
  267. def extend(self, *args, **kwargs):
  268. """
  269. .. warning:: Needs to be documented
  270. """
  271. self.__init_state_variables()
  272. return list.extend(self, *args, **kwargs)
  273. def insert(self, index, coord):
  274. """
  275. .. warning:: Needs to be documented
  276. """
  277. list.insert(self, index, coord)
  278. self.__init_state_variables()
  279. def set_name(self, name):
  280. """
  281. .. warning:: Needs to be documented
  282. """
  283. self._name = name
  284. def area(self):
  285. """
  286. :rtype: geo.gps.area or None
  287. .. warning:: Needs to be documented
  288. """
  289. if self._area is None:
  290. if len(self) > 1:
  291. self._area = area()
  292. for c in self:
  293. self._area.extend_area(c)
  294. return self._area
  295. def average_speed(self):
  296. """
  297. .. warning:: Needs to be documented
  298. """
  299. if self._average_speed is None:
  300. self._average_speed = self.total_distance() / self.total_time()
  301. return self._average_speed
  302. def hightcharacteristic(self):
  303. """
  304. .. warning:: Needs to be documented
  305. """
  306. if self._hightcharacteristic is None:
  307. pass # TODO: implement functionality
  308. return self._hightcharacteristic
  309. def end_date(self):
  310. """
  311. .. warning:: Needs to be documented
  312. """
  313. if self._end_date is None:
  314. if len(self) > 0:
  315. self._end_date = self[len(self) - 1].get(coordinate.TIME)
  316. return self._end_date
  317. def name(self):
  318. """
  319. .. warning:: Needs to be documented
  320. """
  321. return self._name
  322. def optimized_track(self):
  323. """
  324. .. warning:: Needs to be documented
  325. """
  326. # TODO: REWORK TRACK OPTIMIZATION
  327. DIST_MOVED = 15. # 15m
  328. ACCELERATION_MAX = 0.5 * 9.81 # 0.5g
  329. ANGLE_DIF_FOR_BACKWARDS = 10 # +/- 5deg
  330. MAX_DELETED_POINTS = 30
  331. def backwards_direction(l_angle, t_angle):
  332. dang = l_angle - t_angle
  333. if dang < 0:
  334. dang += math.degrees(math.pi * 2)
  335. if dang > math.degrees(math.pi) - ANGLE_DIF_FOR_BACKWARDS / 2 and dang < math.degrees(math.pi) + ANGLE_DIF_FOR_BACKWARDS / 2:
  336. return True
  337. else:
  338. return False
  339. if self._optimized_track is None:
  340. self._optimized_track = track()
  341. self._optimized_track.set_name(self._name)
  342. del_lst = []
  343. for coord in self:
  344. if len(self._optimized_track) == 0:
  345. # first item has to be added always
  346. self._optimized_track.append(coord)
  347. else:
  348. last = self._optimized_track[-1:][0]
  349. #try:
  350. acc = coord.dist_to(last) / ((coord[coordinate.TIME] - last[coordinate.TIME]) ** 2)
  351. #except:
  352. # acc = 0.0
  353. if len(self._optimized_track) > 1:
  354. # calculate last angle
  355. last_angle = self._optimized_track[-2:-1][0].angle_to(last)
  356. if last_angle is not None:
  357. last_angle = math.degrees(last_angle)
  358. # calculate this angle
  359. this_angle = last.angle_to(coord)
  360. if this_angle is not None:
  361. this_angle = math.degrees(this_angle)
  362. else:
  363. last_angle = this_angle = None
  364. if coord.dist_to(last) > DIST_MOVED:
  365. # distance ok.
  366. if acc < ACCELERATION_MAX:
  367. # acceleration ok.
  368. if this_angle is None or last_angle is None or not backwards_direction(last_angle, this_angle):
  369. # direction ok
  370. del_lst = []
  371. self._optimized_track.append(coord)
  372. else:
  373. del_lst.append(coord)
  374. #print "this one was in backwards direction (%d)!" % (len(del_lst))
  375. #print " %.2fkm: last = %.1f\xc2\xb0 - this = %.1f\xc2\xb0" % (self.total_distance()/1000., last_angle, this_angle)
  376. else:
  377. del_lst.append(coord)
  378. #print "this one was with to high acceleration (%d)!" % (len(del_lst))
  379. #print " %.2fkm: %.1fm/s\xc2\xb2" % (self.total_distance()/1000., acc)
  380. if len(del_lst) >= MAX_DELETED_POINTS:
  381. print("Moeglicherweise ist die Optimierung des Tracks noch nicht ausgereift genug.")
  382. print(" Bei %.1f km gab es %d Koordinaten die aussortiert worden waeren. Optimierung ausgelassen." % (self.get_total_distance() / 1000., MAX_DELETED_POINTS))
  383. self._optimized_track.extend(del_lst)
  384. del_lst = []
  385. return self._optimized_track
  386. def osm_map(self, map_code):
  387. return self.area().osm_map(map_code)
  388. def passed_hight(self):
  389. """
  390. .. warning:: Needs to be documented
  391. """
  392. if self._passed_hight is None:
  393. if len(self) > 0:
  394. self._passed_hight = 0.0
  395. hight = self[0][coordinate.HIGHT]
  396. if hight is not None:
  397. for c in self:
  398. last_hight = hight
  399. # hysteresis of 1 meter
  400. hightlist = [c[coordinate.HIGHT] - 1, hight, c[coordinate.HIGHT] + 1]
  401. hightlist.sort()
  402. hight = hightlist[1]
  403. if hight > last_hight:
  404. self._passed_hight += hight - last_hight
  405. return self._passed_hight
  406. def speedcharacteristic(self):
  407. """
  408. .. warning:: Needs to be documented
  409. """
  410. if self._speedcharacteristic is None:
  411. pass # TODO: implement functionality
  412. return self._speedcharacteristic
  413. def start_date(self):
  414. """
  415. .. warning:: Needs to be documented
  416. """
  417. if self._start_date is None:
  418. if len(self) > 0:
  419. self._start_date = self[0].get(coordinate.TIME)
  420. return self._start_date
  421. def total_distance(self):
  422. """
  423. .. warning:: Needs to be documented
  424. """
  425. if self._total_distance is None:
  426. if len(self) > 0:
  427. self._total_distance = 0.
  428. for i in range(0, len(self) - 1):
  429. self._total_distance += self[i].dist_to(self[i + 1])
  430. return self._total_distance
  431. def total_time(self):
  432. """
  433. .. warning:: Needs to be documented
  434. """
  435. if self._total_time is None:
  436. try:
  437. self._total_time = self.end_date() - self.start_date()
  438. except TypeError:
  439. pass # doing nothing will return None as needed, if calculation is not possible
  440. return self._total_time
  441. '''
  442. class gpxmanipu():
  443. debug=False
  444. class trackmanipu():
  445. def __init__(self):
  446. self.lines=[]
  447. def AddLine(self, line):
  448. self.lines.append(line)
  449. def SetName(self, name):
  450. SEARCHFORSTARTTAG=0
  451. SEARCHFORENDTAG=1
  452. state=SEARCHFORSTARTTAG
  453. for i in range(0, len(self.lines)):
  454. if state==SEARCHFORSTARTTAG:
  455. if self.lines[i].find('<name>')>0:
  456. newline=self.lines[i][:self.lines[i].find('<name>')+6]
  457. newline+=name
  458. state=SEARCHFORENDTAG
  459. if state==SEARCHFORENDTAG:
  460. if self.lines[i].find('</name>')>0:
  461. self.lines[i]=newline+self.lines[i][self.lines[i].find('</name>'):]
  462. else:
  463. self.lines.remove(self.lines[i])
  464. def GetTrack(self):
  465. rv=''
  466. for line in self.lines:
  467. rv+=line
  468. return rv
  469. def __init__(self, fh):
  470. HEADERSEARCH=0
  471. TRACKSEARCH=1
  472. state=HEADERSEARCH
  473. self.header=''
  474. self.tracks=[]
  475. for line in fh:
  476. if state==HEADERSEARCH:
  477. if line.lstrip().startswith('<trk>'):
  478. state=TRACKSEARCH
  479. track=self.trackmanipu()
  480. track.AddLine(line)
  481. else:
  482. self.header+=line
  483. elif state==TRACKSEARCH:
  484. if line.lstrip().startswith('</trk>'):
  485. track.AddLine(line)
  486. self.tracks.append(track)
  487. track=self.trackmanipu()
  488. else:
  489. track.AddLine(line)
  490. self.footer=track.GetTrack() # This was no track
  491. del(track)
  492. if self.debug:
  493. print "Header found:"
  494. print self.header[:20]+' ... '+self.header[-20:]
  495. print str(len(self.tracks))+' tracks found:'
  496. for track in self.tracks:
  497. track=track.GetTrack()
  498. print track[:20]+' ... '+track[-20:]
  499. print "Footer found:"
  500. print self.footer
  501. def GetGpx(self):
  502. rv=self.header
  503. for track in self.tracks:
  504. rv+=track.GetTrack()
  505. rv+=self.footer
  506. return rv
  507. def SetTrackname(self, number, name):
  508. if len(self.tracks)>number-1:
  509. self.tracks[number].SetName(name)
  510. def DeleteTrack(self, number):
  511. if len(self.tracks)>number-1:
  512. return self.tracks.pop(number)
  513. else:
  514. return None
  515. '''