binary stumble
an artist's adventures in the digital world
binary stumble
Skip to content
  • Home
  • About
  • Thanks
← Clay and Plaster Frequency Map III
A new way of looking at the GPS traces →

Placename poem (the flyover script)

Posted on 2012-12-11 by Daniel Belasco Rogers
A Soul Trip across Northern Europe

A Soul Trip across Northern Europe. GPX file generated by namereducegpx.py and visualised with Qgis

While working on Soul Walker, I’ve been revisiting the files we made for the Nottingham University research phase last year and wondering about those files generated by NetLogo of a ‘soul’ patiently trudging across the territory I flew over, oblivious to puddles, hedgerow, supermarkets.

The NetLogo script took a GPX file as input and using a flocking algorithm (a flock of two), generated a second GPX file derived from following the points in the first file at 6kph.

The output GPX file from such an operation is very featureless and straight. As you see from the illustration above, the ‘soul’ had to turn around in the North Sea as I flew back to Berlin before it had reached me.

I started to think about a way of extracting the places this ‘soul’ might travel through and get an idea of this fly-over territory and thought I would generate a placename poem from points along its path.

Hence the script below. It takes a GPX file as input and reduces the number of trackpoints to those that are a definable threshold apart (default is 20km) in order to ensure the list contains unique placenames and spare the reverse geocoding server.

The output of namereducegpx.py is a second GPX file with a suffix before the file extension (‘_named’ by default) which you can visualise in Qgis as I have done above. You can also use the script to simply reduce the trackpoints to the distance threshold by running with the ‘-o’ or ‘–offline’ option in which case the points are not sent to a geocoding service. There is also the option of using the Google reverse geocode api instead of the default Mapquest Open reverse geocoder (in Beta at time of writing).

Now to actually write a script that generates the poem from the GPX file.

#!/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 <https://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.
<https://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 https://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": "https://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
    https://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 = "https://open.mapquestapi.com/nominatim/v1/reverse?format=json&lat=%f&lon=%f" % (lat, lon)
    elif google:
        url = "https://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())
This entry was posted in Diary, GPS, Python, Software and tagged data, gis, gpx, NetLogo, psychogeography, QGIS, soul walker, visualisation. Bookmark the permalink.
← Clay and Plaster Frequency Map III
A new way of looking at the GPS traces →
  • Search

  • Archives

  • Tags

    • arnside
    • Berlin
    • Broadway
    • clay
    • crew
    • cumbria
    • data
    • data landscape
    • DIY
    • dowsing
    • drawing
    • drawing machine
    • england
    • eric joris
    • eTrex
    • frequency map
    • Garmin
    • gis
    • gnuplot
    • gpx
    • hack
    • hardware
    • HP DraftPro EXL (7576A)
    • logging
    • martin howse
    • Mathieu Baudier
    • metalwork
    • nature
    • Nottingham
    • openframeworks
    • pen plotter
    • plants
    • plaster
    • printing
    • psychogeography
    • psychogeophysics
    • QGIS
    • residency
    • Spatialite
    • suffolk
    • uk
    • visualisation
    • woodwork
    • workshop
    • xml
  • Blogroll

    • Crazy-wonderful Wilfried Hou Je Bek
    • David Tebbs Micro Story Project
    • Going Solo residency blog
    • Nikki Pugh's Blog
    • Peter Vasil's Blog
    • Sue Palmer's botany / art blog
    • Urban Adventure in Rotterdam
  • Recent Comments

    • uair01 on Gathering plants and fruit with Martin
    • Peter on Using Spatialite to store and represent our GPX data
    • Daniel Belasco Rogers on Why Conjuring is the opposite of Magic (and why it matters)
    • uair01 on Why Conjuring is the opposite of Magic (and why it matters)
    • Peter on Drawinglife, the openFrameworks GPS visualisation application, open sourced
  • Meta

    • Log in
    • Entries feed
    • Comments feed
    • WordPress.org
binary stumble
Proudly powered by WordPress.