WUT Velma robot API
behavior_graph.py
Go to the documentation of this file.
1 # Copyright (c) 2014, Robot Control and Pattern Recognition Group, Warsaw University of Technology
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are met:
6 # * Redistributions of source code must retain the above copyright
7 # notice, this list of conditions and the following disclaimer.
8 # * Redistributions in binary form must reproduce the above copyright
9 # notice, this list of conditions and the following disclaimer in the
10 # documentation and/or other materials provided with the distribution.
11 # * Neither the name of the Warsaw University of Technology nor the
12 # names of its contributors may be used to endorse or promote products
13 # derived from this software without specific prior written permission.
14 #
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 # DISCLAIMED. IN NO EVENT SHALL <COPYright HOLDER> BE LIABLE FOR ANY
19 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 
26 import os
27 import math
28 
29 from python_qt_binding import loadUi
30 from python_qt_binding.QtCore import Qt, Signal, Slot, QRectF, QPointF, QSize, QRect, QPoint
31 from python_qt_binding.QtWidgets import QDialog, QGraphicsView, QGraphicsScene, QGraphicsPathItem, QGraphicsPolygonItem
32 from python_qt_binding.QtGui import QColor, QPen, QBrush, QPainterPath, QPolygonF, QTransform, QPainter
33 import rospkg
34 
35 from subsystem_msgs.srv import *
36 
37 def getComponentBrush(state):
38  b = QBrush(QColor(0,0,255)) # unconfigured/preoperational
39  if state == 'S': # stopped
40  b = QBrush(QColor(255,255,255))
41  elif state == 'R': # running
42  b = QBrush(QColor(0,255,0))
43  elif state == 'E': # error
44  b = QBrush(QColor(255,0,0))
45  elif state == 'F': # fatal error
46  b = QBrush(QColor(255,127,127))
47  elif state == 'X': # exception
48  b = QBrush(QColor(0,255,255))
49  return b
50 
51 def getComponentPen(state):
52  b = QPen(QColor(0,0,255)) # unconfigured/preoperational
53  if state == 'A': # stopped
54  b = QPen(QBrush(QColor(255,0,0)), 5)
55  elif state == 'N': # running
56  b = QPen(QBrush(QColor(0,0,0)), 1)
57  return b
58 
59 class GraphScene(QGraphicsScene):
60  def __init__(self, rect):
61  super(GraphScene, self).__init__(rect)
62 
63 class GraphView(QGraphicsView):
64 
65  comp_select_signal = Signal(str)
66 
67  def __init__ (self, parent = None):
68  super (GraphView, self).__init__ (parent)
69  self.parent = parent
70  self.transform_press = None
71  self.mousePressPos = None
72  self.setTransformationAnchor(QGraphicsView.NoAnchor)
73  self.setResizeAnchor(QGraphicsView.NoAnchor)
74 
75  def isPanActive(self):
76  return (self.transform_press != None and self.mousePressPos != None)
77 
78  def wheelEvent (self, event):
79  if self.isPanActive():
80  return
81 
82  oldPos = self.mapToScene(event.pos())
83  self.translate(oldPos.x(),oldPos.y())
84 
85  factor = 1.2
86  if event.angleDelta().y() < 0 :
87  factor = 1.0 / factor
88  self.scale(factor, factor)
89 
90  # Get the new position
91  newPos = self.mapToScene(event.pos())
92  # Move scene to old position
93  delta = newPos - oldPos
94  self.translate(delta.x(), delta.y())
95 
96  def selectComponent(self, name):
97 # print "selected component: ", name
98  self.comp_select_signal.emit(name)
99 
100  def mousePressEvent(self, event):
101  if event.button() == Qt.MidButton:
102  self.transform_press = self.transform()
103  self.mousePressPos = event.pos()
104  else:
105  super(GraphView, self).mousePressEvent(event)
106  selected = False
107  for item in self.items():
108  if type(item) is QGraphicsPolygonItem:
109  if item.contains( self.mapToScene(event.pos()) ):
110  self.selectComponent(item.data(0))
111  selected = True
112  break
113  elif type(item) is QGraphicsPathItem:
114  if item.contains( self.mapToScene(event.pos()) ):
115  print "connections: ", item.data(0)
116  if not selected:
117  self.selectComponent(None)
118 
119  def mouseMoveEvent(self, event):
120  if event.buttons() == Qt.MidButton:
121  mouseMovePos = event.pos()
122  view_diff = (mouseMovePos - self.mousePressPos)
123  tf = self.transform_press
124  scene_diff = view_diff
125  new_tf = QTransform(tf.m11(), tf.m12(), tf.m21(), tf.m22(), tf.dx()+view_diff.x(), tf.dy()+view_diff.y())
126  self.setTransform(new_tf)
127  else:
128  super(GraphView, self).mouseMoveEvent(event)
129 
130  def mouseReleaseEvent(self, event):
131  if event.button() == Qt.MidButton:
132  self.transform_press = None
133  self.mousePressPos = None
134  else:
135  super(GraphView, self).mouseReleaseEvent(event)
136 
137 class BehaviorGraphDialog(QDialog):
138 
139  def scX(self, x):
140  return self.scale_factor * float(x)
141 
142  def scY(self, y):
143  return self.scale_factor * float(y)
144 
145  def tfX(self, x):
146  return self.scale_factor * float(x)
147 
148  def tfY(self, y):
149  return self.scale_factor * (self.height - float(y))
150 
151  @Slot(int)
152  def portSelected(self, index):
153  for e in self.prev_selected_connections:
154  e.setPen( QPen(QBrush(QColor(0,0,0)), 1) )
156 
157  if not self.component_selected:
158  return
159 
160  for conn in self.parent.all_component_connections:
161  if (conn[0] == self.component_selected and conn[1] == self.selected_component_port_names[index]) or \
162  (conn[2] == self.component_selected and conn[3] == self.selected_component_port_names[index]):
163  for graph_name in self.edges:
164  for e in self.edges[graph_name]:
165  data = e.data(0)
166  if (data[0] == conn[0] and data[1] == conn[2]) or \
167  (data[0] == conn[2] and data[1] == conn[0]):
168  e.setPen( QPen(QBrush(QColor(255,0,0)), 5) )
169  self.prev_selected_connections.append(e)
170 
171  @Slot(str)
172  def componentSelected(self, name):
173  self.comboBoxConnections.clear()
174  self.component_selected = name
175 
176  if name == None:
177  self.labelSelectedComponent.setText('')
178  return
179 
180  self.labelSelectedComponent.setText(name)
181  for comp in self.parent.subsystem_info.components:
182  if comp.name == name:
184  for p in comp.ports:
185  self.selected_component_port_names.append(p.name)
186  port_str = ''
187  if p.is_input:
188  port_str += '[IN]'
189  else:
190  port_str += '[OUT]'
191  port_str += ' ' + p.name
192  if p.is_connected:
193  port_str += ' <conncected>'
194  else:
195  port_str += ' <not conncected>'
196  type_str = ''
197  for tn in p.type_names:
198  type_str += ' ' + tn
199  if len(type_str) == 0:
200  type_str = 'unknown type'
201  port_str += ', type:' + type_str
202  self.comboBoxConnections.addItem(port_str)
203  break
204 
205  for graph_name in self.nodes:
206  for comp_name in self.nodes[graph_name]:
207  self.nodes[graph_name][comp_name].setPen(getComponentPen('N'))
208  if name in self.nodes[graph_name]:
209  self.nodes[graph_name][name].setPen(getComponentPen('A'))
210 
211 
212  @Slot(int)
213  def graphSelected(self, index):
214  graph_name = self.comboBoxGraphs.itemText(index)
215  self.showGraph(graph_name)
216 
217  def __init__(self, subsystem_name, parent=None):
218  super(BehaviorGraphDialog, self).__init__(parent)
219 
220  self.parent = parent
221 
222  self.setWindowFlags(Qt.Window)
223 
224  rp = rospkg.RosPack()
225  ui_file = os.path.join(rp.get_path('rqt_agent'), 'resource', 'BehaviorVis.ui')
226  loadUi(ui_file, self)
227 
228  self.setWindowTitle(subsystem_name + " - transition function")
229  self.components_state = None
230  self.initialized = False
231 
232  self.pushButton_export.clicked.connect(self.exportClick)
233  self.pushButton_reset_view.clicked.connect(self.reset_viewClick)
234 
235  self.pushButton_close.clicked.connect(self.closeClick)
236  self.pushButton_zoom_in.clicked.connect(self.zoomInClick)
237  self.pushButton_zoom_out.clicked.connect(self.zoomOutClick)
238 
239  self.prev_selected_connections = []
240  self.comboBoxConnections.highlighted.connect(self.portSelected)
241  self.comboBoxGraphs.highlighted.connect(self.graphSelected)
242 
243  self.componentSelected(None)
244  self.scene = {}
245  self.graphicsView = None
246  self.nodes = {}
247  self.edges = {}
248 
249  self.quater = []
250  for i in range(10):
251  angle = 0.5*i/9.0 * math.pi
252  self.quater.append( QPointF(math.cos(angle), math.sin(angle)) )
253 
254  def showGraph(self, graph_name):
255  if not graph_name in self.scene:
256  print "could not show graph " + graph_name
257  return
258 
259  if not self.graphicsView:
260  self.graphicsView = GraphView()
261  self.verticalLayout.insertWidget(0, self.graphicsView)
262 
263  self.graphicsView.setScene(self.scene[graph_name])
264 
265  self.graphicsView.comp_select_signal.connect(self.componentSelected)
266  self.initialized = True
267 
268  def generateRoundedBox(self, x, y, w, h):
269  radius = 10.0
270 
271  poly = QPolygonF()
272 
273  for pt in self.quater:
274  poly.append(QPointF(x + w - radius, y + h - radius) + pt * radius)
275 
276  for pt in self.quater:
277  poly.append(QPointF(x + radius, y + h - radius) + QPointF(-pt.y(), pt.x()) * radius)
278 
279  for pt in self.quater:
280  poly.append(QPointF(x + radius, y + radius) + -pt * radius)
281 
282  for pt in self.quater:
283  poly.append(QPointF(x + w - radius, y + radius) + QPointF(pt.y(), -pt.x()) * radius)
284 
285  return poly
286 
287  def addGraph(self, graph_name, graph_str):
288 
289  self.comboBoxGraphs.addItem(graph_name)
290 
291  graph = graph_str.splitlines()
292 
293  header = graph[0].split()
294  if header[0] != 'graph':
295  raise Exception('wrong graph format', 'header is: ' + graph[0])
296 
297  self.scale_factor = 100.0
298  self.width = float(header[2])
299  self.height = float(header[3])
300  print "QGraphicsScene size:", self.width, self.height
301 
302  self.scene[graph_name] = GraphScene(QRectF(0, 0, self.scX(self.width), self.scY(self.height)))
303 
304  self.nodes[graph_name] = {}
305  self.edges[graph_name] = []
306 
307  for l in graph:
308  items = l.split()
309  if len(items) == 0:
310  continue
311  elif items[0] == 'stop':
312  break
313  elif items[0] == 'node':
314  #node CImp 16.472 5.25 0.86659 0.5 CImp filled ellipse lightblue lightblue
315  if len(items) != 11:
316  raise Exception('wrong number of items in line', 'line is: ' + l)
317  name = items[6]
318  if name == "\"\"":
319  name = ""
320  w = self.scX(items[4])
321  h = self.scY(items[5])
322  x = self.tfX(items[2])
323  y = self.tfY(items[3])
324 
325  self.nodes[graph_name][name] = self.scene[graph_name].addPolygon(self.generateRoundedBox(x - w/2, y - h/2, w, h))
326  self.nodes[graph_name][name].setData(0, name)
327  text_item = self.scene[graph_name].addSimpleText(name)
328  br = text_item.boundingRect()
329  text_item.setPos(x - br.width()/2, y - br.height()/2)
330 
331  elif items[0] == 'edge':
332  # without label:
333  # edge CImp Ts 4 16.068 5.159 15.143 4.9826 12.876 4.5503 11.87 4.3583 solid black
334  #
335  # with label:
336  # edge b_stSplit TorsoVelAggregate 7 7.5051 6.3954 7.7054 6.3043 7.9532 6.1899 8.1728 6.0833 8.4432 5.9522 8.7407 5.8012 8.9885 5.6735 aa 8.6798 5.9792 solid black
337  line_len = int(items[3])
338  label_text = None
339  label_pos = None
340  if (line_len * 2 + 6) == len(items):
341  # no label
342  pass
343  elif (line_len * 2 + 9) == len(items):
344  # edge with label
345  label_text = items[4 + line_len*2]
346  label_pos = QPointF(self.tfX(items[4 + line_len*2 + 1]), self.tfY(items[4 + line_len*2 + 2]))
347  else:
348  raise Exception('wrong number of items in line', 'should be: ' + str(line_len * 2 + 6) + " or " + str(line_len * 2 + 9) + ', line is: ' + l)
349 
350  line = []
351  for i in range(line_len):
352  line.append( (self.tfX(items[4+i*2]), self.tfY(items[5+i*2])) )
353  control_points_idx = 1
354  path = QPainterPath(QPointF(line[0][0], line[0][1]))
355  while True:
356  q1 = line[control_points_idx]
357  q2 = line[control_points_idx+1]
358  p2 = line[control_points_idx+2]
359  path.cubicTo( q1[0], q1[1], q2[0], q2[1], p2[0], p2[1] )
360  control_points_idx = control_points_idx + 3
361  if control_points_idx >= len(line):
362  break
363  edge = self.scene[graph_name].addPath(path)
364  edge.setData(0, (items[1], items[2]))
365  self.edges[graph_name].append(edge)
366 
367  end_p = QPointF(line[-1][0], line[-1][1])
368  p0 = end_p - QPointF(line[-2][0], line[-2][1])
369  p0_norm = math.sqrt(p0.x()*p0.x() + p0.y()*p0.y())
370  p0 = p0 / p0_norm
371  p0 = p0 * self.scale_factor * 0.15
372  p1 = QPointF(p0.y(), -p0.x()) * 0.25
373  p2 = -p1
374 
375  poly = QPolygonF()
376  poly.append(p0+end_p)
377  poly.append(p1+end_p)
378  poly.append(p2+end_p)
379  poly.append(p0+end_p)
380 
381 # poly_path = QPainterPath()
382 # poly_path.addPolygon(poly)
383 # painter = QPainter()
384  self.scene[graph_name].addPolygon(poly)
385 
386  if label_text and label_pos:
387  if label_text[0] == "\"":
388  label_text = label_text[1:]
389  if label_text[-1] == "\"":
390  label_text = label_text[:-1]
391  label_text = label_text.replace("\\n", "\n")
392  label_item = self.scene[graph_name].addSimpleText(label_text)
393  br = label_item.boundingRect()
394  label_item.setPos(label_pos.x() - br.width()/2, label_pos.y() - br.height()/2)
395 
396  @Slot()
397  def exportClick(self):
398  if not self.initialized:
399  return
400  self.parent.exportBehaviorGraphs()
401 
402  @Slot()
403  def reset_viewClick(self):
404  if not self.initialized:
405  return
406 
407  self.graphicsView.resetTransform()
408 
409  @Slot()
410  def closeClick(self):
411  self.close()
412 
413  @Slot()
414  def zoomInClick(self):
415  if not self.initialized:
416  return
417 
418  scaleFactor = 1.1
419  self.graphicsView.scale(scaleFactor, scaleFactor)
420 
421  @Slot()
422  def zoomOutClick(self):
423  if not self.initialized:
424  return
425 
426  scaleFactor = 1.0/1.1
427  self.graphicsView.scale(scaleFactor, scaleFactor)
428 
429  def updateState(self, components_state):
430  if not self.initialized:
431  return
432 
433  changed = False
434  if self.components_state == None:
435  changed = True
436  else:
437  for comp_name in self.components_state:
438  if comp_name in components_state and self.components_state[comp_name] != components_state[comp_name]:
439  changed = True
440  break
441 
442  if changed:
443  self.components_state = components_state
444  for graph_name in self.nodes:
445  for comp_name in self.nodes[graph_name]:
446  if comp_name in self.components_state:
447  self.nodes[graph_name][comp_name].setBrush(getComponentBrush(self.components_state[comp_name]))
448  else:
449  self.nodes[graph_name][comp_name].setBrush(QBrush(QColor(200,200,200)))
450 
def addGraph(self, graph_name, graph_str)
def __init__(self, parent=None)
def __init__(self, subsystem_name, parent=None)