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 PKG = 'rxbag_plugins'
00034 import roslib; roslib.load_manifest(PKG)
00035 import rospy
00036
00037 import bisect
00038 import csv
00039 import sys
00040 import time
00041
00042 import Image
00043 import numpy
00044 import pylab
00045 import wx
00046 import wx.lib.wxcairo
00047
00048 from rxbag import TopicMessageView
00049
00050 from chart import Chart
00051 from plot_configure_frame import PlotConfigureFrame
00052 from plot_data_loader import PlotDataLoader
00053 import image_helper
00054
00055 class PlotView(TopicMessageView):
00056 name = 'Plot'
00057
00058 rows = [( 1, 'All'),
00059 ( 2, 'Every 2nd message'),
00060 ( 3, 'Every 3rd message'),
00061 ( 4, 'Every 4th message'),
00062 ( 5, 'Every 5th message'),
00063 ( 10, 'Every 10th message'),
00064 ( 20, 'Every 20th message'),
00065 ( 50, 'Every 50th message'),
00066 ( 100, 'Every 100th message'),
00067 (1000, 'Every 1000th message')]
00068
00069 def __init__(self, timeline, parent):
00070 TopicMessageView.__init__(self, timeline, parent)
00071
00072 self._topic = None
00073 self._message = None
00074 self._plot_paths = []
00075 self._playhead = None
00076 self._chart = Chart()
00077 self._data_loader = None
00078 self._x_view = None
00079 self._dirty_count = 0
00080 self._csv_data_loader = None
00081 self._csv_path = None
00082 self._csv_row_stride = None
00083
00084 self._clicked_pos = None
00085 self._dragged_pos = None
00086
00087 self._configure_frame = None
00088
00089 self._max_interval_pixels = 1.0
00090
00091 tb = self.parent.ToolBar
00092 icons_dir = roslib.packages.get_pkg_dir(PKG) + '/icons/'
00093 tb.AddSeparator()
00094 tb.Bind(wx.EVT_TOOL, lambda e: self.configure(), tb.AddLabelTool(wx.ID_ANY, '', wx.Bitmap(icons_dir + 'cog.png')))
00095
00096 self.parent.Bind(wx.EVT_SIZE, self._on_size)
00097 self.parent.Bind(wx.EVT_PAINT, self._on_paint)
00098 self.parent.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
00099 self.parent.Bind(wx.EVT_MIDDLE_DOWN, self._on_middle_down)
00100 self.parent.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down)
00101 self.parent.Bind(wx.EVT_LEFT_UP, self._on_left_up)
00102 self.parent.Bind(wx.EVT_MIDDLE_UP, self._on_middle_up)
00103 self.parent.Bind(wx.EVT_RIGHT_UP, self._on_right_up)
00104 self.parent.Bind(wx.EVT_MOTION, self._on_mouse_move)
00105 self.parent.Bind(wx.EVT_MOUSEWHEEL, self._on_mousewheel)
00106 self.parent.Bind(wx.EVT_CLOSE, self._on_close)
00107
00108 wx.CallAfter(self.configure)
00109
00110
00111
00112 def message_viewed(self, bag, msg_details):
00113 TopicMessageView.message_viewed(self, bag, msg_details)
00114
00115 topic, msg, t = msg_details
00116
00117 if not self._data_loader:
00118 self._topic = topic
00119 self.start_loading()
00120
00121 self._message = msg
00122
00123 self.playhead = (t - self.timeline.start_stamp).to_sec()
00124
00125 def message_cleared(self):
00126 self._message = None
00127
00128 TopicMessageView.message_cleared(self)
00129
00130 wx.CallAfter(self.parent.Refresh)
00131
00132 def timeline_changed(self):
00133
00134 if self._x_view is not None:
00135 end_elapsed = (self.timeline.end_stamp - self.timeline.start_stamp).to_sec()
00136 if end_elapsed > self._x_view[0] and end_elapsed < self._x_view[1] and self._data_loader:
00137 self._data_loader.invalidate()
00138
00139 wx.CallAfter(self.parent.Refresh)
00140
00141
00142
00143 def _get_plot_paths(self): return self._plot_paths
00144
00145 def _set_plot_paths(self, plot_paths):
00146 self._plot_paths = plot_paths
00147
00148
00149 if self._data_loader:
00150 paths = []
00151 for plot in self._plot_paths:
00152 for path in plot:
00153 if path not in paths:
00154 paths.append(path)
00155
00156 self._data_loader.paths = paths
00157
00158
00159 self._chart.create_areas(self._plot_paths)
00160
00161 self._update_max_interval()
00162
00163 wx.CallAfter(self.parent.Refresh)
00164
00165 plot_paths = property(_get_plot_paths, _set_plot_paths)
00166
00167
00168
00169 def _get_playhead(self): return self._playhead
00170
00171 def _set_playhead(self, playhead):
00172 self._playhead = playhead
00173
00174
00175 if self._x_view is not None:
00176 if self._playhead < self._x_view[0]:
00177 x_view = self._x_view[1] - self._x_view[0]
00178 self._x_view = (self._playhead, self._playhead + x_view)
00179 self._update_data_loader_interval()
00180
00181 elif self._playhead > self._x_view[1]:
00182 x_view = self._x_view[1] - self._x_view[0]
00183 self._x_view = (self._playhead - x_view, self._playhead)
00184 self._update_data_loader_interval()
00185
00186 wx.CallAfter(self.parent.Refresh)
00187
00188 playhead = property(_get_playhead, _set_playhead)
00189
00190 def _update_max_interval(self):
00191 if not self._data_loader:
00192 return
00193
00194 if len(self._chart.areas) > 0:
00195 secs_per_px = (self._data_loader.end_stamp - self._data_loader.start_stamp).to_sec() / self._chart._width
00196 self._data_loader.max_interval = secs_per_px * self._max_interval_pixels
00197
00198
00199
00200 def _on_paint(self, event):
00201 if not self._data_loader or len(self._chart._areas) == 0:
00202 return
00203
00204 self._update_chart_info()
00205
00206 dc = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self.parent))
00207
00208 self._chart.paint(dc)
00209
00210 def _update_chart_info(self):
00211 for area_index, plot in enumerate(self._plot_paths):
00212 area = self._chart.areas[area_index]
00213
00214 area.x_view = self._x_view
00215
00216 if self._message is not None:
00217 area.x_indicator = self._playhead
00218 else:
00219 area.x_indicator = None
00220
00221 area.x_range = (0.0, (self.timeline.end_stamp - self.timeline.start_stamp).to_sec())
00222
00223 if self._data_loader.is_load_complete:
00224 area.data_alpha = 1.0
00225 else:
00226 area.data_alpha = 0.5
00227
00228 area._series_list = plot
00229
00230 data = {}
00231 for plot_path in plot:
00232 if plot_path in self._data_loader._data:
00233 data[plot_path] = self._data_loader._data[plot_path]
00234 area._series_data = data
00235
00236 def _on_size(self, event):
00237 self._chart.set_size(self.parent.ClientSize)
00238
00239 self._update_max_interval()
00240
00241 def _on_left_down(self, event):
00242 self._clicked_pos = self._dragged_pos = event.Position
00243 if len(self._chart.areas) > 0 and self._chart.areas[0].view_min_x is not None and self._chart.areas[0].view_max_x is not None:
00244 self.timeline.playhead = self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._chart.areas[0].x_chart_to_data(event.Position[0])))
00245
00246 def _on_middle_down(self, event):
00247 self._clicked_pos = self._dragged_pos = event.Position
00248
00249 def _on_right_down(self, event):
00250 self._clicked_pos = self._dragged_pos = event.Position
00251 self.parent.PopupMenu(PlotPopupMenu(self.parent, self), self._clicked_pos)
00252
00253 def _on_left_up (self, event): self._on_mouse_up(event)
00254 def _on_middle_up(self, event): self._on_mouse_up(event)
00255 def _on_right_up (self, event): self._on_mouse_up(event)
00256
00257 def _on_mouse_up(self, event): self.parent.Cursor = wx.StockCursor(wx.CURSOR_ARROW)
00258
00259 def _on_mouse_move(self, event):
00260 x, y = event.Position
00261
00262 if event.Dragging():
00263 if event.MiddleIsDown() or event.ShiftDown():
00264
00265
00266 dx_click, dy_click = x - self._clicked_pos[0], y - self._clicked_pos[1]
00267 dx_drag, dy_drag = x - self._dragged_pos[0], y - self._dragged_pos[1]
00268
00269 if dx_drag != 0 and len(self._chart.areas) > 0:
00270 dsecs = self._chart.areas[0].dx_chart_to_data(dx_drag)
00271 self._translate_plot(dsecs)
00272
00273 if dy_drag != 0:
00274 self._zoom_plot(1.0 + 0.005 * dy_drag)
00275
00276 self._update_data_loader_interval()
00277
00278 wx.CallAfter(self.parent.Refresh)
00279
00280 self.parent.Cursor = wx.StockCursor(wx.CURSOR_HAND)
00281
00282 elif event.LeftIsDown():
00283 if len(self._chart.areas) > 0 and self._chart.areas[0].view_min_x is not None and self._chart.areas[0].view_max_x is not None:
00284 self.timeline.playhead = self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._chart.areas[0].x_chart_to_data(x)))
00285
00286 wx.CallAfter(self.parent.Refresh)
00287
00288 self._dragged_pos = event.Position
00289
00290 def _on_mousewheel(self, event):
00291 dz = event.WheelRotation / event.WheelDelta
00292 self._zoom_plot(1.0 - dz * 0.2)
00293
00294 self._update_data_loader_interval()
00295
00296 wx.CallAfter(self.parent.Refresh)
00297
00298 def _on_close(self, event):
00299 if self._configure_frame:
00300 self._configure_frame.Close()
00301
00302 self.stop_loading()
00303
00304 event.Skip()
00305
00306
00307
00308 def _update_data_loader_interval(self):
00309 self._data_loader.set_interval(self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._x_view[0])),
00310 self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._x_view[1])))
00311
00312 def _zoom_plot(self, zoom):
00313 if self._x_view is None:
00314 self._x_view = (0.0, (self.timeline.end_stamp - self.timeline.start_stamp).to_sec())
00315
00316 x_view_interval = self._x_view[1] - self._x_view[0]
00317 if x_view_interval == 0.0:
00318 return
00319
00320 playhead_fraction = (self._playhead - self._x_view[0]) / x_view_interval
00321
00322 new_x_view_interval = zoom * x_view_interval
00323
00324
00325 max_zoom_interval = (self.timeline.end_stamp - self.timeline.start_stamp).to_sec() * 1.5
00326 if new_x_view_interval > max_zoom_interval:
00327 new_x_view_interval = max_zoom_interval
00328 elif new_x_view_interval < 0.1:
00329 new_x_view_interval = 0.1
00330
00331 interval_0 = self._playhead - playhead_fraction * new_x_view_interval
00332 interval_1 = interval_0 + new_x_view_interval
00333
00334 timeline_range = (self.timeline.end_stamp - self.timeline.start_stamp).to_sec()
00335 interval_0 = min(interval_0, timeline_range - 0.1)
00336 interval_1 = max(interval_1, 0.1)
00337
00338 self._x_view = (interval_0, interval_1)
00339
00340 self._update_max_interval()
00341
00342 def _translate_plot(self, dsecs):
00343 if self._x_view is None:
00344 self._x_view = (0.0, (self.timeline.end_stamp - self.timeline.start_stamp).to_sec())
00345
00346 new_start = self._x_view[0] - dsecs
00347 new_end = self._x_view[1] - dsecs
00348
00349 timeline_range = (self.timeline.end_stamp - self.timeline.start_stamp).to_sec()
00350 new_start = min(new_start, timeline_range - 0.1)
00351 new_end = max(new_end, 0.1)
00352
00353 self._x_view = (new_start, new_end)
00354
00355
00356
00357 def stop_loading(self):
00358 if self._data_loader:
00359 self._data_loader.stop()
00360 self._data_loader = None
00361
00362 def start_loading(self):
00363 if self._topic and not self._data_loader:
00364 self._data_loader = PlotDataLoader(self.timeline, self._topic)
00365 self._data_loader.add_progress_listener(self._data_loader_updated)
00366 self._data_loader.add_complete_listener(self._data_loader_complete)
00367 self._data_loader.start()
00368
00369 def _data_loader_updated(self):
00370 self._dirty_count += 1
00371 if self._dirty_count > 5:
00372 wx.CallAfter(self.parent.Refresh)
00373 self._dirty_count = 0
00374
00375 def _data_loader_complete(self):
00376 wx.CallAfter(self.parent.Refresh)
00377
00378 def configure(self):
00379 if self._configure_frame is not None or self._message is None:
00380 return
00381
00382 self._configure_frame = PlotConfigureFrame(self)
00383
00384 frame = self.parent.TopLevelParent
00385 self._configure_frame.Position = (frame.Position[0] + frame.Size[0] + 10, frame.Position[1])
00386 self._configure_frame.Show()
00387
00388
00389
00390 def export_csv(self, rows):
00391 dialog = wx.FileDialog(self.parent.Parent, 'Export to CSV...', wildcard='CSV files (*.csv)|*.csv', style=wx.FD_SAVE)
00392 if dialog.ShowModal() == wx.ID_OK:
00393 if self.timeline.start_background_task('Exporting to "%s"' % dialog.Path):
00394 wx.CallAfter(wx.GetApp().GetTopWindow().StatusBar.gauge.Show)
00395
00396 export_series = set()
00397 for plot in self._plot_paths:
00398 for path in plot:
00399 export_series.add(path)
00400
00401 if self._x_view is None:
00402 self._x_view = (0.0, (self.timeline.end_stamp - self.timeline.start_stamp).to_sec())
00403
00404 self._csv_path = dialog.Path
00405 self._csv_row_stride = rows
00406
00407 self._csv_data_loader = PlotDataLoader(self.timeline, self._topic)
00408 self._csv_data_loader.add_complete_listener(self._csv_data_loaded)
00409 self._csv_data_loader.paths = export_series
00410 self._csv_data_loader.set_interval(self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._x_view[0])),
00411 self.timeline.start_stamp + rospy.Duration.from_sec(max(0.01, self._x_view[1])))
00412 self._csv_data_loader.start()
00413
00414 dialog.Destroy()
00415
00416 def _csv_data_loaded(self):
00417
00418 i = 0
00419 series_dict = {}
00420 unique_stamps = set()
00421 for series in self._csv_data_loader._data:
00422 d = {}
00423 series_dict[series] = d
00424 point_num = 0
00425 for x, y in self._csv_data_loader._data[series].points:
00426 if point_num % self._csv_row_stride == 0:
00427 d[x] = y
00428 unique_stamps.add(x)
00429 point_num += 1
00430 i += 1
00431 series_columns = sorted(series_dict.keys())
00432
00433 try:
00434 csv_writer = csv.DictWriter(open(self._csv_path, 'w'), ['elapsed'] + series_columns)
00435
00436
00437 header_dict = { 'elapsed' : 'elapsed' }
00438 for column in series_columns:
00439 header_dict[column] = column
00440 csv_writer.writerow(header_dict)
00441
00442
00443 progress = 0
00444 def update_progress(v):
00445 wx.GetApp().TopWindow.StatusBar.progress = v
00446 total_stamps = len(unique_stamps)
00447 stamp_num = 0
00448
00449
00450 for stamp in sorted(unique_stamps):
00451 if self.timeline.background_task_cancel:
00452 break
00453
00454 row = { 'elapsed' : stamp }
00455 for column in series_dict:
00456 if stamp in series_dict[column]:
00457 row[column] = series_dict[column][stamp]
00458
00459 csv_writer.writerow(row)
00460
00461 new_progress = int(100.0 * (float(stamp_num) / total_stamps))
00462 if new_progress != progress:
00463 progress = new_progress
00464 wx.CallAfter(update_progress, progress)
00465
00466 stamp_num += 1
00467
00468 except Exception, ex:
00469 print >> sys.stderr, 'Error writing to CSV file: %s' % str(ex)
00470
00471
00472 if not self.timeline.background_task_cancel:
00473 wx.CallAfter(wx.GetApp().TopWindow.StatusBar.gauge.Hide)
00474
00475 self.timeline.stop_background_task()
00476
00477
00478
00479 def export_image(self):
00480 dialog = wx.FileDialog(self.parent.Parent, 'Save plot to...', wildcard='PNG files (*.png)|*.png', style=wx.FD_SAVE)
00481 if dialog.ShowModal() == wx.ID_OK:
00482 path = dialog.Path
00483
00484 bitmap = wx.EmptyBitmap(self.width, self.height)
00485 mem_dc = wx.MemoryDC()
00486 mem_dc.SelectObject(bitmap)
00487 mem_dc.SetBackground(wx.WHITE_BRUSH)
00488 mem_dc.Clear()
00489 cairo_dc = wx.lib.wxcairo.ContextFromDC(mem_dc)
00490 self.paint(cairo_dc)
00491 mem_dc.SelectObject(wx.NullBitmap)
00492
00493 bitmap.SaveFile(path, wx.BITMAP_TYPE_PNG)
00494
00495 dialog.Destroy()
00496
00497 class PlotPopupMenu(wx.Menu):
00498 def __init__(self, parent, plot):
00499 wx.Menu.__init__(self)
00500
00501 self.parent = parent
00502 self.plot = plot
00503
00504
00505 configure_item = wx.MenuItem(self, wx.NewId(), 'Configure...')
00506 self.AppendItem(configure_item)
00507 self.Bind(wx.EVT_MENU, lambda e: self.plot.configure(), id=configure_item.Id)
00508
00509
00510 export_image_item = wx.MenuItem(self, wx.NewId(), 'Export to PNG...')
00511 self.AppendItem(export_image_item)
00512 self.Bind(wx.EVT_MENU, lambda e: self.plot.export_image(), id=export_image_item.Id)
00513
00514
00515 self.export_csv_menu = wx.Menu()
00516 self.AppendSubMenu(self.export_csv_menu, 'Export to CSV...', 'Export data to CSV file')
00517
00518 for rows, label in plot.rows:
00519 rows_item = self.ExportCSVMenuItem(self.export_csv_menu, wx.NewId(), label, rows, plot)
00520 self.export_csv_menu.AppendItem(rows_item)
00521
00522 if label == 'All':
00523 self.export_csv_menu.AppendSeparator()
00524
00525 class ExportCSVMenuItem(wx.MenuItem):
00526 def __init__(self, parent, id, label, rows, plot):
00527 wx.MenuItem.__init__(self, parent, id, label)
00528
00529 self.rows = rows
00530 self.plot = plot
00531
00532 parent.Bind(wx.EVT_MENU, self._on_menu, id=self.Id)
00533
00534 def _on_menu(self, event):
00535 self.plot.export_csv(self.rows)