#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
namereducegpx.py
Tue 11 Dec 2012 10:24:43 CET
Loads a gpx file of a journey (preferably over many kilometres) and
reduces the track points so that only ones of a certain threshold
apart remain. Then the points are iterated through and a place name is
derived from two possible geocoding services and copied to the track
point name sub element. The final gpx file is saved with a suffix.
Argument:
path to a gpx file
Options:
-d --distance Set a threshold distance in km (default 20)
n-s --suffix Specify a suffix to be appended to the file name before
the extension (default '_named')
-o --offline Do not use Internet geocoding services. Has the effect of
reducing a gpx track only
-g --google Use Google reverse geocoder instead of default Mapquest
open api
Copyright 2012 Daniel Belasco Rogers <http://planbperformance.net/dan>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
<http://www.gnu.org/licenses/>.
"""
try:
from lxml import etree
except ImportError:
print """
*************************************************
You do not have a module that this script needs
Please install python-lxml from the repositories
*************************************************
"""
sys.exit(2)
try:
from geopy import distance
except ImportError:
print 'geopy not installed - download from http://code.google.com/p/geopy/'
sys.exit(2)
import sys
import simplejson
from urllib2 import urlopen
from time import sleep
from os import path
from optparse import OptionParser
def parseargs():
"""
Get the path to the gpx file
"""
usage = """
%prog path/to/gpx/file.gpx
"""
parser = OptionParser(usage, version="%prog 0.1")
parser.add_option("-d",
"--distance",
dest="gapdist",
action="store",
type="int",
default=20,
help="Distance between points to get placenames from in km (default is 20)")
parser.add_option("-s",
"--suffix",
dest="suffix",
action="store",
type="string",
default="_named",
help="Suffix to append to output gpx file before the extension. Default is '_named'")
parser.add_option("-o",
"--offline",
dest="offline",
action="store_true",
default=False,
help="Work in Offline mode (default False). If chosen, does not look up placenames.")
parser.add_option("-g",
"--google",
dest="google",
action="store_true",
default=False,
help="Use Google api as the reverse geocoder. If not chosen, defaults to Mapquest open api")
options, args = parser.parse_args()
# warn the user they haven't passed an argument
if len(args) != 1:
parser.error("""
Please enter a path to a gpx file
e.g. python namereducegpx.py ~/dansdocs/soul_walker/mrl/gpsForAnimation/FinalSoulGPXforDB/2011-01-12-15ApartBrumNottsDan.gpx
""")
# args is a list, even if it contains only 1 item
return args[0], options.gapdist, options.suffix, options.offline, options.google
def findelement(xmlns, trkpoint, element):
"""
get text for an element.
return 'NULL' if not found
"""
try:
element = trkpoint.find(('{%s}%s' % (xmlns, element))).text.strip()
except AttributeError:
element = 'NULL'
return element
def getpoints(gpxpath):
"""
read the gpx file and return a list of points in a dictionary- try
and get everything the file has to offer i.e. speed and course if
possible
"""
pointlist = []
# parse gpxfile
tree = etree.parse(gpxpath)
root = tree.getroot()
# get namespace and define variables for the loop
xmlns = root.nsmap[None]
trkseg = '{%s}trkseg' % xmlns
for trackseg in root.iter(trkseg):
# filter out empty tracks by searching for first time element
try:
trktime = trackseg.find(('{%s}trkpt/{%s}time' % (xmlns, xmlns))).text
except AttributeError:
# time element isn't there, which means its an empty track
continue
# create a dictionary for each line in list (each track point)
for trkpoint in trackseg:
pointlist.append({
'lat': trkpoint.get('lat'),
'lon': trkpoint.get('lon'),
'ele': findelement(xmlns, trkpoint, 'ele'),
'time': findelement(xmlns, trkpoint, 'time'),
'course': findelement(xmlns, trkpoint, 'course'),
'speed': findelement(xmlns, trkpoint, 'speed'),
'name': findelement(xmlns, trkpoint, 'name'),
'fix': findelement(xmlns, trkpoint, 'fix'),
'hdop': findelement(xmlns, trkpoint, 'hdop')
})
return pointlist
def reducepoints(pointlist, gapdist):
"""
take the pointlist, calculate distances and reject points that are
nearer than defined threshold apart (gapdist)
"""
# set up the previous trackpoint (1st in list) for comparison
lat1 = pointlist[0]['lat']
lon1 = pointlist[0]['lon']
# start the output list with the first trackpoint
reducedlist = [{
'lat': lat1,
'lon': lon1,
'ele': pointlist[0]['ele'],
'time': pointlist[0]['time'],
'course': pointlist[0]['course'],
'speed': pointlist[0]['speed'],
'name': pointlist[0]['name'],
'fix': pointlist[0]['fix'],
'hdop': pointlist[0]['hdop']
}]
# start iterating through the list from 2nd track point
for x in range(1, len(pointlist) - 1):
lat2 = pointlist[x]['lat']
lon2 = pointlist[x]['lon']
gap = (distance.distance((lat1, lon1), (lat2, lon2)).m) / 1000
if gap >= gapdist:
reducedlist.append({
'lat': lat2,
'lon': lon2,
'ele': pointlist[x]['ele'],
'time': pointlist[x]['time'],
'course': pointlist[x]['course'],
'speed': pointlist[x]['speed'],
'name': pointlist[x]['name'],
'fix': pointlist[x]['fix'],
'hdop': pointlist[x]['hdop']
})
# this trackpoint will be the 'previous' trackpoint in the
# next iteration
lat1 = lat2
lon1 = lon2
return reducedlist
def gpxfrompoints(reducedlist, offline, google):
"""
prepare a gpx xml tree from the reduced list of point
"""
# prepare the gpx xml tree we're going to write
newgpxfile = etree.Element('gpx', attrib={"creator": "reducegpx.py",
"version": "1.0",
"xmlns": "http://www.topografix.com/GPX/1/0"
})
trk = etree.SubElement(newgpxfile, 'trk')
trkname = etree.SubElement(trk, 'name')
if google:
trkname.text = 'namereducegpx.py using Google reverse geocode api'
elif not google:
trkname.text = 'namereducegpx.py using Mapquest open reverse geocode api'
trkseg = etree.SubElement(trk, 'trkseg')
# iterate through the points, writing the attributes from each
# point dictionary in the reduced list
for pointdict in reducedlist:
trkpoint = etree.SubElement(trkseg, 'trkpt')
for key in pointdict:
if key in ('lat', 'lon'):
trkpoint.set(key, pointdict[key])
else:
if pointdict[key] != 'NULL':
element = etree.SubElement(trkpoint, key)
if key == 'name' and not offline:
element.text = lookuppoint(google, (pointdict['lat'], pointdict['lon']))
else:
element.text = pointdict[key]
if not 'name' in pointdict and not offline:
element = ettree.SubElement(trkpoint, 'name')
element.text = lookuppoint(google, (pointdict['lat'], pointdict['lon']))
else:
continue
return newgpxfile
def lookuppoint(google, query):
"""
using open.mapquestapi (not sure about its accuracy...)
or
http://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&sensor=false
"""
lat = float(query[0])
lon = float(query[1])
if not google:
url = "http://open.mapquestapi.com/nominatim/v1/reverse?format=json&lat=%f&lon=%f" % (lat, lon)
elif google:
url = "http://maps.googleapis.com/maps/api/geocode/json?latlng=%f,%f&sensor=false" % (lat, lon)
else:
print "service %s not implemented" % service
print "Looking up lat %f lon %f" % (lat, lon)
result = urlopen(url)
jsonstring = result.read()
json = simplejson.loads(jsonstring)
if not google:
place = json.get('display_name', [])
elif google:
place = json.get('results')
if place == []:
place = 'Unknown'
return place
place = place[0]['formatted_address']
print place
return place
def makenewgpxpath(gpxpath, suffix):
"""
derive a new gpxpath from current one. If a destination path is
specified, use this in constructing newgpxpath
"""
head, tail = path.split(gpxpath)
gpxpath, extension = path.splitext(tail)
newgpxpath = '%s%s%s' % (gpxpath, suffix, extension)
newfilepath = path.join(head, newgpxpath)
return newfilepath
def writegpxfile(newfilepath, newgpxfile):
"""
writes the gpx file to the new file path
"""
with open(newfilepath, 'w') as writefile:
writefile.write(etree.tostring(newgpxfile,
encoding="utf-8",
pretty_print=True,
xml_declaration=True))
return
def main():
"""
"""
gpxpath, gapdist, suffix, offline, google = parseargs()
# read points from gpx file into a list
print "Reading gpx file..."
pointlist = getpoints(gpxpath)
print "Found %d points" % len(pointlist)
# reduce the list by the defined threshold
print "Finding points that are at least %dkm apart" % gapdist
reducedlist = reducepoints(pointlist, gapdist)
print "Reduced points to %d" % len(reducedlist)
# produce a new gpx file from the reduced points list
print "Preparing new gpx file"
if not offline:
if not google:
print "Using mapquest open reverse geocode api"
elif google:
print "Using google reverse geocode api"
newgpxfile = gpxfrompoints(reducedlist, offline, google)
# generate a new file name from old
newfilepath = makenewgpxpath(gpxpath, suffix)
# print the tree to a new gpx file in the same directory and
# finish up
writegpxfile(newfilepath, newgpxfile)
print "Wrote", newfilepath
print "Script ends here sucessfully"
if __name__ == '__main__':
sys.exit(main()) |