WUT Velma robot API
fsm_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, QGraphicsEllipseItem, QGraphicsPathItem
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  return b
44 
45 def getComponentPen(state):
46  b = QPen(QColor(0,0,255)) # unconfigured/preoperational
47  if state == 'A': # stopped
48  b = QPen(QBrush(QColor(255,0,0)), 5)
49  elif state == 'N': # running
50  b = QPen(QBrush(QColor(0,0,0)), 1)
51  return b
52 
53 class GraphScene(QGraphicsScene):
54  def __init__(self, rect):
55  super(GraphScene, self).__init__(rect)
56 
57 class GraphView(QGraphicsView):
58 
59  comp_select_signal = Signal(str)
60 
61  def __init__ (self, parent = None):
62  super (GraphView, self).__init__ (parent)
63  self.parent = parent
64  self.transform_press = None
65  self.mousePressPos = None
66  self.setTransformationAnchor(QGraphicsView.NoAnchor)
67  self.setResizeAnchor(QGraphicsView.NoAnchor)
68 
69  def isPanActive(self):
70  return (self.transform_press != None and self.mousePressPos != None)
71 
72  def wheelEvent (self, event):
73  if self.isPanActive():
74  return
75 
76  oldPos = self.mapToScene(event.pos())
77  self.translate(oldPos.x(),oldPos.y())
78 
79  factor = 1.2
80  if event.angleDelta().y() < 0 :
81  factor = 1.0 / factor
82  self.scale(factor, factor)
83 
84  # Get the new position
85  newPos = self.mapToScene(event.pos())
86  # Move scene to old position
87  delta = newPos - oldPos
88  self.translate(delta.x(), delta.y())
89 
90  def selectComponent(self, name):
91 # print "selected component: ", name
92  self.comp_select_signal.emit(name)
93 
94  def mousePressEvent(self, event):
95  if event.button() == Qt.MidButton:
96  self.transform_press = self.transform()
97  self.mousePressPos = event.pos()
98  else:
99  super(GraphView, self).mousePressEvent(event)
100  selected = False
101  for item in self.items():
102  if type(item) is QGraphicsEllipseItem:
103  if item.contains( self.mapToScene(event.pos()) ):
104  self.selectComponent(item.data(0))
105  selected = True
106  break
107  elif type(item) is QGraphicsPathItem:
108  if item.contains( self.mapToScene(event.pos()) ):
109  print "connections: ", item.data(0)
110  if not selected:
111  self.selectComponent(None)
112 
113  def mouseMoveEvent(self, event):
114  if event.buttons() == Qt.MidButton:
115  mouseMovePos = event.pos()
116  view_diff = (mouseMovePos - self.mousePressPos)
117  tf = self.transform_press
118  scene_diff = view_diff
119  new_tf = QTransform(tf.m11(), tf.m12(), tf.m21(), tf.m22(), tf.dx()+view_diff.x(), tf.dy()+view_diff.y())
120  self.setTransform(new_tf)
121  else:
122  super(GraphView, self).mouseMoveEvent(event)
123 
124  def mouseReleaseEvent(self, event):
125  if event.button() == Qt.MidButton:
126  self.transform_press = None
127  self.mousePressPos = None
128  else:
129  super(GraphView, self).mouseReleaseEvent(event)
130 
131 
132 
133 class StateMachineGraphDialog(QDialog):
134 
135  def scX(self, x):
136  return self.scale_factor * float(x)
137 
138  def scY(self, y):
139  return self.scale_factor * float(y)
140 
141  def tfX(self, x):
142  return self.scale_factor * float(x)
143 
144  def tfY(self, y):
145  return self.scale_factor * (self.height - float(y))
146 
147  @Slot(int)
148  def transitionSelected(self, index):
149  for e in self.prev_selected_connections:
150  e.setPen( QPen(QBrush(QColor(0,0,0)), 1) )
152 
153  if not self.component_selected:
154  return
155 
156  for e in self.edges:
157  data = e.data(0)
158  if (data[0] == self.component_selected) and (data[1] == self.selected_component_next_states_names[index]):
159  e.setPen( QPen(QBrush(QColor(255,0,0)), 5) )
160  self.prev_selected_connections.append(e)
161 
162 
163  @Slot(str)
164  def componentSelected(self, name):
165  self.comboBoxConnections.clear()
166  self.component_selected = name
167 
168  if name == None or name == '':
169  # print "name: NONE"
170  self.labelSelectedComponent.setText('No state selected')
171  self.labelSubBehaviors.setText('No state selected')
172  return
173 
174  # print "name:", name
175 
176  self.labelSelectedComponent.setText(name)
177 
178  # Do combo box comboBoxConnections nalezy dolaczyc wszytkie tranzycje
179 
180  for state in self.parent.subsystem_info.state_machine:
181  if state.name == name:
183  # print "self.parent.subsystem_info.state_machine state:", state
184 
185  behavior_str = ''
186  sb_iter = 0
187  for subbehavior in state.behavior_names:
188  sb_iter +=1
189  if sb_iter > 1:
190  behavior_str += ', '
191  behavior_str += subbehavior
192  self.labelSubBehaviors.setText(behavior_str)
193 
194  for next_state in state.next_states:
195  self.selected_component_next_states_names.append(next_state.name)
196  transition_str = 'next state - NAME: ' + next_state.name + ', INIT_COND: ' + next_state.init_cond
197  self.comboBoxConnections.addItem(transition_str)
198  break
199 
200  for comp_name in self.nodes:
201  self.nodes[comp_name].setPen(getComponentPen('N'))
202  if name in self.nodes:
203  self.nodes[name].setPen(getComponentPen('A'))
204 
205 
206  @Slot(int)
207  def __init__(self, subsystem_name, parent=None):
208  super(StateMachineGraphDialog, self).__init__(parent)
209 
210  self.parent = parent
211 
212  self.setWindowFlags(Qt.Window)
213 
214  rp = rospkg.RosPack()
215  ui_file = os.path.join(rp.get_path('rqt_agent'), 'resource', 'FsmVis.ui')
216  loadUi(ui_file, self)
217 
218  self.setWindowTitle(subsystem_name + " - state machine")
219  self.components_state = None
220  self.initialized = False
221 
222  self.pushButton_export.clicked.connect(self.exportClick)
223  self.pushButton_reset_view.clicked.connect(self.reset_viewClick)
224 
225  self.pushButton_close.clicked.connect(self.closeClick)
226  self.pushButton_zoom_in.clicked.connect(self.zoomInClick)
227  self.pushButton_zoom_out.clicked.connect(self.zoomOutClick)
228 
229  self.prev_selected_connections = []
230  self.comboBoxConnections.highlighted.connect(self.transitionSelected)
231 
232  self.componentSelected(None)
233  self.scene = {}
234  self.graphicsView = None
235  self.nodes = {}
236  self.edges = []
237 
238  def showGraph(self):
239 
240  if not self.graphicsView:
241  self.graphicsView = GraphView()
242  self.verticalLayout.insertWidget(0, self.graphicsView)
243 
244  self.graphicsView.setScene(self.scene)
245 
246  self.graphicsView.comp_select_signal.connect(self.componentSelected)
247  self.initialized = True
248 
249  def addGraph(self, graph_str):
250 
251  graph = graph_str.splitlines()
252 
253  header = graph[0].split()
254  if header[0] != 'graph':
255  raise Exception('wrong graph format', 'header is: ' + graph[0])
256 
257  self.scale_factor = 100.0
258  self.width = float(header[2])
259  self.height = float(header[3])
260  print "QGraphicsScene size:", self.width, self.height
261 
262  self.scene = GraphScene(QRectF(0, 0, self.scX(self.width), self.scY(self.height)))
263 
264  for l in graph:
265  items = l.split()
266  if len(items) == 0:
267  continue
268  elif items[0] == 'stop':
269  break
270  elif items[0] == 'node':
271  #node CImp 16.472 5.25 0.86659 0.5 CImp filled ellipse lightblue lightblue
272  if len(items) != 11:
273  raise Exception('wrong number of items in line', 'line is: ' + l)
274  name = items[6]
275  if name == "\"\"":
276  name = ""
277  w = self.scX(items[4])
278  h = self.scY(items[5])
279  x = self.tfX(items[2])
280  y = self.tfY(items[3])
281 
282  self.nodes[name] = self.scene.addEllipse(x - w/2, y - h/2, w, h)
283  self.nodes[name].setData(0, name)
284  text_item = self.scene.addSimpleText(name)
285  br = text_item.boundingRect()
286  text_item.setPos(x - br.width()/2, y - br.height()/2)
287 
288  elif items[0] == 'edge':
289  # without label:
290  # edge CImp Ts 4 16.068 5.159 15.143 4.9826 12.876 4.5503 11.87 4.3583 solid black
291  #
292  # with label:
293  # 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
294  line_len = int(items[3])
295  label_text = None
296  label_pos = None
297  if (line_len * 2 + 6) == len(items):
298  # no label
299  pass
300  elif (line_len * 2 + 9) == len(items):
301  # edge with label
302  label_text = items[4 + line_len*2]
303  label_pos = QPointF(self.tfX(items[4 + line_len*2 + 1]), self.tfY(items[4 + line_len*2 + 2]))
304  else:
305  raise Exception('wrong number of items in line', 'should be: ' + str(line_len * 2 + 6) + " or " + str(line_len * 2 + 9) + ', line is: ' + l)
306 
307  line = []
308  for i in range(line_len):
309  line.append( (self.tfX(items[4+i*2]), self.tfY(items[5+i*2])) )
310  control_points_idx = 1
311  path = QPainterPath(QPointF(line[0][0], line[0][1]))
312  while True:
313  q1 = line[control_points_idx]
314  q2 = line[control_points_idx+1]
315  p2 = line[control_points_idx+2]
316  path.cubicTo( q1[0], q1[1], q2[0], q2[1], p2[0], p2[1] )
317  control_points_idx = control_points_idx + 3
318  if control_points_idx >= len(line):
319  break
320  edge = self.scene.addPath(path)
321  edge.setData(0, (items[1], items[2]))
322  self.edges.append(edge)
323 
324  end_p = QPointF(line[-1][0], line[-1][1])
325  p0 = end_p - QPointF(line[-2][0], line[-2][1])
326  p0_norm = math.sqrt(p0.x()*p0.x() + p0.y()*p0.y())
327  p0 = p0 / p0_norm
328  p0 = p0 * self.scale_factor * 0.15
329  p1 = QPointF(p0.y(), -p0.x()) * 0.25
330  p2 = -p1
331 
332  poly = QPolygonF()
333  poly.append(p0+end_p)
334  poly.append(p1+end_p)
335  poly.append(p2+end_p)
336  poly.append(p0+end_p)
337 
338 # poly_path = QPainterPath()
339 # poly_path.addPolygon(poly)
340 # painter = QPainter()
341  self.scene.addPolygon(poly)
342 
343  if label_text and label_pos:
344  if label_text[0] == "\"":
345  label_text = label_text[1:]
346  if label_text[-1] == "\"":
347  label_text = label_text[:-1]
348  label_text = label_text.replace("\\n", "\n")
349  label_item = self.scene.addSimpleText(label_text)
350  br = label_item.boundingRect()
351  label_item.setPos(label_pos.x() - br.width()/2, label_pos.y() - br.height()/2)
352 
353  @Slot()
354  def exportClick(self):
355  if not self.initialized:
356  return
357  self.parent.exportStateMachineGraph()
358 
359  @Slot()
360  def reset_viewClick(self):
361  if not self.initialized:
362  return
363 
364  self.graphicsView.resetTransform()
365 
366  @Slot()
367  def closeClick(self):
368  self.close()
369 
370  @Slot()
371  def zoomInClick(self):
372  if not self.initialized:
373  return
374 
375  scaleFactor = 1.1
376  self.graphicsView.scale(scaleFactor, scaleFactor)
377 
378  @Slot()
379  def zoomOutClick(self):
380  if not self.initialized:
381  return
382 
383  scaleFactor = 1.0/1.1
384  self.graphicsView.scale(scaleFactor, scaleFactor)
385 
386  def updateState(self, mcd):
387  if not self.initialized:
388  return
389  active_state_name = mcd.history[0].state_name
390  for comp_name in self.nodes:
391  if comp_name == active_state_name:
392  self.nodes[comp_name].setBrush(getComponentBrush('R'))
393  else:
394  self.nodes[comp_name].setBrush(getComponentBrush('S'))
395 
def __init__(self, rect)
Definition: fsm_graph.py:54
def mouseReleaseEvent(self, event)
Definition: fsm_graph.py:124
def __init__(self, parent=None)
Definition: fsm_graph.py:61
def getComponentBrush(state)
Definition: fsm_graph.py:37
def wheelEvent(self, event)
Definition: fsm_graph.py:72
def mousePressEvent(self, event)
Definition: fsm_graph.py:94
def selectComponent(self, name)
Definition: fsm_graph.py:90
def __init__(self, subsystem_name, parent=None)
Definition: fsm_graph.py:207
def getComponentPen(state)
Definition: fsm_graph.py:45
def mouseMoveEvent(self, event)
Definition: fsm_graph.py:113