Skip to content

Instantly share code, notes, and snippets.

@hutattedonmyarm
Last active October 17, 2023 08:48
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save hutattedonmyarm/70ce9d690c47b43c4ea79c38df298826 to your computer and use it in GitHub Desktop.
Save hutattedonmyarm/70ce9d690c47b43c4ea79c38df298826 to your computer and use it in GitHub Desktop.
Rungap for iOS exports GPS data as JSON file in the free version. This script converts it to GPX. Tested with python3.6. Might need to install required modules. Simply place either the metadata and data json files, or the complete zip file in the same directoryas the script and run it. Warning: Does barely any error checking
import xml.etree.cElementTree as ElementTree
import json, pytz, zipfile, unicodedata, re
from datetime import datetime
from os import listdir
from os.path import isfile, join
import glob
def slugify(value):
"""
Normalizes string, converts to lowercase, removes non-alpha characters.
Slightly modified from https://github.com/django/django/blob/master/django/utils/text.py
"""
value = str(value)
try:
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
value = re.sub(r'[-\s]+', '-', value)
except:
print('Could not slugify filename, will use default one instead')
value = 'output'
return value
creator = "GapX"
version = "1.1"
zip_files = glob.glob("*.zip")
for zip_file in zip_files:
print("Unzipping " + zip_file)
zip_ref = zipfile.ZipFile(zip_file, 'r')
for info in zip_ref.infolist():
print(info.filename)
if info.filename.endswith("metadata.json") or info.filename.endswith("rungap.json") or info.filename.endswith("nike.json"):
zip_ref.extract(info)
zip_ref.close()
metadata_files = glob.glob("*metadata.json")
data_files = glob.glob("*rungap.json")
data_files += glob.glob("*nike.json")
print("Found metadata files")
print(metadata_files)
print("Found data files")
print(data_files)
if len(metadata_files) != len(data_files):
print("Error, number of metadata files does not match number of datafiles!")
exit(1)
for idx, metadata_file in enumerate(metadata_files):
print("=========")
print("Parsing " + metadata_file + " and " + data_files[idx])
metadata_data = json.load(open(metadata_file))
data = json.load(open(data_files[idx]))
root = ElementTree.Element("gpx")
root.set("xmlns","http://www.topografix.com/GPX/1/1")
root.set("creator", creator)
root.set("version", version)
metadata = ElementTree.SubElement(root, "metadata")
name = metadata_data["title"]
desc = metadata_data["description"]
print("Route name: " + name)
print("Route description: " + desc)
ElementTree.SubElement(metadata, "name").text = name
ElementTree.SubElement(metadata, "desc").text = desc
ElementTree.SubElement(metadata, "time").text = metadata_data["startTime"]["time"]
timezone = pytz.timezone(metadata_data["startTime"].get("timeZone", "UTC"))
source = metadata_data["source"] + " exported by Rungap for iOS, version " + metadata_data["appversion"]
track = ElementTree.SubElement(root, "trk")
ElementTree.SubElement(track, "name").text = name
ElementTree.SubElement(track, "desc").text = desc
ElementTree.SubElement(track, "src").text = source
segment = ElementTree.SubElement(track, "trkseg")
if "laps" in data:
print("Found " + str(len(data["laps"][0]["points"])) + " track points")
for point in data["laps"][0]["points"]:
if ("lat" in point and "lon" in point and "ele" in point and "time" in point):
trkpt = ElementTree.SubElement(segment, "trkpt", lat=str(point["lat"]), lon=str(point["lon"]))
ElementTree.SubElement(trkpt, "ele").text = str(point["ele"])
ElementTree.SubElement(trkpt, "time").text = datetime.fromtimestamp(point["time"], timezone).isoformat()
elif "laps" in metadata_data:
print("Found " + str(len(metadata_data["laps"])) + " track points")
for point in metadata_data["laps"]:
p = point.get("startLocation", {})
if ("lat" in p and "lon" in p and "startTime" in point):
#dt = datetime.datetime.strptime(d, "%Y-%m-%dT%H:%M:%SZ")
trkpt = ElementTree.SubElement(segment, "trkpt", lat=str(p["lat"]), lon=str(p["lon"]))
ElementTree.SubElement(trkpt, "time").text = point["startTime"]
gpx_filename = slugify(name + " - " + datetime.now().isoformat()) + ".gpx"
print("Writing " + gpx_filename)
tree = ElementTree.ElementTree(root)
tree.write(gpx_filename, "UTF-8", True)
@homunculus
Copy link

thanks for this. I tried this but i cannot get this to work. I think now the json files are not rungap and metadata but instead *nike.json and *metadata.json.
other than that "laps" is in metadata_data not data
after changing these, I still get stuck at "points" which cannot be found

Traceback (most recent call last):
File "C:\Users\Desktop\test\RungabGPX.py", line 72, in
print("Found " + str(len(metadata_data["laps"][0]["points"])) + " track points")
KeyError: 'points'

