Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ The **third number** is for emergencies when we need to start branches for older

## [Unreleased](https://github.com/sgraaf/gpx/compare/2026.3.0...HEAD)

### Added

- New `operations` module that exposes the editing and merging operations behind the `gpx edit` and `gpx merge` CLI commands as a public, reusable API:
- `crop()`: Crop a GPX to a geographic bounding box
- `trim()`: Trim a GPX to a date/time range
- `reverse()`: Reverse the routes and/or tracks of a GPX
- `strip_metadata()`: Strip metadata (fields) from a GPX
- `reduce_precision()`: Reduce the precision of the coordinates and/or elevations of a GPX
- `filter_points()`: Filter the points of a GPX with an arbitrary predicate
- `merge()`: Merge multiple GPX instances into one
- `split()`: Split track segments at time and/or distance gaps
- `simplify()`: Simplify the tracks and routes of a GPX with the Ramer-Douglas-Peucker algorithm
- `smooth()`: Smooth the coordinates and/or elevations of the tracks and routes of a GPX with a moving average
- `shift_time()`: Shift all point timestamps of a GPX by a time delta
- `strip_extensions()`: Strip all extensions from a GPX
- All operations are pure (they return a new `GPX` instance and never mutate the input) and are importable directly from the top-level package (e.g. `from gpx import crop, merge`).
- New `gpx edit` CLI options exposing the new operations:
- `--split-time-gap SECONDS` and `--split-distance-gap METERS`: Split track segments at gaps
- `--simplify TOLERANCE`: Simplify tracks and routes (tolerance in metres)
- `--smooth WINDOW`: Smooth track and route coordinates and elevations with a moving average
- `--shift-time SECONDS`: Shift all point timestamps (may be negative)
- `--strip-extensions`: Strip all extensions
- New file conversion functions in the `io` module, exposing the operation behind the `gpx convert` CLI command as a public, reusable API:
- `convert_file()`: Convert a file between the GPX, GeoJSON and KML file formats
- `detect_format()`: Detect the file format from a file path's extension
- Both are importable directly from the top-level package (e.g. `from gpx import convert_file`).

### Changed

- The CLI (`gpx edit`, `gpx merge` and `gpx convert`) now uses the new `operations` module and `io` conversion functions internally (behavior is unchanged).

## [2026.3.0](https://github.com/sgraaf/gpx/compare/2026.2.0...2026.3.0) - 2026-05-17

This third release in the year 2026 adds a new `GeoGPXModel` base class for GPX models that carry geometric data.
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ gpx/
│ ├── types.py # Custom types: Latitude, Longitude, Degrees, Fix, DGPSStation
│ ├── utils.py # Utility functions for XML parsing and serialization
│ ├── convert.py # Conversion functions: from_geo_interface, from_wkb, from_wkt
│ ├── io.py # I/O functions: read_gpx, read_geojson, read_kml
│ ├── io.py # I/O functions: read_gpx, read_geojson, read_kml, convert_file, detect_format
│ ├── operations.py # Edit/merge operations: crop, trim, reverse, strip_metadata, strip_extensions, reduce_precision, filter_points, split, simplify, smooth, shift_time, merge
│ ├── cli.py # Command-line interface implementation
│ └── py.typed # PEP 561 marker for type hints
├── docs/ # Sphinx documentation (MyST Markdown)
Expand All @@ -57,6 +58,7 @@ gpx/
│ ├── test_utils.py # Utilities tests
│ ├── test_waypoint.py # Waypoint model tests
│ ├── test_io_convert.py # IO and conversion tests (read/write/convert)
│ ├── test_operations.py # Edit/merge operations tests
│ ├── test_cli.py # CLI tests
│ ├── test_main.py # Main entry point tests
│ ├── test_extensions.py # Extensions tests
Expand Down
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,22 @@ gpx = read_geojson("path/to/file.geojson")
gpx = read_kml("path/to/file.kml")
```

### Converting between file formats

*gpx* can convert files between the GPX, GeoJSON and KML file formats directly:

```python
from gpx import convert_file


# Formats are auto-detected from the file extensions...
convert_file("input.gpx", "output.geojson")
convert_file("input.kml", "output.gpx")

# ... or can be given explicitly
convert_file("input.json", "output.gpx", input_format="geojson")
```

### Converting from data formats

*gpx* can convert from data formats (strings, bytes, objects):
Expand Down Expand Up @@ -326,6 +342,70 @@ geojson = {"type": "Point", "coordinates": [4.9041, 52.3676]}
gpx = from_geo_interface(geojson)
```

### Editing and merging GPX data

*gpx* provides operations for editing and merging GPX data. All operations are pure: they return a new `GPX` instance and never mutate the input:

```python
import datetime as dt

from gpx import (
crop,
filter_points,
merge,
read_gpx,
reduce_precision,
reverse,
shift_time,
simplify,
smooth,
split,
strip_extensions,
strip_metadata,
trim,
)


gpx = read_gpx("path/to/file.gpx")

# Crop to a geographic bounding box
cropped = crop(gpx, min_lat=52.0, max_lat=53.0, min_lon=4.0, max_lon=5.0)

# Trim to a date/time range
trimmed = trim(gpx, start=dt.datetime(2024, 1, 1, 10, 0, 0, tzinfo=dt.UTC))

# Reverse routes and/or tracks
reversed_gpx = reverse(gpx)

# Strip metadata (fields)
anonymous = strip_metadata(gpx, author=True, copyright=True)
bare = strip_metadata(gpx) # removes all metadata

# Reduce coordinate and/or elevation precision
reduced = reduce_precision(gpx, coordinate_precision=6, elevation_precision=1)

# Filter points with an arbitrary predicate
with_elevation = filter_points(gpx, lambda point: point.ele is not None)

# Split track segments at time and/or distance gaps
split_gpx = split(gpx, time_gap=dt.timedelta(minutes=10))

# Simplify tracks and routes (Ramer-Douglas-Peucker, tolerance in metres)
simplified = simplify(gpx, tolerance=10.0)

# Smooth track and route coordinates and elevations (moving average)
smoothed = smooth(gpx, window=5)

# Shift all point timestamps (e.g., to fix timezone mistakes or clock drift)
shifted = shift_time(gpx, dt.timedelta(hours=-2))

# Strip all extensions (e.g., heart rate, cadence, temperature)
without_extensions = strip_extensions(gpx)

# Merge multiple GPX instances into one
merged = merge([read_gpx("one.gpx"), read_gpx("two.gpx")])
```

