20 import xml.etree.ElementTree
as ET
32 'BlackboardCheckDouble',
33 'BlackboardCheckString',
55 'ReinitializeGlobalLocalization',
61 'initialPoseReceived',
80 args = parse_command_line()
81 xml_tree = ET.parse(args.behavior_tree)
82 root_tree_name = find_root_tree_name(xml_tree)
83 behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
84 dot = convert2dot(behavior_tree, xml_tree)
86 legend = make_legend()
88 legend.render(args.legend)
91 print(f
'Saving dot to {args.save_dot}')
92 args.save_dot.write(dot.source)
93 dot.render(args.image_out, view=args.display)
96 def parse_command_line() -> argparse.Namespace:
97 parser = argparse.ArgumentParser(
98 description=
'Convert a behavior tree XML file to an image'
103 help=
'the behavior tree XML file to convert to an image',
108 help=
'The name of the output image file. Leave off the .png extension',
113 help=
'If specified, opens the image in the default viewer',
117 type=argparse.FileType(
'w'),
118 help=
'Saves the intermediate dot source to the specified file',
120 parser.add_argument(
'--legend', help=
'Generate a legend image as well')
121 return parser.parse_args()
124 def find_root_tree_name(xml_tree: ET.ElementTree) -> str:
125 root = xml_tree.getroot()
126 main_tree = root.get(
'main_tree_to_execute')
127 if main_tree
is None:
128 raise RuntimeError(
'No main_tree_to_execute attribute found in XML root')
132 def find_behavior_tree(xml_tree: ET.ElementTree, tree_name: str) -> ET.Element:
133 trees = xml_tree.findall(
'BehaviorTree')
135 raise RuntimeError(
'No behavior trees were found in the XML file')
138 if tree_name == tree.get(
'ID'):
141 raise RuntimeError(f
'No behavior tree for name {tree_name} found in the XML file')
145 def convert2dot(behavior_tree: ET.Element, xml_tree: ET.ElementTree) -> graphviz.Digraph:
146 dot = graphviz.Digraph()
148 parent_dot_name = str(hash(root))
149 dot.node(parent_dot_name, root.get(
'ID'), shape=
'box')
150 convert_subtree(dot, root, parent_dot_name, xml_tree)
158 dot: graphviz.Digraph,
159 parent_node: ET.Element,
160 parent_dot_name: str,
161 xml_tree: ET.ElementTree,
163 if parent_node.tag ==
'SubTree':
164 add_sub_tree(dot, parent_dot_name, parent_node, xml_tree)
166 add_nodes(dot, parent_dot_name, parent_node, xml_tree)
170 dot: graphviz.Digraph,
171 parent_dot_name: str,
172 parent_node: ET.Element,
173 xml_tree: ET.ElementTree,
175 root_tree_name = parent_node.get(
'ID')
176 if root_tree_name
is None:
177 raise RuntimeError(
'SubTree node has no ID attribute')
178 dot.node(parent_dot_name, root_tree_name, shape=
'box')
179 behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
180 convert_subtree(dot, behavior_tree, parent_dot_name, xml_tree)
184 dot: graphviz.Digraph,
185 parent_dot_name: str,
186 parent_node: ET.Element,
187 xml_tree: ET.ElementTree,
189 for node
in list(parent_node):
190 label = make_label(node)
194 color=node_color(node.tag),
198 dot_name = str(hash(node))
199 dot.edge(parent_dot_name, dot_name)
200 convert_subtree(dot, node, dot_name, xml_tree)
205 def make_label(node: ET.Element) -> str:
206 label =
"< <table border='0' cellspacing='0' cellpadding='0'>"
207 label += f
"<tr><td align='text'><i>{node.tag}</i></td></tr>"
208 name = node.get(
'name')
210 label += f
"<tr><td align='text'><b>{name}</b></td></tr>"
212 for param_name, value
in node.items():
213 label += f
"<tr><td align='left'><sub>{param_name}={value}</sub></td></tr>"
214 label +=
'</table> >'
218 def node_color(node_type: str) -> str:
219 if node_type
in control_nodes:
221 if node_type
in action_nodes:
222 return 'cornflowerblue'
223 if node_type
in condition_nodes:
225 if node_type
in decorator_nodes:
227 if node_type
in subtree_nodes:
234 def make_legend() -> graphviz.Digraph:
235 legend = graphviz.Digraph(graph_attr={
'rankdir':
'LR'})
236 legend.attr(label=
'Legend')
237 legend.node(
'Unknown', shape=
'box', style=
'filled', color=
'grey')
239 'Action',
'Action Node', shape=
'box', style=
'filled', color=
'cornflowerblue'
242 'Condition',
'Condition Node', shape=
'box', style=
'filled', color=
'yellow2'
245 'Control',
'Control Node', shape=
'box', style=
'filled', color=
'chartreuse4'
251 if __name__ ==
'__main__':