I tried deleting ["points"] and then it runs. it says it finds 6 track points. However strava reports that the files is empty when trying to import it. Indeed, checking the created file.. it does not have much in it.
any ideas?

@curlyjordi
Copy link

Thanks for the script. saved me 3.99$.
I got it to run with a few changes :
13: value = unicode(value) #from str
63: ElementTree.SubElement(metadata, "time").text = metadata_data["startTime"]["time"] #removed timezone
76: ElementTree.SubElement(trkpt, "time").text = datetime.fromtimestamp(point["time"]).isoformat() #removed timezone

@orr721
Copy link

orr721 commented Nov 17, 2019

Thanks, worked for me without any modifications. Python 3.7.5, RunGap for iOS 2.39.

@jcbigears
Copy link

jcbigears commented Dec 20, 2019

Hi.
I've tried both the original file as well as the modifications from curlyjordi. I have three files in a zip folder from my exported nike run via rungap. Two are JSON files and the other is a png of the map. I've unzipped the folder and put the script in the same folder. Running the script I get the error:

Found metadata files
['2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.metadata.json']
Found data files
[]
Error, number of metadata files does not match number of datafiles!

The other file is called "2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.nike.json"
Can anyone enlighten me please?
Thanks so much,
John

@hutattedonmyarm
Copy link
Author

The other file is called "2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.nike.json"

Oh, try renaming that to "2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.rungap.json" (replace the "nike" with "rungap"). I did not anticipate the field being named differently than rung's usual file names!

@jcbigears
Copy link

Thanks. That works better!
Using your original script I get

Found metadata files
['2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.metadata.json']
Found data files
['2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.rungap.json']

Parsing 2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.metadata.json and 2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.rungap.json
Route name: Friday Morning Run
Route description: Running 8.06 km in 47:37 min (5:55 min/km)
Traceback (most recent call last):
File "RungabGPX.py", line 63, in
timezone = pytz.timezone(metadata_data["startTime"]["timeZone"])
KeyError: 'timeZone'

I then tried the modification from @curlyjordi and got

Found metadata files
['2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.metadata.json']
Found data files
['2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.rungap.json']

Parsing 2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.metadata.json and 2019-12-20_05-17-13_nk_d0301bdd-5c63-4466-97f3-db5fa6bdfcce.rungap.json
Route name: Friday Morning Run
Route description: Running 8.06 km in 47:37 min (5:55 min/km)
Traceback (most recent call last):
File "RungabGPX.py", line 71, in
print("Found " + str(len(data["laps"][0]["points"])) + " track points")
KeyError: 'laps'

Absolutely appreciate your help here!
John

@hutattedonmyarm
Copy link
Author

Looks like rungap has changed their file format, give me a coupe minutes!

@jcbigears
Copy link

Looks like rungap has changed their file format, give me a coupe minutes!

Thank you! Would you like me to send you the files?

@hutattedonmyarm
Copy link
Author

hutattedonmyarm commented Dec 20, 2019

Looks like rungap has changed their file format, give me a coupe minutes!

Thank you! Would you like me to send you the files?

@jcbigears Yeah, that would help! The regular one still works, might be a difference with the nike files

@hutattedonmyarm
Copy link
Author

@jcbigears Alright, I updated the gist. Turns out the .nike files have a different format

@jcbigears
Copy link

jcbigears commented Dec 20, 2019

I tried.
I failed.
Sad face emoji.

I changed line 13: value = unicode(value)
and it worked perfectly!!!!!!
Thank you @huattedonmyarm!

@Vadimner
Copy link

Vadimner commented Mar 1, 2020

Hi! It's very nice script. Thank you for that. I've noticed that it doesn't pass heart rate data to .gpx. I've tried to make some changes on my own but got stuck. Problem is that number of gps (lat|lon|ele) points does not match number of heart rate points. And I don't know how to properly map it to each other. Can you add it to your script or give me some ideas how to do that?

@hutattedonmyarm
Copy link
Author

hutattedonmyarm commented Mar 1, 2020 via email

Copy link

ghost commented May 1, 2020

I'm a noob with this programming stuff.

The script first says it cannot find module pytz, if i remove that from the code:
When I run the script the newly created "__MACOSX" folder contains only blank folders with no content.

@hutattedonmyarm
Copy link
Author

The script first says it cannot find module pytz, if i remove that from the code

you'll need to install the dependencies. Run pip install pytz and try again with the original file

Copy link

ghost commented May 2, 2020

Hello

I have installed pytz but the newly created __MACOSX folder contains empty folders

@hutattedonmyarm
Copy link
Author

hutattedonmyarm commented May 2, 2020 via email

Copy link

ghost commented May 2, 2020

Unzipped it manually then placed every file into one big folder, worked perfectly, thanks

@hutattedonmyarm
Copy link
Author

Ah, I think I know what the issue was. Should work with the zip file now too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment