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 
00056 class RepeatedWordCompleter(QCompleter):
00057     """A completer that completes multiple times from a list"""
00058     def init(self, parent=None):
00059         QCompleter.init(self, parent)
00060 
00061     def pathFromIndex(self, index):
00062         path = QCompleter.pathFromIndex(self, index)
00063         lst = str(self.widget().text()).split(',')
00064         if len(lst) > 1:
00065             path = '%s, %s' % (','.join(lst[:-1]), path)
00066         return path
00067 
00068     def splitPath(self, path):
00069         path = str(path.split(',')[-1]).lstrip(' ')
00070         return [path]
00071 
00072 
00073 class NamespaceCompletionModel(QAbstractListModel):
00074     """Ros package and stacknames"""
00075     def __init__(self, linewidget, topics_only):
00076         super(NamespaceCompletionModel, self).__init__(linewidget)
00077         self.names = []
00078 
00079     def refresh(self, names):
00080         namesset = set()
00081         for n in names:
00082             namesset.add(str(n).strip())
00083             namesset.add("-%s" % (str(n).strip()))
00084         self.names = sorted(namesset)
00085 
00086     def rowCount(self, parent):
00087         return len(self.names)
00088 
00089     def data(self, index, role):
00090         if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00091             return self.names[index.row()]
00092         return None
00093 
00094 
00095 class RosGraph(Plugin):
00096 
00097     _deferred_fit_in_view = Signal()
00098 
00099     def __init__(self, context):
00100         super(RosGraph, self).__init__(context)
00101         self.initialized = False
00102         self.setObjectName('RosGraph')
00103 
00104         self._graph = None
00105         self._current_dotcode = None
00106 
00107         self._widget = QWidget()
00108 
00109         # factory builds generic dotcode items
00110         self.dotcode_factory = PydotFactory()
00111         # self.dotcode_factory = PygraphvizFactory()
00112         # generator builds rosgraph
00113         self.dotcode_generator = RosGraphDotcodeGenerator()
00114         # dot_to_qt transforms into Qt elements using dot layout
00115         self.dot_to_qt = DotToQtGenerator()
00116 
00117         rp = rospkg.RosPack()
00118         ui_file = os.path.join(rp.get_path('rqt_graph'), 'resource', 'RosGraph.ui')
00119         loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00120         self._widget.setObjectName('RosGraphUi')
00121         if context.serial_number() > 1:
00122             self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00123 
00124         self._scene = QGraphicsScene()
00125         self._scene.setBackgroundBrush(Qt.white)
00126         self._widget.graphics_view.setScene(self._scene)
00127 
00128         self._widget.graph_type_combo_box.insertItem(0, self.tr('Nodes only'), NODE_NODE_GRAPH)
00129         self._widget.graph_type_combo_box.insertItem(1, self.tr('Nodes/Topics (active)'), NODE_TOPIC_GRAPH)
00130         self._widget.graph_type_combo_box.insertItem(2, self.tr('Nodes/Topics (all)'), NODE_TOPIC_ALL_GRAPH)
00131         self._widget.graph_type_combo_box.setCurrentIndex(0)
00132         self._widget.graph_type_combo_box.currentIndexChanged.connect(self._refresh_rosgraph)
00133 
00134         self.node_completionmodel = NamespaceCompletionModel(self._widget.filter_line_edit, False)
00135         completer = RepeatedWordCompleter(self.node_completionmodel, self)
00136         completer.setCompletionMode(QCompleter.PopupCompletion)
00137         completer.setWrapAround(True)
00138         completer.setCaseSensitivity(Qt.CaseInsensitive)
00139         self._widget.filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00140         self._widget.filter_line_edit.setCompleter(completer)
00141 
00142         self.topic_completionmodel = NamespaceCompletionModel(self._widget.topic_filter_line_edit, False)
00143         topic_completer = RepeatedWordCompleter(self.topic_completionmodel, self)
00144         topic_completer.setCompletionMode(QCompleter.PopupCompletion)
00145         topic_completer.setWrapAround(True)
00146         topic_completer.setCaseSensitivity(Qt.CaseInsensitive)
00147         self._widget.topic_filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00148         self._widget.topic_filter_line_edit.setCompleter(topic_completer)
00149 
00150         self._widget.namespace_cluster_check_box.clicked.connect(self._refresh_rosgraph)
00151         self._widget.actionlib_check_box.clicked.connect(self._refresh_rosgraph)
00152         self._widget.dead_sinks_check_box.clicked.connect(self._refresh_rosgraph)
00153         self._widget.leaf_topics_check_box.clicked.connect(self._refresh_rosgraph)
00154         self._widget.quiet_check_box.clicked.connect(self._refresh_rosgraph)
00155         self._widget.unreachable_check_box.clicked.connect(self._refresh_rosgraph)
00156 
00157         self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00158         self._widget.refresh_graph_push_button.pressed.connect(self._update_rosgraph)
00159 
00160         self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
00161         self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
00162         self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00163         self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00164 
00165         self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00166         self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00167         self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00168         self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00169         self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00170         self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00171         self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00172         self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00173 
00174         self._update_rosgraph()
00175         self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00176         self._deferred_fit_in_view.emit()
00177 
00178         context.add_widget(self._widget)
00179 
00180     def save_settings(self, plugin_settings, instance_settings):
00181         instance_settings.set_value('graph_type_combo_box_index', self._widget.graph_type_combo_box.currentIndex())
00182         instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00183         instance_settings.set_value('topic_filter_line_edit_text', self._widget.topic_filter_line_edit.text())
00184         instance_settings.set_value('namespace_cluster_check_box_state', self._widget.namespace_cluster_check_box.isChecked())
00185         instance_settings.set_value('actionlib_check_box_state', self._widget.actionlib_check_box.isChecked())
00186         instance_settings.set_value('dead_sinks_check_box_state', self._widget.dead_sinks_check_box.isChecked())
00187         instance_settings.set_value('leaf_topics_check_box_state', self._widget.leaf_topics_check_box.isChecked())
00188         instance_settings.set_value('quiet_check_box_state', self._widget.quiet_check_box.isChecked())
00189         instance_settings.set_value('unreachable_check_box_state', self._widget.unreachable_check_box.isChecked())
00190         instance_settings.set_value('auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00191         instance_settings.set_value('highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
00192 
00193     def restore_settings(self, plugin_settings, instance_settings):
00194         self._widget.graph_type_combo_box.setCurrentIndex(int(instance_settings.value('graph_type_combo_box_index', 0)))
00195         self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', '/'))
00196         self._widget.topic_filter_line_edit.setText(instance_settings.value('topic_filter_line_edit_text', '/'))
00197         self._widget.namespace_cluster_check_box.setChecked(instance_settings.value('namespace_cluster_check_box_state', True) in [True, 'true'])
00198         self._widget.actionlib_check_box.setChecked(instance_settings.value('actionlib_check_box_state', True) in [True, 'true'])
00199         self._widget.dead_sinks_check_box.setChecked(instance_settings.value('dead_sinks_check_box_state', True) in [True, 'true'])
00200         self._widget.leaf_topics_check_box.setChecked(instance_settings.value('leaf_topics_check_box_state', True) in [True, 'true'])
00201         self._widget.quiet_check_box.setChecked(instance_settings.value('quiet_check_box_state', True) in [True, 'true'])
00202         self._widget.unreachable_check_box.setChecked(instance_settings.value('unreachable_check_box_state', True) in [True, 'true'])
00203         self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
00204         self._widget.highlight_connections_check_box.setChecked(instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
00205         self.initialized = True
00206         self._refresh_rosgraph()
00207 
00208     def _update_rosgraph(self):
00209         # re-enable controls customizing fetched ROS graph
00210         self._widget.graph_type_combo_box.setEnabled(True)
00211         self._widget.filter_line_edit.setEnabled(True)
00212         self._widget.topic_filter_line_edit.setEnabled(True)
00213         self._widget.namespace_cluster_check_box.setEnabled(True)
00214         self._widget.actionlib_check_box.setEnabled(True)
00215         self._widget.dead_sinks_check_box.setEnabled(True)
00216         self._widget.leaf_topics_check_box.setEnabled(True)
00217         self._widget.quiet_check_box.setEnabled(True)
00218         self._widget.unreachable_check_box.setEnabled(True)
00219 
00220         self._graph = rosgraph.impl.graph.Graph()
00221         self._graph.set_master_stale(5.0)
00222         self._graph.set_node_stale(5.0)
00223         self._graph.update()
00224         self.node_completionmodel.refresh(self._graph.nn_nodes)
00225         self.topic_completionmodel.refresh(self._graph.nt_nodes)
00226         self._refresh_rosgraph()
00227 
00228     def _refresh_rosgraph(self):
00229         if not self.initialized:
00230             return
00231         self._update_graph_view(self._generate_dotcode())
00232 
00233     def _generate_dotcode(self):
00234         ns_filter = self._widget.filter_line_edit.text()
00235         topic_filter = self._widget.topic_filter_line_edit.text()
00236         graph_mode = self._widget.graph_type_combo_box.itemData(self._widget.graph_type_combo_box.currentIndex())
00237         orientation = 'LR'
00238         if self._widget.namespace_cluster_check_box.isChecked():
00239             namespace_cluster = 1
00240         else:
00241             namespace_cluster = 0
00242         accumulate_actions = self._widget.actionlib_check_box.isChecked()
00243         hide_dead_end_topics = self._widget.dead_sinks_check_box.isChecked()
00244         hide_single_connection_topics = self._widget.leaf_topics_check_box.isChecked()
00245         quiet = self._widget.quiet_check_box.isChecked()
00246         unreachable = self._widget.unreachable_check_box.isChecked()
00247 
00248         return self.dotcode_generator.generate_dotcode(
00249             rosgraphinst=self._graph,
00250             ns_filter=ns_filter,
00251             topic_filter=topic_filter,
00252             graph_mode=graph_mode,
00253             hide_single_connection_topics=hide_single_connection_topics,
00254             hide_dead_end_topics=hide_dead_end_topics,
00255             cluster_namespaces_level=namespace_cluster,
00256             accumulate_actions=accumulate_actions,
00257             dotcode_factory=self.dotcode_factory,
00258             orientation=orientation,
00259             quiet=quiet,
00260             unreachable=unreachable)
00261 
00262     def _update_graph_view(self, dotcode):
00263         if dotcode == self._current_dotcode:
00264             return
00265         self._current_dotcode = dotcode
00266         self._redraw_graph_view()
00267 
00268     def _generate_tool_tip(self, url):
00269         if url is not None and ':' in url:
00270             item_type, item_path = url.split(':', 1)
00271             if item_type == 'node':
00272                 tool_tip = 'Node:\n  %s' % (item_path)
00273                 service_names = rosservice.get_service_list(node=item_path)
00274                 if service_names:
00275                     tool_tip += '\nServices:'
00276                     for service_name in service_names:
00277                         try:
00278                             service_type = rosservice.get_service_type(service_name)
00279                             tool_tip += '\n  %s [%s]' % (service_name, service_type)
00280                         except rosservice.ROSServiceIOException as e:
00281                             tool_tip += '\n  %s' % (e)
00282                 return tool_tip
00283             elif item_type == 'topic':
00284                 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00285                 return 'Topic:\n  %s\nType:\n  %s' % (topic_name, topic_type)
00286         return url
00287 
00288     def _redraw_graph_view(self):
00289         self._scene.clear()
00290 
00291         if self._widget.highlight_connections_check_box.isChecked():
00292             highlight_level = 3
00293         else:
00294             highlight_level = 1
00295 
00296         # layout graph and create qt items
00297         (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode,
00298                                                             highlight_level=highlight_level,
00299                                                             same_label_siblings=True)
00300 
00301         for node_item in nodes.values():
00302             self._scene.addItem(node_item)
00303         for edge_items in edges.values():
00304             for edge_item in edge_items:
00305                 edge_item.add_to_scene(self._scene)
00306 
00307         self._scene.setSceneRect(self._scene.itemsBoundingRect())
00308         if self._widget.auto_fit_graph_check_box.isChecked():
00309             self._fit_in_view()
00310 
00311     def _load_dot(self, file_name=None):
00312         if file_name is None:
00313             file_name, _ = QFileDialog.getOpenFileName(self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00314             if file_name is None or file_name == '':
00315                 return
00316 
00317         try:
00318             fh = open(file_name, 'rb')
00319             dotcode = fh.read()
00320             fh.close()
00321         except IOError:
00322             return
00323 
00324         # disable controls customizing fetched ROS graph
00325         self._widget.graph_type_combo_box.setEnabled(False)
00326         self._widget.filter_line_edit.setEnabled(False)
00327         self._widget.topic_filter_line_edit.setEnabled(False)
00328         self._widget.namespace_cluster_check_box.setEnabled(False)
00329         self._widget.actionlib_check_box.setEnabled(False)
00330         self._widget.dead_sinks_check_box.setEnabled(False)
00331         self._widget.leaf_topics_check_box.setEnabled(False)
00332         self._widget.quiet_check_box.setEnabled(False)
00333         self._widget.unreachable_check_box.setEnabled(False)
00334 
00335         self._update_graph_view(dotcode)
00336 
00337     def _fit_in_view(self):
00338         self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00339 
00340     def _save_dot(self):
00341         file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as DOT'), 'rosgraph.dot', self.tr('DOT graph (*.dot)'))
00342         if file_name is None or file_name == '':
00343             return
00344 
00345         handle = QFile(file_name)
00346         if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00347             return
00348 
00349         handle.write(self._current_dotcode)
00350         handle.close()
00351 
00352     def _save_svg(self):
00353         file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as SVG'), 'rosgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
00354         if file_name is None or file_name == '':
00355             return
00356 
00357         generator = QSvgGenerator()
00358         generator.setFileName(file_name)
00359         generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00360 
00361         painter = QPainter(generator)
00362         painter.setRenderHint(QPainter.Antialiasing)
00363         self._scene.render(painter)
00364         painter.end()
00365 
00366     def _save_image(self):
00367         file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as image'), 'rosgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00368         if file_name is None or file_name == '':
00369             return
00370 
00371         img = QImage((self._scene.sceneRect().size() * 2.0).toSize(), QImage.Format_ARGB32_Premultiplied)
00372         painter = QPainter(img)
00373         painter.setRenderHint(QPainter.Antialiasing)
00374         self._scene.render(painter)
00375         painter.end()
00376         img.save(file_name)


rqt_graph
Author(s): Dirk Thomas
autogenerated on Fri Jul 28 2017 05:50:07