00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042 PKG = 'rxtools'
00043 import collections
00044 import os
00045 import string
00046 import sys
00047 import wx
00048
00049 import roslib.names
00050 import roslib.packages
00051
00052 import rospy
00053
00054 import rxtools.vizutil
00055 rxtools.vizutil.check_matplotlib_deps()
00056
00057
00058
00059 import matplotlib
00060 matplotlib.use('WXAgg')
00061 from matplotlib.figure import Figure
00062 from matplotlib.backends.backend_wxagg import \
00063 FigureCanvasWxAgg as FigCanvas, \
00064 NavigationToolbar2WxAgg as NavigationToolbar
00065
00066 import numpy as np
00067 import pylab
00068
00069 from rxtools.rosplot import ROSData, is_ros_pause, is_ros_stop, toggle_ros_pause, set_ros_stop
00070 import mpl_toolkits.mplot3d.art3d as art3d
00071
00072
00073
00074 COLORS = (1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 1), (0, 1, 1), (0.5, 0.24, 0), (0, 0.5, 0.24), (1, 0.5, 0),
00075
00076
00077
00078 class RxPlotToolbar(NavigationToolbar):
00079 ON_PAUSE = wx.NewId()
00080 ON_STOP = wx.NewId()
00081 def __init__(self, canvas):
00082 NavigationToolbar.__init__(self, canvas)
00083
00084 self.play_bm = wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/play-16.png')
00085 self.pause_bm = wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/pause-16.png')
00086 self.AddSimpleTool(self.ON_PAUSE, self.pause_bm, 'Pause', 'Activate pause')
00087 wx.EVT_TOOL(self, self.ON_PAUSE, self._on_pause)
00088
00089 self.AddSimpleTool(self.ON_STOP, wx.Bitmap(roslib.packages.get_pkg_dir(PKG) + '/icons/stop-16.png'), 'Stop', 'Activate stop')
00090
00091 wx.EVT_TOOL(self, self.ON_STOP, self._on_stop)
00092 self._set_tb_enable()
00093
00094
00095 def _set_tb_enable(self):
00096
00097
00098
00099 tb_enabled = is_ros_pause() or is_ros_stop()
00100 try:
00101 buttons = [self.wx_ids['Back'], self.wx_ids['Forward'], self.wx_ids['Pan'], self.wx_ids['Zoom']]
00102 except AttributeError:
00103 buttons = [self._NTB2_BACK, self._NTB2_FORWARD, self._NTB2_PAN, self._NTB2_ZOOM]
00104 for b in buttons:
00105 self.EnableTool(b, tb_enabled)
00106
00107 def _on_pause(self, evt):
00108 toggle_ros_pause()
00109 self._set_tb_enable()
00110 if is_ros_pause():
00111 self.SetToolNormalBitmap(self.ON_PAUSE, self.play_bm)
00112 else:
00113 self.SetToolNormalBitmap(self.ON_PAUSE, self.pause_bm)
00114
00115 try:
00116 self.ToggleTool(self.wx_ids['Pan'], False)
00117 self.ToggleTool(self.wx_ids['Zoom'], False)
00118 except AttributeError:
00119 self.ToggleTool(self._NTB2_PAN, False)
00120 self.ToggleTool(self._NTB2_ZOOM, False)
00121 self._active = 'ZOOM'
00122 self.zoom(tuple())
00123
00124 def _on_stop(self, evt):
00125 set_ros_stop()
00126 self._set_tb_enable()
00127
00128 self.EnableTool(self.ON_PAUSE, False)
00129 self.EnableTool(self.ON_STOP, False)
00130
00131
00132 class RxPlotFrame(wx.Frame):
00133 def __init__(self, topics, options):
00134 self.title = options.title
00135 self.legend = options.legend
00136 self.mode = options.mode
00137 self.miny = options.miny
00138 self.maxy = options.maxy
00139 if self.mode == '2d':
00140 self.draw_plot = self.draw_plot_2d
00141 self.init_plot = self.init_plot_2d
00142 elif self.mode in ['3d', 'scatter']:
00143 self.draw_plot = self.draw_plot_3d
00144 self.init_plot = self.init_plot_3d
00145 else:
00146 raise ValueError("Mode must be one of [2d, 3d, scatter], not [%s]"%(options.mode))
00147
00148 wx.Frame.__init__(self, None, -1, self.title)
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165 self.axes = []
00166 self.topics = topics
00167 self.period = options.period
00168 self.marker = options.marker
00169
00170
00171
00172 self.datagen = []
00173 self.datax = []
00174 self.datay = []
00175 self.buffer_size = options.buffer
00176 self.redraw_period = int(1. / options.refresh_rate * 1000)
00177
00178 self.start_time = rospy.get_time()
00179 for topic_list in topics:
00180 for t in topic_list:
00181 dg = ROSData(t, self.start_time)
00182 self.datagen.append(dg)
00183 datax, datay = dg.next()
00184 self.datax.append(collections.deque(datax))
00185 self.datay.append(collections.deque(datay))
00186
00187 self.create_menu()
00188 self.statusbar = self.CreateStatusBar()
00189 self.create_main_canvas()
00190
00191 self.redraw_timer = wx.Timer(self)
00192 self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer)
00193
00194
00195 self.redraw_timer.Start(self.redraw_period)
00196 if options.start_paused:
00197 self.toolbar._on_pause(None)
00198
00199 def create_menu(self):
00200 self.menubar = wx.MenuBar()
00201
00202 menu_file = wx.Menu()
00203 m_expt = menu_file.Append(-1, "&Save plot\tCtrl-S", "Save plot to file")
00204 self.Bind(wx.EVT_MENU, self.on_save_plot, m_expt)
00205 menu_file.AppendSeparator()
00206 m_exit = menu_file.Append(-1, "E&xit\tCtrl-X", "Exit")
00207 self.Bind(wx.EVT_MENU, self.on_exit, m_exit)
00208
00209 self.menubar.Append(menu_file, "&File")
00210 self.SetMenuBar(self.menubar)
00211
00212 def onpick(self, event):
00213 thisline = event.artist
00214 xdata, ydata = thisline.get_data()
00215 ind = event.ind
00216 self.statusbar.SetStatusText("%s, %s" % (xdata[ind][len(xdata[ind])/2], ydata[ind][len(ydata[ind])/2]))
00217
00218 def create_main_canvas(self):
00219
00220 self.dpi = 100
00221 params = matplotlib.figure.SubplotParams(left=0.125, bottom=0.12, right=0.99, top=0.99, wspace=0.001, hspace=0.1)
00222 self.fig = Figure((3.0, 3.0), dpi=self.dpi, subplotpars=params)
00223
00224 self.canvas = FigCanvas(self, -1, self.fig)
00225 self.canvas.mpl_connect('pick_event', self.onpick)
00226
00227 self.init_plot()
00228
00229 self.sizer = wx.BoxSizer(wx.VERTICAL)
00230 self.sizer.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW)
00231 self.SetSizer(self.sizer)
00232
00233 self.add_toolbar()
00234
00235 self.SetSize(wx.Size(500, 700))
00236 self.Layout()
00237
00238 def init_plot_2d(self):
00239 self.plot_data = []
00240 flat_topics = []
00241
00242 axes_index = 0
00243 plot_index = 0
00244
00245 for topic_list in self.topics:
00246 axes = self.fig.add_subplot(string.atoi('%d1%d'%(len(self.topics), axes_index+1)))
00247 axes.set_axis_bgcolor('white')
00248
00249 pylab.setp(axes.get_xticklabels(), fontsize=6)
00250 pylab.setp(axes.get_yticklabels(), fontsize=8)
00251
00252 self.axes.append(axes)
00253 axes_index += 1
00254
00255 for topic in topic_list:
00256 flat_topics.append(topic)
00257 plot_data = \
00258 axes.plot(self.datax[plot_index],
00259 self.datay[plot_index],
00260 marker=self.marker,
00261 linewidth=1,
00262 picker=5,
00263 color=COLORS[plot_index],
00264 )[0]
00265
00266 self.plot_data.append(plot_data)
00267 plot_index += 1
00268
00269
00270 for ax in self.axes:
00271 ax.grid(True, color='gray')
00272 pylab.setp(ax.get_xticklabels(), visible=True)
00273
00274
00275 fp = matplotlib.font_manager.FontProperties(size=8)
00276 legends = self.legend.split(",") if self.legend else []
00277 if legends:
00278 if len(legends) != len(flat_topics):
00279 raise "Number of legend strings does not match the number of topics"
00280 self.fig.legend(self.plot_data, legends, 'lower right', prop=fp)
00281 elif len(flat_topics) > 1:
00282 self.fig.legend(self.plot_data, flat_topics, 'lower right', prop=fp)
00283
00284 def init_plot_3d(self):
00285
00286 self.plot_data = []
00287
00288 import random
00289 self.color = COLORS[random.randint(0, len(COLORS))]
00290
00291
00292 import mpl_toolkits.mplot3d.axes3d
00293 self.ax = mpl_toolkits.mplot3d.axes3d.Axes3D(self.fig)
00294
00295 flat_topics = []
00296 for topic_list in self.topics:
00297 for topic in topic_list:
00298 flat_topics.append(topic)
00299
00300 ntopics = len(flat_topics)
00301 if ntopics <= 3:
00302 self.ax.set_xlabel(flat_topics[0])
00303 self.ax.set_ylabel(flat_topics[1])
00304 if ntopics == 2:
00305 self.ax.set_zlabel("time")
00306 elif ntopics == 3:
00307 self.ax.set_zlabel(flat_topics[2])
00308 else:
00309 raise Exception("Expected 2 or 3 topics, but got %d" % (ntopics))
00310
00311
00312 self.ax.mouse_init()
00313
00314 def draw_plot_2d(self, relimit=False):
00315 if not self.plot_data:
00316 return
00317
00318
00319 if relimit and self.datax[0]:
00320 axes_index = 0
00321 plot_index = 0
00322
00323 for topic_list in self.topics:
00324 axes = self.axes[axes_index]
00325 axes_index += 1
00326 ymin = self.miny
00327 ymax = self.maxy
00328 for t in topic_list:
00329 datax = self.datax[plot_index]
00330 datay = self.datay[plot_index]
00331 plot_index += 1
00332
00333 xmax = datax[-1]
00334 xmin = xmax - self.period
00335 if ymin==0 and ymax==0:
00336 ymin = min(datay)
00337 ymax = max(datay)
00338 else:
00339 ymin = min(min(datay), ymin)
00340 ymax = max(max(datay), ymax)
00341
00342
00343 delta = ymax - ymin
00344
00345 if delta == 0:
00346 delta = ymax
00347 ymin -= .05*delta
00348 ymax += .05*delta
00349
00350 axes.set_xbound(lower=xmin, upper=xmax)
00351 axes.set_ybound(lower=ymin, upper=ymax)
00352
00353
00354 for plot_index in xrange(0, len(self.plot_data)):
00355 datax = self.datax[plot_index]
00356 datay = self.datay[plot_index]
00357
00358 plot_data = self.plot_data[plot_index]
00359 plot_data.set_data(np.array(datax), np.array(datay))
00360
00361 self.canvas.draw()
00362
00363 def draw_plot_3d(self, relimit=False):
00364
00365 if self.datax[0] and self.datay[0] and self.datay[1]:
00366
00367 ndata = len(self.datay)
00368 if ndata >=2:
00369 datax = np.array(self.datay[0])
00370 datay = np.array(self.datay[1])
00371
00372
00373 if ndata == 2:
00374 dataz = np.array(self.datax[0])
00375 elif ndata == 3 and self.datay[2]:
00376 dataz = np.array(self.datay[2])
00377 else:
00378 print "unexpected input data dimensions for 3d plotting (%d)" % ndata
00379
00380 if self.mode == '3d':
00381 if not self.plot_data:
00382 self.plot_data = self.ax.plot(datax, datay, dataz)
00383
00384 self.plot_data[0].set_data(datax, datay)
00385 art3d.line_2d_to_3d(self.plot_data[0], zs=dataz, zdir='z')
00386 self.ax.auto_scale_xyz(datax, datay, dataz, True)
00387 else:
00388
00389
00390
00391
00392
00393
00394 self.plot_data = self.ax.scatter(datax, datay, dataz, color=self.color)
00395
00396 self.canvas.draw()
00397
00398 def on_save_plot(self, event):
00399 file_choices = "PNG (*.png)|*.png"
00400
00401 dlg = wx.FileDialog(
00402 self,
00403 message="Save plot as...",
00404 defaultDir=os.getcwd(),
00405 defaultFile="plot.png",
00406 wildcard=file_choices,
00407 style=wx.SAVE)
00408
00409 if dlg.ShowModal() == wx.ID_OK:
00410 path = dlg.GetPath()
00411 self.canvas.print_figure(path, dpi=self.dpi)
00412 self.flash_status_message("Saved to %s" % path)
00413
00414 def on_redraw_timer(self, event):
00415
00416
00417
00418 if is_ros_stop():
00419 for dg in self.datagen:
00420 dg.close()
00421 self.datagen = []
00422
00423 if not is_ros_pause():
00424 for plot_index in xrange(0, len(self.datagen)):
00425 try:
00426 datax, datay = self.datagen[plot_index].next()
00427 except Exception, e:
00428 print >> sys.stderr, str(e)
00429 wx.GetApp().Exit()
00430 return
00431
00432 plot_datax, plot_datay = self.datax[plot_index], self.datay[plot_index]
00433 plot_datax.extend(datax)
00434 plot_datay.extend(datay)
00435
00436 if len(plot_datax) > 0 and self.buffer_size > 0:
00437 xcutoff = plot_datax[-1] - self.buffer_size
00438 while len(plot_datax) > 0 and plot_datax[0] < xcutoff:
00439 plot_datax.popleft()
00440 plot_datay.popleft()
00441
00442 self.draw_plot(relimit=True)
00443
00444 def on_exit(self, event):
00445 self.Destroy()
00446
00447 def flash_status_message(self, msg, flash_len_ms=1500):
00448 self.statusbar.SetStatusText(msg)
00449 self.timeroff = wx.Timer(self)
00450 self.Bind(
00451 wx.EVT_TIMER,
00452 self.on_flash_status_off,
00453 self.timeroff)
00454 self.timeroff.Start(flash_len_ms, oneShot=True)
00455
00456 def on_flash_status_off(self, event):
00457 self.statusbar.SetStatusText('')
00458
00459 def add_toolbar(self):
00460 self.toolbar = RxPlotToolbar(self.canvas)
00461 self.toolbar.Realize()
00462 self.SetToolBar(self.toolbar)
00463 if 0:
00464 if wx.Platform == '__WXMAC__':
00465
00466
00467
00468 self.SetToolBar(self.toolbar)
00469 else:
00470
00471
00472 tw, th = self.toolbar.GetSizeTuple()
00473 fw, fh = self.canvas.GetSizeTuple()
00474
00475
00476
00477 self.toolbar.SetSize(wx.Size(fw, th))
00478 self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
00479
00480 self.toolbar.update()
00481
00482
00483
00484 def rxplot_app(topic_list, options):
00485 rospy.init_node('rxplot', anonymous=True)
00486
00487 try:
00488 app = wx.PySimpleApp()
00489 app.frame = rxtools.rxplot.RxPlotFrame(topic_list, options)
00490 app.frame.Show()
00491 app.MainLoop()
00492 except Exception, e:
00493 rospy.logerr(e)
00494 print >> sys.stderr, str(e)
00495 wx.GetApp().Exit()
00496
00497 rospy.signal_shutdown('GUI shutdown')
00498