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 
00034 from python_qt_binding import loadUi
00035 from python_qt_binding.QtCore import QAbstractListModel, QFile, QIODevice, Qt, Signal
00036 from python_qt_binding.QtGui import QCompleter, QFileDialog, QGraphicsScene, QIcon, QImage, QPainter, QWidget
00037 from python_qt_binding.QtSvg import QSvgGenerator
00038 
00039 import roslib
00040 roslib.load_manifest('rqt_graph')
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         ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), '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 Fri Jan 3 2014 11:54:23