Skip to content

Commit

Permalink
initial public commit
Browse files Browse the repository at this point in the history
  • Loading branch information
andrashann committed Jan 24, 2020
1 parent c0df734 commit 9e8bffb
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 1 deletion.
58 changes: 57 additions & 1 deletion README.md
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,2 +1,58 @@
# gpxslicer
a command line tool to slice up gpx files

gpxslicer slices GPX tracks at a given interval or at a list of provided cut points.

## Installation

The best way to install gpxslicer is via pip: `pip install gpxslicer`. You can alternatively install it from the source code: `python setup.py install`.

## Usage

### Command line

gpxslicer is primarily intended to be used as a command line tool (with support for redirection and piping data in/out).

`gpxslicer -i in.gpx -d 5000 > out.gpx` would, for example, take the tracks in `infile.gpx`, split them at every five kilometers, then pipe the results into `out.gpx`.

Full description of command line options:

| flag | command | description |
|------|----------------|-----------------------------------------------------------------------------------------------------------------|
| -h | --help | Show the help. |
| -i | --input | Specify the input GPX file with tracks to be sliced. If not given, input is read from stdin. |
| -o | --output | Specify the output GPX file. If not given, input is written to stdout. |
| -d | --distance | Slice tracks at every DISTANCE meters. |
| -e | --external | Slice tracks at waypoints found in EXTERNAL file. |
| -w | --waypoints | Slice tracks at waypoints found in INPUT. |
| | --no-tracks | Do not store sliced tracks in the output. Useful when slicing using `-d` and only the cut points are of interest. |
| | --no-waypoints | Do not store cut points in the output. Useful when slicing using `-e` or `-w` so the points are already (approximately) known. |
| -q | --quietly | Do not display status messages (that are normally sent to stderr). |

#### Slicing at intervals

When using `-d`, gpxslicer goes through each track and track segment separately (always restarting the distance counter when a new track or track segment starts in the input file).

Cut points are not interpolated but chosen from available points on the track. Therefore, if there are very few points in the track or the chosen interval is small, there can be a significant variation in the actual length of the cut segments. There should be no major problems with sufficiently many track points and large slice distances.

#### Slicing at waypoints

When using `-e` or `-w`, gpxslicer uses the `get_nearest_location` method from the GPX class of gpxpy. This finds the point of the tracks stored in the input file that is closest to the given slice point, and then splits the track there. Finally, it duplicates this point into the new track to prevent a gap. When the slice points are very far from the track, there can be unexpected or insensible results.

Note that all waypoints in the gpx file will be used, so any unnecessary waypoints should be removed beforehand.

### Python package

The two main functions, `slice_gpx_at_points()` and `slice_gpx_at_interval()` can be accessed by

```python
from gpxslicer import slicer
slicer.slice_gpx_at_interval(gpx_data, interval)
slicer.slice_gpx_at_points(gpx_data)
```

Detailed documentation of these functions can be found in the code.

## More info

