diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py index 041ef6805d..7b57964db2 100644 --- a/sunnypilot/navd/helpers.py +++ b/sunnypilot/navd/helpers.py @@ -72,6 +72,15 @@ class Coordinate: return x * EARTH_MEAN_RADIUS +def bearing_between_two_points(point_one: Coordinate, point_two: Coordinate) -> float: + dlon = math.radians(point_two.longitude - point_one.longitude) + bearing_radians = math.atan2(math.sin(dlon)* math.cos(point_two.latitude), math.cos(point_one.latitude) * math.sin(point_two.latitude) - + math.sin(point_one.latitude) * math.cos(point_two.latitude) * math.cos(dlon)) + bearing_degrees = math.degrees(bearing_radians) + bearing_normalized = (bearing_degrees + 360) % 360 + return bearing_normalized + + def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate): if a.distance_to(b) < 0.01: return a.distance_to(p) diff --git a/sunnypilot/navd/navigation_helpers/nav_instructions.py b/sunnypilot/navd/navigation_helpers/nav_instructions.py index 8c232db958..f32ba5b98d 100644 --- a/sunnypilot/navd/navigation_helpers/nav_instructions.py +++ b/sunnypilot/navd/navigation_helpers/nav_instructions.py @@ -4,22 +4,24 @@ Copyright (c) 2021-, James Vecellio, Haibin Wen, sunnypilot, and a number of oth This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import numpy as np +from numpy import interp -from openpilot.common.constants import CV from openpilot.common.params import Params -from openpilot.sunnypilot.navd.helpers import Coordinate, string_to_direction +from openpilot.sunnypilot.navd.helpers import Coordinate, bearing_between_two_points, distance_along_geometry, string_to_direction class NavigationInstructions: def __init__(self): self.coord = Coordinate(0, 0) self.params = Params() + self._cached_route = None self._route_loaded = False self._no_route = False + self.closest_idx: float = 0 + def get_route_progress(self, current_lat, current_lon) -> dict | None: route = self.get_current_route() if not route or not route['geometry'] or not route['steps']: @@ -29,8 +31,8 @@ class NavigationInstructions: self.coord.longitude = current_lon # Find the closest point on the route relative to self - closest_idx, min_distance = min(((idx, self.coord.distance_to(coord)) for idx, coord in enumerate(route['geometry'])), key=lambda x: x[1]) - closest_cumulative = route['cumulative_distances'][closest_idx] + self.closest_idx, min_distance = min(((idx, self.coord.distance_to(coord)) for idx, coord in enumerate(route['geometry'])), key=lambda x: x[1]) + closest_cumulative = distance_along_geometry(route['geometry'], self.coord) # Find the current step index, which is the HIGHEST idx where the step location cumulative less/equal closest cumulative current_step_idx = max((idx for idx, step in enumerate(route['steps']) if step['cumulative_distance'] <= closest_cumulative), default=-1) @@ -96,6 +98,7 @@ class NavigationInstructions: 'instruction': step['instruction'], }) self._cached_route = { + 'bearings': [bearing_between_two_points(geometry[i], geometry[i+2]) for i in range(len(geometry)-2)], 'steps': steps, 'total_distance': route['totalDistance'], 'total_duration': route['totalDuration'], @@ -111,11 +114,25 @@ class NavigationInstructions: self._route_loaded = False self._no_route = False + def route_bearing_misalign(self, route, bearing, v_ego) -> bool: + route_bearing_misalign:bool = False + + if v_ego < 5.0: + route_bearing_misalign = False + elif 0 < self.closest_idx < len(route['geometry']) -1: + route_bearing = route['bearings'][self.closest_idx -1] + current_bearing_normalized = (bearing + 360) % 360 + bearing_difference = abs(current_bearing_normalized - route_bearing) + + if min(bearing_difference, 360 - bearing_difference) > 95: + route_bearing_misalign = True # flag for recompute/cancellation + return route_bearing_misalign + def get_upcoming_turn_from_progress(self, progress, current_lat, current_lon, v_ego: float) -> str: if progress and progress['next_turn']: speed_breakpoints: list = [0, 5, 10, 15, 20, 25, 30, 35, 40] distance_breakpoints: list = [20, 25, 30, 45, 60, 75, 90, 105, 120] - distance_interp = np.interp(v_ego, speed_breakpoints, distance_breakpoints) + distance_interp = interp(v_ego, speed_breakpoints, distance_breakpoints) self.coord.latitude = current_lat self.coord.longitude = current_lon @@ -127,19 +144,9 @@ class NavigationInstructions: return 'none' @staticmethod - def get_current_speed_limit_from_progress(progress, is_metric: bool) -> int: - if progress and progress['current_maxspeed']: - speed, _ = progress['current_maxspeed'] - if is_metric: - return int(speed) - else: - return int(round(speed * CV.KPH_TO_MPH)) - return 0 - - @staticmethod - def arrived_at_destination(progress) -> bool: - if progress['all_maneuvers'][0]['type'] == 'arrive': - return True - elif progress['all_maneuvers'][0]['instruction'].startswith('Your destination'): - return True + def arrived_at_destination(progress, v_ego) -> bool: + if v_ego < 1.0: + maneuvers = progress['all_maneuvers'][0] + if maneuvers['type'] == 'arrive' or maneuvers['instruction'].startswith('Your destination'): + return True return False diff --git a/sunnypilot/navd/navigation_helpers/tests/test_mapbox.py b/sunnypilot/navd/navigation_helpers/tests/test_mapbox.py index fe47cfa4e1..87717fc896 100644 --- a/sunnypilot/navd/navigation_helpers/tests/test_mapbox.py +++ b/sunnypilot/navd/navigation_helpers/tests/test_mapbox.py @@ -79,16 +79,20 @@ class TestMapbox: assert isinstance(self.progress['all_maneuvers'], list) def test_speed_limit_handling(self): - speed_limit_metric = self.nav.get_current_speed_limit_from_progress(self.progress, True) - speed_limit_imperial = self.nav.get_current_speed_limit_from_progress(self.progress, False) + speed_limit_metric = self.progress['current_maxspeed'][0] + speed_limit_imperial = (round(speed_limit_metric * CV.KPH_TO_MPH)) assert isinstance(speed_limit_metric, int) assert isinstance(speed_limit_imperial, int) - expected_metric = int(self.progress['current_maxspeed'][0]) - expected_imperial = int(round(self.progress['current_maxspeed'][0] * CV.KPH_TO_MPH)) - assert speed_limit_metric == expected_metric - assert speed_limit_imperial == expected_imperial def test_arrival_detection(self): - is_arrived = self.nav.arrived_at_destination(self.progress) + is_arrived = self.nav.arrived_at_destination(self.progress, 2.0) assert isinstance(is_arrived, bool) assert not is_arrived + + def test_bearing_misalign(self): + lat = self.route['steps'][1]['location'].latitude + lon = self.route['steps'][1]['location'].longitude + self.nav.get_route_progress(lat, lon) + route_bearing_misaligned = self.nav.route_bearing_misalign(self.route, 45, 5.0) + # based on math: closest index: 7, normalized bearing: 45 route bearing: 180.5486953778888, expected differential: 135.54869538 + assert route_bearing_misaligned