openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

300 lines
12 KiB

import uuid
import dearpygui.dearpygui as dpg
from abc import ABC, abstractmethod
from openpilot.tools.jotpluggler.data import DataManager
from openpilot.tools.jotpluggler.views import ViewPanel, TimeSeriesPanel
class LayoutNode(ABC):
def __init__(self, node_id: str | None = None):
self.node_id = node_id or str(uuid.uuid4())
self.tag: str | None = None
@abstractmethod
def create_ui(self, parent_tag: str, width: int = -1, height: int = -1):
pass
@abstractmethod
def destroy_ui(self):
pass
@abstractmethod
def preserve_data(self):
pass
class LeafNode(LayoutNode):
"""Leaf node that contains a single ViewPanel with controls"""
def __init__(self, panel: ViewPanel, layout_manager=None, scale: float = 1.0, node_id: str = None):
super().__init__(node_id)
self.panel = panel
self.layout_manager = layout_manager
self.scale = scale
def preserve_data(self):
self.panel.preserve_data()
def create_ui(self, parent_tag: str, width: int = -1, height: int = -1):
"""Create UI container with controls and panel"""
self.tag = f"leaf_{self.node_id}"
with dpg.child_window(tag=self.tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True):
# Control bar
with dpg.group(horizontal=True):
dpg.add_input_text(tag=f"title_{self.node_id}", default_value=self.panel.title, width=int(100 * self.scale), callback=self._on_title_change)
dpg.add_combo(
items=["Time Series"], # "Camera", "Text Log", "Map View"],
tag=f"type_{self.node_id}",
default_value="Time Series",
width=int(100 * self.scale),
callback=self._on_type_change,
)
dpg.add_button(label="Clear", callback=self._clear, width=int(50 * self.scale))
dpg.add_button(label="Delete", callback=self._delete, width=int(50 * self.scale))
dpg.add_button(label="Split H", callback=lambda: self._split("horizontal"), width=int(50 * self.scale))
dpg.add_button(label="Split V", callback=lambda: self._split("vertical"), width=int(50 * self.scale))
dpg.add_separator()
# Panel content area
panel_area_tag = f"panel_area_{self.node_id}"
with dpg.child_window(tag=panel_area_tag, border=False, height=-1, width=-1, no_scrollbar=True):
self.panel.create_ui(panel_area_tag)
def destroy_ui(self):
if self.panel:
self.panel.destroy_ui()
if self.tag and dpg.does_item_exist(self.tag):
dpg.delete_item(self.tag)
def _on_title_change(self, sender, app_data):
self.panel.title = app_data
def _on_type_change(self, sender, app_data):
print(f"Panel type change requested: {app_data}")
def _split(self, orientation: str):
if self.layout_manager:
self.layout_manager.split_node(self, orientation)
def _clear(self):
if hasattr(self.panel, 'clear_all_series'):
self.panel.clear_all_series()
def _delete(self):
if self.layout_manager:
self.layout_manager.delete_node(self)
class SplitterNode(LayoutNode):
def __init__(self, children: list[LayoutNode], orientation: str = "horizontal", node_id: str | None = None):
super().__init__(node_id)
self.children = children if children else []
self.orientation = orientation
self.child_proportions = [1.0 / len(self.children) for _ in self.children] if self.children else []
self.child_container_tags: list[str] = [] # Track container tags for resizing
def preserve_data(self):
for child in self.children:
child.preserve_data()
def add_child(self, child: LayoutNode, index: int = None):
if index is None:
self.children.append(child)
self.child_proportions.append(0.0)
else:
self.children.insert(index, child)
self.child_proportions.insert(index, 0.0)
self._redistribute_proportions()
def remove_child(self, child: LayoutNode):
if child in self.children:
index = self.children.index(child)
self.children.remove(child)
self.child_proportions.pop(index)
child.destroy_ui()
if self.children:
self._redistribute_proportions()
def replace_child(self, old_child: LayoutNode, new_child: LayoutNode):
try:
index = self.children.index(old_child)
self.children[index] = new_child
return index
except ValueError:
return None
def _redistribute_proportions(self):
if self.children:
equal_proportion = 1.0 / len(self.children)
self.child_proportions = [equal_proportion for _ in self.children]
def resize_children(self):
if not self.tag or not dpg.does_item_exist(self.tag):
return
available_width, available_height = dpg.get_item_rect_size(dpg.get_item_parent(self.tag))
for i, container_tag in enumerate(self.child_container_tags):
if not dpg.does_item_exist(container_tag):
continue
proportion = self.child_proportions[i] if i < len(self.child_proportions) else (1.0 / len(self.children))
if self.orientation == "horizontal":
new_width = max(100, int(available_width * proportion))
dpg.configure_item(container_tag, width=new_width)
else:
new_height = max(100, int(available_height * proportion))
dpg.configure_item(container_tag, height=new_height)
child = self.children[i] if i < len(self.children) else None
if child and isinstance(child, SplitterNode):
child.resize_children()
def create_ui(self, parent_tag: str, width: int = -1, height: int = -1):
self.tag = f"splitter_{self.node_id}"
self.child_container_tags = []
if self.orientation == "horizontal":
with dpg.group(tag=self.tag, parent=parent_tag, horizontal=True):
for i, child in enumerate(self.children):
proportion = self.child_proportions[i]
child_width = max(100, int(width * proportion))
container_tag = f"child_container_{self.node_id}_{i}"
self.child_container_tags.append(container_tag)
with dpg.child_window(tag=container_tag, width=child_width, height=-1, border=False, no_scrollbar=True, resizable_x=False):
child.create_ui(container_tag, child_width, height)
else:
with dpg.group(tag=self.tag, parent=parent_tag):
for i, child in enumerate(self.children):
proportion = self.child_proportions[i]
child_height = max(100, int(height * proportion))
container_tag = f"child_container_{self.node_id}_{i}"
self.child_container_tags.append(container_tag)
with dpg.child_window(tag=container_tag, width=-1, height=child_height, border=False, no_scrollbar=True, resizable_y=False):
child.create_ui(container_tag, width, child_height)
def destroy_ui(self):
for child in self.children:
if child:
child.destroy_ui()
if self.tag and dpg.does_item_exist(self.tag):
dpg.delete_item(self.tag)
self.child_container_tags.clear()
class PlotLayoutManager:
def __init__(self, data_manager: DataManager, playback_manager, scale: float = 1.0):
self.data_manager = data_manager
self.playback_manager = playback_manager
self.scale = scale
self.container_tag = "plot_layout_container"
self._initialize_default_layout()
def _initialize_default_layout(self):
panel = TimeSeriesPanel(self.data_manager, self.playback_manager)
self.root_node = LeafNode(panel, layout_manager=self, scale=self.scale)
def create_ui(self, parent_tag: str):
if dpg.does_item_exist(self.container_tag):
dpg.delete_item(self.container_tag)
with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True):
container_width, container_height = dpg.get_item_rect_size(self.container_tag)
self.root_node.create_ui(self.container_tag, container_width, container_height)
def on_viewport_resize(self):
if isinstance(self.root_node, SplitterNode):
self.root_node.resize_children()
def split_node(self, node: LeafNode, orientation: str):
# create new panel for the split
new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager) # TODO: create same type of panel as the split
new_leaf = LeafNode(new_panel, layout_manager=self, scale=self.scale)
parent_node, child_index = self._find_parent_and_index(node)
if parent_node is None: # root node - create new splitter as root
node.preserve_data()
self.root_node = SplitterNode([node, new_leaf], orientation)
self._update_ui_for_node(self.root_node, self.container_tag)
elif isinstance(parent_node, SplitterNode) and parent_node.orientation == orientation: # same orientation - add to existing splitter
parent_node.add_child(new_leaf, child_index + 1)
self._update_ui_for_node(parent_node)
else: # different orientation - replace node with new splitter
node.preserve_data()
new_splitter = SplitterNode([node, new_leaf], orientation)
self._replace_child_in_parent(parent_node, node, new_splitter)
def delete_node(self, node: LeafNode): # TODO: actually delete the node, not just the ui for the node
parent_node, child_index = self._find_parent_and_index(node)
if parent_node is None: # root deletion - replace with new default
node.destroy_ui()
self._initialize_default_layout()
self._update_ui_for_node(self.root_node, self.container_tag)
elif isinstance(parent_node, SplitterNode):
parent_node.remove_child(node)
if len(parent_node.children) == 1: # collapse splitter --> leaf to just leaf
remaining_child = parent_node.children[0]
grandparent_node, parent_index = self._find_parent_and_index(parent_node)
if grandparent_node is None: # promote remaining child to root
remaining_child.preserve_data()
parent_node.children.remove(remaining_child)
self.root_node = remaining_child
parent_node.destroy_ui()
self._update_ui_for_node(self.root_node, self.container_tag)
else: # replace splitter with remaining child in grandparent node
self._replace_child_in_parent(grandparent_node, parent_node, remaining_child)
else: # update splpitter contents
self._update_ui_for_node(parent_node)
def _replace_child_in_parent(self, parent_node: SplitterNode, old_child: LayoutNode, new_child: LayoutNode):
old_child.preserve_data() # save data and for when recreating ui for the node
child_index = parent_node.children.index(old_child)
child_container_tag = f"child_container_{parent_node.node_id}_{child_index}"
parent_node.replace_child(old_child, new_child)
# Clean up old child if it's being replaced (not just moved)
if old_child != new_child:
old_child.destroy_ui()
if dpg.does_item_exist(child_container_tag):
dpg.delete_item(child_container_tag, children_only=True)
container_width, container_height = dpg.get_item_rect_size(child_container_tag)
new_child.create_ui(child_container_tag, container_width, container_height)
def _update_ui_for_node(self, node: LayoutNode, container_tag: str = None):
node.preserve_data()
if container_tag: # update node in a specific container (usually root)
dpg.delete_item(container_tag, children_only=True)
container_width, container_height = dpg.get_item_rect_size(container_tag)
node.create_ui(container_tag, container_width, container_height)
else: # update node in its current location (splitter updates)
if node.tag and dpg.does_item_exist(node.tag):
parent_container = dpg.get_item_parent(node.tag)
node.destroy_ui()
if parent_container and dpg.does_item_exist(parent_container):
parent_width, parent_height = dpg.get_item_rect_size(parent_container)
node.create_ui(parent_container, parent_width, parent_height)
def _find_parent_and_index(self, target_node: LayoutNode): # TODO: probably can be stored in child
def search_recursive(node: LayoutNode | None, parent: LayoutNode | None = None, index: int = 0):
if node == target_node:
return parent, index
if isinstance(node, SplitterNode):
for i, child in enumerate(node.children):
result = search_recursive(child, node, i)
if result[0] is not None:
return result
return None, None
return search_recursive(self.root_node)