gmaps2gpx 1.0.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.
- gmaps2gpx-1.0.0/PKG-INFO +156 -0
- gmaps2gpx-1.0.0/README.md +132 -0
- gmaps2gpx-1.0.0/gmaps2gpx/__init__.py +3 -0
- gmaps2gpx-1.0.0/gmaps2gpx/cli.py +460 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/PKG-INFO +156 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/SOURCES.txt +10 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/dependency_links.txt +1 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/entry_points.txt +2 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/requires.txt +1 -0
- gmaps2gpx-1.0.0/gmaps2gpx.egg-info/top_level.txt +1 -0
- gmaps2gpx-1.0.0/pyproject.toml +34 -0
- gmaps2gpx-1.0.0/setup.cfg +4 -0
gmaps2gpx-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gmaps2gpx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Convert Google Maps direction URLs to GPX files. Supports shortened URLs, dragged routes, alternative routes, and motorcycle (two-wheeler) mode.
|
|
5
|
+
Author-email: Prajwal P <prajwalp@finarb.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/prajwalp/gmaps2gpx
|
|
8
|
+
Project-URL: Issues, https://github.com/prajwalp/gmaps2gpx/issues
|
|
9
|
+
Keywords: gpx,google-maps,converter,gps,motorcycle,two-wheeler,directions
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28
|
|
24
|
+
|
|
25
|
+
# gmaps2gpx
|
|
26
|
+
|
|
27
|
+
Convert Google Maps direction URLs to GPX files with a single command.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Shortened URLs** - Paste `maps.app.goo.gl` links directly
|
|
32
|
+
- **Dragged routes** - Preserves via-points when you drag a route on Google Maps
|
|
33
|
+
- **Motorcycle mode** - Two-wheeler routing via Google Routes API (great for India/SE Asia)
|
|
34
|
+
- **Alternative routes** - `--shortest` picks the shortest route by distance
|
|
35
|
+
- **Batch convert** - Pass multiple URLs at once
|
|
36
|
+
- **All travel modes** - driving, walking, bicycling, transit, motorcycle
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# From PyPI
|
|
42
|
+
pip install gmaps2gpx
|
|
43
|
+
|
|
44
|
+
# Or with pipx (recommended - isolated install)
|
|
45
|
+
pipx install gmaps2gpx
|
|
46
|
+
|
|
47
|
+
# From source
|
|
48
|
+
git clone https://github.com/prajwalp/gmaps2gpx.git
|
|
49
|
+
cd gmaps2gpx
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Setup
|
|
54
|
+
|
|
55
|
+
You need a Google Maps API key with **Directions API** enabled. For motorcycle mode, also enable the **Routes API**.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Set it once (add to your .bashrc/.zshrc)
|
|
59
|
+
export GOOGLE_MAPS_API_KEY="your_key_here"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or pass it each time with `-k YOUR_KEY`.
|
|
63
|
+
|
|
64
|
+
### Getting an API key
|
|
65
|
+
|
|
66
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
|
67
|
+
2. Create a project (or select existing)
|
|
68
|
+
3. Enable **Directions API** (and **Routes API** for motorcycle mode)
|
|
69
|
+
4. Create an API key under Credentials
|
|
70
|
+
5. (Optional) Restrict the key to Directions API + Routes API
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Basic - paste any Google Maps directions link
|
|
76
|
+
gmaps2gpx 'https://maps.app.goo.gl/Mz9Q47fcBMHLa2HB7'
|
|
77
|
+
|
|
78
|
+
# Save to specific file
|
|
79
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -o ride.gpx
|
|
80
|
+
|
|
81
|
+
# Motorcycle / two-wheeler mode (uses Google Routes API)
|
|
82
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m motorcycle -o ride.gpx
|
|
83
|
+
|
|
84
|
+
# Pick the shortest route from alternatives
|
|
85
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' --shortest
|
|
86
|
+
|
|
87
|
+
# Full Google Maps URL works too
|
|
88
|
+
gmaps2gpx 'https://www.google.com/maps/dir/Mumbai/Goa' -o mumbai_goa.gpx
|
|
89
|
+
|
|
90
|
+
# Walking directions
|
|
91
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m walking
|
|
92
|
+
|
|
93
|
+
# Batch convert multiple routes
|
|
94
|
+
gmaps2gpx URL1 URL2 URL3
|
|
95
|
+
|
|
96
|
+
# Pass API key directly
|
|
97
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -k YOUR_API_KEY
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Options
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
positional arguments:
|
|
104
|
+
urls one or more Google Maps direction URLs
|
|
105
|
+
|
|
106
|
+
options:
|
|
107
|
+
-k, --api-key KEY Google Maps API key (or set GOOGLE_MAPS_API_KEY env var)
|
|
108
|
+
-o, --output FILE output GPX filename (auto-generated if omitted)
|
|
109
|
+
-m, --mode MODE travel mode: driving, walking, bicycling, transit, motorcycle
|
|
110
|
+
-s, --shortest fetch alternatives and pick the shortest by distance
|
|
111
|
+
-h, --help show help
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
1. Resolves shortened Google Maps URLs
|
|
117
|
+
2. Parses origin, destination, waypoints, and dragged via-points from the URL
|
|
118
|
+
3. Calls Google Directions API (or Routes API for motorcycle mode)
|
|
119
|
+
4. Decodes polylines into GPS coordinates
|
|
120
|
+
5. Generates a GPX 1.1 file with track points and waypoint markers
|
|
121
|
+
|
|
122
|
+
## Publishing to PyPI
|
|
123
|
+
|
|
124
|
+
One-time setup:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Install build tools
|
|
128
|
+
pip install build twine
|
|
129
|
+
|
|
130
|
+
# Create a PyPI account at https://pypi.org/account/register/
|
|
131
|
+
# Generate an API token at https://pypi.org/manage/account/
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To publish:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Build the package
|
|
138
|
+
python -m build
|
|
139
|
+
|
|
140
|
+
# Upload to PyPI
|
|
141
|
+
twine upload dist/*
|
|
142
|
+
|
|
143
|
+
# You'll be prompted for credentials:
|
|
144
|
+
# Username: __token__
|
|
145
|
+
# Password: pypi-YOUR_API_TOKEN
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
After publishing, anyone can install with:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install gmaps2gpx
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# gmaps2gpx
|
|
2
|
+
|
|
3
|
+
Convert Google Maps direction URLs to GPX files with a single command.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Shortened URLs** - Paste `maps.app.goo.gl` links directly
|
|
8
|
+
- **Dragged routes** - Preserves via-points when you drag a route on Google Maps
|
|
9
|
+
- **Motorcycle mode** - Two-wheeler routing via Google Routes API (great for India/SE Asia)
|
|
10
|
+
- **Alternative routes** - `--shortest` picks the shortest route by distance
|
|
11
|
+
- **Batch convert** - Pass multiple URLs at once
|
|
12
|
+
- **All travel modes** - driving, walking, bicycling, transit, motorcycle
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# From PyPI
|
|
18
|
+
pip install gmaps2gpx
|
|
19
|
+
|
|
20
|
+
# Or with pipx (recommended - isolated install)
|
|
21
|
+
pipx install gmaps2gpx
|
|
22
|
+
|
|
23
|
+
# From source
|
|
24
|
+
git clone https://github.com/prajwalp/gmaps2gpx.git
|
|
25
|
+
cd gmaps2gpx
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
You need a Google Maps API key with **Directions API** enabled. For motorcycle mode, also enable the **Routes API**.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Set it once (add to your .bashrc/.zshrc)
|
|
35
|
+
export GOOGLE_MAPS_API_KEY="your_key_here"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or pass it each time with `-k YOUR_KEY`.
|
|
39
|
+
|
|
40
|
+
### Getting an API key
|
|
41
|
+
|
|
42
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
|
43
|
+
2. Create a project (or select existing)
|
|
44
|
+
3. Enable **Directions API** (and **Routes API** for motorcycle mode)
|
|
45
|
+
4. Create an API key under Credentials
|
|
46
|
+
5. (Optional) Restrict the key to Directions API + Routes API
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Basic - paste any Google Maps directions link
|
|
52
|
+
gmaps2gpx 'https://maps.app.goo.gl/Mz9Q47fcBMHLa2HB7'
|
|
53
|
+
|
|
54
|
+
# Save to specific file
|
|
55
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -o ride.gpx
|
|
56
|
+
|
|
57
|
+
# Motorcycle / two-wheeler mode (uses Google Routes API)
|
|
58
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m motorcycle -o ride.gpx
|
|
59
|
+
|
|
60
|
+
# Pick the shortest route from alternatives
|
|
61
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' --shortest
|
|
62
|
+
|
|
63
|
+
# Full Google Maps URL works too
|
|
64
|
+
gmaps2gpx 'https://www.google.com/maps/dir/Mumbai/Goa' -o mumbai_goa.gpx
|
|
65
|
+
|
|
66
|
+
# Walking directions
|
|
67
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m walking
|
|
68
|
+
|
|
69
|
+
# Batch convert multiple routes
|
|
70
|
+
gmaps2gpx URL1 URL2 URL3
|
|
71
|
+
|
|
72
|
+
# Pass API key directly
|
|
73
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -k YOUR_API_KEY
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
positional arguments:
|
|
80
|
+
urls one or more Google Maps direction URLs
|
|
81
|
+
|
|
82
|
+
options:
|
|
83
|
+
-k, --api-key KEY Google Maps API key (or set GOOGLE_MAPS_API_KEY env var)
|
|
84
|
+
-o, --output FILE output GPX filename (auto-generated if omitted)
|
|
85
|
+
-m, --mode MODE travel mode: driving, walking, bicycling, transit, motorcycle
|
|
86
|
+
-s, --shortest fetch alternatives and pick the shortest by distance
|
|
87
|
+
-h, --help show help
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
1. Resolves shortened Google Maps URLs
|
|
93
|
+
2. Parses origin, destination, waypoints, and dragged via-points from the URL
|
|
94
|
+
3. Calls Google Directions API (or Routes API for motorcycle mode)
|
|
95
|
+
4. Decodes polylines into GPS coordinates
|
|
96
|
+
5. Generates a GPX 1.1 file with track points and waypoint markers
|
|
97
|
+
|
|
98
|
+
## Publishing to PyPI
|
|
99
|
+
|
|
100
|
+
One-time setup:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Install build tools
|
|
104
|
+
pip install build twine
|
|
105
|
+
|
|
106
|
+
# Create a PyPI account at https://pypi.org/account/register/
|
|
107
|
+
# Generate an API token at https://pypi.org/manage/account/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To publish:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Build the package
|
|
114
|
+
python -m build
|
|
115
|
+
|
|
116
|
+
# Upload to PyPI
|
|
117
|
+
twine upload dist/*
|
|
118
|
+
|
|
119
|
+
# You'll be prompted for credentials:
|
|
120
|
+
# Username: __token__
|
|
121
|
+
# Password: pypi-YOUR_API_TOKEN
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
After publishing, anyone can install with:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install gmaps2gpx
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gmaps2gpx - Convert Google Maps direction URLs to GPX files.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
gmaps2gpx <google_maps_url> [options]
|
|
6
|
+
|
|
7
|
+
Setup:
|
|
8
|
+
pipx install .
|
|
9
|
+
export GOOGLE_MAPS_API_KEY="your_key_here"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
import argparse
|
|
16
|
+
import requests
|
|
17
|
+
from urllib.parse import urlparse, unquote
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# 1. URL Parsing
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def resolve_short_url(url: str) -> str:
|
|
26
|
+
"""Resolve shortened Google Maps URLs (goo.gl, maps.app.goo.gl)."""
|
|
27
|
+
if "goo.gl" in url or "maps.app" in url:
|
|
28
|
+
resp = requests.head(url, allow_redirects=True, timeout=10)
|
|
29
|
+
return resp.url
|
|
30
|
+
return url
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_google_maps_url(url: str) -> dict:
|
|
34
|
+
"""
|
|
35
|
+
Extract origin, destination, and waypoints from a Google Maps directions URL.
|
|
36
|
+
|
|
37
|
+
Supports:
|
|
38
|
+
- https://www.google.com/maps/dir/Place+A/Place+B/Place+C
|
|
39
|
+
- https://www.google.com/maps/dir/lat,lng/lat,lng
|
|
40
|
+
- https://maps.app.goo.gl/xxxxx (shortened)
|
|
41
|
+
- Dragged via-points embedded in the data= parameter
|
|
42
|
+
"""
|
|
43
|
+
url = resolve_short_url(url)
|
|
44
|
+
parsed = urlparse(url)
|
|
45
|
+
# Split on raw path BEFORE unquoting — %2F in place names stays intact
|
|
46
|
+
path = parsed.path
|
|
47
|
+
|
|
48
|
+
dir_match = re.search(r'/maps/dir/(.+?)(?:/@|$|\?)', path)
|
|
49
|
+
if not dir_match:
|
|
50
|
+
dir_match = re.search(r'/maps/dir/(.+)', path)
|
|
51
|
+
|
|
52
|
+
if not dir_match:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Could not parse directions from this URL.\n"
|
|
55
|
+
"Make sure it's a Google Maps directions link (contains '/maps/dir/')."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
segments = [s.strip() for s in dir_match.group(1).split('/') if s.strip()]
|
|
59
|
+
|
|
60
|
+
clean_segments = []
|
|
61
|
+
for seg in segments:
|
|
62
|
+
if seg.startswith('@'):
|
|
63
|
+
break
|
|
64
|
+
seg = re.split(r'@', seg)[0].strip()
|
|
65
|
+
if seg:
|
|
66
|
+
clean_segments.append(unquote(seg))
|
|
67
|
+
|
|
68
|
+
if len(clean_segments) < 2:
|
|
69
|
+
raise ValueError("Need at least an origin and destination. Found: " + str(clean_segments))
|
|
70
|
+
|
|
71
|
+
# Extract dragged via-points from the data= parameter.
|
|
72
|
+
# Google Maps encodes them as: !3mN!1m2!1d<lng>!2d<lat>
|
|
73
|
+
via_waypoints = []
|
|
74
|
+
data_match = re.search(r'data=([^&]+)', url)
|
|
75
|
+
if data_match:
|
|
76
|
+
data_str = unquote(data_match.group(1))
|
|
77
|
+
via_points = re.findall(r'!3m\d+!1m2!1d([\d.]+)!2d([\d.]+)', data_str)
|
|
78
|
+
for lng, lat in via_points:
|
|
79
|
+
via_waypoints.append(f"{lat},{lng}")
|
|
80
|
+
|
|
81
|
+
path_waypoints = clean_segments[1:-1] if len(clean_segments) > 2 else []
|
|
82
|
+
all_waypoints = path_waypoints + via_waypoints
|
|
83
|
+
|
|
84
|
+
result = {
|
|
85
|
+
"origin": clean_segments[0],
|
|
86
|
+
"destination": clean_segments[-1],
|
|
87
|
+
"waypoints": all_waypoints
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
print(f" Origin: {result['origin']}")
|
|
91
|
+
print(f" Destination: {result['destination']}")
|
|
92
|
+
if result['waypoints']:
|
|
93
|
+
print(f" Waypoints: {', '.join(result['waypoints'])}")
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# 2. Google Directions API
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def get_directions(origin: str, destination: str, waypoints: list, api_key: str,
|
|
103
|
+
mode: str = "driving", alternatives: bool = False) -> dict:
|
|
104
|
+
"""Call Google Directions API and return the response."""
|
|
105
|
+
if mode == "motorcycle":
|
|
106
|
+
return _get_directions_routes_api(origin, destination, waypoints, api_key, alternatives)
|
|
107
|
+
|
|
108
|
+
params = {
|
|
109
|
+
"origin": origin,
|
|
110
|
+
"destination": destination,
|
|
111
|
+
"mode": mode,
|
|
112
|
+
"key": api_key,
|
|
113
|
+
}
|
|
114
|
+
if waypoints:
|
|
115
|
+
params["waypoints"] = "|".join(waypoints)
|
|
116
|
+
if alternatives:
|
|
117
|
+
params["alternatives"] = "true"
|
|
118
|
+
|
|
119
|
+
resp = requests.get(
|
|
120
|
+
"https://maps.googleapis.com/maps/api/directions/json",
|
|
121
|
+
params=params,
|
|
122
|
+
timeout=15
|
|
123
|
+
)
|
|
124
|
+
resp.raise_for_status()
|
|
125
|
+
data = resp.json()
|
|
126
|
+
|
|
127
|
+
if data["status"] != "OK":
|
|
128
|
+
raise RuntimeError(f"Directions API error: {data['status']} — {data.get('error_message', '')}")
|
|
129
|
+
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _geocode(place: str, api_key: str) -> dict:
|
|
134
|
+
"""Geocode a place name or pass through lat,lng coordinates."""
|
|
135
|
+
if re.match(r'^-?\d+\.?\d*,-?\d+\.?\d*$', place.strip()):
|
|
136
|
+
lat, lng = place.strip().split(',')
|
|
137
|
+
return {"lat": float(lat), "lng": float(lng)}
|
|
138
|
+
|
|
139
|
+
resp = requests.get(
|
|
140
|
+
"https://maps.googleapis.com/maps/api/geocode/json",
|
|
141
|
+
params={"address": place, "key": api_key},
|
|
142
|
+
timeout=10
|
|
143
|
+
)
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
data = resp.json()
|
|
146
|
+
if data["status"] != "OK" or not data["results"]:
|
|
147
|
+
raise RuntimeError(f"Geocode failed for '{place}': {data['status']}")
|
|
148
|
+
return data["results"][0]["geometry"]["location"]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_directions_routes_api(origin: str, destination: str, waypoints: list,
|
|
152
|
+
api_key: str, alternatives: bool) -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Use Google Routes API (v2) for TWO_WHEELER mode.
|
|
155
|
+
Returns data in the same format as the legacy Directions API.
|
|
156
|
+
"""
|
|
157
|
+
print(" (using Routes API for motorcycle/two-wheeler mode)")
|
|
158
|
+
|
|
159
|
+
origin_loc = _geocode(origin, api_key)
|
|
160
|
+
dest_loc = _geocode(destination, api_key)
|
|
161
|
+
|
|
162
|
+
body = {
|
|
163
|
+
"origin": {"location": {"latLng": {"latitude": origin_loc["lat"], "longitude": origin_loc["lng"]}}},
|
|
164
|
+
"destination": {"location": {"latLng": {"latitude": dest_loc["lat"], "longitude": dest_loc["lng"]}}},
|
|
165
|
+
"travelMode": "TWO_WHEELER",
|
|
166
|
+
"computeAlternativeRoutes": alternatives,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if waypoints:
|
|
170
|
+
intermediates = []
|
|
171
|
+
for wp in waypoints:
|
|
172
|
+
loc = _geocode(wp, api_key)
|
|
173
|
+
intermediates.append({"location": {"latLng": {"latitude": loc["lat"], "longitude": loc["lng"]}}})
|
|
174
|
+
body["intermediates"] = intermediates
|
|
175
|
+
|
|
176
|
+
headers = {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"X-Goog-Api-Key": api_key,
|
|
179
|
+
"X-Goog-FieldMask": "routes.legs.steps.polyline,routes.legs.distanceMeters,routes.legs.duration,routes.legs.startLocation,routes.legs.endLocation,routes.description"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
resp = requests.post(
|
|
183
|
+
"https://routes.googleapis.com/directions/v2:computeRoutes",
|
|
184
|
+
json=body,
|
|
185
|
+
headers=headers,
|
|
186
|
+
timeout=15
|
|
187
|
+
)
|
|
188
|
+
resp.raise_for_status()
|
|
189
|
+
data = resp.json()
|
|
190
|
+
|
|
191
|
+
if not data.get("routes"):
|
|
192
|
+
raise RuntimeError("Routes API returned no routes")
|
|
193
|
+
|
|
194
|
+
# Convert Routes API response to legacy Directions API format
|
|
195
|
+
converted = {"routes": []}
|
|
196
|
+
for route in data["routes"]:
|
|
197
|
+
legs = []
|
|
198
|
+
for leg in route["legs"]:
|
|
199
|
+
steps = []
|
|
200
|
+
for step in leg.get("steps", []):
|
|
201
|
+
poly = step.get("polyline", {}).get("encodedPolyline", "")
|
|
202
|
+
steps.append({"polyline": {"points": poly}})
|
|
203
|
+
|
|
204
|
+
dur_str = leg.get("duration", "0s")
|
|
205
|
+
dur_secs = int(dur_str.rstrip("s")) if isinstance(dur_str, str) else 0
|
|
206
|
+
|
|
207
|
+
start = leg.get("startLocation", {}).get("latLng", {})
|
|
208
|
+
end = leg.get("endLocation", {}).get("latLng", {})
|
|
209
|
+
|
|
210
|
+
legs.append({
|
|
211
|
+
"distance": {"value": leg.get("distanceMeters", 0), "text": f"{leg.get('distanceMeters', 0)/1000:.1f} km"},
|
|
212
|
+
"duration": {"value": dur_secs, "text": f"{dur_secs//60} mins"},
|
|
213
|
+
"start_location": {"lat": start.get("latitude", 0), "lng": start.get("longitude", 0)},
|
|
214
|
+
"end_location": {"lat": end.get("latitude", 0), "lng": end.get("longitude", 0)},
|
|
215
|
+
"steps": steps,
|
|
216
|
+
})
|
|
217
|
+
converted["routes"].append({
|
|
218
|
+
"legs": legs,
|
|
219
|
+
"summary": route.get("description", ""),
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return converted
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# 3. Polyline Decoding
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def decode_polyline(encoded: str) -> list[tuple[float, float]]:
|
|
230
|
+
"""Decode a Google encoded polyline into (lat, lng) tuples."""
|
|
231
|
+
points = []
|
|
232
|
+
index = 0
|
|
233
|
+
lat = 0
|
|
234
|
+
lng = 0
|
|
235
|
+
|
|
236
|
+
while index < len(encoded):
|
|
237
|
+
for attr in ('lat', 'lng'):
|
|
238
|
+
shift = 0
|
|
239
|
+
result = 0
|
|
240
|
+
while True:
|
|
241
|
+
b = ord(encoded[index]) - 63
|
|
242
|
+
index += 1
|
|
243
|
+
result |= (b & 0x1F) << shift
|
|
244
|
+
shift += 5
|
|
245
|
+
if b < 0x20:
|
|
246
|
+
break
|
|
247
|
+
delta = ~(result >> 1) if (result & 1) else (result >> 1)
|
|
248
|
+
if attr == 'lat':
|
|
249
|
+
lat += delta
|
|
250
|
+
else:
|
|
251
|
+
lng += delta
|
|
252
|
+
|
|
253
|
+
points.append((lat / 1e5, lng / 1e5))
|
|
254
|
+
|
|
255
|
+
return points
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# 4. GPX Generation
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def escape_xml(text: str) -> str:
|
|
263
|
+
"""Escape special XML characters."""
|
|
264
|
+
return (text
|
|
265
|
+
.replace("&", "&")
|
|
266
|
+
.replace("<", "<")
|
|
267
|
+
.replace(">", ">")
|
|
268
|
+
.replace('"', """)
|
|
269
|
+
.replace("'", "'"))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def build_gpx(points: list[tuple[float, float]], route_name: str = "Google Maps Route",
|
|
273
|
+
legs: list = None) -> str:
|
|
274
|
+
"""Build a GPX 1.1 XML string from (lat, lng) points."""
|
|
275
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
276
|
+
|
|
277
|
+
lines = [
|
|
278
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
279
|
+
'<gpx version="1.1" creator="gmaps2gpx"',
|
|
280
|
+
' xmlns="http://www.topografix.com/GPX/1/1"',
|
|
281
|
+
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
|
|
282
|
+
' xsi:schemaLocation="http://www.topografix.com/GPX/1/1',
|
|
283
|
+
' http://www.topografix.com/GPX/1/1/gpx.xsd">',
|
|
284
|
+
' <metadata>',
|
|
285
|
+
f' <name>{escape_xml(route_name)}</name>',
|
|
286
|
+
f' <time>{now}</time>',
|
|
287
|
+
' </metadata>',
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
if legs:
|
|
291
|
+
start = legs[0]["start_location"]
|
|
292
|
+
start_name = legs[0].get("start_address", "Start")
|
|
293
|
+
lines.append(f' <wpt lat="{start["lat"]}" lon="{start["lng"]}">')
|
|
294
|
+
lines.append(f' <name>{escape_xml(start_name)}</name>')
|
|
295
|
+
lines.append(' </wpt>')
|
|
296
|
+
|
|
297
|
+
for i, leg in enumerate(legs):
|
|
298
|
+
end = leg["end_location"]
|
|
299
|
+
end_name = leg.get("end_address", f"Stop {i+1}")
|
|
300
|
+
lines.append(f' <wpt lat="{end["lat"]}" lon="{end["lng"]}">')
|
|
301
|
+
lines.append(f' <name>{escape_xml(end_name)}</name>')
|
|
302
|
+
lines.append(' </wpt>')
|
|
303
|
+
|
|
304
|
+
lines.append(' <trk>')
|
|
305
|
+
lines.append(f' <name>{escape_xml(route_name)}</name>')
|
|
306
|
+
lines.append(' <trkseg>')
|
|
307
|
+
|
|
308
|
+
for lat, lng in points:
|
|
309
|
+
lines.append(f' <trkpt lat="{lat:.6f}" lon="{lng:.6f}"></trkpt>')
|
|
310
|
+
|
|
311
|
+
lines.append(' </trkseg>')
|
|
312
|
+
lines.append(' </trk>')
|
|
313
|
+
lines.append('</gpx>')
|
|
314
|
+
|
|
315
|
+
return "\n".join(lines)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
# 5. Main Pipeline
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
def convert_url_to_gpx(url: str, api_key: str, output_path: str = None,
|
|
323
|
+
mode: str = "driving", shortest: bool = False) -> str:
|
|
324
|
+
"""Full pipeline: Google Maps URL -> GPX file."""
|
|
325
|
+
print(f"\n{'='*60}")
|
|
326
|
+
print(f"Converting: {url}")
|
|
327
|
+
print(f"{'='*60}")
|
|
328
|
+
|
|
329
|
+
print("\n[1/4] Parsing Google Maps URL...")
|
|
330
|
+
route = parse_google_maps_url(url)
|
|
331
|
+
|
|
332
|
+
print("\n[2/4] Fetching directions from Google API...")
|
|
333
|
+
data = get_directions(
|
|
334
|
+
origin=route["origin"],
|
|
335
|
+
destination=route["destination"],
|
|
336
|
+
waypoints=route["waypoints"],
|
|
337
|
+
api_key=api_key,
|
|
338
|
+
mode=mode,
|
|
339
|
+
alternatives=shortest
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
routes = data["routes"]
|
|
343
|
+
if shortest and len(routes) > 1:
|
|
344
|
+
print(f"\n Found {len(routes)} alternative routes:")
|
|
345
|
+
for i, r in enumerate(routes):
|
|
346
|
+
d = sum(leg["distance"]["value"] for leg in r["legs"])
|
|
347
|
+
t = sum(leg["duration"]["value"] for leg in r["legs"])
|
|
348
|
+
summary = r.get("summary", "N/A")
|
|
349
|
+
print(f" {i+1}. {summary}: {d/1000:.1f} km, {t//3600}h {(t%3600)//60}m")
|
|
350
|
+
chosen_idx = min(range(len(routes)),
|
|
351
|
+
key=lambda i: sum(leg["distance"]["value"] for leg in routes[i]["legs"]))
|
|
352
|
+
chosen = routes[chosen_idx]
|
|
353
|
+
print(f" -> Picking route {chosen_idx+1} (shortest)")
|
|
354
|
+
else:
|
|
355
|
+
chosen = routes[0]
|
|
356
|
+
|
|
357
|
+
print("\n[3/4] Decoding route polylines...")
|
|
358
|
+
all_points = []
|
|
359
|
+
legs = []
|
|
360
|
+
total_distance = 0
|
|
361
|
+
total_duration = 0
|
|
362
|
+
|
|
363
|
+
for leg in chosen["legs"]:
|
|
364
|
+
legs.append(leg)
|
|
365
|
+
total_distance += leg["distance"]["value"]
|
|
366
|
+
total_duration += leg["duration"]["value"]
|
|
367
|
+
for step in leg["steps"]:
|
|
368
|
+
encoded = step["polyline"]["points"]
|
|
369
|
+
points = decode_polyline(encoded)
|
|
370
|
+
all_points.extend(points)
|
|
371
|
+
|
|
372
|
+
print(f" Total points: {len(all_points)}")
|
|
373
|
+
print(f" Total distance: {total_distance / 1000:.1f} km ({total_distance / 1609.34:.1f} mi)")
|
|
374
|
+
print(f" Est. duration: {total_duration // 3600}h {(total_duration % 3600) // 60}m")
|
|
375
|
+
|
|
376
|
+
print("\n[4/4] Generating GPX file...")
|
|
377
|
+
route_name = f"{route['origin']} to {route['destination']}"
|
|
378
|
+
gpx_content = build_gpx(all_points, route_name=route_name, legs=legs)
|
|
379
|
+
|
|
380
|
+
if not output_path:
|
|
381
|
+
safe_name = re.sub(r'[^\w\-]', '_', f"{route['origin']}_to_{route['destination']}")
|
|
382
|
+
safe_name = re.sub(r'_+', '_', safe_name)[:80]
|
|
383
|
+
output_path = f"{safe_name}.gpx"
|
|
384
|
+
|
|
385
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
386
|
+
f.write(gpx_content)
|
|
387
|
+
|
|
388
|
+
print(f"\n Saved: {output_path}")
|
|
389
|
+
return output_path
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# CLI
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
def main():
|
|
397
|
+
parser = argparse.ArgumentParser(
|
|
398
|
+
description="Convert Google Maps direction URLs to GPX files.",
|
|
399
|
+
epilog=(
|
|
400
|
+
"Examples:\n"
|
|
401
|
+
" gmaps2gpx 'https://maps.app.goo.gl/abc123'\n"
|
|
402
|
+
" gmaps2gpx 'https://www.google.com/maps/dir/Mumbai/Goa' -o ride.gpx\n"
|
|
403
|
+
" gmaps2gpx URL1 URL2 URL3 # batch convert\n"
|
|
404
|
+
),
|
|
405
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
406
|
+
)
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"urls",
|
|
409
|
+
nargs="+",
|
|
410
|
+
help="one or more Google Maps direction URLs"
|
|
411
|
+
)
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"-k", "--api-key",
|
|
414
|
+
default=os.environ.get("GOOGLE_MAPS_API_KEY"),
|
|
415
|
+
help="Google Maps API key (or set GOOGLE_MAPS_API_KEY env var)"
|
|
416
|
+
)
|
|
417
|
+
parser.add_argument(
|
|
418
|
+
"-o", "--output",
|
|
419
|
+
help="output GPX filename (auto-generated if omitted, single URL only)"
|
|
420
|
+
)
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"-m", "--mode",
|
|
423
|
+
choices=["driving", "walking", "bicycling", "transit", "motorcycle"],
|
|
424
|
+
default="driving",
|
|
425
|
+
help="travel mode (default: driving). 'motorcycle' uses Google Routes API TWO_WHEELER mode"
|
|
426
|
+
)
|
|
427
|
+
parser.add_argument(
|
|
428
|
+
"-s", "--shortest",
|
|
429
|
+
action="store_true",
|
|
430
|
+
help="fetch alternative routes and pick the shortest by distance"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
args = parser.parse_args()
|
|
434
|
+
|
|
435
|
+
if not args.api_key:
|
|
436
|
+
print("Error: No API key provided.")
|
|
437
|
+
print(" Set via env: export GOOGLE_MAPS_API_KEY='your_key'")
|
|
438
|
+
print(" Or pass flag: gmaps2gpx --api-key YOUR_KEY <url>")
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
|
|
441
|
+
output_files = []
|
|
442
|
+
for url in args.urls:
|
|
443
|
+
out = args.output if (args.output and len(args.urls) == 1) else None
|
|
444
|
+
try:
|
|
445
|
+
path = convert_url_to_gpx(url, args.api_key, output_path=out,
|
|
446
|
+
mode=args.mode, shortest=args.shortest)
|
|
447
|
+
output_files.append(path)
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"\n Failed: {e}")
|
|
450
|
+
|
|
451
|
+
if output_files:
|
|
452
|
+
print(f"\n{'='*60}")
|
|
453
|
+
print(f"Done! Generated {len(output_files)} GPX file(s):")
|
|
454
|
+
for f in output_files:
|
|
455
|
+
print(f" - {f}")
|
|
456
|
+
print(f"{'='*60}")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gmaps2gpx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Convert Google Maps direction URLs to GPX files. Supports shortened URLs, dragged routes, alternative routes, and motorcycle (two-wheeler) mode.
|
|
5
|
+
Author-email: Prajwal P <prajwalp@finarb.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/prajwalp/gmaps2gpx
|
|
8
|
+
Project-URL: Issues, https://github.com/prajwalp/gmaps2gpx/issues
|
|
9
|
+
Keywords: gpx,google-maps,converter,gps,motorcycle,two-wheeler,directions
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: requests>=2.28
|
|
24
|
+
|
|
25
|
+
# gmaps2gpx
|
|
26
|
+
|
|
27
|
+
Convert Google Maps direction URLs to GPX files with a single command.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Shortened URLs** - Paste `maps.app.goo.gl` links directly
|
|
32
|
+
- **Dragged routes** - Preserves via-points when you drag a route on Google Maps
|
|
33
|
+
- **Motorcycle mode** - Two-wheeler routing via Google Routes API (great for India/SE Asia)
|
|
34
|
+
- **Alternative routes** - `--shortest` picks the shortest route by distance
|
|
35
|
+
- **Batch convert** - Pass multiple URLs at once
|
|
36
|
+
- **All travel modes** - driving, walking, bicycling, transit, motorcycle
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# From PyPI
|
|
42
|
+
pip install gmaps2gpx
|
|
43
|
+
|
|
44
|
+
# Or with pipx (recommended - isolated install)
|
|
45
|
+
pipx install gmaps2gpx
|
|
46
|
+
|
|
47
|
+
# From source
|
|
48
|
+
git clone https://github.com/prajwalp/gmaps2gpx.git
|
|
49
|
+
cd gmaps2gpx
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Setup
|
|
54
|
+
|
|
55
|
+
You need a Google Maps API key with **Directions API** enabled. For motorcycle mode, also enable the **Routes API**.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Set it once (add to your .bashrc/.zshrc)
|
|
59
|
+
export GOOGLE_MAPS_API_KEY="your_key_here"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or pass it each time with `-k YOUR_KEY`.
|
|
63
|
+
|
|
64
|
+
### Getting an API key
|
|
65
|
+
|
|
66
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
|
67
|
+
2. Create a project (or select existing)
|
|
68
|
+
3. Enable **Directions API** (and **Routes API** for motorcycle mode)
|
|
69
|
+
4. Create an API key under Credentials
|
|
70
|
+
5. (Optional) Restrict the key to Directions API + Routes API
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Basic - paste any Google Maps directions link
|
|
76
|
+
gmaps2gpx 'https://maps.app.goo.gl/Mz9Q47fcBMHLa2HB7'
|
|
77
|
+
|
|
78
|
+
# Save to specific file
|
|
79
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -o ride.gpx
|
|
80
|
+
|
|
81
|
+
# Motorcycle / two-wheeler mode (uses Google Routes API)
|
|
82
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m motorcycle -o ride.gpx
|
|
83
|
+
|
|
84
|
+
# Pick the shortest route from alternatives
|
|
85
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' --shortest
|
|
86
|
+
|
|
87
|
+
# Full Google Maps URL works too
|
|
88
|
+
gmaps2gpx 'https://www.google.com/maps/dir/Mumbai/Goa' -o mumbai_goa.gpx
|
|
89
|
+
|
|
90
|
+
# Walking directions
|
|
91
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -m walking
|
|
92
|
+
|
|
93
|
+
# Batch convert multiple routes
|
|
94
|
+
gmaps2gpx URL1 URL2 URL3
|
|
95
|
+
|
|
96
|
+
# Pass API key directly
|
|
97
|
+
gmaps2gpx 'https://maps.app.goo.gl/abc123' -k YOUR_API_KEY
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Options
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
positional arguments:
|
|
104
|
+
urls one or more Google Maps direction URLs
|
|
105
|
+
|
|
106
|
+
options:
|
|
107
|
+
-k, --api-key KEY Google Maps API key (or set GOOGLE_MAPS_API_KEY env var)
|
|
108
|
+
-o, --output FILE output GPX filename (auto-generated if omitted)
|
|
109
|
+
-m, --mode MODE travel mode: driving, walking, bicycling, transit, motorcycle
|
|
110
|
+
-s, --shortest fetch alternatives and pick the shortest by distance
|
|
111
|
+
-h, --help show help
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
1. Resolves shortened Google Maps URLs
|
|
117
|
+
2. Parses origin, destination, waypoints, and dragged via-points from the URL
|
|
118
|
+
3. Calls Google Directions API (or Routes API for motorcycle mode)
|
|
119
|
+
4. Decodes polylines into GPS coordinates
|
|
120
|
+
5. Generates a GPX 1.1 file with track points and waypoint markers
|
|
121
|
+
|
|
122
|
+
## Publishing to PyPI
|
|
123
|
+
|
|
124
|
+
One-time setup:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Install build tools
|
|
128
|
+
pip install build twine
|
|
129
|
+
|
|
130
|
+
# Create a PyPI account at https://pypi.org/account/register/
|
|
131
|
+
# Generate an API token at https://pypi.org/manage/account/
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
To publish:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Build the package
|
|
138
|
+
python -m build
|
|
139
|
+
|
|
140
|
+
# Upload to PyPI
|
|
141
|
+
twine upload dist/*
|
|
142
|
+
|
|
143
|
+
# You'll be prompted for credentials:
|
|
144
|
+
# Username: __token__
|
|
145
|
+
# Password: pypi-YOUR_API_TOKEN
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
After publishing, anyone can install with:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install gmaps2gpx
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
gmaps2gpx/__init__.py
|
|
4
|
+
gmaps2gpx/cli.py
|
|
5
|
+
gmaps2gpx.egg-info/PKG-INFO
|
|
6
|
+
gmaps2gpx.egg-info/SOURCES.txt
|
|
7
|
+
gmaps2gpx.egg-info/dependency_links.txt
|
|
8
|
+
gmaps2gpx.egg-info/entry_points.txt
|
|
9
|
+
gmaps2gpx.egg-info/requires.txt
|
|
10
|
+
gmaps2gpx.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gmaps2gpx
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gmaps2gpx"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Convert Google Maps direction URLs to GPX files. Supports shortened URLs, dragged routes, alternative routes, and motorcycle (two-wheeler) mode."
|
|
9
|
+
authors = [{ name = "Prajwal P", email = "prajwalp@finarb.ai" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = ["requests>=2.28"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
26
|
+
]
|
|
27
|
+
keywords = ["gpx", "google-maps", "converter", "gps", "motorcycle", "two-wheeler", "directions"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/prajwalp/gmaps2gpx"
|
|
31
|
+
Issues = "https://github.com/prajwalp/gmaps2gpx/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
gmaps2gpx = "gmaps2gpx.cli:main"
|