Package braun_ :: Module braun_gui

Source Code for Module braun_.braun_gui

  1  import sys 
  2   
  3  WXVER = '2.8' 
  4  WX_GE_2_8 = True 
  5   
  6  # We need to do this for py2app and py2exe builds 
  7  if not hasattr(sys, "frozen"): 
  8      import wxversion 
  9      if wxversion.checkInstalled(WXVER): 
 10          wxversion.select(WXVER) 
 11      # FIX: handle cases when version isn't available, see http://wiki.wxpython.org/index.cgi/MultiVersionInstalls 
 12      else: 
 13          import wx, webbrowser 
 14          app = wx.PySimpleApp() 
 15          retval = wx.MessageBox("This software is optimised for wxPython version %s, but you don't appear to have it installed.\nWould you like to install the latest version?" % WXVER, "wxPython Version Error", style = wx.YES_NO) 
 16          print "Return valur %s" % retval 
 17          if(retval == wx.YES): 
 18              app.MainLoop() 
 19              webbrowser.open("http://wxPython.org/") 
 20              sys.exit() 
 21          WX_GE_2_8 = False 
 22   
 23  import wx 
 24  import wx.html 
 25  import  wx.lib.newevent 
 26  import wx.lib.colourdb 
 27  import random 
 28   
 29  try: 
 30      import numpy as N 
 31      #N.seterr(invalid="raise") 
 32  except ImportError: 
 33      errorText = ( 
 34          "The FloatCanvas requires the numpy module, version 1.* \n\n" 
 35          "You can get info about it at:\n" 
 36          "http://numpy.scipy.org/\n\n" 
 37      ) 
 38   
 39  try: 
 40      from floatcanvas import NavCanvas, FloatCanvas 
 41  except ImportError: 
 42      from wx.lib.floatcanvas import NavCanvas, FloatCanvas 
 43   
 44  import thread 
 45  from mod_braun import * 
 46  from config import * 
 47  import braun_help 
 48   
 49  (UpdateLineEvent, EVT_UPDATE_GRAPH) = wx.lib.newevent.NewEvent() 
