Merge pull request #5627 from markmckinnon/5966-open-source-GPX-module-and-update-how-it-makes-artifacts

5966-open-source-GPX-module-and-update-how-it-makes-artifacts
This commit is contained in:
Richard Cordovano
2020-02-11 18:04:49 -05:00
committed by GitHub
9 changed files with 4446 additions and 0 deletions
@@ -0,0 +1,283 @@
"""
Autopsy Forensic Browser
Copyright 2019-2020 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import os
import jarray
import inspect
import time
import calendar
from datetime import datetime
from java.lang import System
from java.util.logging import Level
from java.io import File
from java.util import ArrayList
from org.sleuthkit.datamodel import SleuthkitCase
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import ReadContentInputStream
from org.sleuthkit.datamodel import Blackboard
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.blackboardutils import GeoArtifactsHelper
from org.sleuthkit.datamodel.blackboardutils.attributes import GeoWaypoint
from org.sleuthkit.datamodel.blackboardutils.attributes import GeoTrackPoints
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestModule
from org.sleuthkit.autopsy.ingest.IngestModule import IngestModuleException
from org.sleuthkit.autopsy.ingest import DataSourceIngestModule
from org.sleuthkit.autopsy.ingest import FileIngestModule
from org.sleuthkit.autopsy.ingest import IngestModuleFactoryAdapter
from org.sleuthkit.autopsy.ingest import IngestMessage
from org.sleuthkit.autopsy.ingest import IngestServices
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.casemodule.services import Services
from org.sleuthkit.autopsy.casemodule.services import FileManager
from org.sleuthkit.autopsy.ingest import ModuleDataEvent
# Based on gpxpy module: https://github.com/tkrajina/gpxpy
import gpxpy
import gpxpy.gpx
import gpxpy.parser
# Factory that defines the name and details of the module and allows Autopsy
# to create instances of the modules that will do the analysis.
class GPXParserDataSourceIngestModuleFactory(IngestModuleFactoryAdapter):
moduleName = "GPX Parser Module"
# True - Verbose debugging messages sent to log file.
# False - Verbose debugging turned off.
debuglevel = False
def getModuleDisplayName(self):
return self.moduleName
# TODO: Give it a description
def getModuleDescription(self):
return "Module that extracts GEO data from GPX files."
def getModuleVersionNumber(self):
return "1.1"
def isDataSourceIngestModuleFactory(self):
return True
def createDataSourceIngestModule(self, ingestOptions):
return GPXParserDataSourceIngestModule()
# Data Source-level ingest module. One gets created per data source.
class GPXParserDataSourceIngestModule(DataSourceIngestModule):
_logger = Logger.getLogger(GPXParserDataSourceIngestModuleFactory.moduleName)
def log(self, level, msg):
self._logger.logp(level, self.__class__.__name__, inspect.stack()[1][3], msg)
def __init__(self):
self.context = None
# Where any setup and configuration is done.
def startUp(self, context):
self.context = context
# Where the analysis is done.
def process(self, dataSource, progressBar):
# We don't know how much work there is yet.
progressBar.switchToIndeterminate()
# This will work in 4.0.1 and beyond.
# Use blackboard class to index blackboard artifacts for keyword search.
blackboard = Case.getCurrentCase().getServices().getBlackboard()
# Get the sleuthkitcase
skCase = Case.getCurrentCase().getSleuthkitCase()
# In the name and then count and read them.
fileManager = Case.getCurrentCase().getServices().getFileManager()
files = fileManager.findFiles(dataSource, "%.gpx")
# TODO: Would like to change this to find files based on mimetype rather than extension.
#files = findFiles(dataSource, "text/xml")
#if (file.isMimeType('text/xml') == False):
numFiles = len(files)
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "found " + str(numFiles) + " files")
progressBar.switchToDeterminate(numFiles)
fileCount = 0;
# Get module name for adding attributes
moduleName = GPXParserDataSourceIngestModuleFactory.moduleName
for file in files:
# Get the GeoArtifactsHelper
geoArtifactHelper = GeoArtifactsHelper(skCase, moduleName, file)
# Check if the user pressed cancel while we were busy.
if self.context.isJobCancelled():
return IngestModule.ProcessResult.OK
#self.log(Level.INFO, "GPX: Processing file: " + file.getName())
fileCount += 1
# Check if module folder is present. If not, create it.
dirName = os.path.join(Case.getCurrentCase().getTempDirectory(), "GPX_Parser_Module")
try:
os.stat(dirName)
except:
os.mkdir(dirName)
fileName = os.path.join(dirName, "tmp.gpx")
# Check to see if temporary file exists. If it does, remove it.
if os.path.exists(fileName):
try:
os.remove(fileName)
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX:\t" + "FILE DELETED " + fileName )
except:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX:\t" + "FILE NOT DELETED " + fileName)
# This writes the file to the local file system.
localFile = File(fileName)
ContentUtils.writeToFile(file, localFile)
# Send to gpxpy for parsing.
gpxfile = open(fileName)
try:
gpx = gpxpy.parse(gpxfile)
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX:\t" + "FILE PARSED")
except:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX:\t" + file.getName() + " - FILE NOT PARSED")
continue
if gpx:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX: TRACKS")
for track in gpx.tracks:
for segment in track.segments:
geoPointList = ArrayList()
for point in segment.points:
elevation = 0
if point.elevation != None:
elevation = point.elevation
dateTime = 0
try:
if (point.time != None):
datetime = long(time.mktime(point.time.timetuple()))
except:
pass
geoPointList.add(GeoWaypoint.GeoTrackPoint(point.latitude, point.longitude, elevation, 0, 0, 0, dateTime))
try:
# Add the trackpoint using the helper class
geoartifact = geoArtifactHelper.addTrack("Trackpoint", geoPointList)
except Blackboard.BlackboardException as e:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX: Error using geo artifact helper with blackboard " )
except TskCoreException as e:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX: Error using geo artifact helper tskcoreexception" )
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX: WAYPOINTS")
for waypoint in gpx.waypoints:
attributes = ArrayList()
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK)
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID(), moduleName, waypoint.latitude))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE.getTypeID(), moduleName, waypoint.longitude))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FLAG.getTypeID(), moduleName, "Waypoint"))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getTypeID(), moduleName, waypoint.name))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME.getTypeID(), moduleName, "GPXParser"))
art.addAttributes(attributes)
try:
# Post the artifact to blackboard
skCase.getBlackboard().postArtifact(art, moduleName)
except Blackboard.BlackboardException as e:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX: Error using geo artifact helper with blackboard for waypoints" )
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX: ROUTES")
for route in gpx.routes:
firstTimeThru = 0
startingPoint = list()
endingPoint = list()
for point in route.points:
# If first time in loop only populate starting point
if (firstTimeThru == 0):
startingPoint.append((point.latitude, point.longitude))
firstTimeThru = 1
else:
startingPoint.append((point.latitude, point.longitude))
endingPoint.append((point.latitude, point.longitude))
if (len(endingPoint) > 0):
# get length of ending point as this ensures that we have equal points to process.
for i in range(0,len(endingPoint) -1):
attributes = ArrayList()
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE)
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID(), moduleName, startingPoint[i][0]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START.getTypeID(), moduleName, startingPoint[i][1]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END.getTypeID(), moduleName, endingPoint[i][0]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END.getTypeID(), moduleName, endingPoint[i][1]))
art.addAttributes(attributes)
try:
# Post the artifact to blackboard
skCase.getBlackboard().postArtifact(art, moduleName)
except Blackboard.BlackboardException as e:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX: Error using geo artifact helper with blackboard for waypoints" )
else:
if (len(startingPoint) > 0):
attributes = ArrayList()
art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE)
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID(), moduleName, startingPoint[0][0]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START.getTypeID(), moduleName, startingPoint[0][1]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END.getTypeID(), moduleName, startingPoint[0][0]))
attributes.add(BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END.getTypeID(), moduleName, startingPoint[0][1]))
art.addAttributes(attributes)
try:
# Post the artifact to blackboard
skCase.getBlackboard().postArtifact(art, moduleName)
except Blackboard.BlackboardException as e:
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.SEVERE, "GPX: Error using geo artifact helper with blackboard for waypoints" )
# Update the progress bar.
progressBar.progress(fileCount)
if os.path.exists(fileName):
try:
os.remove(fileName)
if GPXParserDataSourceIngestModuleFactory.debuglevel: self.log(Level.INFO, "GPX:\t" + "FILE DELETED")
except:
self.log(Level.SEVERE, "GPX:\t" + "FILE NOT DELETED")
# Post a message to the ingest messages inbox.
message = IngestMessage.createMessage(IngestMessage.MessageType.DATA, "GPX Parser Data Source Ingest Module", "Found %d files" % fileCount)
IngestServices.getInstance().postMessage(message)
return IngestModule.ProcessResult.OK;
@@ -0,0 +1,13 @@
Metadata-Version: 1.1
Name: gpxpy
Version: 0.8.8
Summary: GPX file parser and GPS track manipulation library
Home-page: http://www.trackprofiler.com/gpxpy/index.html
Author: Tomo Krajina
Author-email: tkrajina@gmail.com
License: Apache License, Version 2.0
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Tomo Krajina
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__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.
parser may be 'lxml', 'minidom' or None (then it will be automatically
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 parser as mod_parser
parser = mod_parser.GPXParser(xml_or_file)
return parser.parse(version)
@@ -0,0 +1,390 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Tomo Krajina
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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)
# 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
# One degree in meters:
ONE_DEGREE = (2*mod_math.pi*EARTH_RADIUS) / 360 # ==> 111.319 km
def to_rad(x):
return x / 180. * mod_math.pi
def haversine_distance(latitude_1, longitude_1, latitude_2, longitude_2):
"""
Haversine distance between two points, expressed in meters.
Implemented from http://www.movable-type.co.uk/scripts/latlong.html
"""
d_lat = to_rad(latitude_1 - latitude_2)
d_lon = to_rad(longitude_1 - longitude_2)
lat1 = to_rad(latitude_1)
lat2 = to_rad(latitude_2)
a = mod_math.sin(d_lat/2) * mod_math.sin(d_lat/2) + \
mod_math.sin(d_lon/2) * mod_math.sin(d_lon/2) * mod_math.cos(lat1) * mod_math.cos(lat2)
c = 2 * mod_math.atan2(mod_math.sqrt(a), mod_math.sqrt(1-a))
d = EARTH_RADIUS * c
return d
def length(locations=None, _3d=None):
locations = locations or []
if not locations:
return 0
length = 0
for i in range(len(locations)):
if i > 0:
previous_location = locations[i - 1]
location = locations[i]
if _3d:
d = location.distance_3d(previous_location)
else:
d = location.distance_2d(previous_location)
if d:
length += d
return length
def length_2d(locations=None):
""" 2-dimensional length (meters) of locations (only latitude and longitude, no elevation). """
locations = locations or []
return length(locations, False)
def length_3d(locations=None):
""" 3-dimensional length (meters) of locations (it uses latitude, longitude, and elevation). """
locations = locations or []
return length(locations, True)
def calculate_max_speed(speeds_and_distances):
"""
Compute average distance and standard deviation for distance. Extremes
in distances are usually extremes in speeds, so we will ignore them,
here.
speeds_and_distances must be a list containing pairs of (speed, distance)
for every point in a track segment.
"""
assert speeds_and_distances
if len(speeds_and_distances) > 0:
assert len(speeds_and_distances[0]) == 2
# ...
assert len(speeds_and_distances[-1]) == 2
size = len(speeds_and_distances)
if size < 20:
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))/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)
# sort by speed:
speeds = list(map(lambda speed_and_distance: speed_and_distance[0], filtered_speeds_and_distances))
if not isinstance(speeds, list): # python3
speeds = list(speeds)
if not speeds:
return None
speeds.sort()
# Even here there may be some extremes => ignore the last 5%:
index = int(len(speeds) * 0.95)
if index >= len(speeds):
index = -1
return speeds[index]
def calculate_uphill_downhill(elevations):
if not elevations:
return 0, 0
size = len(elevations)
def __filter(n):
current_ele = elevations[n]
if current_ele is None:
return False
if 0 < n < size - 1:
previous_ele = elevations[n-1]
next_ele = elevations[n+1]
if previous_ele is not None and current_ele is not None and next_ele is not None:
return previous_ele*.3 + current_ele*.4 + next_ele*.3
return current_ele
smoothed_elevations = list(map(__filter, range(size)))
uphill, downhill = 0., 0.
for n, elevation in enumerate(smoothed_elevations):
if n > 0 and elevation is not None and smoothed_elevations is not None:
d = elevation - smoothed_elevations[n-1]
if d > 0:
uphill += d
else:
downhill -= d
return uphill, downhill
def distance(latitude_1, longitude_1, elevation_1, latitude_2, longitude_2, elevation_2,
haversine=None):
"""
Distance between two points. If elevation is None compute a 2d distance
if haversine==True -- haversine will be used for every computations,
otherwise...
Haversine distance will be used for distant points where elevation makes a
small difference, so it is ignored. That's because haversine is 5-6 times
slower than the dummy distance algorithm (which is OK for most GPS tracks).
"""
# If points too distant -- compute haversine distance:
if haversine or (abs(latitude_1 - latitude_2) > .2 or abs(longitude_1 - longitude_2) > .2):
return haversine_distance(latitude_1, longitude_1, latitude_2, longitude_2)
coef = mod_math.cos(latitude_1 / 180. * mod_math.pi)
x = latitude_1 - latitude_2
y = (longitude_1 - longitude_2) * coef
distance_2d = mod_math.sqrt(x * x + y * y) * ONE_DEGREE
if elevation_1 is None or elevation_2 is None or elevation_1 == elevation_2:
return distance_2d
return mod_math.sqrt(distance_2d ** 2 + (elevation_1 - elevation_2) ** 2)
def elevation_angle(location1, location2, radians=False):
""" Uphill/downhill angle between two locations. """
if location1.elevation is None or location2.elevation is None:
return None
b = float(location2.elevation - location1.elevation)
a = location2.distance_2d(location1)
if a == 0:
return 0
angle = mod_math.atan(b / a)
if radians:
return angle
return 180 * angle / mod_math.pi
def distance_from_line(point, line_point_1, line_point_2):
""" Distance of point from a line given with two points. """
assert point, point
assert line_point_1, line_point_1
assert line_point_2, line_point_2
a = line_point_1.distance_2d(line_point_2)
if a == 0:
return line_point_1.distance_2d(point)
b = line_point_1.distance_2d(point)
c = line_point_2.distance_2d(point)
s = (a + b + c) / 2.
return 2. * mod_math.sqrt(abs(s * (s - a) * (s - b) * (s - c))) / a
def get_line_equation_coefficients(location1, location2):
"""
Get line equation coefficients for:
latitude * a + longitude * b + c = 0
This is a normal cartesian line (not spherical!)
"""
if location1.longitude == location2.longitude:
# Vertical line:
return float(0), float(1), float(-location1.longitude)
else:
a = float(location1.latitude - location2.latitude) / (location1.longitude - location2.longitude)
b = location1.latitude - location1.longitude * a
return float(1), float(-a), float(-b)
def simplify_polyline(points, max_distance):
"""Does Ramer-Douglas-Peucker algorithm for simplification of polyline """
if len(points) < 3:
return points
begin, end = points[0], points[-1]
# Use a "normal" line just to detect the most distant point (not its real distance)
# this is because this is faster to compute than calling distance_from_line() for
# every point.
#
# This is an approximation and may have some errors near the poles and if
# the points are too distant, but it should be good enough for most use
# cases...
a, b, c = get_line_equation_coefficients(begin, end)
# 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:
tmp_max_distance = d
tmp_max_distance_position = point_no
# 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]
# 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:
""" Generic geographical location """
latitude = None
longitude = None
elevation = None
def __init__(self, latitude, longitude, elevation=None):
self.latitude = latitude
self.longitude = longitude
self.elevation = elevation
def has_elevation(self):
return self.elevation or self.elevation == 0
def remove_elevation(self):
self.elevation = None
def distance_2d(self, location):
if not location:
return None
return distance(self.latitude, self.longitude, None, location.latitude, location.longitude, None)
def distance_3d(self, location):
if not location:
return None
return distance(self.latitude, self.longitude, self.elevation, location.latitude, location.longitude, location.elevation)
def elevation_angle(self, location, radians=False):
return elevation_angle(self, location, radians)
def move(self, location_delta):
self.latitude, self.longitude = location_delta.move(self)
def __add__(self, location_delta):
latitude, longitude = location_delta.move(self)
return Location(latitude, longitude)
def __str__(self):
return '[loc:%s,%s@%s]' % (self.latitude, self.longitude, self.elevation)
def __repr__(self):
if self.elevation is None:
return 'Location(%s, %s)' % (self.latitude, self.longitude)
else:
return 'Location(%s, %s, %s)' % (self.latitude, self.longitude, self.elevation)
def __hash__(self):
return mod_utils.hash_object(self, ('latitude', 'longitude', 'elevation'))
class LocationDelta:
"""
Intended to use similar to timestamp.timedelta, but for Locations.
"""
NORTH = 0
EAST = 90
SOUTH = 180
WEST = 270
def __init__(self, distance=None, angle=None, latitude_diff=None, longitude_diff=None):
"""
Version 1:
Distance (in meters).
angle_from_north *clockwise*.
...must be given
Version 2:
latitude_diff and longitude_diff
...must be given
"""
if (distance is not None) and (angle is not None):
if (latitude_diff is not None) or (longitude_diff is not None):
raise Exception('No lat/lon diff if using distance and angle!')
self.distance = distance
self.angle_from_north = angle
self.move_function = self.move_by_angle_and_distance
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!')
self.latitude_diff = latitude_diff
self.longitude_diff = longitude_diff
self.move_function = self.move_by_lat_lon_diff
def move(self, location):
"""
Move location by this timedelta.
"""
return self.move_function(location)
def move_by_angle_and_distance(self, location):
coef = mod_math.cos(location.latitude / 180. * mod_math.pi)
vertical_distance_diff = mod_math.sin((90 - self.angle_from_north) / 180. * mod_math.pi) / ONE_DEGREE
horizontal_distance_diff = mod_math.cos((90 - self.angle_from_north) / 180. * mod_math.pi) / ONE_DEGREE
lat_diff = self.distance * vertical_distance_diff
lon_diff = self.distance * horizontal_distance_diff / coef
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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,640 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Tomo Krajina
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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
class GPXFieldTypeConverter:
def __init__(self, from_string, to_string):
self.from_string = from_string
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
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))
# ----------------------------------------------------------------------------------------------------
# Type converters used to convert from/to the string in the XML:
# ----------------------------------------------------------------------------------------------------
class FloatConverter:
def __init__(self):
self.from_string = lambda string : None if string is None else float(string.strip())
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)
class TimeConverter:
def from_string(self, string):
try:
return parse_time(string)
except:
return None
def to_string(self, time):
return format_time(time) if time else None
INT_TYPE = IntConverter()
FLOAT_TYPE = FloatConverter()
TIME_TYPE = TimeConverter()
# ----------------------------------------------------------------------------------------------------
# Field converters:
# ----------------------------------------------------------------------------------------------------
class AbstractGPXField:
def __init__(self, attribute_field=None, is_list=None):
self.attribute_field = attribute_field
self.is_list = is_list
self.attribute = False
def from_xml(self, node, version):
raise Exception('Not implemented')
def to_xml(self, value, version, nsmap):
raise Exception('Not implemented')
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):
AbstractGPXField.__init__(self)
self.name = name
if tag and attribute:
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
elif tag:
self.tag = name if tag is True else tag
self.attribute = None
else:
self.tag = name
self.attribute = None
self.type_converter = type
self.possible = possible
self.mandatory = mandatory
def from_xml(self, node, version):
if self.attribute:
if node is not None:
result = node.get(self.attribute)
else:
__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('{0} is mandatory in {1} (got {2})'.format(self.name, self.tag, result))
return None
if self.type_converter:
try:
result = self.type_converter.from_string(result)
except Exception as e:
from . import gpx as mod_gpx
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 "{0}", possible: {1}'.format(result, self.possible))
return result
def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''):
if value is None:
return ''
if not prettyprint:
indent = ''
if self.attribute:
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):
def __init__(self, name, classs, tag=None, is_list=None):
AbstractGPXField.__init__(self, is_list=is_list)
self.name = name
self.tag = tag or name
self.classs = classs
def from_xml(self, node, version):
if self.is_list:
result = []
for child in node:
if child.tag == self.tag:
result.append(gpx_fields_from_xml(self.classs, child,
version))
return result
else:
field_node = node.find(self.tag)
if field_node is None:
return None
return gpx_fields_from_xml(self.classs, field_node, version)
def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''):
if not prettyprint:
indent = ''
if self.is_list:
result = []
for obj in value:
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,
prettyprint=prettyprint, indent=indent)
class GPXEmailField(AbstractGPXField):
"""
Converts GPX1.1 email tag group from/to string.
"""
def __init__(self, name, tag=None):
AbstractGPXField.__init__(self, is_list=False)
self.name = name
self.tag = tag or name
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 ''
email_id = email_node.get('id')
email_domain = email_node.get('domain')
return '{0}@{1}'.format(email_id, email_domain)
def to_xml(self, value, version, nsmap=None, prettyprint=True, indent=''):
"""
Write email address to XML
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]
email_domain = value[pos+1:]
else:
email_id = value
email_domain = 'unknown'
return ('\n' + indent +
'<{0} id="{1}" domain="{2}" />'.format(self.tag,
email_id, email_domain))
class GPXExtensionsField(AbstractGPXField):
"""
GPX1.1 extensions <extensions>...</extensions> key-value type.
"""
def __init__(self, name, tag=None, is_list=True):
AbstractGPXField.__init__(self, is_list=is_list)
self.name = name
self.tag = tag or 'extensions'
def from_xml(self, node, version):
"""
Build a list of extension Elements.
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
for child in extensions_node:
result.append(mod_copy.deepcopy(child))
return result
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('</' + prefixedname + '>' + 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 = []
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 + '</' + self.tag + '>')
return ''.join(result)
# ----------------------------------------------------------------------------------------------------
# Utility methods:
# ----------------------------------------------------------------------------------------------------
def _check_dependents(gpx_object, fieldname):
"""
Check for data in subelements.
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 = []
if 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:
# 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):
# Suppress empty tags
if suppressuntil:
if suppressuntil == gpx_field:
suppressuntil = ''
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.append(' ' + gpx_field.to_xml(value, version, nsmap,
prettyprint=prettyprint,
indent=indent + ' '))
elif value is not None:
if tag_open:
body.append('>')
tag_open = False
xml_value = gpx_field.to_xml(value, version, nsmap,
prettyprint=prettyprint,
indent=indent + ' ')
if xml_value:
body.append(xml_value)
if tag:
if tag_open:
body.append('>')
body.append('\n' + indent + '</' + tag + '>')
return ''.join(body)
def gpx_fields_from_xml(class_or_instance, node, version):
if mod_inspect.isclass(class_or_instance):
result = class_or_instance()
else:
result = class_or_instance
fields = result.gpx_10_fields
if version == '1.1':
fields = result.gpx_11_fields
node_path = [node]
for gpx_field in fields:
current_node = node_path[-1]
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(current_node.find(gpx_field))
else:
if current_node is not None:
value = gpx_field.from_xml(current_node, version)
setattr(result, gpx_field.name, value)
elif gpx_field.attribute:
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
"""
fields = classs.gpx_10_fields + classs.gpx_11_fields
gpx_field_names = []
instance = classs()
try:
attributes = list(filter(lambda x : x[0] != '_', dir(instance)))
attributes = list(filter(lambda x : not callable(getattr(instance, x)), attributes))
attributes = list(filter(lambda x : not x.startswith('gpx_'), attributes))
except Exception as e:
raise Exception('Error reading attributes for %s: %s' % (classs.__name__, e))
attributes.sort()
slots = list(classs.__slots__)
slots.sort()
if attributes != slots:
raise Exception('Attributes for %s is\n%s but should be\n%s' % (classs.__name__, attributes, slots))
for field in fields:
if not isinstance(field, str):
if field.is_list:
value = []
else:
value = None
try:
actual_value = getattr(instance, field.name)
except:
raise Exception('%s has no attribute %s' % (classs.__name__, field.name))
if value != actual_value:
raise Exception('Invalid default value %s.%s is %s but should be %s'
% (classs.__name__, field.name, actual_value, value))
#print('%s.%s -> %s' % (classs, field.name, value))
if not field.name in gpx_field_names:
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))
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
import xml.dom.minidom as mod_minidom
def split_gpxs(xml):
"""
Split single tracks from this one, without parsing with gpxpy
"""
dom = mod_minidom.parseString(xml)
gpx_node = _find_gpx_node(dom)
gpx_track_nodes = []
if gpx_node:
for child_node in gpx_node.childNodes:
if child_node.nodeName == 'trk':
gpx_track_nodes.append(child_node)
gpx_node.removeChild(child_node)
for gpx_track_node in gpx_track_nodes:
gpx_node.appendChild(gpx_track_node)
yield dom.toxml()
gpx_node.removeChild(gpx_track_node)
def join_gpxs(xmls):
"""
Utility to join GPX files without parsing them with gpxpy
"""
result = None
wpt_elements = []
rte_elements = []
trk_elements = []
for xml in xmls:
dom = mod_minidom.parseString(xml)
if not result:
result = dom
gpx_node = _find_gpx_node(dom)
if gpx_node:
for child_node in gpx_node.childNodes:
if child_node.nodeName == 'wpt':
wpt_elements.append(child_node)
gpx_node.removeChild(child_node)
elif child_node.nodeName == 'rte':
rte_elements.append(child_node)
gpx_node.removeChild(child_node)
elif child_node.nodeName == 'trk':
trk_elements.append(child_node)
gpx_node.removeChild(child_node)
gpx_node = _find_gpx_node(result)
if gpx_node:
for wpt_element in wpt_elements:
gpx_node.appendChild(wpt_element)
for rte_element in rte_elements:
gpx_node.appendChild(rte_element)
for trk_element in trk_elements:
gpx_node.appendChild(trk_element)
return result.toxml()
def _find_gpx_node(dom):
for gpx_candidate_node in dom.childNodes:
if gpx_candidate_node.nodeName == 'gpx':
return gpx_candidate_node
return None
@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Tomo Krajina
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging as mod_logging
import re as mod_re
try:
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
log = mod_logging.getLogger(__name__)
class GPXParser:
"""
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):
"""
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()
def init(self, 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)
def parse(self, version=None):
"""
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
"""
# 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:
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('"')
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)
except Exception as 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 ElementTree or lxml exception e).
#
# 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)
if root is None:
raise mod_gpx.GPXException('Document must have a `gpx` root node.')
if version is None:
version = root.get('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"
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright 2011 Tomo Krajina
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys as mod_sys
import math as mod_math
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, prettyprint=True, indent=''):
if not prettyprint:
indent = ''
attributes = attributes or {}
result = []
result.append('\n' + indent + '<{0}'.format(tag))
if content is None and default:
content = default
if attributes:
for attribute in attributes.keys():
result.append(make_str(' %s="%s"' % (attribute, attributes[attribute])))
if content is None:
result.append('/>')
else:
if escape:
result.append(make_str('>%s</%s>' % (mod_saxutils.escape(content), tag)))
else:
result.append(make_str('>%s</%s>' % (content, tag)))
result = make_str(''.join(result))
return result
def is_numeric(object):
try:
float(object)
return True
except TypeError:
return False
except ValueError:
return False
def to_number(s, default=0, nan_value=None):
try:
result = float(s)
if mod_math.isnan(result):
return nan_value
return result
except TypeError:
pass
except ValueError:
pass
return default
def total_seconds(timedelta):
""" Some versions of python don't have the timedelta.total_seconds() method. """
if timedelta is None:
return None
return (timedelta.days * 86400) + timedelta.seconds
def make_str(s):
""" 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")
return str(s)