-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c0df734
commit 9e8bffb
Showing
5 changed files
with
336 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/). | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__version__ = "0.1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] | ||
}, | ||
) |