gpx-link 1.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.3
2
+ Name: gpx-link
3
+ Version: 1.19.0
4
+ Summary: View GPX waypoints on OpenStreetMap and open locations in Google Maps
5
+ Author: Markus Borris
6
+ Author-email: Markus Borris <devmborrs@gmail.com>
7
+ Requires-Dist: gpxpy>=1.6.2
8
+ Requires-Dist: pyside6>=6.6.0 ; extra == 'gui'
9
+ Requires-Python: >=3.10
10
+ Provides-Extra: gui
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "gpx-link"
3
+ version = "1.19.0"
4
+ description = "View GPX waypoints on OpenStreetMap and open locations in Google Maps"
5
+ requires-python = ">=3.10"
6
+ authors = [
7
+ { name = "Markus Borris", email = "devmborrs@gmail.com" },
8
+ ]
9
+ dependencies = [
10
+ "gpxpy>=1.6.2",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ gui = [
15
+ "PySide6>=6.6.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ gpx-link = "gpx_link.cli:main"
20
+ gpx-link-gui = "gpx_link.gui.app:main"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.11.11,<0.12.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "detect-secrets>=1.5.0",
29
+ "pre-commit>=4.6.0",
30
+ "pytest>=9.0.3",
31
+ "pytest-cov>=7.1.0",
32
+ "python-semantic-release>=10.5.0,<11",
33
+ "ruff>=0.15.12",
34
+ ]
35
+
36
+ [tool.ruff]
37
+ line-length = 88
38
+ target-version = "py310"
39
+ src = ["src"]
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "I", "UP", "B"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = [
47
+ "--cov=gpx_link",
48
+ "--cov-report=term-missing",
49
+ "--cov-report=xml",
50
+ ]
51
+
52
+ [tool.coverage.run]
53
+ source = ["gpx_link"]
54
+
55
+ [tool.coverage.report]
56
+ show_missing = true
57
+
58
+ [tool.codespell]
59
+ skip = ".venv,uv.lock,android/Gemfile.lock"
60
+
61
+ [tool.semantic_release]
62
+ version_toml = ["pyproject.toml:project.version"]
63
+ build_command = "uv build"
@@ -0,0 +1,23 @@
1
+ """GPX waypoint parsing, map HTML generation, and Google Maps URLs."""
2
+
3
+ from gpx_link.bounds import Bounds, bounds_for_waypoints
4
+ from gpx_link.html_map import build_leaflet_html
5
+ from gpx_link.maps_urls import google_maps_url
6
+ from gpx_link.models import GeoPath, Waypoint
7
+ from gpx_link.parser import (
8
+ load_map_features_from_paths,
9
+ load_waypoints_from_files,
10
+ load_waypoints_from_paths,
11
+ )
12
+
13
+ __all__ = [
14
+ "Bounds",
15
+ "GeoPath",
16
+ "Waypoint",
17
+ "bounds_for_waypoints",
18
+ "build_leaflet_html",
19
+ "google_maps_url",
20
+ "load_map_features_from_paths",
21
+ "load_waypoints_from_files",
22
+ "load_waypoints_from_paths",
23
+ ]
@@ -0,0 +1,4 @@
1
+ from gpx_link.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from gpx_link.models import GeoPath, Waypoint
7
+
8
+ # Minimum span so a single point still gets a sensible zoom (degrees).
9
+ _MIN_SPAN_DEG = 0.002
10
+ # Fraction of lat/lon span added on each side before map fit (smaller = tighter crop).
11
+ _DEFAULT_PAD_RATIO = 0.04
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class Bounds:
16
+ min_lat: float
17
+ max_lat: float
18
+ min_lon: float
19
+ max_lon: float
20
+
21
+ def padded(self, pad_ratio: float = _DEFAULT_PAD_RATIO) -> Bounds:
22
+ lat_span = max(self.max_lat - self.min_lat, _MIN_SPAN_DEG)
23
+ lon_span = max(self.max_lon - self.min_lon, _MIN_SPAN_DEG)
24
+ plat = lat_span * pad_ratio
25
+ plon = lon_span * pad_ratio
26
+ return Bounds(
27
+ min_lat=self.min_lat - plat,
28
+ max_lat=self.max_lat + plat,
29
+ min_lon=self.min_lon - plon,
30
+ max_lon=self.max_lon + plon,
31
+ )
32
+
33
+
34
+ def bounds_for_waypoints(waypoints: list[Waypoint]) -> Bounds | None:
35
+ """Return axis-aligned bounds for all waypoints, or None if empty."""
36
+ return bounds_for_map(waypoints, [])
37
+
38
+
39
+ def bounds_for_tracks_from_file(paths: list[GeoPath], source: Path) -> Bounds | None:
40
+ """Bounds for ``kind == track`` segments originating from ``source`` only."""
41
+ resolved = source.resolve()
42
+ tracks = [
43
+ p for p in paths if p.kind == "track" and p.source_path.resolve() == resolved
44
+ ]
45
+ return bounds_for_map([], tracks)
46
+
47
+
48
+ def bounds_for_map(
49
+ waypoints: list[Waypoint],
50
+ paths: list[GeoPath],
51
+ ) -> Bounds | None:
52
+ """Bounding box over waypoints and path vertices; None if both are empty."""
53
+ lats: list[float] = []
54
+ lons: list[float] = []
55
+ for w in waypoints:
56
+ lats.append(w.latitude)
57
+ lons.append(w.longitude)
58
+ for p in paths:
59
+ for lat, lon in p.points:
60
+ lats.append(lat)
61
+ lons.append(lon)
62
+ if not lats:
63
+ return None
64
+ return Bounds(
65
+ min_lat=min(lats),
66
+ max_lat=max(lats),
67
+ min_lon=min(lons),
68
+ max_lon=max(lons),
69
+ )
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from gpx_link.html_map import build_leaflet_html
9
+ from gpx_link.parser import load_map_features_from_paths
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="gpx-link",
15
+ description="List GPX waypoints and export an OpenStreetMap HTML view.",
16
+ )
17
+ sub = parser.add_subparsers(dest="command", required=True)
18
+
19
+ p_list = sub.add_parser("list", help="Print waypoints as JSON lines")
20
+ p_list.add_argument(
21
+ "gpx",
22
+ nargs="+",
23
+ type=Path,
24
+ help="GPX file path(s)",
25
+ )
26
+
27
+ p_html = sub.add_parser("html", help="Write Leaflet + OSM HTML to stdout or file")
28
+ p_html.add_argument(
29
+ "gpx",
30
+ nargs="+",
31
+ type=Path,
32
+ help="GPX file path(s)",
33
+ )
34
+ p_html.add_argument(
35
+ "-o",
36
+ "--output",
37
+ type=Path,
38
+ help="Output file (default: stdout)",
39
+ )
40
+
41
+ args = parser.parse_args(argv)
42
+ paths = [p.expanduser().resolve() for p in args.gpx]
43
+
44
+ if args.command == "list":
45
+ wpts, _ = load_map_features_from_paths(paths)
46
+ for w in wpts:
47
+ line = {
48
+ "source": str(w.source_path),
49
+ "name": w.name,
50
+ "latitude": w.latitude,
51
+ "longitude": w.longitude,
52
+ "elevation_m": w.elevation_m,
53
+ "description": w.description,
54
+ "symbol": w.symbol,
55
+ "waypoint_type": w.waypoint_type,
56
+ }
57
+ sys.stdout.write(json.dumps(line, ensure_ascii=False) + "\n")
58
+ return 0
59
+
60
+ if args.command == "html":
61
+ wpts, geopaths = load_map_features_from_paths(paths)
62
+ html = build_leaflet_html(wpts, geopaths)
63
+ if args.output:
64
+ args.output.write_text(html, encoding="utf-8")
65
+ else:
66
+ sys.stdout.write(html)
67
+ return 0
68
+
69
+ return 1
@@ -0,0 +1 @@
1
+ """Qt desktop UI (optional dependency: PySide6)."""