diff options
Diffstat (limited to 'gr-wxgui/src/python/plotter')
-rw-r--r-- | gr-wxgui/src/python/plotter/Makefile.am | 5 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/__init__.py | 1 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/bar_plotter.py | 144 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/channel_plotter.py | 127 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/common.py | 131 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/grid_plotter_base.py | 370 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/plotter_base.py | 418 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/waterfall_plotter.py | 107 |
8 files changed, 858 insertions, 445 deletions
diff --git a/gr-wxgui/src/python/plotter/Makefile.am b/gr-wxgui/src/python/plotter/Makefile.am index ada506794..d00f0a425 100644 --- a/gr-wxgui/src/python/plotter/Makefile.am +++ b/gr-wxgui/src/python/plotter/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2004,2005,2008 Free Software Foundation, Inc. +# Copyright 2004,2005,2008,2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -30,8 +30,11 @@ ourlibdir = $(grpyexecdir)/wxgui/plotter ourpython_PYTHON = \ __init__.py \ + bar_plotter.py \ channel_plotter.py \ + common.py \ gltext.py \ + grid_plotter_base.py \ plotter_base.py \ waterfall_plotter.py diff --git a/gr-wxgui/src/python/plotter/__init__.py b/gr-wxgui/src/python/plotter/__init__.py index 12f8b3450..616492a3e 100644 --- a/gr-wxgui/src/python/plotter/__init__.py +++ b/gr-wxgui/src/python/plotter/__init__.py @@ -21,3 +21,4 @@ from channel_plotter import channel_plotter from waterfall_plotter import waterfall_plotter +from bar_plotter import bar_plotter diff --git a/gr-wxgui/src/python/plotter/bar_plotter.py b/gr-wxgui/src/python/plotter/bar_plotter.py new file mode 100644 index 000000000..3f9259e9d --- /dev/null +++ b/gr-wxgui/src/python/plotter/bar_plotter.py @@ -0,0 +1,144 @@ +# +# Copyright 2009 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Radio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +import wx +from grid_plotter_base import grid_plotter_base +from OpenGL import GL +import common +import numpy + +LEGEND_TEXT_FONT_SIZE = 8 +LEGEND_BOX_PADDING = 3 +MIN_PADDING = 0, 0, 0, 70 #top, right, bottom, left +#constants for the waveform storage +SAMPLES_KEY = 'samples' +COLOR_SPEC_KEY = 'color_spec' +MARKERY_KEY = 'marker' +TRIG_OFF_KEY = 'trig_off' + +################################################## +# Bar Plotter for histogram waveforms +################################################## +class bar_plotter(grid_plotter_base): + + def __init__(self, parent): + """ + Create a new bar plotter. + """ + #init + grid_plotter_base.__init__(self, parent, MIN_PADDING) + self._bars = list() + self._bar_width = .5 + self._color_spec = (0, 0, 0) + #setup bar cache + self._bar_cache = self.new_gl_cache(self._draw_bars) + #setup bar plotter + self.register_init(self._init_bar_plotter) + + def _init_bar_plotter(self): + """ + Run gl initialization tasks. + """ + GL.glEnableClientState(GL.GL_VERTEX_ARRAY) + + def _draw_bars(self): + """ + Draw the vertical bars. + """ + bars = self._bars + num_bars = len(bars) + if num_bars == 0: return + #use scissor to prevent drawing outside grid + GL.glEnable(GL.GL_SCISSOR_TEST) + GL.glScissor( + self.padding_left, + self.padding_bottom+1, + self.width-self.padding_left-self.padding_right-1, + self.height-self.padding_top-self.padding_bottom-1, + ) + #load the points + points = list() + width = self._bar_width/2 + for i, bar in enumerate(bars): + points.extend([ + (i-width, 0), + (i+width, 0), + (i+width, bar), + (i-width, bar), + ] + ) + GL.glColor3f(*self._color_spec) + #matrix transforms + GL.glPushMatrix() + GL.glTranslatef(self.padding_left, self.padding_top, 0) + GL.glScalef( + (self.width-self.padding_left-self.padding_right), + (self.height-self.padding_top-self.padding_bottom), + 1, + ) + GL.glTranslatef(0, 1, 0) + GL.glScalef(1.0/(num_bars-1), -1.0/(self.y_max-self.y_min), 1) + GL.glTranslatef(0, -self.y_min, 0) + #draw the bars + GL.glVertexPointerf(points) + GL.glDrawArrays(GL.GL_QUADS, 0, len(points)) + GL.glPopMatrix() + GL.glDisable(GL.GL_SCISSOR_TEST) + + def _populate_point_label(self, x_val, y_val): + """ + Get the text the will populate the point label. + Give X and Y values for the current point. + Give values for the channel at the X coordinate. + @param x_val the current x value + @param y_val the current y value + @return a string with newlines + """ + if len(self._bars) == 0: return '' + scalar = float(len(self._bars)-1)/(self.x_max - self.x_min) + #convert x val to bar # + bar_index = scalar*(x_val - self.x_min) + #if abs(bar_index - round(bar_index)) > self._bar_width/2: return '' + bar_index = int(round(bar_index)) + bar_start = (bar_index - self._bar_width/2)/scalar + self.x_min + bar_end = (bar_index + self._bar_width/2)/scalar + self.x_min + bar_value = self._bars[bar_index] + return '%s to %s\n%s: %s'%( + common.eng_format(bar_start, self.x_units), + common.eng_format(bar_end, self.x_units), + self.y_label, common.eng_format(bar_value, self.y_units), + ) + + def set_bars(self, bars, bar_width, color_spec): + """ + Set the bars. + @param bars a list of bars + @param bar_width the fractional width of the bar, between 0 and 1 + @param color_spec the color tuple + """ + self.lock() + self._bars = bars + self._bar_width = float(bar_width) + self._color_spec = color_spec + self._bar_cache.changed(True) + self.unlock() + + diff --git a/gr-wxgui/src/python/plotter/channel_plotter.py b/gr-wxgui/src/python/plotter/channel_plotter.py index 8fa28410b..ff0a3a160 100644 --- a/gr-wxgui/src/python/plotter/channel_plotter.py +++ b/gr-wxgui/src/python/plotter/channel_plotter.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -20,20 +20,21 @@ # import wx -from plotter_base import grid_plotter_base -from OpenGL.GL import * -from gnuradio.wxgui import common +from grid_plotter_base import grid_plotter_base +from OpenGL import GL +import common import numpy import gltext import math LEGEND_TEXT_FONT_SIZE = 8 LEGEND_BOX_PADDING = 3 -PADDING = 35, 15, 40, 60 #top, right, bottom, left +MIN_PADDING = 35, 10, 0, 0 #top, right, bottom, left #constants for the waveform storage SAMPLES_KEY = 'samples' COLOR_SPEC_KEY = 'color_spec' MARKERY_KEY = 'marker' +TRIG_OFF_KEY = 'trig_off' ################################################## # Channel Plotter for X Y Waveforms @@ -45,16 +46,21 @@ class channel_plotter(grid_plotter_base): Create a new channel plotter. """ #init - grid_plotter_base.__init__(self, parent, PADDING) - self._channels = dict() + grid_plotter_base.__init__(self, parent, MIN_PADDING) + #setup legend cache + self._legend_cache = self.new_gl_cache(self._draw_legend, 50) self.enable_legend(False) + #setup waveform cache + self._waveform_cache = self.new_gl_cache(self._draw_waveforms, 50) + self._channels = dict() + #init channel plotter + self.register_init(self._init_channel_plotter) - def _gl_init(self): + def _init_channel_plotter(self): """ Run gl initialization tasks. """ - glEnableClientState(GL_VERTEX_ARRAY) - self._grid_compiled_list_id = glGenLists(1) + GL.glEnableClientState(GL.GL_VERTEX_ARRAY) def enable_legend(self, enable=None): """ @@ -65,73 +71,55 @@ class channel_plotter(grid_plotter_base): if enable is None: return self._enable_legend self.lock() self._enable_legend = enable - self.changed(True) + self._legend_cache.changed(True) self.unlock() - def draw(self): + def _draw_waveforms(self): """ - Draw the grid and waveforms. + Draw the waveforms for each channel. + Scale the waveform data to the grid using gl matrix operations. """ - self.lock() - self.clear() - #store the grid drawing operations - if self.changed(): - glNewList(self._grid_compiled_list_id, GL_COMPILE) - self._draw_grid() - self._draw_legend() - glEndList() - self.changed(False) - #draw the grid - glCallList(self._grid_compiled_list_id) #use scissor to prevent drawing outside grid - glEnable(GL_SCISSOR_TEST) - glScissor( + GL.glEnable(GL.GL_SCISSOR_TEST) + GL.glScissor( self.padding_left+1, self.padding_bottom+1, self.width-self.padding_left-self.padding_right-1, self.height-self.padding_top-self.padding_bottom-1, ) - #draw the waveforms - self._draw_waveforms() - glDisable(GL_SCISSOR_TEST) - self._draw_point_label() - #swap buffer into display - self.SwapBuffers() - self.unlock() - - def _draw_waveforms(self): - """ - Draw the waveforms for each channel. - Scale the waveform data to the grid using gl matrix operations. - """ for channel in reversed(sorted(self._channels.keys())): samples = self._channels[channel][SAMPLES_KEY] num_samps = len(samples) if not num_samps: continue #use opengl to scale the waveform - glPushMatrix() - glTranslatef(self.padding_left, self.padding_top, 0) - glScalef( + GL.glPushMatrix() + GL.glTranslatef(self.padding_left, self.padding_top, 0) + GL.glScalef( (self.width-self.padding_left-self.padding_right), (self.height-self.padding_top-self.padding_bottom), 1, ) - glTranslatef(0, 1, 0) + GL.glTranslatef(0, 1, 0) if isinstance(samples, tuple): x_scale, x_trans = 1.0/(self.x_max-self.x_min), -self.x_min points = zip(*samples) else: - x_scale, x_trans = 1.0/(num_samps-1), 0 + x_scale, x_trans = 1.0/(num_samps-1), -self._channels[channel][TRIG_OFF_KEY] points = zip(numpy.arange(0, num_samps), samples) - glScalef(x_scale, -1.0/(self.y_max-self.y_min), 1) - glTranslatef(x_trans, -self.y_min, 0) + GL.glScalef(x_scale, -1.0/(self.y_max-self.y_min), 1) + GL.glTranslatef(x_trans, -self.y_min, 0) #draw the points/lines - glColor3f(*self._channels[channel][COLOR_SPEC_KEY]) + GL.glColor3f(*self._channels[channel][COLOR_SPEC_KEY]) marker = self._channels[channel][MARKERY_KEY] - if marker: glPointSize(marker) - glVertexPointerf(points) - glDrawArrays(marker is None and GL_LINE_STRIP or GL_POINTS, 0, len(points)) - glPopMatrix() + if marker is None: + GL.glVertexPointerf(points) + GL.glDrawArrays(GL.GL_LINE_STRIP, 0, len(points)) + elif isinstance(marker, (int, float)) and marker > 0: + GL.glPointSize(marker) + GL.glVertexPointerf(points) + GL.glDrawArrays(GL.GL_POINTS, 0, len(points)) + GL.glPopMatrix() + GL.glDisable(GL.GL_SCISSOR_TEST) def _populate_point_label(self, x_val, y_val): """ @@ -143,12 +131,9 @@ class channel_plotter(grid_plotter_base): @return a string with newlines """ #create text - label_str = '%s: %s %s\n%s: %s %s'%( - self.x_label, - common.label_format(x_val), - self.x_units, self.y_label, - common.label_format(y_val), - self.y_units, + label_str = '%s: %s\n%s: %s'%( + self.x_label, common.eng_format(x_val, self.x_units), + self.y_label, common.eng_format(y_val, self.y_units), ) for channel in sorted(self._channels.keys()): samples = self._channels[channel][SAMPLES_KEY] @@ -156,11 +141,12 @@ class channel_plotter(grid_plotter_base): if not num_samps: continue if isinstance(samples, tuple): continue #linear interpolation - x_index = (num_samps-1)*(x_val/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + x_index = (num_samps-1)*(x_val-self.x_min)/(self.x_max-self.x_min) x_index_low = int(math.floor(x_index)) x_index_high = int(math.ceil(x_index)) - y_value = (samples[x_index_high] - samples[x_index_low])*(x_index - x_index_low) + samples[x_index_low] - label_str += '\n%s: %s %s'%(channel, common.label_format(y_value), self.y_units) + scale = x_index - x_index_low + self._channels[channel][TRIG_OFF_KEY] + y_value = (samples[x_index_high] - samples[x_index_low])*scale + samples[x_index_low] + label_str += '\n%s: %s'%(channel, common.eng_format(y_value, self.y_units)) return label_str def _draw_legend(self): @@ -178,7 +164,7 @@ class channel_plotter(grid_plotter_base): txt = gltext.Text(channel, font_size=LEGEND_TEXT_FONT_SIZE) w, h = txt.get_size() #draw rect + text - glColor3f(*color_spec) + GL.glColor3f(*color_spec) self._draw_rect( x_off - w - LEGEND_BOX_PADDING, self.padding_top/2 - h/2 - LEGEND_BOX_PADDING, @@ -188,21 +174,36 @@ class channel_plotter(grid_plotter_base): txt.draw_text(wx.Point(x_off - w, self.padding_top/2 - h/2)) x_off -= w + 4*LEGEND_BOX_PADDING - def set_waveform(self, channel, samples, color_spec, marker=None): + def clear_waveform(self, channel): + """ + Remove a waveform from the list of waveforms. + @param channel the channel key + """ + self.lock() + if channel in self._channels.keys(): + self._channels.pop(channel) + self._legend_cache.changed(True) + self._waveform_cache.changed(True) + self.unlock() + + def set_waveform(self, channel, samples=[], color_spec=(0, 0, 0), marker=None, trig_off=0): """ Set the waveform for a given channel. @param channel the channel key @param samples the waveform samples @param color_spec the 3-tuple for line color @param marker None for line + @param trig_off fraction of sample for trigger offset """ self.lock() - if channel not in self._channels.keys(): self.changed(True) + if channel not in self._channels.keys(): self._legend_cache.changed(True) self._channels[channel] = { SAMPLES_KEY: samples, COLOR_SPEC_KEY: color_spec, MARKERY_KEY: marker, + TRIG_OFF_KEY: trig_off, } + self._waveform_cache.changed(True) self.unlock() if __name__ == '__main__': diff --git a/gr-wxgui/src/python/plotter/common.py b/gr-wxgui/src/python/plotter/common.py new file mode 100644 index 000000000..7699986aa --- /dev/null +++ b/gr-wxgui/src/python/plotter/common.py @@ -0,0 +1,131 @@ +# +# Copyright 2009 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Radio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +import threading +import time +import math +import wx + +################################################## +# Number formatting +################################################## +def get_exp(num): + """ + Get the exponent of the number in base 10. + @param num the floating point number + @return the exponent as an integer + """ + if num == 0: return 0 + return int(math.floor(math.log10(abs(num)))) + +def get_si_components(num): + """ + Get the SI units for the number. + Extract the coeff and exponent of the number. + The exponent will be a multiple of 3. + @param num the floating point number + @return the tuple coeff, exp, prefix + """ + num = float(num) + exp = get_exp(num) + exp -= exp%3 + exp = min(max(exp, -24), 24) #bounds on SI table below + prefix = { + 24: 'Y', 21: 'Z', + 18: 'E', 15: 'P', + 12: 'T', 9: 'G', + 6: 'M', 3: 'k', + 0: '', + -3: 'm', -6: 'u', + -9: 'n', -12: 'p', + -15: 'f', -18: 'a', + -21: 'z', -24: 'y', + }[exp] + coeff = num/10**exp + return coeff, exp, prefix + +def sci_format(num): + """ + Format a floating point number into scientific notation. + @param num the number to format + @return a label string + """ + coeff, exp, prefix = get_si_components(num) + if -3 <= exp < 3: return '%g'%num + return '%.3ge%d'%(coeff, exp) + +def eng_format(num, units=''): + """ + Format a floating point number into engineering notation. + @param num the number to format + @param units the units to append + @return a label string + """ + coeff, exp, prefix = get_si_components(num) + if -3 <= exp < 3: return '%g'%num + return '%g%s%s%s'%(coeff, units and ' ' or '', prefix, units) + +################################################## +# Interface with thread safe lock/unlock +################################################## +class mutex(object): + _lock = threading.Lock() + def lock(self): self._lock.acquire() + def unlock(self): self._lock.release() + +################################################## +# Periodic update thread for point label +################################################## +class point_label_thread(threading.Thread, mutex): + + def __init__(self, plotter): + self._plotter = plotter + self._coor_queue = list() + #bind plotter mouse events + self._plotter.Bind(wx.EVT_MOTION, lambda evt: self.enqueue(evt.GetPosition())) + self._plotter.Bind(wx.EVT_LEAVE_WINDOW, lambda evt: self.enqueue(None)) + #start the thread + threading.Thread.__init__(self) + self.start() + + def enqueue(self, coor): + self.lock() + self._coor_queue.append(coor) + self.unlock() + + def run(self): + last_ts = time.time() + last_coor = coor = None + try: + while True: + time.sleep(1.0/30.0) + self.lock() + #get most recent coor change + if self._coor_queue: + coor = self._coor_queue[-1] + self._coor_queue = list() + self.unlock() + #update if coor change, or enough time expired + if last_coor != coor or (time.time() - last_ts) > (1.0/2.0): + self._plotter.set_point_label_coordinate(coor) + last_coor = coor + last_ts = time.time() + except wx.PyDeadObjectError: pass diff --git a/gr-wxgui/src/python/plotter/grid_plotter_base.py b/gr-wxgui/src/python/plotter/grid_plotter_base.py new file mode 100644 index 000000000..fd318ffa0 --- /dev/null +++ b/gr-wxgui/src/python/plotter/grid_plotter_base.py @@ -0,0 +1,370 @@ +# +# Copyright 2009 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Radio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +import wx +import wx.glcanvas +from OpenGL import GL +import common +from plotter_base import plotter_base +import gltext +import math + +GRID_LINE_COLOR_SPEC = (.7, .7, .7) #gray +GRID_BORDER_COLOR_SPEC = (0, 0, 0) #black +TICK_TEXT_FONT_SIZE = 9 +TITLE_TEXT_FONT_SIZE = 13 +UNITS_TEXT_FONT_SIZE = 9 +AXIS_LABEL_PADDING = 5 +TICK_LABEL_PADDING = 5 +TITLE_LABEL_PADDING = 7 +POINT_LABEL_FONT_SIZE = 8 +POINT_LABEL_COLOR_SPEC = (1, 1, .5) +POINT_LABEL_PADDING = 3 +GRID_LINE_DASH_LEN = 4 + +################################################## +# Grid Plotter Base Class +################################################## +class grid_plotter_base(plotter_base): + + def __init__(self, parent, min_padding=(0, 0, 0, 0)): + plotter_base.__init__(self, parent) + #setup grid cache + self._grid_cache = self.new_gl_cache(self._draw_grid, 25) + self.enable_grid_lines(True) + #setup padding + self.padding_top_min, self.padding_right_min, self.padding_bottom_min, self.padding_left_min = min_padding + #store title and unit strings + self.set_title('Title') + self.set_x_label('X Label') + self.set_y_label('Y Label') + #init the grid to some value + self.set_x_grid(-1, 1, 1) + self.set_y_grid(-1, 1, 1) + #setup point label cache + self._point_label_cache = self.new_gl_cache(self._draw_point_label, 75) + self.enable_point_label(False) + self.set_point_label_coordinate(None) + common.point_label_thread(self) + #init grid plotter + self.register_init(self._init_grid_plotter) + + def _init_grid_plotter(self): + """ + Run gl initialization tasks. + """ + GL.glEnableClientState(GL.GL_VERTEX_ARRAY) + + def set_point_label_coordinate(self, coor): + """ + Set the point label coordinate. + @param coor the coordinate x, y tuple or None + """ + self.lock() + self._point_label_coordinate = coor + self._point_label_cache.changed(True) + self.update() + self.unlock() + + def enable_point_label(self, enable=None): + """ + Enable/disable the point label. + @param enable true to enable + @return the enable state when None + """ + if enable is None: return self._enable_point_label + self.lock() + self._enable_point_label = enable + self._point_label_cache.changed(True) + self.unlock() + + def set_title(self, title): + """ + Set the title. + @param title the title string + """ + self.lock() + self.title = title + self._grid_cache.changed(True) + self.unlock() + + def set_x_label(self, x_label, x_units=''): + """ + Set the x label and units. + @param x_label the x label string + @param x_units the x units string + """ + self.lock() + self.x_label = x_label + self.x_units = x_units + self._grid_cache.changed(True) + self.unlock() + + def set_y_label(self, y_label, y_units=''): + """ + Set the y label and units. + @param y_label the y label string + @param y_units the y units string + """ + self.lock() + self.y_label = y_label + self.y_units = y_units + self._grid_cache.changed(True) + self.unlock() + + def set_x_grid(self, minimum, maximum, step, scale=False): + """ + Set the x grid parameters. + @param minimum the left-most value + @param maximum the right-most value + @param step the grid spacing + @param scale true to scale the x grid + """ + self.lock() + self.x_min = float(minimum) + self.x_max = float(maximum) + self.x_step = float(step) + if scale: + coeff, exp, prefix = common.get_si_components(max(abs(self.x_min), abs(self.x_max))) + self.x_scalar = 10**(-exp) + self.x_prefix = prefix + else: + self.x_scalar = 1.0 + self.x_prefix = '' + for cache in self._gl_caches: cache.changed(True) + self.unlock() + + def set_y_grid(self, minimum, maximum, step, scale=False): + """ + Set the y grid parameters. + @param minimum the bottom-most value + @param maximum the top-most value + @param step the grid spacing + @param scale true to scale the y grid + """ + self.lock() + self.y_min = float(minimum) + self.y_max = float(maximum) + self.y_step = float(step) + if scale: + coeff, exp, prefix = common.get_si_components(max(abs(self.y_min), abs(self.y_max))) + self.y_scalar = 10**(-exp) + self.y_prefix = prefix + else: + self.y_scalar = 1.0 + self.y_prefix = '' + for cache in self._gl_caches: cache.changed(True) + self.unlock() + + def _draw_grid(self): + """ + Create the x, y, tick, and title labels. + Resize the padding for the labels. + Draw the border, grid, title, and labels. + """ + ################################################## + # Create GL text labels + ################################################## + #create x tick labels + x_tick_labels = [(tick, self._get_tick_label(tick, self.x_units)) + for tick in self._get_ticks(self.x_min, self.x_max, self.x_step, self.x_scalar)] + #create x tick labels + y_tick_labels = [(tick, self._get_tick_label(tick, self.y_units)) + for tick in self._get_ticks(self.y_min, self.y_max, self.y_step, self.y_scalar)] + #create x label + x_label_str = self.x_units and "%s (%s%s)"%(self.x_label, self.x_prefix, self.x_units) or self.x_label + x_label = gltext.Text(x_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) + #create y label + y_label_str = self.y_units and "%s (%s%s)"%(self.y_label, self.y_prefix, self.y_units) or self.y_label + y_label = gltext.Text(y_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) + #create title + title_label = gltext.Text(self.title, bold=True, font_size=TITLE_TEXT_FONT_SIZE, centered=True) + ################################################## + # Resize the padding + ################################################## + self.padding_top = max(2*TITLE_LABEL_PADDING + title_label.get_size()[1], self.padding_top_min) + self.padding_right = max(2*TICK_LABEL_PADDING, self.padding_right_min) + self.padding_bottom = max(2*AXIS_LABEL_PADDING + TICK_LABEL_PADDING + x_label.get_size()[1] + max([label.get_size()[1] for tick, label in x_tick_labels]), self.padding_bottom_min) + self.padding_left = max(2*AXIS_LABEL_PADDING + TICK_LABEL_PADDING + y_label.get_size()[1] + max([label.get_size()[0] for tick, label in y_tick_labels]), self.padding_left_min) + ################################################## + # Draw Grid X + ################################################## + for tick, label in x_tick_labels: + scaled_tick = (self.width-self.padding_left-self.padding_right)*\ + (tick/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + self.padding_left + self._draw_grid_line( + (scaled_tick, self.padding_top), + (scaled_tick, self.height-self.padding_bottom), + ) + w, h = label.get_size() + label.draw_text(wx.Point(scaled_tick-w/2, self.height-self.padding_bottom+TICK_LABEL_PADDING)) + ################################################## + # Draw Grid Y + ################################################## + for tick, label in y_tick_labels: + scaled_tick = (self.height-self.padding_top-self.padding_bottom)*\ + (1 - (tick/self.y_scalar-self.y_min)/(self.y_max-self.y_min)) + self.padding_top + self._draw_grid_line( + (self.padding_left, scaled_tick), + (self.width-self.padding_right, scaled_tick), + ) + w, h = label.get_size() + label.draw_text(wx.Point(self.padding_left-w-TICK_LABEL_PADDING, scaled_tick-h/2)) + ################################################## + # Draw Border + ################################################## + GL.glColor3f(*GRID_BORDER_COLOR_SPEC) + self._draw_rect( + self.padding_left, + self.padding_top, + self.width - self.padding_right - self.padding_left, + self.height - self.padding_top - self.padding_bottom, + fill=False, + ) + ################################################## + # Draw Labels + ################################################## + #draw title label + title_label.draw_text(wx.Point(self.width/2.0, TITLE_LABEL_PADDING + title_label.get_size()[1]/2)) + #draw x labels + x_label.draw_text(wx.Point( + (self.width-self.padding_left-self.padding_right)/2.0 + self.padding_left, + self.height-(AXIS_LABEL_PADDING + x_label.get_size()[1]/2), + ) + ) + #draw y labels + y_label.draw_text(wx.Point( + AXIS_LABEL_PADDING + y_label.get_size()[1]/2, + (self.height-self.padding_top-self.padding_bottom)/2.0 + self.padding_top, + ), rotation=90, + ) + + def _get_tick_label(self, tick, unit): + """ + Format the tick value and create a gl text. + @param tick the floating point tick value + @param unit the axis unit + @return the tick label text + """ + if unit: tick_str = common.sci_format(tick) + else: tick_str = common.eng_format(tick) + return gltext.Text(tick_str, font_size=TICK_TEXT_FONT_SIZE) + + def _get_ticks(self, min, max, step, scalar): + """ + Determine the positions for the ticks. + @param min the lower bound + @param max the upper bound + @param step the grid spacing + @param scalar the grid scaling + @return a list of tick positions between min and max + """ + #cast to float + min = float(min) + max = float(max) + step = float(step) + #check for valid numbers + try: + assert step > 0 + assert max > min + assert max - min > step + except AssertionError: return [-1, 1] + #determine the start and stop value + start = int(math.ceil(min/step)) + stop = int(math.floor(max/step)) + return [i*step*scalar for i in range(start, stop+1)] + + def enable_grid_lines(self, enable=None): + """ + Enable/disable the grid lines. + @param enable true to enable + @return the enable state when None + """ + if enable is None: return self._enable_grid_lines + self.lock() + self._enable_grid_lines = enable + self._grid_cache.changed(True) + self.unlock() + + def _draw_grid_line(self, coor1, coor2): + """ + Draw a dashed line from coor1 to coor2. + @param corr1 a tuple of x, y + @param corr2 a tuple of x, y + """ + if not self.enable_grid_lines(): return + length = math.sqrt((coor1[0] - coor2[0])**2 + (coor1[1] - coor2[1])**2) + num_points = int(length/GRID_LINE_DASH_LEN) + #calculate points array + points = [( + coor1[0] + i*(coor2[0]-coor1[0])/(num_points - 1), + coor1[1] + i*(coor2[1]-coor1[1])/(num_points - 1) + ) for i in range(num_points)] + #set color and draw + GL.glColor3f(*GRID_LINE_COLOR_SPEC) + GL.glVertexPointerf(points) + GL.glDrawArrays(GL.GL_LINES, 0, len(points)) + + def _draw_rect(self, x, y, width, height, fill=True): + """ + Draw a rectangle on the x, y plane. + X and Y are the top-left corner. + @param x the left position of the rectangle + @param y the top position of the rectangle + @param width the width of the rectangle + @param height the height of the rectangle + @param fill true to color inside of rectangle + """ + GL.glBegin(fill and GL.GL_QUADS or GL.GL_LINE_LOOP) + GL.glVertex2f(x, y) + GL.glVertex2f(x+width, y) + GL.glVertex2f(x+width, y+height) + GL.glVertex2f(x, y+height) + GL.glEnd() + + def _draw_point_label(self): + """ + Draw the point label for the last mouse motion coordinate. + The mouse coordinate must be an X, Y tuple. + The label will be drawn at the X, Y coordinate. + The values of the X, Y coordinate will be scaled to the current X, Y bounds. + """ + if not self.enable_point_label(): return + if not self._point_label_coordinate: return + x, y = self._point_label_coordinate + if x < self.padding_left or x > self.width-self.padding_right: return + if y < self.padding_top or y > self.height-self.padding_bottom: return + #scale to window bounds + x_win_scalar = float(x - self.padding_left)/(self.width-self.padding_left-self.padding_right) + y_win_scalar = float((self.height - y) - self.padding_bottom)/(self.height-self.padding_top-self.padding_bottom) + #scale to grid bounds + x_val = x_win_scalar*(self.x_max-self.x_min) + self.x_min + y_val = y_win_scalar*(self.y_max-self.y_min) + self.y_min + #create text + label_str = self._populate_point_label(x_val, y_val) + if not label_str: return + txt = gltext.Text(label_str, font_size=POINT_LABEL_FONT_SIZE) + w, h = txt.get_size() + #draw rect + text + GL.glColor3f(*POINT_LABEL_COLOR_SPEC) + if x > self.width/2: x -= w+2*POINT_LABEL_PADDING + self._draw_rect(x, y-h-2*POINT_LABEL_PADDING, w+2*POINT_LABEL_PADDING, h+2*POINT_LABEL_PADDING) + txt.draw_text(wx.Point(x+POINT_LABEL_PADDING, y-h-POINT_LABEL_PADDING)) diff --git a/gr-wxgui/src/python/plotter/plotter_base.py b/gr-wxgui/src/python/plotter/plotter_base.py index 6d5349a5f..662365a37 100644 --- a/gr-wxgui/src/python/plotter/plotter_base.py +++ b/gr-wxgui/src/python/plotter/plotter_base.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -21,27 +21,59 @@ import wx import wx.glcanvas -from OpenGL.GL import * -from gnuradio.wxgui import common -import threading -import gltext -import math -import time +from OpenGL import GL +import common BACKGROUND_COLOR_SPEC = (1, 0.976, 1, 1) #creamy white -GRID_LINE_COLOR_SPEC = (0, 0, 0) #black -TICK_TEXT_FONT_SIZE = 9 -TITLE_TEXT_FONT_SIZE = 13 -UNITS_TEXT_FONT_SIZE = 9 -TICK_LABEL_PADDING = 5 -POINT_LABEL_FONT_SIZE = 8 -POINT_LABEL_COLOR_SPEC = (1, 1, .5) -POINT_LABEL_PADDING = 3 + +################################################## +# GL caching interface +################################################## +class gl_cache(object): + """ + Cache a set of gl drawing routines in a compiled list. + """ + + def __init__(self, draw): + """ + Create a new cache. + @param draw a function to draw gl stuff + """ + self.changed(True) + self._draw = draw + + def init(self): + """ + To be called when gl initializes. + Create a new compiled list. + """ + self._grid_compiled_list_id = GL.glGenLists(1) + + def draw(self): + """ + Draw the gl stuff using a compiled list. + If changed, reload the compiled list. + """ + if self.changed(): + GL.glNewList(self._grid_compiled_list_id, GL.GL_COMPILE) + self._draw() + GL.glEndList() + self.changed(False) + #draw the grid + GL.glCallList(self._grid_compiled_list_id) + + def changed(self, state=None): + """ + Set the changed flag if state is not None. + Otherwise return the changed flag. + """ + if state is None: return self._changed + self._changed = state ################################################## # OpenGL WX Plotter Canvas ################################################## -class _plotter_base(wx.glcanvas.GLCanvas): +class plotter_base(wx.glcanvas.GLCanvas, common.mutex): """ Plotter base class for all plot types. """ @@ -54,340 +86,86 @@ class _plotter_base(wx.glcanvas.GLCanvas): Bind the paint and size events. @param parent the parent widgit """ - self._global_lock = threading.Lock() attribList = (wx.glcanvas.WX_GL_DOUBLEBUFFER, wx.glcanvas.WX_GL_RGBA) wx.glcanvas.GLCanvas.__init__(self, parent, attribList=attribList) - self.changed(False) self._gl_init_flag = False self._resized_flag = True - self._update_ts = 0 + self._init_fcns = list() + self._draw_fcns = list() + self._gl_caches = list() self.Bind(wx.EVT_PAINT, self._on_paint) self.Bind(wx.EVT_SIZE, self._on_size) + self.Bind(wx.EVT_ERASE_BACKGROUND, lambda e: None) - def lock(self): self._global_lock.acquire() - def unlock(self): self._global_lock.release() + def new_gl_cache(self, draw_fcn, draw_pri=50): + """ + Create a new gl cache. + Register its draw and init function. + @return the new cache object + """ + cache = gl_cache(draw_fcn) + self.register_init(cache.init) + self.register_draw(cache.draw, draw_pri) + self._gl_caches.append(cache) + return cache + + def register_init(self, init_fcn): + self._init_fcns.append(init_fcn) + + def register_draw(self, draw_fcn, draw_pri=50): + """ + Register a draw function with a layer priority. + Large pri values are drawn last. + Small pri values are drawn first. + """ + for i in range(len(self._draw_fcns)): + if draw_pri < self._draw_fcns[i][0]: + self._draw_fcns.insert(i, (draw_pri, draw_fcn)) + return + self._draw_fcns.append((draw_pri, draw_fcn)) def _on_size(self, event): """ Flag the resize event. The paint event will handle the actual resizing. """ + self.lock() self._resized_flag = True + self.unlock() def _on_paint(self, event): """ - Respond to paint events, call update. + Respond to paint events. Initialize GL if this is the first paint event. + Resize the view port if the width or height changed. + Redraw the screen, calling the draw functions. """ + self.lock() self.SetCurrent() #check if gl was initialized if not self._gl_init_flag: - glClearColor(*BACKGROUND_COLOR_SPEC) - self._gl_init() + GL.glClearColor(*BACKGROUND_COLOR_SPEC) + for fcn in self._init_fcns: fcn() self._gl_init_flag = True #check for a change in window size if self._resized_flag: - self.lock() self.width, self.height = self.GetSize() - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - glOrtho(0, self.width, self.height, 0, 1, 0) - glMatrixMode(GL_MODELVIEW) - glLoadIdentity() - glViewport(0, 0, self.width, self.height) + GL.glMatrixMode(GL.GL_PROJECTION) + GL.glLoadIdentity() + GL.glOrtho(0, self.width, self.height, 0, 1, 0) + GL.glMatrixMode(GL.GL_MODELVIEW) + GL.glLoadIdentity() + GL.glViewport(0, 0, self.width, self.height) + for cache in self._gl_caches: cache.changed(True) self._resized_flag = False - self.changed(True) - self.unlock() - self.draw() + #clear, draw functions, swap + GL.glClear(GL.GL_COLOR_BUFFER_BIT) + for fcn in self._draw_fcns: fcn[1]() + self.SwapBuffers() + self.unlock() def update(self): """ Force a paint event. - Record the timestamp. """ wx.PostEvent(self, wx.PaintEvent()) - self._update_ts = time.time() - - def clear(self): glClear(GL_COLOR_BUFFER_BIT) - - def changed(self, state=None): - """ - Set the changed flag if state is not None. - Otherwise return the changed flag. - """ - if state is not None: self._changed = state - else: return self._changed - -################################################## -# Grid Plotter Base Class -################################################## -class grid_plotter_base(_plotter_base): - - def __init__(self, parent, padding): - _plotter_base.__init__(self, parent) - self.padding_top, self.padding_right, self.padding_bottom, self.padding_left = padding - #store title and unit strings - self.set_title('Title') - self.set_x_label('X Label') - self.set_y_label('Y Label') - #init the grid to some value - self.set_x_grid(-1, 1, 1) - self.set_y_grid(-1, 1, 1) - #setup point label - self.enable_point_label(False) - self._mouse_coordinate = None - self.Bind(wx.EVT_MOTION, self._on_motion) - self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave_window) - - def _on_motion(self, event): - """ - Mouse motion, record the position X, Y. - """ - self.lock() - self._mouse_coordinate = event.GetPosition() - #update based on last known update time - if time.time() - self._update_ts > 0.03: self.update() - self.unlock() - - def _on_leave_window(self, event): - """ - Mouse leave window, set the position to None. - """ - self.lock() - self._mouse_coordinate = None - self.update() - self.unlock() - - def enable_point_label(self, enable=None): - """ - Enable/disable the point label. - @param enable true to enable - @return the enable state when None - """ - if enable is None: return self._enable_point_label - self.lock() - self._enable_point_label = enable - self.changed(True) - self.unlock() - - def set_title(self, title): - """ - Set the title. - @param title the title string - """ - self.lock() - self.title = title - self.changed(True) - self.unlock() - - def set_x_label(self, x_label, x_units=''): - """ - Set the x label and units. - @param x_label the x label string - @param x_units the x units string - """ - self.lock() - self.x_label = x_label - self.x_units = x_units - self.changed(True) - self.unlock() - - def set_y_label(self, y_label, y_units=''): - """ - Set the y label and units. - @param y_label the y label string - @param y_units the y units string - """ - self.lock() - self.y_label = y_label - self.y_units = y_units - self.changed(True) - self.unlock() - - def set_x_grid(self, x_min, x_max, x_step, x_scalar=1.0): - """ - Set the x grid parameters. - @param x_min the left-most value - @param x_max the right-most value - @param x_step the grid spacing - @param x_scalar the scalar factor - """ - self.lock() - self.x_min = float(x_min) - self.x_max = float(x_max) - self.x_step = float(x_step) - self.x_scalar = float(x_scalar) - self.changed(True) - self.unlock() - - def set_y_grid(self, y_min, y_max, y_step, y_scalar=1.0): - """ - Set the y grid parameters. - @param y_min the bottom-most value - @param y_max the top-most value - @param y_step the grid spacing - @param y_scalar the scalar factor - """ - self.lock() - self.y_min = float(y_min) - self.y_max = float(y_max) - self.y_step = float(y_step) - self.y_scalar = float(y_scalar) - self.changed(True) - self.unlock() - - def _draw_grid(self): - """ - Draw the border, grid, title, and units. - """ - ################################################## - # Draw Border - ################################################## - glColor3f(*GRID_LINE_COLOR_SPEC) - self._draw_rect( - self.padding_left, - self.padding_top, - self.width - self.padding_right - self.padding_left, - self.height - self.padding_top - self.padding_bottom, - fill=False, - ) - ################################################## - # Draw Grid X - ################################################## - for tick in self._get_ticks(self.x_min, self.x_max, self.x_step, self.x_scalar): - scaled_tick = (self.width-self.padding_left-self.padding_right)*\ - (tick/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + self.padding_left - glColor3f(*GRID_LINE_COLOR_SPEC) - self._draw_line( - (scaled_tick, self.padding_top, 0), - (scaled_tick, self.height-self.padding_bottom, 0), - ) - txt = self._get_tick_label(tick) - w, h = txt.get_size() - txt.draw_text(wx.Point(scaled_tick-w/2, self.height-self.padding_bottom+TICK_LABEL_PADDING)) - ################################################## - # Draw Grid Y - ################################################## - for tick in self._get_ticks(self.y_min, self.y_max, self.y_step, self.y_scalar): - scaled_tick = (self.height-self.padding_top-self.padding_bottom)*\ - (1 - (tick/self.y_scalar-self.y_min)/(self.y_max-self.y_min)) + self.padding_top - glColor3f(*GRID_LINE_COLOR_SPEC) - self._draw_line( - (self.padding_left, scaled_tick, 0), - (self.width-self.padding_right, scaled_tick, 0), - ) - txt = self._get_tick_label(tick) - w, h = txt.get_size() - txt.draw_text(wx.Point(self.padding_left-w-TICK_LABEL_PADDING, scaled_tick-h/2)) - ################################################## - # Draw Title - ################################################## - #draw x units - txt = gltext.Text(self.title, bold=True, font_size=TITLE_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point(self.width/2.0, .5*self.padding_top)) - ################################################## - # Draw Labels - ################################################## - #draw x labels - x_label_str = self.x_units and "%s (%s)"%(self.x_label, self.x_units) or self.x_label - txt = gltext.Text(x_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point( - (self.width-self.padding_left-self.padding_right)/2.0 + self.padding_left, - self.height-.25*self.padding_bottom, - ) - ) - #draw y labels - y_label_str = self.y_units and "%s (%s)"%(self.y_label, self.y_units) or self.y_label - txt = gltext.Text(y_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point( - .25*self.padding_left, - (self.height-self.padding_top-self.padding_bottom)/2.0 + self.padding_top, - ), rotation=90, - ) - - def _get_tick_label(self, tick): - """ - Format the tick value and create a gl text. - @param tick the floating point tick value - @return the tick label text - """ - tick_str = common.label_format(tick) - return gltext.Text(tick_str, font_size=TICK_TEXT_FONT_SIZE) - - def _get_ticks(self, min, max, step, scalar): - """ - Determine the positions for the ticks. - @param min the lower bound - @param max the upper bound - @param step the grid spacing - @param scalar the grid scaling - @return a list of tick positions between min and max - """ - #cast to float - min = float(min) - max = float(max) - step = float(step) - #check for valid numbers - assert step > 0 - assert max > min - assert max - min > step - #determine the start and stop value - start = int(math.ceil(min/step)) - stop = int(math.floor(max/step)) - return [i*step*scalar for i in range(start, stop+1)] - - def _draw_line(self, coor1, coor2): - """ - Draw a line from coor1 to coor2. - @param corr1 a tuple of x, y, z - @param corr2 a tuple of x, y, z - """ - glBegin(GL_LINES) - glVertex3f(*coor1) - glVertex3f(*coor2) - glEnd() - - def _draw_rect(self, x, y, width, height, fill=True): - """ - Draw a rectangle on the x, y plane. - X and Y are the top-left corner. - @param x the left position of the rectangle - @param y the top position of the rectangle - @param width the width of the rectangle - @param height the height of the rectangle - @param fill true to color inside of rectangle - """ - glBegin(fill and GL_QUADS or GL_LINE_LOOP) - glVertex2f(x, y) - glVertex2f(x+width, y) - glVertex2f(x+width, y+height) - glVertex2f(x, y+height) - glEnd() - - def _draw_point_label(self): - """ - Draw the point label for the last mouse motion coordinate. - The mouse coordinate must be an X, Y tuple. - The label will be drawn at the X, Y coordinate. - The values of the X, Y coordinate will be scaled to the current X, Y bounds. - """ - if not self.enable_point_label(): return - if not self._mouse_coordinate: return - x, y = self._mouse_coordinate - if x < self.padding_left or x > self.width-self.padding_right: return - if y < self.padding_top or y > self.height-self.padding_bottom: return - #scale to window bounds - x_win_scalar = float(x - self.padding_left)/(self.width-self.padding_left-self.padding_right) - y_win_scalar = float((self.height - y) - self.padding_bottom)/(self.height-self.padding_top-self.padding_bottom) - #scale to grid bounds - x_val = self.x_scalar*(x_win_scalar*(self.x_max-self.x_min) + self.x_min) - y_val = self.y_scalar*(y_win_scalar*(self.y_max-self.y_min) + self.y_min) - #create text - label_str = self._populate_point_label(x_val, y_val) - txt = gltext.Text(label_str, font_size=POINT_LABEL_FONT_SIZE) - w, h = txt.get_size() - #draw rect + text - glColor3f(*POINT_LABEL_COLOR_SPEC) - if x > self.width/2: x -= w+2*POINT_LABEL_PADDING - self._draw_rect(x, y-h-2*POINT_LABEL_PADDING, w+2*POINT_LABEL_PADDING, h+2*POINT_LABEL_PADDING) - txt.draw_text(wx.Point(x+POINT_LABEL_PADDING, y-h-POINT_LABEL_PADDING)) diff --git a/gr-wxgui/src/python/plotter/waterfall_plotter.py b/gr-wxgui/src/python/plotter/waterfall_plotter.py index 4dc19f672..2e0669961 100644 --- a/gr-wxgui/src/python/plotter/waterfall_plotter.py +++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -20,9 +20,9 @@ # import wx -from plotter_base import grid_plotter_base -from OpenGL.GL import * -from gnuradio.wxgui import common +from grid_plotter_base import grid_plotter_base +from OpenGL import GL +import common import numpy import gltext import math @@ -33,7 +33,7 @@ LEGEND_NUM_LABELS = 9 LEGEND_WIDTH = 8 LEGEND_FONT_SIZE = 8 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black -PADDING = 35, 60, 40, 60 #top, right, bottom, left +MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2))) @@ -91,7 +91,13 @@ class waterfall_plotter(grid_plotter_base): Create a new channel plotter. """ #init - grid_plotter_base.__init__(self, parent, PADDING) + grid_plotter_base.__init__(self, parent, MIN_PADDING) + #setup legend cache + self._legend_cache = self.new_gl_cache(self._draw_legend) + #setup waterfall cache + self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50) + #setup waterfall plotter + self.register_init(self._init_waterfall) self._resize_texture(False) self._minimum = 0 self._maximum = 0 @@ -102,35 +108,11 @@ class waterfall_plotter(grid_plotter_base): self.set_num_lines(0) self.set_color_mode(COLORS.keys()[0]) - def _gl_init(self): + def _init_waterfall(self): """ Run gl initialization tasks. """ - self._grid_compiled_list_id = glGenLists(1) - self._waterfall_texture = glGenTextures(1) - - def draw(self): - """ - Draw the grid and waveforms. - """ - self.lock() - #resize texture - self._resize_texture() - #store the grid drawing operations - if self.changed(): - glNewList(self._grid_compiled_list_id, GL_COMPILE) - self._draw_grid() - self._draw_legend() - glEndList() - self.changed(False) - self.clear() - #draw the grid - glCallList(self._grid_compiled_list_id) - self._draw_waterfall() - self._draw_point_label() - #swap buffer into display - self.SwapBuffers() - self.unlock() + self._waterfall_texture = GL.glGenTextures(1) def _draw_waterfall(self): """ @@ -138,42 +120,44 @@ class waterfall_plotter(grid_plotter_base): The texture is circularly filled and will wrap around. Use matrix modeling to shift and scale the texture onto the coordinate plane. """ + #resize texture + self._resize_texture() #setup texture - glBindTexture(GL_TEXTURE_2D, self._waterfall_texture) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) + GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture) + GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR) + GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR) + GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT) + GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE) #write the buffer to the texture while self._buffer: - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL_RGBA, GL_UNSIGNED_BYTE, self._buffer.pop(0)) + GL.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, self._buffer.pop(0)) self._pointer = (self._pointer + 1)%self._num_lines #begin drawing - glEnable(GL_TEXTURE_2D) - glPushMatrix() + GL.glEnable(GL.GL_TEXTURE_2D) + GL.glPushMatrix() #matrix scaling - glTranslatef(self.padding_left, self.padding_top, 0) - glScalef( + GL.glTranslatef(self.padding_left, self.padding_top, 0) + GL.glScalef( float(self.width-self.padding_left-self.padding_right), float(self.height-self.padding_top-self.padding_bottom), 1.0, ) #draw texture with wrapping - glBegin(GL_QUADS) + GL.glBegin(GL.GL_QUADS) prop_y = float(self._pointer)/(self._num_lines-1) prop_x = float(self._fft_size)/ceil_log2(self._fft_size) off = 1.0/(self._num_lines-1) - glTexCoord2f(0, prop_y+1-off) - glVertex2f(0, 1) - glTexCoord2f(prop_x, prop_y+1-off) - glVertex2f(1, 1) - glTexCoord2f(prop_x, prop_y) - glVertex2f(1, 0) - glTexCoord2f(0, prop_y) - glVertex2f(0, 0) - glEnd() - glPopMatrix() - glDisable(GL_TEXTURE_2D) + GL.glTexCoord2f(0, prop_y+1-off) + GL.glVertex2f(0, 1) + GL.glTexCoord2f(prop_x, prop_y+1-off) + GL.glVertex2f(1, 1) + GL.glTexCoord2f(prop_x, prop_y) + GL.glVertex2f(1, 0) + GL.glTexCoord2f(0, prop_y) + GL.glVertex2f(0, 0) + GL.glEnd() + GL.glPopMatrix() + GL.glDisable(GL.GL_TEXTURE_2D) def _populate_point_label(self, x_val, y_val): """ @@ -183,7 +167,7 @@ class waterfall_plotter(grid_plotter_base): @param y_val the current y value @return a value string with units """ - return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units) + return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units)) def _draw_legend(self): """ @@ -196,11 +180,11 @@ class waterfall_plotter(grid_plotter_base): x = self.width - self.padding_right + LEGEND_LEFT_PAD for i in range(LEGEND_NUM_BLOCKS): color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))] - glColor4f(*map(lambda c: ord(c)/255.0, color)) + GL.glColor4f(*map(lambda c: ord(c)/255.0, color)) y = self.height - (i+1)*block_height - self.padding_bottom self._draw_rect(x, y, LEGEND_WIDTH, block_height) #draw rectangle around color scale border - glColor3f(*LEGEND_BORDER_COLOR_SPEC) + GL.glColor3f(*LEGEND_BORDER_COLOR_SPEC) self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False) #draw each legend label label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1) @@ -224,9 +208,9 @@ class waterfall_plotter(grid_plotter_base): self._buffer = list() self._pointer = 0 if self._num_lines and self._fft_size: - glBindTexture(GL_TEXTURE_2D, self._waterfall_texture) + GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture) data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring() - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL_RGBA, GL_UNSIGNED_BYTE, data) + GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, data) self._resize_texture_flag = False def set_color_mode(self, color_mode): @@ -239,7 +223,7 @@ class waterfall_plotter(grid_plotter_base): self.lock() if color_mode in COLORS.keys(): self._color_mode = color_mode - self.changed(True) + self._legend_cache.changed(True) self.update() self.unlock() @@ -268,7 +252,7 @@ class waterfall_plotter(grid_plotter_base): if self._minimum != minimum or self._maximum != maximum: self._minimum = minimum self._maximum = maximum - self.changed(True) + self._legend_cache.changed(True) if self._fft_size != len(samples): self._fft_size = len(samples) self._resize_texture(True) @@ -279,4 +263,5 @@ class waterfall_plotter(grid_plotter_base): #convert the samples to RGBA data data = numpy.choose(samples, COLORS[self._color_mode]).tostring() self._buffer.append(data) + self._waterfall_cache.changed(True) self.unlock() |