|
|
@ -2,6 +2,7 @@ import os |
|
|
|
import re |
|
|
|
import re |
|
|
|
import uuid |
|
|
|
import uuid |
|
|
|
import threading |
|
|
|
import threading |
|
|
|
|
|
|
|
from collections import deque |
|
|
|
import dearpygui.dearpygui as dpg |
|
|
|
import dearpygui.dearpygui as dpg |
|
|
|
from abc import ABC, abstractmethod |
|
|
|
from abc import ABC, abstractmethod |
|
|
|
from openpilot.tools.jotpluggler.data import DataManager |
|
|
|
from openpilot.tools.jotpluggler.data import DataManager |
|
|
@ -183,36 +184,68 @@ class DataTreeNode: |
|
|
|
self.full_path = full_path |
|
|
|
self.full_path = full_path |
|
|
|
self.children: dict[str, DataTreeNode] = {} |
|
|
|
self.children: dict[str, DataTreeNode] = {} |
|
|
|
self.is_leaf = False |
|
|
|
self.is_leaf = False |
|
|
|
|
|
|
|
self.child_count = 0 |
|
|
|
|
|
|
|
self.is_plottable_cached = None |
|
|
|
|
|
|
|
self.ui_created = False |
|
|
|
|
|
|
|
self.ui_tag: str | None = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DataTreeView: |
|
|
|
class DataTreeView: |
|
|
|
|
|
|
|
MAX_ITEMS_PER_FRAME = 50 |
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data_manager: DataManager, ui_lock: threading.Lock): |
|
|
|
def __init__(self, data_manager: DataManager, ui_lock: threading.Lock): |
|
|
|
self.data_manager = data_manager |
|
|
|
self.data_manager = data_manager |
|
|
|
self.ui_lock = ui_lock |
|
|
|
self.ui_lock = ui_lock |
|
|
|
self.current_search = "" |
|
|
|
self.current_search = "" |
|
|
|
self.data_tree = DataTreeNode(name="root") |
|
|
|
self.data_tree = DataTreeNode(name="root") |
|
|
|
self.active_leaf_nodes: list[DataTreeNode] = [] |
|
|
|
self.ui_render_queue: deque[tuple[DataTreeNode, str, str, bool]] = deque() # (node, parent_tag, search_term, is_leaf) |
|
|
|
self.data_manager.add_observer(self.on_data_loaded) |
|
|
|
self.visible_expanded_nodes: set[str] = set() |
|
|
|
|
|
|
|
self.created_leaf_paths: set[str] = set() |
|
|
|
def on_data_loaded(self, data: dict): |
|
|
|
self._all_paths_cache: list[str] = [] |
|
|
|
|
|
|
|
self.data_manager.add_observer(self._on_data_loaded) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_data_loaded(self, data: dict): |
|
|
|
|
|
|
|
if data.get('loading_complete'): |
|
|
|
with self.ui_lock: |
|
|
|
with self.ui_lock: |
|
|
|
self.populate_data_tree() |
|
|
|
self._all_paths_cache = self.data_manager.get_all_paths() |
|
|
|
|
|
|
|
self._populate_tree() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_frame(self): |
|
|
|
|
|
|
|
items_processed = 0 |
|
|
|
|
|
|
|
while self.ui_render_queue and items_processed < self.MAX_ITEMS_PER_FRAME: # process up to MAX_ITEMS_PER_FRAME to maintain perforamnce |
|
|
|
|
|
|
|
node, parent_tag, search_term, is_leaf = self.ui_render_queue.popleft() |
|
|
|
|
|
|
|
if is_leaf: |
|
|
|
|
|
|
|
self._create_leaf_ui(node, parent_tag) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
self._create_node_ui(node, parent_tag, search_term) |
|
|
|
|
|
|
|
items_processed += 1 |
|
|
|
|
|
|
|
|
|
|
|
def populate_data_tree(self): |
|
|
|
def search_data(self, search_term: str): |
|
|
|
if not dpg.does_item_exist("data_tree_container"): |
|
|
|
self.current_search = search_term |
|
|
|
return |
|
|
|
self._all_paths_cache = self.data_manager.get_all_paths() |
|
|
|
|
|
|
|
self._populate_tree() |
|
|
|
|
|
|
|
|
|
|
|
dpg.delete_item("data_tree_container", children_only=True) |
|
|
|
def _populate_tree(self): |
|
|
|
|
|
|
|
self._clear_ui() |
|
|
|
search_term = self.current_search.strip().lower() |
|
|
|
search_term = self.current_search.strip().lower() |
|
|
|
|
|
|
|
self.data_tree = self._build_tree_structure(search_term) |
|
|
|
|
|
|
|
for child in sorted(self.data_tree.children.values(), key=self._natural_sort_key): # queue top level nodes |
|
|
|
|
|
|
|
self.ui_render_queue.append((child, "data_tree_container", search_term, child.is_leaf)) |
|
|
|
|
|
|
|
|
|
|
|
self.data_tree = DataTreeNode(name="root") |
|
|
|
def _clear_ui(self): |
|
|
|
all_paths = self.data_manager.get_all_paths() |
|
|
|
dpg.delete_item("data_tree_container", children_only=True) |
|
|
|
|
|
|
|
self.ui_render_queue.clear() |
|
|
|
for path in sorted(all_paths): |
|
|
|
self.visible_expanded_nodes.clear() |
|
|
|
if not self._should_display_path(path, search_term): |
|
|
|
self.created_leaf_paths.clear() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_tree_structure(self, search_term: str) -> DataTreeNode: |
|
|
|
|
|
|
|
root = DataTreeNode(name="root") |
|
|
|
|
|
|
|
for path in sorted(self._all_paths_cache): |
|
|
|
|
|
|
|
if not self._should_show_path(path, search_term): |
|
|
|
continue |
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
parts = path.split('/') |
|
|
|
parts = path.split('/') |
|
|
|
current_node = self.data_tree |
|
|
|
current_node = root |
|
|
|
current_path_prefix = "" |
|
|
|
current_path_prefix = "" |
|
|
|
|
|
|
|
|
|
|
|
for part in parts: |
|
|
|
for part in parts: |
|
|
@ -220,76 +253,102 @@ class DataTreeView: |
|
|
|
if part not in current_node.children: |
|
|
|
if part not in current_node.children: |
|
|
|
current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix) |
|
|
|
current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix) |
|
|
|
current_node = current_node.children[part] |
|
|
|
current_node = current_node.children[part] |
|
|
|
|
|
|
|
|
|
|
|
current_node.is_leaf = True |
|
|
|
current_node.is_leaf = True |
|
|
|
|
|
|
|
self._calculate_child_counts(root) |
|
|
|
|
|
|
|
return root |
|
|
|
|
|
|
|
|
|
|
|
self._create_ui_from_tree_recursive(self.data_tree, "data_tree_container", search_term) |
|
|
|
def _calculate_child_counts(self, node: DataTreeNode): |
|
|
|
self.update_active_nodes_list() |
|
|
|
if node.is_leaf: |
|
|
|
|
|
|
|
node.child_count = 0 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
node.child_count = len(node.children) |
|
|
|
|
|
|
|
for child in node.children.values(): |
|
|
|
|
|
|
|
self._calculate_child_counts(child) |
|
|
|
|
|
|
|
|
|
|
|
def _should_display_path(self, path: str, search_term: str) -> bool: |
|
|
|
def _create_node_ui(self, node: DataTreeNode, parent_tag: str, search_term: str): |
|
|
|
if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'): |
|
|
|
if node.is_leaf: |
|
|
|
return False |
|
|
|
self._create_leaf_ui(node, parent_tag) |
|
|
|
return not search_term or search_term in path.lower() |
|
|
|
else: |
|
|
|
|
|
|
|
self._create_tree_node_ui(node, parent_tag, search_term) |
|
|
|
|
|
|
|
|
|
|
|
def _natural_sort_key(self, node: DataTreeNode): |
|
|
|
def _create_tree_node_ui(self, node: DataTreeNode, parent_tag: str, search_term: str): |
|
|
|
node_type_key = node.is_leaf |
|
|
|
node_tag = f"tree_{node.full_path}" |
|
|
|
parts = [int(p) if p.isdigit() else p.lower() for p in re.split(r'(\d+)', node.name) if p] |
|
|
|
node.ui_tag = node_tag |
|
|
|
return (node_type_key, parts) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_ui_from_tree_recursive(self, node: DataTreeNode, parent_tag: str, search_term: str): |
|
|
|
label = f"{node.name} ({node.child_count} fields)" |
|
|
|
sorted_children = sorted(node.children.values(), key=self._natural_sort_key) |
|
|
|
should_open = (bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node))) |
|
|
|
|
|
|
|
|
|
|
|
for child in sorted_children: |
|
|
|
with dpg.tree_node(label=label, parent=parent_tag, tag=node_tag, default_open=should_open, open_on_arrow=True, open_on_double_click=True) as tree_node: |
|
|
|
if child.is_leaf: |
|
|
|
with dpg.item_handler_registry() as handler: |
|
|
|
is_plottable = self.data_manager.is_plottable(child.full_path) |
|
|
|
dpg.add_item_toggled_open_handler(callback=lambda s, d, u: self._on_node_expanded(node, search_term)) |
|
|
|
|
|
|
|
dpg.bind_item_handler_registry(tree_node, handler) |
|
|
|
|
|
|
|
|
|
|
|
# Create draggable item |
|
|
|
node.ui_created = True |
|
|
|
with dpg.group(parent=parent_tag) as draggable_group: |
|
|
|
|
|
|
|
with dpg.table(header_row=False, borders_innerV=False, borders_outerH=False, borders_outerV=False, policy=dpg.mvTable_SizingStretchProp): |
|
|
|
|
|
|
|
dpg.add_table_column(init_width_or_weight=0.5) |
|
|
|
|
|
|
|
dpg.add_table_column(init_width_or_weight=0.5) |
|
|
|
|
|
|
|
with dpg.table_row(): |
|
|
|
|
|
|
|
dpg.add_text(child.name) |
|
|
|
|
|
|
|
dpg.add_text("N/A", tag=f"value_{child.full_path}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Add drag payload if plottable |
|
|
|
if should_open: |
|
|
|
if is_plottable: |
|
|
|
self.visible_expanded_nodes.add(node.full_path) |
|
|
|
with dpg.drag_payload(parent=draggable_group, drag_data=child.full_path, payload_type="TIMESERIES_PAYLOAD"): |
|
|
|
self._queue_children(node, node_tag, search_term) |
|
|
|
dpg.add_text(f"Plot: {child.full_path}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
|
def _create_leaf_ui(self, node: DataTreeNode, parent_tag: str): |
|
|
|
node_tag = f"tree_{child.full_path}" |
|
|
|
half_split_size = dpg.get_item_rect_size("data_pool_window")[0] // 2 |
|
|
|
label = child.name |
|
|
|
with dpg.group(parent=parent_tag, horizontal=True, xoffset=half_split_size, tag=f"group_{node.full_path}") as draggable_group: |
|
|
|
|
|
|
|
dpg.add_text(node.name) |
|
|
|
|
|
|
|
dpg.add_text("N/A", tag=f"value_{node.full_path}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if node.is_plottable_cached is None: |
|
|
|
|
|
|
|
node.is_plottable_cached = self.data_manager.is_plottable(node.full_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if node.is_plottable_cached: |
|
|
|
|
|
|
|
with dpg.drag_payload(parent=draggable_group, drag_data=node.full_path, |
|
|
|
|
|
|
|
payload_type="TIMESERIES_PAYLOAD"): |
|
|
|
|
|
|
|
dpg.add_text(f"Plot: {node.full_path}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
node.ui_created = True |
|
|
|
|
|
|
|
node.ui_tag = f"value_{node.full_path}" |
|
|
|
|
|
|
|
self.created_leaf_paths.add(node.full_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _queue_children(self, node: DataTreeNode, parent_tag: str, search_term: str): |
|
|
|
|
|
|
|
for child in sorted(node.children.values(), key=self._natural_sort_key): |
|
|
|
|
|
|
|
self.ui_render_queue.append((child, parent_tag, search_term, child.is_leaf)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_node_expanded(self, node: DataTreeNode, search_term: str): |
|
|
|
|
|
|
|
node_tag = f"tree_{node.full_path}" |
|
|
|
|
|
|
|
if not dpg.does_item_exist(node_tag): |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
should_open = bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_all_descendant_paths(child)) |
|
|
|
is_expanded = dpg.get_value(node_tag) |
|
|
|
|
|
|
|
|
|
|
|
with dpg.tree_node(label=label, parent=parent_tag, tag=node_tag, default_open=should_open): |
|
|
|
if is_expanded: |
|
|
|
dpg.bind_item_handler_registry(node_tag, "tree_node_handler") |
|
|
|
if node.full_path not in self.visible_expanded_nodes: |
|
|
|
self._create_ui_from_tree_recursive(child, node_tag, search_term) |
|
|
|
self.visible_expanded_nodes.add(node.full_path) |
|
|
|
|
|
|
|
self._queue_children(node, node_tag, search_term) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
self.visible_expanded_nodes.discard(node.full_path) |
|
|
|
|
|
|
|
self._remove_children_from_queue(node.full_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _remove_children_from_queue(self, collapsed_node_path: str): |
|
|
|
|
|
|
|
new_queue = deque() |
|
|
|
|
|
|
|
for node, parent_tag, search_term, is_leaf in self.ui_render_queue: |
|
|
|
|
|
|
|
# Keep items that are not children of the collapsed node |
|
|
|
|
|
|
|
if not node.full_path.startswith(collapsed_node_path + "/"): |
|
|
|
|
|
|
|
new_queue.append((node, parent_tag, search_term, is_leaf)) |
|
|
|
|
|
|
|
self.ui_render_queue = new_queue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _should_show_path(self, path: str, search_term: str) -> bool: |
|
|
|
|
|
|
|
if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'): |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
return not search_term or search_term in path.lower() |
|
|
|
|
|
|
|
|
|
|
|
def _get_all_descendant_paths(self, node: DataTreeNode): |
|
|
|
def _natural_sort_key(self, node: DataTreeNode): |
|
|
|
|
|
|
|
node_type_key = node.is_leaf |
|
|
|
|
|
|
|
parts = [int(p) if p.isdigit() else p.lower() for p in re.split(r'(\d+)', node.name) if p] |
|
|
|
|
|
|
|
return (node_type_key, parts) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_descendant_paths(self, node: DataTreeNode): |
|
|
|
for child_name, child_node in node.children.items(): |
|
|
|
for child_name, child_node in node.children.items(): |
|
|
|
child_name_lower = child_name.lower() |
|
|
|
child_name_lower = child_name.lower() |
|
|
|
if child_node.is_leaf: |
|
|
|
if child_node.is_leaf: |
|
|
|
yield child_name_lower |
|
|
|
yield child_name_lower |
|
|
|
else: |
|
|
|
else: |
|
|
|
for path in self._get_all_descendant_paths(child_node): |
|
|
|
for path in self._get_descendant_paths(child_node): |
|
|
|
yield f"{child_name_lower}/{path}" |
|
|
|
yield f"{child_name_lower}/{path}" |
|
|
|
|
|
|
|
|
|
|
|
def search_data(self, search_term: str): |
|
|
|
|
|
|
|
self.current_search = search_term |
|
|
|
|
|
|
|
self.populate_data_tree() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_active_nodes_list(self, sender=None, app_data=None, user_data=None): |
|
|
|
|
|
|
|
self.active_leaf_nodes = self.get_active_leaf_nodes(self.data_tree) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_active_leaf_nodes(self, node: DataTreeNode): |
|
|
|
|
|
|
|
active_leaves = [] |
|
|
|
|
|
|
|
for child in node.children.values(): |
|
|
|
|
|
|
|
if child.is_leaf: |
|
|
|
|
|
|
|
active_leaves.append(child) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
node_tag = f"tree_{child.full_path}" |
|
|
|
|
|
|
|
if dpg.does_item_exist(node_tag) and dpg.get_value(node_tag): |
|
|
|
|
|
|
|
active_leaves.extend(self.get_active_leaf_nodes(child)) |
|
|
|
|
|
|
|
return active_leaves |
|
|
|
|
|
|
|