### Command-Line Interface

*gpx* provides a command-line interface (CLI) for common GPX operations:
Expand All @@ -344,6 +424,10 @@ gpx edit input.gpx -o output.gpx --min-lat 52.0 --max-lat 53.0
gpx edit input.gpx -o output.gpx --start 2024-01-01T10:00:00 --end 2024-01-01T12:00:00
gpx edit input.gpx -o output.gpx --precision 5 --elevation-precision 1
gpx edit input.gpx -o output.gpx --strip-all-metadata
gpx edit input.gpx -o output.gpx --split-time-gap 600
gpx edit input.gpx -o output.gpx --simplify 10 --smooth 5
gpx edit input.gpx -o output.gpx --shift-time -7200
gpx edit input.gpx -o output.gpx --strip-extensions

# Merge multiple GPX files
gpx merge file1.gpx file2.gpx file3.gpx -o merged.gpx
Expand Down
7 changes: 7 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@
:members:
```

## `gpx.operations` Module

```{eval-rst}
.. automodule:: gpx.operations
:members:
```

## `gpx.utils` Module

```{eval-rst}
Expand Down
41 changes: 37 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ Running `gpx edit --help` or `python -m gpx edit --help` shows a list of all of
usage: gpx edit [-h] -o <OUTPUT_FILE> [--min-lat LATITUDE]
[--max-lat LATITUDE] [--min-lon LONGITUDE]
[--max-lon LONGITUDE] [--start DATETIME] [--end DATETIME]
[--reverse] [--reverse-routes] [--reverse-tracks]
[--strip-name] [--strip-desc] [--strip-author]
[--strip-copyright] [--strip-time] [--strip-keywords]
[--strip-links] [--strip-all-metadata] [--precision DIGITS]
[--split-time-gap SECONDS] [--split-distance-gap METERS]
[--simplify TOLERANCE] [--smooth WINDOW]
[--shift-time SECONDS] [--reverse] [--reverse-routes]
[--reverse-tracks] [--strip-name] [--strip-desc]
[--strip-author] [--strip-copyright] [--strip-time]
[--strip-keywords] [--strip-links] [--strip-all-metadata]
[--strip-extensions] [--precision DIGITS]
[--elevation-precision DIGITS]
<INPUT_FILE>

Expand Down Expand Up @@ -150,6 +153,35 @@ trim options:
--end DATETIME End datetime (ISO 8601 format, e.g.,
2024-01-01T12:00:00)

split options:
Split track segments at gaps

--split-time-gap SECONDS
Split track segments where the time between
consecutive points exceeds this many seconds
--split-distance-gap METERS
Split track segments where the distance between
consecutive points exceeds this many metres

simplify options:
Simplify tracks and routes

--simplify TOLERANCE Simplify tracks and routes with the Ramer-Douglas-
Peucker algorithm using this tolerance (in metres)

smooth options:
Smooth tracks and routes

--smooth WINDOW Smooth track and route coordinates and elevations with
a moving average over this many points (an odd integer
of at least 3)

time shift options:
Shift timestamps

--shift-time SECONDS Shift all point timestamps by this many seconds (may
be negative)

reverse options:
Reverse routes and/or tracks

Expand All @@ -168,6 +200,7 @@ strip options:
--strip-keywords Strip metadata keywords
--strip-links Strip metadata links
--strip-all-metadata Strip all metadata
--strip-extensions Strip all extensions

precision options:
Reduce coordinate precision
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,8 @@ pydocstyle.convention = "google"
"PLR2004", # magic-value-comparison (Pylint)
]

[tool.pyrefly]
search_path = ["src", "."]

[tool.mypy]
python_version = "3.11"
30 changes: 29 additions & 1 deletion src/gpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,23 @@
from .email import Email
from .extensions import Extensions
from .gpx import GPX
from .io import read_geojson, read_gpx, read_kml
from .io import convert_file, detect_format, read_geojson, read_gpx, read_kml
from .link import Link
from .metadata import Metadata
from .operations import (
crop,
filter_points,
merge,
reduce_precision,
reverse,
shift_time,
simplify,
smooth,
split,
strip_extensions,
strip_metadata,
trim,
)
from .person import Person
from .route import Route
from .track import Track
Expand Down Expand Up @@ -37,11 +51,25 @@
"Track",
"TrackSegment",
"Waypoint",
"convert_file",
"crop",
"detect_format",
"filter_points",
"from_geo_interface",
"from_string",
"from_wkb",
"from_wkt",
"merge",
"read_geojson",
"read_gpx",
"read_kml",
"reduce_precision",
"reverse",
"shift_time",
"simplify",
"smooth",
"split",
"strip_extensions",
"strip_metadata",
"trim",
]
Loading
Loading