ros_pack_graph.py
Go to the documentation of this file.
00001 # Software License Agreement (BSD License)
00002 #
00003 # Copyright (c) 2011, Willow Garage, Inc.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #  * Redistributions of source code must retain the above copyright
00011 #    notice, this list of conditions and the following disclaimer.
00012 #  * Redistributions in binary form must reproduce the above
00013 #    copyright notice, this list of conditions and the following
00014 #    disclaimer in the documentation and/or other materials provided
00015 #    with the distribution.
00016 #  * Neither the name of Willow Garage, Inc. nor the names of its
00017 #    contributors may be used to endorse or promote products derived
00018 #    from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 
00033 from __future__ import division
00034 from __future__ import print_function
00035 import os
00036 import pickle
00037 import sys
00038 
00039 import rospkg
00040 
00041 from python_qt_binding import loadUi
00042 from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, Slot, QAbstractListModel
00043 from python_qt_binding.QtGui import QIcon, QImage, QPainter
00044 from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget, QCompleter
00045 from python_qt_binding.QtSvg import QSvgGenerator
00046 
00047 import rosservice
00048 import rostopic
00049 
00050 from .dotcode_pack import RosPackageGraphDotcodeGenerator
00051 from qt_dotgraph.pydotfactory import PydotFactory
00052 # from qt_dotgraph.pygraphvizfactory import PygraphvizFactory
00053 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00054 from qt_gui_py_common.worker_thread import WorkerThread
00055 
00056 from rqt_gui_py.plugin import Plugin
00057 from rqt_graph.interactive_graphics_view import InteractiveGraphicsView
00058 
00059 
00060 class RepeatedWordCompleter(QCompleter):
00061 
00062     """A completer that completes multiple times from a list"""
00063 
00064     def pathFromIndex(self, index):
00065         path = QCompleter.pathFromIndex(self, index)
00066         lst = str(self.widget().text()).split(',')
00067         if len(lst) > 1:
00068             path = '%s, %s' % (','.join(lst[:-1]), path)
00069         return path
00070 
00071     def splitPath(self, path):
00072         path = str(path.split(',')[-1]).lstrip(' ')
00073         return [path]
00074 
00075 
00076 class StackageCompletionModel(QAbstractListModel):
00077 
00078     """Ros package and stacknames"""
00079 
00080     def __init__(self, linewidget, rospack, rosstack):
00081         super(StackageCompletionModel, self).__init__(linewidget)
00082         self.allnames = sorted(list(set(rospack.list() + rosstack.list())))
00083         self.allnames = self.allnames + ['-%s' % name for name in self.allnames]
00084 
00085     def rowCount(self, parent):
00086         return len(self.allnames)
00087 
00088     def data(self, index, role):
00089         # TODO: symbols to distinguish stacks from packages
00090         if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00091             return self.allnames[index.row()]
00092         return None
00093 
00094 
00095 class RosPackGraph(Plugin):
00096 
00097     _deferred_fit_in_view = Signal()
00098 
00099     def __init__(self, context):
00100         super(RosPackGraph, self).__init__(context)
00101         self.initialized = False
00102         self._current_dotcode = None
00103         self._update_thread = WorkerThread(self._update_thread_run, self._update_finished)
00104         self._nodes = {}
00105         self._edges = {}
00106         self._options = {}
00107         self._options_serialized = ''
00108 
00109         self.setObjectName('RosPackGraph')
00110 
00111         rospack = rospkg.RosPack()
00112         rosstack = rospkg.RosStack()
00113 
00114         # factory builds generic dotcode items
00115         self.dotcode_factory = PydotFactory()
00116         # self.dotcode_factory = PygraphvizFactory()
00117         # generator builds rosgraph
00118         self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack)
00119         # dot_to_qt transforms into Qt elements using dot layout
00120         self.dot_to_qt = DotToQtGenerator()
00121 
00122         self._widget = QWidget()
00123         rp = rospkg.RosPack()
00124         ui_file = os.path.join(rp.get_path('rqt_dep'), 'resource', 'RosPackGraph.ui')
00125         loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00126         self._widget.setObjectName('RosPackGraphUi')
00127         if context.serial_number() > 1:
00128             self._widget.setWindowTitle(
00129                 self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00130 
00131         self._scene = QGraphicsScene()
00132         self._scene.setBackgroundBrush(Qt.white)
00133         self._widget.graphics_view.setScene(self._scene)
00134 
00135         self._widget.depth_combo_box.insertItem(0, self.tr('infinite'), -1)
00136         self._widget.depth_combo_box.insertItem(1, self.tr('1'), 2)
00137         self._widget.depth_combo_box.insertItem(2, self.tr('2'), 3)
00138         self._widget.depth_combo_box.insertItem(3, self.tr('3'), 4)
00139         self._widget.depth_combo_box.insertItem(4, self.tr('4'), 5)
00140         self._widget.depth_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00141 
00142         self._widget.directions_combo_box.insertItem(0, self.tr('depends'), 0)
00143         self._widget.directions_combo_box.insertItem(1, self.tr('depends_on'), 1)
00144         self._widget.directions_combo_box.insertItem(2, self.tr('both'), 2)
00145         self._widget.directions_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00146 
00147         self._widget.package_type_combo_box.insertItem(0, self.tr('wet & dry'), 3)
00148         self._widget.package_type_combo_box.insertItem(1, self.tr('wet only'), 2)
00149         self._widget.package_type_combo_box.insertItem(2, self.tr('dry only'), 1)
00150         self._widget.package_type_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00151 
00152         completionmodel = StackageCompletionModel(self._widget.filter_line_edit, rospack, rosstack)
00153         completer = RepeatedWordCompleter(completionmodel, self)
00154         completer.setCompletionMode(QCompleter.PopupCompletion)
00155         completer.setWrapAround(True)
00156 
00157         completer.setCaseSensitivity(Qt.CaseInsensitive)
00158         self._widget.filter_line_edit.editingFinished.connect(self._refresh_rospackgraph)
00159         self._widget.filter_line_edit.setCompleter(completer)
00160         self._widget.filter_line_edit.selectionChanged.connect(self._clear_filter)
00161 
00162         self._widget.with_stacks_check_box.clicked.connect(self._refresh_rospackgraph)
00163         self._widget.mark_check_box.clicked.connect(self._refresh_rospackgraph)
00164         self._widget.colorize_check_box.clicked.connect(self._refresh_rospackgraph)
00165         self._widget.hide_transitives_check_box.clicked.connect(self._refresh_rospackgraph)
00166         self._widget.show_system_check_box.clicked.connect(self._refresh_rospackgraph)
00167 
00168         self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00169         self._widget.refresh_graph_push_button.pressed.connect(self._update_rospackgraph)
00170 
00171         self._widget.highlight_connections_check_box.toggled.connect(self._refresh_rospackgraph)
00172         self._widget.auto_fit_graph_check_box.toggled.connect(self._refresh_rospackgraph)
00173         self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00174         self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00175 
00176         self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00177         self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00178         self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00179         self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00180         self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00181         self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00182         self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00183         self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00184 
00185         self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00186         self._deferred_fit_in_view.emit()
00187 
00188         context.add_widget(self._widget)
00189 
00190         # If in either of following case, this turnes True
00191         # - 1st filtering key is already input by user
00192         # - filtering key is restored
00193         self._filtering_started = False
00194 
00195     def shutdown_plugin(self):
00196         self._update_thread.kill()
00197 
00198     def save_settings(self, plugin_settings, instance_settings):
00199         instance_settings.set_value(
00200             'depth_combo_box_index', self._widget.depth_combo_box.currentIndex())
00201         instance_settings.set_value(
00202             'directions_combo_box_index', self._widget.directions_combo_box.currentIndex())
00203         instance_settings.set_value(
00204             'package_type_combo_box', self._widget.package_type_combo_box.currentIndex())
00205         instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00206         instance_settings.set_value(
00207             'with_stacks_state', self._widget.with_stacks_check_box.isChecked())
00208         instance_settings.set_value(
00209             'hide_transitives_state', self._widget.hide_transitives_check_box.isChecked())
00210         instance_settings.set_value(
00211             'show_system_state', self._widget.show_system_check_box.isChecked())
00212         instance_settings.set_value('mark_state', self._widget.mark_check_box.isChecked())
00213         instance_settings.set_value('colorize_state', self._widget.colorize_check_box.isChecked())
00214         instance_settings.set_value(
00215             'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00216         instance_settings.set_value('highlight_connections_check_box_state',
00217                                     self._widget.highlight_connections_check_box.isChecked())
00218 
00219     def restore_settings(self, plugin_settings, instance_settings):
00220         _str_filter = instance_settings.value('filter_line_edit_text', '')
00221         if (_str_filter == None or _str_filter == '') and \
00222            not self._filtering_started:
00223             _str_filter = '(Separate pkgs by comma)'
00224         else:
00225             self._filtering_started = True
00226 
00227         self._widget.depth_combo_box.setCurrentIndex(
00228             int(instance_settings.value('depth_combo_box_index', 0)))
00229         self._widget.directions_combo_box.setCurrentIndex(
00230             int(instance_settings.value('directions_combo_box_index', 0)))
00231         self._widget.package_type_combo_box.setCurrentIndex(
00232             int(instance_settings.value('package_type_combo_box', 0)))
00233         self._widget.filter_line_edit.setText(_str_filter)
00234         self._widget.with_stacks_check_box.setChecked(
00235             instance_settings.value('with_stacks_state', True) in [True, 'true'])
00236         self._widget.mark_check_box.setChecked(
00237             instance_settings.value('mark_state', True) in [True, 'true'])
00238         self._widget.colorize_check_box.setChecked(
00239             instance_settings.value('colorize_state', False) in [True, 'true'])
00240         self._widget.hide_transitives_check_box.setChecked(
00241             instance_settings.value('hide_transitives_state', False) in [True, 'true'])
00242         self._widget.show_system_check_box.setChecked(
00243             instance_settings.value('show_system_state', False) in [True, 'true'])
00244         self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value(
00245             'auto_fit_graph_check_box_state', True) in [True, 'true'])
00246         self._widget.highlight_connections_check_box.setChecked(instance_settings.value(
00247             'highlight_connections_check_box_state', True) in [True, 'true'])
00248         self.initialized = True
00249         self._refresh_rospackgraph()
00250 
00251     def _update_rospackgraph(self):
00252         # re-enable controls customizing fetched ROS graph
00253         self._widget.depth_combo_box.setEnabled(True)
00254         self._widget.directions_combo_box.setEnabled(True)
00255         self._widget.package_type_combo_box.setEnabled(True)
00256         self._widget.filter_line_edit.setEnabled(True)
00257         self._widget.with_stacks_check_box.setEnabled(True)
00258         self._widget.mark_check_box.setEnabled(True)
00259         self._widget.colorize_check_box.setEnabled(True)
00260         self._widget.hide_transitives_check_box.setEnabled(True)
00261         self._widget.show_system_check_box.setEnabled(True)
00262 
00263         self._refresh_rospackgraph(force_update=True)
00264 
00265     def _update_options(self):
00266         self._options['depth'] = self._widget.depth_combo_box.itemData(
00267             self._widget.depth_combo_box.currentIndex())
00268         self._options['directions'] = self._widget.directions_combo_box.itemData(
00269             self._widget.directions_combo_box.currentIndex())
00270         self._options['package_types'] = self._widget.package_type_combo_box.itemData(
00271             self._widget.package_type_combo_box.currentIndex())
00272         self._options['with_stacks'] = self._widget.with_stacks_check_box.isChecked()
00273         self._options['mark_selected'] = self._widget.mark_check_box.isChecked()
00274         self._options['hide_transitives'] = self._widget.hide_transitives_check_box.isChecked()
00275         self._options['show_system'] = self._widget.show_system_check_box.isChecked()
00276         # TODO: Allow different color themes
00277         self._options['colortheme'] = True if self._widget.colorize_check_box.isChecked() else None
00278         self._options['names'] = self._widget.filter_line_edit.text().split(',')
00279         if self._options['names'] == [u'None']:
00280             self._options['names'] = []
00281         self._options['highlight_level'] = \
00282             3 if self._widget.highlight_connections_check_box.isChecked() else 1
00283         self._options['auto_fit'] = self._widget.auto_fit_graph_check_box.isChecked()
00284 
00285     def _refresh_rospackgraph(self, force_update=False):
00286         if not self.initialized:
00287             return
00288 
00289         self._update_thread.kill()
00290 
00291         self._update_options()
00292 
00293         # avoid update if options did not change and force_update is not set
00294         new_options_serialized = pickle.dumps(self._options)
00295         if new_options_serialized == self._options_serialized and not force_update:
00296             return
00297         self._options_serialized = pickle.dumps(self._options)
00298 
00299         self._scene.setBackgroundBrush(Qt.lightGray)
00300 
00301         self._update_thread.start()
00302 
00303     # this runs in a non-gui thread, so don't access widgets here directly
00304     def _update_thread_run(self):
00305         try:
00306             dotcode = self._generate_dotcode()
00307         except Exception as e:
00308             print(str(type(e)), str(e), file=sys.stderr)
00309             return
00310         self._update_graph(dotcode)
00311 
00312     @Slot()
00313     def _update_finished(self):
00314         self._scene.setBackgroundBrush(Qt.white)
00315         self._redraw_graph_scene()
00316 
00317     # this runs in a non-gui thread, so don't access widgets here directly
00318     def _generate_dotcode(self):
00319         includes = []
00320         excludes = []
00321         for name in self._options['names']:
00322             if name.strip().startswith('-'):
00323                 excludes.append(name.strip()[1:])
00324             else:
00325                 includes.append(name.strip())
00326         # orientation = 'LR'
00327         descendants = True
00328         ancestors = True
00329         if self._options['directions'] == 1:
00330             descendants = False
00331         if self._options['directions'] == 0:
00332             ancestors = False
00333         return self.dotcode_generator.generate_dotcode(
00334             dotcode_factory=self.dotcode_factory,
00335             selected_names=includes,
00336             excludes=excludes,
00337             depth=self._options['depth'],
00338             with_stacks=self._options['with_stacks'],
00339             descendants=descendants,
00340             ancestors=ancestors,
00341             mark_selected=self._options['mark_selected'],
00342             colortheme=self._options['colortheme'],
00343             hide_transitives=self._options['hide_transitives'],
00344             show_system=self._options['show_system'],
00345             hide_wet=self._options['package_types'] == 1,
00346             hide_dry=self._options['package_types'] == 2)
00347 
00348     # this runs in a non-gui thread, so don't access widgets here directly
00349     def _update_graph(self, dotcode):
00350         self._current_dotcode = dotcode
00351         self._nodes, self._edges = self.dot_to_qt.dotcode_to_qt_items(
00352             self._current_dotcode, self._options['highlight_level'])
00353 
00354     def _generate_tool_tip(self, url):
00355         if url is not None and ':' in url:
00356             item_type, item_path = url.split(':', 1)
00357             if item_type == 'node':
00358                 tool_tip = 'Node:\n  %s' % (item_path)
00359                 service_names = rosservice.get_service_list(node=item_path)
00360                 if service_names:
00361                     tool_tip += '\nServices:'
00362                     for service_name in service_names:
00363                         try:
00364                             service_type = rosservice.get_service_type(service_name)
00365                             tool_tip += '\n  %s [%s]' % (service_name, service_type)
00366                         except rosservice.ROSServiceIOException as e:
00367                             tool_tip += '\n  %s' % (e)
00368                 return tool_tip
00369             elif item_type == 'topic':
00370                 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00371                 return 'Topic:\n  %s\nType:\n  %s' % (topic_name, topic_type)
00372         return url
00373 
00374     def _redraw_graph_scene(self):
00375         # remove items in order to not garbage nodes which will be continued to be used
00376         for item in self._scene.items():
00377             self._scene.removeItem(item)
00378         self._scene.clear()
00379         for node_item in self._nodes.values():
00380             self._scene.addItem(node_item)
00381         for edge_items in self._edges.values():
00382             for edge_item in edge_items:
00383                 edge_item.add_to_scene(self._scene)
00384 
00385         self._scene.setSceneRect(self._scene.itemsBoundingRect())
00386         if self._options['auto_fit']:
00387             self._fit_in_view()
00388 
00389     def _load_dot(self, file_name=None):
00390         if file_name is None:
00391             file_name, _ = QFileDialog.getOpenFileName(
00392                 self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00393             if file_name is None or file_name == '':
00394                 return
00395 
00396         try:
00397             fh = open(file_name, 'rb')
00398             dotcode = fh.read()
00399             fh.close()
00400         except IOError:
00401             return
00402 
00403         # disable controls customizing fetched ROS graph
00404         self._widget.depth_combo_box.setEnabled(False)
00405         self._widget.directions_combo_box.setEnabled(False)
00406         self._widget.package_type_combo_box.setEnabled(False)
00407         self._widget.filter_line_edit.setEnabled(False)
00408         self._widget.with_stacks_check_box.setEnabled(False)
00409         self._widget.mark_check_box.setEnabled(False)
00410         self._widget.colorize_check_box.setEnabled(False)
00411         self._widget.hide_transitives_check_box.setEnabled(False)
00412         self._widget.show_system_check_box.setEnabled(False)
00413 
00414         self._update_graph(dotcode)
00415         self._redraw_graph_scene()
00416 
00417     @Slot()
00418     def _fit_in_view(self):
00419         self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00420 
00421     def _save_dot(self):
00422         file_name, _ = QFileDialog.getSaveFileName(
00423             self._widget, self.tr('Save as DOT'), 'rospackgraph.dot', self.tr('DOT graph (*.dot)'))
00424         if file_name is None or file_name == '':
00425             return
00426 
00427         handle = QFile(file_name)
00428         if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00429             return
00430 
00431         handle.write(self._current_dotcode)
00432         handle.close()
00433 
00434     def _save_svg(self):
00435         file_name, _ = QFileDialog.getSaveFileName(
00436             self._widget,
00437             self.tr('Save as SVG'),
00438             'rospackgraph.svg',
00439             self.tr('Scalable Vector Graphic (*.svg)'))
00440         if file_name is None or file_name == '':
00441             return
00442 
00443         generator = QSvgGenerator()
00444         generator.setFileName(file_name)
00445         generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00446 
00447         painter = QPainter(generator)
00448         painter.setRenderHint(QPainter.Antialiasing)
00449         self._scene.render(painter)
00450         painter.end()
00451 
00452     def _save_image(self):
00453         file_name, _ = QFileDialog.getSaveFileName(
00454             self._widget,
00455             self.tr('Save as image'),
00456             'rospackgraph.png',
00457             self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00458         if file_name is None or file_name == '':
00459             return
00460 
00461         img = QImage(
00462             (self._scene.sceneRect().size() * 2.0).toSize(),
00463             QImage.Format_ARGB32_Premultiplied)
00464         painter = QPainter(img)
00465         painter.setRenderHint(QPainter.Antialiasing)
00466         self._scene.render(painter)
00467         painter.end()
00468         img.save(file_name)
00469 
00470     def _clear_filter(self):
00471         if not self._filtering_started:
00472             self._widget.filter_line_edit.setText('')
00473             self._filtering_started = True


rqt_dep
Author(s): Thibault Kruse
autogenerated on Thu Jun 6 2019 20:36:24