50 51 # Function taken from wxPython demo 52 -def opj(path):
53 """Convert paths to the platform-specific separator""" 54 import os 55 st = apply(os.path.join, tuple(path.split('/'))) 56 # HACK: on Linux, a leading / gets lost... 57 if path.startswith('/'): 58 st = '/' + st 59 return st
60 61 if 0:
62 - class BraunSplashScreen(wx.SplashScreen):
63 - def __init__(self, MainWindowClass, queue):
64 self.queue = queue 65 self.MainWindowClass = MainWindowClass 66 bmp = wx.Image(opj(SPLASH_PATH)).ConvertToBitmap() 67 style = wx.SPLASH_CENTRE_ON_SCREEN | wx.SPLASH_TIMEOUT 68 wx.SplashScreen.__init__(self, bmp, style, 3000, None, -1) 69 self.Bind(wx.EVT_CLOSE, self.OnClose) 70 wx.Yield() 71 self.ShowMain()
72
73 - def OnClose(self, evt):
74 self.Hide() 75 evt.Skip()
76
77 - def ShowMain(self):
78 frame = self.MainWindowClass(self.queue) 79 frame.Show()
80
81 -class BraunHelp(wx.Dialog):
82 "Class to display some HTML documentation in a dialog window" 83 text = braun_help.TEXT
84 - def __init__(self, parent):
85 wx.Dialog.__init__(self, parent, -1, 'Braun help') 86 html = wx.html.HtmlWindow(self, -1, size=(420, -1)) 87 py_version = sys.version.split()[0] 88 html.SetPage(self.text) 89 btn = html.FindWindowById(wx.ID_OK) 90 ir = html.GetInternalRepresentation() 91 html.SetSize( (ir.GetWidth()+25, ir.GetHeight()+25) ) 92 self.SetClientSize(html.GetSize()) 93 self.CentreOnParent(wx.BOTH)
94
95 -class SmoothLine(FloatCanvas.Line):
96 """ 97 98 The SmoothLine class is identical to the Line class except that it uses a 99 GC rather than a DC. 100 101 """
102 - def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
103 Points = WorldToPixel(self.Points) 104 GC = wx.GraphicsContext.Create(dc) 105 c = wx.Color() 106 c.SetFromName(self.LineColor) 107 r,g,b = c.Get() 108 c = wx.Color(r,g,b,LINE_OPACITY) 109 GC.SetPen(wx.Pen(c, self.LineWidth, self.LineStyleList[self.LineStyle])) 110 GC.DrawLines(Points)
111
112 -class RingBuffer:
113 _highest_index = 0
114 - def __init__(self):
115 self.index = 0 116 self.size = RING_BUFFER_SIZE 117 self.current_value_index = RING_BUFFER_SIZE - 1 118 self.changed = False 119 self.started = False 120 self.__content = [0] * self.size
121 - def append(self, value):
122 self.index = (self.index + 1) % self.size 123 # FIX: race conditions 124 self.current_value_index = self.index 125 self.started = True 126 self.changed = True 127 self.__content[self.index] = value 128 self.set_highest_index()
129 - def maintain(self):
130 self.current_value = self.__content[self.index] 131 self.index = (self.index + 1) % self.size 132 # FIX: race conditions 133 self.__content[self.index] = self.current_value
134 - def maintain_as_current(self):
135 self.current_value = self.__content[self.index] 136 self.index = (self.index + 1) % self.size 137 self.current_value_index = self.index 138 self.__content[self.index] = self.current_value
139 - def maintain_as_other(self):
140 self.current_value = self.__content[self.index] 141 while self.index != self.highest_index(): 142 self.index = (self.index + 1) % self.size 143 self.current_value_index = self.index 144 self.__content[self.index] = self.current_value 145 self.set_highest_index()
146 - def insert(self, value):
147 self.__content.insert(0, value) 148 self.__content.pop()
149 - def current_index(self):
150 return self.current_value_index
151 - def highest_index(self):
153 - def set_highest_index(self):
154 RingBuffer._highest_index = self.index
155 156 # FIX: race conditions here (dump could be called when content is too long)
157 - def dump(self):
158 return self.__content
159
160 -class Dispatcher(Receiver):
161 - def __init__(self, queue, frame):
162 Receiver.__init__(self, queue) 163 self.frame = frame
164 - def dispatch(self):
165 event = UpdateLineEvent(id = self.id, value = self.value,\ 166 realvalue = self.realvalue) 167 wx.PostEvent(self.frame, event)
168
169 -class MainAppWindow(wx.Frame):
170 '''Standard application main window with file, view and help menus'''
171 - def __init__(self, *args, **kwargs):
172 wx.Frame.__init__(self, style = wx.DEFAULT_FRAME_STYLE, *args, **kwargs) 173 self.SetExtraStyle(wx.FRAME_EX_METAL) 174 self.CreateStatusBar() 175 176 filemenu = wx.Menu() 177 helpmenu = wx.Menu() 178 viewmenu = wx.Menu() 179 180 filemenu.Append(wx.ID_SAVEAS, 181 "Save &As...\tctrl-s", "Save as PNG") 182 filemenu.Append(wx.ID_EXIT, 183 "E&xit\tctrl-q", "Terminate the program") 184 185 helpmenu.Append(wx.ID_ABOUT, "&About Braun...") 186 helpmenu.Append(wx.ID_HELP, "&Help\tctrl-?", "Help using Braun") 187 viewmenu.Append(wx.ID_ZOOM_IN, "Zoom in\tctrl-+") 188 viewmenu.Append(wx.ID_ZOOM_OUT, "Zoom out\tctrl--") 189 viewmenu.Append(ID_ZOOM_TO_FIT, "Zoom to fit\tctrl-0") 190 191 menubar = wx.MenuBar() 192 menubar.Append(filemenu, "&File") 193 menubar.Append(viewmenu, "&View") 194 menubar.Append(helpmenu, "&Help") 195 self.SetMenuBar(menubar) 196 197 wx.EVT_MENU(self, wx.ID_ZOOM_IN, self.on_zoom_in) 198 wx.EVT_MENU(self, wx.ID_ZOOM_OUT, self.on_zoom_out) 199 wx.EVT_MENU(self, ID_ZOOM_TO_FIT, self.on_zoom_to_fit) 200 wx.EVT_MENU(self, wx.ID_ABOUT, self.on_about) 201 wx.EVT_MENU(self, wx.ID_HELP, self.on_help) 202 wx.EVT_MENU(self, wx.ID_CLOSE, self.on_close) 203 wx.EVT_MENU(self, wx.ID_SAVEAS, self.on_saveas) 204 wx.EVT_MENU(self, wx.ID_EXIT, self.on_exit)
205
206 - def on_about(self, event):
207 d = wx.MessageDialog(self, 208 "Copyright Jamie Bullock 2007.\nLicensed under the GNU GPL version 3 or later", "About Braun %s" % VERSION, wx.OK) 209 d.ShowModal() 210 d.Destroy()
211
212 - def on_help(self, event):
213 helptext = "" 214 d = BraunHelp(None) 215 d.ShowModal() 216 d.Destroy()
217
218 - def on_close(self, event):
219 self.exit()
220
221 - def exit(self):
222 self.Destroy()
223
224 -class BraunMainWindow(MainAppWindow):
225 - def __init__(self, queue):
226 MainAppWindow.__init__(self, None, wx.ID_ANY, title="Braun", 227 size=(800,600)) 228 self.queue = queue 229 self.fps = FPS 230 self.clock = None 231 self.lines = [None] * MAX_LINES 232 self.data = [None] * MAX_LINES 233 self.text_ids = [None] * MAX_LINES 234 self.text_names = [None] * MAX_LINES 235 self.text_values = [None] * MAX_LINES 236 self.real_values = [0.0] * MAX_LINES 237 self.Canvas = None 238 self.done_first_draw = False 239 self.tics = [] 240 self.tic_values = [0] * N_TICS 241 self.redraw_tics = False 242 self.redraw_values = False 243 self.tic_max = GLOBAL_MAXIMUM 244 self.tic_min = GLOBAL_MINIMUM 245 self.do_resize = True 246 self.other_frame = False 247 248 wx.lib.colourdb.updateColourDB() 249 colours = wx.lib.colourdb.getColourList() 250 colours.remove('BLACK') 251 self.my_colour_list = ['TOMATO1'] 252 for colour in self.my_colour_list: 253 colours.remove(colour) 254 for i, colour in enumerate(colours): 255 if i % 22 == 0 and 'GREY' not in colour: 256 self.my_colour_list.append(colour) 257 assert len(self.my_colour_list) >= MAX_LINES 258 259 self.buffers = [RingBuffer() for i in range(MAX_LINES)] 260 261 self.Bind(EVT_UPDATE_GRAPH, self.on_update) 262 Dispatcher(queue, self).start() 263 FloatCanvas.FloatCanvas.OnSize = self.on_size 264 265 NC = NavCanvas.NavCanvas(self,wx.ID_ANY,(500,300), 266 ProjectionFun = None, 267 Debug = 0, 268 BackgroundColor = "BLACK" 269 ) 270 271 self.zoom_factor = 1.0 272 self.Canvas = NC.Canvas 273 self.Canvas.NumBetweenBlits = 1000 274 275 tb = NC.ToolBar 276 StopButton = wx.Button(tb, wx.ID_ANY, "Stop") 277 PlayButton = wx.Button(tb, wx.ID_ANY, "Run") 278 tb.AddSeparator() 279 tb.AddControl(StopButton) 280 tb.AddControl(PlayButton) 281 tb.Realize() 282 283 StopButton.Bind(wx.EVT_BUTTON, self.on_stop) 284 PlayButton.Bind(wx.EVT_BUTTON, self.on_run) 285 #FIX: this is a workaround for a bug (see also Chris Barker's note in 286 # the NavCanvas source 287 wx.CallAfter(self.hide_show_hack, StopButton) 288 wx.CallAfter(self.hide_show_hack, PlayButton) 289 290 self.Show(True) 291 self.Bind(wx.EVT_TIMER, self.on_timer) 292 self.clock = wx.Timer(self) 293 self.delay = 1000 / self.fps 294 self.run_braun()
295
296 - def hide_show_hack(self, object):
297 #FIX 298 "This is a complete hack to work around a bug on OS-X" 299 object.Hide() 300 object.Show()
301
302 - def on_saveas(self, event=None):
303 import os 304 dlg = wx.FileDialog( 305 self, message="Save as PNG ...", defaultDir=os.getcwd(), 306 defaultFile="", wildcard="*.png", style=wx.SAVE 307 ) 308 if dlg.ShowModal() == wx.ID_OK: 309 path = dlg.GetPath() 310 if not(path[-4:].lower() == ".png"): 311 path = path+".png" 312 self.Canvas.SaveAsImage(path)
313
314 - def on_exit(self, event):
315 self.exit()
316
317 - def on_timer(self, event):
318 for i,buffer in enumerate(self.buffers): 319 data = buffer.dump() 320 self.data[i][:,1] = movavg(data, SMOOTHING) 321 self.do_resize_zoom() 322 self.draw()
323
324 - def on_stop(self, event):
325 if self.clock is not None: 326 self.clock.Stop()
327
328 - def on_run(self, event):
329 self.redraw_values = True 330 self.run_braun()
331
332 - def on_size(self, event):
333 "Used to override FloatCanvas.OnSize" 334 if self.Canvas is not None: 335 self.Canvas.InitializePanel() 336 self.Canvas.SizeTimer.Start(50, oneShot=True) 337 self.Canvas.ZoomToBB()
338
339 - def on_zoom_to_fit(self, event):
340 self.Canvas.ZoomToBB()
341
342 - def on_zoom_in(self, event):
343 self.zoom_factor = 1.1 344 self.do_zoom()
345
346 - def on_zoom_out(self, event):
347 self.zoom_factor = 0.9 348 self.do_zoom()
349
350 - def on_update(self, event):
351 self.buffers[event.id].append(event.value) 352 for i,buffer in enumerate(self.buffers): 353 if i != event.id: 354 buffer.maintain_as_other() 355 self.real_values[event.id] = event.realvalue
356
357 - def do_zoom(self):
358 zoom = self.zoom_factor 359 self.Canvas.Zoom(zoom)
360
361 - def run_braun(self):
362 Canvas = self.Canvas 363 if self.done_first_draw: 364 Canvas.ClearAll(ResetBB=False) 365 self.redraw_tics = True 366 else: 367 Canvas.ClearAll() 368 self.draw_text() 369 self.draw_axis() 370 self.do_resize_zoom() 371 self.update_text() 372 smooth = SMOOTHING - 1 373 374 if not self.done_first_draw: 375 self.time = \ 376 GRAPH_WIDTH*N.pi*N.arange(RING_BUFFER_SIZE - \ 377 smooth)/float(RING_BUFFER_SIZE - smooth) 378 for i in range(MAX_LINES): 379 self.data[i] = N.zeros((RING_BUFFER_SIZE - smooth, 2)) 380 self.data[i][:,0] = self.time 381 382 for i,data in enumerate(self.data): 383 if self.buffers[i].started: 384 if WX_GE_2_8 and ANTIALIASED_LINES: 385 line = SmoothLine(data, 386 LineColor = self.my_colour_list[i], 387 LineWidth = LINE_WIDTH) 388 Canvas.AddObject(line) 389 self.lines[i] = line 390 else: 391 self.lines[i] = Canvas.AddLine(data, 392 LineColor = self.my_colour_list[i], 393 LineWidth = 1) 394 395 self.clock.Start(self.delay) 396 self.do_resize_zoom() 397 Canvas.Draw() 398 self.done_first_draw = True
399
400 - def do_resize_zoom(self):
401 if self.do_resize: 402 self.Canvas.ZoomToBB() 403 self.do_resize = False
404
405 - def every_other_frame(self):
406 if self.other_frame: 407 self.other_frame = False 408 return True 409 else: 410 self.other_frame = True 411 return False
412
413 - def draw(self):
414 #FIX: potential bug - we have to this before the for loop, because update_text() checks whether buffers[i].changed is True. We should probably do this differently! 415 if self.every_other_frame(): 416 self.update_text() 417 for i,buffer in enumerate(self.buffers): 418 if buffer.changed: 419 self.buffers[i].changed = False 420 else: 421 self.buffers[i].maintain_as_current() 422 if self.lines[i] is not None: 423 self.lines[i].SetPoints(self.data[i]) 424 elif buffer.started: 425 if WX_GE_2_8 and ANTIALIASED_LINES: 426 line = SmoothLine(self.data[i], 427 LineColor = self.my_colour_list[i], 428 LineWidth = LINE_WIDTH) 429 self.Canvas.AddObject(line) 430 self.lines[i] = line 431 else: 432 self.lines[i] = self.Canvas.AddLine(self.data[i], 433 LineColor = self.my_colour_list[i], 434 LineWidth = 1) 435 self.Canvas.Draw(Force=True)
436
437 - def draw_axis(self):
438 Canvas = self.Canvas 439 tic_x = SCALE_X_OFFSET 440 scale_multiplier = SCALE_MULTIPLIER * TIC_SCALE_MULTIPLIER 441 self.compute_tics() 442 self.tics = [] 443 for i, tic_value in enumerate(self.tic_values): 444 tic_pos = i * TIC_DISTANCE 445 tic_y = tic_pos * scale_multiplier + SCALE_Y_OFFSET 446 tic_value = compact_number(tic_value, Y_AXIS_NUMBER_PRECISION) 447 mytic = self.Canvas.AddScaledText(tic_value, 448 (tic_x, tic_y), 449 Size = FONT_SIZE, 450 Color = "Grey", 451 Position = 'cr') 452 self.tics.append(mytic) 453 454 # FIX: too many magic numbers in the code here. Need to convert to 455 # ratios to main rectangle or whatever 456 self.Canvas.AddRectangle((-0.3, -1.2 * SCALE_MULTIPLIER), 457 (GRAPH_WIDTH * N.pi + 1.5, 2.7 * SCALE_MULTIPLIER), 458 LineColor = None)
459
460 - def draw_text(self):
461 x = NAMESPACE_X_OFFSET 462 y = NAMESPACE_Y_OFFSET 463 for i in xrange(MAX_LINES): 464 Point = (x, y) 465 name = namespace[i][:10] 466 self.text_names[i] = \ 467 self.Canvas.AddScaledText("%s:" % name, Point, 468 Size = FONT_SIZE, Color = self.my_colour_list[i]) 469 x += NAMESPACE_NUMBER_PADDING 470 Point = (x, y) 471 self.text_values[i] = \ 472 self.Canvas.AddScaledText("", Point, Size = FONT_SIZE, 473 Color = self.my_colour_list[i]) 474 x += NAMESPACE_X_HOPSIZE
475
476 - def do_rescale(self, value):
477 "Rescale Y axis and return true if scale has changed" 478 scale_changed = False 479 if value > self.tic_max: 480 self.tic_max = value 481 scale_changed = True 482 if value < self.tic_min: 483 self.tic_min = value 484 scale_changed = True 485 return scale_changed
486
487 - def compute_tics(self):
488 tic_distance = (self.tic_max - self.tic_min) / float(N_TICS - 1) 489 for i in range(N_TICS): 490 self.tic_values[i] = (i * tic_distance)
491 492 @staticmethod
493 - def oversize_number(value):
494 if value > 10e3 or value < -10e3: 495 return True 496 else: 497 return False
498
499 - def update_text(self):
500 need_compute_tics = False 501 for i,value in enumerate(self.text_values): 502 real_value = self.real_values[i] 503 504 # Rescale the y-axis? 505 if not need_compute_tics: 506 need_compute_tics = self.do_rescale(real_value) 507 508 # Clip the OSC address 509 name = namespace[i][:8] 510 511 self.text_names[i].SetText("%s" % name) 512 if self.buffers[i].changed or self.redraw_values: 513 val = compact_number(real_value, 3) 514 value.SetText(val) 515 self.redraw_values = False 516 517 if need_compute_tics: 518 self.compute_tics() 519 520 for i, tic_value in enumerate(self.tic_values): 521 y = self.tic_values[i] 522 y = compact_number(y, Y_AXIS_NUMBER_PRECISION) 523 self.tics[i].SetText(y)
524
525 - def exit(self):
526 # FIX: we should wait for threads to finish here 527 Dispatcher(self.queue, self).stop() 528 self.Destroy()
529
530 -def movavg(s, n):
531 ''' returns an n period moving average for the time series s 532 533 s is a list ordered from oldest (index 0) to most recent (index -1) 534 n is an integer 535 536 returns a numeric array of the moving average 537 ''' 538 539 s = N.array(s) 540 c = N.cumsum(s) 541 return (c[n-1:] - c[:-n+1]) / float(n)
542
543 544 -def movavg1(s, n):
545 546 D = N.array(s) 547 S = D.size 548 A = N.zeros(S-(n-1), float) 549 for j in range(n): 550 A += D[j:S-(n-1)+j] 551 A /= float(n * .05) 552 553 return A
554
555 -def compact_number(number, digits):
556 "Return a number as a string compacted to a given number of digits" 557 import decimal 558 decimal.getcontext().prec = digits 559 x = decimal.Decimal(str(number)) 560 x += 0 561 return x.to_eng_string()
562