AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
tree_document.cpp
1// Copyright 2024 Robin Müller
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "auto_apms_behavior_tree_core/tree/tree_document.hpp"
16
17#include <algorithm>
18#include <filesystem>
19
20#include "ament_index_cpp/get_package_share_directory.hpp"
21#include "auto_apms_behavior_tree_core/builder.hpp"
22#include "auto_apms_behavior_tree_core/exceptions.hpp"
23#include "auto_apms_behavior_tree_core/node/node_model_type.hpp"
24#include "auto_apms_behavior_tree_core/tree/tree_resource.hpp"
25#include "auto_apms_util/container.hpp"
26#include "auto_apms_util/logging.hpp"
27#include "auto_apms_util/string.hpp"
28#include "behaviortree_cpp/xml_parsing.h"
29
31{
32
33std::vector<std::string> getAllTreeNamesImpl(const tinyxml2::XMLDocument & doc)
34{
35 std::vector<std::string> names;
36 if (const tinyxml2::XMLElement * root = doc.RootElement()) {
37 if (strcmp(root->Name(), TreeDocument::ROOT_ELEMENT_NAME) == 0) {
38 // Found root element
39 for (const tinyxml2::XMLElement * child = root->FirstChildElement(); child != nullptr;
40 child = child->NextSiblingElement()) {
41 if (strcmp(TreeDocument::TREE_ELEMENT_NAME, child->Name()) == 0) {
42 if (const char * name = child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) {
43 names.push_back(name);
44 } else {
45 throw exceptions::TreeDocumentError(
46 "Cannot get tree name, because required attribute '" +
47 std::string(TreeDocument::TREE_NAME_ATTRIBUTE_NAME) + "' is missing.");
48 }
49 }
50 }
51 } else if (strcmp(root->Name(), TreeDocument::TREE_ELEMENT_NAME) == 0) {
52 // Found behavior tree element as root
53 if (const char * name = root->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) names.push_back(name);
54 }
55 }
56 return names;
57}
58
60: doc_ptr_(doc_ptr), ele_ptr_(ele_ptr)
61{
62 if (!doc_ptr) {
63 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with doc_ptr=nullptr.");
64 }
65 if (!ele_ptr) {
66 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with ele_ptr=nullptr.");
67 }
68 const NodeModelMap model = doc_ptr_->getNodeModel(true);
69 NodeModelMap::const_iterator it = model.find(ele_ptr_->Name());
70 std::vector<NodePortInfo> port_infos_str_vec;
71 if (it != model.end()) port_infos_str_vec = it->second.port_infos;
72
73 // There should always be a corresponding element in the nodes model. However, if the constructor is called e.g.
74 // before the document registered the respective node, that's not the case and setPorts() won't work as expected.
75 // Therefore, we must ensure that all factory methods for a NodeElement verify that the node is known to the document
76 // before creating an instance. NOTE: We must not throw, since the constructor must also succeed for the derived
77 // TreeElement class for which there is no node model and setPorts() is deleted anyways.
78 if (!port_infos_str_vec.empty()) {
79 for (const NodePortInfo & port_info : port_infos_str_vec) {
80 port_names_.push_back(port_info.port_name);
81 if (!port_info.port_default.empty()) port_default_values_[port_info.port_name] = port_info.port_default;
82 }
83
84 // Set the node's attributes according to the default values for all the ports that have a default value and have
85 // not been set previously.
86 PortValues existing_port_values = getPorts();
87 for (const auto & [name, val] : port_default_values_) {
88 if (existing_port_values.find(name) == existing_port_values.end()) {
89 ele_ptr_->SetAttribute(name.c_str(), val.c_str());
90 }
91 }
92 }
93}
94
96{
97 XMLElement * other_ele = other.ele_ptr_;
98 if (ele_ptr_ != other_ele) {
99 // Copy the other element and all its children
100 other_ele = other_ele->DeepClone(doc_ptr_)->ToElement();
101 // Insert new element in place of this
102 XMLElement * prev = ele_ptr_->PreviousSiblingElement();
103 if (prev) {
104 ele_ptr_->Parent()->InsertAfterChild(ele_ptr_->PreviousSibling(), other_ele);
105 } else {
106 ele_ptr_->Parent()->InsertFirstChild(other_ele);
107 }
108 // Delete this element
109 doc_ptr_->DeleteNode(ele_ptr_);
110 // Update the internal pointer
111 ele_ptr_ = other_ele;
112 }
113 port_names_ = other.port_names_;
114 port_default_values_ = other.port_default_values_;
115 return *this;
116}
117
118bool TreeDocument::NodeElement::operator==(const NodeElement & other) const { return ele_ptr_ == other.ele_ptr_; }
119
120bool TreeDocument::NodeElement::operator!=(const NodeElement & other) const { return !this->operator==(other); }
121
123 const std::string & name, const NodeElement * before_this)
124{
125 if (const std::set<std::string> names = doc_ptr_->getRegisteredNodeNames(true); names.find(name) == names.end()) {
126 throw exceptions::TreeDocumentError(
127 "Cannot insert unkown node <" + name +
128 ">. Before inserting a new node, the associated document must register the corresponding behavior tree "
129 "node. Consider using a signature of insertNode() that does this automatically.");
130 }
131 XMLElement * ele = doc_ptr_->NewElement(name.c_str());
132 return insertBeforeImpl(before_this, ele);
133}
134
136 const std::string & name, const NodeRegistrationOptions & registration_options, const NodeElement * before_this)
137{
138 // Check if the node name is known. If the user tries to register a node with the same name as one of the native
139 // nodes, we want registerNodes to throw since those names are reserved (Therefore we set include_native to false).
140 if (const std::set<std::string> names = doc_ptr_->getRegisteredNodeNames(false); names.find(name) == names.end()) {
141 doc_ptr_->registerNodes(NodeManifest({{name, registration_options}}));
142 }
143 return insertNode(name, before_this);
144}
145
147 const std::string & tree_name, const NodeElement * before_this)
148{
149 if (!doc_ptr_->hasTreeName(tree_name)) {
150 throw exceptions::TreeDocumentError(
151 "Cannot insert subtree node for tree '" + tree_name + "' because no tree with that name exists.");
152 }
153 NodeElement ele = insertNode(SUBTREE_ELEMENT_NAME, before_this);
154 ele.ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
155 return model::SubTree(ele.doc_ptr_, ele.ele_ptr_);
156}
157
159{
160 const std::string tree_name = tree.getName();
161 if (doc_ptr_->hasTreeName(tree_name)) {
162 // We don't allow inserting a subtree node for a tree with the same name of another existing tree
163 if (doc_ptr_->getTree(tree_name) != tree) {
164 throw exceptions::TreeDocumentError(
165 "Cannot insert subtree node using the provided tree element, because another tree with name '" + tree_name +
166 "' already exists.");
167 }
168 } else {
169 // Add the tree provided as argument to the document
170 doc_ptr_->mergeTree(tree, false);
171 }
172 return insertSubTreeNode(tree_name, before_this);
173}
174
176 const TreeElement & tree, const NodeElement * before_this)
177{
178 const XMLElement * root_child = tree.ele_ptr_->FirstChildElement();
179 if (!root_child) {
180 throw exceptions::TreeDocumentError(
181 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has no child nodes.");
182 }
183 if (root_child->NextSibling()) {
184 throw exceptions::TreeDocumentError(
185 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has more than one child node.");
186 }
187 doc_ptr_->registerNodes(tree.getRequiredNodeManifest());
188 return insertTreeFromDocument(*tree.doc_ptr_, tree.getName(), before_this);
189}
190
192 const TreeDocument & doc, const std::string & tree_name, const NodeElement * before_this)
193{
194 const std::vector<std::string> other_tree_names = doc.getAllTreeNames();
195 if (other_tree_names.empty()) {
196 throw exceptions::TreeDocumentError(
197 "Cannot insert tree '" + tree_name + "' because document has no <" + TREE_ELEMENT_NAME + "> elements.");
198 }
199 if (!auto_apms_util::contains(other_tree_names, tree_name)) {
200 throw exceptions::TreeDocumentError(
201 "Cannot insert tree '" + tree_name + "' because document doesn't specify a tree with that name.");
202 }
203
204 // List including the target tree name and the names of its dependencies (Trees required by SubTree nodes)
205 std::set<std::string> required_tree_names = {tree_name};
206
207 // Find all subtree nodes and push back the associated tree to required_tree_names.
208 // NOTE: All tree elements within a TreeDocument instance always have exactly one child, so we don't have to verify
209 // that here again
210 TreeDocument temp_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_); // Temporary working document
211 doc.DeepCopy(&temp_doc);
212 const TreeElement other_tree(&temp_doc, temp_doc.getXMLElementForTreeWithName(tree_name));
213 ConstDeepApplyCallback collect_dependency_tree_names;
214 collect_dependency_tree_names = [&required_tree_names, &collect_dependency_tree_names](const NodeElement & ele) {
215 if (ele.getRegistrationName() == SUBTREE_ELEMENT_NAME) {
216 if (const char * name = ele.ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
217 // Add the tree name to the list
218 required_tree_names.insert(name);
219
220 // Search for more tree dependencies under the tree pointed by this subtree node
221 ele.doc_ptr_->getTree(name).deepApplyConst(collect_dependency_tree_names);
222 } else {
223 throw exceptions::TreeDocumentError("Subtree element has no name attribute.");
224 };
225 }
226 // The return value doesn't matter here
227 return false;
228 };
229 other_tree.deepApplyConst(collect_dependency_tree_names);
230
231 // Verify that all child nodes of the trees listed in required_tree_names are known to the builder
232 ConstDeepApplyCallback apply = [available_names = doc_ptr_->getRegisteredNodeNames(true)](const NodeElement & ele) {
233 return available_names.find(ele.getRegistrationName()) == available_names.end();
234 };
235 for (const std::string & name : required_tree_names) {
236 const NodeElement ele(&temp_doc, temp_doc.getXMLElementForTreeWithName(name)->FirstChildElement());
237 if (const std::vector<NodeElement> found = ele.deepApplyConst(apply); !found.empty()) {
238 std::vector<std::string> names;
239 for (const NodeElement & ele : found) names.push_back(ele.getFullyQualifiedName());
240 throw exceptions::TreeDocumentError(
241 "Cannot insert tree '" + tree_name + "' because the following nodes found in tree '" + name +
242 "' are unkown to the builder:\n\t- " + auto_apms_util::join(names, "\n\t- "));
243 }
244 }
245
246 // Remove the data from each tree that the target tree doesn't directly depend on and merge the working document. This
247 // approach ensures, that the verification logic inside merge() is applied to the working document.
248 required_tree_names.erase(tree_name); // Don't add the target tree twice
249 for (const std::string & name : other_tree_names) {
250 if (required_tree_names.find(name) == required_tree_names.end()) {
251 // Remove if not required
252 temp_doc.removeTree(name);
253 }
254 }
255 doc_ptr_->mergeTreeDocument(static_cast<const XMLDocument &>(temp_doc), false);
256
257 // Clone and insert the target tree
258 return insertBeforeImpl(
259 before_this, doc.getXMLElementForTreeWithName(tree_name)->FirstChildElement()->DeepClone(doc_ptr_)->ToElement());
260}
261
263 const TreeDocument & doc, const NodeElement * before_this)
264{
265 return insertTreeFromDocument(doc, doc.getRootTreeName(), before_this);
266}
267
269 const std::string & tree_str, const std::string & tree_name, const NodeElement * before_this)
270{
271 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
272 insert_doc.mergeString(tree_str);
273 return insertTreeFromDocument(insert_doc, tree_name, before_this);
274}
275
277 const std::string & tree_str, const NodeElement * before_this)
278{
279 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
280 insert_doc.mergeString(tree_str);
281 return insertTreeFromDocument(insert_doc, before_this);
282}
283
285 const std::string & path, const std::string & tree_name, const NodeElement * before_this)
286{
287 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
288 insert_doc.mergeFile(path);
289 return insertTreeFromDocument(insert_doc, tree_name, before_this);
290}
291
293 const std::string & path, const NodeElement * before_this)
294{
295 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
296 insert_doc.mergeFile(path);
297 return insertTreeFromDocument(insert_doc, before_this);
298}
299
301 const TreeResource & resource, const std::string & tree_name, const NodeElement * before_this)
302{
303 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
304 insert_doc.mergeFile(resource.build_request_file_path_);
305
306 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This means,
307 // that also potentially unused nodes are available and registered with the factory. This seems unnecessary, but it's
308 // very convenient and the performance probably doesn't suffer too much.
309 doc_ptr_->registerNodes(resource.getNodeManifest(), false);
310
311 return insertTreeFromDocument(insert_doc, tree_name, before_this);
312}
313
315 const TreeResource & resource, const NodeElement * before_this)
316{
317 return insertTreeFromResource(resource, resource.getRootTreeName(), before_this);
318}
319
320bool TreeDocument::NodeElement::hasChildren() const { return ele_ptr_->FirstChild() == nullptr ? false : true; }
321
323 const std::string & registration_name, const std::string & instance_name) const
324{
325 if (registration_name.empty() && instance_name.empty()) return NodeElement(doc_ptr_, ele_ptr_->FirstChildElement());
326
327 // If name is given, recursively search for the first node with this name
328 ConstDeepApplyCallback apply = [&registration_name, &instance_name](const NodeElement & ele) {
329 if (registration_name.empty()) return ele.getName() == instance_name;
330 if (instance_name.empty()) return ele.getRegistrationName() == registration_name;
331 return ele.getRegistrationName() == registration_name && ele.getName() == instance_name;
332 };
333 if (const std::vector<NodeElement> found = deepApplyConst(apply); !found.empty()) return found[0];
334
335 // Cannot find node in children of this
336 throw exceptions::TreeDocumentError(
337 "Cannot find a child node that matches the search arguments (registration_name: '" + registration_name +
338 "' - instance_name: '" + instance_name + "') in parent element '" + getFullyQualifiedName() + "'.");
339}
340
342 const std::string & registration_name, const std::string & instance_name)
343{
344 ele_ptr_->DeleteChild(getFirstNode(registration_name, instance_name).ele_ptr_);
345 return *this;
346}
347
349{
350 ele_ptr_->DeleteChildren();
351 return *this;
352}
353
354const std::vector<std::string> & TreeDocument::NodeElement::getPortNames() const { return port_names_; }
355
357{
358 PortValues values;
359 for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
360 if (const std::string attr_name = attr->Name(); auto_apms_util::contains(port_names_, attr_name)) {
361 values[attr_name] = attr->Value();
362 }
363 }
364 return values;
365}
366
368{
369 // Verify port_values
370 std::vector<std::string> unkown_keys;
371 for (const auto & [key, _] : port_values) {
372 if (!auto_apms_util::contains(port_names_, key)) unkown_keys.push_back(key);
373 }
374 if (!unkown_keys.empty()) {
375 throw exceptions::TreeDocumentError(
376 "Cannot set ports. According to the node model, the following ports are not implemented by '" +
377 std::string(ele_ptr_->Name()) + "': [ " + auto_apms_util::join(unkown_keys, ", ") + " ].");
378 }
379
380 // Populate attributes according to the content of port_values
381 for (const auto & [key, val] : port_values) {
382 ele_ptr_->SetAttribute(key.c_str(), val.c_str());
383 }
384 return *this;
385}
386
388{
389 for (const std::string & name : port_names_) {
390 PortValues::const_iterator it = port_default_values_.find(name);
391 if (it != port_default_values_.end()) {
392 ele_ptr_->SetAttribute(it->first.c_str(), it->second.c_str());
393 } else {
394 ele_ptr_->DeleteAttribute(name.c_str());
395 }
396 }
397 return *this;
398}
399
401{
402 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
403 return *this;
404}
405
407{
408 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
409 return *this;
410}
411
413{
414 ele_ptr_->SetAttribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME, instance_name.c_str());
415 return *this;
416}
417
418std::string TreeDocument::NodeElement::getRegistrationName() const { return ele_ptr_->Name(); }
419
421{
422 if (const char * name = ele_ptr_->Attribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME)) return name;
423 return getRegistrationName();
424}
425
427{
428 std::string registration_name = getRegistrationName();
429 std::string instance_name = getName();
430 if (registration_name == instance_name) return registration_name;
431 return instance_name + " (" + registration_name + ")";
432}
433
435
436const std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApplyConst(
437 ConstDeepApplyCallback apply_callback) const
438{
439 std::vector<NodeElement> found;
440 deepApplyImpl(*this, apply_callback, found);
441 return found;
442}
443
444std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApply(DeepApplyCallback apply_callback)
445{
446 std::vector<NodeElement> found;
447 deepApplyImpl(*this, apply_callback, found);
448 return found;
449}
450
451TreeDocument::NodeElement TreeDocument::NodeElement::insertBeforeImpl(
452 const NodeElement * before_this, XMLElement * add_this)
453{
454 if (before_this) {
455 XMLElement * prev = nullptr;
456 XMLElement * curr = ele_ptr_->FirstChildElement();
457
458 // Traverse through siblings of first child to find the before_this node.
459 // If there are no siblings, add the first child to this;
460 bool found = false;
461 while (curr) {
462 if (curr == before_this->ele_ptr_) {
463 found = true;
464 break;
465 }
466 prev = curr;
467 curr = curr->NextSiblingElement();
468 }
469 if (prev) {
470 if (found) {
471 ele_ptr_->InsertAfterChild(prev, add_this);
472 } else {
473 throw exceptions::TreeDocumentError(
474 "NodeElement before_this (" + before_this->getFullyQualifiedName() + ") is not a child of " +
475 getFullyQualifiedName() + ".");
476 }
477 } else {
478 ele_ptr_->InsertFirstChild(add_this);
479 }
480 } else {
481 ele_ptr_->InsertEndChild(add_this);
482 }
483 return NodeElement(doc_ptr_, add_this);
484}
485
486void TreeDocument::NodeElement::deepApplyImpl(
487 const NodeElement & parent, ConstDeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
488{
489 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
490 child = child->NextSiblingElement()) {
491 const NodeElement child_ele(parent.doc_ptr_, child);
492
493 // Apply on current child element
494 if (apply_callback(child_ele)) vec.push_back(child_ele);
495
496 // Search children of child before evaluating the siblings
497 deepApplyImpl(child_ele, apply_callback, vec);
498 }
499}
500
501void TreeDocument::NodeElement::deepApplyImpl(
502 NodeElement & parent, DeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
503{
504 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
505 child = child->NextSiblingElement()) {
506 NodeElement child_ele(parent.doc_ptr_, child);
507
508 // Apply on current child element
509 if (apply_callback(child_ele)) vec.push_back(child_ele);
510
511 // Search children of child before evaluating the siblings
512 deepApplyImpl(child_ele, apply_callback, vec);
513 }
514}
515
516TreeDocument::TreeElement::TreeElement(TreeDocument * doc_ptr, XMLElement * ele_ptr) : NodeElement(doc_ptr, ele_ptr)
517{
518 if (!ele_ptr->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
519 throw exceptions::TreeDocumentError(
520 "Cannot create tree element without a '" + std::string(TREE_NAME_ATTRIBUTE_NAME) + "' attribute.");
521 }
522}
523
525{
526 const std::string other_tree_name = other.getName();
527 // For a tree replacement to be allowed, the other tree name must be the same or at least not already taken by another
528 // tree inside this document.
529 if (getName() != other_tree_name && doc_ptr_->hasTreeName(other.getName())) {
530 throw exceptions::TreeDocumentError(
531 "Cannot copy tree '" + other.getName() + "' because another tree with this name already exists.");
532 }
534 return *this;
535}
536
538{
539 ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
540 return *this;
541}
542
544{
545 if (const char * name = ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) return name;
546 return "unkown";
547}
548
550{
551 doc_ptr_->setRootTreeName(getName());
552 return *this;
553}
554
556{
557 NodeManifest m;
558 deepApplyConst([this, &m](const NodeElement & node) {
559 const std::string name = node.getRegistrationName();
560 const bool is_native_node = doc_ptr_->native_node_names_.find(name) != doc_ptr_->native_node_names_.end();
561 if (!is_native_node && !m.contains(name)) {
562 if (!doc_ptr_->registered_nodes_manifest_.contains(name)) {
563 throw exceptions::NodeManifestError(
564 "Cannot assemble the required node manifest for tree '" + getName() +
565 "' since there are no registration options for node '" + name + "'.");
566 }
567 m.add(name, doc_ptr_->registered_nodes_manifest_[name]);
568 }
569 return false;
570 });
571 return m;
572}
573
575{
576 TreeDocument doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
577 return doc.mergeTree(*this, true).verify();
578}
579
581{
582 XMLDocument tree_doc;
583 tree_doc.InsertEndChild(ele_ptr_->DeepClone(&tree_doc));
584 tinyxml2::XMLPrinter printer;
585 tree_doc.Print(&printer);
586 return printer.CStr();
587}
588
590 const std::string & registration_name, const std::string & instance_name)
591{
592 NodeElement::removeFirstChild(registration_name, instance_name);
593 return *this;
594}
595
601
602TreeDocument::TreeDocument(const std::string & format_version, NodeRegistrationLoader::SharedPtr tree_node_loader)
603// It's important to initialize XMLDocument using PRESERVE_WHITESPACE, since encoded port data (like
604// NodeRegistrationOptions) may be sensitive to changes in the whitespaces (think of the YAML format).
605: XMLDocument(true, tinyxml2::PRESERVE_WHITESPACE),
606 all_node_classes_package_map_(auto_apms_behavior_tree::core::NodeRegistrationLoader().getClassPackageMap()),
607 native_node_names_(BT::BehaviorTreeFactory().builtinNodes()),
608 format_version_(format_version),
609 tree_node_loader_ptr_(tree_node_loader),
610 registered_nodes_manifest_(),
611 factory_(),
612 logger_(rclcpp::get_logger(LOGGER_NAME))
613{
614 reset();
615}
616
617TreeDocument & TreeDocument::mergeTreeDocument(const XMLDocument & other, bool adopt_root_tree)
618{
619 std::set<std::string> include_stack;
620 return mergeTreeDocumentImpl(other, adopt_root_tree, include_stack);
621}
622
623TreeDocument & TreeDocument::mergeTreeDocumentImpl(
624 const XMLDocument & other, bool adopt_root_tree, std::set<std::string> & include_stack)
625{
626 const XMLElement * other_root = other.RootElement();
627 if (!other_root) {
628 throw exceptions::TreeDocumentError("Cannot merge tree documents: other_root is nullptr.");
629 };
630
631 auto verify_tree_structure = [](const XMLElement * tree_ele) {
632 const char * tree_id = tree_ele->Attribute(TREE_NAME_ATTRIBUTE_NAME);
633 if (!tree_id) {
634 throw exceptions::TreeDocumentError(
635 "Cannot merge tree document: Found a <" + std::string(TREE_ELEMENT_NAME) +
636 "> element that doesn't specify the required attribute '" + TREE_NAME_ATTRIBUTE_NAME + "'.");
637 }
638 const XMLElement * tree_root_child = tree_ele->FirstChildElement();
639 if (!tree_root_child) {
640 throw exceptions::TreeDocumentError(
641 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has no child nodes.");
642 }
643 if (tree_root_child->NextSibling()) {
644 throw exceptions::TreeDocumentError(
645 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has more than one child node.");
646 }
647 };
648
649 // Helper lambda to check for duplicate tree names and throw with a descriptive message
650 auto check_duplicates = [this](const std::vector<std::string> & new_tree_names, const std::string & source) {
651 const auto common = auto_apms_util::getCommonElements(getAllTreeNames(), new_tree_names);
652 if (!common.empty()) {
653 throw exceptions::TreeDocumentError(
654 "Cannot merge tree document: The following trees from " + source + " are already defined: [ " +
655 auto_apms_util::join(common, ", ") + " ].");
656 }
657 };
658
659 // Get tree names from the other document (used for duplicate check and adopt_root_tree logic)
660 const std::vector<std::string> other_tree_names = getAllTreeNamesImpl(other);
661
662 if (strcmp(other_root->Name(), ROOT_ELEMENT_NAME) == 0) {
663 // Verify format
664 if (const char * ver = other_root->Attribute(BTCPP_FORMAT_ATTRIBUTE_NAME)) {
665 if (std::string(ver) != format_version_) {
666 throw exceptions::TreeDocumentError(
667 "Cannot merge tree document: Format of other document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
668 ver + ") is not compatible with this document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
669 format_version_ + ").");
670 }
671 } else {
672 throw exceptions::TreeDocumentError(
673 "Cannot merge tree document: Root element of other document doesn't have required attribute '" +
674 std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
675 }
676
677 // Iterate over all include elements and store the associated content to a temporary buffer document
678 TreeDocument include_doc(format_version_, tree_node_loader_ptr_);
679 for (const XMLElement * include_ele = other_root->FirstChildElement(INCLUDE_ELEMENT_NAME); include_ele != nullptr;
680 include_ele = include_ele->NextSiblingElement(INCLUDE_ELEMENT_NAME)) {
681 if (const char * path = include_ele->Attribute(INCLUDE_PATH_ATTRIBUTE_NAME)) {
682 if (std::string(path).empty()) {
683 throw exceptions::TreeDocumentError(
684 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
685 "> element that specifies an empty path in attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
686 }
687 std::string absolute_path = path;
688 if (const char * ros_pkg = include_ele->Attribute(INCLUDE_ROS_PKG_ATTRIBUTE_NAME)) {
689 if (std::string(ros_pkg).empty()) {
690 throw exceptions::TreeDocumentError(
691 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
692 "> element that specifies an empty ROS package name in attribute '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
693 "'.");
694 }
695 if (std::filesystem::path(path).is_absolute()) {
696 throw exceptions::TreeDocumentError(
697 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
698 "> element that specifies an absolute path '" + std::string(path) + "' in attribute '" +
699 INCLUDE_PATH_ATTRIBUTE_NAME + "' together with a ROS package name in attribute '" +
700 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'. Please remove the attribute " + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
701 " or use a relative path if you want to refer to a location "
702 "relative to the package's share directory.");
703 }
704 try {
705 absolute_path =
706 (std::filesystem::path(ament_index_cpp::get_package_share_directory(ros_pkg)) / path).string();
707 } catch (const ament_index_cpp::PackageNotFoundError & e) {
708 throw exceptions::TreeDocumentError(
709 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
710 "> element that specifies a non-existing ROS package '" + std::string(ros_pkg) + "' in attribute '" +
711 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'.");
712 }
713 }
714
715 // Recursively merge the included file to the buffer document, passing the include stack for circular detection
716 try {
717 include_doc.mergeFileImpl(absolute_path, false, include_stack);
718 } catch (const std::exception & e) {
719 throw exceptions::TreeDocumentError(
720 "Cannot merge tree document: Failed to include file '" + absolute_path + "': " + e.what());
721 }
722 } else {
723 throw exceptions::TreeDocumentError(
724 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
725 "> element that doesn't specify the required attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
726 }
727 }
728
729 // Check for duplicates from included files before inserting
730 check_duplicates(include_doc.getAllTreeNames(), "included files");
731
732 // Iterate over all the tree elements of the include buffer and include them in this document
733 for (const XMLElement * child = include_doc.RootElement()->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
734 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
735 // We do not need to verify the tree structure here, since it has already been verified during the recursive
736 // mergeFile() calls by the for loop below
737
738 // If tree element is valid, append to this document
739 RootElement()->InsertEndChild(child->DeepClone(this));
740 }
741
742 // Check for duplicates from other document's direct trees before inserting
743 check_duplicates(other_tree_names, "the merged document");
744
745 // Iterate over all the tree elements of other root and verify them
746 for (const XMLElement * child = other_root->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
747 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
748 verify_tree_structure(child);
749
750 // If tree element is valid, append to this document
751 RootElement()->InsertEndChild(child->DeepClone(this));
752 }
753
754 if (adopt_root_tree) {
755 // After (!) all tree elements have been inserted, adopt the name specified in the corresponding attribute of the
756 // root element
757 if (const char * name = other_root->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) setRootTreeName(name);
758 }
759 } else if (strcmp(other_root->Name(), TREE_ELEMENT_NAME) == 0) {
760 // Allow a single behavior tree without <root> element.
761 // We assume that the the format complies with the version configured at construction time
762 verify_tree_structure(other_root);
763
764 // Check for duplicates before inserting
765 check_duplicates(other_tree_names, "the merged document");
766
767 // If tree element is valid, append to this document
768 RootElement()->InsertEndChild(other_root->DeepClone(this));
769 } else {
770 throw exceptions::TreeDocumentError(
771 "Cannot merge tree document: Root element of other document must either be <" + std::string(ROOT_ELEMENT_NAME) +
772 "> or <" + TREE_ELEMENT_NAME + ">.");
773 }
774
775 if (adopt_root_tree && other_tree_names.size() == 1) {
776 // If there is only one tree element, we change the root tree to the respective tree after (!) all tree elements
777 // have been inserted
778 setRootTreeName(other_tree_names[0]);
779 }
780
781 return *this;
782}
783
784TreeDocument & TreeDocument::mergeTreeDocument(const TreeDocument & other, bool adopt_root_tree)
785{
787 std::set<std::string> include_stack;
788 return mergeTreeDocumentImpl(static_cast<const XMLDocument &>(other), adopt_root_tree, include_stack);
789}
790
791TreeDocument & TreeDocument::mergeString(const std::string & tree_str, bool adopt_root_tree)
792{
793 XMLDocument other_doc;
794 if (other_doc.Parse(tree_str.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
795 throw exceptions::TreeDocumentError("Cannot merge tree document from string: " + std::string(other_doc.ErrorStr()));
796 }
797 std::set<std::string> include_stack;
798 return mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
799}
800
801TreeDocument & TreeDocument::mergeFile(const std::string & path, bool adopt_root_tree)
802{
803 std::set<std::string> include_stack;
804 return mergeFileImpl(path, adopt_root_tree, include_stack);
805}
806
807TreeDocument & TreeDocument::mergeFileImpl(
808 const std::string & path, bool adopt_root_tree, std::set<std::string> & include_stack)
809{
810 // Get canonical path for consistent circular include detection
811 std::string canonical_path;
812 try {
813 canonical_path = std::filesystem::canonical(path).string();
814 } catch (const std::filesystem::filesystem_error &) {
815 // If canonical fails (e.g., file doesn't exist), use the original path
816 // The error will be caught later when loading the file
817 canonical_path = path;
818 }
819
820 // Check for circular includes
821 if (include_stack.count(canonical_path) > 0) {
822 throw exceptions::TreeDocumentError(
823 "Cannot merge tree document: Circular include detected for file '" + path + "'.");
824 }
825
826 // Add to include stack
827 include_stack.insert(canonical_path);
828
829 XMLDocument other_doc;
830 if (other_doc.LoadFile(path.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
831 throw exceptions::TreeDocumentError("Cannot create tree document from file " + path + ": " + other_doc.ErrorStr());
832 }
833
834 mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
835
836 // Remove from include stack (for recursive calls that continue after this one)
837 include_stack.erase(canonical_path);
838 return *this;
839}
840
841TreeDocument & TreeDocument::mergeResource(const TreeResource & resource, bool adopt_root_tree)
842{
843 registerNodes(resource.getNodeManifest(), false);
844 return mergeFile(resource.build_request_file_path_, adopt_root_tree);
845}
846
847TreeDocument & TreeDocument::mergeTree(const TreeElement & tree, bool make_root_tree)
848{
849 XMLDocument tree_doc;
850 tree_doc.InsertEndChild(tree.ele_ptr_->DeepClone(&tree_doc));
852 mergeTreeDocument(tree_doc, make_root_tree);
853 return *this;
854}
855
857{
858 if (tree_name.empty()) {
859 throw exceptions::TreeDocumentError("Cannot create a new tree with an empty name");
860 }
861 if (hasTreeName(tree_name)) {
862 throw exceptions::TreeDocumentError(
863 "Cannot create a new tree with name '" + tree_name + "' because it already exists.");
864 }
865 TreeDocument::XMLElement * new_ele = RootElement()->InsertNewChildElement(TreeDocument::TREE_ELEMENT_NAME);
866 new_ele->SetAttribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
867 return TreeElement(this, new_ele);
868}
869
871{
872 return mergeTree(other_tree).getTree(other_tree.getName());
873}
874
876{
877 std::string name(tree_name);
878 if (name.empty()) {
879 if (other.hasRootTreeName()) {
880 name = other.getRootTreeName();
881 } else if (const std::vector<std::string> names = other.getAllTreeNames(); names.size() == 1) {
882 name = names[0];
883 } else {
884 throw exceptions::TreeDocumentError(
885 "Failed to create new tree element from another document because argument tree_name was omitted and it was not "
886 "possible to determine the root tree automatically.");
887 }
888 }
889 TreeElement tree_ele = newTree(name);
890 tree_ele.insertTreeFromDocument(other, name);
891 return tree_ele;
892}
893
894TreeDocument::TreeElement TreeDocument::newTreeFromString(const std::string & tree_str, const std::string & tree_name)
895{
896 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
897 new_doc.mergeString(tree_str, true);
898 return newTreeFromDocument(new_doc, tree_name);
899}
900
901TreeDocument::TreeElement TreeDocument::newTreeFromFile(const std::string & path, const std::string & tree_name)
902{
903 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
904 new_doc.mergeFile(path, true);
905 return newTreeFromDocument(new_doc, tree_name);
906}
907
909 const TreeResource & resource, const std::string & tree_name)
910{
911 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
912 new_doc.mergeFile(resource.build_request_file_path_);
913 if (resource.hasRootTreeName()) new_doc.setRootTreeName(resource.getRootTreeName());
914 registerNodes(resource.getNodeManifest(), false);
915 return newTreeFromDocument(new_doc, tree_name);
916}
917
918bool TreeDocument::hasTreeName(const std::string & tree_name) const
919{
920 if (auto_apms_util::contains(getAllTreeNames(), tree_name)) return true;
921 return false;
922}
923
925{
926 return TreeElement(this, getXMLElementForTreeWithName(tree_name));
927}
928
929TreeDocument & TreeDocument::setRootTreeName(const std::string & tree_name)
930{
931 if (tree_name.empty()) {
932 throw exceptions::TreeDocumentError("Cannot set root tree name with empty string.");
933 }
934 if (!hasTreeName(tree_name)) {
935 throw exceptions::TreeDocumentError(
936 "Cannot make tree with name '" + tree_name + "' the root tree because it doesn't exist.");
937 }
938 RootElement()->SetAttribute(ROOT_TREE_ATTRIBUTE_NAME, tree_name.c_str());
939 return *this;
940}
941
943{
944 if (RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return true;
945 return false;
946}
947
949{
950 if (const auto tree_name = RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return tree_name;
951 throw exceptions::TreeDocumentError(
952 "Cannot get root tree name because the document's root element has no attribute '" +
953 std::string(ROOT_TREE_ATTRIBUTE_NAME) + "'.");
954}
955
957
958TreeDocument & TreeDocument::removeTree(const std::string & tree_name)
959{
960 RootElement()->DeleteChild(getXMLElementForTreeWithName(tree_name));
961 return *this;
962}
963
965
966std::vector<std::string> TreeDocument::getAllTreeNames() const { return getAllTreeNamesImpl(*this); }
967
968TreeDocument & TreeDocument::registerNodes(const NodeManifest & tree_node_manifest, bool override)
969{
970 // Make sure that there are no node names that are reserved for native nodes
971 std::set<std::string> all_registration_names;
972 for (const auto & [name, _] : tree_node_manifest.map()) all_registration_names.insert(name);
973 if (const std::set<std::string> common =
974 auto_apms_util::getCommonElements(all_registration_names, native_node_names_);
975 !common.empty()) {
976 throw exceptions::TreeDocumentError(
977 "Found reserved node registration names in the node manifest. The following names are not allowed, because "
978 "they refer to native behavior tree nodes: [ " +
979 auto_apms_util::join(std::vector<std::string>(common.begin(), common.end()), ", ") + " ].");
980 }
981
982 for (const auto & [node_name, params] : tree_node_manifest.map()) {
983 // If the node is already registered
984 if (registered_nodes_manifest_.contains(node_name)) {
985 // Check whether the specified node class is different from the currently registered one by comparing the
986 // respective plugin class names.
987 if (registered_nodes_manifest_[node_name].class_name == params.class_name) {
988 // We assume that the manifest entry refers to the exact same node plugin, because all NodeRegistrationLoader
989 // instances verify that there are no ambiguous class names during initialization. Since the node is already
990 // registered, we may skip registering it as there's nothing new to do.
991 continue;
992 } else if (override) {
993 // If it's actually a different class and override is true, register the new node plugin instead of the
994 // current one.
995 factory_.unregisterBuilder(node_name);
996 } else {
997 // If it's actually a different class and override is false, we must throw.
998 throw exceptions::TreeDocumentError(
999 "Tried to register node '" + node_name + "' (Class : " + params.class_name +
1000 ") which is already known to the builder, but under a different class name (" +
1001 registered_nodes_manifest_[node_name].class_name +
1002 "). You must explicitly set override=true to allow for overriding previously registered nodes.");
1003 }
1004 }
1005
1006 // Check if the class we search for is actually available with the loader.
1007 if (!tree_node_loader_ptr_->isClassAvailable(params.class_name)) {
1008 if (all_node_classes_package_map_.find(params.class_name) == all_node_classes_package_map_.end()) {
1009 throw exceptions::TreeDocumentError(
1010 "Node '" + node_name + " (" + params.class_name +
1011 ")' cannot be registered, because the class name is not known to the class loader. "
1012 "Make sure that it's spelled correctly and registered by calling "
1013 "auto_apms_behavior_tree_register_nodes() in the CMakeLists.txt of the "
1014 "corresponding package.");
1015 }
1016 throw exceptions::TreeDocumentError(
1017 "Node '" + node_name + " (" + params.class_name +
1018 ")' cannot be registered, because the corresponding resource belongs to excluded package '" +
1019 all_node_classes_package_map_.at(params.class_name) + "'.");
1020 }
1021
1022 pluginlib::UniquePtr<NodeRegistrationInterface> plugin_instance;
1023 try {
1024 plugin_instance = tree_node_loader_ptr_->createUniqueInstance(params.class_name);
1025 } catch (const pluginlib::CreateClassException & e) {
1026 throw pluginlib::CreateClassException(
1027 "Failed to create an instance of node '" + node_name + " (" + params.class_name +
1028 ")'. Remember that the AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE "
1029 "macro must be called in the source file for the node class to be discoverable. "
1030 "Error message: " +
1031 e.what() + ".");
1032 }
1033
1034 try {
1035 if (plugin_instance->requiresRosNodeContext()) {
1036 if (only_non_ros_nodes_) {
1037 throw exceptions::NodeRegistrationError(
1038 "Node '" + node_name +
1039 "' relies on ROS 2 functionality but this instance only allows to use non-ROS nodes.");
1040 }
1041 RosNodeContext ros_node_context(
1042 ros_node_wptr_.lock(), tree_node_waitables_callback_group_wptr_.lock(),
1043 tree_node_waitables_executor_wptr_.lock(), params);
1044 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1045 } else {
1046 // Create a dummy object of RosNodeContext to allow for parsing the registration params nevertheless
1047 RosNodeContext ros_node_context(nullptr, nullptr, nullptr, params);
1048 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1049 }
1050 } catch (const std::exception & e) {
1051 throw exceptions::NodeRegistrationError(
1052 "Cannot register node '" + node_name + " (" + params.class_name + ")': " + e.what() + ".");
1053 }
1054 registered_nodes_manifest_.add(node_name, params);
1055 }
1056 return *this;
1057}
1058
1059std::set<std::string> TreeDocument::getRegisteredNodeNames(bool include_native) const
1060{
1061 std::set<std::string> names;
1062 if (include_native) names = native_node_names_;
1063 for (const auto & [name, _] : registered_nodes_manifest_.map()) names.insert(name);
1064 return names;
1065}
1066
1068{
1069 NodeManifest m;
1070 TreeDocument * doc = const_cast<TreeDocument *>(this);
1071 for (const std::string & tree_name : getAllTreeNames()) {
1072 XMLElement * ptr = const_cast<XMLElement *>(getXMLElementForTreeWithName(tree_name));
1073 const TreeElement ele(doc, ptr);
1074 m.merge(ele.getRequiredNodeManifest(), true);
1075 }
1076 return m;
1077}
1078
1080{
1081 // Create or get the TreeNodesModel element
1082 tinyxml2::XMLElement * model_root = RootElement()->FirstChildElement(TREE_NODE_MODEL_ELEMENT_NAME);
1083
1084 // If no TreeNodesModel element exists, create one
1085 if (!model_root) {
1086 model_root = NewElement(TREE_NODE_MODEL_ELEMENT_NAME);
1087 RootElement()->InsertEndChild(model_root);
1088 }
1089
1090 // Iterate through the model_map and create XML elements for each node
1091 for (const auto & [node_name, model] : model_map) {
1092 // Create the element with the node type as the tag name
1093 tinyxml2::XMLElement * node_element = NewElement(BT::toStr(model.type).c_str());
1094 node_element->SetAttribute("ID", node_name.c_str());
1095
1096 // Add port information
1097 for (const auto & port_info : model.port_infos) {
1098 tinyxml2::XMLElement * port_element = nullptr;
1099
1100 // Create the appropriate port element based on direction
1101 switch (port_info.port_direction) {
1102 case BT::PortDirection::INPUT:
1103 port_element = NewElement("input_port");
1104 break;
1105 case BT::PortDirection::OUTPUT:
1106 port_element = NewElement("output_port");
1107 break;
1108 case BT::PortDirection::INOUT:
1109 port_element = NewElement("inout_port");
1110 break;
1111 }
1112
1113 // Set port attributes
1114 port_element->SetAttribute("name", port_info.port_name.c_str());
1115
1116 if (!port_info.port_type.empty()) {
1117 port_element->SetAttribute("type", port_info.port_type.c_str());
1118 }
1119
1120 if (port_info.port_has_default) {
1121 port_element->SetAttribute("default", port_info.port_default.c_str());
1122 }
1123
1124 // Set port description as text content
1125 if (!port_info.port_description.empty()) {
1126 port_element->SetText(port_info.port_description.c_str());
1127 }
1128
1129 node_element->InsertEndChild(port_element);
1130 }
1131
1132 model_root->InsertEndChild(node_element);
1133 }
1134
1135 return *this;
1136}
1137
1138NodeModelMap TreeDocument::getNodeModel(tinyxml2::XMLDocument & doc, const NodeManifest & manifest)
1139{
1140 const tinyxml2::XMLElement * root = doc.RootElement();
1141 if (!root) {
1142 throw exceptions::TreeDocumentError("Node model document has no root element.");
1143 }
1144 if (const char * ver = root->Attribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME)) {
1145 const std::string expected_format = TreeDocument::BTCPP_FORMAT_DEFAULT_VERSION;
1146 if (std::string(ver) != expected_format) {
1147 throw exceptions::TreeDocumentError(
1148 "Cannot parse node model document: Format of model document (" +
1149 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " + ver +
1150 ") doesn't comply with the expected format (" + std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
1151 expected_format + ").");
1152 }
1153 } else {
1154 throw exceptions::TreeDocumentError(
1155 "Cannot parse node model document: Root element of model document doesn't have required attribute '" +
1156 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
1157 }
1158 tinyxml2::XMLElement * model_ele = doc.RootElement()->FirstChildElement(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME);
1159 if (!model_ele) {
1160 throw exceptions::TreeDocumentError(
1161 "Element <" + std::string(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME) +
1162 "> doesn't exist in node model document.");
1163 }
1164
1165 NodeModelMap model_map;
1166 for (tinyxml2::XMLElement * ele = model_ele->FirstChildElement(); ele != nullptr; ele = ele->NextSiblingElement()) {
1167 const char * node_name = ele->Attribute("ID");
1168 if (!node_name) {
1169 throw exceptions::TreeDocumentError(
1170 "Element '" + std::string(ele->Name()) + "' in node model document is missing the required attribute 'ID'");
1171 }
1172 NodeModel & model = model_map[node_name];
1173 model.type = BT::convertFromString<BT::NodeType>(ele->Name());
1174 for (const tinyxml2::XMLElement * port_ele = ele->FirstChildElement(); port_ele != nullptr;
1175 port_ele = port_ele->NextSiblingElement()) {
1176 const std::string direction = port_ele->Name();
1177 NodePortInfo port_info;
1178 if (direction == "input_port") {
1179 port_info.port_direction = BT::PortDirection::INPUT;
1180 } else if (direction == "output_port") {
1181 port_info.port_direction = BT::PortDirection::OUTPUT;
1182 } else if (direction == "inout_port") {
1183 port_info.port_direction = BT::PortDirection::INOUT;
1184 } else {
1185 throw exceptions::TreeDocumentError(
1186 "Unkown port direction in node model for '" + std::string(node_name) + "': " + direction);
1187 }
1188 if (const char * c = port_ele->Attribute("name")) {
1189 port_info.port_name = c;
1190 }
1191 if (const char * c = port_ele->Attribute("type")) {
1192 port_info.port_type = c;
1193 }
1194 if (const char * c = port_ele->Attribute("default")) {
1195 port_info.port_has_default = true;
1196 port_info.port_default = c;
1197 } else {
1198 port_info.port_has_default = false;
1199 }
1200 if (const char * c = port_ele->GetText()) {
1201 port_info.port_description = c;
1202 }
1203 model.port_infos.push_back(std::move(port_info));
1204 }
1205 // Port infos may be empty if there are no ports
1206 }
1207
1208 // Collect hidden ports from the node manifest
1209 std::map<std::string, std::vector<std::string>> hidden_ports;
1210 for (const auto & [node_name, registration_options] : manifest.map()) {
1211 // Extract hidden_ports from registration options
1212 for (const std::string & port_name : registration_options.hidden_ports) {
1213 hidden_ports[node_name].push_back(port_name);
1214 }
1215
1216 // Implicitly hide ports that are specified in port_alias
1217 for (const auto & [port_name, _] : registration_options.port_alias) {
1218 hidden_ports[node_name].push_back(port_name);
1219 }
1220 }
1221
1222 // Apply port hiding
1223 for (const auto & [node_name, ports_to_hide] : hidden_ports) {
1224 auto it = model_map.find(node_name);
1225 if (it == model_map.end()) {
1226 continue;
1227 }
1228 NodeModel & model = it->second;
1229 model.port_infos.erase(
1230 std::remove_if(
1231 model.port_infos.begin(), model.port_infos.end(),
1232 [&ports_to_hide](const NodePortInfo & port_info) {
1233 return std::find(ports_to_hide.begin(), ports_to_hide.end(), port_info.port_name) != ports_to_hide.end();
1234 }),
1235 model.port_infos.end());
1236 }
1237
1238 return model_map;
1239}
1240
1242{
1243 // Generate XML from the factory
1244 std::string model_xml;
1245 try {
1246 model_xml = BT::writeTreeNodesModelXML(factory_, include_native);
1247 } catch (const std::exception & e) {
1248 throw exceptions::TreeDocumentError("Error generating node model XML from factory: " + std::string(e.what()));
1249 }
1250
1251 // Parse the XML
1252 tinyxml2::XMLDocument model_doc;
1253 if (model_doc.Parse(model_xml.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1254 throw exceptions::TreeDocumentError(
1255 "Error parsing the model of the currently registered nodes: " + std::string(model_doc.ErrorStr()));
1256 }
1257
1258 // Get the node model with hidden ports applied
1259 return getNodeModel(model_doc, registered_nodes_manifest_);
1260}
1261
1262BT::Result TreeDocument::verify() const
1263{
1264 NodeModelMap model_map = getNodeModel(true);
1265 std::unordered_map<std::string, BT::NodeType> registered_nodes;
1266 for (const auto & [node_name, model] : model_map) {
1267 registered_nodes[node_name] = model.type;
1268 }
1269 try {
1270 BT::VerifyXML(writeToString(), registered_nodes);
1271 } catch (const BT::RuntimeError & e) {
1272 return nonstd::make_unexpected(e.what());
1273 }
1274 return {};
1275}
1276
1278{
1279 tinyxml2::XMLPrinter printer;
1280 Print(&printer);
1281 return printer.CStr();
1282}
1283
1284void TreeDocument::writeToFile(const std::string & path) const
1285{
1286 XMLDocument doc;
1287 DeepCopy(&doc);
1288 tinyxml2::XMLError result = doc.SaveFile(path.c_str());
1289 if (result != tinyxml2::XML_SUCCESS) {
1290 throw exceptions::TreeDocumentError(
1291 "Failed to write tree document to file. Error ID: " + std::string(doc.ErrorIDToName(result)));
1292 }
1293}
1294
1296{
1297 Clear();
1298 tinyxml2::XMLElement * root_ele = NewElement(TreeDocument::ROOT_ELEMENT_NAME);
1299 root_ele->SetAttribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME, format_version_.c_str());
1300 InsertFirstChild(root_ele);
1301 return *this;
1302}
1303
1304const TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name) const
1305{
1306 return getXMLElementForTreeWithNameImpl<const XMLElement *>(*this, tree_name);
1307}
1308
1309TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name)
1310{
1311 return getXMLElementForTreeWithNameImpl<XMLElement *>(*this, tree_name);
1312}
1313
1314template <typename ReturnT, typename DocumentT>
1315ReturnT TreeDocument::getXMLElementForTreeWithNameImpl(DocumentT & doc, const std::string & tree_name)
1316{
1317 if (tree_name.empty()) {
1318 throw exceptions::TreeDocumentError("Cannot get tree with an empty name.");
1319 }
1320 if (!doc.hasTreeName(tree_name)) {
1321 throw exceptions::TreeDocumentError("Cannot get tree with name '" + tree_name + "' because it doesn't exist.");
1322 }
1323 auto child = doc.RootElement()->FirstChildElement(TreeDocument::TREE_ELEMENT_NAME);
1324 while (child && !child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str())) {
1325 child = child->NextSiblingElement();
1326 }
1327 if (!child) {
1328 throw std::logic_error(
1329 "Unexpected error trying to get tree element with name '" + tree_name +
1330 "'. Since hasTreeName() returned true, there MUST be a corresponding element.");
1331 }
1332 return child;
1333}
1334
1335} // namespace auto_apms_behavior_tree::core
const NodeManifest & getNodeManifest() const
Get the node manifest associated with this resource.
Definition behavior.hpp:430
Data structure for information about which behavior tree node plugin to load and how to configure the...
NodeManifest & add(const std::string &node_name, const RegistrationOptions &opt)
Add registration options for a behavior tree node to the manifest.
bool contains(const std::string &node_name) const
Determine if a behavior tree node has been added to the manifest.
NodeManifest & merge(const NodeManifest &other, bool replace=false)
Merges another NodeManifest with this one.
const Map & map() const
Get a view of the internal map.
A pluginlib::ClassLoader specifically for loading installed behavior tree node plugins.
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
Class that encapsulates behavior tree script expressions.
Definition script.hpp:30
std::string str() const
Concatenate all expressions of this instance to a single string.
Definition script.cpp:41
Handle for a single node of a TreeDocument.
const TreeDocument & getParentDocument() const
Get a const view of this node's parent tree document.
NodeElement insertTreeFromResource(const TreeResource &resource, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from one of the installed package's behavior tree resources and add its first chil...
const std::vector< std::string > & getPortNames() const
Get the names of the data ports implemented by the node represented by this element.
virtual std::string getRegistrationName() const
Get the name of this node given during registration representing its dynamic type.
std::function< bool(const NodeElement &)> ConstDeepApplyCallback
Callback invoked for every node found under another node. It cannot modify the current node.
NodeElement & resetPorts()
Delete all currently specified port values and reset with the defaults.
std::function< bool(NodeElement &)> DeepApplyCallback
Callback invoked for every node found under another node. It may modify the current node.
NodeElement & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="")
Recursively visit this node's children in execution order and remove the first node with a particular...
NodeElement & setPorts(const PortValues &port_values)
Populate the the node's data ports.
virtual NodeElement & setName(const std::string &instance_name)
Assign a name for this specific node instance.
NodeElement getFirstNode(const std::string &registration_name="", const std::string &instance_name="") const
Recursively visit this node's children in execution order and get the first node with a particular re...
NodeElement(TreeDocument *doc_ptr, XMLElement *ele_ptr)
Protected constructor intended for internal use only.
std::map< std::string, std::string > PortValues
Mapping of port names and its respective value encoded as string.
const std::vector< NodeElement > deepApplyConst(ConstDeepApplyCallback apply_callback) const
Recursively apply a callback to this node's children.
TreeDocument * doc_ptr_
Pointer to the tree document that created the tree this node belongs to.
virtual std::string getName() const
Get the name of this node given to this specific instance by the developer.
PortValues getPorts() const
Assemble the values given to each data port implemented by this node.
NodeElement & setConditionalScript(BT::PreCond type, const Script &script)
Specify a script that is evaluated before this node is ticked.
NodeElement insertTreeFromString(const std::string &tree_str, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document created from a string and add its first child node to the children...
tinyxml2::XMLElement * ele_ptr_
Pointer to the corresponding XMLElement of the base document.
NodeElement insertTree(const TreeElement &tree, const NodeElement *before_this=nullptr)
Concatenate a tree and add its first child node to the children of this node.
std::string getFullyQualifiedName() const
Create a string that uniquely identifies this node considering its registration and its instance name...
NodeElement insertTreeFromDocument(const TreeDocument &doc, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document and add its first child node to the children of this node.
model::SubTree insertSubTreeNode(const std::string &tree_name, const NodeElement *before_this=nullptr)
Add a subtree node for a specific tree element to the children of this node.
bool hasChildren() const
Determine whether any children have been given to this node.
bool operator==(const NodeElement &other) const
Determine if two node elements refer to the same node.
NodeElement insertNode(const std::string &name, const NodeElement *before_this=nullptr)
Add a new node to the children of this node.
NodeElement insertTreeFromFile(const std::string &path, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document created from a file and add its first child node to the children o...
NodeElement & removeChildren()
Recursively remove all children of this node element.
NodeElement & operator=(const NodeElement &other)
Replace this node with another.
std::vector< NodeElement > deepApply(DeepApplyCallback apply_callback)
Recursively apply a callback to this node's children.
Handle for a single behavior tree of a TreeDocument.
TreeElement(TreeDocument *doc_ptr, XMLElement *ele_ptr)
Protected constructor intended to be used only by certain factory methods of TreeDocument.
std::string getName() const override
Get the name of the behavior tree.
BT::Result verify() const
Verify that this behavior tree is structured correctly and can be created successfully.
NodeManifest getRequiredNodeManifest() const
Assemble the node manifest that is required for successfully creating an instance of this tree.
TreeElement & makeRoot()
Set this behavior tree as the root tree of the parent document.
TreeElement & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="")
TreeElement & operator=(const TreeElement &other)
Replace the behavior tree represented by this element with another.
TreeElement & setName(const std::string &tree_name) override
Set the name of the behavior tree.
std::string writeToString() const
Write this behavior tree to an XML encoded in a string.
Document Object Model (DOM) for the behavior tree XML schema. This class offers a programmatic approa...
TreeElement getRootTree()
Get the corresponding behavior tree element for the root tree of this document.
TreeDocument & mergeResource(const TreeResource &resource, bool adopt_root_tree=false)
Merge the behavior trees from one of the installed package's behavior tree resources.
std::string getRootTreeName() const
Get the name of this document's root tree.
TreeDocument & mergeTreeDocument(const XMLDocument &other, bool adopt_root_tree=false)
Merge another tree document with this one.
TreeDocument & mergeString(const std::string &tree_str, bool adopt_root_tree=false)
Create a tree document from a string and merge it with this one.
TreeElement newTreeFromDocument(const TreeDocument &other, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside another tree doc...
void writeToFile(const std::string &path) const
Write the XML of this tree document to a file.
TreeElement newTreeFromFile(const std::string &path, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside the XML file.
BT::Result verify() const
Verify that all behavior trees of this document are structured correctly and can be created successfu...
NodeManifest getRequiredNodeManifest() const
Assemble the node manifest that is required for successfully creating an instance of any of the docum...
std::set< std::string > getRegisteredNodeNames(bool include_native=true) const
Get the names of all nodes that are known to this document.
TreeElement newTreeFromString(const std::string &tree_str, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside the XML string.
TreeElement newTreeFromResource(const TreeResource &resource, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one the trees found inside a part...
TreeDocument & reset()
Clear this document and reset it to its initial state.
TreeElement getTree(const std::string &tree_name)
Get the corresponding behavior tree element for a tree inside this document.
virtual TreeDocument & registerNodes(const NodeManifest &tree_node_manifest, bool override=false)
Load behavior tree node plugins and register them with the internal behavior tree factory.
TreeDocument & setRootTreeName(const std::string &tree_name)
Define the root tree of this document.
bool hasTreeName(const std::string &tree_name) const
Determine if this document contains a behavior tree with a particular name.
TreeDocument & mergeFile(const std::string &path, bool adopt_root_tree=false)
Create a tree document from a file and merge it with this one.
std::vector< std::string > getAllTreeNames() const
Get the names of all behavior trees inside this document.
TreeDocument & addNodeModel(NodeModelMap model_map)
Add a behavior tree node model element to the document by parsing the contents of model_map.
TreeDocument & mergeTree(const TreeElement &tree, bool make_root_tree=false)
Merge an existing behavior tree with this tree document.
std::string writeToString() const
Write the XML of this tree document to a string.
bool hasRootTreeName() const
Determine if this document specifies which of its trees is the root tree.
TreeDocument & removeTree(const std::string &tree_name)
Remove a particular behavior tree from this document.
TreeDocument(const std::string &format_version=BTCPP_FORMAT_DEFAULT_VERSION, NodeRegistrationLoader::SharedPtr tree_node_loader=NodeRegistrationLoader::make_shared())
Create a an empty tree document.
static NodeModelMap getNodeModel(tinyxml2::XMLDocument &doc, const NodeManifest &manifest)
Convert a behavior tree node model document to the corresponding data structure.
TreeElement newTree(const std::string &tree_name)
Create a new behavior tree inside this document.
Class containing behavior tree resource data.
std::string getRootTreeName() const
Get the name of the root tree of this behavior tree resource.
bool hasRootTreeName() const
Determine if this behavior tree resource specifies a root tree.
Subtree behavior tree node model.
bool contains(const ContainerT< ValueT, AllocatorT > &c, const ValueT &val)
Check whether a particular container structure contains a value.
Definition container.hpp:36
std::set< KeyT, CompareT, AllocatorT > getCommonElements(std::set< KeyT, CompareT, AllocatorT > c1, std::set< KeyT, CompareT, AllocatorT > c2)
Assemble common elements of two sets.
Definition container.hpp:53
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
Models for all available behavior tree nodes.
Powerful tooling for incorporating behavior trees for task development.
Definition behavior.hpp:32
std::map< std::string, NodeModel > NodeModelMap
Mapping of node registration names and their implementation details.
Data structure encapsulating the information of all ports implemented by a behavior tree node.
Implementation details of a single data port.
std::string port_default
Default value of the port encoded as string.
bool port_has_default
Flag whether the port implements a default value or not.
std::string port_description
Description of the port.
BT::PortDirection port_direction
Direction of the port.
std::string port_type
String representation of the C++ type given to the port.
std::string port_name
Name of the port.
Parameters for loading and registering a behavior tree node class from a shared library using e....