Read more about this package, including the motivation to write it [on my blog](https://hann.io/articles/2020/introducing-gpxslicer/).

1 change: 1 addition & 0 deletions gpxslicer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
100 changes: 100 additions & 0 deletions gpxslicer/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import argparse
import os
import sys

import gpxslicer
from gpxslicer import slicer

def file_exists(x):
"""
'Type' for argparse - checks that file exists but does not open it.
https://stackoverflow.com/posts/11541495/revisions
"""
if not os.path.exists(x):
# Argparse uses the ArgumentTypeError to give a rejection message like:
# error: argument input: x does not exist
raise argparse.ArgumentTypeError("input file {0} does not exist".format(x))
return x

def main():
parser = argparse.ArgumentParser(description='Slice GPX tracks at given intervals or near provided points.')
parser.add_argument("-i", "--input",
dest="inputfile", type=file_exists,
help="GPX file to be sliced", metavar="FILE")
parser.add_argument("-o", "--output",
dest="outputfile", required=False,
help="Output file to be written. If not given, will print results to stdout.",
metavar="FILE")

slice_group = parser.add_mutually_exclusive_group(required=True)

slice_group.add_argument("-d", "--distance",
dest="slice_distance", required=False, type=int, metavar="METERS",
help="Slice the GPX track(s) at every METERS meters starting from the beginning")
slice_group.add_argument("-e", "--external",
dest="slice_file", required=False, metavar="EXT_WPTS_FILE", type=file_exists,
help="Slice the GPX track(s) at points nearest to the waypoints in the file EXT_WPTS_FILE")
slice_group.add_argument("-w", "--waypoints",
dest="slice_waypoints", required=False, action="store_true",
help="Use waypoints found in inputfile to slice tracks found in inputfile at the points closest to the waypoints")

parser.add_argument("--no-tracks",
dest="no_tracks", required=False, action="store_true",
help="Do not store sliced tracks in output")
parser.add_argument("--no-waypoints",
dest="no_waypoints", required=False, action="store_true",
help="Do not store cut points in output")

parser.add_argument("-q", "--quietly",
dest="quietly", required=False, action='store_true',
help="Don't print diagnostic messages")


args = parser.parse_args()

if args.inputfile:
input_data = open(args.inputfile, 'r')
else:
input_data = sys.stdin.read()

sourcedata = slicer.parse_gpx(input_data)

if args.slice_distance:
result = slicer.slice_gpx_at_interval(sourcedata, args.slice_distance)
elif args.slice_file:
result = slicer.slice_gpx_at_points(sourcedata, slicer.load_gpx(args.slice_file))
elif args.slice_waypoints:
result = slicer.slice_gpx_at_points(sourcedata)

if not args.quietly:
p = 0
for t in result.tracks:
for s in t.segments:
p += len(s.points)
sys.stderr.write('GPX result has {t} tracks with {p} points in total and {w} waypoints.\n'.format(
t = len(result.tracks),
p = p,
w = len(result.waypoints)
))

if args.no_waypoints:
result.waypoints = []
if not args.quietly:
sys.stderr.write('No waypoints will be saved in the output.\n')
if args.no_tracks:
result.tracks = []
if not args.quietly:
sys.stderr.write('No tracks will be saved in the output.\n')

if args.outputfile:
with open(args.outputfile, 'w') as o:
o.write(result.to_xml())
if not args.quietly:
sys.stderr.write('Saved data to {}.\n'.format(args.outputfile))
else:
if not args.quietly:
sys.stderr.write('Your GPX data will be printed to stdout.\n')
sys.stdout.write(result.to_xml())

if __name__ == '__main__':
main()
142 changes: 142 additions & 0 deletions gpxslicer/slicer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import gpxpy


def slice_gpx_at_points(source_gpx, other_gpx=None):
""" Takes all tracks segments found in source_gpx and splits them up by
taking all waypoints in other_gpx (or source_gpx if other_gpx is None) and
finding the track points of source_gpx closest to these points and taking
these as the points to cut the tracks.
Arguments:
`source_gpx`: gpxpy.GPX
`other_gpx`: gpxpy.GPX or None
Returns:
a new gpxpy.GPX object with the now-split segments as separate tracks
and all identified cut points as waypoints
"""

if not other_gpx:
other_gpx = source_gpx.clone()

out_gpx = gpxpy.gpx.GPX()

# GPX.split() is done in place, so we'll copy the source not to modify it
temp_gpx = gpxpy.gpx.GPX()
temp_gpx.tracks = source_gpx.tracks[:]

for point in other_gpx.waypoints:
nearest_point_on_track = temp_gpx.get_nearest_location(gpxpy.geo.Location(latitude = point.latitude,
longitude = point.longitude))

cut_point = gpxpy.gpx.GPXWaypoint(latitude = nearest_point_on_track.location.latitude,
longitude = nearest_point_on_track.location.longitude,
elevation = nearest_point_on_track.location.elevation)

# split at the closest point on the closest track segment
temp_gpx.split(track_no = nearest_point_on_track.track_no,
track_segment_no = nearest_point_on_track.segment_no,
track_point_no = nearest_point_on_track.point_no)

# splitting causes a hole between the last and next first point,
# therefore we add the split point to the next (new) segment as well.
(temp_gpx
.tracks[nearest_point_on_track.track_no]
.segments[nearest_point_on_track.segment_no + 1]
.points.insert(0,
gpxpy.gpx.GPXTrackPoint(latitude = cut_point.latitude,
longitude = cut_point.longitude,
elevation = cut_point.elevation)
)
)

# store the cut point in the output
out_gpx.waypoints.append(cut_point)

# every new segment should be its own track (for analysis, display etc.)
# so let's take all tracks and all segments in the temporary gpx and turn
# them into tracks with one segment each.
tracker = 0
for track in temp_gpx.tracks:
for segment in track.segments:
out_track = gpxpy.gpx.GPXTrack(name = 'track{}'.format(tracker))
tracker += 1
out_track.segments.append(segment)
out_gpx.tracks.append(out_track)

return out_gpx

def slice_gpx_at_interval(source_gpx, slice_interval, dist3d=True):
""" Takes all track segments found in source_gpx and splits them up into
segments of approximately slice_interval long (always takes the first point
after reaching the next multiple of slice_interval; distance is counted
cumulatively, not after the cut point to ensure more precision).
Arguments:
`source_gpx`: gpxpy.GPX
`slice_interval`: a number (will be converted to int)
Returns:
a gpxpy.GPX object with the now-split segments as separate tracks
and all identified cut points as waypoints
"""
out_gpx = gpxpy.gpx.GPX()

tracker = 0
for track in source_gpx.tracks:
out_track = gpxpy.gpx.GPXTrack(name = 'track{}'.format(tracker))
tracker += 1
for segment in track.segments:
out_segment = gpxpy.gpx.GPXTrackSegment()

# we need a previous_point to calculate distances later on
# for the first point, it will be itself
previous_point = segment.points[0]

# we store both a cumulative distance since the start of the segment
# as well as the distance since the last cut point
cumulative_distance = 0
distance_since_slice = 0

for point in segment.points:
if dist3d:
current_distance = point.distance_3d(previous_point)
else:
current_distance = point.distance_2d(previous_point)

cumulative_distance += current_distance
distance_since_slice += current_distance

# we always add a point to a segment, which we then add to the track
# since there will be points in the track after the last slice point as well
# and we don't want to lose that part of the track
out_segment.points.append(point)

# when we pass the slice interval:
if distance_since_slice > int(slice_interval):
distance_since_slice = 0

# store the point as a waypoint
out_gpx.waypoints.append(gpxpy.gpx.GPXWaypoint(latitude = point.latitude,
longitude = point.longitude,
elevation = point.elevation))

# if we arrive at a slice point, finish the currently
# running segment AND track and initialize a clean one
# for each, add the current point to the new segment
# to ensure continuity
out_track.segments.append(out_segment)
out_gpx.tracks.append(out_track)
out_track = gpxpy.gpx.GPXTrack(name = 'track{}'.format(tracker))
tracker += 1
out_segment = gpxpy.gpx.GPXTrackSegment()
out_segment.points.append(point)

previous_point = point
out_track.segments.append(out_segment)
out_gpx.tracks.append(out_track)

return out_gpx

def parse_gpx(file):
return gpxpy.parse(file)
36 changes: 36 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pathlib
from setuptools import setup

# The directory containing this file
HERE = pathlib.Path(__file__).parent

# The text of the README file
README = (HERE / "README.md").read_text()

# This call to setup() does all the work
setup(
name="gpxslicer",
version="0.1.0",
description="Slice up gpx tracks based on distance from the start or custom waypoints",
long_description=README,
long_description_content_type="text/markdown",
url="https://github.com/andrashann/gpxslicer",
author="András Hann",
author_email="dev@hann.io",
license="Apache License 2.0",
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Environment :: Console",
"License :: OSI Approved :: Apache Software License",
"Topic :: Scientific/Engineering :: GIS",
"Topic :: Utilities"
],
packages=["gpxslicer"],
install_requires=["gpxpy"],
entry_points={
"console_scripts": [
"gpxslicer=gpxslicer.__main__:main",
]
},
)

0 comments on commit 9e8bffb

Please sign in to comment.