From 80daa1eb52352a9002aecd05c4d6dc1b4e510228 Mon Sep 17 00:00:00 2001
From: Ptosiek <16878205+Ptosiek@users.noreply.github.com>
Date: Wed, 4 Oct 2023 13:06:11 -0700
Subject: [PATCH] fix loader_tcx to handle missing distance easier to
understand what's happening remove commented code that preloaded course more
pythonic code for loading course list move things around, rename a bit list
courses should not be in tcx_loader fix missing notes when inserting
start/end points
---
.gitignore | 1 +
modules/config.py | 42 +-
modules/{logger/loader_tcx.py => course.py} | 493 +-
modules/gui_pyqt.py | 2 +-
modules/loaders/__init__.py | 1 +
modules/loaders/tcx.py | 168 +
modules/logger_core.py | 8 +-
modules/pyqt/graph/pyqt_base_map.py | 3 +-
modules/pyqt/graph/pyqt_course_profile.py | 4 +-
modules/pyqt/graph/pyqt_map.py | 162 +-
modules/pyqt/menu/pyqt_course_menu_widget.py | 6 +-
modules/pyqt/pyqt_cuesheet_widget.py | 12 +-
modules/sensor/sensor_gps.py | 104 +-
tests/data/tcx/Mt_Angel_Abbey.tcx | 8986 ++++++++++++++++++
tests/test_loader.py | 12 +
15 files changed, 9538 insertions(+), 466 deletions(-)
rename modules/{logger/loader_tcx.py => course.py} (63%)
create mode 100644 modules/loaders/__init__.py
create mode 100644 modules/loaders/tcx.py
create mode 100644 tests/data/tcx/Mt_Angel_Abbey.tcx
create mode 100644 tests/test_loader.py
diff --git a/.gitignore b/.gitignore
index 0ae6237e..c47999a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
/log/
/screenshots/
/maptile/*
+/courses/.current
/courses/ridewithgps/*
/courses/*html*
/fonts/*
diff --git a/modules/config.py b/modules/config.py
index 535aa7f9..34d22c0e 100644
--- a/modules/config.py
+++ b/modules/config.py
@@ -7,6 +7,7 @@
import shutil
import traceback
import math
+from glob import glob
import numpy as np
import oyaml as yaml
@@ -93,7 +94,7 @@ class Config:
# courses
G_COURSE_DIR = "courses"
- G_COURSE_FILE_PATH = os.path.join(G_COURSE_DIR, "course.tcx")
+ G_COURSE_FILE_PATH = os.path.join(G_COURSE_DIR, ".current")
G_CUESHEET_DISPLAY_NUM = 3 # max: 5
G_CUESHEET_SCROLL = False
G_OBEXD_CMD = "/usr/libexec/bluetooth/obexd"
@@ -1327,3 +1328,42 @@ def get_lon_lat_from_tile_xy(z, x, y):
lat = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
return lon, lat
+
+ def get_courses(self):
+ dirs = sorted(
+ glob(os.path.join(self.G_COURSE_DIR, "*.tcx")),
+ key=lambda f: os.stat(f).st_mtime,
+ reverse=True,
+ )
+
+ # heavy: delayed updates required
+ # def get_course_info(c):
+ # pattern = {
+ # "name": re.compile(r"(?P[\s\S]*?)"),
+ # "distance_meters": re.compile(
+ # r"(?P[\s\S]*?)"
+ # ),
+ # # "track": re.compile(r''),
+ # # "altitude": re.compile(r'(?P[^<]*)'),
+ # }
+ # info = {}
+ # with open(c, "r", encoding="utf-8_sig") as f:
+ # tcx = f.read()
+ # match_name = pattern["name"].search(tcx)
+ # if match_name:
+ # info["name"] = match_name.group("text").strip()
+ #
+ # match_distance_meter = pattern["distance_meters"].search(tcx)
+ # if match_distance_meter:
+ # info["distance"] = float(match_distance_meter.group("text").strip())
+ # return info
+
+ return [
+ {
+ "path": f,
+ "name": os.path.basename(f),
+ # **get_course_info(f)
+ }
+ for f in dirs
+ if os.path.isfile(f) and f != self.G_COURSE_FILE_PATH
+ ]
diff --git a/modules/logger/loader_tcx.py b/modules/course.py
similarity index 63%
rename from modules/logger/loader_tcx.py
rename to modules/course.py
index 78669342..e935c684 100644
--- a/modules/logger/loader_tcx.py
+++ b/modules/course.py
@@ -1,15 +1,16 @@
import os
-import glob
import json
-import shutil
import re
+import shutil
from math import factorial
-from crdp import rdp
+import oyaml
import numpy as np
+from crdp import rdp
from logger import app_logger
+from modules.loaders import TcxLoader
from modules.utils.timer import Timer, log_timers
POLYLINE_DECODER = False
@@ -20,8 +21,39 @@
except ImportError:
pass
+LOADERS = {"tcx": TcxLoader}
+
+
+class CoursePoints:
+ name = None
+ type = None
+ altitude = None
+ distance = None
+ latitude = None
+ longitude = None
+ notes = None
+
+ def __init__(self):
+ self.reset()
+
+ @property
+ def is_set(self):
+ # https://developer.garmin.com/fit/file-types/course/
+ # no field is mandatory, but they will be zeroes/empty anyway so len will not be 0 is coursePoints are set
+ return bool(len(self.name))
+
+ def reset(self):
+ self.name = []
+ self.type = []
+ self.altitude = []
+ self.distance = []
+ self.latitude = []
+ self.longitude = []
+ self.notes = []
-class LoaderTcx:
+
+# we have mutable attributes but course is supposed to be a singleton anyway
+class Course:
config = None
# for course
@@ -31,22 +63,17 @@ class LoaderTcx:
latitude = np.array([])
longitude = np.array([])
+ course_points = None
+
+ # calculated
points_diff = np.array([])
azimuth = np.array([])
slope = np.array([])
slope_smoothing = np.array([])
colored_altitude = np.array([])
+ # [start_index, end_index, distance, average_grade, volume(=dist*average), cat]
climb_segment = []
- # for course points
- point_name = np.array([])
- point_latitude = np.array([])
- point_longitude = np.array([])
- point_type = np.array([])
- point_notes = np.array([])
- point_distance = np.array([])
- point_altitude = np.array([])
-
html_remove_pattern = [
re.compile(r"\
"),
re.compile(r"\<.+?\>"),
@@ -55,6 +82,16 @@ class LoaderTcx:
def __init__(self, config):
super().__init__()
self.config = config
+ self.course_points = CoursePoints()
+
+ def __str__(self):
+ return f"Course:\n" f"{oyaml.dump(self.info, allow_unicode=True)}\n"
+
+ @property
+ def is_set(self):
+ # we keep checking distance as it's how it was done in the original code,
+ # but we can load tcx file with no distance in it load (it gets populated as np.zeros in load)
+ return bool(len(self.distance))
def reset(self, delete_course_file=False, replace=False):
# for course
@@ -70,18 +107,10 @@ def reset(self, delete_course_file=False, replace=False):
self.slope = np.array([])
self.slope_smoothing = np.array([])
self.colored_altitude = np.array([])
- self.climb_segment = (
- []
- ) # [start_index, end_index, distance, average_grade, volume(=dist*average), cat]
-
- # for course points
- self.point_name = np.array([])
- self.point_latitude = np.array([])
- self.point_longitude = np.array([])
- self.point_type = np.array([])
- self.point_notes = np.array([])
- self.point_distance = np.array([])
- self.point_altitude = np.array([])
+ self.climb_segment = []
+
+ if self.course_points:
+ self.course_points.reset()
if delete_course_file:
if os.path.exists(self.config.G_COURSE_FILE_PATH):
@@ -89,19 +118,52 @@ def reset(self, delete_course_file=False, replace=False):
if not replace and self.config.G_THINGSBOARD_API["STATUS"]:
self.config.network.api.send_livetrack_course_reset()
- def load(self):
+ def load(self, file=None):
+ # if file is given, copy it to self.config.G_COURSE_FILE_PATH firsthand, we are loading a new course
+ if file:
+ _, ext = os.path.splitext(file)
+ shutil.copy2(file, self.config.G_COURSE_FILE_PATH)
+ if ext:
+ os.setxattr(
+ self.config.G_COURSE_FILE_PATH, "user.ext", ext[1:].encode()
+ )
+
self.reset()
timers = [
- Timer(auto_start=False, text="read_tcx : {0:.3f} sec"),
+ Timer(auto_start=False, text="read_file : {0:.3f} sec"),
Timer(auto_start=False, text="downsample : {0:.3f} sec"),
Timer(auto_start=False, text="calc_slope_smoothing: {0:.3f} sec"),
Timer(auto_start=False, text="modify_course_points: {0:.3f} sec"),
]
with timers[0]:
- self.read_tcx()
-
+ # get loader based on the extension
+ if os.path.exists(self.config.G_COURSE_FILE_PATH):
+ # get file extension in order to find the correct loader
+ # extension was set in custom attributes as the current course is always
+ # loaded from '.current'
+ try:
+ ext = os.getxattr(
+ self.config.G_COURSE_FILE_PATH, "user.ext"
+ ).decode()
+ if ext in LOADERS:
+ course_data, course_points_data = LOADERS[ext].load_file(
+ self.config.G_COURSE_FILE_PATH
+ )
+ if course_data:
+ for k, v in course_data.items():
+ setattr(self, k, v)
+ if course_points_data:
+ for k, v in course_points_data.items():
+ setattr(self.course_points, k, v)
+ else:
+ app_logger.warning(f".{ext} files are not handled")
+ except (AttributeError, OSError) as e:
+ app_logger.error(
+ f"Incorrect course file: {e}. Please reload the course and make sure your file"
+ f"has a proper extension set"
+ )
with timers[1]:
self.downsample()
@@ -111,9 +173,6 @@ def load(self):
with timers[3]:
self.modify_course_points()
- if not len(self.latitude):
- return
-
app_logger.info("[logger] Loading course:")
log_timers(timers, text_total="total : {0:.3f} sec")
@@ -158,199 +217,19 @@ async def search_route(self, x1, y1, x2, y2):
if self.config.G_THINGSBOARD_API["STATUS"]:
self.config.network.api.send_livetrack_course_load()
- def get_courses(self):
- dir_list = sorted(
- glob.glob(os.path.join(self.config.G_COURSE_DIR, "*.tcx")),
- key=lambda f: os.stat(f).st_mtime,
- reverse=True,
- )
- file_list = [
- f
- for f in dir_list
- if os.path.isfile(f) and f != self.config.G_COURSE_FILE_PATH
- ]
-
- courses = []
- for c in file_list:
- info = {
- "path": c,
- "name": os.path.basename(c),
- }
- # heavy: delayed updates required
- # pattern = {
- # "name": re.compile(r"(?P[\s\S]*?)"),
- # "distance_meters": re.compile(
- # r"(?P[\s\S]*?)"
- # ),
- # # "track": re.compile(r''),
- # # "altitude": re.compile(r'(?P[^<]*)'),
- # }
- # with open(c, "r", encoding="utf-8_sig") as f:
- # tcx = f.read()
- # match_name = pattern["name"].search(tcx)
- # if match_name:
- # info["name"] = match_name.group("text").strip()
- #
- # match_distance_meter = pattern["distance_meters"].search(tcx)
- # if match_distance_meter:
- # info["distance"] = float(match_distance_meter.group("text").strip())
-
- courses.append(info)
-
- return courses
-
def get_ridewithgps_privacycode(self, route_id):
privacy_code = None
filename = (
self.config.G_RIDEWITHGPS_API["URL_ROUTE_DOWNLOAD_DIR"]
+ "course-{route_id}.json"
).format(route_id=route_id)
+
with open(filename, "r") as json_file:
json_contents = json.load(json_file)
if "privacy_code" in json_contents["route"]:
privacy_code = json_contents["route"]["privacy_code"]
- return privacy_code
-
- def set_course(self, file):
- shutil.copy(file, self.config.G_COURSE_FILE_PATH)
- self.load()
- def read_tcx(self):
- if not os.path.exists(self.config.G_COURSE_FILE_PATH):
- return
- app_logger.info(f"loading {self.config.G_COURSE_FILE_PATH}")
-
- # read with regex
- pattern = {
- "name": re.compile(r"(?P[\s\S]*?)"),
- "distance_meters": re.compile(
- r"(?P[\s\S]*?)"
- ),
- "track": re.compile(r""),
- "latitude": re.compile(
- r"(?P[^<]*)"
- ),
- "longitude": re.compile(
- r"(?P[^<]*)"
- ),
- "altitude": re.compile(r"(?P[^<]*)"),
- "distance": re.compile(r"(?P[^<]*)"),
- "course_point": re.compile(r"(?P[\s\S]+)"),
- "course_name": re.compile(r"(?P[^<]*)"),
- "course_point_type": re.compile(r"(?P[^<]*)"),
- "course_notes": re.compile(r"(?P[^<]*)"),
- }
-
- with open(self.config.G_COURSE_FILE_PATH, "r", encoding="utf-8_sig") as f:
- tcx = f.read()
-
- match_name = pattern["name"].search(tcx)
- if match_name:
- self.info["Name"] = match_name.group("text").strip()
-
- match_distance_meter = pattern["distance_meters"].search(tcx)
- if match_distance_meter:
- self.info["DistanceMeters"] = round(
- float(match_distance_meter.group("text").strip()) / 1000, 1
- )
-
- match_track = pattern["track"].search(tcx)
- if match_track:
- track = match_track.group("text")
- self.latitude = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["latitude"].finditer(track)
- ]
- )
- self.longitude = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["longitude"].finditer(track)
- ]
- )
- self.altitude = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["altitude"].finditer(track)
- ]
- )
- self.distance = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["distance"].finditer(track)
- ]
- )
-
- match_course = pattern["course_point"].search(tcx)
- if match_course:
- course_point = match_course.group("text")
- self.point_name = [
- m.group("text").strip()
- for m in pattern["course_name"].finditer(course_point)
- ]
- self.point_latitude = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["latitude"].finditer(course_point)
- ]
- )
- self.point_longitude = np.array(
- [
- float(m.group("text").strip())
- for m in pattern["longitude"].finditer(course_point)
- ]
- )
- self.point_type = [
- m.group("text").strip()
- for m in pattern["course_point_type"].finditer(course_point)
- ]
- self.point_notes = [
- m.group("text").strip()
- for m in pattern["course_notes"].finditer(course_point)
- ]
-
- check_course = False
- if not (
- len(self.latitude)
- == len(self.longitude)
- == len(self.altitude)
- == len(self.distance)
- ):
- app_logger.error("Can not parse course")
- check_course = True
- if not (
- len(self.point_name)
- == len(self.point_latitude)
- == len(self.point_longitude)
- == len(self.point_type)
- ):
- app_logger.error("Can not parse course point")
- check_course = True
- if check_course:
- self.distance = np.array([])
- self.altitude = np.array([])
- self.latitude = np.array([])
- self.longitude = np.array([])
- self.point_name = np.array([])
- self.point_latitude = np.array([])
- self.point_longitude = np.array([])
- self.point_type = np.array([])
- return
-
- # delete 'Straight' of course points
- if len(self.point_type):
- ptype = np.array(self.point_type)
- not_straight_cond = np.where(ptype != "Straight", True, False)
- self.point_type = list(ptype[not_straight_cond])
- if len(self.point_name):
- self.point_name = list(np.array(self.point_name)[not_straight_cond])
- if len(self.point_latitude):
- self.point_latitude = np.array(self.point_latitude)[not_straight_cond]
- if len(self.point_longitude):
- self.point_longitude = np.array(self.point_longitude)[not_straight_cond]
- if len(self.point_notes):
- self.point_notes = list(np.array(self.point_notes)[not_straight_cond])
+ return privacy_code
async def get_google_route_from_mapstogpx(self, url):
json_routes = await self.config.network.api.get_google_route_from_mapstogpx(url)
@@ -361,76 +240,79 @@ async def get_google_route_from_mapstogpx(self, url):
self.latitude = np.array([p["lat"] for p in json_routes["points"]])
self.longitude = np.array([p["lng"] for p in json_routes["points"]])
- self.point_name = []
- self.point_latitude = []
- self.point_longitude = []
- self.point_distance = []
- self.point_type = []
- self.point_notes = []
+ point_name = []
+ point_latitude = []
+ point_longitude = []
+ point_distance = []
+ point_type = []
+ point_notes = []
- self.point_distance.append(0)
+ point_distance.append(0)
cp = [p for p in json_routes["points"] if len(p) > 2]
cp_n = len(cp) - 1
cp_i = -1
+
for p in cp:
cp_i += 1
# skip
if ("step" in p and p["step"] in ["straight", "merge", "keep"]) or (
"step" not in p and cp_i not in [0, cp_n]
):
- self.point_distance[-1] = round(p["dist"]["total"] / 1000, 1)
+ point_distance[-1] = round(p["dist"]["total"] / 1000, 1)
continue
- self.point_latitude.append(p["lat"])
- self.point_longitude.append(p["lng"])
+ point_latitude.append(p["lat"])
+ point_longitude.append(p["lng"])
if "dist" in p:
dist = round(p["dist"]["total"] / 1000, 1)
- self.point_distance.append(dist)
+ point_distance.append(dist)
turn_str = ""
+
if "step" in p:
turn_str = p["step"]
if turn_str[-4:] == "left":
turn_str = "Left"
elif turn_str[-5:] == "right":
turn_str = "Right"
- self.point_name.append(turn_str)
- self.point_type.append(turn_str)
+
+ point_name.append(turn_str)
+ point_type.append(turn_str)
text = ""
+
if "dir" in p:
text = self.remove_html_tag(p["dir"])
- self.point_notes.append(text)
- self.point_name[0] = "Start"
- self.point_name[-1] = "End"
+ point_notes.append(text)
+
+ point_name[0] = "Start"
+ point_name[-1] = "End"
# print(self.point_name)
# print(self.point_type)
# print(self.point_notes)
# print(self.point_distance)
- self.point_latitude = np.array(self.point_latitude)
- self.point_longitude = np.array(self.point_longitude)
- self.point_distance = np.array(self.point_distance)
+ self.course_points.name = point_name
+ self.course_points.type = point_type
+ self.course_points.notes = point_notes
+ self.course_points.latitude = np.array(point_latitude)
+ self.course_points.longitude = np.array(point_longitude)
+ self.course_points.distance = np.array(point_distance)
check_course = False
if not (len(self.latitude) == len(self.longitude)):
- print("ERROR parse course")
+ app_logger.warning("ERROR parse course")
check_course = True
if check_course:
self.latitude = np.array([])
self.longitude = np.array([])
- self.point_name = np.array([])
- self.point_latitude = np.array([])
- self.point_longitude = np.array([])
- self.point_distance = np.array([])
- self.point_type = np.array([])
- self.point_notes = []
+ self.course_points.reset()
return
async def get_google_route(self, x1, y1, x2, y2):
@@ -446,12 +328,7 @@ async def get_google_route(self, x1, y1, x2, y2):
# points = np.array(polyline.decode(json_routes["routes"][0]["overview_polyline"]["points"]))
points_detail = []
- self.point_name = []
- self.point_latitude = []
- self.point_longitude = []
- self.point_distance = []
- self.point_type = []
- self.point_notes = []
+ self.course_points.reset()
dist = 0
pre_dist = 0
@@ -480,19 +357,21 @@ async def get_google_route(self, x1, y1, x2, y2):
turn_str = "Left"
elif turn_str[-5:] == "right":
turn_str = "Right"
- self.point_type.append(turn_str)
- self.point_latitude.append(step["start_location"]["lat"])
- self.point_longitude.append(step["start_location"]["lng"])
- self.point_distance.append(dist)
- self.point_notes.append(self.remove_html_tag(step["html_instructions"]))
- self.point_name.append(turn_str)
+ self.course_points.type.append(turn_str)
+ self.course_points.latitude.append(step["start_location"]["lat"])
+ self.course_points.longitude.append(step["start_location"]["lng"])
+ self.course_points.distance.append(dist)
+ self.course_points.notes.append(
+ self.remove_html_tag(step["html_instructions"])
+ )
+ self.course_points.name.append(turn_str)
points_detail = np.array(points_detail)
self.latitude = np.array(points_detail)[:, 0]
self.longitude = np.array(points_detail)[:, 1]
- self.point_latitude = np.array(self.point_latitude)
- self.point_longitude = np.array(self.point_longitude)
- self.point_distance = np.array(self.point_distance)
+ self.course_points.latitude = np.array(self.course_points.latitude)
+ self.course_points.longitude = np.array(self.course_points.longitude)
+ self.course_points.distance = np.array(self.course_points.distance)
def remove_html_tag(self, text):
res = text.replace(" ", "")
@@ -580,6 +459,7 @@ def downsample(self):
app_logger.info(f"downsampling:{len_lat} -> {len(self.latitude)}")
+ # make route colors by slope for MapWidget, CourseProfileWidget
def calc_slope_smoothing(self):
# parameters
course_n = len(self.distance)
@@ -738,26 +618,26 @@ def calc_slope_smoothing(self):
self.colored_altitude = np.array(self.config.G_SLOPE_COLOR)[slope_smoothing_cat]
def modify_course_points(self):
- # make route colors by slope for MapWidget, CourseProfileWidget
+ course_points = self.course_points
- len_pnt_lat = len(self.point_latitude)
- len_pnt_dist = len(self.point_distance)
- len_pnt_alt = len(self.point_altitude)
+ len_pnt_lat = len(course_points.latitude)
+ len_pnt_dist = len(course_points.distance)
+ len_pnt_alt = len(course_points.altitude)
len_dist = len(self.distance)
len_alt = len(self.altitude)
# calculate course point distance
if not len_pnt_dist and len_dist:
- self.point_distance = np.empty(len_pnt_lat)
+ course_points.distance = np.empty(len_pnt_lat)
if not len_pnt_alt and len_alt:
- self.point_altitude = np.zeros(len_pnt_lat)
+ course_points.altitude = np.zeros(len_pnt_lat)
min_index = 0
for i in range(len_pnt_lat):
b_a_x = self.points_diff[0][min_index:]
b_a_y = self.points_diff[1][min_index:]
- lon_diff = self.point_longitude[i] - self.longitude[min_index:]
- lat_diff = self.point_latitude[i] - self.latitude[min_index:]
+ lon_diff = course_points.longitude[i] - self.longitude[min_index:]
+ lat_diff = course_points.latitude[i] - self.latitude[min_index:]
p_a_x = lon_diff[:-1]
p_a_y = lat_diff[:-1]
inner_p = (b_a_x * p_a_x + b_a_y * p_a_y) / self.points_diff_sum_of_squares[
@@ -784,7 +664,10 @@ def modify_course_points(self):
* inner_p[j]
)
dist_diff_h = self.config.get_dist_on_earth(
- h_lon, h_lat, self.point_longitude[i], self.point_latitude[i]
+ h_lon,
+ h_lat,
+ course_points.longitude[i],
+ course_points.latitude[i],
)
if (
@@ -822,47 +705,57 @@ def modify_course_points(self):
min_index = min_index + min_j
if not len_pnt_dist and len_dist:
- self.point_distance[i] = self.distance[min_index] + min_dist_delta
+ course_points.distance[i] = self.distance[min_index] + min_dist_delta
if not len_pnt_alt and len_alt:
- self.point_altitude[i] = self.altitude[min_index] + min_alt_delta
+ course_points.altitude[i] = self.altitude[min_index] + min_alt_delta
# add climb tops
# if len(self.climb_segment):
# min_index = 0
# for i in range(len(self.climb_segment)):
- # diff_dist = np.abs(self.point_distance - self.climb_segment[i]['course_point_distance'])
+ # diff_dist = np.abs(course_points.distance - self.climb_segment[i]['course_point_distance'])
# min_index = np.where(diff_dist == np.min(diff_dist))[0][0]+1
- # self.point_name.insert(min_index, "Top of Climb")
- # self.point_latitude = np.insert(self.point_latitude, min_index, self.climb_segment[i]['course_point_latitude'])
- # self.point_longitude = np.insert(self.point_longitude, min_index, self.climb_segment[i]['course_point_longitude'])
- # self.point_type.insert(min_index, "Summit")
- # self.point_distance = np.insert(self.point_distance, min_index, self.climb_segment[i]['course_point_distance'])
- # self.point_altitude = np.insert(self.point_altitude, min_index, self.climb_segment[i]['course_point_altitude'])
+ # course_points.name.insert(min_index, "Top of Climb")
+ # course_points.latitude = np.insert(course_points._latitude, min_index, self.climb_segment[i]['course_point_latitude'])
+ # course_points.longitude = np.insert(course_points.longitude, min_index, self.climb_segment[i]['course_point_longitude'])
+ # course_points.type.insert(min_index, "Summit")
+ # course_points.distance = np.insert(course_points.distance, min_index, self.climb_segment[i]['course_point_distance'])
+ # course_points.altitude = np.insert(course_points.altitude, min_index, self.climb_segment[i]['course_point_altitude'])
- len_pnt_dist = len(self.point_distance)
- len_pnt_alt = len(self.point_altitude)
+ len_pnt_dist = len(course_points.distance)
+ len_pnt_alt = len(course_points.latitude)
# add start course point
- if len_pnt_lat and len_pnt_dist and len_dist and self.point_distance[0] != 0.0:
- self.point_name.insert(0, "Start")
- self.point_latitude = np.insert(self.point_latitude, 0, self.latitude[0])
- self.point_longitude = np.insert(self.point_longitude, 0, self.longitude[0])
- self.point_type.insert(0, "")
+ if (
+ len_pnt_lat
+ and len_pnt_dist
+ and len_dist
+ # TODO do not use float
+ and course_points.distance[0] != 0.0
+ ):
+ course_points.name.insert(0, "Start")
+ course_points.latitude = np.insert(
+ course_points.latitude, 0, self.latitude[0]
+ )
+ course_points.longitude = np.insert(
+ course_points.longitude, 0, self.longitude[0]
+ )
+ course_points.type.insert(0, "")
+ course_points.notes.insert(0, "")
if len_pnt_dist and len_dist:
- self.point_distance = np.insert(self.point_distance, 0, 0.0)
+ course_points.distance = np.insert(course_points.distance, 0, 0.0)
if len_pnt_alt and len_alt:
- self.point_altitude = np.insert(
- self.point_altitude, 0, self.altitude[0]
+ course_points.altitude = np.insert(
+ course_points.altitude, 0, self.altitude[0]
)
# add end course point
- # print(self.point_latitude, self.latitude, self.point_longitude, self.longitude)
end_distance = None
- if len(self.latitude) and len(self.point_longitude):
+ if len(self.latitude) and len(course_points.longitude):
end_distance = self.config.get_dist_on_earth_array(
self.longitude[-1],
self.latitude[-1],
- self.point_longitude[-1],
- self.point_latitude[-1],
+ course_points.longitude[-1],
+ course_points.latitude[-1],
)
if (
len_pnt_lat
@@ -871,18 +764,26 @@ def modify_course_points(self):
and end_distance is not None
and end_distance > 5
):
- self.point_name.append("End")
- self.point_latitude = np.append(self.point_latitude, self.latitude[-1])
- self.point_longitude = np.append(self.point_longitude, self.longitude[-1])
- self.point_type.append("")
+ course_points.name.append("End")
+ course_points.latitude = np.append(
+ course_points.latitude, self.latitude[-1]
+ )
+ course_points.longitude = np.append(
+ course_points.longitude, self.longitude[-1]
+ )
+ course_points.type.append("")
+ course_points.notes.append("")
if len_pnt_dist and len_dist:
- self.point_distance = np.append(self.point_distance, self.distance[-1])
+ course_points.distance = np.append(
+ course_points.distance, self.distance[-1]
+ )
if len_pnt_alt and len_alt:
- self.point_altitude = np.append(self.point_altitude, self.altitude[-1])
+ course_points.altitude = np.append(
+ course_points.altitude, self.altitude[-1]
+ )
- self.point_name = np.array(self.point_name)
- self.point_type = np.array(self.point_type)
- self.point_name = np.array(self.point_name)
+ course_points.name = np.array(course_points.name)
+ course_points.type = np.array(course_points.type)
@staticmethod
def savitzky_golay(y, window_size, order, deriv=0, rate=1):
diff --git a/modules/gui_pyqt.py b/modules/gui_pyqt.py
index a95855fb..308b5433 100644
--- a/modules/gui_pyqt.py
+++ b/modules/gui_pyqt.py
@@ -392,7 +392,7 @@ def delay_init(self):
self.main_page.addWidget(self.map_widget)
elif (
k == "CUESHEET"
- and len(self.config.logger.course.point_name)
+ and self.config.logger.course.course_points.is_set
and self.config.G_COURSE_INDEXING
and self.config.G_CUESHEET_DISPLAY_NUM
):
diff --git a/modules/loaders/__init__.py b/modules/loaders/__init__.py
new file mode 100644
index 00000000..67a1d1af
--- /dev/null
+++ b/modules/loaders/__init__.py
@@ -0,0 +1 @@
+from .tcx import TcxLoader
diff --git a/modules/loaders/tcx.py b/modules/loaders/tcx.py
new file mode 100644
index 00000000..2ded2bbc
--- /dev/null
+++ b/modules/loaders/tcx.py
@@ -0,0 +1,168 @@
+import os
+import re
+from collections import defaultdict
+
+import numpy as np
+
+from logger import app_logger
+
+POLYLINE_DECODER = False
+try:
+ import polyline
+
+ POLYLINE_DECODER = True
+except ImportError:
+ pass
+
+patterns = {
+ "name": re.compile(r"(?P[\s\S]*?)"),
+ "distance_meters": re.compile(
+ r"(?P[\s\S]*?)"
+ ),
+ "track": re.compile(r""),
+ "latitude": re.compile(r"(?P[^<]*)"),
+ "longitude": re.compile(r"(?P[^<]*)"),
+ "altitude": re.compile(r"(?P[^<]*)"),
+ "distance": re.compile(r"(?P[^<]*)"),
+ "course_point": re.compile(r"(?P[\s\S]+)"),
+ "course_name": re.compile(r"(?P[^<]*)"),
+ "course_point_type": re.compile(r"(?P[^<]*)"),
+ "course_notes": re.compile(r"(?P[^<]*)"),
+}
+
+
+class TcxLoader:
+ config = None
+
+ @classmethod
+ def load_file(cls, file):
+ if not os.path.exists(file):
+ return None, None
+ app_logger.info(f"[{cls.__name__}]: loading {file}")
+
+ # should just return a Course object
+ course = {
+ "info": {},
+ "latitude": None,
+ "longitude": None,
+ "altitude": None,
+ "distance": None,
+ }
+ course_points = defaultdict(list)
+
+ with open(file, "r", encoding="utf-8_sig") as f:
+ tcx = f.read()
+
+ match_name = patterns["name"].search(tcx)
+ if match_name:
+ course["info"]["Name"] = match_name.group("text").strip()
+
+ match_distance_meter = patterns["distance_meters"].search(tcx)
+ if match_distance_meter:
+ course["info"]["DistanceMeters"] = round(
+ float(match_distance_meter.group("text").strip()) / 1000, 1
+ )
+
+ match_track = patterns["track"].search(tcx)
+ if match_track:
+ track = match_track.group("text")
+ course["latitude"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["latitude"].finditer(track)
+ ]
+ )
+ course["longitude"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["longitude"].finditer(track)
+ ]
+ )
+ course["altitude"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["altitude"].finditer(track)
+ ]
+ )
+ course["distance"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["distance"].finditer(track)
+ ]
+ )
+
+ match_course_point = patterns["course_point"].search(tcx)
+
+ if match_course_point:
+ course_point = match_course_point.group("text")
+ course_points["name"] = [
+ m.group("text").strip()
+ for m in patterns["course_name"].finditer(course_point)
+ ]
+ course_points["latitude"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["latitude"].finditer(course_point)
+ ]
+ )
+ course_points["longitude"] = np.array(
+ [
+ float(m.group("text").strip())
+ for m in patterns["longitude"].finditer(course_point)
+ ]
+ )
+ course_points["type"] = [
+ m.group("text").strip()
+ for m in patterns["course_point_type"].finditer(course_point)
+ ]
+ course_points["notes"] = [
+ m.group("text").strip()
+ for m in patterns["course_notes"].finditer(course_point)
+ ]
+
+ valid_course = True
+ if len(course["latitude"]) != len(course["longitude"]):
+ app_logger.error("Could not parse course")
+ valid_course = False
+ if not (
+ len(course["latitude"])
+ == len(course["altitude"])
+ == len(course["distance"])
+ ):
+ app_logger.warning(
+ f"Course has missing data: points {len(course['latitude'])} altitude {len(course['altitude'])} "
+ f"distance {len(course['distance'])}"
+ )
+ if not (
+ len(course_points["name"])
+ == len(course_points["latitude"])
+ == len(course_points["longitude"])
+ == len(course_points["type"])
+ ):
+ app_logger.error("Could not parse course points")
+ valid_course = False
+
+ if not valid_course:
+ course["distance"] = np.array([])
+ course["altitude"] = np.array([])
+ course["latitude"] = np.array([])
+ course["longitude"] = np.array([])
+ course_points = defaultdict(list)
+ else:
+ # delete 'Straight' from course points
+ if len(course_points["type"]):
+ ptype = np.array(course_points["type"])
+ not_straight_cond = np.where(ptype != "Straight", True, False)
+ course_points["type"] = list(ptype[not_straight_cond])
+
+ for key in ["name", "latitude", "longitude", "notes"]:
+ if len(course_points[key]):
+ # TODO, probably not necessary but kept so logic is 1-1
+ # we should avoid to mix data types here (or using typings)
+ course_points[key] = np.array(course_points[key])[
+ not_straight_cond
+ ]
+ if key in ["name", "notes"]:
+ course_points[key] = list(course_points[key])
+
+ return course, course_points
diff --git a/modules/logger_core.py b/modules/logger_core.py
index 73d6a4df..de3ebfc0 100644
--- a/modules/logger_core.py
+++ b/modules/logger_core.py
@@ -98,13 +98,13 @@ def start_coroutine(self):
traceback.print_exc()
def delay_init(self):
- from . import sensor_core
- from .logger import loader_tcx
+ from .course import Course
from .logger import logger_csv
from .logger import logger_fit
+ from . import sensor_core
self.sensor = sensor_core.SensorCore(self.config)
- self.course = loader_tcx.LoaderTcx(self.config)
+ self.course = Course(self.config)
self.logger_csv = logger_csv.LoggerCsv(self.config)
self.logger_fit = logger_fit.LoggerFit(self.config)
@@ -477,7 +477,7 @@ def reset_course(self, delete_course_file=False, replace=False):
self.sensor.sensor_gps.reset_course_index()
def set_new_course(self, course_file):
- self.course.set_course(course_file)
+ self.course.load(course_file)
async def record_log(self):
# need to detect location delta for smart recording
diff --git a/modules/pyqt/graph/pyqt_base_map.py b/modules/pyqt/graph/pyqt_base_map.py
index adf476fa..c1175afb 100644
--- a/modules/pyqt/graph/pyqt_base_map.py
+++ b/modules/pyqt/graph/pyqt_base_map.py
@@ -175,7 +175,7 @@ async def zoom_minus(self):
await self.update_extra()
def get_max_zoom(self):
- if not len(self.config.logger.course.distance):
+ if not self.config.logger.course.is_set:
return
if self.config.G_MAX_ZOOM != 0:
@@ -190,7 +190,6 @@ def get_max_zoom(self):
while z / 1000 > dist:
z /= 2
self.config.G_MAX_ZOOM = z
- # print("MAX_ZOOM", self.config.G_MAX_ZOOM, dist)
def load_course(self):
pass
diff --git a/modules/pyqt/graph/pyqt_course_profile.py b/modules/pyqt/graph/pyqt_course_profile.py
index 0382f33b..fbd080ae 100644
--- a/modules/pyqt/graph/pyqt_course_profile.py
+++ b/modules/pyqt/graph/pyqt_course_profile.py
@@ -49,7 +49,7 @@ def setup_ui_extra(self):
# load course profile and display
def load_course(self):
- if not len(self.config.logger.course.distance) or not len(
+ if not self.config.logger.course.is_set or not len(
self.config.logger.course.altitude
):
return
@@ -126,7 +126,7 @@ def init_course(self):
self.resizeEvent(None)
async def update_extra(self):
- if not len(self.config.logger.course.distance) or not len(
+ if not self.config.logger.course.is_set or not len(
self.config.logger.course.altitude
):
return
diff --git a/modules/pyqt/graph/pyqt_map.py b/modules/pyqt/graph/pyqt_map.py
index 78bd9d22..5a604aed 100644
--- a/modules/pyqt/graph/pyqt_map.py
+++ b/modules/pyqt/graph/pyqt_map.py
@@ -230,7 +230,7 @@ def reset_map(self):
def init_cuesheet_and_instruction(self):
# init cuesheet_widget
if (
- len(self.config.logger.course.point_name)
+ self.config.logger.course.course_points.is_set
and self.config.G_CUESHEET_DISPLAY_NUM
and self.config.G_COURSE_INDEXING
):
@@ -252,7 +252,7 @@ def init_cuesheet_and_instruction(self):
def resizeEvent(self, event):
if (
- not len(self.config.logger.course.point_name)
+ not self.config.logger.course.course_points.is_set
or not self.config.G_CUESHEET_DISPLAY_NUM
or not self.config.G_COURSE_INDEXING
):
@@ -286,86 +286,90 @@ def switch_lock(self):
super().switch_lock()
def load_course(self):
- if not len(self.config.logger.course.latitude):
- return
-
timers = [
Timer(auto_start=False, text="course plot : {0:.3f} sec"),
Timer(auto_start=False, text="course points: {0:.3f} sec"),
]
+ course = self.config.logger.course
with timers[0]:
if self.course_plot is not None:
self.plot.removeItem(self.course_plot)
- self.course_plot = CoursePlotItem(
- x=self.config.logger.course.longitude,
- y=self.get_mod_lat_np(self.config.logger.course.latitude),
- brushes=self.config.logger.course.colored_altitude,
- width=6,
- )
- self.course_plot.setZValue(20)
- self.plot.addItem(self.course_plot)
-
- # test
- if not self.config.G_IS_RASPI:
- if self.plot_verification is not None:
- self.plot.removeItem(self.plot_verification)
- self.plot_verification = pg.ScatterPlotItem(pxMode=True)
- self.plot_verification.setZValue(25)
- test_points = []
- for i in range(len(self.config.logger.course.longitude)):
- p = {
- "pos": [
- self.config.logger.course.longitude[i],
- self.get_mod_lat(self.config.logger.course.latitude[i]),
- ],
- "size": 2,
- "pen": {"color": "w", "width": 1},
- "brush": pg.mkBrush(color=(255, 0, 0)),
- }
- test_points.append(p)
- self.plot_verification.setData(test_points)
- self.plot.addItem(self.plot_verification)
+ if not len(course.latitude):
+ app_logger.warning("Course has no points")
+ else:
+ self.course_plot = CoursePlotItem(
+ x=course.longitude,
+ y=self.get_mod_lat_np(course.latitude),
+ brushes=course.colored_altitude,
+ width=6,
+ )
+ self.course_plot.setZValue(20)
+ self.plot.addItem(self.course_plot)
+
+ # test
+ if not self.config.G_IS_RASPI:
+ if self.plot_verification is not None:
+ self.plot.removeItem(self.plot_verification)
+ self.plot_verification = pg.ScatterPlotItem(pxMode=True)
+ self.plot_verification.setZValue(25)
+ test_points = []
+ for i in range(len(course.longitude)):
+ p = {
+ "pos": [
+ course.longitude[i],
+ self.get_mod_lat(course.latitude[i]),
+ ],
+ "size": 2,
+ "pen": {"color": "w", "width": 1},
+ "brush": pg.mkBrush(color=(255, 0, 0)),
+ }
+ test_points.append(p)
+ self.plot_verification.setData(test_points)
+ self.plot.addItem(self.plot_verification)
with timers[1]:
- # course point
- if not len(self.config.logger.course.point_longitude):
- app_logger.warning("Point_longitude is empty")
- return
+ # course points
+ course_points = course.course_points
if self.course_points_plot is not None:
self.plot.removeItem(self.course_points_plot)
- self.course_points_plot = pg.ScatterPlotItem(
- pxMode=True, symbol="t", size=12
- )
- self.course_points_plot.setZValue(40)
- self.course_points = []
-
- for i in reversed(range(len(self.config.logger.course.point_longitude))):
- color = (255, 0, 0)
- symbol = "t"
- if self.config.logger.course.point_type[i] == "Left":
- symbol = "t3"
- elif self.config.logger.course.point_type[i] == "Right":
- symbol = "t2"
- cp = {
- "pos": [
- self.config.logger.course.point_longitude[i],
- self.get_mod_lat(self.config.logger.course.point_latitude[i]),
- ],
- "pen": {"color": color, "width": 1},
- "symbol": symbol,
- "brush": pg.mkBrush(color=color),
- }
- self.course_points.append(cp)
- self.course_points_plot.setData(self.course_points)
- self.plot.addItem(self.course_points_plot)
+
+ if not len(course_points.longitude):
+ app_logger.warning("No course points found")
+
+ else:
+ self.course_points_plot = pg.ScatterPlotItem(
+ pxMode=True, symbol="t", size=12
+ )
+ self.course_points_plot.setZValue(40)
+ formatted_course_points = []
+
+ for i in reversed(range(len(course_points.longitude))):
+ color = (255, 0, 0)
+ symbol = "t"
+ if course_points.type[i] == "Left":
+ symbol = "t3"
+ elif course_points.type[i] == "Right":
+ symbol = "t2"
+ cp = {
+ "pos": [
+ course_points.longitude[i],
+ self.get_mod_lat(course_points.latitude[i]),
+ ],
+ "pen": {"color": color, "width": 1},
+ "symbol": symbol,
+ "brush": pg.mkBrush(color=color),
+ }
+ formatted_course_points.append(cp)
+ self.course_points_plot.setData(formatted_course_points)
+ self.plot.addItem(self.course_points_plot)
app_logger.info("Plotting course:")
log_timers(timers, text_total=f"total : {0:.3f} sec")
async def update_extra(self):
- # t = datetime.datetime.utcnow()
+ course = self.config.logger.course
# display current position
if len(self.location):
@@ -383,12 +387,10 @@ async def update_extra(self):
# recent point(from log or pre_point) / course start / dummy
if len(self.tracks_lon) and len(self.tracks_lat):
self.point["pos"] = [self.tracks_lon_pos, self.tracks_lat_pos]
- elif len(self.config.logger.course.longitude) and len(
- self.config.logger.course.latitude
- ):
+ elif len(course.longitude) and len(course.latitude):
self.point["pos"] = [
- self.config.logger.course.longitude[0],
- self.config.logger.course.latitude[0],
+ course.longitude[0],
+ course.latitude[0],
]
else:
self.point["pos"] = [
@@ -417,7 +419,7 @@ async def update_extra(self):
x_move = y_move = 0
if (
self.lock_status
- and len(self.config.logger.course.distance)
+ and len(course.distance)
and self.gps_values["on_course_status"]
):
index = self.gps_sensor.get_index_with_distance_cutoff(
@@ -425,8 +427,8 @@ async def update_extra(self):
# get some forward distance [m]
self.get_width_distance(self.map_pos["y"], self.map_area["w"]) / 1000,
)
- x2 = self.config.logger.course.longitude[index]
- y2 = self.config.logger.course.latitude[index]
+ x2 = course.longitude[index]
+ y2 = course.latitude[index]
x_delta = x2 - self.map_pos["x"]
y_delta = y2 - self.map_pos["y"]
# slide from center
@@ -488,14 +490,12 @@ async def update_extra(self):
self.center_point.setData(self.center_point_location)
self.plot.addItem(self.center_point)
- # print("\tpyqt_graph : update_extra init : ", (datetime.datetime.utcnow()-t).total_seconds(), "sec")
- # t = datetime.datetime.utcnow()
-
# set x and y ranges
x_start = self.map_pos["x"] - self.map_area["w"] / 2
x_end = x_start + self.map_area["w"]
y_start = self.map_pos["y"] - self.map_area["h"] / 2
y_end = y_start + self.map_area["h"]
+
if not np.isnan(x_start) and not np.isnan(x_end):
self.plot.setXRange(x_start, x_end, padding=0)
if not np.isnan(y_start) and not np.isnan(y_end):
@@ -505,9 +505,8 @@ async def update_extra(self):
if not np.any(np.isnan([x_start, x_end, y_start, y_end])):
await self.draw_map_tile(x_start, x_end, y_start, y_end)
- # print("\tpyqt_graph : update_extra map : ", (datetime.datetime.utcnow()-t).total_seconds(), "sec")
- # t = datetime.datetime.utcnow()
+ # TODO shouldn't be there but does not plot if removed !
if not self.course_loaded:
self.load_course()
self.course_loaded = True
@@ -516,21 +515,14 @@ async def update_extra(self):
x_start, x_end, y_start, y_end, auto_zoom=True
)
- # print("\tpyqt_graph : update_extra cuesheet : ", (datetime.datetime.utcnow()-t).total_seconds(), "sec")
- # t = datetime.datetime.utcnow()
-
# draw track
self.get_track()
self.track_plot.setData(self.tracks_lon, self.tracks_lat)
- # print("\tpyqt_graph : update_extra track : ", (datetime.datetime.utcnow()-t).total_seconds(), "sec")
- # t = datetime.datetime.utcnow()
# draw scale
self.draw_scale(x_start, y_start)
# draw map attribution
self.draw_map_attribution(x_start, y_start)
- # print("\tpyqt_graph : update_extra draw map : ", (datetime.datetime.utcnow()-t).total_seconds(), "sec")
- # t = datetime.datetime.utcnow()
def get_track(self):
# get track from SQL
@@ -998,7 +990,7 @@ async def update_cuesheet_and_instruction(
self, x_start, x_end, y_start, y_end, auto_zoom=False
):
if (
- not len(self.config.logger.course.point_name)
+ not self.config.logger.course.course_points.is_set
or not self.config.G_CUESHEET_DISPLAY_NUM
or not self.config.G_COURSE_INDEXING
):
diff --git a/modules/pyqt/menu/pyqt_course_menu_widget.py b/modules/pyqt/menu/pyqt_course_menu_widget.py
index 5d742125..92d6d6d6 100644
--- a/modules/pyqt/menu/pyqt_course_menu_widget.py
+++ b/modules/pyqt/menu/pyqt_course_menu_widget.py
@@ -71,7 +71,7 @@ def google_directions_api_setting_menu(self):
self.change_page("Google Directions API mode", preprocess=True)
def onoff_course_cancel_button(self):
- status = bool(len(self.config.logger.course.distance))
+ status = self.config.logger.course.is_set
self.buttons["Cancel Course"].onoff_button(status)
def cancel_course(self, replace=False):
@@ -214,7 +214,7 @@ def preprocess_extra(self):
self.page_name_label.setText(self.list_type)
async def list_local_courses(self):
- courses = self.config.logger.course.get_courses()
+ courses = self.config.get_courses()
for c in courses:
course_item = CourseListItemWidget(self, self.list_type, c)
self.add_list_item(course_item)
@@ -238,7 +238,7 @@ def set_course(self, course_file=None):
self.course_file = course_file
# exist course: cancel and set new course
- if len(self.config.logger.course.distance):
+ if self.config.logger.course.is_set:
self.config.gui.show_dialog(
self.cancel_and_set_new_course, "Replace this course?"
)
diff --git a/modules/pyqt/pyqt_cuesheet_widget.py b/modules/pyqt/pyqt_cuesheet_widget.py
index 6f11a8d5..6ba447e4 100644
--- a/modules/pyqt/pyqt_cuesheet_widget.py
+++ b/modules/pyqt/pyqt_cuesheet_widget.py
@@ -135,26 +135,24 @@ def resizeEvent(self, event):
@qasync.asyncSlot()
async def update_display(self):
- if (
- not len(self.config.logger.course.point_distance)
- or not self.config.G_CUESHEET_DISPLAY_NUM
- ):
+ course_points = self.config.logger.course.course_points
+ if not course_points.is_set or not self.config.G_CUESHEET_DISPLAY_NUM:
return
cp_i = self.gps_values["course_point_index"]
# cuesheet
for i, cuesheet_item in enumerate(self.cuesheet):
- if cp_i + i > len(self.config.logger.course.point_distance) - 1:
+ if cp_i + i > len(course_points.distance) - 1:
cuesheet_item.reset()
continue
dist = cuesheet_item.dist_num = (
- self.config.logger.course.point_distance[cp_i + i] * 1000
+ course_points.distance[cp_i + i] * 1000
- self.gps_values["course_distance"]
)
if dist < 0:
continue
dist_text = f"{dist / 1000:4.1f}km " if dist > 1000 else f"{dist:6.0f}m "
cuesheet_item.dist.setText(dist_text)
- name_text = self.config.logger.course.point_type[cp_i + i]
+ name_text = course_points.type[cp_i + i]
cuesheet_item.name.setText(name_text)
diff --git a/modules/sensor/sensor_gps.py b/modules/sensor/sensor_gps.py
index c58adcb6..ce04895d 100644
--- a/modules/sensor/sensor_gps.py
+++ b/modules/sensor/sensor_gps.py
@@ -642,6 +642,7 @@ def set_time(self):
return True
def get_course_index(self):
+ course = self.config.logger.course
if not self.config.G_COURSE_INDEXING:
self.values["on_course_status"] = False
return
@@ -660,7 +661,7 @@ def get_course_index(self):
if self.config.G_IS_RASPI and self.config.G_STOPWATCH_STATUS != "START":
return
- course_n = len(self.config.logger.course.longitude)
+ course_n = len(course.longitude)
if course_n == 0:
return
@@ -676,23 +677,19 @@ def get_course_index(self):
start, -self.config.G_GPS_SEARCH_RANGE
)
- b_a_x = self.config.logger.course.points_diff[0]
- b_a_y = self.config.logger.course.points_diff[1]
- lon_diff = self.values["lon"] - self.config.logger.course.longitude
- lat_diff = self.values["lat"] - self.config.logger.course.latitude
+ b_a_x = course.points_diff[0]
+ b_a_y = course.points_diff[1]
+ lon_diff = self.values["lon"] - course.longitude
+ lat_diff = self.values["lat"] - course.latitude
p_a_x = lon_diff[0:-1]
p_a_y = lat_diff[0:-1]
p_b_x = lon_diff[1:]
p_b_y = lat_diff[1:]
- inner_p = (
- b_a_x * p_a_x + b_a_y * p_a_y
- ) / self.config.logger.course.points_diff_sum_of_squares
+ inner_p = (b_a_x * p_a_x + b_a_y * p_a_y) / course.points_diff_sum_of_squares
- azimuth_diff = np.full(len(self.config.logger.course.azimuth), np.nan)
+ azimuth_diff = np.full(len(course.azimuth), np.nan)
if not np.isnan(self.values["track"]):
- azimuth_diff = (
- self.values["track"] - self.config.logger.course.azimuth
- ) % 360
+ azimuth_diff = (self.values["track"] - course.azimuth) % 360
dist_diff = np.where(
inner_p <= 0.0,
@@ -700,8 +697,7 @@ def get_course_index(self):
np.where(
inner_p >= 1.0,
np.sqrt(p_b_x**2 + p_b_y**2),
- np.abs(b_a_x * p_a_y - b_a_y * p_a_x)
- / self.config.logger.course.points_diff_dist,
+ np.abs(b_a_x * p_a_y - b_a_y * p_a_x) / course.points_diff_dist,
),
)
@@ -749,7 +745,7 @@ def get_course_index(self):
# check azimuth
# print("i:{}, s:{}, m:{}, azimuth_diff:{}".format(i, s, m, azimuth_diff[m]))
# print("self.values['track']:{}, m:{}".format(self.values['track'], m))
- # print(self.config.logger.course.azimuth)
+ # print(course.azimuth)
# print("azimuth_diff:{}".format(azimuth_diff))
if np.isnan(azimuth_diff[m]):
# GPS is lost(return start finally)
@@ -763,26 +759,25 @@ def get_course_index(self):
else:
# go backward
# print("self.values['track']:{}, m:{}".format(self.values['track'], m))
- # print(self.config.logger.course.azimuth)
+ # print(course.azimuth)
# print("azimuth_diff:{}".format(azimuth_diff))
continue
# print("i:{}, s:{}, m:{}, azimuth_diff:{}, course_index:{}, course_point_index:{}".format(i, s, m, azimuth_diff[m], self.values['course_index'], self.values['course_point_index']))
# print("\t lat_lon: {}, {}".format(self.values['lat'], self.values['lon']))
- # print("\t course: {}, {}".format(self.config.logger.course.latitude[self.values['course_index']], self.config.logger.course.longitude[self.values['course_index']]))
- # print("\t course_point: {}, {}".format(self.config.logger.course.point_latitude[self.values['course_point_index']], self.config.logger.course.point_longitude[self.values['course_point_index']]))
+ # print("\t course: {}, {}".format(course.latitude[self.values['course_index']], course.longitude[self.values['course_index']]))
+ # print("\t course_point: {}, {}".format(course.course_points.latitude[self.values['course_point_index']], course.course_points.longitude[self.values['course_point_index']]))
# grade check if available
grade = self.config.logger.sensor.values["integrated"]["grade"]
if not np.isnan(grade) and (grade > self.config.G_SLOPE_CUTOFF[0]) != (
- self.config.logger.course.slope_smoothing[m]
- > self.config.G_SLOPE_CUTOFF[0]
+ course.slope_smoothing[m] > self.config.G_SLOPE_CUTOFF[0]
):
continue
if m == 0 and inner_p[0] <= 0.0:
app_logger.info(f"before start of course: {start} -> {m}")
app_logger.info(
- f"\t {self.values['lat']} {self.values['lon']} / {self.config.logger.course.latitude[m]} {self.config.logger.course.longitude[m]}"
+ f"\t {self.values['lat']} {self.values['lon']} / {course.latitude[m]} {course.longitude[m]}"
)
self.values["on_course_status"] = False
self.values["course_distance"] = 0
@@ -792,32 +787,22 @@ def get_course_index(self):
elif m == len(dist_diff) - 1 and inner_p[-1] >= 1.0:
app_logger.info(f"after end of course {start} -> {m}")
app_logger.info(
- f"\t {self.values['lat']} {self.values['lon']} / {self.config.logger.course.latitude[m]} {self.config.logger.course.longitude[m]}",
+ f"\t {self.values['lat']} {self.values['lon']} / {course.latitude[m]} {course.longitude[m]}",
)
self.values["on_course_status"] = False
m = course_n - 1
- self.values["course_distance"] = (
- self.config.logger.course.distance[-1] * 1000
- )
+ self.values["course_distance"] = course.distance[-1] * 1000
self.values["course_altitude"] = np.nan
self.values["course_index"] = m
return
h_lon = (
- self.config.logger.course.longitude[m]
- + (
- self.config.logger.course.longitude[m + 1]
- - self.config.logger.course.longitude[m]
- )
- * inner_p[m]
+ course.longitude[m]
+ + (course.longitude[m + 1] - course.longitude[m]) * inner_p[m]
)
h_lat = (
- self.config.logger.course.latitude[m]
- + (
- self.config.logger.course.latitude[m + 1]
- - self.config.logger.course.latitude[m]
- )
- * inner_p[m]
+ course.latitude[m]
+ + (course.latitude[m + 1] - course.latitude[m]) * inner_p[m]
)
dist_diff_h = self.config.get_dist_on_earth(
h_lon, h_lat, self.values["lon"], self.values["lat"]
@@ -827,8 +812,8 @@ def get_course_index(self):
# print("dist_diff_h: {:.0f} > cutoff {}[m]".format(dist_diff_h, self.config.G_GPS_ON_ROUTE_CUTOFF))
# print("\t i:{}, s:{}, m:{}, azimuth_diff:{}, course_point_index:{}".format(i, s, m, azimuth_diff[m], self.values['course_point_index']))
# print("\t h_lon/h_lat: {}, {}, lat_lon: {}, {}".format(h_lat, h_lon, self.values['lat'], self.values['lon']))
- # print("\t course[m]: {}, {}".format(self.config.logger.course.latitude[m], self.config.logger.course.longitude[m]))
- # print("\t course[m+1]: {}, {}".format(self.config.logger.course.latitude[m+1], self.config.logger.course.longitude[m+1]))
+ # print("\t course[m]: {}, {}".format(course.latitude[m], course.longitude[m]))
+ # print("\t course[m+1]: {}, {}".format(course.latitude[m+1], course.longitude[m+1]))
continue
# stay forward while self.config.G_GPS_KEEP_ON_COURSE_CUTOFF if search_indexes is except forward
@@ -849,59 +834,48 @@ def get_course_index(self):
self.values["on_course_status"] = True
dist_diff_course = self.config.get_dist_on_earth(
- self.config.logger.course.longitude[m],
- self.config.logger.course.latitude[m],
+ course.longitude[m],
+ course.latitude[m],
self.values["lon"],
self.values["lat"],
)
self.values["course_distance"] = (
- self.config.logger.course.distance[m] * 1000 + dist_diff_course
+ course.distance[m] * 1000 + dist_diff_course
)
- if len(self.config.logger.course.altitude):
+ if len(course.altitude):
alt_diff_course = 0
- if m + 1 < len(self.config.logger.course.altitude):
+ if m + 1 < len(course.altitude):
alt_diff_course = (
- (
- self.config.logger.course.altitude[m + 1]
- - self.config.logger.course.altitude[m]
- )
- / (
- (
- self.config.logger.course.distance[m + 1]
- - self.config.logger.course.distance[m]
- )
- * 1000
- )
+ (course.altitude[m + 1] - course.altitude[m])
+ / ((course.distance[m + 1] - course.distance[m]) * 1000)
* dist_diff_course
)
- self.values["course_altitude"] = (
- self.config.logger.course.altitude[m] + alt_diff_course
- )
+ self.values["course_altitude"] = course.altitude[m] + alt_diff_course
# print("search: ", (datetime.datetime.utcnow()-t).total_seconds(), "sec, index:", m)
self.values["course_index"] = m
- if len(self.config.logger.course.point_distance):
+ if len(course.course_points.distance):
cp_m = np.abs(
- self.config.logger.course.point_distance
+ course.course_points.distance
- self.values["course_distance"] / 1000
).argmin()
# specify next points for displaying in cuesheet widget
if (
- self.config.logger.course.point_distance[cp_m]
+ course.course_points.distance[cp_m]
< self.values["course_distance"] / 1000
):
cp_m += 1
- if cp_m >= len(self.config.logger.course.point_distance):
- cp_m = len(self.config.logger.course.point_distance) - 1
+ if cp_m >= len(course.course_points.distance):
+ cp_m = len(course.course_points.distance) - 1
self.values["course_point_index"] = cp_m
if i >= penalty_index:
app_logger.info(f"{s_state[i]} {start} -> {m}")
app_logger.info(
- f"\t {self.values['lat']} {self.values['lon']} / {self.config.logger.course.latitude[m]} {self.config.logger.course.longitude[m]}"
+ f"\t {self.values['lat']} {self.values['lon']} / {course.latitude[m]} {course.longitude[m]}"
)
app_logger.info(f"\t azimuth_diff: {azimuth_diff[m]}")
@@ -910,7 +884,7 @@ def get_course_index(self):
self.values["on_course_status"] = False
def get_index_with_distance_cutoff(self, start, search_range):
- if self.config.logger is None or not len(self.config.logger.course.distance):
+ if self.config.logger is None or not self.config.logger.course.is_set:
return 0
dist_to = self.config.logger.course.distance[start] + search_range
diff --git a/tests/data/tcx/Mt_Angel_Abbey.tcx b/tests/data/tcx/Mt_Angel_Abbey.tcx
new file mode 100644
index 00000000..6b542165
--- /dev/null
+++ b/tests/data/tcx/Mt_Angel_Abbey.tcx
@@ -0,0 +1,8986 @@
+
+
+
+
+
+
+ Mt-Angel-Abbey
+
+
+
+
+
+
+ Mt-Angel-Abbey
+
+ 946
+ 67455.5
+
+ 44.93903
+ -123.02959
+
+
+ 44.93903
+ -123.02959
+
+ Active
+
+
+
+ Start of r
+
+
+ 44.93903
+ -123.02959
+
+ Generic
+ Start of route
+
+
+ North Capi
+
+
+ 44.939047
+ -123.029581
+
+ Left
+ North on Capitol Mall to start
+
+
+ Center St
+
+
+ 44.94113
+ -123.02854
+
+ Right
+ Right on Center St NE
+
+
+ 17th St NE
+
+
+ 44.93934
+ -123.01811
+
+ Left
+ Left on 17th St NE
+
+
+ Sunnyview
+
+
+ 44.95626
+ -123.01072
+
+ Right
+ Right on Sunnyview Rd NE
+
+
+ Cordon Rd
+
+
+ 44.9549
+ -122.95885
+
+ Left
+ Left on Cordon Rd NE
+
+
+ Hazelgreen
+
+
+ 45.0047
+ -122.94851
+
+ Right
+ Right on Hazelgreen Rd NE
+
+
+ 62nd Ave N
+
+
+ 45.0097
+ -122.93313
+
+ Left
+ Left on 62nd Ave NE
+
+
+ Perkins St
+
+
+ 45.02726
+ -122.93276
+
+ Right
+ Continue onto Perkins St NE
+
+
+ 65th Ave N
+
+
+ 45.02762
+ -122.92813
+
+ Left
+ Continue onto 65th Ave NE
+
+
+ Labish Cen
+
+
+ 45.03069
+ -122.92782
+
+ Right
+ Right on Labish Center Rd NE
+
+
+ 72nd Ave N
+
+
+ 45.03117
+ -122.91311
+
+ Left
+ Left on 72nd Ave NE
+
+
+ Brooklake
+
+
+ 45.0379099
+ -122.9126
+
+ Right
+ Continue onto Brooklake Rd NE
+
+
+ 75th Ave N
+
+
+ 45.0384699
+ -122.9082
+
+ Left
+ Continue onto 75th Ave NE
+
+
+ Rambler Dr
+
+
+ 45.04159
+ -122.90756
+
+ Right
+ Right on Rambler Dr NE
+
+
+ Howell Pra
+
+
+ 45.04601
+ -122.86717
+
+ Left
+ Left on Howell Prairie Rd NE
+
+
+ Saratoga D
+
+
+ 45.0505
+ -122.86655
+
+ Right
+ Right on Saratoga Dr NE
+
+
+ 114th St N
+
+
+ 45.06242
+ -122.82735
+
+ Left
+ Left on 114th St NE
+
+
+ W Church R
+
+
+ 45.06492
+ -122.82704
+
+ Right
+ Right on W Church Rd NE
+
+
+ E College
+
+
+ 45.06757
+ -122.79527
+
+ Straight
+ Continue onto E College St
+
+
+ Abbey Dr
+
+
+ 45.06387
+ -122.78492
+
+ Right
+ Right on Abbey Dr
+
+
+ Return dow
+
+
+ 45.05792
+ -122.77689
+
+ U turn
+ Return down Abbey Dr after visiting the Abbey
+
+
+ East Colle
+
+
+ 45.06385
+ -122.78498
+
+ Left
+ Left on East College Rd NE
+
+
+ Humpert Ln
+
+
+ 45.06429
+ -122.7865
+
+ Left
+ Left on Humpert Ln
+
+
+ Downs Rd N
+
+
+ 45.04219
+ -122.78215
+
+ Right
+ Right on Downs Rd NE
+
+
+ Gallon Hou
+
+
+ 45.04209
+ -122.80201
+
+ Left
+ Left on Gallon House Rd NE
+
+
+ Hobart Rd
+
+
+ 45.02603
+ -122.79764
+
+ Right
+ Right on Hobart Rd NE
+
+
+ Mount Ange
+
+
+ 45.0275
+ -122.81583
+
+ Left
+ Left on Mount Angel Hwy
+
+
+ Hazelgreen
+
+
+ 45.01206
+ -122.81717
+
+ Right
+ Right on Hazelgreen Rd NE
+
+
+ Brush Cree
+
+
+ 45.01129
+ -122.8252
+
+ Left
+ Left on Brush Creek Dr NE
+
+
+ Selah Spri
+
+
+ 44.9843
+ -122.82369
+
+ Right
+ Right on Selah Springs Dr NE
+
+
+ Desart Rd
+
+
+ 44.98061
+ -122.85969
+
+ Left
+ Left on Desart Rd NE
+
+
+ Kaufman Rd
+
+
+ 44.97059
+ -122.86079
+
+ Right
+ Turn right onto Kaufman Rd NE
+
+
+ Cross Howe
+
+
+ 44.97087
+ -122.87738
+
+ Straight
+ Cross Howell Prairie Rd onto Lardon Rd NE
+
+
+ 82nd Ave N
+
+
+ 44.97018
+ -122.89091
+
+ Left
+ Left on 82nd Ave NE
+
+
+ Sunnyview
+
+
+ 44.9583299
+ -122.89348
+
+ Right
+ Right on Sunnyview Rd NE
+
+
+ 17th St NE
+
+
+ 44.95625
+ -123.0108
+
+ Left
+ Left on 17th St NE
+
+
+ D St NE
+
+
+ 44.94414
+ -123.01585
+
+ Right
+ Right on D St NE
+
+
+ 14th St NE
+
+
+ 44.94516
+ -123.01992
+
+ Left
+ Left on 14th St NE
+
+
+ Marion St
+
+
+ 44.94063
+ -123.02218
+
+ Right
+ Right on Marion St NE
+
+
+ Summer St
+
+
+ 44.94236
+ -123.02845
+
+ Left
+ Left on Summer St NE
+
+
+ Center St
+
+
+ 44.9412499
+ -123.02899
+
+ Left
+ Left on Center St NE
+
+
+ Capitol Ma
+
+
+ 44.94113
+ -123.02854
+
+ Right
+ Right on Capitol Mall to return to the starting point
+
+
+ End of rou
+
+
+ 44.93903
+ -123.02959
+
+ Generic
+ End of route
+
+
+
+
diff --git a/tests/test_loader.py b/tests/test_loader.py
new file mode 100644
index 00000000..543ce1ea
--- /dev/null
+++ b/tests/test_loader.py
@@ -0,0 +1,12 @@
+import unittest
+
+from modules.loaders.tcx import TcxLoader
+
+
+class TestLoader(unittest.TestCase):
+ def test_tcx(self):
+ data_course, data_course_points = TcxLoader.load_file(
+ "tests/data/tcx/Mt_Angel_Abbey.tcx"
+ )
+ self.assertEqual(len(data_course["latitude"]), 946)
+ self.assertEqual(len(data_course_points["latitude"]), 42)