IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
3
iqpilot/ui/onroad/__init__.py
Normal file
3
iqpilot/ui/onroad/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
18
iqpilot/ui/onroad/augmented_road_view.py
Normal file
18
iqpilot/ui/onroad/augmented_road_view.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from iqpilot.selfdrive.ui.ui_state import UIStatus
|
||||
|
||||
BORDER_COLORS_IQ = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF),
|
||||
}
|
||||
|
||||
|
||||
class AugmentedRoadViewIQ:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def update_fade_out_bottom_overlay(self, _content_rect):
|
||||
pass
|
||||
71
iqpilot/ui/onroad/big_model_status.py
Normal file
71
iqpilot/ui/onroad/big_model_status.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import math
|
||||
from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
class SourceState(IntEnum):
|
||||
HIDDEN = 0
|
||||
LOADING = 1
|
||||
ACTIVE = 2
|
||||
FAILED = 3
|
||||
CROSSED = 4
|
||||
|
||||
|
||||
_GREEN = rl.Color(46, 204, 113, 255)
|
||||
_ORANGE = rl.Color(255, 115, 0, 255)
|
||||
_WHITE = rl.Color(255, 255, 255, 255)
|
||||
|
||||
|
||||
def _emac_state(params: Params, engaged: bool) -> SourceState:
|
||||
if not params.get_bool("MacModelReachable"):
|
||||
return SourceState.HIDDEN
|
||||
if params.get_bool("MacModelActive"):
|
||||
return SourceState.ACTIVE
|
||||
if params.get_bool("MacModelFailed"):
|
||||
return SourceState.CROSSED if engaged else SourceState.FAILED
|
||||
return SourceState.LOADING
|
||||
|
||||
|
||||
def _egpu_state(params: Params, engaged: bool) -> SourceState:
|
||||
if not params.get_bool("UsbGpuPresent"):
|
||||
return SourceState.HIDDEN
|
||||
if params.get_bool("UsbGpuActive"):
|
||||
return SourceState.ACTIVE
|
||||
if params.get_bool("UsbGpuFailed"):
|
||||
return SourceState.CROSSED if engaged else SourceState.FAILED
|
||||
return SourceState.LOADING
|
||||
|
||||
|
||||
def resolve_source(params: Params, engaged: bool) -> tuple[str, SourceState]:
|
||||
if params.get_bool("IQEmacEnabled"):
|
||||
return "MAC", _emac_state(params, engaged)
|
||||
if params.get_bool("UsbGpuPresent") or params.get_bool("IQEgpuEnabled"):
|
||||
return "GPU", _egpu_state(params, engaged)
|
||||
return "", SourceState.HIDDEN
|
||||
|
||||
|
||||
def draw_source_label(font: rl.Font, label: str, state: SourceState,
|
||||
pos: rl.Vector2, font_size: int) -> None:
|
||||
if state == SourceState.HIDDEN or not label:
|
||||
return
|
||||
if state == SourceState.ACTIVE:
|
||||
color, opacity, strike = _GREEN, 1.0, False
|
||||
elif state == SourceState.FAILED:
|
||||
color, opacity, strike = _ORANGE, 1.0, False
|
||||
elif state == SourceState.CROSSED:
|
||||
color, opacity, strike = _WHITE, 0.65, True
|
||||
else:
|
||||
color, opacity, strike = _WHITE, 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)), False
|
||||
|
||||
col = rl.Color(color.r, color.g, color.b, int(255 * opacity))
|
||||
rl.draw_text_ex(font, label, pos, font_size, 0, col)
|
||||
if strike:
|
||||
size = measure_text_cached(font, label, font_size)
|
||||
y = int(pos.y + size.y / 2)
|
||||
rl.draw_line_ex(rl.Vector2(pos.x - 2, y), rl.Vector2(pos.x + size.x + 2, y), 4, col)
|
||||
159
iqpilot/ui/onroad/driver_state.py
Normal file
159
iqpilot/ui/onroad/driver_state.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
|
||||
from iqpilot.ui.onroad.hud_overlays import IQDevMetricsOverlay
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
_PERSONALITY_RELAXED = 0
|
||||
_PERSONALITY_STANDARD = 1
|
||||
_PERSONALITY_AGGRESSIVE = 2
|
||||
|
||||
PERSONALITY_COLORS = {
|
||||
_PERSONALITY_RELAXED: rl.Color(0x17, 0xC9, 0x64, 0xFF),
|
||||
_PERSONALITY_STANDARD: rl.Color(0x0C, 0x94, 0x96, 0xFF),
|
||||
_PERSONALITY_AGGRESSIVE: rl.Color(0xE8, 0x2C, 0x2C, 0xFF),
|
||||
}
|
||||
|
||||
PERSONALITY_NAMES = {
|
||||
_PERSONALITY_RELAXED: "Relaxed",
|
||||
_PERSONALITY_STANDARD: "Standard",
|
||||
_PERSONALITY_AGGRESSIVE: "Aggressive",
|
||||
}
|
||||
|
||||
_TOAST_DURATION = 2.0
|
||||
_TOAST_FONT_SIZE = 52
|
||||
_TOAST_PAD_X = 52
|
||||
_TOAST_PAD_Y = 22
|
||||
_TOAST_BOTTOM_MARGIN = UI_BORDER_SIZE + 36
|
||||
_TOAST_RADIUS = 0.45
|
||||
_TOAST_FADE = 0.25
|
||||
|
||||
class DriverStateRendererIQ(DriverStateRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._personality: int = _PERSONALITY_STANDARD
|
||||
self._personality_color: rl.Color = PERSONALITY_COLORS[_PERSONALITY_STANDARD]
|
||||
|
||||
self._toast_end_time: float = 0.0
|
||||
self._toast_text: str = ""
|
||||
self._toast_color: rl.Color = rl.WHITE
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
self.dev_ui_offset = IQDevMetricsOverlay.get_bottom_dev_ui_offset()
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", BTN_SIZE, BTN_SIZE)
|
||||
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", 118, 118)
|
||||
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", 118, 118)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
personality = self._params.get("LongitudinalPersonality", return_default=True)
|
||||
if personality is not None:
|
||||
self._personality = int(personality)
|
||||
self._personality_color = PERSONALITY_COLORS.get(self._personality, PERSONALITY_COLORS[_PERSONALITY_STANDARD])
|
||||
|
||||
def cycle_personality(self):
|
||||
next_p = (self._personality + 1) % 3
|
||||
self._params.put_nonblocking("LongitudinalPersonality", next_p)
|
||||
self._personality = next_p
|
||||
self._personality_color = PERSONALITY_COLORS[next_p]
|
||||
self._toast_text = PERSONALITY_NAMES[next_p]
|
||||
self._toast_color = PERSONALITY_COLORS[next_p]
|
||||
self._toast_end_time = time.monotonic() + _TOAST_DURATION
|
||||
|
||||
def _render(self, _):
|
||||
fade = max(0.35, 1.0 - self.dm_fade_state)
|
||||
alpha = int(255 * fade)
|
||||
pc = self._personality_color
|
||||
|
||||
rl.draw_texture(
|
||||
self._dm_background,
|
||||
int(self.position_x - self._dm_background.width / 2),
|
||||
int(self.position_y - self._dm_background.height / 2),
|
||||
rl.Color(pc.r, pc.g, pc.b, alpha),
|
||||
)
|
||||
|
||||
rl.draw_texture(
|
||||
self._dm_person,
|
||||
int(self.position_x - self._dm_person.width / 2),
|
||||
int(self.position_y - self._dm_person.height / 2),
|
||||
rl.Color(255, 255, 255, int(alpha * 0.9)),
|
||||
)
|
||||
|
||||
if self.is_active:
|
||||
dest_rect = rl.Rectangle(self.position_x, self.position_y, self._dm_cone.width, self._dm_cone.height)
|
||||
rl.draw_texture_pro(
|
||||
self._dm_cone,
|
||||
rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height),
|
||||
dest_rect,
|
||||
rl.Vector2(dest_rect.width / 2, dest_rect.height / 2),
|
||||
180.0,
|
||||
rl.Color(pc.r, pc.g, pc.b, alpha),
|
||||
)
|
||||
else:
|
||||
rl.draw_circle(int(self.position_x), int(self.position_y), 14, rl.Color(255, 255, 255, alpha))
|
||||
|
||||
self._draw_personality_toast()
|
||||
|
||||
def _draw_personality_toast(self):
|
||||
now = time.monotonic()
|
||||
remaining = self._toast_end_time - now
|
||||
if remaining <= 0 or not self._toast_text:
|
||||
return
|
||||
|
||||
elapsed = _TOAST_DURATION - remaining
|
||||
fade_in = min(1.0, elapsed / _TOAST_FADE)
|
||||
fade_out = min(1.0, remaining / _TOAST_FADE)
|
||||
a = int(255 * fade_in * fade_out)
|
||||
|
||||
text_size = measure_text_cached(self._font, self._toast_text, _TOAST_FONT_SIZE)
|
||||
toast_w = text_size.x + _TOAST_PAD_X * 2
|
||||
toast_h = text_size.y + _TOAST_PAD_Y * 2
|
||||
|
||||
cx = self._rect.x + self._rect.width / 2
|
||||
toast_x = cx - toast_w / 2
|
||||
toast_y = self._rect.y + self._rect.height - _TOAST_BOTTOM_MARGIN - toast_h
|
||||
|
||||
tc = self._toast_color
|
||||
toast_rect = rl.Rectangle(toast_x, toast_y, toast_w, toast_h)
|
||||
rl.draw_rectangle_rounded(toast_rect, _TOAST_RADIUS, 10, rl.Color(tc.r, tc.g, tc.b, a))
|
||||
rl.draw_text_ex(
|
||||
self._font, self._toast_text,
|
||||
rl.Vector2(toast_x + _TOAST_PAD_X, toast_y + _TOAST_PAD_Y),
|
||||
_TOAST_FONT_SIZE, 0,
|
||||
rl.Color(255, 255, 255, a),
|
||||
)
|
||||
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = self._rect.y + height - offset - self.dev_ui_offset
|
||||
|
||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||
for i in range(len(positioned_keypoints)):
|
||||
self.face_lines[i].x = positioned_keypoints[i][0]
|
||||
self.face_lines[i].y = positioned_keypoints[i][1]
|
||||
|
||||
delta_x = -self.driver_pose_sins[1] * ARC_LENGTH / 2.0
|
||||
delta_y = -self.driver_pose_sins[0] * ARC_LENGTH / 2.0
|
||||
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
81
iqpilot/ui/onroad/emac_status.py
Normal file
81
iqpilot/ui/onroad/emac_status.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.ui.onroad.big_model_status import SourceState, draw_source_label, resolve_source
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
|
||||
_POLL_S = 1.0
|
||||
_FONT_SIZE = 70
|
||||
_ICON_H = 76
|
||||
_ICONS = {"MAC": ("mac", 62 / 46), "GPU": ("egpu", 1.0)}
|
||||
_GREY = rl.Color(165, 165, 170, 235)
|
||||
|
||||
|
||||
class EmacStatusRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._last_poll = 0.0
|
||||
self._label = ""
|
||||
self._state = SourceState.HIDDEN
|
||||
self._icons: dict[str, dict[str, rl.Texture]] = {}
|
||||
|
||||
def _icon_set(self, label: str) -> dict[str, rl.Texture] | None:
|
||||
spec = _ICONS.get(label)
|
||||
if spec is None:
|
||||
return None
|
||||
if label not in self._icons:
|
||||
base, aspect = spec
|
||||
w = int(_ICON_H * aspect)
|
||||
self._icons[label] = {
|
||||
"base": gui_app.texture(f"icons_mici/{base}.png", w, _ICON_H),
|
||||
"green": gui_app.texture(f"icons_mici/{base}_green.png", w, _ICON_H),
|
||||
"orange": gui_app.texture(f"icons_mici/{base}_orange.png", int(w * 1.26), _ICON_H),
|
||||
}
|
||||
return self._icons[label]
|
||||
|
||||
def update(self):
|
||||
now = time.monotonic()
|
||||
if now - self._last_poll < _POLL_S:
|
||||
return
|
||||
self._last_poll = now
|
||||
self._label, self._state = resolve_source(self._params, ui_state.engaged)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._state == SourceState.HIDDEN:
|
||||
return
|
||||
icons = self._icon_set(self._label)
|
||||
if icons is None:
|
||||
size = measure_text_cached(self._font, self._label, _FONT_SIZE)
|
||||
x = rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - size.x / 2
|
||||
y = rect.y + rect.height / 2 - size.y / 2
|
||||
draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE)
|
||||
return
|
||||
if self._state == SourceState.ACTIVE:
|
||||
tex, tint = icons["green"], rl.Color(255, 255, 255, 255)
|
||||
elif self._state == SourceState.FAILED:
|
||||
tex, tint = icons["orange"], rl.Color(255, 255, 255, 255)
|
||||
elif self._state == SourceState.CROSSED:
|
||||
tex, tint = icons["base"], rl.Color(255, 255, 255, 165)
|
||||
else:
|
||||
pulse = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
tex, tint = icons["base"], rl.Color(_GREY.r, _GREY.g, _GREY.b, int(_GREY.a * pulse))
|
||||
x = int(rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - tex.width / 2)
|
||||
y = int(rect.y + rect.height / 2 - tex.height / 2)
|
||||
rl.draw_texture(tex, x, y, tint)
|
||||
if self._state == SourceState.CROSSED:
|
||||
cy = y + tex.height // 2
|
||||
rl.draw_line_ex(rl.Vector2(x - 4, cy), rl.Vector2(x + tex.width + 4, cy), 4, rl.Color(255, 255, 255, 165))
|
||||
918
iqpilot/ui/onroad/hud_overlays.py
Normal file
918
iqpilot/ui/onroad/hud_overlays.py
Normal file
@@ -0,0 +1,918 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import math
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.cereal import car, custom
|
||||
from iqpilot.common.constants import CV
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.hud_renderer import COLORS, FONT_SIZES, UI_CONFIG
|
||||
from iqpilot.selfdrive.ui.mici.onroad.alert_renderer import IconSide, TURN_SIGNAL_BLINK_PERIOD
|
||||
from iqpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.iqwidgets.lib import canvas
|
||||
from iqdbc.car.volkswagen.values import VolkswagenFlags
|
||||
|
||||
def _feed():
|
||||
return ui_state.sm
|
||||
|
||||
|
||||
def _speed_scale() -> float:
|
||||
return CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
|
||||
_STRIP_WIDTH = 28
|
||||
_STRIP_INSET = 14
|
||||
_STRIP_CEILING = 0.85
|
||||
_STRIP_EMA = 5.0
|
||||
_STRIP_ARC_SEGMENTS = 24
|
||||
_STRIP_ACCEL = (33, 112, 115)
|
||||
_STRIP_ACCEL_NEON = (94, 232, 236)
|
||||
_STRIP_DECEL = (255, 0, 247)
|
||||
_STRIP_DECEL_NEON = (255, 145, 251)
|
||||
_STRIP_TIP_ALPHA = 235
|
||||
_STRIP_ROOT_ALPHA = 55
|
||||
_STRIP_CORE_DEPTH = 7.0
|
||||
_STRIP_CORE_LAYERS = 8
|
||||
_STRIP_CORE_ALPHA = 0.15
|
||||
_STRIP_CORE_FLOOR = 0.3
|
||||
_STRIP_CORE_TAIL = 22.0
|
||||
_STRIP_HALO_SPREAD = 11.0
|
||||
_STRIP_HALO_LAYERS = 11
|
||||
_STRIP_HALO_ALPHA = 0.095
|
||||
_STRIP_HALO_TAIL = 34.0
|
||||
_STRIP_NEON_FULL = 2.0
|
||||
|
||||
|
||||
class IQAccelBar:
|
||||
def __init__(self):
|
||||
self._eased = 0.0
|
||||
|
||||
def _reach(self) -> float:
|
||||
mag = abs(self._eased)
|
||||
return 0.0 if mag == 0.0 else max(0.0, _STRIP_CEILING - 0.1 / mag)
|
||||
|
||||
@staticmethod
|
||||
def _tint(fill, frac: float):
|
||||
return canvas.shade(*fill, int(_STRIP_TIP_ALPHA + (_STRIP_ROOT_ALPHA - _STRIP_TIP_ALPHA) * frac))
|
||||
|
||||
@staticmethod
|
||||
def _halo(center, cap: float, up: bool, neon, heat: float, tail: float) -> None:
|
||||
step = int(255 * _STRIP_HALO_ALPHA * heat)
|
||||
if step < 1:
|
||||
return
|
||||
start, end = (180.0, 360.0) if up else (0.0, 180.0)
|
||||
tint = canvas.shade(*neon, step)
|
||||
faded = canvas.shade(*neon, 0)
|
||||
top, bottom = (tint, faded) if up else (faded, tint)
|
||||
for i in range(_STRIP_HALO_LAYERS):
|
||||
spread = _STRIP_HALO_SPREAD * (1.0 - i / _STRIP_HALO_LAYERS)
|
||||
if spread <= 0.0:
|
||||
continue
|
||||
canvas.annulus(center, cap, cap + spread, start, end, _STRIP_ARC_SEGMENTS, tint)
|
||||
if tail <= 0.0:
|
||||
continue
|
||||
run = min(tail, _STRIP_HALO_TAIL)
|
||||
y = center.y if up else center.y - run
|
||||
canvas.v_sweep(center.x - cap - spread, y, spread, run, top, bottom)
|
||||
canvas.v_sweep(center.x + cap, y, spread, run, top, bottom)
|
||||
|
||||
@staticmethod
|
||||
def _cap(center, cap: float, up: bool, fill, neon, heat: float, tail: float) -> None:
|
||||
start, end = (180.0, 360.0) if up else (0.0, 180.0)
|
||||
canvas.annulus(center, 0.0, cap, start, end, _STRIP_ARC_SEGMENTS, fill)
|
||||
IQAccelBar._halo(center, cap, up, neon, heat, tail)
|
||||
lit = _STRIP_CORE_FLOOR + (1.0 - _STRIP_CORE_FLOOR) * heat
|
||||
core = canvas.shade(*neon, max(1, int(255 * _STRIP_CORE_ALPHA * lit)))
|
||||
faded = canvas.shade(*neon, 0)
|
||||
top, bottom = (core, faded) if up else (faded, core)
|
||||
run = min(tail, _STRIP_CORE_TAIL)
|
||||
for i in range(_STRIP_CORE_LAYERS):
|
||||
depth = _STRIP_CORE_DEPTH * (1.0 - i / _STRIP_CORE_LAYERS)
|
||||
if depth <= 0.0:
|
||||
continue
|
||||
canvas.annulus(center, max(0.0, cap - depth), cap, start, end, _STRIP_ARC_SEGMENTS, core)
|
||||
if run <= 0.0:
|
||||
continue
|
||||
y = center.y if up else center.y - run
|
||||
canvas.v_sweep(center.x - cap, y, depth, run, top, bottom)
|
||||
canvas.v_sweep(center.x + cap - depth, y, depth, run, top, bottom)
|
||||
|
||||
def render(self, rect, sm) -> None:
|
||||
if not ui_state.rocket_fuel:
|
||||
return
|
||||
self._eased += (sm['carState'].aEgo - self._eased) / _STRIP_EMA
|
||||
reach = self._reach() * rect.height / 2.0
|
||||
if reach <= 0.0:
|
||||
return
|
||||
accelerating = self._eased > 0.0
|
||||
mid = rect.y + rect.height / 2.0
|
||||
top = mid - reach if accelerating else mid
|
||||
x = rect.x + _STRIP_INSET
|
||||
cap = min(_STRIP_WIDTH / 2.0, reach / 2.0)
|
||||
fill, neon = (_STRIP_ACCEL, _STRIP_ACCEL_NEON) if accelerating else (_STRIP_DECEL, _STRIP_DECEL_NEON)
|
||||
near = cap / reach
|
||||
frac_top, frac_bottom = (near, 1.0 - near) if accelerating else (1.0 - near, near)
|
||||
heat = min(abs(self._eased) / _STRIP_NEON_FULL, 1.0)
|
||||
|
||||
canvas.v_sweep(x, top + cap, cap * 2.0, reach - cap * 2.0,
|
||||
self._tint(fill, frac_top), self._tint(fill, frac_bottom))
|
||||
tail = max(0.0, reach / 2.0 - cap)
|
||||
self._cap(canvas.Pt(x + cap, top + cap), cap, True, self._tint(fill, frac_top), neon, heat, tail)
|
||||
self._cap(canvas.Pt(x + cap, top + reach - cap), cap, False, self._tint(fill, frac_bottom), neon, heat, tail)
|
||||
|
||||
_BS_INSET = 20
|
||||
_BS_DROP = 100
|
||||
_BS_MIN = 0.01
|
||||
|
||||
|
||||
class _BlindSide:
|
||||
def __init__(self, name: str):
|
||||
self.texture = gui_app.texture(f'icons_mici/onroad/blind_spot_{name}.png', 108, 128)
|
||||
self.glow = FirstOrderFilter(0, 0.15, 1 / gui_app.target_fps)
|
||||
self.on_left = name == "left"
|
||||
|
||||
def feed(self, present: bool):
|
||||
self.glow.update(1.0 if present else 0.0)
|
||||
|
||||
def lit(self) -> bool:
|
||||
return self.glow.x > _BS_MIN
|
||||
|
||||
def place(self, rect):
|
||||
tex = self.texture
|
||||
x = rect.x + _BS_INSET if self.on_left else rect.x + rect.width - _BS_INSET - tex.width
|
||||
canvas.stamp(tex, x, rect.y + _BS_DROP, canvas.shade(255, 255, 255, int(255 * self.glow.x)))
|
||||
|
||||
|
||||
class IQBlindSpotOverlay:
|
||||
def __init__(self):
|
||||
self._left = _BlindSide("left")
|
||||
self._right = _BlindSide("right")
|
||||
|
||||
def update(self) -> None:
|
||||
cs = _feed()['carState']
|
||||
self._left.feed(cs.leftBlindspot)
|
||||
self._right.feed(cs.rightBlindspot)
|
||||
|
||||
@property
|
||||
def detected(self) -> bool:
|
||||
return self._left.lit() or self._right.lit()
|
||||
|
||||
def render(self, rect) -> None:
|
||||
if not ui_state.blindspot:
|
||||
return
|
||||
for side in (self._left, self._right):
|
||||
if side.lit():
|
||||
side.place(rect)
|
||||
|
||||
_MQB_CLUSTER_EXEMPT = (VolkswagenFlags.PQ | VolkswagenFlags.MLB | VolkswagenFlags.MEB | VolkswagenFlags.MEB_GEN2 | VolkswagenFlags.MQB_EVO)
|
||||
|
||||
class IQSpeedOverlay:
|
||||
def __init__(self):
|
||||
self.speed: float = 0.0
|
||||
self._cluster_ever_live: bool = False
|
||||
self._heavy = gui_app.font(FontWeight.BOLD)
|
||||
self._mid = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _source_speed(self, cs) -> float:
|
||||
cp = ui_state.CP
|
||||
if cp is not None and cp.brand == "volkswagen" and not (cp.flags & _MQB_CLUSTER_EXEMPT):
|
||||
return cs.vEgoCluster
|
||||
self._cluster_ever_live = self._cluster_ever_live or cs.vEgoCluster != 0.0
|
||||
return cs.vEgoCluster if self._cluster_ever_live else cs.vEgo
|
||||
|
||||
def update(self) -> None:
|
||||
self.speed = max(0.0, self._source_speed(_feed()['carState']) * _speed_scale())
|
||||
|
||||
def _stack(self, face, text: str, size: int, rect, top: float, color) -> float:
|
||||
extent = canvas.span(face, text, size)
|
||||
canvas.glyphs(face, text, canvas.Pt(rect.x + (rect.width - extent.x) / 2, top), size, color)
|
||||
return extent.y
|
||||
|
||||
def render(self, rect) -> None:
|
||||
top = rect.y + 52
|
||||
number_h = self._stack(self._heavy, str(round(self.speed)), FONT_SIZES.current_speed, rect, top, COLORS.WHITE)
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
self._stack(self._mid, unit, FONT_SIZES.speed_unit, rect, top + number_h - 10, COLORS.WHITE_TRANSLUCENT)
|
||||
|
||||
def clip_to_width(font, words: str, size: int, limit: float) -> str:
|
||||
if canvas.span(font, words, size).x <= limit:
|
||||
return words
|
||||
trimmed = words
|
||||
while len(trimmed) > 3 and canvas.span(font, trimmed + "...", size).x > limit:
|
||||
trimmed = trimmed[:-1]
|
||||
return trimmed + "..."
|
||||
|
||||
|
||||
class RoadNameBanner(Widget):
|
||||
TYPE_SIZE = 46
|
||||
MARGIN = 40
|
||||
GAP = 10
|
||||
TORQUE_SCALE = 3.0
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.road_name = ""
|
||||
self._face = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
def update(self):
|
||||
sm = _feed()
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return
|
||||
if sm.updated["iqLiveData"]:
|
||||
self.road_name = sm["iqLiveData"].roadName
|
||||
|
||||
def _render(self, rect):
|
||||
if not self.road_name or not ui_state.road_name_toggle:
|
||||
return
|
||||
label = clip_to_width(self._face, self.road_name, self.TYPE_SIZE, rect.width - self.MARGIN)
|
||||
extent = canvas.span(self._face, label, self.TYPE_SIZE)
|
||||
top = min(TorqueBar.resting_bottom(rect, self.TORQUE_SCALE) + self.GAP,
|
||||
rect.y + rect.height - extent.y)
|
||||
canvas.glyphs(self._face, label,
|
||||
canvas.Pt(rect.x + (rect.width - extent.x) / 2, top),
|
||||
self.TYPE_SIZE, COLORS.WHITE)
|
||||
|
||||
|
||||
RoadNameRenderer = RoadNameBanner
|
||||
ellipsize = clip_to_width
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
_ARROW = 'signal'
|
||||
_WARN = 'blind_spot'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TurnSignalConfig:
|
||||
left_x: int = 80
|
||||
left_y: int = 190
|
||||
right_x: int = 80
|
||||
right_y: int = 190
|
||||
size: int = 150
|
||||
|
||||
class _IndicatorLamp(Widget):
|
||||
def __init__(self, direction: IconSide):
|
||||
super().__init__()
|
||||
self.mode: str | None = None
|
||||
self._epoch = 0.0
|
||||
self._glow = FirstOrderFilter(0.0, 0.3, 1 / gui_app.target_fps)
|
||||
self._art = {
|
||||
_ARROW: gui_app.texture(f'icons_mici/onroad/turn_signal_{direction}.png', 120, 109),
|
||||
_WARN: gui_app.texture(f'icons_mici/onroad/blind_spot_{direction}.png', 120, 109),
|
||||
}
|
||||
|
||||
def set_mode(self, mode: str | None):
|
||||
if mode != self.mode or mode is None:
|
||||
self._epoch = 0.0
|
||||
self.mode = mode
|
||||
|
||||
def _pulse(self) -> int:
|
||||
self._glow.dt = 1 / gui_app.target_fps
|
||||
self._glow.update_alpha(0.3)
|
||||
if time.monotonic() - self._epoch > TURN_SIGNAL_BLINK_PERIOD:
|
||||
self._epoch = time.monotonic()
|
||||
self._glow.x = 255 * 2
|
||||
else:
|
||||
self._glow.update(255 * 0.2)
|
||||
return int(min(self._glow.x, 255))
|
||||
|
||||
def _render(self, _):
|
||||
if self.mode is None:
|
||||
return
|
||||
alpha = self._pulse() if self.mode == _ARROW else 255
|
||||
tex = self._art[self.mode]
|
||||
canvas.stamp(tex, self._rect.x + (self._rect.width - tex.width) / 2,
|
||||
self._rect.y + (self._rect.height - tex.height) / 2, canvas.shade(255, 255, 255, alpha))
|
||||
|
||||
|
||||
def _lamp_modes(event_name: str, cs, remembered):
|
||||
if event_name == 'preLaneChangeLeft':
|
||||
return _ARROW, None, IconSide.left
|
||||
if event_name == 'preLaneChangeRight':
|
||||
return None, _ARROW, IconSide.right
|
||||
if event_name == 'laneChange':
|
||||
if remembered == IconSide.left:
|
||||
return _ARROW, None, remembered
|
||||
if remembered == IconSide.right:
|
||||
return None, _ARROW, remembered
|
||||
return None, None, remembered
|
||||
if event_name == 'laneChangeBlocked':
|
||||
side = IconSide.left if cs.leftBlinker else IconSide.right if cs.rightBlinker else remembered
|
||||
if side == IconSide.left:
|
||||
return _WARN, None, remembered
|
||||
if side == IconSide.right:
|
||||
return None, _WARN, remembered
|
||||
return None, None, remembered
|
||||
left = _WARN if cs.leftBlindspot else _ARROW if cs.leftBlinker else None
|
||||
right = _WARN if cs.rightBlindspot else _ARROW if cs.rightBlinker else None
|
||||
return left, right, None
|
||||
|
||||
|
||||
class IQTurnSignalOverlay:
|
||||
def __init__(self, config: TurnSignalConfig | None = None):
|
||||
self._config = config or TurnSignalConfig()
|
||||
self._lamps = {IconSide.left: _IndicatorLamp(IconSide.left),
|
||||
IconSide.right: _IndicatorLamp(IconSide.right)}
|
||||
self._remembered: IconSide | None = None
|
||||
|
||||
def update(self):
|
||||
sm = _feed()
|
||||
alert = sm['selfdriveState'].alertType
|
||||
event_name = alert.split('/')[0] if alert else ''
|
||||
left, right, self._remembered = _lamp_modes(event_name, sm['carState'], self._remembered)
|
||||
self._lamps[IconSide.left].set_mode(left)
|
||||
self._lamps[IconSide.right].set_mode(right)
|
||||
|
||||
def render(self, rect):
|
||||
if not ui_state.turn_signals:
|
||||
return
|
||||
c = self._config
|
||||
mid_x = rect.x + rect.width / 2
|
||||
spots = {
|
||||
IconSide.left: canvas.Box(mid_x - c.left_x - c.size, rect.y + c.left_y, c.size, c.size),
|
||||
IconSide.right: canvas.Box(mid_x + c.right_x, rect.y + c.right_y, c.size, c.size),
|
||||
}
|
||||
for side, lamp in self._lamps.items():
|
||||
if lamp.mode is not None:
|
||||
lamp.render(spots[side])
|
||||
|
||||
@property
|
||||
def config(self) -> TurnSignalConfig:
|
||||
return self._config
|
||||
|
||||
@config.setter
|
||||
def config(self, new_config: TurnSignalConfig):
|
||||
self._config = new_config
|
||||
|
||||
_PROVIDER_TAGS = {0: "", 1: "NAV", 2: "MBX", 3: "VIS", 4: "OSM"}
|
||||
_NAV_TEX_W, _NAV_TEX_H = 256, 128
|
||||
_NAV_BADGE_W = 160
|
||||
_NAV_FONT = 36
|
||||
_NAV_SHIFT = -260
|
||||
|
||||
class NavInfluenceRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.engaged = False
|
||||
self.valid = False
|
||||
self.provider = 0
|
||||
self.long_override = False
|
||||
self._streak = 0
|
||||
self.font = gui_app.font(FontWeight.BOLD)
|
||||
self._offscreen = rl.load_render_texture(_NAV_TEX_W, _NAV_TEX_H)
|
||||
|
||||
def update(self):
|
||||
sm = _feed()
|
||||
if sm.updated["iqPlan"]:
|
||||
nav = sm["iqPlan"].iqNavState.nav
|
||||
self.engaged = nav.engaged
|
||||
self.valid = nav.valid
|
||||
self.provider = getattr(nav.provider, "raw", nav.provider)
|
||||
if sm.updated["carControl"]:
|
||||
self.long_override = sm["carControl"].cruiseControl.override
|
||||
self._streak = self._streak + 1 if (self.engaged and self.valid) else 0
|
||||
|
||||
def _blinked_off(self) -> bool:
|
||||
fps = gui_app.target_fps
|
||||
return self.engaged and (self._streak % fps) < (fps / 2.5)
|
||||
|
||||
def _bake(self, label: str):
|
||||
extent = canvas.span(self.font, label, _NAV_FONT)
|
||||
badge = canvas.Box((_NAV_TEX_W - _NAV_BADGE_W) // 2, (_NAV_TEX_H - extent.y - 10) // 2,
|
||||
_NAV_BADGE_W, int(extent.y + 10))
|
||||
rl.begin_texture_mode(self._offscreen)
|
||||
rl.clear_background(canvas.CLEAR)
|
||||
canvas.panel(badge, 0.2, 10, COLORS.OVERRIDE if self.long_override else canvas.shade(0, 255, 0, 255))
|
||||
rl.rl_set_blend_factors(rl.RL_ZERO, rl.RL_ONE_MINUS_SRC_ALPHA, 0x8006)
|
||||
rl.rl_set_blend_mode(rl.BLEND_CUSTOM)
|
||||
canvas.glyphs(self.font, label,
|
||||
canvas.Pt(badge.x + (badge.width - extent.x) / 2, badge.y + (badge.height - extent.y) / 2),
|
||||
_NAV_FONT, canvas.WHITE)
|
||||
rl.rl_set_blend_mode(rl.BLEND_ALPHA)
|
||||
rl.end_texture_mode()
|
||||
|
||||
def _render(self, rect):
|
||||
if not self.valid or self._blinked_off():
|
||||
return
|
||||
label = _PROVIDER_TAGS.get(int(self.provider), "NAV")
|
||||
if not label:
|
||||
return
|
||||
self._bake(label)
|
||||
ax = rect.x + rect.width / 2 + _NAV_SHIFT - _NAV_TEX_W / 2
|
||||
ay = (rect.height / 4 - 40) - _NAV_TEX_H / 2
|
||||
canvas.stamp_scaled(self._offscreen.texture, canvas.Box(0, 0, _NAV_TEX_W, -_NAV_TEX_H),
|
||||
canvas.Box(ax, ay, _NAV_TEX_W, _NAV_TEX_H), canvas.Pt(0, 0), 0, canvas.WHITE)
|
||||
|
||||
class ChevronOptions:
|
||||
OFF = 0
|
||||
DISTANCE_ONLY = 1
|
||||
SPEED_ONLY = 2
|
||||
TTC_ONLY = 3
|
||||
ALL = 4
|
||||
|
||||
_CH_FONT = 40
|
||||
_CH_LINE = 50
|
||||
_CH_MARGIN = 20
|
||||
_CH_FADE_DOWN = 0.05
|
||||
_CH_FADE_UP = 0.1
|
||||
_CH_DEDUP = 3.0
|
||||
|
||||
def _gap_label(d_rel: float, _v_abs: float) -> str:
|
||||
val = max(0.0, d_rel)
|
||||
return f"{val:.0f} m" if ui_state.is_metric else f"{val * 3.28084:.0f} ft"
|
||||
|
||||
def _pace_label(_d_rel: float, v_abs: float) -> str:
|
||||
unit = "km/h" if ui_state.is_metric else "mph"
|
||||
return f"{max(0.0, v_abs * _speed_scale()):.0f} {unit}"
|
||||
|
||||
def _ttc_label(d_rel: float, _v_abs: float, v_ego: float) -> str:
|
||||
ttc = (d_rel / v_ego) if (d_rel > 0 and v_ego > 0) else 0.0
|
||||
return f"{ttc:.1f} s" if 0 < ttc < 200 else "---"
|
||||
|
||||
_CH_METRICS = (
|
||||
((ChevronOptions.DISTANCE_ONLY, ChevronOptions.ALL), lambda d, va, ve: _gap_label(d, va)),
|
||||
((ChevronOptions.SPEED_ONLY, ChevronOptions.ALL), lambda d, va, ve: _pace_label(d, va)),
|
||||
((ChevronOptions.TTC_ONLY, ChevronOptions.ALL), _ttc_label),
|
||||
)
|
||||
|
||||
class ChevronMetrics:
|
||||
def __init__(self):
|
||||
self._alpha: float = 0.0
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
def update_alpha(self, has_lead: bool):
|
||||
self._alpha = float(np.clip(self._alpha + (_CH_FADE_UP if has_lead else -_CH_FADE_DOWN), 0.0, 1.0))
|
||||
|
||||
def should_render(self) -> bool:
|
||||
return ui_state.chevron_metrics != ChevronOptions.OFF and self._alpha > 0.0
|
||||
|
||||
@staticmethod
|
||||
def _marker_size(d_rel: float) -> float:
|
||||
return float(np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0)) * 2.35
|
||||
|
||||
def _labels(self, d_rel: float, v_rel: float, v_ego: float) -> list[str]:
|
||||
mode = ui_state.chevron_metrics
|
||||
return [fn(d_rel, v_rel + v_ego, v_ego) for modes, fn in _CH_METRICS if mode in modes]
|
||||
|
||||
def _top_y(self, anchor_y: float, size: float, n: int, rect) -> float:
|
||||
y = anchor_y + size + 15
|
||||
block = n * _CH_LINE
|
||||
floor = rect.y + rect.height - _CH_MARGIN
|
||||
if y + block > floor:
|
||||
y = max(rect.y + _CH_MARGIN, min(anchor_y, floor) - 15 - block)
|
||||
return y
|
||||
|
||||
def _stack(self, lines, cx: float, top: float, rect):
|
||||
a = self._alpha
|
||||
fg = canvas.shade(255, 255, 255, int(255 * a))
|
||||
shadow = canvas.shade(0, 0, 0, int(200 * a))
|
||||
floor = rect.y + rect.height - _CH_MARGIN
|
||||
for i, line in enumerate(lines):
|
||||
y = int(top + i * _CH_LINE)
|
||||
if y + _CH_LINE > floor:
|
||||
break
|
||||
w = canvas.span(self._font, line, _CH_FONT).x
|
||||
x = int(np.clip(cx - w / 2, rect.x + _CH_MARGIN, rect.x + rect.width - w - _CH_MARGIN))
|
||||
canvas.glyphs(self._font, line, canvas.Pt(x + 2, y + 2), _CH_FONT, shadow)
|
||||
canvas.glyphs(self._font, line, canvas.Pt(x, y), _CH_FONT, fg)
|
||||
|
||||
def _one_lead(self, lead, marker, v_ego: float, rect):
|
||||
if not self.should_render() or marker.center is None:
|
||||
return
|
||||
lines = self._labels(lead.dRel, lead.vRel, v_ego)
|
||||
if not lines:
|
||||
return
|
||||
self._stack(lines, marker.center[0], self._top_y(marker.center[1], self._marker_size(lead.dRel), len(lines), rect), rect)
|
||||
|
||||
@staticmethod
|
||||
def _active_leads(radar_state, markers):
|
||||
tracked = []
|
||||
for lead, marker in zip((radar_state.leadOne, radar_state.leadTwo), markers, strict=False):
|
||||
if lead and lead.status and marker.center is not None:
|
||||
tracked.append((lead, marker))
|
||||
if len(tracked) == 2 and abs(tracked[0][0].dRel - tracked[1][0].dRel) <= _CH_DEDUP:
|
||||
tracked.pop()
|
||||
return tracked
|
||||
|
||||
def draw_lead_status(self, sm, radar_state, rect, lead_vehicles):
|
||||
present = [radar_state.leadOne, radar_state.leadTwo]
|
||||
self.update_alpha(any(bool(x) and x.status for x in present))
|
||||
if not self.should_render():
|
||||
return
|
||||
v_ego = sm['carState'].vEgo
|
||||
for lead, marker in self._active_leads(radar_state, lead_vehicles):
|
||||
self._one_lead(lead, marker, v_ego, rect)
|
||||
|
||||
_TEAL = canvas.shade(0x0C, 0x94, 0x96, 0xFF)
|
||||
_AMBER = canvas.shade(255, 188, 0, 255)
|
||||
_GREEN = canvas.shade(0, 255, 0, 255)
|
||||
_GREY = canvas.shade(145, 155, 149, 255)
|
||||
_G = 9.81
|
||||
_BAR_FONT = 38
|
||||
_ANGLE_TYPES = (car.CarParams.SteerControlType.angle, car.CarParams.SteerControlType.curvatureDEPRECATED)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Readout:
|
||||
tag: str
|
||||
value: str
|
||||
unit: str = ""
|
||||
color: object = field(default_factory=lambda: canvas.WHITE)
|
||||
tag_text: str = ""
|
||||
value_text: str = ""
|
||||
unit_text: str = ""
|
||||
tag_w: float = 0.0
|
||||
value_w: float = 0.0
|
||||
unit_w: float = 0.0
|
||||
span: float = 0.0
|
||||
|
||||
def size_up(self, font, px: int):
|
||||
self.tag_text = f"{self.tag} "
|
||||
self.value_text = self.value
|
||||
self.unit_text = f" {self.unit}" if self.unit else ""
|
||||
self.tag_w = canvas.span(font, self.tag_text, px, 0).x
|
||||
self.value_w = canvas.span(font, self.value_text, px, 0).x
|
||||
self.unit_w = canvas.span(font, self.unit_text, px, 0).x if self.unit else 0
|
||||
self.span = self.tag_w + self.value_w + self.unit_w
|
||||
|
||||
@property
|
||||
def total_width(self):
|
||||
return self.span
|
||||
|
||||
def measure(self, font, px):
|
||||
self.size_up(font, px)
|
||||
|
||||
|
||||
UiElement = Readout
|
||||
|
||||
def _banded(magnitude, warn, crit, ok):
|
||||
if magnitude > crit:
|
||||
return canvas.RED
|
||||
return _AMBER if magnitude > warn else ok
|
||||
|
||||
def _closing(v_rel):
|
||||
return _banded(-v_rel if v_rel < 0 else 0.0, 0.0, 4.4704, canvas.WHITE)
|
||||
|
||||
def _following(d_rel):
|
||||
if d_rel < 5:
|
||||
return canvas.RED
|
||||
return _AMBER if d_rel < 15 else canvas.WHITE
|
||||
|
||||
def _steer_tint(sm):
|
||||
if not sm['carControl'].latActive:
|
||||
return canvas.WHITE
|
||||
return _GREY if sm['carState'].steeringPressed else _TEAL
|
||||
|
||||
def _angle_tint(sm, deg):
|
||||
floor = _steer_tint(sm) if sm['carControl'].latActive else canvas.WHITE
|
||||
return _banded(abs(deg), 90.0, 180.0, floor)
|
||||
|
||||
def _yaw_offset(sm):
|
||||
return sm['vehicleParameters'].angleOffsetAverageDeg if sm.valid['vehicleParameters'] else 0.0
|
||||
|
||||
def _bank(sm):
|
||||
return sm['vehicleParameters'].roll if sm.valid['vehicleParameters'] else 0.0
|
||||
|
||||
def _units(is_metric):
|
||||
return (CV.MS_TO_KPH, "km/h") if is_metric else (CV.MS_TO_MPH, "mph")
|
||||
|
||||
def _fix(sm):
|
||||
for svc in ('gpsLocationExternal', 'gpsLocation'):
|
||||
if sm.valid[svc]:
|
||||
return sm[svc], svc
|
||||
return None, None
|
||||
|
||||
def steering_angle(sm, is_metric):
|
||||
deg = sm['carState'].steeringAngleDeg - _yaw_offset(sm)
|
||||
return Readout("R.S.", f"{deg:.1f}°", color=_angle_tint(sm, deg))
|
||||
|
||||
def desired_steering_angle(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
off = _yaw_offset(sm)
|
||||
lat = sm['controlsState'].lateralControlState
|
||||
if lat.which() == 'angleState':
|
||||
want = lat.angleState.steeringAngleDesiredDeg - off
|
||||
else:
|
||||
want = sm['carControl'].actuators.steeringAngleDeg - off
|
||||
seen = sm['carState'].steeringAngleDeg - off
|
||||
tint = _banded(abs(seen), 90.0, 180.0, _TEAL) if live else canvas.WHITE
|
||||
return Readout("D.S.", f"{want:.1f}°" if live else "-", color=tint)
|
||||
|
||||
def desired_steering_pid(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
off = _yaw_offset(sm)
|
||||
want = sm['controlsState'].lateralControlState.pidState.steeringAngleDesiredDeg - off
|
||||
seen = sm['carState'].steeringAngleDeg - off
|
||||
tint = _banded(abs(seen), 90.0, 180.0, _TEAL) if live else canvas.WHITE
|
||||
return Readout("D.S.", f"{want:.1f}°" if live else "-", color=tint)
|
||||
|
||||
def actual_lat_accel(sm, is_metric):
|
||||
a = sm['controlsState'].curvature * sm['carState'].vEgo ** 2 - _bank(sm) * _G
|
||||
return Readout("A.L.A.", f"{a:.2f}", "m/s^2", _steer_tint(sm))
|
||||
|
||||
def desired_lat_accel(sm, is_metric):
|
||||
live = sm['carControl'].latActive
|
||||
a = sm['controlsState'].desiredCurvature * sm['carState'].vEgo ** 2 - _bank(sm) * _G
|
||||
return Readout("D.L.A.", f"{a:.2f}" if live else "-", "m/s^2", _steer_tint(sm))
|
||||
|
||||
def a_ego(sm, is_metric):
|
||||
return Readout("L.ACC.", f"{sm['carState'].aEgo:.1f}", "m/s^2")
|
||||
|
||||
def lead_distance(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
return Readout("REL DIST", "-", "m") if not lead.status else Readout("REL DIST", f"{lead.dRel:.0f}", "m", _following(lead.dRel))
|
||||
|
||||
def lead_rel_speed(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
k, unit = _units(is_metric)
|
||||
return Readout("REL SPEED", "-", unit) if not lead.status else Readout("REL SPEED", f"{lead.vRel * k:.0f}", unit, _closing(lead.vRel))
|
||||
|
||||
def lead_speed(sm, is_metric):
|
||||
lead = sm['radarState'].leadOne
|
||||
k, unit = _units(is_metric)
|
||||
if not lead.status:
|
||||
return Readout("L.S.", "-", unit)
|
||||
return Readout("L.S.", f"{(lead.vRel + sm['carState'].vEgo) * k:.0f}", unit, _closing(lead.vRel))
|
||||
|
||||
def friction_coefficient(sm, is_metric):
|
||||
ltp = sm['lateralTorqueParameters']
|
||||
return Readout("FRIC.", f"{ltp.frictionCoefficientFiltered:.3f}", color=_GREEN if ltp.valid else canvas.WHITE)
|
||||
|
||||
def lat_accel_factor(sm, is_metric):
|
||||
ltp = sm['lateralTorqueParameters']
|
||||
return Readout("L.A.F.", f"{ltp.latAccelFactorFiltered:.3f}", color=_GREEN if ltp.valid else canvas.WHITE)
|
||||
|
||||
def eps_torque(sm, is_metric):
|
||||
return Readout("E.T.", f"{abs(sm['carState'].steeringTorqueEps):.1f}", "N·dm")
|
||||
|
||||
_COMPASS = ("N", "NE", "E", "SE", "S", "SW", "W", "NW")
|
||||
|
||||
def bearing(sm, is_metric):
|
||||
fix, _ = _fix(sm)
|
||||
if fix is None or fix.bearingAccuracyDeg == 180.0:
|
||||
return Readout("B.D.", "OFF | -")
|
||||
heading = _COMPASS[int(((fix.bearingDeg + 22.5) % 360) // 45)]
|
||||
return Readout("B.D.", f"{heading} | {fix.bearingDeg:.0f}°")
|
||||
|
||||
def altitude(sm, is_metric):
|
||||
fix, svc = _fix(sm)
|
||||
if fix is None:
|
||||
return Readout("ALT.", "-", "m")
|
||||
acc = fix.horizontalAccuracy if svc == 'gpsLocationExternal' else 1.0
|
||||
return Readout("ALT.", f"{fix.altitude:.1f}" if acc != 0.0 else "-", "m")
|
||||
|
||||
def _desired_probe(sm):
|
||||
if sm['controlsState'].lateralControlState.which() == 'angleState':
|
||||
return desired_steering_angle
|
||||
if ui_state.CP is not None and ui_state.CP.steerControlType in _ANGLE_TYPES:
|
||||
return desired_steering_angle
|
||||
if sm['controlsState'].lateralControlState.which() == 'pidState':
|
||||
return desired_steering_pid
|
||||
return desired_lat_accel
|
||||
|
||||
class IQDevMetricsOverlay(Widget):
|
||||
DEV_UI_OFF = 0
|
||||
DEV_UI_RIGHT = 1
|
||||
DEV_UI_BOTTOM = 2
|
||||
DEV_UI_BOTH = 3
|
||||
BOTTOM_BAR_HEIGHT = 61
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._face = gui_app.font(FontWeight.BOLD)
|
||||
self.dev_ui_mode = self.DEV_UI_OFF
|
||||
|
||||
@staticmethod
|
||||
def get_bottom_dev_ui_offset():
|
||||
return IQDevMetricsOverlay.BOTTOM_BAR_HEIGHT if ui_state.developer_ui != IQDevMetricsOverlay.DEV_UI_OFF else 0
|
||||
|
||||
def _update_state(self) -> None:
|
||||
self.dev_ui_mode = ui_state.developer_ui
|
||||
|
||||
def _render(self, rect) -> None:
|
||||
if self.dev_ui_mode == self.DEV_UI_OFF:
|
||||
return
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return
|
||||
self._paint_bar(rect)
|
||||
|
||||
def _gather(self, sm):
|
||||
probes = (_desired_probe(sm), actual_lat_accel, steering_angle, a_ego, lead_speed)
|
||||
cells = [probe(sm, ui_state.is_metric) for probe in probes]
|
||||
for cell in cells:
|
||||
cell.size_up(self._face, _BAR_FONT)
|
||||
return cells
|
||||
|
||||
def _paint_bar(self, rect) -> None:
|
||||
height = self.BOTTOM_BAR_HEIGHT
|
||||
top = int(rect.y + rect.height - height)
|
||||
canvas.slab(rect.x, top, rect.width, height, canvas.shade(0, 0, 0, 100))
|
||||
|
||||
cells = self._gather(ui_state.sm)
|
||||
slack = (rect.width - sum(c.span for c in cells)) / (len(cells) + 1)
|
||||
baseline = top + height // 2 - _BAR_FONT // 2
|
||||
|
||||
cursor = rect.x + slack
|
||||
for cell in cells:
|
||||
self._paint_cell(cursor, baseline, cell)
|
||||
cursor += cell.span + slack
|
||||
|
||||
def _paint_cell(self, x, y, cell) -> None:
|
||||
canvas.glyphs(self._face, cell.tag_text, canvas.Pt(x, y), _BAR_FONT, canvas.WHITE)
|
||||
canvas.glyphs(self._face, cell.value_text, canvas.Pt(x + cell.tag_w, y), _BAR_FONT, cell.color)
|
||||
if cell.unit:
|
||||
canvas.glyphs(self._face, cell.unit_text, canvas.Pt(x + cell.tag_w + cell.value_w, y), _BAR_FONT, canvas.WHITE)
|
||||
|
||||
_SL_M_TO_FT = 3.28084
|
||||
_SL_M_TO_MI = 0.000621371
|
||||
_SL_AHEAD_STEPS = 5
|
||||
_SL_ASSIST = custom.IQPlan.SpeedLimit.AssistState
|
||||
_SL_SOURCE = custom.IQPlan.SpeedLimit.Source
|
||||
_SL_GREY = canvas.shade(145, 155, 149, 255)
|
||||
_SL_DARK = canvas.shade(77, 77, 77, 255)
|
||||
_SL_PANEL_BG = canvas.shade(0, 0, 0, 180)
|
||||
_SL_PANEL_EDGE = canvas.shade(255, 255, 255, 100)
|
||||
|
||||
def _dim(color, alpha: float):
|
||||
return canvas.with_opacity(color, 255 * alpha)
|
||||
|
||||
class IQSpeedLimitOverlay(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.speed_limit = 0.0
|
||||
self.speed_limit_last = 0.0
|
||||
self.speed_limit_offset = 0.0
|
||||
self.speed_limit_valid = False
|
||||
self.speed_limit_last_valid = False
|
||||
self.speed_limit_final_last = 0.0
|
||||
self.speed_limit_source = _SL_SOURCE.none
|
||||
self.assist_state = _SL_ASSIST.disabled
|
||||
|
||||
self.ahead_limit = 0.0
|
||||
self.ahead_dist = 0.0
|
||||
self._ahead_prev = 0.0
|
||||
self.ahead_valid = False
|
||||
self._ahead_streak = 0
|
||||
|
||||
self.assist_frame = 0
|
||||
self.speed = 0.0
|
||||
self.set_speed = 0.0
|
||||
|
||||
self._bold = gui_app.font(FontWeight.BOLD)
|
||||
self._demi = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._norm = gui_app.font(FontWeight.NORMAL)
|
||||
self._pulse_ema = FirstOrderFilter(1.0, 0.5, 1 / gui_app.target_fps)
|
||||
|
||||
px = 90
|
||||
self._up = gui_app.texture("img_plus_arrow_up.png", px, px)
|
||||
self._down = gui_app.texture("img_minus_arrow_down.png", px, px)
|
||||
|
||||
@property
|
||||
def speed_limit_assist_state(self):
|
||||
return self.assist_state
|
||||
|
||||
@property
|
||||
def _scale(self):
|
||||
return CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
def _take_plan(self, lp_iq):
|
||||
k = self._scale
|
||||
r = lp_iq.speedLimit.resolver
|
||||
self.speed_limit = r.speedLimit * k
|
||||
self.speed_limit_last = r.speedLimitLast * k
|
||||
self.speed_limit_offset = r.speedLimitOffset * k
|
||||
self.speed_limit_valid = r.speedLimitValid
|
||||
self.speed_limit_last_valid = r.speedLimitLastValid
|
||||
self.speed_limit_final_last = r.speedLimitFinalLast * k
|
||||
self.speed_limit_source = r.source
|
||||
self.assist_state = lp_iq.speedLimit.assist.state
|
||||
|
||||
def _take_ahead(self, lmd):
|
||||
self.ahead_valid = lmd.speedLimitAheadValid
|
||||
self.ahead_limit = lmd.speedLimitAhead * self._scale
|
||||
self.ahead_dist = lmd.speedLimitAheadDistance
|
||||
if self.ahead_dist < self._ahead_prev:
|
||||
self._ahead_streak = min(_SL_AHEAD_STEPS, self._ahead_streak + 1)
|
||||
elif self.ahead_dist > self._ahead_prev:
|
||||
self._ahead_streak = max(0, self._ahead_streak - 1)
|
||||
self._ahead_prev = self.ahead_dist
|
||||
|
||||
def update(self):
|
||||
sm = _feed()
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
return
|
||||
if sm.updated["iqPlan"]:
|
||||
self._take_plan(sm["iqPlan"])
|
||||
if sm.updated["iqLiveData"]:
|
||||
self._take_ahead(sm["iqLiveData"])
|
||||
cs = sm["carState"]
|
||||
self.set_speed = cs.cruiseState.speed * self._scale
|
||||
v_ego = cs.vEgoCluster if cs.vEgoCluster != 0.0 else cs.vEgo
|
||||
self.speed = max(0.0, v_ego * self._scale)
|
||||
|
||||
def _spec(self):
|
||||
has_limit = self.speed_limit_valid or self.speed_limit_last_valid
|
||||
value = str(round(self.speed_limit_last)) if has_limit else "---"
|
||||
badge = ""
|
||||
if self.speed_limit_offset != 0:
|
||||
badge = f"{'' if self.speed_limit_offset > 0 else '-'}{round(abs(self.speed_limit_offset))}"
|
||||
warn = ui_state.speed_limit_mode >= 2
|
||||
over = has_limit and round(self.speed_limit_final_last) < round(self.speed)
|
||||
tint = canvas.RED if (warn and over) else (_SL_GREY if not self.speed_limit_valid else canvas.BLACK)
|
||||
return value, badge, tint, has_limit
|
||||
|
||||
def _render(self, rect):
|
||||
if ui_state.speed_limit_mode == 0:
|
||||
return
|
||||
w = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
sign = canvas.Box(rect.x + 60 - 6, rect.y + 45 + UI_CONFIG.set_speed_height + 12, w + 12, 160)
|
||||
if self.assist_state == _SL_ASSIST.preActive:
|
||||
self.assist_frame += 1
|
||||
pulse = 0.65 + 0.35 * math.sin(self.assist_frame * math.pi / gui_app.target_fps)
|
||||
self._sign(sign, self._pulse_ema.update(pulse))
|
||||
self._nudge_arrow(sign)
|
||||
else:
|
||||
self.assist_frame = 0
|
||||
self._pulse_ema.update(1.0)
|
||||
self._sign(sign)
|
||||
self._ahead(sign)
|
||||
|
||||
def _sign(self, rect, alpha=1.0):
|
||||
value, badge, tint, has_limit = self._spec()
|
||||
(self._vienna if ui_state.is_metric else self._mutcd)(rect, value, badge, tint, has_limit, alpha)
|
||||
|
||||
def _nudge_arrow(self, sign):
|
||||
delta = round(self.speed_limit_final_last) - round(self.set_speed)
|
||||
if delta == 0:
|
||||
return
|
||||
arrow = self._up if delta > 0 else self._down
|
||||
bounce = int(20 * math.sin(self.assist_frame * 2.0 * math.pi / (gui_app.target_fps * 2.5)))
|
||||
x = sign.x + (sign.width - arrow.width) / 2
|
||||
y = sign.y + (sign.height - arrow.height) / 2 + (bounce if delta > 0 else -bounce)
|
||||
canvas.stamp(arrow, x, y, canvas.WHITE)
|
||||
|
||||
def _vienna(self, rect, value, badge, tint, has_limit, alpha=1.0):
|
||||
hub = canvas.Pt(rect.x + rect.width / 2, rect.y + rect.height / 2)
|
||||
radius = (rect.width + 18) / 2
|
||||
canvas.disc_at(hub, radius, _dim(canvas.WHITE, alpha))
|
||||
canvas.annulus(hub, radius * 0.80, radius, 0, 360, 36, _dim(canvas.RED, alpha))
|
||||
canvas.glyphs_centered(self._bold, value, 70 if len(value) >= 3 else 85, hub, _dim(tint, alpha))
|
||||
if badge and has_limit:
|
||||
br = radius * 0.4
|
||||
bc = canvas.Pt(rect.x + rect.width - br / 2, rect.y + br / 2)
|
||||
canvas.disc_at(bc, br, _dim(canvas.BLACK, alpha))
|
||||
canvas.annulus(bc, br - 3, br, 0, 360, 36, _dim(_SL_DARK, alpha))
|
||||
canvas.glyphs_centered(self._bold, badge, int(br * 2 * (0.5 if len(badge) < 3 else 0.45)), bc, _dim(canvas.WHITE, alpha))
|
||||
|
||||
def _mutcd(self, rect, value, badge, tint, has_limit, alpha=1.0):
|
||||
canvas.panel(rect, 0.35, 10, _dim(canvas.WHITE, alpha))
|
||||
inner = canvas.Box(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20)
|
||||
canvas.panel_outline(inner, 0.35, 10, 4, _dim(canvas.BLACK, alpha))
|
||||
mid = rect.x + rect.width / 2
|
||||
canvas.glyphs_centered(self._demi, "SPEED", 40, canvas.Pt(mid, rect.y + 40), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._demi, "LIMIT", 40, canvas.Pt(mid, rect.y + 80), _dim(canvas.BLACK, alpha))
|
||||
canvas.glyphs_centered(self._bold, value, 90, canvas.Pt(mid, rect.y + 150), _dim(tint, alpha))
|
||||
if badge and has_limit:
|
||||
side = rect.width * 0.3
|
||||
overlap = side * 0.2
|
||||
chip = canvas.Box(rect.x + rect.width - side / 1.5 + overlap, rect.y - side / 1.25 + overlap, side, side)
|
||||
canvas.panel(chip, 0.35, 10, _dim(canvas.BLACK, alpha))
|
||||
canvas.panel_outline(chip, 0.35, 10, 6, _dim(_SL_DARK, alpha))
|
||||
canvas.glyphs_centered(self._bold, badge, int(side * (0.6 if len(badge) < 3 else 0.475)),
|
||||
canvas.Pt(chip.x + side / 2, chip.y + side / 2), _dim(canvas.WHITE, alpha))
|
||||
|
||||
def _ahead(self, sign):
|
||||
if not (self.ahead_valid and self.ahead_limit > 0 and self.ahead_limit != self.speed_limit_last and self._ahead_streak > 0):
|
||||
return
|
||||
panel = canvas.Box(sign.x + (sign.width - 170) / 2, sign.y + sign.height + 10, 170, 160)
|
||||
canvas.panel(panel, 0.35, 10, _SL_PANEL_BG)
|
||||
canvas.panel_outline(panel, 0.35, 10, 3, _SL_PANEL_EDGE)
|
||||
mid = panel.x + panel.width / 2
|
||||
canvas.glyphs_centered(self._demi, "AHEAD", 40, canvas.Pt(mid, panel.y + 28), _SL_GREY)
|
||||
canvas.glyphs_centered(self._bold, str(round(self.ahead_limit)), 70, canvas.Pt(mid, panel.y + 82), canvas.WHITE)
|
||||
canvas.glyphs_centered(self._norm, self._dist(self.ahead_dist), 36, canvas.Pt(mid, panel.y + 134), _SL_GREY)
|
||||
|
||||
@staticmethod
|
||||
def _dist(d):
|
||||
if ui_state.is_metric:
|
||||
if d < 50:
|
||||
return tr("Near")
|
||||
if d >= 1000:
|
||||
return f"{d / 1000:.1f} km"
|
||||
return f"{int(round(d, -1) if d < 200 else round(d, -2))} m"
|
||||
ft = d * _SL_M_TO_FT
|
||||
if ft < 100:
|
||||
return tr("Near")
|
||||
if ft >= 900:
|
||||
return f"{d * _SL_M_TO_MI:.1f} mi"
|
||||
step = 50 if ft < 500 else 100
|
||||
return f"{int(round(ft / step) * step)} ft"
|
||||
90
iqpilot/ui/onroad/hud_renderer.py
Normal file
90
iqpilot/ui/onroad/hud_renderer.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from iqpilot.ui.onroad.hud_overlays import (
|
||||
IQDevMetricsOverlay,
|
||||
RoadNameRenderer,
|
||||
IQAccelBar,
|
||||
IQSpeedLimitOverlay,
|
||||
IQTurnSignalOverlay,
|
||||
IQSpeedOverlay,
|
||||
)
|
||||
from iqpilot.ui.onroad.nav_map_panel import NavMapPanel
|
||||
from iqpilot.ui.onroad.soft_warning import SoftWarningRenderer
|
||||
from iqpilot.ui.onroad.emac_status import EmacStatusRenderer
|
||||
|
||||
ENABLE_FLOATING_NAV_MAP_PANEL = False
|
||||
ENABLE_SPLIT_NAV_MAP_PANEL = True
|
||||
|
||||
|
||||
class IQHudRenderer(HudRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.developer_ui = IQDevMetricsOverlay()
|
||||
self.emac_status = EmacStatusRenderer()
|
||||
self.nav_map_panel = NavMapPanel()
|
||||
self.road_name_renderer = RoadNameRenderer()
|
||||
self.rocket_fuel = IQAccelBar()
|
||||
self.speed_limit_renderer = IQSpeedLimitOverlay()
|
||||
self.turn_signal_controller = IQTurnSignalOverlay()
|
||||
self.speed_renderer = IQSpeedOverlay()
|
||||
self.soft_warning_renderer = SoftWarningRenderer()
|
||||
self._torque_bar = TorqueBar(scale=3.0, always=True)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
super()._update_state()
|
||||
if ENABLE_FLOATING_NAV_MAP_PANEL or ENABLE_SPLIT_NAV_MAP_PANEL:
|
||||
self.nav_map_panel.update()
|
||||
self.emac_status.update()
|
||||
self.road_name_renderer.update()
|
||||
self.speed_limit_renderer.update()
|
||||
has_limit = self.speed_limit_renderer.speed_limit_valid or self.speed_limit_renderer.speed_limit_last_valid
|
||||
self.limit_available = has_limit
|
||||
self.limit_speed_text = str(round(self.speed_limit_renderer.speed_limit_last)) if has_limit else "---"
|
||||
offset = round(self.speed_limit_renderer.speed_limit_offset)
|
||||
self.limit_offset_text = f"{offset:+d}" if has_limit and offset != 0 else ""
|
||||
self.turn_signal_controller.update()
|
||||
self.speed_renderer.update()
|
||||
self.soft_warning_renderer.update()
|
||||
|
||||
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
|
||||
self.speed_renderer.render(rect)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
super()._render(rect)
|
||||
|
||||
torque_rect = rect
|
||||
if ui_state.developer_ui in (IQDevMetricsOverlay.DEV_UI_BOTTOM, IQDevMetricsOverlay.DEV_UI_BOTH):
|
||||
torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - IQDevMetricsOverlay.BOTTOM_BAR_HEIGHT)
|
||||
if ui_state.torque_bar:
|
||||
self._torque_bar.render(torque_rect)
|
||||
|
||||
if not self.split_nav_enabled():
|
||||
self.developer_ui.render(rect)
|
||||
if ENABLE_FLOATING_NAV_MAP_PANEL:
|
||||
self.nav_map_panel.render(rect)
|
||||
self.emac_status.render(rect)
|
||||
self.road_name_renderer.render(torque_rect)
|
||||
self.turn_signal_controller.render(rect)
|
||||
self.soft_warning_renderer.render(rect)
|
||||
self.rocket_fuel.render(rect, ui_state.sm)
|
||||
|
||||
def split_nav_enabled(self) -> bool:
|
||||
if not ENABLE_SPLIT_NAV_MAP_PANEL:
|
||||
return False
|
||||
if hasattr(self.nav_map_panel, "maps_enabled"):
|
||||
return bool(self.nav_map_panel.maps_enabled())
|
||||
return bool(getattr(self.nav_map_panel, "_maps_enabled", False))
|
||||
|
||||
def render_split_nav(self, rect: rl.Rectangle) -> None:
|
||||
if self.split_nav_enabled():
|
||||
self.nav_map_panel.render_split(rect)
|
||||
|
||||
def render_full_width_overlays(self, rect: rl.Rectangle) -> None:
|
||||
if self.split_nav_enabled():
|
||||
self.developer_ui.render(rect)
|
||||
70
iqpilot/ui/onroad/lead_confidence.py
Normal file
70
iqpilot/ui/onroad/lead_confidence.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
|
||||
ACTIVE_TOP = rl.Color(0x22, 0xB8, 0xB9, 255)
|
||||
ACTIVE_BOTTOM = rl.Color(0x0C, 0x94, 0x96, 255)
|
||||
MEDIUM_TOP = rl.Color(255, 200, 0, 255)
|
||||
MEDIUM_BOTTOM = rl.Color(255, 115, 0, 255)
|
||||
LOW_TOP = rl.Color(255, 0, 21, 255)
|
||||
LOW_BOTTOM = rl.Color(255, 0, 89, 255)
|
||||
OVERRIDE_TOP = rl.Color(255, 255, 255, 255)
|
||||
OVERRIDE_BOTTOM = rl.Color(82, 82, 82, 255)
|
||||
IDLE_TOP = rl.Color(120, 120, 120, 255)
|
||||
IDLE_BOTTOM = rl.Color(60, 60, 60, 255)
|
||||
|
||||
|
||||
def _zone_colors(confidence: float) -> tuple[rl.Color, rl.Color]:
|
||||
if confidence > 0.5:
|
||||
return ACTIVE_TOP, ACTIVE_BOTTOM
|
||||
if confidence > 0.2:
|
||||
return MEDIUM_TOP, MEDIUM_BOTTOM
|
||||
return LOW_TOP, LOW_BOTTOM
|
||||
|
||||
|
||||
class DrivingConfidence:
|
||||
def __init__(self):
|
||||
self._filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps)
|
||||
self._last_frame = -1
|
||||
|
||||
def update(self) -> None:
|
||||
frame = ui_state.sm.frame
|
||||
if frame == self._last_frame:
|
||||
return
|
||||
self._last_frame = frame
|
||||
try:
|
||||
predictions = ui_state.sm['modelV2'].meta.disengagePredictions
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if ui_state.status == UIStatus.DISENGAGED:
|
||||
value = -0.5
|
||||
elif ui_state.status == UIStatus.LAT_ONLY:
|
||||
value = 1 - max(predictions.steerOverrideProbs or [1])
|
||||
elif ui_state.status == UIStatus.LONG_ONLY:
|
||||
value = 1 - max(predictions.brakeDisengageProbs or [1])
|
||||
else:
|
||||
value = (1 - max(predictions.brakeDisengageProbs or [1])) * (1 - max(predictions.steerOverrideProbs or [1]))
|
||||
|
||||
self._filter.update(value)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self._filter.x
|
||||
|
||||
def colors(self, demo: bool = False) -> tuple[rl.Color, rl.Color]:
|
||||
confidence = self._filter.x
|
||||
if ui_state.status == UIStatus.ENGAGED or demo:
|
||||
return _zone_colors(confidence)
|
||||
if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY):
|
||||
return _zone_colors(confidence)
|
||||
if ui_state.status == UIStatus.OVERRIDE:
|
||||
return OVERRIDE_TOP, OVERRIDE_BOTTOM
|
||||
return IDLE_TOP, IDLE_BOTTOM
|
||||
|
||||
|
||||
driving_confidence = DrivingConfidence()
|
||||
1800
iqpilot/ui/onroad/nav_map_panel.py
Normal file
1800
iqpilot/ui/onroad/nav_map_panel.py
Normal file
File diff suppressed because it is too large
Load Diff
170
iqpilot/ui/onroad/nav_map_utils.py
Normal file
170
iqpilot/ui/onroad/nav_map_utils.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import math
|
||||
from urllib.parse import quote
|
||||
|
||||
EARTH_RADIUS_M = 6378137.0
|
||||
TILE_SIZE = 256.0
|
||||
NAV_DRIVE_ZOOM_BOOST = 1.0
|
||||
|
||||
def _mercator_normalized(latitude: float, longitude: float) -> tuple[float, float]:
|
||||
x = (longitude + 180.0) / 360.0
|
||||
siny = min(max(math.sin(math.radians(latitude)), -0.9999), 0.9999)
|
||||
y = 0.5 - math.log((1.0 + siny) / (1.0 - siny)) / (4.0 * math.pi)
|
||||
return x, y
|
||||
|
||||
def mercator_world_px(latitude: float, longitude: float, zoom: float) -> tuple[float, float]:
|
||||
world_size = TILE_SIZE * (2.0 ** zoom)
|
||||
nx, ny = _mercator_normalized(latitude, longitude)
|
||||
x = nx * world_size
|
||||
y = ny * world_size
|
||||
return x, y
|
||||
|
||||
def destination_point(latitude: float, longitude: float, bearing_deg: float, distance_m: float) -> tuple[float, float]:
|
||||
if abs(distance_m) < 1e-3:
|
||||
return latitude, longitude
|
||||
|
||||
angular_distance = distance_m / EARTH_RADIUS_M
|
||||
bearing = math.radians(bearing_deg)
|
||||
lat1 = math.radians(latitude)
|
||||
lon1 = math.radians(longitude)
|
||||
|
||||
sin_lat1 = math.sin(lat1)
|
||||
cos_lat1 = math.cos(lat1)
|
||||
sin_ad = math.sin(angular_distance)
|
||||
cos_ad = math.cos(angular_distance)
|
||||
|
||||
lat2 = math.asin(sin_lat1 * cos_ad + cos_lat1 * sin_ad * math.cos(bearing))
|
||||
lon2 = lon1 + math.atan2(
|
||||
math.sin(bearing) * sin_ad * cos_lat1,
|
||||
cos_ad - sin_lat1 * math.sin(lat2),
|
||||
)
|
||||
return math.degrees(lat2), math.degrees(lon2)
|
||||
|
||||
def fit_zoom_for_points(points, width: float, height: float, max_zoom: float = 17.6,
|
||||
min_zoom: float = 12.8, padding: float = 56.0) -> float:
|
||||
coords = [(float(point.latitude), float(point.longitude)) for point in points if point is not None]
|
||||
if len(coords) < 2:
|
||||
return max_zoom
|
||||
|
||||
xs, ys = zip(*[_mercator_normalized(lat, lon) for lat, lon in coords])
|
||||
span_x = max(max(xs) - min(xs), 1e-6)
|
||||
span_y = max(max(ys) - min(ys), 1e-6)
|
||||
|
||||
usable_width = max(width - 2.0 * padding, 32.0)
|
||||
usable_height = max(height - 2.0 * padding, 32.0)
|
||||
zoom_x = math.log2(usable_width / (TILE_SIZE * span_x))
|
||||
zoom_y = math.log2(usable_height / (TILE_SIZE * span_y))
|
||||
return max(min(min(zoom_x, zoom_y), max_zoom), min_zoom)
|
||||
|
||||
def choose_nav_camera(current_latitude: float, current_longitude: float, bearing_deg: float, points,
|
||||
width: float, height: float, preferred_zoom: float) -> tuple[float, float, float]:
|
||||
preferred_zoom += NAV_DRIVE_ZOOM_BOOST
|
||||
zoom = preferred_zoom
|
||||
if points:
|
||||
zoom = fit_zoom_for_points(points, width, height * 0.78, max_zoom=preferred_zoom + 0.6)
|
||||
zoom = min(max(zoom, preferred_zoom - 1.2), preferred_zoom + 0.6)
|
||||
|
||||
meters_per_pixel = 156543.03392 * math.cos(math.radians(current_latitude)) / (2.0 ** zoom)
|
||||
lookahead_pixels = height * 0.16
|
||||
lookahead_m = max(lookahead_pixels * meters_per_pixel, 12.0)
|
||||
center_latitude, center_longitude = destination_point(current_latitude, current_longitude, bearing_deg, lookahead_m)
|
||||
return center_latitude, center_longitude, zoom
|
||||
|
||||
def build_mapbox_static_url(latitude: float, longitude: float, zoom: float, bearing: float,
|
||||
width: int, height: int, points=None) -> str:
|
||||
overlay = ""
|
||||
if points:
|
||||
overlay = f"path-7+34d17a-0.85({encode_polyline(points)})/"
|
||||
return (
|
||||
f"https://api.mapbox.com/styles/v1/mapbox/navigation-night-v1/static/"
|
||||
f"{overlay}{longitude:.6f},{latitude:.6f},{zoom:.2f},{bearing:.1f},0/{width}x{height}@2x"
|
||||
)
|
||||
|
||||
def build_mapbox_tile_url(z: int, x: int, y: int, tile_size: int = 256, scale: int = 2,
|
||||
style: str = "navigation-night-v1") -> str:
|
||||
suffix = f"@{scale}x" if scale > 1 else ""
|
||||
return (
|
||||
f"https://api.mapbox.com/styles/v1/mapbox/{style}/tiles/"
|
||||
f"{tile_size}/{z}/{x}/{y}{suffix}"
|
||||
)
|
||||
|
||||
def tile_world_size(z: int, tile_size: int = 256) -> int:
|
||||
return tile_size * (2 ** z)
|
||||
|
||||
def mercator_world_px_at_zoom(latitude: float, longitude: float, z: int, tile_size: int = 256) -> tuple[float, float]:
|
||||
world_size = tile_world_size(z, tile_size)
|
||||
nx, ny = _mercator_normalized(latitude, longitude)
|
||||
return nx * world_size, ny * world_size
|
||||
|
||||
def encode_polyline(points) -> str:
|
||||
result = []
|
||||
last_lat = 0
|
||||
last_lon = 0
|
||||
|
||||
for point in points:
|
||||
lat = int(round(float(point.latitude if hasattr(point, "latitude") else point[0]) * 1e5))
|
||||
lon = int(round(float(point.longitude if hasattr(point, "longitude") else point[1]) * 1e5))
|
||||
|
||||
for value in (lat - last_lat, lon - last_lon):
|
||||
shifted = ~(value << 1) if value < 0 else (value << 1)
|
||||
while shifted >= 0x20:
|
||||
result.append(chr((0x20 | (shifted & 0x1f)) + 63))
|
||||
shifted >>= 5
|
||||
result.append(chr(shifted + 63))
|
||||
|
||||
last_lat = lat
|
||||
last_lon = lon
|
||||
|
||||
return quote("".join(result), safe="")
|
||||
|
||||
def project_nav_point(latitude: float, longitude: float, center_latitude: float, center_longitude: float,
|
||||
zoom: float, bearing_deg: float, width: float, height: float,
|
||||
anchor_x: float = 0.5, anchor_y: float = 0.5) -> tuple[float, float]:
|
||||
px, py = mercator_world_px(latitude, longitude, zoom)
|
||||
cx, cy = mercator_world_px(center_latitude, center_longitude, zoom)
|
||||
dx = px - cx
|
||||
dy = py - cy
|
||||
|
||||
theta = math.radians(bearing_deg)
|
||||
cos_theta = math.cos(theta)
|
||||
sin_theta = math.sin(theta)
|
||||
rx = dx * cos_theta + dy * sin_theta
|
||||
ry = -dx * sin_theta + dy * cos_theta
|
||||
return width * anchor_x + rx, height * anchor_y + ry
|
||||
|
||||
def project_nav_polyline(points, center_latitude: float, center_longitude: float, zoom: float, bearing_deg: float,
|
||||
width: float, height: float, anchor_x: float = 0.5, anchor_y: float = 0.5) -> list[tuple[float, float]]:
|
||||
projected = []
|
||||
for point in points:
|
||||
projected.append(
|
||||
project_nav_point(
|
||||
float(point.latitude),
|
||||
float(point.longitude),
|
||||
center_latitude,
|
||||
center_longitude,
|
||||
zoom,
|
||||
bearing_deg,
|
||||
width,
|
||||
height,
|
||||
anchor_x=anchor_x,
|
||||
anchor_y=anchor_y,
|
||||
)
|
||||
)
|
||||
return projected
|
||||
|
||||
def solar_elevation_deg(latitude: float, longitude: float, unix_time: float) -> float:
|
||||
days = unix_time / 86400.0 - 10957.5
|
||||
mean_longitude = math.radians((280.460 + 0.9856474 * days) % 360.0)
|
||||
mean_anomaly = math.radians((357.528 + 0.9856003 * days) % 360.0)
|
||||
ecliptic_longitude = mean_longitude + math.radians(1.915) * math.sin(mean_anomaly) \
|
||||
+ math.radians(0.020) * math.sin(2.0 * mean_anomaly)
|
||||
obliquity = math.radians(23.439 - 0.0000004 * days)
|
||||
declination = math.asin(math.sin(obliquity) * math.sin(ecliptic_longitude))
|
||||
right_ascension = math.atan2(math.cos(obliquity) * math.sin(ecliptic_longitude), math.cos(ecliptic_longitude))
|
||||
gmst_deg = (280.46061837 + 360.98564736629 * days) % 360.0
|
||||
hour_angle = math.radians(gmst_deg) + math.radians(longitude) - right_ascension
|
||||
lat_rad = math.radians(latitude)
|
||||
elevation = math.asin(
|
||||
math.sin(lat_rad) * math.sin(declination)
|
||||
+ math.cos(lat_rad) * math.cos(declination) * math.cos(hour_angle)
|
||||
)
|
||||
return math.degrees(elevation)
|
||||
257
iqpilot/ui/onroad/offline_tiles.py
Normal file
257
iqpilot/ui/onroad/offline_tiles.py
Normal file
@@ -0,0 +1,257 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
except Exception:
|
||||
sqlite3 = None
|
||||
|
||||
OFFLINE_MBTILES_ENV = "IQPILOT_OFFLINE_MBTILES"
|
||||
OFFLINE_TILE_ROOT_ENV = "IQPILOT_OFFLINE_TILE_ROOT"
|
||||
DEFAULT_OFFLINE_TILE_ROOT = Path("/data/offline_maps/tiles" if Path("/data").exists() else "/tmp/offline_maps/tiles")
|
||||
DEFAULT_OFFLINE_MAP_ROOT = Path("/data/offline_maps" if Path("/data").exists() else "/tmp/offline_maps")
|
||||
SQLITE_ERRORS = (sqlite3.Error,) if sqlite3 is not None else (Exception,)
|
||||
SQLiteConnection = Any
|
||||
|
||||
def offline_tile_root() -> Path:
|
||||
override = os.getenv(OFFLINE_TILE_ROOT_ENV)
|
||||
return Path(override) if override else DEFAULT_OFFLINE_TILE_ROOT
|
||||
|
||||
def offline_map_root() -> Path:
|
||||
root = offline_tile_root()
|
||||
if root.name == "tiles":
|
||||
return root.parent
|
||||
if root.name == "xyz":
|
||||
return root.parent.parent if root.parent.name == "tiles" else root.parent
|
||||
return DEFAULT_OFFLINE_MAP_ROOT
|
||||
|
||||
def _parse_bounds(bounds: str) -> tuple[float, float, float, float] | None:
|
||||
try:
|
||||
min_lon, min_lat, max_lon, max_lat = [float(part) for part in bounds.split(",")]
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
return min_lat, min_lon, max_lat, max_lon
|
||||
|
||||
def _bounds_contains(bounds: tuple[float, float, float, float], latitude: float, longitude: float) -> bool:
|
||||
min_lat, min_lon, max_lat, max_lon = bounds
|
||||
return min_lat <= latitude <= max_lat and min_lon <= longitude <= max_lon
|
||||
|
||||
def _bounds_area(bounds: tuple[float, float, float, float]) -> float:
|
||||
min_lat, min_lon, max_lat, max_lon = bounds
|
||||
return max(max_lat - min_lat, 0.0) * max(max_lon - min_lon, 0.0)
|
||||
|
||||
_region_bounds_cache: dict[Path, tuple[float, float, float, float]] = {}
|
||||
|
||||
def _load_region_bounds(region_root: Path) -> tuple[float, float, float, float] | None:
|
||||
cached = _region_bounds_cache.get(region_root)
|
||||
if cached is not None:
|
||||
return cached
|
||||
bounds = _load_region_bounds_uncached(region_root)
|
||||
if bounds is not None:
|
||||
if len(_region_bounds_cache) > 64:
|
||||
_region_bounds_cache.clear()
|
||||
_region_bounds_cache[region_root] = bounds
|
||||
return bounds
|
||||
|
||||
def _load_region_bounds_uncached(region_root: Path) -> tuple[float, float, float, float] | None:
|
||||
manifest_path = region_root / "manifest.json"
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
bounds = manifest.get("mbtiles", {}).get("bounds")
|
||||
parsed = _parse_bounds(bounds) if bounds else None
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
mbtiles_path = region_root / "tiles" / "offline.mbtiles"
|
||||
if not mbtiles_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
conn = open_mbtiles(mbtiles_path)
|
||||
row = conn.execute("SELECT value FROM metadata WHERE name = 'bounds'").fetchone()
|
||||
conn.close()
|
||||
except SQLITE_ERRORS:
|
||||
return None
|
||||
|
||||
return _parse_bounds(row["value"]) if row is not None else None
|
||||
|
||||
_REGION_ROOTS_TTL_S = 15.0
|
||||
_region_roots_cache: tuple[float, Path, tuple[Path, ...]] | None = None
|
||||
|
||||
def _candidate_region_roots() -> tuple[Path, ...]:
|
||||
global _region_roots_cache
|
||||
root = offline_map_root()
|
||||
now = time.monotonic()
|
||||
if _region_roots_cache is not None:
|
||||
cached_at, cached_root, cached = _region_roots_cache
|
||||
if cached_root == root and now - cached_at < _REGION_ROOTS_TTL_S:
|
||||
return cached
|
||||
|
||||
candidates: list[Path] = []
|
||||
if (root / "tiles").exists():
|
||||
candidates.append(root)
|
||||
|
||||
regions_root = root / "regions"
|
||||
if regions_root.exists():
|
||||
for child in sorted(regions_root.iterdir()):
|
||||
if child.is_dir() and (child / "tiles").exists():
|
||||
candidates.append(child)
|
||||
|
||||
result = tuple(candidates)
|
||||
_region_roots_cache = (now, root, result)
|
||||
return result
|
||||
|
||||
def _region_covers_point(region_root: Path, latitude: float, longitude: float) -> bool:
|
||||
mb = region_root / "tiles" / "offline.mbtiles"
|
||||
if not mb.exists():
|
||||
return True
|
||||
try:
|
||||
conn = open_mbtiles(mb)
|
||||
try:
|
||||
_, max_zoom = mbtiles_zoom_bounds(conn)
|
||||
z = max_zoom if max_zoom is not None else 14
|
||||
import math
|
||||
n = 2 ** z
|
||||
lat_r = math.radians(max(min(latitude, 85.05112878), -85.05112878))
|
||||
x = int((longitude + 180.0) / 360.0 * n)
|
||||
y = int((1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n)
|
||||
for dx in (-1, 0, 1):
|
||||
for dy in (-1, 0, 1):
|
||||
if load_raster_tile_blob(conn, z, x + dx, y + dy) is not None:
|
||||
return True
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
except SQLITE_ERRORS:
|
||||
return True
|
||||
|
||||
def find_offline_region_root(latitude: float | None = None, longitude: float | None = None) -> Path | None:
|
||||
candidates = _candidate_region_roots()
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
if latitude is None or longitude is None:
|
||||
return candidates[0]
|
||||
|
||||
bounded: list[tuple[float, Path]] = []
|
||||
for candidate in candidates:
|
||||
bounds = _load_region_bounds(candidate)
|
||||
if bounds is None:
|
||||
continue
|
||||
if _bounds_contains(bounds, latitude, longitude):
|
||||
bounded.append((_bounds_area(bounds), candidate))
|
||||
|
||||
if bounded:
|
||||
if len(bounded) == 1:
|
||||
return bounded[0][1]
|
||||
bounded.sort(key=lambda item: item[0])
|
||||
for _, candidate in bounded:
|
||||
if _region_covers_point(candidate, latitude, longitude):
|
||||
return candidate
|
||||
return bounded[0][1]
|
||||
|
||||
return candidates[0]
|
||||
|
||||
def find_offline_mbtiles_path(latitude: float | None = None, longitude: float | None = None,
|
||||
day: bool = False) -> Path | None:
|
||||
explicit = os.getenv(OFFLINE_MBTILES_ENV)
|
||||
if explicit:
|
||||
path = Path(explicit)
|
||||
if day:
|
||||
day_path = path.with_name("offline_day.mbtiles")
|
||||
if day_path.exists():
|
||||
return day_path
|
||||
return path if path.exists() else None
|
||||
|
||||
region_root = find_offline_region_root(latitude, longitude)
|
||||
if region_root is None:
|
||||
return None
|
||||
|
||||
if day:
|
||||
day_preferred = region_root / "tiles" / "offline_day.mbtiles"
|
||||
if day_preferred.exists():
|
||||
return day_preferred
|
||||
|
||||
preferred = region_root / "tiles" / "offline.mbtiles"
|
||||
if preferred.exists():
|
||||
return preferred
|
||||
|
||||
matches = sorted(p for p in (region_root / "tiles").glob("*.mbtiles") if "_day" not in p.name or day)
|
||||
return matches[0] if matches else None
|
||||
|
||||
def find_offline_xyz_root(latitude: float | None = None, longitude: float | None = None) -> Path | None:
|
||||
root = offline_tile_root()
|
||||
if not root.exists():
|
||||
region_root = find_offline_region_root(latitude, longitude)
|
||||
if region_root is None:
|
||||
return None
|
||||
root = region_root / "tiles"
|
||||
|
||||
if any(child.is_dir() and child.name.isdigit() for child in root.iterdir()):
|
||||
return root
|
||||
|
||||
xyz_dir = root / "xyz"
|
||||
if xyz_dir.exists() and any(child.is_dir() and child.name.isdigit() for child in xyz_dir.iterdir()):
|
||||
return xyz_dir
|
||||
|
||||
return None
|
||||
|
||||
def xyz_to_tms_y(z: int, y: int) -> int:
|
||||
return (2 ** z - 1) - y
|
||||
|
||||
def open_mbtiles(path: Path) -> SQLiteConnection:
|
||||
if sqlite3 is None:
|
||||
raise RuntimeError("sqlite3 unavailable")
|
||||
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True, check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def mbtiles_is_raster(conn: SQLiteConnection) -> bool:
|
||||
row = conn.execute("SELECT value FROM metadata WHERE name = 'format'").fetchone()
|
||||
if row is None:
|
||||
return False
|
||||
return row["value"] in {"png", "jpg", "jpeg", "webp"}
|
||||
|
||||
def mbtiles_zoom_bounds(conn: SQLiteConnection) -> tuple[int | None, int | None]:
|
||||
rows = {
|
||||
row["name"]: row["value"]
|
||||
for row in conn.execute("SELECT name, value FROM metadata WHERE name IN ('minzoom', 'maxzoom')")
|
||||
}
|
||||
min_zoom = int(rows["minzoom"]) if "minzoom" in rows else None
|
||||
max_zoom = int(rows["maxzoom"]) if "maxzoom" in rows else None
|
||||
return min_zoom, max_zoom
|
||||
|
||||
def xyz_zoom_bounds(root: Path) -> tuple[int | None, int | None]:
|
||||
zoom_dirs = sorted(
|
||||
int(child.name)
|
||||
for child in root.iterdir()
|
||||
if child.is_dir() and child.name.isdigit()
|
||||
)
|
||||
if not zoom_dirs:
|
||||
return None, None
|
||||
return zoom_dirs[0], zoom_dirs[-1]
|
||||
|
||||
def load_raster_tile_blob(conn: SQLiteConnection, z: int, x: int, y: int) -> bytes | None:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT tile_data
|
||||
FROM tiles
|
||||
WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?
|
||||
""",
|
||||
(z, x, xyz_to_tms_y(z, y)),
|
||||
).fetchone()
|
||||
return bytes(row["tile_data"]) if row is not None else None
|
||||
|
||||
def load_raster_xyz_tile_blob(root: Path, z: int, x: int, y: int) -> bytes | None:
|
||||
for suffix in ("png", "webp", "jpg", "jpeg"):
|
||||
for filename in (f"{y}.{suffix}", f"{y}@2x.{suffix}"):
|
||||
tile_path = root / str(z) / str(x) / filename
|
||||
if tile_path.exists():
|
||||
return tile_path.read_bytes()
|
||||
return None
|
||||
50
iqpilot/ui/onroad/soft_warning.py
Normal file
50
iqpilot/ui/onroad/soft_warning.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import pyray as rl
|
||||
from iqpilot.cereal import log
|
||||
|
||||
from iqpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
|
||||
EventName = log.OnroadEvent.EventName
|
||||
SOFT_WARNING_EVENTS = {
|
||||
EventName.commIssue,
|
||||
EventName.commIssueAvgFreq,
|
||||
EventName.selfdrivedLagging,
|
||||
}
|
||||
ICON_SIZE = 96
|
||||
_SPEED_BOX_X_OFFSET = 60
|
||||
_SPEED_BOX_Y_OFFSET = 45
|
||||
_SPEED_BOX_WIDTH = 180
|
||||
_SPEED_BOX_HEIGHT = 228
|
||||
|
||||
_DM_OFFSET = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
|
||||
class SoftWarningRenderer:
|
||||
def __init__(self):
|
||||
self._icon = gui_app.texture("icons_mici/offroad_alerts/orange_warning.png", ICON_SIZE, ICON_SIZE)
|
||||
self._active = False
|
||||
|
||||
def update(self) -> None:
|
||||
sm = ui_state.sm
|
||||
self._active = any(e.name in SOFT_WARNING_EVENTS for e in sm['onroadEvents'])
|
||||
|
||||
def render(self, rect: rl.Rectangle) -> None:
|
||||
if not self._active:
|
||||
return
|
||||
speed_cx = rect.x + _SPEED_BOX_X_OFFSET + _SPEED_BOX_WIDTH / 2
|
||||
speed_cy = rect.y + _SPEED_BOX_Y_OFFSET + _SPEED_BOX_HEIGHT / 2
|
||||
|
||||
dm_cx = rect.x + _DM_OFFSET
|
||||
dm_cy = rect.y + rect.height - _DM_OFFSET
|
||||
|
||||
mid_x = (speed_cx + dm_cx) / 2
|
||||
mid_y = (speed_cy + dm_cy) / 2
|
||||
|
||||
draw_x = int(mid_x - ICON_SIZE / 2)
|
||||
draw_y = int(mid_y - ICON_SIZE / 2)
|
||||
|
||||
rl.draw_texture(self._icon, draw_x, draw_y, rl.WHITE)
|
||||
Reference in New Issue
Block a user