#!/usr/bin/env python # -*- coding: utf-8 -*- # """ geo.gps (Geo-Positioning) ========================= **Author:** * Dirk Alders **Description:** This module is a submodule of :mod:`geo` and includes functions and classes for geographic issues (e.g. coordinate, area, tracks, ...). **Contentlist:** * :class:`geo.gps.area` * :class:`geo.gps.coordinate` * :class:`geo.gps.tracklist` * :class:`geo.gps.track` **Unittest:** See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation. """ import calendar import math import time import xml.parsers.expat class area(object): """ :param coord1: Corner Coordinate 1 :type coord1: coordinate :param coord2: Corner Coordinate 2 :type coord2: coordinate Class to store a geographic area and support some calculations. **Example:** .. code-block:: python >>> import geo >>> ar = geo.gps.area(...) """ def __init__(self, coord1, coord2): min_lon = min(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE]) max_lon = max(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE]) min_lat = min(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE]) max_lat = max(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE]) self.coord1 = coordinate(lon=min_lon, lat=min_lat) self.coord2 = coordinate(lon=max_lon, lat=max_lat) def _max_lat(self): return self.coord2[coordinate.LATITUDE] def _max_lon(self): return self.coord2[coordinate.LONGITUDE] def _min_lat(self): return self.coord1[coordinate.LATITUDE] def _min_lon(self): return self.coord1[coordinate.LONGITUDE] def center_pos(self): """ .. warning:: Needs sphinx documentation! """ clon = (self.coord1[coordinate.LONGITUDE] + self.coord2[coordinate.LONGITUDE]) / 2 clat = (self.coord1[coordinate.LATITUDE] + self.coord2[coordinate.LATITUDE]) / 2 return coordinate(lon=clon, lat=clat) def coordinate_in_area(self, coord): """ .. warning:: Needs sphinx documentation! """ lon = coord[coordinate.LONGITUDE] lat = coord[coordinate.LATITUDE] return lon >= self._min_lon() and lon <= self._max_lon() and lat >= self._min_lat() and lat <= self._max_lat() def corner_coordinates(self): """ .. warning:: Needs sphinx documentation! """ return self.coord1, self.coord2 def extend_area(self, coord): """ .. warning:: Needs sphinx documentation! """ if coord[coordinate.LONGITUDE] < self.coord1[coordinate.LONGITUDE]: self.coord1[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE] elif coord[coordinate.LONGITUDE] > self.coord2[coordinate.LONGITUDE]: self.coord2[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE] if coord[coordinate.LATITUDE] < self.coord1[coordinate.LATITUDE]: self.coord1[coordinate.LATITUDE] = coord[coordinate.LATITUDE] elif coord[coordinate.LATITUDE] > self.coord2[coordinate.LATITUDE]: self.coord2[coordinate.LATITUDE] = coord[coordinate.LATITUDE] def osm_map(self, map_code): """ :param map_code: Map code as defined in :class:`geo.osm` (e.g. :class:`geo.osm.MAP_STANDARD`) This Method returns a :class:`geo.osm.map` instance. .. warning:: Needs sphinx documentation! """ # TODO: needs to be implemented pass def __str__(self): return "%s / %s" % self.coordinates() class coordinate(dict): """ :param lon: Londitude :type lon: float :param lat: Latitude :type lat: float :param height: Height :type height: float :param time: Time (Seconds since 1970) :type time: int Class to store a geographic coodinate and support some calculations. **Example:** .. code-block:: python >>> import geo >>> ab = geo.gps.coordinate(lat=49.976596,lon=9.1481443) >>> gb = geo.gps.coordinate(lat=53.6908298,lon=12.1583252) >>> ab.dist_to(gb) / 1000 462.3182843470017 >>> ab.angle_to(gb) / math.pi * 180 39.02285256685333 """ LATITUDE = 'lat' LONGITUDE = 'lon' HIGHT = 'hight' TIME = 'time' def __init__(self, **kwargs): dict.__init__(self, **kwargs) def __str__(self): def to_string(lon_or_lat, plus_minus=('N', 'S')): degrees = int(lon_or_lat) lon_or_lat -= degrees minutes = lon_or_lat * 60 pm = 0 if degrees >= 0 else 1 return "%d°%.4f%s" % (abs(degrees), abs(minutes), plus_minus[pm]) lon = self.get(self.LONGITUDE) lat = self.get(self.LATITUDE) if lon is not None and lat is not None: return to_string(lat) + ' ' + to_string(lon, ['E', 'W']) else: return None def angle_to(self, coord): """ This Method calculates the geographic direction in radiant from this to the given coordinate. .. note:: North is 0 (turning right). That means east is :class:`math.pi`/2. :param coord: Target coordinate. :type coord: corrdinate :returns: The geographic direction in radiant. :rtype: int or float """ lat1 = coord[self.LATITUDE] lon1 = coord[self.LONGITUDE] lat2 = self[self.LATITUDE] lon2 = self[self.LONGITUDE] if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None: dlon = lon1 - lon2 dlat = lat1 - lat2 if dlat > 0: # case (half circle north) angle = math.atan(dlon / dlat) pass elif dlat < 0: # case (half circle south) angle = math.pi + math.atan(dlon / dlat) elif dlon > 0: # case (east) angle = math.pi / 2 elif dlon < 0: # case (west) angle = math.pi * 3 / 2 else: # same point return None if angle < 0: angle += 2 * math.pi return angle else: return None def dist_to(self, coord): """ This Method calcultes the distance from this coordinate to a given coordinate. :param coord: Target coordinate. :type coord: coordinate :return: The distance between two coordinates in meters. :rtype: int or float :raises: - """ lat1 = coord[self.LATITUDE] lon1 = coord[self.LONGITUDE] lat2 = self[self.LATITUDE] lon2 = self[self.LONGITUDE] if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None: R = 6378140 dLat = math.radians(lat2 - lat1) dLon = math.radians(lon2 - lon1) 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) c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) return R * c else: return None class tracklist(list): """ Class to store a a list of tracks and parse xml files created by a navigation system like Etrax Vista. **Example:** .. code-block:: python >>> import geo >>> ... """ def __init__(self): list.__init__(self) self.__xml_track_on_read = None self.__xml_data_on_read = '' def __xml_start_element(self, name, attrs): self.__xml_data_on_read = '' if name == 'trk': # new track found in file self.__xml_track_on_read = track() elif name == 'trkpt': # new waypoint to append if 'lon' in attrs and 'lat' in attrs: self.__xml_track_on_read.append(coordinate(lon=float(attrs['lon']), lat=float(attrs['lat']))) def __xml_end_element(self, name): if name == 'trk': if self.__xml_track_on_read is not None and len(self.__xml_track_on_read) > 0: self.append(self.__xml_track_on_read) self.__xml_track_on_read = None elif name == 'name': if self.__xml_data_on_read != '': self.__xml_track_on_read.set_name(self.__xml_data_on_read) elif name == 'ele': c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1] c[c.HIGHT] = float(self.__xml_data_on_read) elif name == 'time': c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1] c[c.TIME] = int(calendar.timegm(time.strptime(self.__xml_data_on_read, '%Y-%m-%dT%H:%M:%SZ'))) def __xml_char_data(self, data): self.__xml_data_on_read += data def load_from_file(self, xmlfilehandle): """ .. warning:: Needs to be documented """ # TODO: implement either usage of a filename or filehandle # parse xml-handle p = xml.parsers.expat.ParserCreate() p.StartElementHandler = self.__xml_start_element p.EndElementHandler = self.__xml_end_element p.CharacterDataHandler = self.__xml_char_data p.ParseFile(xmlfilehandle) class track(list): """ Class to store a a tracks and support some calculations. **Example:** .. code-block:: python >>> import geo >>> ... """ def __init__(self): self._name = None list.__init__(self) self.__init_state_variables() def __init_state_variables(self): self._area = None self._average_speed = None self._hightcharacteristic = None self._end_date = None self._optimized_track = None self._passed_hight = None self._speedcharacteristic = None self._start_date = None self._total_distance = None self._total_time = None def append(self, coord): """ .. warning:: Needs to be documented """ list.append(self, coord) self.__init_state_variables() def extend(self, *args, **kwargs): """ .. warning:: Needs to be documented """ self.__init_state_variables() return list.extend(self, *args, **kwargs) def insert(self, index, coord): """ .. warning:: Needs to be documented """ list.insert(self, index, coord) self.__init_state_variables() def set_name(self, name): """ .. warning:: Needs to be documented """ self._name = name def area(self): """ :rtype: geo.gps.area or None .. warning:: Needs to be documented """ if self._area is None: if len(self) > 1: self._area = area() for c in self: self._area.extend_area(c) return self._area def average_speed(self): """ .. warning:: Needs to be documented """ if self._average_speed is None: self._average_speed = self.total_distance() / self.total_time() return self._average_speed def hightcharacteristic(self): """ .. warning:: Needs to be documented """ if self._hightcharacteristic is None: pass # TODO: implement functionality return self._hightcharacteristic def end_date(self): """ .. warning:: Needs to be documented """ if self._end_date is None: if len(self) > 0: self._end_date = self[len(self) - 1].get(coordinate.TIME) return self._end_date def name(self): """ .. warning:: Needs to be documented """ return self._name def optimized_track(self): """ .. warning:: Needs to be documented """ # TODO: REWORK TRACK OPTIMIZATION DIST_MOVED = 15. # 15m ACCELERATION_MAX = 0.5 * 9.81 # 0.5g ANGLE_DIF_FOR_BACKWARDS = 10 # +/- 5deg MAX_DELETED_POINTS = 30 def backwards_direction(l_angle, t_angle): dang = l_angle - t_angle if dang < 0: dang += math.degrees(math.pi * 2) if dang > math.degrees(math.pi) - ANGLE_DIF_FOR_BACKWARDS / 2 and dang < math.degrees(math.pi) + ANGLE_DIF_FOR_BACKWARDS / 2: return True else: return False if self._optimized_track is None: self._optimized_track = track() self._optimized_track.set_name(self._name) del_lst = [] for coord in self: if len(self._optimized_track) == 0: # first item has to be added always self._optimized_track.append(coord) else: last = self._optimized_track[-1:][0] #try: acc = coord.dist_to(last) / ((coord[coordinate.TIME] - last[coordinate.TIME]) ** 2) #except: # acc = 0.0 if len(self._optimized_track) > 1: # calculate last angle last_angle = self._optimized_track[-2:-1][0].angle_to(last) if last_angle is not None: last_angle = math.degrees(last_angle) # calculate this angle this_angle = last.angle_to(coord) if this_angle is not None: this_angle = math.degrees(this_angle) else: last_angle = this_angle = None if coord.dist_to(last) > DIST_MOVED: # distance ok. if acc < ACCELERATION_MAX: # acceleration ok. if this_angle is None or last_angle is None or not backwards_direction(last_angle, this_angle): # direction ok del_lst = [] self._optimized_track.append(coord) else: del_lst.append(coord) #print "this one was in backwards direction (%d)!" % (len(del_lst)) #print " %.2fkm: last = %.1f\xc2\xb0 - this = %.1f\xc2\xb0" % (self.total_distance()/1000., last_angle, this_angle) else: del_lst.append(coord) #print "this one was with to high acceleration (%d)!" % (len(del_lst)) #print " %.2fkm: %.1fm/s\xc2\xb2" % (self.total_distance()/1000., acc) if len(del_lst) >= MAX_DELETED_POINTS: print("Moeglicherweise ist die Optimierung des Tracks noch nicht ausgereift genug.") print(" Bei %.1f km gab es %d Koordinaten die aussortiert worden waeren. Optimierung ausgelassen." % (self.get_total_distance() / 1000., MAX_DELETED_POINTS)) self._optimized_track.extend(del_lst) del_lst = [] return self._optimized_track def osm_map(self, map_code): return self.area().osm_map(map_code) def passed_hight(self): """ .. warning:: Needs to be documented """ if self._passed_hight is None: if len(self) > 0: self._passed_hight = 0.0 hight = self[0][coordinate.HIGHT] if hight is not None: for c in self: last_hight = hight # hysteresis of 1 meter hightlist = [c[coordinate.HIGHT] - 1, hight, c[coordinate.HIGHT] + 1] hightlist.sort() hight = hightlist[1] if hight > last_hight: self._passed_hight += hight - last_hight return self._passed_hight def speedcharacteristic(self): """ .. warning:: Needs to be documented """ if self._speedcharacteristic is None: pass # TODO: implement functionality return self._speedcharacteristic def start_date(self): """ .. warning:: Needs to be documented """ if self._start_date is None: if len(self) > 0: self._start_date = self[0].get(coordinate.TIME) return self._start_date def total_distance(self): """ .. warning:: Needs to be documented """ if self._total_distance is None: if len(self) > 0: self._total_distance = 0. for i in range(0, len(self) - 1): self._total_distance += self[i].dist_to(self[i + 1]) return self._total_distance def total_time(self): """ .. warning:: Needs to be documented """ if self._total_time is None: try: self._total_time = self.end_date() - self.start_date() except TypeError: pass # doing nothing will return None as needed, if calculation is not possible return self._total_time ''' class gpxmanipu(): debug=False class trackmanipu(): def __init__(self): self.lines=[] def AddLine(self, line): self.lines.append(line) def SetName(self, name): SEARCHFORSTARTTAG=0 SEARCHFORENDTAG=1 state=SEARCHFORSTARTTAG for i in range(0, len(self.lines)): if state==SEARCHFORSTARTTAG: if self.lines[i].find('')>0: newline=self.lines[i][:self.lines[i].find('')+6] newline+=name state=SEARCHFORENDTAG if state==SEARCHFORENDTAG: if self.lines[i].find('')>0: self.lines[i]=newline+self.lines[i][self.lines[i].find(''):] else: self.lines.remove(self.lines[i]) def GetTrack(self): rv='' for line in self.lines: rv+=line return rv def __init__(self, fh): HEADERSEARCH=0 TRACKSEARCH=1 state=HEADERSEARCH self.header='' self.tracks=[] for line in fh: if state==HEADERSEARCH: if line.lstrip().startswith(''): state=TRACKSEARCH track=self.trackmanipu() track.AddLine(line) else: self.header+=line elif state==TRACKSEARCH: if line.lstrip().startswith(''): track.AddLine(line) self.tracks.append(track) track=self.trackmanipu() else: track.AddLine(line) self.footer=track.GetTrack() # This was no track del(track) if self.debug: print "Header found:" print self.header[:20]+' ... '+self.header[-20:] print str(len(self.tracks))+' tracks found:' for track in self.tracks: track=track.GetTrack() print track[:20]+' ... '+track[-20:] print "Footer found:" print self.footer def GetGpx(self): rv=self.header for track in self.tracks: rv+=track.GetTrack() rv+=self.footer return rv def SetTrackname(self, number, name): if len(self.tracks)>number-1: self.tracks[number].SetName(name) def DeleteTrack(self, number): if len(self.tracks)>number-1: return self.tracks.pop(number) else: return None '''