diff --git a/modules/course.py b/modules/course.py
index 24ed013c..e320d69c 100644
--- a/modules/course.py
+++ b/modules/course.py
@@ -641,95 +641,104 @@ def modify_course_points(self):
len_dist = len(self.distance)
len_alt = len(self.altitude)
- # calculate course point distance
- if not len_pnt_dist and len_dist:
- course_points.distance = np.empty(len_pnt_lat)
- if not len_pnt_alt and len_alt:
- 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 = 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[min_index:]
- inner_p_check = np.where(
- (0.0 <= inner_p) & (inner_p <= 1.0), True, False
- )
-
- min_j = None
- min_dist_diff_h = np.inf
- min_dist_delta = 0
- min_alt_delta = 0
- for j in list(*np.where(inner_p_check == True)):
- h_lon = (
- self.longitude[min_index + j]
- + (
- self.longitude[min_index + j + 1]
- - self.longitude[min_index + j]
- )
- * inner_p[j]
- )
- h_lat = (
- self.latitude[min_index + j]
- + (
- self.latitude[min_index + j + 1]
- - self.latitude[min_index + j]
- )
- * inner_p[j]
- )
- dist_diff_h = get_dist_on_earth(
- h_lon,
- h_lat,
- course_points.longitude[i],
- course_points.latitude[i],
+ # calculate course point distance/altitude if not both already set
+ # If one is already set, it's not going to be overwritten
+ # But if both are already set there's no need to recalculate anything
+ if not len_pnt_dist or not len_pnt_alt:
+ if not len_pnt_dist and len_dist:
+ course_points.distance = np.empty(len_pnt_lat)
+ if not len_pnt_alt and len_alt:
+ 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 = 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[min_index:]
+ inner_p_check = np.where(
+ (0.0 <= inner_p) & (inner_p <= 1.0), True, False
)
- if (
- dist_diff_h < self.config.G_GPS_ON_ROUTE_CUTOFF
- and dist_diff_h < min_dist_diff_h
- ):
- if min_j is not None and j - min_j > 2:
- continue
- min_j = j
- min_dist_diff_h = dist_diff_h
- min_dist_delta = (
- get_dist_on_earth(
- self.longitude[min_index + j],
- self.latitude[min_index + j],
- h_lon,
- h_lat,
+ min_j = None
+ min_dist_diff_h = np.inf
+ min_dist_delta = 0
+ min_alt_delta = 0
+
+ for j in list(*np.where(inner_p_check == True)):
+ h_lon = (
+ self.longitude[min_index + j]
+ + (
+ self.longitude[min_index + j + 1]
+ - self.longitude[min_index + j]
)
- / 1000
+ * inner_p[j]
)
- if len_alt:
- min_alt_delta = (
- (
- self.altitude[min_index + j + 1]
- - self.altitude[min_index + j]
- )
- / (
- self.distance[min_index + j + 1]
- - self.distance[min_index + j]
+ h_lat = (
+ self.latitude[min_index + j]
+ + (
+ self.latitude[min_index + j + 1]
+ - self.latitude[min_index + j]
+ )
+ * inner_p[j]
+ )
+ dist_diff_h = get_dist_on_earth(
+ h_lon,
+ h_lat,
+ course_points.longitude[i],
+ course_points.latitude[i],
+ )
+
+ if (
+ dist_diff_h < self.config.G_GPS_ON_ROUTE_CUTOFF
+ and dist_diff_h < min_dist_diff_h
+ ):
+ if min_j is not None and j - min_j > 2:
+ continue
+
+ min_j = j
+ min_dist_diff_h = dist_diff_h
+ min_dist_delta = (
+ get_dist_on_earth(
+ self.longitude[min_index + j],
+ self.latitude[min_index + j],
+ h_lon,
+ h_lat,
)
- * min_dist_delta
+ / 1000
)
+ if len_alt:
+ min_alt_delta = (
+ (
+ self.altitude[min_index + j + 1]
+ - self.altitude[min_index + j]
+ )
+ / (
+ self.distance[min_index + j + 1]
+ - self.distance[min_index + j]
+ )
+ * min_dist_delta
+ )
- if min_j is None:
- min_j = 0
- min_index = min_index + min_j
+ if min_j is None:
+ min_j = 0
- if not len_pnt_dist and len_dist:
- course_points.distance[i] = (
- self.distance[min_index] + min_dist_delta
- )
- if not len_pnt_alt and len_alt:
- course_points.altitude[i] = self.altitude[min_index] + min_alt_delta
+ min_index = min_index + min_j
+
+ if not len_pnt_dist and len_dist:
+ course_points.distance[i] = (
+ self.distance[min_index] + min_dist_delta
+ )
+ if not len_pnt_alt and len_alt:
+ course_points.altitude[i] = (
+ self.altitude[min_index] + min_alt_delta
+ )
# add climb tops
# if len(self.climb_segment):
@@ -755,6 +764,9 @@ def modify_course_points(self):
# TODO do not use float
and course_points.distance[0] != 0.0
):
+ app_logger.info(
+ f"Missing start of the course point, first value is {course_points.distance[0]}, inserting"
+ )
course_points.name = np.insert(course_points.name, 0, "Start")
course_points.latitude = np.insert(
course_points.latitude, 0, self.latitude[0]
@@ -770,6 +782,7 @@ def modify_course_points(self):
course_points.altitude = np.insert(
course_points.altitude, 0, self.altitude[0]
)
+
# add end course point
end_distance = None
if len(self.latitude) and len(course_points.longitude):
@@ -786,6 +799,9 @@ def modify_course_points(self):
and end_distance is not None
and end_distance > 5
):
+ app_logger.info(
+ f"Missing end of the course point last distance is {end_distance}, inserting"
+ )
course_points.name = np.append(course_points.name, "End")
course_points.latitude = np.append(
course_points.latitude, self.latitude[-1]
diff --git a/modules/helper/api.py b/modules/helper/api.py
index 02dfe097..9dfb2b3a 100644
--- a/modules/helper/api.py
+++ b/modules/helper/api.py
@@ -564,13 +564,13 @@ async def send_livetrack_data_internal(self, quick_send=False):
await asyncio.sleep(5)
self.bt_cmd_lock = False
app_logger.error(
- f"[BT] {timestamp_str} connect error, network status: {detect_network()}"
+ f"[BT] {timestamp_str} connect error, network status: {bool(detect_network())}"
)
self.config.logger.sensor.values["integrated"]["send_time"] = (
datetime.datetime.now().strftime("%H:%M") + "CE"
)
return
- # print("[BT] {} connect, network status:{} {}".format(timestamp_str, detect_network(), count))
+ # print(f"[BT] {timestamp_str} connect, network status:{bool(detect_network())} {count}")
await asyncio.sleep(5)
diff --git a/modules/helper/network.py b/modules/helper/network.py
index 0cbf37bb..1de5176c 100644
--- a/modules/helper/network.py
+++ b/modules/helper/network.py
@@ -175,12 +175,15 @@ async def download_maptile(
additional_save_paths = []
max_zoom_cond = True
+
if (
"max_zoomlevel" in map_config[map_name]
and z + 1 >= map_config[map_name]["max_zoomlevel"]
):
max_zoom_cond = False
+
min_zoom_cond = True
+
if (
"min_zoomlevel" in map_config[map_name]
and z - 1 <= map_config[map_name]["min_zoomlevel"]
@@ -243,6 +246,7 @@ async def download_maptile(
async def download_demtile(self, z, x, y):
if not detect_network():
return False
+
try:
os.makedirs(
f"maptile/{self.config.G_DEM_MAP}/{z}/{x}/",
diff --git a/modules/loaders/tcx.py b/modules/loaders/tcx.py
index 979721eb..0e6a26e8 100644
--- a/modules/loaders/tcx.py
+++ b/modules/loaders/tcx.py
@@ -16,10 +16,12 @@
"longitude": re.compile(r"(?P[^<]*)"),
"altitude": re.compile(r"(?P[^<]*)"),
"distance": re.compile(r"(?P[^<]*)"),
+ "time": re.compile(r""),
"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[^<]*)"),
+ "course_time": re.compile(r""),
}
@@ -39,8 +41,9 @@ def load_file(cls, file):
"longitude": None,
"altitude": None,
"distance": None,
+ "time": None,
}
- course_points = defaultdict(list)
+ course_points = defaultdict(lambda: np.array([]))
with open(file, "r", encoding="utf-8_sig") as f:
tcx = f.read()
@@ -82,6 +85,9 @@ def load_file(cls, file):
for m in patterns["distance"].finditer(track)
]
)
+ course["time"] = np.array(
+ [m.group("text").strip() for m in patterns["time"].finditer(track)]
+ )
match_course_point = patterns["course_point"].search(tcx)
@@ -117,6 +123,12 @@ def load_file(cls, file):
for m in patterns["course_notes"].finditer(course_point)
]
)
+ course_points["time"] = np.array(
+ [
+ m.group("text").strip()
+ for m in patterns["course_time"].finditer(course_point)
+ ]
+ )
valid_course = True
if len(course["latitude"]) != len(course["longitude"]):
@@ -145,7 +157,7 @@ def load_file(cls, file):
course["altitude"] = np.array([])
course["latitude"] = np.array([])
course["longitude"] = np.array([])
- course_points = defaultdict(lambda x: np.array([]))
+ course_points = defaultdict(lambda: np.array([]))
else:
# delete 'Straight' from course points
if len(course_points["type"]):
@@ -156,4 +168,45 @@ def load_file(cls, file):
for key in ["name", "latitude", "longitude", "notes", "type"]:
course_points[key] = course_points[key][not_straight_cond]
+ # if time is given in the field, try to set the course point distance/altitude directly from there
+ # if a point can not be found, let's fail and modify_course_point will try to compute it instead
+ if (
+ course["time"] is not None
+ and len(course["time"])
+ and len(course_points["time"])
+ ):
+ distance_error = False
+ altitude_error = False
+
+ for point_time in course_points["time"]:
+ try:
+ index = np.where(course["time"] == point_time)[0][0]
+ except Exception: # noqa
+ # Point time not found in trackpoint reset and break
+ course_points.distance = np.array([])
+ course_points.altitude = np.array([])
+ break
+ if not distance_error:
+ try:
+ # course_point distance is set in [km]
+ course_points["distance"] = np.append(
+ course_points["distance"], course["distance"][index] / 1000
+ )
+ except IndexError:
+ distance_error = True
+ course_points.distance = np.array([])
+ if not altitude_error:
+ try:
+ # course_point altitude is set in [m]
+ course_points["altitude"] = np.append(
+ course_points["altitude"], course["altitude"][index]
+ )
+ except IndexError:
+ altitude_error = True
+ course_points.altitude = np.array([])
+
+ # do not keep these in memory
+ del course["time"]
+ del course_points["time"]
+
return course, course_points
diff --git a/modules/pyqt/graph/pyqt_map.py b/modules/pyqt/graph/pyqt_map.py
index 2f872b9a..02c73d59 100644
--- a/modules/pyqt/graph/pyqt_map.py
+++ b/modules/pyqt/graph/pyqt_map.py
@@ -946,7 +946,7 @@ def get_image_file(self, use_mbtiles, map_name, z_draw, x, y):
else:
sql = (
f"select tile_data from tiles where "
- f"zoom_level={z_draw} and tile_column={x} and tile_row={2 ** z_draw - 1 - y}"
+ f"zoom_level={z_draw} and tile_column={x} and tile_row={2**z_draw - 1 - y}"
)
img_file = io.BytesIO((self.cur.execute(sql).fetchone())[0])
return img_file
diff --git a/tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride-CP-Removed.tcx b/tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride-CP-Removed.tcx
new file mode 100644
index 00000000..204fc946
--- /dev/null
+++ b/tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride-CP-Removed.tcx
@@ -0,0 +1,1848 @@
+
+
+
+
+
+
+ Heart-of-St--Jo
+
+
+
+
+
+
+ Heart-of-St--Jo
+
+ 184
+ 12286.5
+
+ 45.57873
+ -122.71318
+
+
+ 45.5788
+ -122.7135
+
+ Active
+
+
+
+ North Lomb
+
+
+ 45.57868
+ -122.71339
+
+ Right
+ Turn right onto North Lombard Street
+
+
+ Sharp Nort
+
+
+ 45.5787199
+ -122.7135
+
+ Left
+ Turn sharp left onto North Woolsey Avenue
+
+
+ North Will
+
+
+ 45.5753
+ -122.71351
+
+ Right
+ Turn right onto North Willamette Boulevard
+
+
+ Sharp Nort
+
+
+ 45.57502
+ -122.71586
+
+ Left
+ Turn sharp left onto North Willamette Boulevard
+
+
+ North Reno
+
+
+ 45.59672
+ -122.76473
+
+ Right
+ Turn right onto North Reno Avenue
+
+
+ North Cent
+
+
+ 45.59892
+ -122.75933
+
+ Left
+ Turn left onto North Central Street
+
+
+ North Bruc
+
+
+ 45.60198
+ -122.76118
+
+ Right
+ Turn right onto North Bruce Avenue
+
+
+ North Smit
+
+
+ 45.6028
+ -122.75924
+
+ Right
+ Turn right onto North Smith Street
+
+
+ North Sain
+
+
+ 45.59818
+ -122.75645
+
+ Right
+ Turn right onto North Saint Johns Avenue
+
+
+ North Smit
+
+
+ 45.59811
+ -122.75662
+
+ Left
+ Turn left onto North Smith Street
+
+
+ North Colu
+
+
+ 45.59126
+ -122.73836
+
+ Left
+ Turn left onto North Columbia Way
+
+
+ North Wall
+
+
+ 45.59171
+ -122.72821
+
+ Right
+ Turn right onto North Wall Avenue
+
+
+ Sharp Nort
+
+
+ 45.58643
+ -122.72825
+
+ Left
+ Turn sharp left onto North Houghton Street
+
+
+ North McKe
+
+
+ 45.58643
+ -122.72646
+
+ Left
+ Turn left onto North McKenna Avenue
+
+
+ North Houg
+
+
+ 45.58653
+ -122.72639
+
+ Right
+ Turn right onto North Houghton Street
+
+
+ North Wool
+
+
+ 45.58662
+ -122.71344
+
+ Right
+ Turn right onto North Woolsey Avenue
+
+
+
+
diff --git a/tests/test_course.py b/tests/test_course.py
new file mode 100644
index 00000000..36956bd5
--- /dev/null
+++ b/tests/test_course.py
@@ -0,0 +1,89 @@
+import unittest
+from tempfile import NamedTemporaryFile
+
+from modules.course import Course
+
+
+class Config:
+ G_COURSE_INDEXING = False
+ G_COURSE_FILE_PATH = None
+
+ G_GPS_KEEP_ON_COURSE_CUTOFF = 60
+
+ # for search point on course
+ G_GPS_ON_ROUTE_CUTOFF = 50 # [m] #generate from course
+ G_GPS_SEARCH_RANGE = 6 # [km] #100km/h -> 27.7m/s
+ G_GPS_AZIMUTH_CUTOFF = 60 # degree(30/45/90): 0~G_GPS_AZIMUTH_CUTOFF, (360-G_GPS_AZIMUTH_CUTOFF)~G_GPS_AZIMUTH_CUTOFF
+
+ # Graph color by slope
+ G_CLIMB_DISTANCE_CUTOFF = 0.3 # [km]
+ G_CLIMB_GRADE_CUTOFF = 2 # [%]
+ G_SLOPE_CUTOFF = (1, 3, 6, 9, 12, float("inf")) # by grade
+ G_SLOPE_COLOR = (
+ (128, 128, 128), # gray(base)
+ (0, 255, 0), # green
+ (255, 255, 0), # yellow
+ (255, 128, 0), # orange
+ (255, 0, 0), # red
+ (128, 0, 0), # dark red
+ )
+ G_CLIMB_CATEGORY = [
+ {"volume": 8000, "name": "Cat4"},
+ {"volume": 16000, "name": "Cat3"},
+ {"volume": 32000, "name": "Cat2"},
+ {"volume": 64000, "name": "Cat1"},
+ {"volume": 80000, "name": "HC"},
+ ]
+
+ G_THINGSBOARD_API = {"STATUS": False}
+
+ def __init__(self, indexing=False):
+ self.G_COURSE_INDEXING = indexing
+ self.G_COURSE_FILE_PATH = NamedTemporaryFile().name
+
+
+class TestCourse(unittest.TestCase):
+ # TODO find/create a file where time is not set so distance is kept empty with no_indexing
+ # def test_load_no_indexing(self):
+ # config = Config()
+ # course = Course(config)
+ # course.load(file="tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride.tcx")
+ #
+ # # downsampled from 184 to 31 points
+ # self.assertEqual(len(course.latitude), 31)
+ # self.assertEqual(len(course.course_points.latitude), 18)
+ #
+ # # distance was not set since there's no indexing
+ # self.assertEqual(len(course.course_points.distance), 0)
+ #
+ # self.assertEqual(len(course.colored_altitude), 31)
+
+ def test_load_with_tcx_indexing(self):
+ config = Config(indexing=True)
+ course = Course(config)
+ course.load(file="tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride.tcx")
+
+ # downsampled from 184 to 31 points
+ self.assertEqual(len(course.latitude), 31)
+ self.assertEqual(len(course.course_points.latitude), 18)
+ self.assertEqual(len(course.course_points.distance), 18)
+
+ def test_load_insert_course_point(self):
+ config = Config(indexing=True)
+ course = Course(config)
+ course.load(
+ file="tests/data/tcx/Heart_of_St._Johns_Peninsula_Ride-CP-Removed.tcx"
+ )
+
+ self.assertEqual(len(course.course_points.latitude), 18)
+ self.assertEqual(len(course.course_points.distance), 18)
+
+ self.assertEqual(course.course_points.name[0], "Start")
+ self.assertEqual(course.course_points.latitude[0], 45.57873)
+ self.assertEqual(course.course_points.longitude[0], -122.71318)
+ self.assertEqual(course.course_points.distance[0], 0.0)
+
+ self.assertEqual(course.course_points.name[-1], "End")
+ self.assertEqual(course.course_points.latitude[-1], 45.5788)
+ self.assertEqual(course.course_points.longitude[-1], -122.7135)
+ self.assertEqual(course.course_points.distance[-1], 12.286501)
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 543ce1ea..5f48705b 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -10,3 +10,6 @@ def test_tcx(self):
)
self.assertEqual(len(data_course["latitude"]), 946)
self.assertEqual(len(data_course_points["latitude"]), 42)
+
+ # validate that course_point distance was set correctly
+ self.assertTrue(len(data_course_points["distance"]))