POI Types Editor tool has been added. Updates and fixes. Project structure has been changed. README updated.
This commit is contained in:
47
README.md
47
README.md
@@ -1,13 +1,13 @@
|
||||
# GPXtoCNXConverter
|
||||
|
||||
This is a Python script for converting .GPX files to .CNX files. It allows you to export your tracks and waypoints (POIs). The script supports bulk conversion of multiple .GPX files.
|
||||
This is a Python app for converting .GPX files to .CNX files. It allows you to export your tracks and waypoints (POI's). It also supports bulk conversion of multiple .GPX files. The app includes a tool for editing POI types, allowing appropriate icons to be used for POI's on the bike computer.
|
||||
|
||||
* CNX is a GIS data file format used by iGPSPORT GPS sport devices.
|
||||
* GPX is an XML based format for GPS tracks.
|
||||
|
||||
## Data
|
||||
|
||||
The script exports the following GPX data to the CNX file:
|
||||
The GPX2CNX tool in the app exports the following GPX data to the CNX file:
|
||||
|
||||
Track name from GPX tags.
|
||||
|
||||
@@ -16,26 +16,61 @@ Trackpoints:
|
||||
* Longitude
|
||||
* Elevation
|
||||
|
||||
Waypoints (POIs):
|
||||
Waypoints (POI's):
|
||||
* Latitude
|
||||
* Longitude
|
||||
* Name
|
||||
|
||||
The script also calculates the following data based on the coordinates (features of the format and devices where it is used):
|
||||
|
||||
The app also calculates the following data based on the coordinates (features of the format and devices where it is used):
|
||||
* Distance
|
||||
* Ascent
|
||||
* Descent
|
||||
* Trackpoint count
|
||||
* Waypoint count
|
||||
|
||||
The POI TYPES EDITOR tool in the app allows you to assign POI's to 23 supported icon types:
|
||||
1. Waypoint
|
||||
2. Sprint Point
|
||||
3. HC Climb
|
||||
4. Level 1 Climb
|
||||
5. Level 2 Climb
|
||||
6. Level 3 Climb
|
||||
7. Level 4 Climb
|
||||
8. Supply Point
|
||||
9. Garbage recycle area
|
||||
10. Restroom
|
||||
11. Service Point
|
||||
12. Medical Aid Station
|
||||
13. Equipment Area
|
||||
14. Shop
|
||||
15. Meeting Point
|
||||
16. Viewing Platform
|
||||
17. Instagram-Worthy Location
|
||||
18. Tunnel
|
||||
19. Valley
|
||||
10. Dangerous Road
|
||||
21. Sharp Turn
|
||||
22. Steep Slope
|
||||
23. Intersection
|
||||
|
||||
## Run & Troubleshoot
|
||||
|
||||
This uses built-in Python libraries. To run on Python for Linux and older versions of Python for Windows, you may need to install tkinter module for Windows and python3-tk for Linux.
|
||||
|
||||
Nuitka is used to build the .exe file in the releases. Python applications packaged to .exe with various tools often trigger antivirus alerts.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the gnx2cnx.py script or gnx2cnx.exe (from the release) to start the GUI. Browse and open your files. The converted files will be placed in the same directory as the original files, in a subfolder called 'cnx_routes'.
|
||||
Run the main.py script or gnx2cnx.exe (from the release) to start the GUI. Select the tool in the app for converting or changing POI types.
|
||||
|
||||
#### GPX2CNX Tool
|
||||
|
||||
Browse and open your files. The converted files will be placed in the same directory as the original files, in a subfolder called 'cnx_routes'.
|
||||
|
||||
#### POI TYPES EDITOR Tool
|
||||
|
||||
Select the desired CNX file. All POI's from the track will be loaded with names and types. For each point, you can select the desired type from the dropdown menu. When you're done, click "Save All".
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
23
config/poi_types_list.txt
Normal file
23
config/poi_types_list.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
0, Waypoint
|
||||
1, Sprint Point
|
||||
2, HC Climb
|
||||
3, Level 1 Climb
|
||||
4, Level 2 Climb
|
||||
5, Level 3 Climb
|
||||
6, Level 4 Climb
|
||||
7, Supply Point
|
||||
8, Garbage recycle area
|
||||
9, Restroom
|
||||
10, Service Point
|
||||
11, Medical Aid Station
|
||||
12, Equipment Area
|
||||
13, Shop
|
||||
14, Meeting Point
|
||||
15, Viewing Platform
|
||||
16, Instagram-Worthy Location
|
||||
17, Tunnel
|
||||
18, Valley
|
||||
19, Dangerous Road
|
||||
20, Sharp Turn
|
||||
21, Steep Slope
|
||||
22, Intersection
|
||||
8
main.py
Normal file
8
main.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# main.py
|
||||
import tkinter as tk
|
||||
import src.gpx2cnx as G2C
|
||||
|
||||
# Run App
|
||||
root = tk.Tk()
|
||||
G2C.GPX2CNX(root)
|
||||
root.mainloop()
|
||||
@@ -1,198 +0,0 @@
|
||||
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('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
|
||||
f.write(xml_string[xml_string.find('<Route'):])
|
||||
|
||||
results.append(f"-- {filename} -> 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()
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
256
src/gpx2cnx.py
Normal file
256
src/gpx2cnx.py
Normal file
@@ -0,0 +1,256 @@
|
||||
# gpx2cnx.py
|
||||
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
|
||||
import src.poi_types_editor as PE
|
||||
|
||||
|
||||
getcontext().prec = 28
|
||||
|
||||
|
||||
class GPX2CNX:
|
||||
# Creating GUI and calling button functions
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
root.title("GPXtoCNX Converter")
|
||||
|
||||
self.status_label = tk.Label(root, text="", width=30)
|
||||
self.status_label.grid(row=4, column=1, columnspan=3, sticky="ew", padx=25, pady=5)
|
||||
|
||||
self.button1 = tk.Button(root, text="GPX -> CNX", command=lambda:[self.status_label.config(text=""), self.select_files()])
|
||||
self.button1.grid(row=1, column=1, columnspan=1, sticky="ew", padx=25, pady=5)
|
||||
|
||||
self.button2 = tk.Button(root, text="POI TYPES EDITOR", command=self.poi_editor)
|
||||
self.button2.grid(row=2, column=1, columnspan=1, sticky="ew", padx=25, pady=5)
|
||||
|
||||
self.button3 = tk.Button(root, text="Log", command=self.log_window)
|
||||
self.button3.grid(row=3, column=1, columnspan=1, sticky="ew", padx=25, pady=5)
|
||||
|
||||
root.grid_columnconfigure(1, weight=1)
|
||||
|
||||
def logging(self):
|
||||
log = "gpx2cnx.py log"
|
||||
for result in self.results:
|
||||
log = log + "\n" + result
|
||||
try:
|
||||
with open("log.txt", "w", encoding="utf8") as logtxt:
|
||||
logtxt.write(log + "\n")
|
||||
except Exception as e:
|
||||
self.status_label.config(text="* Error write log *")
|
||||
if self.rstat == 1:
|
||||
self.status_label.config(text="* DONE *")
|
||||
else:
|
||||
self.status_label.config(text="* ERROR *")
|
||||
|
||||
# Run POI Types Editor
|
||||
def poi_editor(self):
|
||||
window = tk.Toplevel()
|
||||
PE.POIEditor(window)
|
||||
window.grab_set()
|
||||
|
||||
# log_window
|
||||
def log_window(self):
|
||||
textinfo = ""
|
||||
flogout = 0
|
||||
try:
|
||||
with open("log.txt", "r") as logtxt:
|
||||
textinfo = logtxt.read()
|
||||
flogout = 1
|
||||
except FileNotFoundError:
|
||||
self.status_label.config(text="* File not found *")
|
||||
except Exception as e:
|
||||
self.status_label.config(text="* Error read log *")
|
||||
|
||||
if flogout == 1:
|
||||
loginfo = tk.Toplevel()
|
||||
loginfo.title("Log")
|
||||
info_area = scrolledtext.ScrolledText(loginfo, width=80, height=20)
|
||||
info_area.pack(pady=10)
|
||||
loginfo.grab_set()
|
||||
|
||||
info_area.insert(tk.END, textinfo)
|
||||
|
||||
# Select files and start conversion
|
||||
def select_files(self):
|
||||
file_paths = filedialog.askopenfilenames(filetypes=[("GPX files", "*.gpx")])
|
||||
if file_paths:
|
||||
self.convert_gpx2cnx(list(file_paths))
|
||||
|
||||
def prettify(self, 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(self):
|
||||
R = 6371 * 1000 # Earth radius in meters
|
||||
dlat = Decimal(str(math.radians(self.lat2 - self.lat1)))
|
||||
dlon = Decimal(str(math.radians(self.lon2 - self.lon1)))
|
||||
dele = Decimal(str(self.ele2 - self.ele1))
|
||||
a = Decimal(str((math.sin(dlat/2) * math.sin(dlat/2)) +
|
||||
(math.cos(math.radians(self.lat1)) * math.cos(math.radians(self.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(self, 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)
|
||||
|
||||
log = "gpx2cnx.py log"
|
||||
self.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)):
|
||||
self.lat1 = track_points[i - 1]['lat']
|
||||
self.lon1 = track_points[i - 1]['lon']
|
||||
self.ele1 = track_points[i - 1]['ele']
|
||||
self.lat2 = track_points[i]['lat']
|
||||
self.lon2 = track_points[i]['lon']
|
||||
self.ele2 = track_points[i]['ele']
|
||||
|
||||
distance += self.calc_distance()
|
||||
ele_diff = self.ele2 - self.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 = self.prettify(route)
|
||||
with codecs.open(output_filename, "w", encoding='utf-8-sig') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
|
||||
f.write(xml_string[xml_string.find('<Route'):])
|
||||
|
||||
self.results.append(f"-- {gpx_file} -> SUCCESS")
|
||||
self.rstat = 1
|
||||
except Exception as e:
|
||||
self.results.append(f"ERROR - file {gpx_file}: {e}")
|
||||
self.rstat = 0
|
||||
self.logging()
|
||||
self.logging()
|
||||
return self.results
|
||||
|
||||
|
||||
# Run app
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = GPX2CNX(root)
|
||||
root.mainloop()
|
||||
204
src/poi_types_editor.py
Normal file
204
src/poi_types_editor.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# poi_types_editor.py
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import filedialog
|
||||
from xml.etree import ElementTree as ET
|
||||
import codecs
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
CONFIG_PATH = ROOT_DIR / 'config'
|
||||
TYPES_LIST_FILE = 'poi_types_list.txt'
|
||||
LIST_FILEPATH = CONFIG_PATH / TYPES_LIST_FILE
|
||||
|
||||
|
||||
# Loads POI types from a file as a dictionary {code: name}
|
||||
def load_poi_types(filepath=LIST_FILEPATH):
|
||||
poi_types = {}
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts = line.split(',')
|
||||
if len(parts) == 2:
|
||||
type_code = parts[0].strip()
|
||||
type_name = parts[1].strip()
|
||||
poi_types[type_code] = type_name
|
||||
except FileNotFoundError:
|
||||
poi_types = {"0": "Waypoint"} # default value
|
||||
return poi_types
|
||||
|
||||
|
||||
class POIEditor:
|
||||
# Creating GUI and calling button functions
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
root.title("POI TYPES EDITOR")
|
||||
|
||||
self.poi_types = load_poi_types()
|
||||
self.type_list_for_combo = list(self.poi_types.values())
|
||||
self.points = []
|
||||
self.current_index = 0
|
||||
self.xml_filepath = None
|
||||
self.xml_tree = None
|
||||
self.xml_root = None
|
||||
|
||||
self.name_label = tk.Label(root, text="POI Name:")
|
||||
self.name_label.grid(row=0, column=0, sticky="w", padx=5, pady=5)
|
||||
|
||||
self.name_text = tk.StringVar()
|
||||
self.name_entry = ttk.Entry(root, textvariable=self.name_text, state="readonly", width=50)
|
||||
self.name_entry.grid(row=0, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
|
||||
self.type_label = ttk.Label(root, text="POI Type:")
|
||||
self.type_label.grid(row=1, column=0, sticky="w", padx=5, pady=5)
|
||||
|
||||
self.type_combo = ttk.Combobox(root, values=self.type_list_for_combo, state="readonly")
|
||||
self.type_combo.grid(row=1, column=1,columnspan=3, sticky="ew", padx=5, pady=5)
|
||||
self.type_combo.bind("<<ComboboxSelected>>", self.update_current_poi_type)
|
||||
|
||||
self.prev_button = ttk.Button(root, text="Prev", command=self.show_previous_poi, state="disabled")
|
||||
self.prev_button.grid(row=2, column=0, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.next_button = ttk.Button(root, text="Next", command=self.show_next_poi, state="disabled")
|
||||
self.next_button.grid(row=2, column=1, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.counter_label = ttk.Label(root, text="0/0")
|
||||
self.counter_label.grid(row=2, column=2, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.save_button = ttk.Button(root, text="Save All", command=self.save_all_changes, state="disabled")
|
||||
self.save_button.grid(row=3, column=0, columnspan=3, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.load_button = ttk.Button(root, text="Load CNX", command=self.load_cnx)
|
||||
self.load_button.grid(row=4, column=0, columnspan=3, sticky="ew", padx=5, pady=10)
|
||||
|
||||
self.status_label = ttk.Label(root, text="")
|
||||
self.status_label.grid(row=5, column=0, columnspan=3, sticky="e", padx=5, pady=5)
|
||||
|
||||
root.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# Loads file and parses the data
|
||||
def load_cnx(self):
|
||||
filepath = filedialog.askopenfilename(
|
||||
defaultextension=".cnx",
|
||||
filetypes=[("CNX files", "*.cnx")],
|
||||
title="Select CNX file"
|
||||
)
|
||||
if filepath:
|
||||
try:
|
||||
self.xml_tree = ET.parse(filepath)
|
||||
self.xml_root = self.xml_tree.getroot()
|
||||
points_element = self.xml_root.find("Points")
|
||||
if points_element is not None:
|
||||
self.points = []
|
||||
for point_element in points_element.findall("Point"):
|
||||
lat = point_element.findtext("Lat")
|
||||
lng = point_element.findtext("Lng")
|
||||
type_val = point_element.findtext("Type")
|
||||
descr = point_element.findtext("Descr")
|
||||
self.points.append({"lat": lat, "lng": lng, "type": type_val, "descr": descr, "element": point_element})
|
||||
if self.points:
|
||||
self.current_index = 0
|
||||
self.show_current_poi()
|
||||
self.prev_button["state"] = "disabled"
|
||||
self.next_button["state"] = "normal" if len(self.points) > 1 else "disabled"
|
||||
self.save_button["state"] = "normal"
|
||||
self.status_label.config(text=f"* Loaded {len(self.points)} POI's *")
|
||||
self.xml_filepath = filepath
|
||||
else:
|
||||
self.status_label.config(text="* No POI's found in the file *")
|
||||
self.clear_ui()
|
||||
self.xml_filepath = None
|
||||
else:
|
||||
self.status_label.config(text="* The <Points> element not found in the file *")
|
||||
self.clear_ui()
|
||||
xml_filepath = None
|
||||
except ET.ParseError:
|
||||
self.status_label.config(text="* CNX file parsing error *")
|
||||
self.clear_ui()
|
||||
self.xml_filepath = None
|
||||
except FileNotFoundError:
|
||||
self.status_label.config(text="* File not found *")
|
||||
self.clear_ui()
|
||||
self.xml_filepath = None
|
||||
|
||||
# Displays the current POI in the interface
|
||||
def show_current_poi(self):
|
||||
if self.points:
|
||||
current_poi = self.points[self.current_index]
|
||||
self.name_text.set(current_poi["descr"])
|
||||
poi_type = self.poi_types.get(current_poi["type"], f"Unknown type ({current_poi['type']})")
|
||||
self.type_combo.set(poi_type)
|
||||
self.counter_label.config(text=f"{self.current_index + 1}/{len(self.points)}")
|
||||
|
||||
# Next POI
|
||||
def show_next_poi(self):
|
||||
if self.current_index < len(self.points) - 1:
|
||||
self.current_index += 1
|
||||
self.show_current_poi()
|
||||
self.prev_button["state"] = "normal"
|
||||
if self.current_index == len(self.points) - 1:
|
||||
self.next_button["state"] = "disabled"
|
||||
|
||||
# Prev POI
|
||||
def show_previous_poi(self):
|
||||
if self.current_index > 0:
|
||||
self.current_index -= 1
|
||||
self.show_current_poi()
|
||||
self.next_button["state"] = "normal"
|
||||
if self.current_index == 0:
|
||||
self.prev_button["state"] = "disabled"
|
||||
|
||||
# Returns the type code by type name
|
||||
def get_type_code(self, type_name):
|
||||
for code, name in self.poi_types.items():
|
||||
if name == type_name:
|
||||
return code
|
||||
return "0" # default value
|
||||
|
||||
# Updates the type of the current POI in the self.points list when a value is selected in the Combobox
|
||||
def update_current_poi_type(self, event):
|
||||
if self.points and 0 <= self.current_index < len(self.points):
|
||||
selected_type_name = self.type_combo.get()
|
||||
selected_type_code = self.get_type_code(selected_type_name)
|
||||
self.points[self.current_index]["type"] = selected_type_code
|
||||
|
||||
# Saves all changes to the original file
|
||||
def save_all_changes(self):
|
||||
if self.points and self.xml_filepath and self.xml_tree:
|
||||
points_element = self.xml_root.find("Points")
|
||||
if points_element is not None:
|
||||
for i, poi in enumerate(self.points):
|
||||
point_element = points_element[i]
|
||||
point_element.find("Type").text = poi["type"]
|
||||
try:
|
||||
xml_string = ET.tostring(self.xml_root, encoding='unicode')
|
||||
with codecs.open(self.xml_filepath, "w", encoding='utf-8-sig') as f:
|
||||
f.write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n')
|
||||
f.write(xml_string[xml_string.find('<Route'):])
|
||||
self.status_label.config(text="* SAVED *")
|
||||
except Exception as e:
|
||||
self.status_label.config(text=f"* File save error: {e} *")
|
||||
else:
|
||||
self.status_label.config(text="* Error: <Points> element not found for saving *")
|
||||
elif not self.xml_filepath:
|
||||
self.status_label.config(text="* CNX file not loaded *")
|
||||
elif not self.xml_tree:
|
||||
self.status_label.config(text="* Error working with the XML tree *")
|
||||
|
||||
# Clearing UI elements
|
||||
def clear_ui(self):
|
||||
self.name_text.set("")
|
||||
self.type_combo.set("")
|
||||
self.prev_button["state"] = "disabled"
|
||||
self.next_button["state"] = "disabled"
|
||||
self.save_button["state"] = "disabled"
|
||||
|
||||
|
||||
# Run app
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = POIEditor(root)
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user