diff --git a/InternalPythonModules/GPX_Module/gpxpy/__init__.py b/InternalPythonModules/GPX_Module/gpxpy/__init__.py index 996f22917a..ca97054890 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/__init__.py +++ b/InternalPythonModules/GPX_Module/gpxpy/__init__.py @@ -15,7 +15,9 @@ # limitations under the License. -def parse(xml_or_file, parser=None): +__version__ = '1.3.5' + +def parse(xml_or_file, version = None): """ Parse xml (string) or file object. This is just an wrapper for GPXParser.parse() function. @@ -24,11 +26,13 @@ def parse(xml_or_file, parser=None): detected, lxml if possible). xml_or_file must be the xml to parse or a file-object with the XML. + + version may be '1.0', '1.1' or None (then it will be read from the gpx + xml node if possible, if not then version 1.0 will be used). """ - from . import gpx as mod_gpx from . import parser as mod_parser - parser = mod_parser.GPXParser(xml_or_file, parser=parser) + parser = mod_parser.GPXParser(xml_or_file) - return parser.parse() + return parser.parse(version) diff --git a/InternalPythonModules/GPX_Module/gpxpy/geo.py b/InternalPythonModules/GPX_Module/gpxpy/geo.py index 4891d28d1b..7decbe5054 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/geo.py +++ b/InternalPythonModules/GPX_Module/gpxpy/geo.py @@ -14,19 +14,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pdb - import logging as mod_logging import math as mod_math from . import utils as mod_utils +log = mod_logging.getLogger(__name__) + # Generic geo related function and class(es) -# One degree in meters: -ONE_DEGREE = 1000. * 10000.8 / 90. +# latitude/longitude in GPX files is always in WGS84 datum +# WGS84 defined the Earth semi-major axis with 6378.137 km +EARTH_RADIUS = 6378.137 * 1000 -EARTH_RADIUS = 6371 * 1000 +# One degree in meters: +ONE_DEGREE = (2*mod_math.pi*EARTH_RADIUS) / 360 # ==> 111.319 km def to_rad(x): @@ -66,9 +68,7 @@ def length(locations=None, _3d=None): d = location.distance_3d(previous_location) else: d = location.distance_2d(previous_location) - if d != 0 and not d: - pass - else: + if d: length += d return length @@ -100,15 +100,15 @@ def calculate_max_speed(speeds_and_distances): # ... assert len(speeds_and_distances[-1]) == 2 - size = float(len(speeds_and_distances)) + size = len(speeds_and_distances) if size < 20: - mod_logging.debug('Segment too small to compute speed, size=%s', size) + log.debug('Segment too small to compute speed, size=%s', size) return None distances = list(map(lambda x: x[1], speeds_and_distances)) average_distance = sum(distances) / float(size) - standard_distance_deviation = mod_math.sqrt(sum(map(lambda distance: (distance-average_distance)**2, distances))/size) + standard_distance_deviation = mod_math.sqrt(sum(map(lambda distance: (distance-average_distance)**2, distances))/float(size)) # Ignore items where the distance is too big: filtered_speeds_and_distances = filter(lambda speed_and_distance: abs(speed_and_distance[1] - average_distance) <= standard_distance_deviation * 1.5, speeds_and_distances) @@ -261,9 +261,12 @@ def simplify_polyline(points, max_distance): # cases... a, b, c = get_line_equation_coefficients(begin, end) - tmp_max_distance = -1000000 - tmp_max_distance_position = None - for point_no in range(len(points[1:-1])): + # Initialize to safe values + tmp_max_distance = 0 + tmp_max_distance_position = 1 + + # Check distance of all points between begin and end, exclusive + for point_no in range(1,len(points)-1): point = points[point_no] d = abs(a * point.latitude + b * point.longitude + c) if d > tmp_max_distance: @@ -273,11 +276,14 @@ def simplify_polyline(points, max_distance): # Now that we have the most distance point, compute its real distance: real_max_distance = distance_from_line(points[tmp_max_distance_position], begin, end) + # If furthest point is less than max_distance, remove all points between begin and end if real_max_distance < max_distance: return [begin, end] - - return (simplify_polyline(points[:tmp_max_distance_position + 2], max_distance) + - simplify_polyline(points[tmp_max_distance_position + 1:], max_distance)[1:]) + + # If furthest point is more than max_distance, use it as anchor and run + # function again using (begin to anchor) and (anchor to end), remove extra anchor + return (simplify_polyline(points[:tmp_max_distance_position + 1], max_distance) + + simplify_polyline(points[tmp_max_distance_position:], max_distance)[1:]) class Location: @@ -362,8 +368,8 @@ class LocationDelta: elif (latitude_diff is not None) and (longitude_diff is not None): if (distance is not None) or (angle is not None): raise Exception('No distance/angle if using lat/lon diff!') - this.latitude_diff = latitude_diff - this.longitude_diff = longitude_diff + self.latitude_diff = latitude_diff + self.longitude_diff = longitude_diff self.move_function = self.move_by_lat_lon_diff def move(self, location): @@ -381,4 +387,4 @@ class LocationDelta: return location.latitude + lat_diff, location.longitude + lon_diff def move_by_lat_lon_diff(self, location): - return location.latitude + self.latitude_diff, location.longitude + self.longitude_diff + return location.latitude + self.latitude_diff, location.longitude + self.longitude_diff diff --git a/InternalPythonModules/GPX_Module/gpxpy/gpx.py b/InternalPythonModules/GPX_Module/gpxpy/gpx.py index 5eb0d957f9..0f02e78d1a 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/gpx.py +++ b/InternalPythonModules/GPX_Module/gpxpy/gpx.py @@ -18,8 +18,6 @@ GPX related stuff """ -import pdb - import logging as mod_logging import math as mod_math import collections as mod_collections @@ -30,17 +28,18 @@ from . import utils as mod_utils from . import geo as mod_geo from . import gpxfield as mod_gpxfield +log = mod_logging.getLogger(__name__) + # GPX date format to be used when writing the GPX output: DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' # GPX date format(s) used for parsing. The T between date and time and Z after # time are allowed, too: DATE_FORMATS = [ - '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', - #'%Y-%m-%d %H:%M:%S%z', - #'%Y-%m-%d %H:%M:%S.%f%z', + '%Y-%m-%d %H:%M:%S', ] + # Used in smoothing, sum must be 1: SMOOTHING_RATIO = (0.4, 0.2, 0.4) @@ -73,6 +72,7 @@ GPX_10_POINT_FIELDS = [ mod_gpxfield.GPXField('dgps_id', 'dgpsid'), ] GPX_11_POINT_FIELDS = [ + # See GPX for description of text fields mod_gpxfield.GPXField('latitude', attribute='lat', type=mod_gpxfield.FLOAT_TYPE, mandatory=True), mod_gpxfield.GPXField('longitude', attribute='lon', type=mod_gpxfield.FLOAT_TYPE, mandatory=True), mod_gpxfield.GPXField('elevation', 'ele', type=mod_gpxfield.FLOAT_TYPE), @@ -83,7 +83,7 @@ GPX_11_POINT_FIELDS = [ mod_gpxfield.GPXField('comment', 'cmt'), mod_gpxfield.GPXField('description', 'desc'), mod_gpxfield.GPXField('source', 'src'), - 'link', + 'link:@link', mod_gpxfield.GPXField('link', attribute='href'), mod_gpxfield.GPXField('link_text', tag='text'), mod_gpxfield.GPXField('link_type', tag='type'), @@ -97,7 +97,7 @@ GPX_11_POINT_FIELDS = [ mod_gpxfield.GPXField('position_dilution', 'pdop', type=mod_gpxfield.FLOAT_TYPE), mod_gpxfield.GPXField('age_of_dgps_data', 'ageofdgpsdata', type=mod_gpxfield.FLOAT_TYPE), mod_gpxfield.GPXField('dgps_id', 'dgpsid'), - mod_gpxfield.GPXExtensionsField('extensions'), + mod_gpxfield.GPXExtensionsField('extensions', is_list=True), ] # GPX1.0 track points have two more fields after time @@ -132,7 +132,7 @@ PointData = mod_collections.namedtuple( class GPXException(Exception): """ - Exception used for invalid GPX files. Is is used when the XML file is + Exception used for invalid GPX files. It is used when the XML file is valid but something is wrong with the GPX data. """ pass @@ -157,13 +157,10 @@ class GPXBounds: def __iter__(self): return (self.min_latitude, self.max_latitude, self.min_longitude, self.max_longitude,).__iter__() - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) - class GPXXMLSyntaxException(GPXException): """ - Exception used when the the XML syntax is invalid. + Exception used when the XML syntax is invalid. The __cause__ can be a minidom or lxml exception (See http://www.python.org/dev/peps/pep-3134/). """ @@ -212,7 +209,7 @@ class GPXWaypoint(mod_geo.Location): self.position_dilution = position_dilution self.age_of_dgps_data = None self.dgps_id = None - self.extensions = None + self.extensions = [] def __str__(self): return '[wpt{%s}:%s,%s@%s]' % (self.name, self.latitude, self.longitude, self.elevation) @@ -226,15 +223,29 @@ class GPXWaypoint(mod_geo.Location): representation += ', %s=%s' % (attribute, repr(value)) return 'GPXWaypoint(%s)' % representation + def adjust_time(self, delta): + """ + Adjusts the time of the point by the specified delta + + Parameters + ---------- + delta : datetime.timedelta + Positive time delta will adjust time into the future + Negative time delta will adjust time into the past + """ + if self.time: + self.time += delta + + def remove_time(self): + """ Will remove time metadata. """ + self.time = None + def get_max_dilution_of_precision(self): """ Only care about the max dop for filtering, no need to go into too much detail """ return max(self.horizontal_dilution, self.vertical_dilution, self.position_dilution) - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) - class GPXRoutePoint(mod_geo.Location): gpx_10_fields = GPX_10_POINT_FIELDS @@ -276,7 +287,7 @@ class GPXRoutePoint(mod_geo.Location): self.age_of_dgps_data = None self.dgps_id = None self.link_type = None - self.extensions = None + self.extensions = [] def __str__(self): return '[rtept{%s}:%s,%s@%s]' % (self.name, self.latitude, self.longitude, self.elevation) @@ -290,8 +301,22 @@ class GPXRoutePoint(mod_geo.Location): representation += ', %s=%s' % (attribute, repr(value)) return 'GPXRoutePoint(%s)' % representation - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) + def adjust_time(self, delta): + """ + Adjusts the time of the point by the specified delta + + Parameters + ---------- + delta : datetime.timedelta + Positive time delta will adjust time into the future + Negative time delta will adjust time into the past + """ + if self.time: + self.time += delta + + def remove_time(self): + """ Will remove time metadata. """ + self.time = None class GPXRoute: @@ -306,18 +331,19 @@ class GPXRoute: mod_gpxfield.GPXComplexField('points', tag='rtept', classs=GPXRoutePoint, is_list=True), ] gpx_11_fields = [ + # See GPX for description of text fields mod_gpxfield.GPXField('name'), mod_gpxfield.GPXField('comment', 'cmt'), mod_gpxfield.GPXField('description', 'desc'), mod_gpxfield.GPXField('source', 'src'), - 'link', + 'link:@link', mod_gpxfield.GPXField('link', attribute='href'), mod_gpxfield.GPXField('link_text', tag='text'), mod_gpxfield.GPXField('link_type', tag='type'), '/link', mod_gpxfield.GPXField('number', type=mod_gpxfield.INT_TYPE), mod_gpxfield.GPXField('type'), - mod_gpxfield.GPXExtensionsField('extensions'), + mod_gpxfield.GPXExtensionsField('extensions', is_list=True), mod_gpxfield.GPXComplexField('points', tag='rtept', classs=GPXRoutePoint, is_list=True), ] @@ -336,7 +362,25 @@ class GPXRoute: self.points = [] self.link_type = None self.type = None - self.extensions = None + self.extensions = [] + + def adjust_time(self, delta): + """ + Adjusts the time of the all the points in the route by the specified delta. + + Parameters + ---------- + delta : datetime.timedelta + Positive time delta will adjust time into the future + Negative time delta will adjust time into the past + """ + for point in self.points: + point.adjust_time(delta) + + def remove_time(self): + """ Removes time meta data from route. """ + for point in self.points: + point.remove_time() def remove_elevation(self): """ Removes elevation data from route """ @@ -430,9 +474,6 @@ class GPXRoute: for route_point in self.points: route_point.move(location_delta) - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) - def __repr__(self): representation = '' for attribute in 'name', 'description', 'number': @@ -483,7 +524,7 @@ class GPXTrackPoint(mod_geo.Location): self.position_dilution = position_dilution self.age_of_dgps_data = None self.dgps_id = None - self.extensions = None + self.extensions = [] def __repr__(self): representation = '%s, %s' % (self.latitude, self.longitude) @@ -572,9 +613,6 @@ class GPXTrackPoint(mod_geo.Location): def __str__(self): return '[trkpt:%s,%s@%s@%s]' % (self.latitude, self.longitude, self.elevation, self.time) - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) - class GPXTrackSegment: gpx_10_fields = [ @@ -582,14 +620,14 @@ class GPXTrackSegment: ] gpx_11_fields = [ mod_gpxfield.GPXComplexField('points', tag='trkpt', classs=GPXTrackPoint, is_list=True), - mod_gpxfield.GPXExtensionsField('extensions'), + mod_gpxfield.GPXExtensionsField('extensions', is_list=True), ] __slots__ = ('points', 'extensions', ) def __init__(self, points=None): self.points = points if points else [] - self.extensions = None + self.extensions = [] def simplify(self, max_distance=None): """ @@ -776,9 +814,9 @@ class GPXTrackSegment: ---------- moving_data : MovingData : named tuple moving_time : float - time (seconds) of segment in which movement was occuring + time (seconds) of segment in which movement was occurring stopped_time : float - time (seconds) of segment in which no movement was occuring + time (seconds) of segment in which no movement was occurring stopped_distance : float distance (meters) travelled during stopped times moving_distance : float @@ -805,7 +843,6 @@ class GPXTrackSegment: # Won't compute max_speed for first and last because of common GPS # recording errors, and because smoothing don't work well for those # points: - first_or_last = i in [0, 1, len(self.points) - 1] if point.time and previous.time: timedelta = point.time - previous.time @@ -817,7 +854,7 @@ class GPXTrackSegment: seconds = mod_utils.total_seconds(timedelta) speed_kmh = 0 if seconds > 0: - # TODO: compute treshold in m/s instead this to kmh every time: + # TODO: compute threshold in m/s instead this to kmh every time: speed_kmh = (distance / 1000.) / (mod_utils.total_seconds(timedelta) / 60. ** 2) #print speed, stopped_speed_threshold @@ -871,11 +908,11 @@ class GPXTrackSegment: min_latitude : float Minimum latitude of segment in decimal degrees [-90, 90] max_latitude : float - Maxium latitude of segment in decimal degrees [-90, 90] + Maximum latitude of segment in decimal degrees [-90, 90] min_longitude : float - Minium longitude of segment in decimal degrees [-180, 180] + Minimum longitude of segment in decimal degrees [-180, 180] max_longitude : float - Maxium longitude of segment in decimal degrees [-180, 180] + Maximum longitude of segment in decimal degrees [-180, 180] """ min_lat = None max_lat = None @@ -915,11 +952,11 @@ class GPXTrackSegment: if 0 < point_no < len(self.points): previous_point = self.points[point_no - 1] - if 0 < point_no < len(self.points) - 1: + if 0 <= point_no < len(self.points) - 1: next_point = self.points[point_no + 1] - #mod_logging.debug('previous: %s' % previous_point) - #mod_logging.debug('next: %s' % next_point) + #log.debug('previous: %s' % previous_point) + #log.debug('next: %s' % next_point) speed_1 = point.speed_between(previous_point) speed_2 = point.speed_between(next_point) @@ -946,7 +983,7 @@ class GPXTrackSegment: delta : float Elevation delta in meters to apply to track """ - mod_logging.debug('delta = %s' % delta) + log.debug('delta = %s' % delta) if not delta: return @@ -1039,11 +1076,11 @@ class GPXTrackSegment: last = self.points[-2] if not last.time or not first.time: - mod_logging.debug('Can\'t find time') + log.debug('Can\'t find time') return None if last.time < first.time: - mod_logging.debug('Not enough time data') + log.debug('Not enough time data') return None return mod_utils.total_seconds(last.time - first.time) @@ -1111,11 +1148,11 @@ class GPXTrackSegment: last_time = self.points[-1].time if not first_time and not last_time: - mod_logging.debug('No times for track segment') + log.debug('No times for track segment') return None if not first_time <= time <= last_time: - mod_logging.debug('Not in track (search for:%s, start:%s, end:%s)' % (time, first_time, last_time)) + log.debug('Not in track (search for:%s, start:%s, end:%s)' % (time, first_time, last_time)) return None for point in self.points: @@ -1296,8 +1333,6 @@ class GPXTrackSegment: return len(self.points) > 2 and float(found) / float(len(self.points)) > .75 - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) def __repr__(self): return 'GPXTrackSegment(points=[%s])' % ('...' if self.points else '') @@ -1318,18 +1353,19 @@ class GPXTrack: mod_gpxfield.GPXComplexField('segments', tag='trkseg', classs=GPXTrackSegment, is_list=True), ] gpx_11_fields = [ + # See GPX for text field description mod_gpxfield.GPXField('name'), mod_gpxfield.GPXField('comment', 'cmt'), mod_gpxfield.GPXField('description', 'desc'), mod_gpxfield.GPXField('source', 'src'), - 'link', + 'link:@link', mod_gpxfield.GPXField('link', attribute='href'), mod_gpxfield.GPXField('link_text', tag='text'), mod_gpxfield.GPXField('link_type', tag='type'), '/link', mod_gpxfield.GPXField('number', type=mod_gpxfield.INT_TYPE), mod_gpxfield.GPXField('type'), - mod_gpxfield.GPXExtensionsField('extensions'), + mod_gpxfield.GPXExtensionsField('extensions', is_list=True), mod_gpxfield.GPXComplexField('segments', tag='trkseg', classs=GPXTrackSegment, is_list=True), ] @@ -1348,7 +1384,7 @@ class GPXTrack: self.segments = [] self.link_type = None self.type = None - self.extensions = None + self.extensions = [] def simplify(self, max_distance=None): """ @@ -1455,11 +1491,11 @@ class GPXTrack: min_latitude : float Minimum latitude of track in decimal degrees [-90, 90] max_latitude : float - Maxium latitude of track in decimal degrees [-90, 90] + Maximum latitude of track in decimal degrees [-90, 90] min_longitude : float - Minium longitude of track in decimal degrees [-180, 180] + Minimum longitude of track in decimal degrees [-180, 180] max_longitude : float - Maxium longitude of track in decimal degrees [-180, 180] + Maximum longitude of track in decimal degrees [-180, 180] """ min_lat = None max_lat = None @@ -1618,9 +1654,9 @@ class GPXTrack: ---------- moving_data : MovingData : named tuple moving_time : float - time (seconds) of track in which movement was occuring + time (seconds) of track in which movement was occurring stopped_time : float - time (seconds) of track in which no movement was occuring + time (seconds) of track in which no movement was occurring stopped_distance : float distance (meters) travelled during stopped times moving_distance : float @@ -1853,8 +1889,6 @@ class GPXTrack: def clone(self): return mod_copy.deepcopy(self) - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) def __repr__(self): representation = '' @@ -1883,27 +1917,34 @@ class GPX: mod_gpxfield.GPXComplexField('routes', classs=GPXRoute, tag='rte', is_list=True), mod_gpxfield.GPXComplexField('tracks', classs=GPXTrack, tag='trk', is_list=True), ] + # Text fields serialize as empty container tags, dependents are + # are listed after as 'tag:dep1:dep2:dep3'. If no dependents are + # listed, it will always serialize. The container is closed with + # '/tag'. Required dependents are preceded by an @. If a required + # dependent is empty, nothing in the container will serialize. The + # format is 'tag:@dep2'. No optional dependents need to be listed. + # Extensions not yet supported gpx_11_fields = [ mod_gpxfield.GPXField('version', attribute=True), mod_gpxfield.GPXField('creator', attribute=True), - 'metadata', + 'metadata:name:description:author_name:author_email:author_link:copyright_author:copyright_year:copyright_license:link:time:keywords:bounds', mod_gpxfield.GPXField('name', 'name'), mod_gpxfield.GPXField('description', 'desc'), - 'author', + 'author:author_name:author_email:author_link', mod_gpxfield.GPXField('author_name', 'name'), mod_gpxfield.GPXEmailField('author_email', 'email'), - 'link', + 'link:@author_link', mod_gpxfield.GPXField('author_link', attribute='href'), mod_gpxfield.GPXField('author_link_text', tag='text'), mod_gpxfield.GPXField('author_link_type', tag='type'), '/link', '/author', - 'copyright', + 'copyright:copyright_author:copyright_year:copyright_license', mod_gpxfield.GPXField('copyright_author', attribute='author'), mod_gpxfield.GPXField('copyright_year', tag='year'), mod_gpxfield.GPXField('copyright_license', tag='license'), '/copyright', - 'link', + 'link:@link', mod_gpxfield.GPXField('link', attribute='href'), mod_gpxfield.GPXField('link_text', tag='text'), mod_gpxfield.GPXField('link_type', tag='type'), @@ -1916,7 +1957,7 @@ class GPX: mod_gpxfield.GPXComplexField('waypoints', classs=GPXWaypoint, tag='wpt', is_list=True), mod_gpxfield.GPXComplexField('routes', classs=GPXRoute, tag='rte', is_list=True), mod_gpxfield.GPXComplexField('tracks', classs=GPXTrack, tag='trk', is_list=True), - mod_gpxfield.GPXExtensionsField('extensions'), + mod_gpxfield.GPXExtensionsField('extensions', is_list=True), ] __slots__ = ('version', 'creator', 'name', 'description', 'author_name', @@ -1924,7 +1965,8 @@ class GPX: 'bounds', 'waypoints', 'routes', 'tracks', 'author_link', 'author_link_text', 'author_link_type', 'copyright_author', 'copyright_year', 'copyright_license', 'link_type', - 'metadata_extensions', 'extensions') + 'metadata_extensions', 'extensions', 'nsmap', + 'schema_locations') def __init__(self): self.version = None @@ -1945,11 +1987,13 @@ class GPX: self.copyright_author = None self.copyright_year = None self.copyright_license = None - self.metadata_extensions = None - self.extensions = None + self.metadata_extensions = [] + self.extensions = [] self.waypoints = [] self.routes = [] self.tracks = [] + self.nsmap = {} + self.schema_locations = [] def simplify(self, max_distance=None): """ @@ -1993,29 +2037,54 @@ class GPX: track.reduce_points(min_distance) # TODO - mod_logging.debug('Track reduced to %s points' % self.get_track_points_no()) + log.debug('Track reduced to %s points' % self.get_track_points_no()) - def adjust_time(self, delta): + def adjust_time(self, delta, all=False): """ Adjusts the time of all points in all of the segments of all tracks by the specified delta. + If all=True, waypoints and routes will also be adjusted by the specified delta. + Parameters ---------- delta : datetime.timedelta Positive time delta will adjust times into the future Negative time delta will adjust times into the past + all : bool + When true, also adjusts time for waypoints and routes. """ if self.time: self.time += delta for track in self.tracks: track.adjust_time(delta) - def remove_time(self): - """ Removes time data. """ + if all: + for waypoint in self.waypoints: + waypoint.adjust_time(delta) + for route in self.routes: + route.adjust_time(delta) + + def remove_time(self, all=False): + """ + Removes time data of all points in all of the segments of all tracks. + + If all=True, time date will also be removed from waypoints and routes. + + Parameters + ---------- + all : bool + When true, also removes time data for waypoints and routes. + """ for track in self.tracks: track.remove_time() + if all: + for waypoint in self.waypoints: + waypoint.remove_time() + for route in self.routes: + route.remove_time() + def remove_elevation(self, tracks=True, routes=False, waypoints=False): """ Removes elevation data. """ if tracks: @@ -2062,11 +2131,11 @@ class GPX: min_latitude : float Minimum latitude of track in decimal degrees [-90, 90] max_latitude : float - Maxium latitude of track in decimal degrees [-90, 90] + Maximum latitude of track in decimal degrees [-90, 90] min_longitude : float - Minium longitude of track in decimal degrees [-180, 180] + Minimum longitude of track in decimal degrees [-180, 180] max_longitude : float - Maxium longitude of track in decimal degrees [-180, 180] + Maximum longitude of track in decimal degrees [-180, 180] """ min_lat = None max_lat = None @@ -2191,7 +2260,7 @@ class GPX: def length_2d(self): """ Computes 2-dimensional length of the GPX file (only latitude and - longitude, no elevation). This is the sum of 3D length of all segments + longitude, no elevation). This is the sum of 2D length of all segments in all tracks. Returns @@ -2202,7 +2271,7 @@ class GPX: result = 0 for track in self.tracks: length = track.length_2d() - if length or length == 0: + if length: result += length return result @@ -2219,7 +2288,7 @@ class GPX: result = 0 for track in self.tracks: length = track.length_3d() - if length or length == 0: + if length: result += length return result @@ -2392,7 +2461,7 @@ class GPX: Returns a list of locations of elements like consisting of points where the location may be on the track - threshold_distance is the the minimum distance from the track + threshold_distance is the minimum distance from the track so that the point *may* be counted as to be "on the track". For example 0.01 means 1% of the track distance. """ @@ -2484,10 +2553,10 @@ class GPX: def add_missing_elevations(self): def _add(interval, start, end, distances_ratios): + if (start.elevation is None) or (end.elevation is None): + return assert start assert end - assert start.elevation is not None - assert end.elevation is not None assert interval assert len(interval) == len(distances_ratios) for i in range(len(interval)): @@ -2534,7 +2603,7 @@ class GPX: time_dist_after = (interval[-1].time_difference(end), interval[-1].distance_3d(end)) - # Assemble list of times and distance to neighboring points + # Assemble list of times and distance to neighbour points times_dists = [(interval[i].time_difference(interval[i+1]), interval[i].distance_3d(interval[i+1])) for i in range(len(interval) - 1)] @@ -2549,6 +2618,54 @@ class GPX: self.add_missing_data(get_data_function=lambda point: point.speed, add_missing_function=_add) + def fill_time_data_with_regular_intervals(self, start_time=None, time_delta=None, end_time=None, force=True): + """ + Fills the time data for all points in the GPX file. At least two of the parameters start_time, time_delta, and + end_time have to be provided. If the three are provided, time_delta will be ignored and will be recalculated + using start_time and end_time. + + The first GPX point will have a time equal to start_time. Then points are assumed to be recorded at regular + intervals time_delta. + + If the GPX file currently contains time data, it will be overwritten, unless the force flag is set to False, in + which case the function will return a GPXException error. + + Parameters + ---------- + start_time: datetime.datetime object + Start time of the GPX file (corresponds to the time of the first point) + time_delta: datetime.timedelta object + Time interval between two points in the GPX file + end_time: datetime.datetime object + End time of the GPX file (corresponds to the time of the last point) + force: bool + Overwrite current data if the GPX file currently contains time data + """ + if not (start_time and end_time) and not (start_time and time_delta) and not (time_delta and end_time): + raise GPXException('You must provide at least two parameters among start_time, time_step, and end_time') + + if self.has_times() and not force: + raise GPXException('GPX file currently contains time data. Use force=True to overwrite.') + + point_no = self.get_points_no() + + if start_time and end_time: + if start_time > end_time: + raise GPXException('Invalid parameters: end_time must occur after start_time') + time_delta = (end_time - start_time) / (point_no - 1) + elif not start_time: + start_time = end_time - (point_no - 1) * time_delta + + self.time = start_time + + i = 0 + for point in self.walk(only_points=True): + if i == 0: + point.time = start_time + else: + point.time = start_time + i * time_delta + i += 1 + def move(self, location_delta): """ Moves each point in the gpx file (routes, waypoints, tracks). @@ -2567,7 +2684,7 @@ class GPX: for track in self.tracks: track.move(location_delta) - def to_xml(self, version=None): + def to_xml(self, version=None, prettyprint=True): """ FIXME: Note, this method will change self.version """ @@ -2575,7 +2692,7 @@ class GPX: if self.version: version = self.version else: - version = '1.0' + version = '1.1' if version != '1.0' and version != '1.1': raise GPXException('Invalid version %s' % version) @@ -2584,21 +2701,33 @@ class GPX: if not self.creator: self.creator = 'gpx.py -- https://github.com/tkrajina/gpxpy' - v = version.replace('.', '/') - xml_attributes = { - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns': 'http://www.topografix.com/GPX/%s' % v, - 'xsi:schemaLocation': 'http://www.topografix.com/GPX/%s http://www.topografix.com/GPX/%s/gpx.xsd' % (v, v) - } + self.nsmap['xsi'] = 'http://www.w3.org/2001/XMLSchema-instance' - content = mod_gpxfield.gpx_fields_to_xml(self, 'gpx', version, custom_attributes=xml_attributes) + version_path = version.replace('.', '/') + + self.nsmap['defaultns'] = 'http://www.topografix.com/GPX/{0}'.format( + version_path + ) + + if not self.schema_locations: + self.schema_locations = [ + p.format(version_path) for p in ( + 'http://www.topografix.com/GPX/{0}', + 'http://www.topografix.com/GPX/{0}/gpx.xsd', + ) + ] + + content = mod_gpxfield.gpx_fields_to_xml( + self, 'gpx', version, + custom_attributes={ + 'xsi:schemaLocation': ' '.join(self.schema_locations) + }, + nsmap=self.nsmap, + prettyprint=prettyprint + ) return '\n' + content.strip() - def smooth(self, vertical=True, horizontal=False, remove_extremes=False): - for track in self.tracks: - track.smooth(vertical, horizontal, remove_extremes) - def has_times(self): """ See GPXTrackSegment.has_times() """ if not self.tracks: @@ -2621,9 +2750,6 @@ class GPX: return result - def __hash__(self): - return mod_utils.hash_object(self, self.__slots__) - def __repr__(self): representation = '' for attribute in 'waypoints', 'routes', 'tracks': diff --git a/InternalPythonModules/GPX_Module/gpxpy/gpxfield.py b/InternalPythonModules/GPX_Module/gpxpy/gpxfield.py index 3c9d676ed5..78ab57db66 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/gpxfield.py +++ b/InternalPythonModules/GPX_Module/gpxpy/gpxfield.py @@ -16,6 +16,8 @@ import inspect as mod_inspect import datetime as mod_datetime +import re as mod_re +import copy as mod_copy from . import utils as mod_utils @@ -26,20 +28,78 @@ class GPXFieldTypeConverter: self.to_string = to_string +RE_TIMESTAMP = mod_re.compile( + r'^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})[T ]([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})' + r'(\.[0-9]{1,8})?(Z|[+-−][0-9]{2}:?(?:[0-9]{2})?)?$') + + +class SimpleTZ(mod_datetime.tzinfo): + __slots__ = ('offset',) + + def __init__(self, s=None): + self.offset = 0 + if s and len(s) >= 2: + if s[0] in ('−', '-'): + mult = -1 + s = s[1:] + else: + if s[0] == '+': + s = s[1:] + mult = 1 + hour = int(s[:2]) if s[:2].isdigit() else 0 + if len(s) >= 4: + minute = int(s[-2:]) if s[-2:].isdigit() else 0 + else: + minute = 0 + self.offset = mult * (hour * 60 + minute) + + def utcoffset(self, dt): + return mod_datetime.timedelta(minutes=self.offset) + + def dst(self, dt): + return mod_datetime.timedelta(0) + + def tzname(self, dt): + if self.offset == 0: + return 'Z' + return '{:02}:{:02}'.format(self.offset // 60, self.offset % 60) + + def __repr__(self): + return 'SimpleTZ("{}")'.format(self.tzname(None)) + + def __eq__(self, other): + return self.offset == other.offset + + def parse_time(string): from . import gpx as mod_gpx if not string: return None - if 'T' in string: - string = string.replace('T', ' ') - if 'Z' in string: - string = string.replace('Z', '') - for date_format in mod_gpx.DATE_FORMATS: - try: - return mod_datetime.datetime.strptime(string, date_format) - except ValueError as e: - pass - raise GPXException('Invalid time: %s' % string) + m = RE_TIMESTAMP.match(string) + if m: + dt = [int(m.group(i)) for i in range(1, 7)] + if m.group(7): + f = m.group(7)[1:7] + dt.append(int(f + "0" * (6 - len(f)))) + else: + dt.append(0) + dt.append(SimpleTZ(m.group(8))) + return mod_datetime.datetime(*dt) + raise mod_gpx.GPXException('Invalid time: {0}'.format(string)) + + +def format_time(time): + offset = time.utcoffset() + if not offset or offset == 0: + tz = 'Z' + else: + tz = time.strftime('%z') + if time.microsecond: + ms = time.strftime('.%f') + else: + ms = '' + return ''.join((time.strftime('%Y-%m-%dT%H:%M:%S'), ms, tz)) + # ---------------------------------------------------------------------------------------------------- @@ -50,33 +110,24 @@ def parse_time(string): class FloatConverter: def __init__(self): self.from_string = lambda string : None if string is None else float(string.strip()) - self.to_string = lambda flt : str(flt) + self.to_string = lambda flt : mod_utils.make_str(flt) class IntConverter: def __init__(self): - self.from_string = lambda string : None if string is None else int(string.strip()) - self.to_string = lambda flt : str(flt) + self.from_string = lambda string: None if string is None else int(string.strip()) + self.to_string = lambda flt: str(flt) class TimeConverter: def from_string(self, string): - from . import gpx as mod_gpx - if not string: + try: + return parse_time(string) + except: return None - if 'T' in string: - string = string.replace('T', ' ') - if 'Z' in string: - string = string.replace('Z', '') - for date_format in mod_gpx.DATE_FORMATS: - try: - return mod_datetime.datetime.strptime(string, date_format) - except ValueError as e: - pass - return None + def to_string(self, time): - from . import gpx as mod_gpx - return time.strftime(mod_gpx.DATE_FORMAT) if time else None + return format_time(time) if time else None INT_TYPE = IntConverter() @@ -95,10 +146,10 @@ class AbstractGPXField: self.is_list = is_list self.attribute = False - def from_xml(self, parser, node, version): + def from_xml(self, node, version): raise Exception('Not implemented') - def to_xml(self, value, version): + def to_xml(self, value, version, nsmap): raise Exception('Not implemented') @@ -106,11 +157,13 @@ class GPXField(AbstractGPXField): """ Used for to (de)serialize fields with simple field<->xml_tag mapping. """ - def __init__(self, name, tag=None, attribute=None, type=None, possible=None, mandatory=None): + def __init__(self, name, tag=None, attribute=None, type=None, + possible=None, mandatory=None): AbstractGPXField.__init__(self) self.name = name if tag and attribute: - raise GPXException('Only tag *or* attribute may be given!') + from . import gpx as mod_gpx + raise mod_gpx.GPXException('Only tag *or* attribute may be given!') if attribute: self.tag = None self.attribute = name if attribute is True else attribute @@ -124,17 +177,20 @@ class GPXField(AbstractGPXField): self.possible = possible self.mandatory = mandatory - def from_xml(self, parser, node, version): + def from_xml(self, node, version): if self.attribute: - result = parser.get_node_attribute(node, self.attribute) + if node is not None: + result = node.get(self.attribute) else: - __node = parser.get_first_child(node, self.tag) - result = parser.get_node_data(__node) - + __node = node.find(self.tag) + if __node is not None: + result = __node.text + else: + result = None if result is None: if self.mandatory: from . import gpx as mod_gpx - raise mod_gpx.GPXException('%s is mandatory in %s' % (self.name, self.tag)) + raise mod_gpx.GPXException('{0} is mandatory in {1} (got {2})'.format(self.name, self.tag, result)) return None if self.type_converter: @@ -142,27 +198,26 @@ class GPXField(AbstractGPXField): result = self.type_converter.from_string(result) except Exception as e: from . import gpx as mod_gpx - raise mod_gpx.GPXException('Invalid value for <%s>... %s (%s)' % (self.tag, result, e)) + raise mod_gpx.GPXException('Invalid value for <{0}>... {1} ({2})'.format(self.tag, result, e)) if self.possible: if not (result in self.possible): from . import gpx as mod_gpx - raise mod_gpx.GPXException('Invalid value "%s", possible: %s' % (result, self.possible)) + raise mod_gpx.GPXException('Invalid value "{0}", possible: {1}'.format(result, self.possible)) return result - def to_xml(self, value, version): - if not value: + def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): + if value is None: return '' - + if not prettyprint: + indent = '' if self.attribute: - return '%s="%s"' % (self.attribute, mod_utils.make_str(value)) - else: - if self.type_converter: - value = self.type_converter.to_string(value) - if isinstance(self.tag, list) or isinstance(self.tag, tuple): - raise Exception('Not yet implemented') - return mod_utils.to_xml(self.tag, content=value, escape=True) + return '{0}="{1}"'.format(self.attribute, mod_utils.make_str(value)) + elif self.type_converter: + value = self.type_converter.to_string(value) + return mod_utils.to_xml(self.tag, content=value, escape=True, + prettyprint=prettyprint, indent=indent) class GPXComplexField(AbstractGPXField): @@ -172,27 +227,34 @@ class GPXComplexField(AbstractGPXField): self.tag = tag or name self.classs = classs - def from_xml(self, parser, node, version): + def from_xml(self, node, version): if self.is_list: result = [] - for child_node in parser.get_children(node): - if parser.get_node_name(child_node) == self.tag: - result.append(gpx_fields_from_xml(self.classs, parser, child_node, version)) + for child in node: + if child.tag == self.tag: + result.append(gpx_fields_from_xml(self.classs, child, + version)) return result else: - field_node = parser.get_first_child(node, self.tag) + field_node = node.find(self.tag) if field_node is None: return None - return gpx_fields_from_xml(self.classs, parser, field_node, version) + return gpx_fields_from_xml(self.classs, field_node, version) - def to_xml(self, value, version): + def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): + if not prettyprint: + indent = '' if self.is_list: - result = '' + result = [] for obj in value: - result += gpx_fields_to_xml(obj, self.tag, version) - return result + result.append(gpx_fields_to_xml(obj, self.tag, version, + nsmap=nsmap, + prettyprint=prettyprint, + indent=indent)) + return ''.join(result) else: - return gpx_fields_to_xml(value, self.tag, version) + return gpx_fields_to_xml(value, self.tag, version, + prettyprint=prettyprint, indent=indent) class GPXEmailField(AbstractGPXField): @@ -200,26 +262,47 @@ class GPXEmailField(AbstractGPXField): Converts GPX1.1 email tag group from/to string. """ def __init__(self, name, tag=None): - self.attribute = False - self.is_list = False + AbstractGPXField.__init__(self, is_list=False) self.name = name self.tag = tag or name - def from_xml(self, parser, node, version): - email_node = parser.get_first_child(node, self.tag) + def from_xml(self, node, version): + """ + Extract email address. + Args: + node: ETree node with child node containing self.tag + version: str of the gpx output version "1.0" or "1.1" + + Returns: + A string containing the email address. + """ + email_node = node.find(self.tag) if email_node is None: - return None + return '' - email_id = parser.get_node_attribute(email_node, 'id') - email_domain = parser.get_node_attribute(email_node, 'domain') + email_id = email_node.get('id') + email_domain = email_node.get('domain') + return '{0}@{1}'.format(email_id, email_domain) - return '%s@%s' % (email_id, email_domain) + def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): + """ + Write email address to XML - def to_xml(self, value, version): + Args: + value: str representing an email address + version: str of the gpx output version "1.0" or "1.1" + + Returns: + None if value is empty or str of XML representation of the + address. Representation starts with a \n. + """ if not value: return '' + if not prettyprint: + indent = '' + if '@' in value: pos = value.find('@') email_id = value[:pos] @@ -228,100 +311,252 @@ class GPXEmailField(AbstractGPXField): email_id = value email_domain = 'unknown' - return '\n<%s id="%s" domain="%s" />' % (self.tag, email_id, email_domain) + return ('\n' + indent + + '<{0} id="{1}" domain="{2}" />'.format(self.tag, + email_id, email_domain)) class GPXExtensionsField(AbstractGPXField): """ GPX1.1 extensions ... key-value type. """ - def __init__(self, name, tag=None): - self.attribute = False + def __init__(self, name, tag=None, is_list=True): + AbstractGPXField.__init__(self, is_list=is_list) self.name = name - self.is_list = False self.tag = tag or 'extensions' - def from_xml(self, parser, node, version): - result = {} + def from_xml(self, node, version): + """ + Build a list of extension Elements. - if node is None: - return result - - extensions_node = parser.get_first_child(node, self.tag) + Args: + node: Element at the root of the extensions + version: unused, only 1.1 supports extensions + Returns: + a list of Element objects + """ + result = [] + extensions_node = node.find(self.tag) if extensions_node is None: return result - - children = parser.get_children(extensions_node) - if children is None: - return result - - for child in children: - result[parser.get_node_name(child)] = parser.get_node_data(child) - + for child in extensions_node: + result.append(mod_copy.deepcopy(child)) return result - def to_xml(self, value, version): - if value is None or not value: + def _resolve_prefix(self, qname, nsmap): + """ + Convert a tag from Clark notation into prefix notation. + + Convert a tag from Clark notation using the nsmap into a + prefixed tag. If the tag isn't in Clark notation, return the + qname back. Converts {namespace}tag -> prefix:tag + + Args: + qname: string with the fully qualified name in Clark notation + nsmap: a dict of prefix, namespace pairs + + Returns: + string of the tag ready to be serialized. + """ + if nsmap is not None and '}' in qname: + uri, _, localname = qname.partition("}") + uri = uri.lstrip("{") + qname = uri + ':' + localname + for prefix, namespace in nsmap.items(): + if uri == namespace: + qname = prefix + ':' + localname + break + return qname + + def _ETree_to_xml(self, node, nsmap=None, prettyprint=True, indent=''): + """ + Serialize ETree element and all subelements. + + Creates a string of the ETree and all children. The prefixes are + resolved through the nsmap for easier to read XML. + + Args: + node: ETree with the extension data + version: string of GPX version, must be 1.1 + nsmap: dict of prefixes and URIs + prettyprint: boolean, when true, indent line + indent: string prepended to tag, usually 2 spaces per level + + Returns: + string with all the prefixed tags and data for the node + and its children as XML. + + """ + if not prettyprint: + indent = '' + + # Build element tag and text + result = [] + prefixedname = self._resolve_prefix(node.tag, nsmap) + result.append('\n' + indent + '<' + prefixedname) + for attrib, value in node.attrib.items(): + attrib = self._resolve_prefix(attrib, nsmap) + result.append(' {0}="{1}"'.format(attrib, value)) + result.append('>') + if node.text is not None: + result.append(node.text.strip()) + + + # Build subelement nodes + for child in node: + result.append(self._ETree_to_xml(child, nsmap, + prettyprint=prettyprint, + indent=indent+' ')) + + # Add tail and close tag + tail = node.tail + if tail is not None: + tail = tail.strip() + else: + tail = '' + if len(node) > 0: + result.append('\n' + indent) + result.append('' + tail) + + return ''.join(result) + + def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''): + """ + Serialize list of ETree. + + Creates a string of all the ETrees in the list. The prefixes are + resolved through the nsmap for easier to read XML. + + Args: + value: list of ETrees with the extension data + version: string of GPX version, must be 1.1 + nsmap: dict of prefixes and URIs + prettyprint: boolean, when true, indent line + indent: string prepended to tag, usually 2 spaces per level + + Returns: + string with all the prefixed tags and data for each node + as XML. + + """ + if not prettyprint: + indent = '' + if not value or version != "1.1": return '' - - result = '\n<' + self.tag + '>' - for ext_key, ext_value in value.items(): - result += mod_utils.to_xml(ext_key, content=ext_value) - result += '' - - return result - + result = [] + result.append('\n' + indent + '<' + self.tag + '>') + for extension in value: + result.append(self._ETree_to_xml(extension, nsmap, + prettyprint=prettyprint, + indent=indent+' ')) + result.append('\n' + indent + '') + return ''.join(result) # ---------------------------------------------------------------------------------------------------- # Utility methods: # ---------------------------------------------------------------------------------------------------- +def _check_dependents(gpx_object, fieldname): + """ + Check for data in subelements. -def gpx_fields_to_xml(instance, tag, version, custom_attributes=None): + Fieldname takes the form of 'tag:dep1:dep2:dep3' for an arbitrary + number of dependents. If all the gpx_object.dep attributes are + empty, return a sentinel value to suppress serialization of all + subelements. + + Args: + gpx_object: GPXField object to check for data + fieldname: string with tag and dependents delimited with ':' + + Returns: + Two strings. The first is a sentinel value, '/' + tag, if all + the subelements are empty and an empty string otherwise. The + second is the bare tag name. + """ + if ':' in fieldname: + children = fieldname.split(':') + field = children.pop(0) + for child in children: + if getattr(gpx_object, child.lstrip('@')): + return '', field # Child has data + return '/' + field, field # No child has data + return '', fieldname # No children + +def gpx_fields_to_xml(instance, tag, version, custom_attributes=None, + nsmap=None, prettyprint=True, indent=''): + if not prettyprint: + indent = '' fields = instance.gpx_10_fields if version == '1.1': fields = instance.gpx_11_fields tag_open = bool(tag) - body = '' + body = [] if tag: - body = '\n<' + tag + body.append('\n' + indent + '<' + tag) + if tag == 'gpx': # write nsmap in root node + body.append(' xmlns="{0}"'.format(nsmap['defaultns'])) + namespaces = set(nsmap.keys()) + namespaces.remove('defaultns') + for prefix in sorted(namespaces): + body.append( + ' xmlns:{0}="{1}"'.format(prefix, nsmap[prefix]) + ) if custom_attributes: - for key, value in custom_attributes.items(): - body += ' %s="%s"' % (key, mod_utils.make_str(value)) - + # Make sure to_xml() always return attributes in the same order: + for key in sorted(custom_attributes.keys()): + body.append(' {0}="{1}"'.format(key, mod_utils.make_str(custom_attributes[key]))) + suppressuntil = '' for gpx_field in fields: + # strings indicate non-data container tags with subelements if isinstance(gpx_field, str): - if tag_open: - body += '>' - tag_open = False - if gpx_field[0] == '/': - body += '<%s>' % gpx_field + # Suppress empty tags + if suppressuntil: + if suppressuntil == gpx_field: + suppressuntil = '' else: - body += '\n<%s' % gpx_field - tag_open = True - else: + suppressuntil, gpx_field = _check_dependents(instance, + gpx_field) + if not suppressuntil: + if tag_open: + body.append('>') + tag_open = False + if gpx_field[0] == '/': + body.append('\n' + indent + '<{0}>'.format(gpx_field)) + if prettyprint and len(indent) > 1: + indent = indent[:-2] + else: + if prettyprint: + indent += ' ' + body.append('\n' + indent + '<{0}'.format(gpx_field)) + tag_open = True + elif not suppressuntil: value = getattr(instance, gpx_field.name) if gpx_field.attribute: - body += ' ' + gpx_field.to_xml(value, version) - elif value: + body.append(' ' + gpx_field.to_xml(value, version, nsmap, + prettyprint=prettyprint, + indent=indent + ' ')) + elif value is not None: if tag_open: - body += '>' + body.append('>') tag_open = False - xml_value = gpx_field.to_xml(value, version) + xml_value = gpx_field.to_xml(value, version, nsmap, + prettyprint=prettyprint, + indent=indent + ' ') if xml_value: - body += xml_value + body.append(xml_value) if tag: if tag_open: - body += '>' - body += '' + body.append('>') + body.append('\n' + indent + '') - return body + return ''.join(body) -def gpx_fields_from_xml(class_or_instance, parser, node, version): +def gpx_fields_from_xml(class_or_instance, node, version): if mod_inspect.isclass(class_or_instance): result = class_or_instance() else: @@ -331,34 +566,33 @@ def gpx_fields_from_xml(class_or_instance, parser, node, version): if version == '1.1': fields = result.gpx_11_fields - node_path = [ node ] + node_path = [node] for gpx_field in fields: current_node = node_path[-1] - if isinstance (gpx_field, str): + if isinstance(gpx_field, str): + gpx_field = gpx_field.partition(':')[0] if gpx_field.startswith('/'): node_path.pop() else: if current_node is None: node_path.append(None) else: - node_path.append(parser.get_first_child(current_node, gpx_field)) + node_path.append(current_node.find(gpx_field)) else: if current_node is not None: - value = gpx_field.from_xml(parser, current_node, version) + value = gpx_field.from_xml(current_node, version) setattr(result, gpx_field.name, value) elif gpx_field.attribute: - value = gpx_field.from_xml(parser, node, version) + value = gpx_field.from_xml(node, version) setattr(result, gpx_field.name, value) return result - def gpx_check_slots_and_default_values(classs): """ Will fill the default values for this class. Instances will inherit those values so we don't need to fill default values for every instance. - This method will also fill the attribute gpx_field_names with a list of gpx field names. This can be used """ @@ -400,7 +634,7 @@ def gpx_check_slots_and_default_values(classs): gpx_field_names.append(field.name) gpx_field_names = tuple(gpx_field_names) - if not hasattr(classs, '__slots__') or not classs.__slots__ or classs.__slots__ != gpx_field_names: - try: slots = classs.__slots__ - except Exception as e: slots = '[Unknown:%s]' % e - raise Exception('%s __slots__ invalid, found %s, but should be %s' % (classs, slots, gpx_field_names)) +## if not hasattr(classs, '__slots__') or not classs.__slots__ or classs.__slots__ != gpx_field_names: +## try: slots = classs.__slots__ +## except Exception as e: slots = '[Unknown:%s]' % e +## raise Exception('%s __slots__ invalid, found %s, but should be %s' % (classs, slots, gpx_field_names)) diff --git a/InternalPythonModules/GPX_Module/gpxpy/gpxxml.py b/InternalPythonModules/GPX_Module/gpxpy/gpxxml.py index dd0012748e..2794fb8d75 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/gpxxml.py +++ b/InternalPythonModules/GPX_Module/gpxpy/gpxxml.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import xml.dom.minidom as mod_minidom -import gpxpy as mod_gpxpy def split_gpxs(xml): """ diff --git a/InternalPythonModules/GPX_Module/gpxpy/parser.py b/InternalPythonModules/GPX_Module/gpxpy/parser.py index 98c3abec9f..72730dccc4 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/parser.py +++ b/InternalPythonModules/GPX_Module/gpxpy/parser.py @@ -14,208 +14,142 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import print_function - -import pdb - -import re as mod_re import logging as mod_logging -import datetime as mod_datetime -import xml.dom.minidom as mod_minidom +import re as mod_re try: - import lxml.etree as mod_etree -except: - mod_etree = None - pass # LXML not available + import lxml.etree as mod_etree # Load LXML or fallback to cET or ET +except ImportError: + try: + import xml.etree.cElementTree as mod_etree + except ImportError: + import xml.etree.ElementTree as mod_etree from . import gpx as mod_gpx from . import utils as mod_utils from . import gpxfield as mod_gpxfield - -class XMLParser: - """ - Used when lxml is not available. Uses standard minidom. - """ - - def __init__(self, xml): - self.xml = xml - self.dom = mod_minidom.parseString(xml) - - def get_first_child(self, node=None, name=None): - # TODO: Remove find_first_node from utils! - if not node: - node = self.dom - - children = node.childNodes - if not children: - return None - - if not name: - return children[0] - - for tmp_node in children: - if tmp_node.nodeName == name: - return tmp_node - - return None - - def get_node_name(self, node): - if not node: - return None - return node.nodeName - - def get_children(self, node=None): - if not node: - node = self.dom - - return list(filter(lambda node : node.nodeType == node.ELEMENT_NODE, node.childNodes)) - - def get_node_data(self, node): - if node is None: - return None - - child_nodes = node.childNodes - if not child_nodes or len(child_nodes) == 0: - return None - - return child_nodes[0].nodeValue - - def get_node_attribute(self, node, attribute): - if (not hasattr(node, 'attributes')) or (not node.attributes): - return None - if attribute in node.attributes.keys(): - return node.attributes[attribute].nodeValue - return None - - -class LXMLParser: - """ - Used when lxml is available. - """ - - def __init__(self, xml): - if not mod_etree: - raise Exception('Cannot use LXMLParser without lxml installed') - - if mod_utils.PYTHON_VERSION[0] == '3': - # In python 3 all strings are unicode and for some reason lxml - # don't like unicode strings with XMLs declared as UTF-8: - self.xml = xml.encode('utf-8') - else: - self.xml = xml - - self.dom = mod_etree.XML(self.xml) - # get the namespace - self.ns = self.dom.nsmap.get(None) - - def get_first_child(self, node=None, name=None): - if node is None: - if name: - if self.get_node_name(self.dom) == name: - return self.dom - return self.dom - - children = node.getchildren() - - if not children: - return None - - if name: - for node in children: - if self.get_node_name(node) == name: - return node - return None - - return children[0] - - def get_node_name(self, node): - if callable(node.tag): - tag = str(node.tag()) - else: - tag = str(node.tag) - if '}' in tag: - return tag.split('}')[1] - return tag - - def get_children(self, node=None): - if node is None: - node = self.dom - return node.getchildren() - - def get_node_data(self, node): - if node is None: - return None - - return node.text - - def get_node_attribute(self, node, attribute): - if node is None: - return None - return node.attrib.get(attribute) - +log = mod_logging.getLogger(__name__) class GPXParser: - def __init__(self, xml_or_file=None, parser=None): + """ + Parse the XML and provide new GPX instance. + + Methods: + __init__: initialize new instance + init: format XML + parse: parse XML, build tree, build GPX + + Attributes: + gpx: GPX instance of the most recently parsed XML + xml: string containing the XML text + + """ + + def __init__(self, xml_or_file=None): """ - Parser may be lxml of minidom. If you set to None then lxml will be used if installed - otherwise minidom. + Initialize new GPXParser instance. + + Arguments: + xml_or_file: string or file object containing the gpx + formatted xml + """ self.init(xml_or_file) self.gpx = mod_gpx.GPX() - self.xml_parser_type = parser - self.xml_parser = None def init(self, xml_or_file): - text = xml_or_file.read() if hasattr(xml_or_file, 'read') else xml_or_file + """ + Store the XML and remove utf-8 Byte Order Mark if present. + + Args: + xml_or_file: string or file object containing the gpx + formatted xml + + """ + text = xml_or_file.read() if hasattr(xml_or_file, 'read') else xml_or_file self.xml = mod_utils.make_str(text) - self.gpx = mod_gpx.GPX() - def parse(self): + def parse(self, version=None): """ - Parses the XML file and returns a GPX object. + Parse the XML and return a GPX object. + + Args: + version: str or None indicating the GPX Schema to use. + Options are '1.0', '1.1' and None. When version is None + the version is read from the file or falls back on 1.0. + + Returns: + A GPX object loaded from the xml + + Raises: + GPXXMLSyntaxException: XML file is invalid + GPXException: XML is valid but GPX data contains errors - It will throw GPXXMLSyntaxException if the XML file is invalid or - GPXException if the XML file is valid but something is wrong with the - GPX data. """ - try: - if self.xml_parser_type is None: - if mod_etree: - self.xml_parser = LXMLParser(self.xml) - else: - self.xml_parser = XMLParser(self.xml) - elif self.xml_parser_type == 'lxml': - self.xml_parser = LXMLParser(self.xml) - elif self.xml_parser_type == 'minidom': - self.xml_parser = XMLParser(self.xml) + # Build prefix map for reserialization and extension handlings + for namespace in mod_re.findall(r'\sxmlns:?[^=]*="[^"]+"', self.xml): + prefix, _, URI = namespace[6:].partition('=') + prefix = prefix.lstrip(':') + if prefix == '': + prefix = 'defaultns' # alias default for easier handling else: - raise mod_gpx.GPXException('Invalid parser type: %s' % self.xml_parser_type) + if prefix.startswith("ns"): + mod_etree.register_namespace("noglobal_" + prefix, URI.strip('"')) + else: + mod_etree.register_namespace(prefix, URI.strip('"')) + self.gpx.nsmap[prefix] = URI.strip('"') - self.__parse_dom() + schema_loc = mod_re.search(r'\sxsi:schemaLocation="[^"]+"', self.xml) + if schema_loc: + _, _, value = schema_loc.group(0).partition('=') + self.gpx.schema_locations = value.strip('"').split() + + # Remove default namespace to simplify processing later + self.xml = mod_re.sub(r"""\sxmlns=(['"])[^'"]+\1""", '', self.xml, count=1) + + # Build tree + try: + if GPXParser.__library() == "LXML": + # lxml does not like unicode strings when it's expecting + # UTF-8. Also, XML comments result in a callable .tag(). + # Strip them out to avoid handling them later. + if mod_utils.PYTHON_VERSION[0] >= '3': + self.xml = self.xml.encode('utf-8') + root = mod_etree.XML(self.xml, + mod_etree.XMLParser(remove_comments=True)) + else: + root = mod_etree.XML(self.xml) - return self.gpx except Exception as e: - # The exception here can be a lxml or minidom exception. - mod_logging.debug('Error in:\n%s\n-----------\n' % self.xml) - mod_logging.exception(e) + # The exception here can be a lxml or ElementTree exception. + log.debug('Error in:\n%s\n-----------\n' % self.xml, exc_info=True) # The library should work in the same way regardless of the # underlying XML parser that's why the exception thrown # here is GPXXMLSyntaxException (instead of simply throwing the - # original minidom or lxml exception e). + # original ElementTree or lxml exception e). # - # But, if the user need the original exception (lxml or minidom) + # But, if the user needs the original exception (lxml or ElementTree) # it is available with GPXXMLSyntaxException.original_exception: raise mod_gpx.GPXXMLSyntaxException('Error parsing XML: %s' % str(e), e) - def __parse_dom(self): - node = self.xml_parser.get_first_child(name='gpx') - - if node is None: + if root is None: raise mod_gpx.GPXException('Document must have a `gpx` root node.') - version = self.xml_parser.get_node_attribute(node, 'version') + if version is None: + version = root.get('version') - mod_gpxfield.gpx_fields_from_xml(self.gpx, self.xml_parser, node, version) + mod_gpxfield.gpx_fields_from_xml(self.gpx, root, version) + return self.gpx + + @staticmethod + def __library(): + """ + Return the underlying ETree. + + Provided for convenient unittests. + """ + if "lxml" in str(mod_etree): + return "LXML" + return "STDLIB" diff --git a/InternalPythonModules/GPX_Module/gpxpy/utils.py b/InternalPythonModules/GPX_Module/gpxpy/utils.py index b2221a10db..13a900a901 100644 --- a/InternalPythonModules/GPX_Module/gpxpy/utils.py +++ b/InternalPythonModules/GPX_Module/gpxpy/utils.py @@ -21,26 +21,29 @@ import xml.sax.saxutils as mod_saxutils PYTHON_VERSION = mod_sys.version.split(' ')[0] -def to_xml(tag, attributes=None, content=None, default=None, escape=False): +def to_xml(tag, attributes=None, content=None, default=None, escape=False, prettyprint=True, indent=''): + if not prettyprint: + indent = '' attributes = attributes or {} - result = '\n<%s' % tag + result = [] + result.append('\n' + indent + '<{0}'.format(tag)) if content is None and default: content = default if attributes: for attribute in attributes.keys(): - result += make_str(' %s="%s"' % (attribute, attributes[attribute])) + result.append(make_str(' %s="%s"' % (attribute, attributes[attribute]))) if content is None: - result += '/>' + result.append('/>') else: if escape: - result += make_str('>%s' % (mod_saxutils.escape(content), tag)) + result.append(make_str('>%s' % (mod_saxutils.escape(content), tag))) else: - result += make_str('>%s' % (content, tag)) + result.append(make_str('>%s' % (content, tag))) - result = make_str(result) + result = make_str(''.join(result)) return result @@ -69,47 +72,20 @@ def to_number(s, default=0, nan_value=None): def total_seconds(timedelta): - """ Some versions of python dont have timedelta.total_seconds() method. """ + """ Some versions of python don't have the timedelta.total_seconds() method. """ if timedelta is None: return None return (timedelta.days * 86400) + timedelta.seconds -# Hash utilities: - - -def __hash(obj): - result = 0 - - if obj is None: - return result - elif isinstance(obj, dict): - raise RuntimeError('__hash_single_object for dict not yet implemented') - elif isinstance(obj, list) or isinstance(obj, tuple): - return hash_list_or_tuple(obj) - - return hash(obj) - - -def hash_list_or_tuple(iteration): - result = 17 - - for obj in iteration: - result = result * 31 + __hash(obj) - - return result - - -def hash_object(obj, attributes): - result = 19 - - for attribute in attributes: - result = result * 31 + __hash(getattr(obj, attribute)) - - return result - def make_str(s): - """ Convert a str or unicode object into a str type. """ + """ Convert a str or unicode or float object into a str type. """ + if isinstance(s, float): + result = str(s) + if not 'e' in result: + return result + # scientific notation is illegal in GPX 1/1 + return format(s, '.10f').rstrip('0.') if PYTHON_VERSION[0] == '2': if isinstance(s, unicode): return s.encode("utf-8")