POI Types Editor tool has been added. Updates and fixes. Project structure has been changed. README updated.

This commit is contained in:
LudvvigB
2025-04-11 23:09:34 +03:00
parent b42df18f68
commit c23cb42a13
7 changed files with 532 additions and 204 deletions

View File

@@ -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
View 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
View 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()

View File

@@ -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
View File

256
src/gpx2cnx.py Normal file
View 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
View 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()