summaryrefslogtreecommitdiff
path: root/gr-wxgui/src/python/plotter
diff options
context:
space:
mode:
Diffstat (limited to 'gr-wxgui/src/python/plotter')
-rw-r--r--gr-wxgui/src/python/plotter/__init__.py24
-rw-r--r--gr-wxgui/src/python/plotter/bar_plotter.py144
-rw-r--r--gr-wxgui/src/python/plotter/channel_plotter.py236
-rw-r--r--gr-wxgui/src/python/plotter/common.py133
-rw-r--r--gr-wxgui/src/python/plotter/gltext.py503
-rw-r--r--gr-wxgui/src/python/plotter/grid_plotter_base.py419
-rw-r--r--gr-wxgui/src/python/plotter/plotter_base.py214
-rw-r--r--gr-wxgui/src/python/plotter/waterfall_plotter.py278
8 files changed, 1951 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/plotter/__init__.py b/gr-wxgui/src/python/plotter/__init__.py
new file mode 100644
index 000000000..616492a3e
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/__init__.py
@@ -0,0 +1,24 @@
+#
+# Copyright 2008 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.
+#
+
+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
new file mode 100644
index 000000000..4bcc36fd4
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/channel_plotter.py
@@ -0,0 +1,236 @@
+#
+# Copyright 2008, 2009, 2010 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
+import gltext
+import math
+
+LEGEND_TEXT_FONT_SIZE = 8
+LEGEND_BOX_PADDING = 3
+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
+##################################################
+class channel_plotter(grid_plotter_base):
+
+ def __init__(self, parent):
+ """
+ Create a new channel plotter.
+ """
+ #init
+ grid_plotter_base.__init__(self, parent, MIN_PADDING)
+ self.set_use_persistence(False)
+ #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)
+ self.callback = None
+
+ def _init_channel_plotter(self):
+ """
+ Run gl initialization tasks.
+ """
+ GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
+
+ def enable_legend(self, enable=None):
+ """
+ Enable/disable the legend.
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_legend
+ self.lock()
+ self._enable_legend = enable
+ self._legend_cache.changed(True)
+ self.unlock()
+
+ def _draw_waveforms(self):
+ """
+ Draw the waveforms for each channel.
+ Scale the waveform data to the grid using gl matrix operations.
+ """
+ #use scissor to prevent drawing outside grid
+ 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,
+ )
+ 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
+ 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)
+ 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), -self._channels[channel][TRIG_OFF_KEY]
+ points = zip(numpy.arange(0, num_samps), samples)
+ 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
+ GL.glColor3f(*self._channels[channel][COLOR_SPEC_KEY])
+ marker = self._channels[channel][MARKERY_KEY]
+ 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):
+ """
+ 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
+ """
+ #create text
+ 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]
+ num_samps = len(samples)
+ if not num_samps: continue
+ if isinstance(samples, tuple): continue
+ #linear interpolation
+ 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))
+ 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 _call_callback (self, x_val, y_val):
+ if self.callback != None:
+ self.callback(x_val, y_val)
+
+ def set_callback (self, callback):
+ self.callback = callback
+
+ def _draw_legend(self):
+ """
+ Draw the legend in the upper right corner.
+ For each channel, draw a rectangle out of the channel color,
+ and overlay the channel text on top of the rectangle.
+ """
+ if not self.enable_legend(): return
+ x_off = self.width - self.padding_right - LEGEND_BOX_PADDING
+ for i, channel in enumerate(reversed(sorted(self._channels.keys()))):
+ samples = self._channels[channel][SAMPLES_KEY]
+ if not len(samples): continue
+ color_spec = self._channels[channel][COLOR_SPEC_KEY]
+ txt = gltext.Text(channel, font_size=LEGEND_TEXT_FONT_SIZE)
+ w, h = txt.get_size()
+ #draw rect + text
+ GL.glColor3f(*color_spec)
+ self._draw_rect(
+ x_off - w - LEGEND_BOX_PADDING,
+ self.padding_top/2 - h/2 - LEGEND_BOX_PADDING,
+ w+2*LEGEND_BOX_PADDING,
+ h+2*LEGEND_BOX_PADDING,
+ )
+ txt.draw_text(wx.Point(x_off - w, self.padding_top/2 - h/2))
+ x_off -= w + 4*LEGEND_BOX_PADDING
+
+ 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._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__':
+ app = wx.PySimpleApp()
+ frame = wx.Frame(None, -1, 'Demo', wx.DefaultPosition)
+ vbox = wx.BoxSizer(wx.VERTICAL)
+
+ plotter = channel_plotter(frame)
+ plotter.set_x_grid(-1, 1, .2)
+ plotter.set_y_grid(-1, 1, .4)
+ vbox.Add(plotter, 1, wx.EXPAND)
+
+ plotter = channel_plotter(frame)
+ plotter.set_x_grid(-1, 1, .2)
+ plotter.set_y_grid(-1, 1, .4)
+ vbox.Add(plotter, 1, wx.EXPAND)
+
+ frame.SetSizerAndFit(vbox)
+ frame.SetSize(wx.Size(800, 600))
+ frame.Show()
+ app.MainLoop()
diff --git a/gr-wxgui/src/python/plotter/common.py b/gr-wxgui/src/python/plotter/common.py
new file mode 100644
index 000000000..88215e039
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/common.py
@@ -0,0 +1,133 @@
+#
+# 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))
+ self._plotter.Bind(wx.EVT_RIGHT_DOWN, lambda evt: plotter.enable_point_label(not plotter.enable_point_label()))
+ self._plotter.Bind(wx.EVT_LEFT_DOWN, lambda evt: plotter.call_freq_callback(evt.GetPosition()))
+ #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/gltext.py b/gr-wxgui/src/python/plotter/gltext.py
new file mode 100644
index 000000000..0b6e3f55b
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/gltext.py
@@ -0,0 +1,503 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+#
+# Provides some text display functions for wx + ogl
+# Copyright (C) 2007 Christian Brugger, Stefan Hacker
+#
+# This program 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 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import wx
+from OpenGL.GL import *
+
+"""
+Optimize with psyco if possible, this gains us about 50% speed when
+creating our textures in trade for about 4MBytes of additional memory usage for
+psyco. If you don't like loosing the memory you have to turn the lines following
+"enable psyco" into a comment while uncommenting the line after "Disable psyco".
+"""
+#Try to enable psyco
+try:
+ import psyco
+ psyco_optimized = False
+except ImportError:
+ psyco = None
+
+#Disable psyco
+#psyco = None
+
+class TextElement(object):
+ """
+ A simple class for using system Fonts to display
+ text in an OpenGL scene
+ """
+ def __init__(self,
+ text = '',
+ font = None,
+ foreground = wx.BLACK,
+ centered = False):
+ """
+ text (String) - Text
+ font (wx.Font) - Font to draw with (None = System default)
+ foreground (wx.Color) - Color of the text
+ or (wx.Bitmap)- Bitmap to overlay the text with
+ centered (bool) - Center the text
+
+ Initializes the TextElement
+ """
+ # save given variables
+ self._text = text
+ self._lines = text.split('\n')
+ self._font = font
+ self._foreground = foreground
+ self._centered = centered
+
+ # init own variables
+ self._owner_cnt = 0 #refcounter
+ self._texture = None #OpenGL texture ID
+ self._text_size = None #x/y size tuple of the text
+ self._texture_size= None #x/y Texture size tuple
+
+ # create Texture
+ self.createTexture()
+
+
+ #---Internal helpers
+
+ def _getUpper2Base(self, value):
+ """
+ Returns the lowest value with the power of
+ 2 greater than 'value' (2^n>value)
+ """
+ base2 = 1
+ while base2 < value:
+ base2 *= 2
+ return base2
+
+ #---Functions
+
+ def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
+ """
+ position (wx.Point) - x/y Position to draw in scene
+ scale (float) - Scale
+ rotation (int) - Rotation in degree
+
+ Draws the text to the scene
+ """
+ #Enable necessary functions
+ glColor(1,1,1,1)
+ glEnable(GL_TEXTURE_2D)
+ glEnable(GL_ALPHA_TEST) #Enable alpha test
+ glAlphaFunc(GL_GREATER, 0)
+ glEnable(GL_BLEND) #Enable blending
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
+ #Bind texture
+ glBindTexture(GL_TEXTURE_2D, self._texture)
+
+ ow, oh = self._text_size
+ w , h = self._texture_size
+ #Perform transformations
+ glPushMatrix()
+ glTranslated(position.x, position.y, 0)
+ glRotate(-rotation, 0, 0, 1)
+ glScaled(scale, scale, scale)
+ if self._centered:
+ glTranslate(-w/2, -oh/2, 0)
+ #Draw vertices
+ glBegin(GL_QUADS)
+ glTexCoord2f(0,0); glVertex2f(0,0)
+ glTexCoord2f(0,1); glVertex2f(0,h)
+ glTexCoord2f(1,1); glVertex2f(w,h)
+ glTexCoord2f(1,0); glVertex2f(w,0)
+ glEnd()
+ glPopMatrix()
+
+ #Disable features
+ glDisable(GL_BLEND)
+ glDisable(GL_ALPHA_TEST)
+ glDisable(GL_TEXTURE_2D)
+
+ def createTexture(self):
+ """
+ Creates a texture from the settings saved in TextElement, to be able to use normal
+ system fonts conviently a wx.MemoryDC is used to draw on a wx.Bitmap. As wxwidgets
+ device contexts don't support alpha at all it is necessary to apply a little hack
+ to preserve antialiasing without sticking to a fixed background color:
+
+ We draw the bmp in b/w mode so we can use its data as a alpha channel for a solid
+ color bitmap which after GL_ALPHA_TEST and GL_BLEND will show a nicely antialiased
+ text on any surface.
+
+ To access the raw pixel data the bmp gets converted to a wx.Image. Now we just have
+ to merge our foreground color with the alpha data we just created and push it all
+ into a OpenGL texture and we are DONE *inhalesdelpy*
+
+ DRAWBACK of the whole conversion thing is a really long time for creating the
+ texture. If you see any optimizations that could save time PLEASE CREATE A PATCH!!!
+ """
+ # get a memory dc
+ dc = wx.MemoryDC()
+
+ # set our font
+ dc.SetFont(self._font)
+
+ # Approximate extend to next power of 2 and create our bitmap
+ # REMARK: You wouldn't believe how much fucking speed this little
+ # sucker gains compared to sizes not of the power of 2. It's like
+ # 500ms --> 0.5ms (on my ATI-GPU powered Notebook). On Sams nvidia
+ # machine there don't seem to occur any losses...bad drivers?
+ ow, oh = dc.GetMultiLineTextExtent(self._text)[:2]
+ w, h = self._getUpper2Base(ow), self._getUpper2Base(oh)
+
+ self._text_size = wx.Size(ow,oh)
+ self._texture_size = wx.Size(w,h)
+ bmp = wx.EmptyBitmap(w,h)
+
+
+ #Draw in b/w mode to bmp so we can use it as alpha channel
+ dc.SelectObject(bmp)
+ dc.SetBackground(wx.BLACK_BRUSH)
+ dc.Clear()
+ dc.SetTextForeground(wx.WHITE)
+ x,y = 0,0
+ centered = self.centered
+ for line in self._lines:
+ if not line: line = ' '
+ tw, th = dc.GetTextExtent(line)
+ if centered:
+ x = int(round((w-tw)/2))
+ dc.DrawText(line, x, y)
+ x = 0
+ y += th
+ #Release the dc
+ dc.SelectObject(wx.NullBitmap)
+ del dc
+
+ #Generate a correct RGBA data string from our bmp
+ """
+ NOTE: You could also use wx.AlphaPixelData to access the pixel data
+ in 'bmp' directly, but the iterator given by it is much slower than
+ first converting to an image and using wx.Image.GetData().
+ """
+ img = wx.ImageFromBitmap(bmp)
+ alpha = img.GetData()
+
+ if isinstance(self._foreground, wx.Colour):
+ """
+ If we have a static color...
+ """
+ r,g,b = self._foreground.Get()
+ color = "%c%c%c" % (chr(r), chr(g), chr(b))
+
+ data = ''
+ for i in xrange(0, len(alpha)-1, 3):
+ data += color + alpha[i]
+
+ elif isinstance(self._foreground, wx.Bitmap):
+ """
+ If we have a bitmap...
+ """
+ bg_img = wx.ImageFromBitmap(self._foreground)
+ bg = bg_img.GetData()
+ bg_width = self._foreground.GetWidth()
+ bg_height = self._foreground.GetHeight()
+
+ data = ''
+
+ for y in xrange(0, h):
+ for x in xrange(0, w):
+ if (y > (bg_height-1)) or (x > (bg_width-1)):
+ color = "%c%c%c" % (chr(0),chr(0),chr(0))
+ else:
+ pos = (x+y*bg_width) * 3
+ color = bg[pos:pos+3]
+ data += color + alpha[(x+y*w)*3]
+
+
+ # now convert it to ogl texture
+ self._texture = glGenTextures(1)
+ glBindTexture(GL_TEXTURE_2D, self._texture)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 2)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
+
+ def deleteTexture(self):
+ """
+ Deletes the OpenGL texture object
+ """
+ if self._texture:
+ if glIsTexture(self._texture):
+ glDeleteTextures(self._texture)
+ else:
+ self._texture = None
+
+ def bind(self):
+ """
+ Increase refcount
+ """
+ self._owner_cnt += 1
+
+ def release(self):
+ """
+ Decrease refcount
+ """
+ self._owner_cnt -= 1
+
+ def isBound(self):
+ """
+ Return refcount
+ """
+ return self._owner_cnt
+
+ def __del__(self):
+ """
+ Destructor
+ """
+ self.deleteTexture()
+
+ #---Getters/Setters
+
+ def getText(self): return self._text
+ def getFont(self): return self._font
+ def getForeground(self): return self._foreground
+ def getCentered(self): return self._centered
+ def getTexture(self): return self._texture
+ def getTexture_size(self): return self._texture_size
+
+ def getOwner_cnt(self): return self._owner_cnt
+ def setOwner_cnt(self, value):
+ self._owner_cnt = value
+
+ #---Properties
+
+ text = property(getText, None, None, "Text of the object")
+ font = property(getFont, None, None, "Font of the object")
+ foreground = property(getForeground, None, None, "Color of the text")
+ centered = property(getCentered, None, None, "Is text centered")
+ owner_cnt = property(getOwner_cnt, setOwner_cnt, None, "Owner count")
+ texture = property(getTexture, None, None, "Used texture")
+ texture_size = property(getTexture_size, None, None, "Size of the used texture")
+
+
+class Text(object):
+ """
+ A simple class for using System Fonts to display text in
+ an OpenGL scene. The Text adds a global Cache of already
+ created text elements to TextElement's base functionality
+ so you can save some memory and increase speed
+ """
+ _texts = [] #Global cache for TextElements
+
+ def __init__(self,
+ text = 'Text',
+ font = None,
+ font_size = 8,
+ foreground = wx.BLACK,
+ centered = False,
+ bold = False):
+ """
+ text (string) - displayed text
+ font (wx.Font) - if None, system default font will be used with font_size
+ font_size (int) - font size in points
+ foreground (wx.Color) - Color of the text
+ or (wx.Bitmap) - Bitmap to overlay the text with
+ centered (bool) - should the text drawn centered towards position?
+
+ Initializes the text object
+ """
+ #Init/save variables
+ self._aloc_text = None
+ self._text = text
+ self._font_size = font_size
+ self._foreground= foreground
+ self._centered = centered
+
+ #Check if we are offered a font
+ if not font:
+ #if not use the system default
+ self._font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
+ else:
+ #save it
+ self._font = font
+
+ if bold: self._font.SetWeight(wx.FONTWEIGHT_BOLD)
+
+ #Bind us to our texture
+ self._initText()
+
+ #---Internal helpers
+
+ def _initText(self):
+ """
+ Initializes/Reinitializes the Text object by binding it
+ to a TextElement suitable for its current settings
+ """
+ #Check if we already bound to a texture
+ if self._aloc_text:
+ #if so release it
+ self._aloc_text.release()
+ if not self._aloc_text.isBound():
+ self._texts.remove(self._aloc_text)
+ self._aloc_text = None
+
+ #Adjust our font
+ self._font.SetPointSize(self._font_size)
+
+ #Search for existing element in our global buffer
+ for element in self._texts:
+ if element.text == self._text and\
+ element.font == self._font and\
+ element.foreground == self._foreground and\
+ element.centered == self._centered:
+ # We already exist in global buffer ;-)
+ element.bind()
+ self._aloc_text = element
+ break
+
+ if not self._aloc_text:
+ # We are not in the global buffer, let's create ourselves
+ aloc_text = self._aloc_text = TextElement(self._text,
+ self._font,
+ self._foreground,
+ self._centered)
+ aloc_text.bind()
+ self._texts.append(aloc_text)
+
+ def __del__(self):
+ """
+ Destructor
+ """
+ aloc_text = self._aloc_text
+ aloc_text.release()
+ if not aloc_text.isBound():
+ self._texts.remove(aloc_text)
+
+ #---Functions
+
+ def draw_text(self, position = wx.Point(0,0), scale = 1.0, rotation = 0):
+ """
+ position (wx.Point) - x/y Position to draw in scene
+ scale (float) - Scale
+ rotation (int) - Rotation in degree
+
+ Draws the text to the scene
+ """
+
+ self._aloc_text.draw_text(position, scale, rotation)
+
+ #---Setter/Getter
+
+ def getText(self): return self._text
+ def setText(self, value, reinit = True):
+ """
+ value (bool) - New Text
+ reinit (bool) - Create a new texture
+
+ Sets a new text
+ """
+ self._text = value
+ if reinit:
+ self._initText()
+
+ def getFont(self): return self._font
+ def setFont(self, value, reinit = True):
+ """
+ value (bool) - New Font
+ reinit (bool) - Create a new texture
+
+ Sets a new font
+ """
+ self._font = value
+ if reinit:
+ self._initText()
+
+ def getFont_size(self): return self._font_size
+ def setFont_size(self, value, reinit = True):
+ """
+ value (bool) - New font size
+ reinit (bool) - Create a new texture
+
+ Sets a new font size
+ """
+ self._font_size = value
+ if reinit:
+ self._initText()
+
+ def getForeground(self): return self._foreground
+ def setForeground(self, value, reinit = True):
+ """
+ value (bool) - New centered value
+ reinit (bool) - Create a new texture
+
+ Sets a new value for 'centered'
+ """
+ self._foreground = value
+ if reinit:
+ self._initText()
+
+ def getCentered(self): return self._centered
+ def setCentered(self, value, reinit = True):
+ """
+ value (bool) - New centered value
+ reinit (bool) - Create a new texture
+
+ Sets a new value for 'centered'
+ """
+ self._centered = value
+ if reinit:
+ self._initText()
+
+ def get_size(self):
+ """
+ Returns a text size tuple
+ """
+ return self._aloc_text._text_size
+
+ def getTexture_size(self):
+ """
+ Returns a texture size tuple
+ """
+ return self._aloc_text.texture_size
+
+ def getTextElement(self):
+ """
+ Returns the text element bound to the Text class
+ """
+ return self._aloc_text
+
+ def getTexture(self):
+ """
+ Returns the texture of the bound TextElement
+ """
+ return self._aloc_text.texture
+
+
+ #---Properties
+
+ text = property(getText, setText, None, "Text of the object")
+ font = property(getFont, setFont, None, "Font of the object")
+ font_size = property(getFont_size, setFont_size, None, "Font size")
+ foreground = property(getForeground, setForeground, None, "Color/Overlay bitmap of the text")
+ centered = property(getCentered, setCentered, None, "Display the text centered")
+ texture_size = property(getTexture_size, None, None, "Size of the used texture")
+ texture = property(getTexture, None, None, "Texture of bound TextElement")
+ text_element = property(getTextElement,None , None, "TextElement bound to this class")
+
+#Optimize critical functions
+if psyco and not psyco_optimized:
+ psyco.bind(TextElement.createTexture)
+ psyco_optimized = True
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..f1bc8f546
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/grid_plotter_base.py
@@ -0,0 +1,419 @@
+#
+# 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, 0.5, 0.75)
+POINT_LABEL_PADDING = 3
+POINT_LABEL_OFFSET = 10
+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.enable_grid_aspect_ratio(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 call_freq_callback(self, coor):
+ 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
+ self._call_callback(x_val, y_val)
+
+ def enable_grid_aspect_ratio(self, enable=None):
+ """
+ Enable/disable the grid aspect ratio.
+ If enabled, enforce the aspect ratio on the padding:
+ horizontal_padding:vertical_padding == width:height
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_grid_aspect_ratio
+ self.lock()
+ self._enable_grid_aspect_ratio = enable
+ for cache in self._gl_caches: cache.changed(True)
+ 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)
+ #enforce padding aspect ratio if enabled
+ if self.enable_grid_aspect_ratio():
+ w_over_h_ratio = float(self.width)/float(self.height)
+ horizontal_padding = float(self.padding_right + self.padding_left)
+ veritical_padding = float(self.padding_top + self.padding_bottom)
+ if w_over_h_ratio > horizontal_padding/veritical_padding:
+ #increase the horizontal padding
+ new_padding = veritical_padding*w_over_h_ratio - horizontal_padding
+ #distribute the padding to left and right
+ self.padding_left += int(round(new_padding/2))
+ self.padding_right += int(round(new_padding/2))
+ else:
+ #increase the vertical padding
+ new_padding = horizontal_padding/w_over_h_ratio - veritical_padding
+ #distribute the padding to top and bottom
+ self.padding_top += int(round(new_padding/2))
+ self.padding_bottom += int(round(new_padding/2))
+ ##################################################
+ # 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()
+ #enable transparency
+ GL.glEnable(GL.GL_BLEND)
+ GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
+ #draw rect + text
+ GL.glColor4f(*POINT_LABEL_COLOR_SPEC)
+ if x > self.width/2: x -= w+2*POINT_LABEL_PADDING + POINT_LABEL_OFFSET
+ else: x += POINT_LABEL_OFFSET
+ 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
new file mode 100644
index 000000000..6d9463458
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/plotter_base.py
@@ -0,0 +1,214 @@
+#
+# Copyright 2008, 2009, 2010 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
+
+BACKGROUND_COLOR_SPEC = (1, 0.976, 1, 1) #creamy white
+
+##################################################
+# 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, common.mutex):
+ """
+ Plotter base class for all plot types.
+ """
+
+ def __init__(self, parent):
+ """
+ Create a new plotter base.
+ Initialize the GLCanvas with double buffering.
+ Initialize various plotter flags.
+ Bind the paint and size events.
+ @param parent the parent widgit
+ """
+ attribList = (wx.glcanvas.WX_GL_DOUBLEBUFFER, wx.glcanvas.WX_GL_RGBA)
+ wx.glcanvas.GLCanvas.__init__(self, parent, wx.ID_ANY, attribList); # Specifically use the CTOR which does NOT create an implicit GL context
+ self._gl_ctx = wx.glcanvas.GLContext(self) # Create the explicit GL context
+ self.use_persistence=False
+ self.persist_alpha=2.0/15
+ self.clear_accum=True
+ self._gl_init_flag = False
+ self._resized_flag = True
+ 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 set_use_persistence(self,enable):
+ self.use_persistence=enable
+ self.clear_accum=True
+
+ def set_persist_alpha(self,analog_alpha):
+ self.persist_alpha=analog_alpha
+
+ 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.clear_accum=True
+ self.unlock()
+
+ def _on_paint(self, event):
+ """
+ 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.
+ """
+ if not self.IsShownOnScreen(): # Cannot realise a GL context on OS X if window is not yet shown
+ return
+ # create device context (needed on Windows, noop on X)
+ dc = None
+ if event.GetEventObject(): # Only create DC if paint triggered by WM message (for OS X)
+ dc = wx.PaintDC(self)
+ self.lock()
+ self.SetCurrent(self._gl_ctx) # Real the explicit GL context
+
+ # check if gl was initialized
+ if not self._gl_init_flag:
+ 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.width, self.height = self.GetSize()
+ 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
+
+ # clear buffer if needed
+ if self.clear_accum or not self.use_persistence:
+ GL.glClear(GL.GL_COLOR_BUFFER_BIT)
+ self.clear_accum=False
+
+ # apply fading
+ if self.use_persistence:
+ GL.glEnable(GL.GL_BLEND)
+ GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
+
+ GL.glBegin(GL.GL_QUADS)
+ GL.glColor4f(1,1,1,self.persist_alpha)
+ GL.glVertex2f(0, self.height)
+ GL.glVertex2f(self.width, self.height)
+ GL.glVertex2f(self.width, 0)
+ GL.glVertex2f(0, 0)
+ GL.glEnd()
+
+ GL.glDisable(GL.GL_BLEND)
+
+ # draw functions
+ for fcn in self._draw_fcns: fcn[1]()
+
+ # show result
+ self.SwapBuffers()
+ self.unlock()
+
+ def update(self):
+ """
+ Force a paint event.
+ """
+ if not self._gl_init_flag: return
+ wx.PostEvent(self, wx.PaintEvent())
diff --git a/gr-wxgui/src/python/plotter/waterfall_plotter.py b/gr-wxgui/src/python/plotter/waterfall_plotter.py
new file mode 100644
index 000000000..6a6bf6330
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py
@@ -0,0 +1,278 @@
+#
+# Copyright 2008, 2009, 2010 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
+import gltext
+import math
+import struct
+
+LEGEND_LEFT_PAD = 7
+LEGEND_NUM_BLOCKS = 256
+LEGEND_NUM_LABELS = 9
+LEGEND_WIDTH = 8
+LEGEND_FONT_SIZE = 8
+LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
+MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left
+
+ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
+
+pack_color = lambda x: struct.unpack('I', struct.pack('BBBB', *x))[0]
+unpack_color = lambda x: struct.unpack('BBBB', struct.pack('I', int(x)))
+
+def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
+ """
+ Get an array of 256 rgba values where each index maps to a color.
+ The scaling for red, green, blue, alpha are specified in piece-wise functions.
+ The piece-wise functions consist of a set of x, y coordinates.
+ The x and y values of the coordinates range from 0 to 1.
+ The coordinates must be specified so that x increases with the index value.
+ Resulting values are calculated along the line formed between 2 coordinates.
+ @param *_pts an array of x,y coordinates for each color element
+ @return array of rbga values (4 bytes) each
+ """
+ def _fcn(x, pw):
+ for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
+ #linear interpolation
+ if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
+ raise Exception
+ return numpy.array([pack_color(map(
+ lambda pw: int(255*_fcn(i/255.0, pw)),
+ (red_pts, green_pts, blue_pts, alpha_pts),
+ )) for i in range(0, 256)], numpy.uint32)
+
+COLORS = {
+ 'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
+ red_pts = [(0, 0), (.5, 0), (1, 1)],
+ green_pts = [(0, 0), (.5, 1), (1, 0)],
+ blue_pts = [(0, 1), (.5, 0), (1, 0)],
+ ),
+ 'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
+ red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
+ green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
+ blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
+ ),
+ 'rgb3': _get_rbga(
+ red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
+ green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
+ blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
+ ),
+ 'gray': _get_rbga(
+ red_pts = [(0, 0), (1, 1)],
+ green_pts = [(0, 0), (1, 1)],
+ blue_pts = [(0, 0), (1, 1)],
+ ),
+}
+
+##################################################
+# Waterfall Plotter
+##################################################
+class waterfall_plotter(grid_plotter_base):
+ def __init__(self, parent):
+ """
+ Create a new channel plotter.
+ """
+ #init
+ 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
+ self._fft_size = 1
+ self._buffer = list()
+ self._pointer = 0
+ self._counter = 0
+ self.set_num_lines(0)
+ self.set_color_mode(COLORS.keys()[0])
+ self.callback = None
+
+ def _init_waterfall(self):
+ """
+ Run gl initialization tasks.
+ """
+ self._waterfall_texture = GL.glGenTextures(1)
+
+ def _draw_waterfall(self):
+ """
+ Draw the waterfall from the texture.
+ 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
+ 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:
+ 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
+ GL.glEnable(GL.GL_TEXTURE_2D)
+ GL.glPushMatrix()
+ #matrix scaling
+ 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
+ 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)
+ 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):
+ """
+ Get the text the will populate the point label.
+ Give the X value for the current point.
+ @param x_val the current x value
+ @param y_val the current y value
+ @return a value string with units
+ """
+ return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
+
+ def _call_callback(self, x_val, y_val):
+ if self.callback != None:
+ self.callback(x_val,y_val)
+
+ def set_callback(self,callback):
+ self.callback = callback
+
+ def _draw_legend(self):
+ """
+ Draw the color scale legend.
+ """
+ if not self._color_mode: return
+ legend_height = self.height-self.padding_top-self.padding_bottom
+ #draw each legend block
+ block_height = float(legend_height)/LEGEND_NUM_BLOCKS
+ x = self.width - self.padding_right + LEGEND_LEFT_PAD
+ for i in range(LEGEND_NUM_BLOCKS):
+ color = unpack_color(COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))])
+ GL.glColor4f(*numpy.array(color)/255.0)
+ 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
+ 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)
+ x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
+ for i in range(LEGEND_NUM_LABELS):
+ proportion = i/float(LEGEND_NUM_LABELS-1)
+ dB = proportion*(self._maximum - self._minimum) + self._minimum
+ y = self.height - i*label_spacing - self.padding_bottom
+ txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
+ txt.draw_text(wx.Point(x, y))
+
+ def _resize_texture(self, flag=None):
+ """
+ Create the texture to fit the fft_size X num_lines.
+ @param flag the set/unset or update flag
+ """
+ if flag is not None:
+ self._resize_texture_flag = flag
+ return
+ if not self._resize_texture_flag: return
+ self._buffer = list()
+ self._pointer = 0
+ if self._num_lines and self._fft_size:
+ GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
+ data = numpy.zeros(self._num_lines*ceil_log2(self._fft_size)*4, numpy.uint8).tostring()
+ 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):
+ """
+ Set the color mode.
+ New samples will be converted to the new color mode.
+ Old samples will not be recolorized.
+ @param color_mode the new color mode string
+ """
+ self.lock()
+ if color_mode in COLORS.keys():
+ self._color_mode = color_mode
+ self._legend_cache.changed(True)
+ self.update()
+ self.unlock()
+
+ def set_num_lines(self, num_lines):
+ """
+ Set number of lines.
+ Powers of two only.
+ @param num_lines the new number of lines
+ """
+ self.lock()
+ self._num_lines = num_lines
+ self._resize_texture(True)
+ self.update()
+ self.unlock()
+
+ def set_samples(self, samples, minimum, maximum):
+ """
+ Set the samples to the waterfall.
+ Convert the samples to color data.
+ @param samples the array of floats
+ @param minimum the minimum value to scale
+ @param maximum the maximum value to scale
+ """
+ self.lock()
+ #set the min, max values
+ if self._minimum != minimum or self._maximum != maximum:
+ self._minimum = minimum
+ self._maximum = maximum
+ self._legend_cache.changed(True)
+ if self._fft_size != len(samples):
+ self._fft_size = len(samples)
+ self._resize_texture(True)
+ #normalize the samples to min/max
+ samples = (samples - minimum)*float(255/(maximum-minimum))
+ samples = numpy.clip(samples, 0, 255) #clip
+ samples = numpy.array(samples, numpy.uint8)
+ #convert the samples to RGBA data
+ data = COLORS[self._color_mode][samples].tostring()
+ self._buffer.append(data)
+ self._waterfall_cache.changed(True)
+ self.unlock()