From eb0cd4eb2c2ce671110ac66210ab7dcf507ee268 Mon Sep 17 00:00:00 2001 From: LudvvigB Date: Fri, 7 Mar 2025 00:22:49 +0300 Subject: [PATCH] Added Python script --- scripts/gpx2cnx.py | 198 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 scripts/gpx2cnx.py diff --git a/scripts/gpx2cnx.py b/scripts/gpx2cnx.py new file mode 100644 index 0000000..8e5fc6d --- /dev/null +++ b/scripts/gpx2cnx.py @@ -0,0 +1,198 @@ +import os +import math +import codecs +import xml.etree.ElementTree as ET +from xml.dom import minidom +import tkinter as tk +from tkinter import filedialog, scrolledtext +import decimal +from decimal import Decimal, getcontext + + +getcontext().prec = 28 + +# Select files and start conversion +def select_files(): + file_paths = filedialog.askopenfilenames(filetypes=[("GPX files", "*.gpx")]) + if file_paths: + info_area.delete(1.0, tk.END) + results = convert_gpx2cnx(list(file_paths)) + for result in results: + info_area.insert(tk.END, result + "\n") + + +def prettify(elem): + rough_string = ET.tostring(elem, 'utf-8') + reparsed = minidom.parseString(rough_string) + return reparsed.toprettyxml(indent=" ") + + +# Calculate 3D distance between coordinates +def calc_distance(lat1, lon1, ele1, lat2, lon2, ele2): + R = 6371 * 1000 # Earth radius in meters + dlat = Decimal(str(math.radians(lat2 - lat1))) + dlon = Decimal(str(math.radians(lon2 - lon1))) + dele = Decimal(str(ele2 - ele1)) + a = Decimal(str((math.sin(dlat/2) * math.sin(dlat/2)) + \ + (math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * \ + math.sin(dlon/2) * math.sin(dlon/2)))) + b = Decimal(str(2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)))) + h_distance = R * b + distance = (h_distance**2 + dele**2).sqrt() # 3D distance + return distance + + +# Converts a list of GPX files to CNX xml format +def convert_gpx2cnx(gpx_files): + output_dir= os.path.join(os.path.normpath(os.path.split(gpx_files[0])[0]),'cnx_routes') + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + results = [] + for gpx_file in gpx_files: + try: + tree = ET.parse(gpx_file) + root = tree.getroot() + + # Extracting waypoints + waypoints = [] + for wpt in root.findall('{http://www.topografix.com/GPX/1/1}wpt'): + lat = Decimal(wpt.get('lat')) + lon = Decimal(wpt.get('lon')) + name = wpt.find('{http://www.topografix.com/GPX/1/1}name').text \ + if wpt.find('{http://www.topografix.com/GPX/1/1}name') is not None else "" + + waypoints.append({'lat': lat, 'lon': lon, 'name': name}) + + # Extracting track points + track_points = [] + track = root.find('{http://www.topografix.com/GPX/1/1}trk') + if track is not None: + track_name = track.find('{http://www.topografix.com/GPX/1/1}name').text \ + if track.find('{http://www.topografix.com/GPX/1/1}name') is not None else "Unknown" + trkseg = track.find('{http://www.topografix.com/GPX/1/1}trkseg') + if trkseg is not None: + for trkpt in trkseg.findall('{http://www.topografix.com/GPX/1/1}trkpt'): + lat = Decimal(trkpt.get('lat')) + lon = Decimal(trkpt.get('lon')) + ele = Decimal(trkpt.find('{http://www.topografix.com/GPX/1/1}ele').text) \ + if trkpt.find('{http://www.topografix.com/GPX/1/1}ele') is not None else Decimal('0.0') + + track_points.append({'lat': lat, 'lon': lon, 'ele': ele}) + + # Calc Distance, Ascent, Descent + distance = Decimal('0.0') + ascent = Decimal('0.0') + descent = Decimal('0.0') + if len(track_points) > 1: + for i in range(1, len(track_points)): + lat1 = track_points[i - 1]['lat'] + lon1 = track_points[i - 1]['lon'] + ele1 = track_points[i - 1]['ele'] + lat2 = track_points[i]['lat'] + lon2 = track_points[i]['lon'] + ele2 = track_points[i]['ele'] + + distance += calc_distance(lat1, lon1, ele1, lat2, lon2, ele2) + ele_diff = ele2 - ele1 + + if ele_diff > 0: + ascent += ele_diff + else: + descent += ele_diff + + distance = distance.quantize(Decimal('1.00'), decimal.ROUND_HALF_UP) + ascent = ascent.quantize(Decimal('1.00'), decimal.ROUND_HALF_UP) + descent = descent.quantize(Decimal('1.00'), decimal.ROUND_HALF_UP) + + # Calc Track - Relative Coordinates + if track_points: + first_lat = track_points[0]['lat'] + first_lon = track_points[0]['lon'] + first_ele = (track_points[0]['ele'] * 100).quantize(Decimal('1'), decimal.ROUND_HALF_UP) + + relative_points = [f"{first_lat},{first_lon},{first_ele}"] # First Point - Absolute Coordinates + + first_diffs = [] + for i in range(1, len(track_points)): # Calc first diffs + lat1 = track_points[i-1]['lat'] + lon1 = track_points[i-1]['lon'] + lat2 = track_points[i]['lat'] + lon2 = track_points[i]['lon'] + + lat_diff = (lat2 - lat1) * 10000000 + lon_diff = (lon2 - lon1) * 10000000 + ele_diff = track_points[i]['ele'] * 100 - track_points[i-1]['ele'] * 100 + + first_diffs.append((lat_diff, lon_diff, ele_diff)) + + if i == 1: # Second Point + lat_diff = lat_diff.quantize(Decimal('1'), decimal.ROUND_HALF_UP) + lon_diff = lon_diff.quantize(Decimal('1'), decimal.ROUND_HALF_UP) + ele_diff = ele_diff.quantize(Decimal('1'), decimal.ROUND_HALF_UP) + + relative_points.append(f"{lat_diff},{lon_diff},{ele_diff}") + + for i in range(1, len(first_diffs)): # Calc second diffs + lat_diff = (first_diffs[i][0] - first_diffs[i-1][0]).quantize(Decimal('1'), decimal.ROUND_HALF_UP) + lon_diff = (first_diffs[i][1] - first_diffs[i-1][1]).quantize(Decimal('1'), decimal.ROUND_HALF_UP) + ele_diff = (first_diffs[i][2]).quantize(Decimal('1'), decimal.ROUND_HALF_UP) + + relative_points.append(f"{lat_diff},{lon_diff},{ele_diff}") + + # Create XML + route = ET.Element('Route') + ET.SubElement(route, 'Id').text = track_name # Use track name as Id + ET.SubElement(route, 'Distance').text = f'{distance}' + + duration = ET.SubElement(route, 'Duration') + duration.text = '\n ' + + ET.SubElement(route, 'Ascent').text = f'{ascent}' + ET.SubElement(route, 'Descent').text = f'{descent}' + ET.SubElement(route, 'Encode').text = '2' + ET.SubElement(route, 'Lang').text = '0' + ET.SubElement(route, 'TracksCount').text = str(len(track_points)) + + if relative_points: + tracks_str = ';'.join(relative_points) + ET.SubElement(route, 'Tracks').text = tracks_str + else: + ET.SubElement(route, 'Tracks').text = "" + + ET.SubElement(route, 'Navs') + ET.SubElement(route, 'PointsCount').text = str(len(waypoints)) + + points = ET.SubElement(route, 'Points') + for wpt in waypoints: + point = ET.SubElement(points, 'Point') + ET.SubElement(point, 'Lat').text = str(wpt['lat']) + ET.SubElement(point, 'Lng').text = str(wpt['lon']) + ET.SubElement(point, 'Type').text = '0' # Type 0 - point marker without differentiation by purpose + ET.SubElement(point, 'Descr').text = wpt['name'] + + filename = os.path.splitext(os.path.basename(gpx_file))[0] + trim_filename = filename[:18] + output_filename = os.path.join(output_dir, f"route_{trim_filename}.cnx") + xml_string = prettify(route) + with codecs.open(output_filename, "w", encoding='utf-8-sig') as f: + f.write('\n') + f.write(xml_string[xml_string.find(' SUCCESS") + except Exception as e: + results.append(f"ERROR - file {gpx_file}: {e}") + return results + + +# Create GUI +root = tk.Tk() +root.title("GPXtoCNX Converter") + +button = tk.Button(root, text="Browse files", command=select_files) +button.pack(pady=10) + +info_area = scrolledtext.ScrolledText(root, width=80, height=20) +info_area.pack(pady=10) + +root.mainloop() \ No newline at end of file