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.
@@ -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,3 @@
1
+ """gmaps2gpx - Convert Google Maps direction URLs to GPX files."""
2
+
3
+ __version__ = "1.0.0"
@@ -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("&", "&amp;")
266
+ .replace("<", "&lt;")
267
+ .replace(">", "&gt;")
268
+ .replace('"', "&quot;")
269
+ .replace("'", "&apos;"))
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,2 @@
1
+ [console_scripts]
2
+ gmaps2gpx = gmaps2gpx.cli:main
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+