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


rqt_graph
Author(s): Dirk Thomas
autogenerated on Mon Oct 6 2014 07:15:04