00001
00002
00003
00004
00005
00006
00007
00008
00009 from __future__ import division
00010 import os
00011
00012 from python_qt_binding import loadUi
00013 from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, QAbstractListModel
00014 from python_qt_binding.QtGui import QFileDialog, QGraphicsScene, QIcon, QImage, QPainter, QWidget, QCompleter, QBrush, QColor, QPen
00015 from python_qt_binding.QtSvg import QSvgGenerator
00016
00017 import rosservice
00018 import rostopic
00019 import rospkg
00020
00021 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00022 from qt_gui.plugin import Plugin
00023
00024
00025 from qt_dotgraph.pydotfactory import PydotFactory
00026
00027
00028
00029
00030 from rocon_gateway import Graph
00031
00032 from .dotcode import RosGraphDotcodeGenerator, GATEWAY_GATEWAY_GRAPH, GATEWAY_FLIPPED_GRAPH, GATEWAY_PULLED_GRAPH
00033 from .interactive_graphics_view import InteractiveGraphicsView
00034
00035
00036
00037
00038
00039
00040 class RepeatedWordCompleter(QCompleter):
00041 """A completer that completes multiple times from a list"""
00042
00043 def init(self, parent=None):
00044 QCompleter.init(self, parent)
00045
00046 def pathFromIndex(self, index):
00047 path = QCompleter.pathFromIndex(self, index)
00048 lst = str(self.widget().text()).split(',')
00049 if len(lst) > 1:
00050 path = '%s, %s' % (','.join(lst[:-1]), path)
00051 return path
00052
00053 def splitPath(self, path):
00054 path = str(path.split(',')[-1]).lstrip(' ')
00055 return [path]
00056
00057
00058 class NamespaceCompletionModel(QAbstractListModel):
00059 """Ros package and stacknames"""
00060 def __init__(self, linewidget, topics_only):
00061 super(QAbstractListModel, self).__init__(linewidget)
00062 self.names = []
00063
00064 def refresh(self, names):
00065 namesset = set()
00066 for n in names:
00067 namesset.add(str(n).strip())
00068 namesset.add("-%s" % (str(n).strip()))
00069 self.names = sorted(namesset)
00070
00071 def rowCount(self, parent):
00072 return len(self.names)
00073
00074 def data(self, index, role):
00075 if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00076 return self.names[index.row()]
00077 return None
00078
00079
00080
00081
00082
00083
00084 class GatewayGraph(Plugin):
00085
00086 _deferred_fit_in_view = Signal()
00087
00088 def __init__(self, context):
00089 super(GatewayGraph, self).__init__(context)
00090 self.initialised = False
00091 self.setObjectName('Gateway Graph')
00092 self._current_dotcode = None
00093
00094 self._widget = QWidget()
00095
00096
00097 self.dotcode_factory = PydotFactory()
00098
00099 self.dotcode_generator = RosGraphDotcodeGenerator()
00100 self.dot_to_qt = DotToQtGenerator()
00101 self._graph = Graph()
00102
00103 rospack = rospkg.RosPack()
00104 ui_file = os.path.join(rospack.get_path('rocon_gateway_graph'), 'ui', 'gateway_graph.ui')
00105
00106 loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00107 self._widget.setObjectName('GatewayGraphUi')
00108 if context.serial_number() > 1:
00109 self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00110
00111 self._scene = QGraphicsScene()
00112 self._scene.setBackgroundBrush(Qt.white)
00113 self._widget.graphics_view.setScene(self._scene)
00114
00115 self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00116 self._widget.refresh_graph_push_button.pressed.connect(self._update_gateway_graph)
00117
00118 self._widget.graph_type_combo_box.insertItem(0, self.tr('Gateways'), GATEWAY_GATEWAY_GRAPH)
00119 self._widget.graph_type_combo_box.insertItem(1, self.tr('Pulled Connections'), GATEWAY_PULLED_GRAPH)
00120 self._widget.graph_type_combo_box.insertItem(2, self.tr('Flipped Connections'), GATEWAY_FLIPPED_GRAPH)
00121 self._widget.graph_type_combo_box.setCurrentIndex(0)
00122 self._widget.graph_type_combo_box.currentIndexChanged.connect(self._refresh_rosgraph)
00123
00124 self.node_completionmodel = NamespaceCompletionModel(self._widget.filter_line_edit, False)
00125 completer = RepeatedWordCompleter(self.node_completionmodel, self)
00126 completer.setCompletionMode(QCompleter.PopupCompletion)
00127 completer.setWrapAround(True)
00128 completer.setCaseSensitivity(Qt.CaseInsensitive)
00129 self._widget.filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00130 self._widget.filter_line_edit.setCompleter(completer)
00131 self.topic_completionmodel = NamespaceCompletionModel(self._widget.topic_filter_line_edit, False)
00132 topic_completer = RepeatedWordCompleter(self.topic_completionmodel, self)
00133 topic_completer.setCompletionMode(QCompleter.PopupCompletion)
00134 topic_completer.setWrapAround(True)
00135 topic_completer.setCaseSensitivity(Qt.CaseInsensitive)
00136 self._widget.topic_filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00137 self._widget.topic_filter_line_edit.setCompleter(topic_completer)
00138
00139 self._widget.namespace_cluster_check_box.clicked.connect(self._refresh_rosgraph)
00140 self._widget.watchlist_check_box.clicked.connect(self._refresh_rosgraph)
00141 self._widget.all_advertisements_check_box.clicked.connect(self._refresh_rosgraph)
00142
00143 self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
00144 self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
00145 self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00146 self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00147
00148 self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00149 self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00150 self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00151 self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00152 self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00153 self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00154 self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00155 self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00156
00157 self._update_gateway_graph()
00158 self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00159 self._deferred_fit_in_view.emit()
00160 context.add_widget(self._widget)
00161
00162 def save_settings(self, plugin_settings, instance_settings):
00163 instance_settings.set_value('graph_type_combo_box_index', self._widget.graph_type_combo_box.currentIndex())
00164 instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00165 instance_settings.set_value('topic_filter_line_edit_text', self._widget.topic_filter_line_edit.text())
00166 instance_settings.set_value('namespace_cluster_check_box_state', self._widget.namespace_cluster_check_box.isChecked())
00167 instance_settings.set_value('watchlist_check_box_state', self._widget.watchlist_check_box.isChecked())
00168 instance_settings.set_value('all_advertisements_check_box_state', self._widget.all_advertisements_check_box.isChecked())
00169 instance_settings.set_value('auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00170 instance_settings.set_value('highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
00171
00172 def restore_settings(self, plugin_settings, instance_settings):
00173 self._widget.graph_type_combo_box.setCurrentIndex(int(instance_settings.value('graph_type_combo_box_index', 0)))
00174 self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', ''))
00175 self._widget.topic_filter_line_edit.setText(instance_settings.value('topic_filter_line_edit_text', '/'))
00176 self._widget.namespace_cluster_check_box.setChecked(instance_settings.value('namespace_cluster_check_box_state', True) in [True, 'true'])
00177 self._widget.watchlist_check_box.setChecked(instance_settings.value('watchlist_check_box_state', True) in [True, 'true'])
00178 self._widget.all_advertisements_check_box.setChecked(instance_settings.value('all_advertisements_check_box_state', False) in [False, 'false'])
00179 self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
00180 self._widget.highlight_connections_check_box.setChecked(instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
00181 self.initialised = True
00182 self._refresh_rosgraph()
00183
00184 def shutdown_plugin(self):
00185 pass
00186
00187 def _update_gateway_graph(self):
00188
00189 self._widget.graph_type_combo_box.setEnabled(True)
00190 self._widget.filter_line_edit.setEnabled(True)
00191 self._widget.topic_filter_line_edit.setEnabled(True)
00192 self._widget.namespace_cluster_check_box.setEnabled(True)
00193 self._widget.watchlist_check_box.setEnabled(True)
00194 self._widget.all_advertisements_check_box.setEnabled(True)
00195
00196 self._graph.update()
00197 self.node_completionmodel.refresh(self._graph.gateway_nodes)
00198 self.topic_completionmodel.refresh(self._graph.flipped_nodes)
00199 self._refresh_rosgraph()
00200
00201 def _refresh_rosgraph(self):
00202 if not self.initialised:
00203 return
00204 self._update_graph_view(self._generate_dotcode())
00205
00206 def _generate_dotcode(self):
00207 ns_filter = self._widget.filter_line_edit.text()
00208 topic_filter = self._widget.topic_filter_line_edit.text()
00209 graph_mode = self._widget.graph_type_combo_box.itemData(self._widget.graph_type_combo_box.currentIndex())
00210 orientation = 'LR'
00211 if self._widget.namespace_cluster_check_box.isChecked():
00212 namespace_cluster = 1
00213 else:
00214 namespace_cluster = 0
00215 hide_dead_end_topics = self._widget.watchlist_check_box.isChecked()
00216 show_all_advertisements = self._widget.all_advertisements_check_box.isChecked()
00217
00218 return self.dotcode_generator.generate_dotcode(
00219 rosgraphinst=self._graph,
00220 ns_filter=ns_filter,
00221 topic_filter=topic_filter,
00222 graph_mode=graph_mode,
00223 show_all_advertisements=show_all_advertisements,
00224 hide_dead_end_topics=hide_dead_end_topics,
00225 cluster_namespaces_level=namespace_cluster,
00226 dotcode_factory=self.dotcode_factory,
00227 orientation=orientation,
00228 )
00229
00230 def _update_graph_view(self, dotcode):
00231 if dotcode == self._current_dotcode:
00232 return
00233 self._current_dotcode = dotcode
00234 self._redraw_graph_view()
00235
00236 def _generate_tool_tip(self, url):
00237 if url is not None and ':' in url:
00238 item_type, item_path = url.split(':', 1)
00239 if item_type == 'node':
00240 tool_tip = 'Node:\n %s' % (item_path)
00241 service_names = rosservice.get_service_list(node=item_path)
00242 if service_names:
00243 tool_tip += '\nServices:'
00244 for service_name in service_names:
00245 try:
00246 service_type = rosservice.get_service_type(service_name)
00247 tool_tip += '\n %s [%s]' % (service_name, service_type)
00248 except rosservice.ROSServiceIOException as e:
00249 tool_tip += '\n %s' % (e)
00250 return tool_tip
00251 elif item_type == 'topic':
00252 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00253 return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type)
00254 return url
00255
00256 def _redraw_graph_view(self):
00257 self._scene.clear()
00258
00259 if self._widget.highlight_connections_check_box.isChecked():
00260 highlight_level = 3
00261 else:
00262 highlight_level = 1
00263
00264
00265 (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode,
00266 highlight_level=highlight_level,
00267 same_label_siblings=True)
00268
00269
00270 for node_item in nodes.itervalues():
00271 if node_item._label.text() == self._graph.local_gateway_name():
00272 orange = QColor(232, 132, 3)
00273 node_item._default_color = orange
00274 node_item.set_node_color(orange)
00275 print "Local gateway: %s" % self._graph.local_gateway_name()
00276 self._scene.addItem(node_item)
00277 for edge_items in edges.itervalues():
00278 for edge_item in edge_items:
00279 edge_item.add_to_scene(self._scene)
00280
00281 self._scene.setSceneRect(self._scene.itemsBoundingRect())
00282 if self._widget.auto_fit_graph_check_box.isChecked():
00283 self._fit_in_view()
00284
00285 def _load_dot(self, file_name=None):
00286 if file_name is None:
00287 file_name, _ = QFileDialog.getOpenFileName(self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00288 if file_name is None or file_name == '':
00289 return
00290
00291 try:
00292 fh = open(file_name, 'rb')
00293 dotcode = fh.read()
00294 fh.close()
00295 except IOError:
00296 return
00297
00298
00299 self._widget.graph_type_combo_box.setEnabled(False)
00300 self._widget.filter_line_edit.setEnabled(False)
00301 self._widget.topic_filter_line_edit.setEnabled(False)
00302 self._widget.namespace_cluster_check_box.setEnabled(False)
00303 self._widget.watchlist_check_box.setEnabled(False)
00304 self._widget.all_advertisements_check_box.setEnabled(False)
00305
00306 self._update_graph_view(dotcode)
00307
00308 def _fit_in_view(self):
00309 self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00310
00311 def _save_dot(self):
00312 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as DOT'), 'rosgraph.dot', self.tr('DOT graph (*.dot)'))
00313 if file_name is None or file_name == '':
00314 return
00315
00316 handle = QFile(file_name)
00317 if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00318 return
00319
00320 handle.write(self._current_dotcode)
00321 handle.close()
00322
00323 def _save_svg(self):
00324 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as SVG'), 'rosgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
00325 if file_name is None or file_name == '':
00326 return
00327
00328 generator = QSvgGenerator()
00329 generator.setFileName(file_name)
00330 generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00331
00332 painter = QPainter(generator)
00333 painter.setRenderHint(QPainter.Antialiasing)
00334 self._scene.render(painter)
00335 painter.end()
00336
00337 def _save_image(self):
00338 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as image'), 'rosgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00339 if file_name is None or file_name == '':
00340 return
00341
00342 img = QImage((self._scene.sceneRect().size() * 2.0).toSize(), QImage.Format_ARGB32_Premultiplied)
00343 painter = QPainter(img)
00344 painter.setRenderHint(QPainter.Antialiasing)
00345 self._scene.render(painter)
00346 painter.end()
00347 img.save(file_name)