ros_graph.py
Go to the documentation of this file.
00001 # Copyright (c) 2011, Dirk Thomas, TU Darmstadt
00002 # All rights reserved.
00003 #
00004 # Redistribution and use in source and binary forms, with or without
00005 # modification, are permitted provided that the following conditions
00006 # are met:
00007 #
00008 #   * Redistributions of source code must retain the above copyright
00009 #     notice, this list of conditions and the following disclaimer.
00010 #   * Redistributions in binary form must reproduce the above
00011 #     copyright notice, this list of conditions and the following
00012 #     disclaimer in the documentation and/or other materials provided
00013 #     with the distribution.
00014 #   * Neither the name of the TU Darmstadt nor the names of its
00015 #     contributors may be used to endorse or promote products derived
00016 #     from this software without specific prior written permission.
00017 #
00018 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00019 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00020 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00021 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00022 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00023 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00024 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00025 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00026 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00027 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00028 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00029 # POSSIBILITY OF SUCH DAMAGE.
00030 
00031 from __future__ import division
00032 import os
00033 import rospkg
00034 
00035 from python_qt_binding import loadUi
00036 from python_qt_binding.QtCore import QAbstractListModel, QFile, QIODevice, Qt, Signal
00037 from python_qt_binding.QtGui import QIcon, QImage, QPainter
00038 from python_qt_binding.QtWidgets import QCompleter, QFileDialog, QGraphicsScene, QWidget
00039 from python_qt_binding.QtSvg import QSvgGenerator
00040 
00041 import rosgraph.impl.graph
00042 import rosservice
00043 import rostopic
00044 
00045 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00046 # pydot requires some hacks
00047 from qt_dotgraph.pydotfactory import PydotFactory
00048 from rqt_gui_py.plugin import Plugin
00049 # TODO: use pygraphviz instead, but non-deterministic layout will first be resolved in graphviz 2.30
00050 # from qtgui_plugin.pygraphvizfactory import PygraphvizFactory
00051 
00052 from .dotcode import RosGraphDotcodeGenerator, NODE_NODE_GRAPH, NODE_TOPIC_ALL_GRAPH, NODE_TOPIC_GRAPH
00053 from .interactive_graphics_view import InteractiveGraphicsView
00054 
00055 try:
00056     unicode
00057     # we're on python2, or the "unicode" function has already been defined elsewhere
00058 except NameError:
00059     unicode = str
00060     # we're on python3
00061 
00062 
00063 class RepeatedWordCompleter(QCompleter):
00064 
00065     """A completer that completes multiple times from a list"""
00066 
00067     def init(self, parent=None):
00068         QCompleter.init(self, parent)
00069 
00070     def pathFromIndex(self, index):
00071         path = QCompleter.pathFromIndex(self, index)
00072         lst = unicode(self.widget().text()).split(',')
00073         if len(lst) > 1:
00074             path = '%s, %s' % (','.join(lst[:-1]), path)
00075         return path
00076 
00077     def splitPath(self, path):
00078         path = unicode(path.split(',')[-1]).lstrip(' ')
00079         return [path]
00080 
00081 
00082 class NamespaceCompletionModel(QAbstractListModel):
00083 
00084     """Ros package and stacknames"""
00085 
00086     def __init__(self, linewidget, topics_only):
00087         super(NamespaceCompletionModel, self).__init__(linewidget)
00088         self.names = []
00089 
00090     def refresh(self, names):
00091         namesset = set()
00092         for n in names:
00093             namesset.add(unicode(n).strip())
00094             namesset.add("-%s" % (unicode(n).strip()))
00095         self.names = sorted(namesset)
00096 
00097     def rowCount(self, parent):
00098         return len(self.names)
00099 
00100     def data(self, index, role):
00101         if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00102             return self.names[index.row()]
00103         return None
00104 
00105 
00106 class RosGraph(Plugin):
00107 
00108     _deferred_fit_in_view = Signal()
00109 
00110     def __init__(self, context):
00111         super(RosGraph, self).__init__(context)
00112         self.initialized = False
00113         self.setObjectName('RosGraph')
00114 
00115         self._graph = None
00116         self._current_dotcode = None
00117 
00118         self._widget = QWidget()
00119 
00120         # factory builds generic dotcode items
00121         self.dotcode_factory = PydotFactory()
00122         # self.dotcode_factory = PygraphvizFactory()
00123         # generator builds rosgraph
00124         self.dotcode_generator = RosGraphDotcodeGenerator()
00125         # dot_to_qt transforms into Qt elements using dot layout
00126         self.dot_to_qt = DotToQtGenerator()
00127 
00128         rp = rospkg.RosPack()
00129         ui_file = os.path.join(rp.get_path('rqt_graph'), 'resource', 'RosGraph.ui')
00130         loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00131         self._widget.setObjectName('RosGraphUi')
00132         if context.serial_number() > 1:
00133             self._widget.setWindowTitle(
00134                 self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00135 
00136         self._scene = QGraphicsScene()
00137         self._scene.setBackgroundBrush(Qt.white)
00138         self._widget.graphics_view.setScene(self._scene)
00139 
00140         self._widget.graph_type_combo_box.insertItem(0, self.tr('Nodes only'), NODE_NODE_GRAPH)
00141         self._widget.graph_type_combo_box.insertItem(
00142             1, self.tr('Nodes/Topics (active)'), NODE_TOPIC_GRAPH)
00143         self._widget.graph_type_combo_box.insertItem(
00144             2, self.tr('Nodes/Topics (all)'), NODE_TOPIC_ALL_GRAPH)
00145         self._widget.graph_type_combo_box.setCurrentIndex(0)
00146         self._widget.graph_type_combo_box.currentIndexChanged.connect(self._refresh_rosgraph)
00147 
00148         self.node_completionmodel = NamespaceCompletionModel(self._widget.filter_line_edit, False)
00149         completer = RepeatedWordCompleter(self.node_completionmodel, self)
00150         completer.setCompletionMode(QCompleter.PopupCompletion)
00151         completer.setWrapAround(True)
00152         completer.setCaseSensitivity(Qt.CaseInsensitive)
00153         self._widget.filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00154         self._widget.filter_line_edit.setCompleter(completer)
00155 
00156         self.topic_completionmodel = NamespaceCompletionModel(
00157             self._widget.topic_filter_line_edit, False)
00158         topic_completer = RepeatedWordCompleter(self.topic_completionmodel, self)
00159         topic_completer.setCompletionMode(QCompleter.PopupCompletion)
00160         topic_completer.setWrapAround(True)
00161         topic_completer.setCaseSensitivity(Qt.CaseInsensitive)
00162         self._widget.topic_filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00163         self._widget.topic_filter_line_edit.setCompleter(topic_completer)
00164 
00165         self._widget.namespace_cluster_spin_box.valueChanged.connect(self._refresh_rosgraph)
00166         self._widget.actionlib_check_box.clicked.connect(self._refresh_rosgraph)
00167         self._widget.dead_sinks_check_box.clicked.connect(self._refresh_rosgraph)
00168         self._widget.leaf_topics_check_box.clicked.connect(self._refresh_rosgraph)
00169         self._widget.quiet_check_box.clicked.connect(self._refresh_rosgraph)
00170         self._widget.unreachable_check_box.clicked.connect(self._refresh_rosgraph)
00171         self._widget.group_tf_check_box.clicked.connect(self._refresh_rosgraph)
00172         self._widget.hide_tf_nodes_check_box.clicked.connect(self._refresh_rosgraph)
00173         self._widget.group_image_check_box.clicked.connect(self._refresh_rosgraph)
00174 
00175         self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00176         self._widget.refresh_graph_push_button.pressed.connect(self._update_rosgraph)
00177 
00178         self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
00179         self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
00180         self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00181         self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00182 
00183         self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00184         self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00185         self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00186         self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00187         self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00188         self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00189         self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00190         self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00191 
00192         self._update_rosgraph()
00193         self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00194         self._deferred_fit_in_view.emit()
00195 
00196         context.add_widget(self._widget)
00197 
00198     def save_settings(self, plugin_settings, instance_settings):
00199         instance_settings.set_value(
00200             'graph_type_combo_box_index', self._widget.graph_type_combo_box.currentIndex())
00201         instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00202         instance_settings.set_value(
00203             'topic_filter_line_edit_text', self._widget.topic_filter_line_edit.text())
00204         instance_settings.set_value(
00205             'namespace_cluster_spin_box_value', self._widget.namespace_cluster_spin_box.value())
00206         instance_settings.set_value(
00207             'actionlib_check_box_state', self._widget.actionlib_check_box.isChecked())
00208         instance_settings.set_value(
00209             'dead_sinks_check_box_state', self._widget.dead_sinks_check_box.isChecked())
00210         instance_settings.set_value(
00211             'leaf_topics_check_box_state', self._widget.leaf_topics_check_box.isChecked())
00212         instance_settings.set_value(
00213             'quiet_check_box_state', self._widget.quiet_check_box.isChecked())
00214         instance_settings.set_value(
00215             'unreachable_check_box_state', self._widget.unreachable_check_box.isChecked())
00216         instance_settings.set_value(
00217             'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00218         instance_settings.set_value(
00219             'highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
00220         instance_settings.set_value(
00221             'group_tf_check_box_state', self._widget.group_tf_check_box.isChecked())
00222         instance_settings.set_value(
00223             'hide_tf_nodes_check_box_state', self._widget.hide_tf_nodes_check_box.isChecked())
00224         instance_settings.set_value(
00225             'group_image_check_box_state', self._widget.group_image_check_box.isChecked())
00226         instance_settings.set_value(
00227             'hide_dynamic_reconfigure_check_box_state', self._widget.hide_dynamic_reconfigure_check_box.isChecked())
00228 
00229     def restore_settings(self, plugin_settings, instance_settings):
00230         self._widget.graph_type_combo_box.setCurrentIndex(
00231             int(instance_settings.value('graph_type_combo_box_index', 0)))
00232         self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', '/'))
00233         self._widget.topic_filter_line_edit.setText(
00234             instance_settings.value('topic_filter_line_edit_text', '/'))
00235         self._widget.namespace_cluster_spin_box.setValue(
00236             int(instance_settings.value('namespace_cluster_spin_box_value', 2)))
00237         self._widget.actionlib_check_box.setChecked(
00238             instance_settings.value('actionlib_check_box_state', True) in [True, 'true'])
00239         self._widget.dead_sinks_check_box.setChecked(
00240             instance_settings.value('dead_sinks_check_box_state', True) in [True, 'true'])
00241         self._widget.leaf_topics_check_box.setChecked(
00242             instance_settings.value('leaf_topics_check_box_state', True) in [True, 'true'])
00243         self._widget.quiet_check_box.setChecked(
00244             instance_settings.value('quiet_check_box_state', True) in [True, 'true'])
00245         self._widget.unreachable_check_box.setChecked(
00246             instance_settings.value('unreachable_check_box_state', True) in [True, 'true'])
00247         self._widget.auto_fit_graph_check_box.setChecked(
00248             instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
00249         self._widget.highlight_connections_check_box.setChecked(
00250             instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
00251         self._widget.hide_tf_nodes_check_box.setChecked(
00252             instance_settings.value('hide_tf_nodes_check_box_state', False) in [True, 'true'])
00253         self._widget.group_tf_check_box.setChecked(
00254             instance_settings.value('group_tf_check_box_state', True) in [True, 'true'])
00255         self._widget.group_image_check_box.setChecked(
00256             instance_settings.value('group_image_check_box_state', True) in [True, 'true'])
00257         self._widget.hide_dynamic_reconfigure_check_box.setChecked(
00258             instance_settings.value('hide_dynamic_reconfigure_check_box_state', True) in [True, 'true'])
00259         self.initialized = True
00260         self._refresh_rosgraph()
00261 
00262     def _update_rosgraph(self):
00263         # re-enable controls customizing fetched ROS graph
00264         self._widget.graph_type_combo_box.setEnabled(True)
00265         self._widget.filter_line_edit.setEnabled(True)
00266         self._widget.topic_filter_line_edit.setEnabled(True)
00267         self._widget.namespace_cluster_spin_box.setEnabled(True)
00268         self._widget.actionlib_check_box.setEnabled(True)
00269         self._widget.dead_sinks_check_box.setEnabled(True)
00270         self._widget.leaf_topics_check_box.setEnabled(True)
00271         self._widget.quiet_check_box.setEnabled(True)
00272         self._widget.unreachable_check_box.setEnabled(True)
00273         self._widget.group_tf_check_box.setEnabled(True)
00274         self._widget.hide_tf_nodes_check_box.setEnabled(True)
00275         self._widget.group_image_check_box.setEnabled(True)
00276         self._widget.hide_dynamic_reconfigure_check_box.setEnabled(True)
00277 
00278         self._graph = rosgraph.impl.graph.Graph()
00279         self._graph.set_master_stale(5.0)
00280         self._graph.set_node_stale(5.0)
00281         self._graph.update()
00282         self.node_completionmodel.refresh(self._graph.nn_nodes)
00283         self.topic_completionmodel.refresh(self._graph.nt_nodes)
00284         self._refresh_rosgraph()
00285 
00286     def _refresh_rosgraph(self):
00287         if not self.initialized:
00288             return
00289         self._update_graph_view(self._generate_dotcode())
00290 
00291     def _generate_dotcode(self):
00292         ns_filter = self._widget.filter_line_edit.text()
00293         topic_filter = self._widget.topic_filter_line_edit.text()
00294         graph_mode = self._widget.graph_type_combo_box.itemData(
00295             self._widget.graph_type_combo_box.currentIndex())
00296         orientation = 'LR'
00297         namespace_cluster = self._widget.namespace_cluster_spin_box.value()
00298         accumulate_actions = self._widget.actionlib_check_box.isChecked()
00299         hide_dead_end_topics = self._widget.dead_sinks_check_box.isChecked()
00300         hide_single_connection_topics = self._widget.leaf_topics_check_box.isChecked()
00301         quiet = self._widget.quiet_check_box.isChecked()
00302         unreachable = self._widget.unreachable_check_box.isChecked()
00303         group_tf_nodes = self._widget.group_tf_check_box.isChecked()
00304         hide_tf_nodes = self._widget.hide_tf_nodes_check_box.isChecked()
00305         group_image_nodes = self._widget.group_image_check_box.isChecked()
00306         hide_dynamic_reconfigure = self._widget.hide_dynamic_reconfigure_check_box.isChecked()
00307 
00308         return self.dotcode_generator.generate_dotcode(
00309             rosgraphinst=self._graph,
00310             ns_filter=ns_filter,
00311             topic_filter=topic_filter,
00312             graph_mode=graph_mode,
00313             hide_single_connection_topics=hide_single_connection_topics,
00314             hide_dead_end_topics=hide_dead_end_topics,
00315             cluster_namespaces_level=namespace_cluster,
00316             accumulate_actions=accumulate_actions,
00317             dotcode_factory=self.dotcode_factory,
00318             orientation=orientation,
00319             quiet=quiet,
00320             unreachable=unreachable,
00321             group_tf_nodes=group_tf_nodes,
00322             hide_tf_nodes=hide_tf_nodes,
00323             group_image_nodes=group_image_nodes,
00324             hide_dynamic_reconfigure=hide_dynamic_reconfigure)
00325 
00326     def _update_graph_view(self, dotcode):
00327         if dotcode == self._current_dotcode:
00328             return
00329         self._current_dotcode = dotcode
00330         self._redraw_graph_view()
00331 
00332     def _generate_tool_tip(self, url):
00333         if url is not None and ':' in url:
00334             item_type, item_path = url.split(':', 1)
00335             if item_type == 'node':
00336                 tool_tip = 'Node:\n  %s' % (item_path)
00337                 service_names = rosservice.get_service_list(node=item_path)
00338                 if service_names:
00339                     tool_tip += '\nServices:'
00340                     for service_name in service_names:
00341                         try:
00342                             service_type = rosservice.get_service_type(service_name)
00343                             tool_tip += '\n  %s [%s]' % (service_name, service_type)
00344                         except rosservice.ROSServiceIOException as e:
00345                             tool_tip += '\n  %s' % (e)
00346                 return tool_tip
00347             elif item_type == 'topic':
00348                 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00349                 return 'Topic:\n  %s\nType:\n  %s' % (topic_name, topic_type)
00350         return url
00351 
00352     def _redraw_graph_view(self):
00353         self._scene.clear()
00354 
00355         if self._widget.highlight_connections_check_box.isChecked():
00356             highlight_level = 3
00357         else:
00358             highlight_level = 1
00359 
00360         # layout graph and create qt items
00361         (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode,
00362                                                             highlight_level=highlight_level,
00363                                                             same_label_siblings=True,
00364                                                             scene=self._scene)
00365 
00366         self._scene.setSceneRect(self._scene.itemsBoundingRect())
00367         if self._widget.auto_fit_graph_check_box.isChecked():
00368             self._fit_in_view()
00369 
00370     def _load_dot(self, file_name=None):
00371         if file_name is None:
00372             file_name, _ = QFileDialog.getOpenFileName(
00373                 self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00374             if file_name is None or file_name == '':
00375                 return
00376 
00377         try:
00378             fh = open(file_name, 'rb')
00379             dotcode = fh.read()
00380             fh.close()
00381         except IOError:
00382             return
00383 
00384         # disable controls customizing fetched ROS graph
00385         self._widget.graph_type_combo_box.setEnabled(False)
00386         self._widget.filter_line_edit.setEnabled(False)
00387         self._widget.topic_filter_line_edit.setEnabled(False)
00388         self._widget.namespace_cluster_check_box.setEnabled(False)
00389         self._widget.actionlib_check_box.setEnabled(False)
00390         self._widget.dead_sinks_check_box.setEnabled(False)
00391         self._widget.leaf_topics_check_box.setEnabled(False)
00392         self._widget.quiet_check_box.setEnabled(False)
00393         self._widget.unreachable_check_box.setEnabled(False)
00394         self._widget.group_tf_check_box.setEnabled(False)
00395         self._widget.hide_tf_nodes_check_box.setEnabled(False)
00396         self._widget.group_image_check_box.setEnabled(False)
00397         self._widget.hide_dynamic_reconfigure_check_box.setEnabled(False)
00398 
00399         self._update_graph_view(dotcode)
00400 
00401     def _fit_in_view(self):
00402         self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00403 
00404     def _save_dot(self):
00405         file_name, _ = QFileDialog.getSaveFileName(
00406             self._widget, self.tr('Save as DOT'), 'rosgraph.dot', self.tr('DOT graph (*.dot)'))
00407         if file_name is None or file_name == '':
00408             return
00409 
00410         handle = QFile(file_name)
00411         if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00412             return
00413 
00414         handle.write(self._current_dotcode)
00415         handle.close()
00416 
00417     def _save_svg(self):
00418         file_name, _ = QFileDialog.getSaveFileName(
00419             self._widget, self.tr('Save as SVG'), 'rosgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
00420         if file_name is None or file_name == '':
00421             return
00422 
00423         generator = QSvgGenerator()
00424         generator.setFileName(file_name)
00425         generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00426 
00427         painter = QPainter(generator)
00428         painter.setRenderHint(QPainter.Antialiasing)
00429         self._scene.render(painter)
00430         painter.end()
00431 
00432     def _save_image(self):
00433         file_name, _ = QFileDialog.getSaveFileName(
00434             self._widget, self.tr('Save as image'), 'rosgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00435         if file_name is None or file_name == '':
00436             return
00437 
00438         img = QImage((self._scene.sceneRect().size() * 2.0)
00439                      .toSize(), QImage.Format_ARGB32_Premultiplied)
00440         painter = QPainter(img)
00441         painter.setRenderHint(QPainter.Antialiasing)
00442         self._scene.render(painter)
00443         painter.end()
00444         img.save(file_name)


rqt_graph
Author(s): Dirk Thomas
autogenerated on Thu Jun 6 2019 17:35:26