pose_view_widget.py
Go to the documentation of this file.
1 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
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
6 # are met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following
12 # disclaimer in the documentation and/or other materials provided
13 # with the distribution.
14 # * Neither the name of the TU Darmstadt nor the names of its
15 # contributors may be used to endorse or promote products derived
16 # from this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 
31 from __future__ import division
32 import os
33 import rospkg
34 
35 from python_qt_binding import loadUi
36 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot
37 from python_qt_binding.QtWidgets import QAction, QMenu, QWidget
38 
39 import rospy
40 from rostopic import get_topic_class
41 from rqt_py_common.topic_helpers import find_slots_by_type_bfs
42 from tf.transformations import quaternion_matrix, quaternion_about_axis
43 from geometry_msgs.msg import Quaternion, Pose, Point
44 
45 from OpenGL.GL import glBegin, glColor3f, glEnd, glLineWidth, glMultMatrixf, glTranslatef, \
46  glVertex3f, GL_LINES, GL_QUADS
47 from .gl_widget import GLWidget
48 
49 
50 # main class inherits from the ui window class
51 class PoseViewWidget(QWidget):
52 
53  def __init__(self, plugin):
54  super(PoseViewWidget, self).__init__()
55  rp = rospkg.RosPack()
56  ui_file = os.path.join(rp.get_path('rqt_pose_view'), 'resource', 'PoseViewWidget.ui')
57  loadUi(ui_file, self)
58  self._plugin = plugin
59 
60  self._position = (2.0, 2.0, 2.0)
61  self._orientation = quaternion_about_axis(0.0, (1.0, 0.0, 0.0))
62  self._topic_name = None
63  self._subscriber = None
64 
65  # create GL view
66  self._gl_view = GLWidget()
67  self._gl_view.setAcceptDrops(True)
68 
69  # backup and replace original paint method
70  self._gl_view.paintGL_original = self._gl_view.paintGL
71  self._gl_view.paintGL = self._gl_view_paintGL
72 
73  # backup and replace original mouse release method
74  self._gl_view.mouseReleaseEvent_original = self._gl_view.mouseReleaseEvent
75  self._gl_view.mouseReleaseEvent = self._gl_view_mouseReleaseEvent
76 
77  # add GL view to widget layout
78  self.layout().addWidget(self._gl_view)
79 
80  # init and start update timer with 40ms (25fps)
81  self._update_timer = QTimer(self)
82  self._update_timer.timeout.connect(self.update_timeout)
83  self._update_timer.start(40)
84 
85  # subscribe immediately if topic name found on param server
86  if rospy.has_param('~pose_view_topic'):
87  topic = rospy.get_param('~pose_view_topic')
88  try:
89  self._subscribe_topic(topic)
90  except AttributeError:
91  rospy.logwarn("invalid topic name '{}'".format(topic))
92 
93  def save_settings(self, plugin_settings, instance_settings):
94  view_matrix_string = repr(self._gl_view.get_view_matrix())
95  instance_settings.set_value('view_matrix', view_matrix_string)
96 
97  def restore_settings(self, plugin_settings, instance_settings):
98  view_matrix_string = instance_settings.value('view_matrix')
99  try:
100  view_matrix = eval(view_matrix_string)
101  except Exception:
102  view_matrix = None
103 
104  if view_matrix is not None:
105  self._gl_view.set_view_matrix(view_matrix)
106  else:
107  self._set_default_view()
108 
109  def _set_default_view(self):
110  self._gl_view.makeCurrent()
111  self._gl_view.reset_view()
112  self._gl_view.rotate((0, 0, 1), 45)
113  self._gl_view.rotate((1, 0, 0), -65)
114  self._gl_view.translate((0, -3, -15))
115 
116  def update_timeout(self):
117  self._gl_view.makeCurrent()
118  self._gl_view.updateGL()
119 
120  def _gl_view_paintGL(self):
121  self._gl_view.paintGL_original()
122  self._paintGLGrid()
123  self._paintGLCoorsystem()
124  self._paintGLBox()
125 
126  def _paintGLBox(self):
127  # FIXME: add user configurable setting to allow use of translation as well
128  self._position = (2.0, 2.0, 2.0) # Set fixed translation for now
129 
130  glTranslatef(*self._position) # Translate Box
131 
132  matrix = quaternion_matrix(self._orientation) # convert quaternion to translation matrix
133  # tf uses row-major while gl expects column-major
134  matrix = matrix.transpose()
135  glMultMatrixf(matrix) # Rotate Box
136 
137  glBegin(GL_QUADS) # Start Drawing The Box
138 
139  glColor3f(0.0, 1.0, 0.0)
140  glVertex3f(1.0, 1.0, -1.0) # Top Right Of The Quad (Top)
141  glVertex3f(-1.0, 1.0, -1.0) # Top Left Of The Quad (Top)
142  glVertex3f(-1.0, 1.0, 1.0) # Bottom Left Of The Quad (Top)
143  glVertex3f(1.0, 1.0, 1.0) # Bottom Right Of The Quad (Top)
144 
145  glColor3f(0.5, 1.0, 0.5)
146  glVertex3f(1.0, -1.0, 1.0) # Top Right Of The Quad (Bottom)
147  glVertex3f(-1.0, -1.0, 1.0) # Top Left Of The Quad (Bottom)
148  glVertex3f(-1.0, -1.0, -1.0) # Bottom Left Of The Quad (Bottom)
149  glVertex3f(1.0, -1.0, -1.0) # Bottom Right Of The Quad (Bottom)
150 
151  glColor3f(0.0, 0.0, 1.0)
152  glVertex3f(1.0, 1.0, 1.0) # Top Right Of The Quad (Front)
153  glVertex3f(-1.0, 1.0, 1.0) # Top Left Of The Quad (Front)
154  glVertex3f(-1.0, -1.0, 1.0) # Bottom Left Of The Quad (Front)
155  glVertex3f(1.0, -1.0, 1.0) # Bottom Right Of The Quad (Front)
156 
157  glColor3f(0.5, 0.5, 1.0)
158  glVertex3f(1.0, -1.0, -1.0) # Bottom Left Of The Quad (Back)
159  glVertex3f(-1.0, -1.0, -1.0) # Bottom Right Of The Quad (Back)
160  glVertex3f(-1.0, 1.0, -1.0) # Top Right Of The Quad (Back)
161  glVertex3f(1.0, 1.0, -1.0) # Top Left Of The Quad (Back)
162 
163  glColor3f(1.0, 0.5, 0.5)
164  glVertex3f(-1.0, 1.0, 1.0) # Top Right Of The Quad (Left)
165  glVertex3f(-1.0, 1.0, -1.0) # Top Left Of The Quad (Left)
166  glVertex3f(-1.0, -1.0, -1.0) # Bottom Left Of The Quad (Left)
167  glVertex3f(-1.0, -1.0, 1.0) # Bottom Right Of The Quad (Left)
168 
169  glColor3f(1.0, 0.0, 0.0)
170  glVertex3f(1.0, 1.0, -1.0) # Top Right Of The Quad (Right)
171  glVertex3f(1.0, 1.0, 1.0) # Top Left Of The Quad (Right)
172  glVertex3f(1.0, -1.0, 1.0) # Bottom Left Of The Quad (Right)
173  glVertex3f(1.0, -1.0, -1.0) # Bottom Right Of The Quad (Right)
174  glEnd() # Done Drawing The Quad
175 
176  def _paintGLGrid(self):
177  resolution_millimeters = 1
178  gridded_area_size = 100
179 
180  glLineWidth(1.0)
181 
182  glBegin(GL_LINES)
183 
184  glColor3f(1.0, 1.0, 1.0)
185 
186  glVertex3f(gridded_area_size, 0, 0)
187  glVertex3f(-gridded_area_size, 0, 0)
188  glVertex3f(0, gridded_area_size, 0)
189  glVertex3f(0, -gridded_area_size, 0)
190 
191  num_of_lines = int(gridded_area_size / resolution_millimeters)
192 
193  for i in range(num_of_lines):
194  glVertex3f(resolution_millimeters * i, -gridded_area_size, 0)
195  glVertex3f(resolution_millimeters * i, gridded_area_size, 0)
196  glVertex3f(gridded_area_size, resolution_millimeters * i, 0)
197  glVertex3f(-gridded_area_size, resolution_millimeters * i, 0)
198 
199  glVertex3f(resolution_millimeters * (-i), -gridded_area_size, 0)
200  glVertex3f(resolution_millimeters * (-i), gridded_area_size, 0)
201  glVertex3f(gridded_area_size, resolution_millimeters * (-i), 0)
202  glVertex3f(-gridded_area_size, resolution_millimeters * (-i), 0)
203 
204  glEnd()
205 
207  glLineWidth(10.0)
208 
209  glBegin(GL_LINES)
210 
211  glColor3f(1.0, 0.0, 0.0)
212  glVertex3f(0.0, 0.0, 0.0)
213  glVertex3f(1.0, 0.0, 0.0)
214 
215  glColor3f(0.0, 1.0, 0.0)
216  glVertex3f(0.0, 0.0, 0.0)
217  glVertex3f(0.0, 1.0, 0.0)
218 
219  glColor3f(0.0, 0.0, 1.0)
220  glVertex3f(0.0, 0.0, 0.0)
221  glVertex3f(0.0, 0.0, 1.0)
222 
223  glEnd()
224 
225  def _gl_view_mouseReleaseEvent(self, event):
226  if event.button() == Qt.RightButton:
227  menu = QMenu(self._gl_view)
228  action = QAction(self._gl_view.tr("Reset view"), self._gl_view)
229  menu.addAction(action)
230  action.triggered.connect(self._set_default_view)
231  menu.exec_(self._gl_view.mapToGlobal(event.pos()))
232 
233  @Slot('QDragEnterEvent*')
234  def dragEnterEvent(self, event):
235  if event.mimeData().hasText():
236  topic_name = str(event.mimeData().text())
237  if len(topic_name) == 0:
238  qWarning('PoseViewWidget.dragEnterEvent(): event.mimeData() text is empty')
239  return
240  else:
241  if not hasattr(event.source(), 'selectedItems') or len(event.source().selectedItems()) == 0:
242  qWarning('PoseViewWidget.dragEnterEvent(): event.source() has no attribute selectedItems or length of selectedItems is 0')
243  return
244  item = event.source().selectedItems()[0]
245  topic_name = item.data(0, Qt.UserRole)
246 
247  if topic_name is None:
248  qWarning('PoseViewWidget.dragEnterEvent(): selectedItem has no UserRole data with a topic name')
249  return
250 
251  # check for valid topic
252  msg_class, self._topic_name, _ = get_topic_class(topic_name)
253  if msg_class is None:
254  qWarning('PoseViewWidget.dragEnterEvent(): No message class was found for topic "%s".' % topic_name)
255  return
256 
257  # check for valid message class
258  quaternion_slot_path, point_slot_path = self._get_slot_paths(msg_class)
259 
260  if quaternion_slot_path is None and point_slot_path is None:
261  qWarning('PoseViewWidget.dragEnterEvent(): No Pose, Quaternion or Point data was found outside of arrays in "%s" on topic "%s".'
262  % (msg_class._type, topic_name))
263  return
264 
265  event.acceptProposedAction()
266 
267  @Slot('QDropEvent*')
268  def dropEvent(self, event):
269  if event.mimeData().hasText():
270  topic_name = str(event.mimeData().text())
271  else:
272  dropped_item = event.source().selectedItems()[0]
273  topic_name = str(dropped_item.data(0, Qt.UserRole))
274 
275  self._unregister_topic()
276  self._subscribe_topic(topic_name)
277 
278  def _unregister_topic(self):
279  if self._subscriber:
280  self._subscriber.unregister()
281 
282  @staticmethod
284  path = path.split('/')
285  if path == ['']:
286  return []
287  return path
288 
289  @staticmethod
290  def _get_slot_paths(msg_class):
291  # find first Pose in msg_class
292  pose_slot_paths = find_slots_by_type_bfs(msg_class, Pose)
293  for path in pose_slot_paths:
294  # make sure the path does not contain an array, because we don't want to deal with empty arrays...
295  if '[' not in path:
296  path = PoseViewWidget._make_path_list_from_path_string(pose_slot_paths[0])
297  return path + ['orientation'], path + ['position']
298 
299  # if no Pose is found, find first Quaternion and Point
300  quaternion_slot_paths = find_slots_by_type_bfs(msg_class, Quaternion)
301  for path in quaternion_slot_paths:
302  if '[' not in path:
303  quaternion_slot_path = PoseViewWidget._make_path_list_from_path_string(path)
304  break
305  else:
306  quaternion_slot_path = None
307 
308  point_slot_paths = find_slots_by_type_bfs(msg_class, Point)
309  for path in point_slot_paths:
310  if '[' not in path:
311  point_slot_path = PoseViewWidget._make_path_list_from_path_string(path)
312  break
313  else:
314  point_slot_path = None
315 
316  return quaternion_slot_path, point_slot_path
317 
318 
319  def _subscribe_topic(self, topic_name):
320  msg_class, self._topic_name, _ = get_topic_class(topic_name)
321  quaternion_slot_path, point_slot_path = self._get_slot_paths(msg_class)
322 
323  self._subscriber = rospy.Subscriber(
324  self._topic_name,
325  msg_class,
326  self.message_callback,
327  callback_args=(quaternion_slot_path, point_slot_path)
328  )
329 
330  def message_callback(self, message, slot_paths):
331  quaternion_slot_path = slot_paths[0]
332  point_slot_path = slot_paths[1]
333 
334  if quaternion_slot_path is None:
335  self._orientation = quaternion_about_axis(0.0, (1.0, 0.0, 0.0))
336  else:
337  orientation = message
338  for slot_name in quaternion_slot_path:
339  orientation = getattr(orientation, slot_name)
340  self._orientation = (orientation.x, orientation.y, orientation.z, orientation.w)
341 
342  if point_slot_path is None:
343  # if no point is given, set it to a fixed offset so the axes can be seen
344  self._position = (2.0, 2.0, 2.0)
345  else:
346  position = message
347  for slot_name in point_slot_path:
348  position = getattr(position, slot_name)
349  self._position = (position.x, position.y, position.z)
350 
351  def shutdown_plugin(self):
352  self._unregister_topic()
def restore_settings(self, plugin_settings, instance_settings)
def save_settings(self, plugin_settings, instance_settings)
def message_callback(self, message, slot_paths)


rqt_pose_view
Author(s): Aaron Blasdel, Dirk Thomas, Dorian Scholz
autogenerated on Sat May 1 2021 02:41:36