|
import xml.etree.ElementTree as etree |
|
import datetime |
|
from geopy.distance import geodesic |
|
import re |
|
import argparse |
|
|
|
def extract(filename, tz, threshold, verbose): |
|
ns = {"kml": "http://www.opengis.net/kml/2.2"} |
|
|
|
outfile = filename.split(".")[0] + ".csv" |
|
tree = etree.parse(filename) |
|
root = tree.getroot() |
|
|
|
track1 = root.find("kml:Document/kml:Folder[@id='Tracks']/kml:Folder[@id='track 1']", ns) |
|
points = track1.find("kml:Folder[@id='track 1 points']", ns) |
|
placemarks = points.findall("kml:Placemark", ns) |
|
placemarks_count = len(placemarks) |
|
last_elevation = None |
|
last_coordinates = None |
|
last_timestamp = None |
|
first_timestamp = None |
|
distance = 0.0 |
|
stop_time = 0 |
|
elev_gain = 0 |
|
elev_loss = 0 |
|
|
|
if verbose: |
|
print(f"Processing {placemarks_count} points") |
|
|
|
with open(outfile, 'w') as csv_file: |
|
csv_file.write("TimeStamp,Elapsed Time (s),Distance (km),Speed (km/h),Elevation (m),Elev. gain (m),Elev. loss (m),Stop time (min)\n") |
|
|
|
for placemark in placemarks: |
|
timestamp = datetime.datetime.strptime(placemark.find("kml:TimeStamp/kml:when", ns).text, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone(datetime.timedelta(hours=0))) |
|
lng_lat_elev = placemark.find("kml:Point/kml:coordinates", ns).text.split(",") |
|
longitude = float(lng_lat_elev[0]) |
|
latitude = float(lng_lat_elev[1]) |
|
elevation = float(lng_lat_elev[2]) |
|
|
|
if first_timestamp is None: |
|
first_timestamp = timestamp |
|
|
|
if last_coordinates is not None and last_timestamp is not None: |
|
delta_km = geodesic(last_coordinates, (latitude, longitude)).km |
|
time_spent = (timestamp - last_timestamp).total_seconds() |
|
speed = 3600 * delta_km / time_spent |
|
if speed < threshold: |
|
stop_time = stop_time + time_spent |
|
else: |
|
distance = distance + delta_km |
|
if last_elevation is not None: |
|
delta_elev = elevation - last_elevation |
|
if delta_elev >= 0: |
|
elev_gain += delta_elev |
|
else: |
|
elev_loss += delta_elev |
|
elapsed_time = timestamp - first_timestamp |
|
formatted_stop_time = (datetime.datetime(1,1,1) + datetime.timedelta(seconds=stop_time)).strftime("%H:%M:%S") |
|
formatted_elapsed_time = (datetime.datetime(1,1,1) + elapsed_time).strftime("%H:%M:%S") |
|
formatted_timestamp = timestamp.astimezone(datetime.timezone(datetime.timedelta(hours=tz))).strftime("%Y-%m-%d %H:%M:%S") |
|
line = "{},{},{:.3f},{:.1f},{},{:.0f},{:.0f},{}".format(formatted_timestamp, formatted_elapsed_time, distance, speed, elevation, elev_gain, elev_loss, formatted_stop_time) |
|
if verbose: |
|
print(line) |
|
|
|
with open(outfile, 'a+') as csv_file: |
|
csv_file.write(f"{line}\n") |
|
|
|
last_coordinates = (latitude, longitude) |
|
last_timestamp = timestamp |
|
last_elevation = elevation |
|
|
|
run_time = elapsed_time - datetime.timedelta(seconds=stop_time) |
|
avg_speed = distance / run_time.total_seconds() * 3600 |
|
formatted_run_time = (datetime.datetime(1,1,1) + run_time).strftime("%H:%M:%S") |
|
line = "{},{},,{},,{:.2f}km,,+{:.0f}m{:.0f}m,{:.2f}km/h".format(formatted_elapsed_time, formatted_run_time, formatted_stop_time, distance, elev_gain, elev_loss, avg_speed) |
|
if verbose: |
|
print(line) |
|
|
|
with open(outfile, 'a+') as csv_file: |
|
csv_file.write("\ntime,run time,,stop time,,distance,,elev. gain-loss,avg. speed\n") |
|
csv_file.write(f"{line}\n") |
|
|
|
print(f"File {outfile} created.") |
|
|
|
def get_options(): |
|
parser = argparse.ArgumentParser(prog='SpeedTable by ClemRz', description='Extracts a CSV speed, elevation and distance table from a KML file.') |
|
parser.add_argument('filename', help='The path to the KML file') |
|
parser.add_argument('-tz', '--timezone', dest='timezone', default='0', help='The destination timezone') |
|
parser.add_argument('-th', '--threshold', dest='threshold', default='3', help='The speed threshold in km/h') |
|
parser.add_argument('-v', '--verbose', dest='verbose', help='Enables verbose output', action='store_true') |
|
return parser.parse_args() |
|
|
|
def main(): |
|
options = get_options() |
|
extract(options.filename, float(options.timezone), float(options.threshold), options.verbose) |
|
|
|
if __name__ == '__main__': |
|
main() |