diff options
Diffstat (limited to 'gr-wxgui/src/python/plotter')
-rw-r--r-- | gr-wxgui/src/python/plotter/Makefile.am | 37 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/__init__.py | 23 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/channel_plotter.py | 225 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/gltext.py | 503 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/plotter_base.py | 390 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/waterfall_plotter.py | 282 |
6 files changed, 1460 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/plotter/Makefile.am b/gr-wxgui/src/python/plotter/Makefile.am new file mode 100644 index 000000000..ada506794 --- /dev/null +++ b/gr-wxgui/src/python/plotter/Makefile.am @@ -0,0 +1,37 @@ +# +# Copyright 2004,2005,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. +# + +include $(top_srcdir)/Makefile.common + +# Install this stuff so that it ends up as the gnuradio.wxgui module +# This usually ends up at: +# ${prefix}/lib/python${python_version}/site-packages/gnuradio/wxgui + +ourpythondir = $(grpythondir)/wxgui/plotter +ourlibdir = $(grpyexecdir)/wxgui/plotter + +ourpython_PYTHON = \ + __init__.py \ + channel_plotter.py \ + gltext.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 new file mode 100644 index 000000000..12f8b3450 --- /dev/null +++ b/gr-wxgui/src/python/plotter/__init__.py @@ -0,0 +1,23 @@ +# +# 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 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..22126bd0b --- /dev/null +++ b/gr-wxgui/src/python/plotter/channel_plotter.py @@ -0,0 +1,225 @@ +# +# 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. +# + +import wx +from plotter_base import grid_plotter_base +from OpenGL.GL import * +from gnuradio.wxgui 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 +#constants for the waveform storage +SAMPLES_KEY = 'samples' +COLOR_SPEC_KEY = 'color_spec' +MARKERY_KEY = 'marker' + +################################################## +# 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, PADDING) + self._channels = dict() + self.enable_legend(False) + + def _gl_init(self): + """! + Run gl initialization tasks. + """ + glEnableClientState(GL_VERTEX_ARRAY) + self._grid_compiled_list_id = glGenLists(1) + + 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.changed(True) + self.unlock() + + def draw(self): + """! + Draw the grid and waveforms. + """ + 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( + 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) + #use opengl to scale the waveform + glPushMatrix() + glTranslatef(self.padding_left, self.padding_top, 0) + glScalef( + (self.width-self.padding_left-self.padding_right), + (self.height-self.padding_top-self.padding_bottom), + 1, + ) + 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 + 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) + #draw the points/lines + glColor3f(*self._channels[channel][COLOR_SPEC_KEY]) + marker = self._channels[channel][MARKERY_KEY] + if marker: glPointSize(marker) + glVertexPointer(2, GL_FLOAT, 0, points) + glDrawArrays(marker is None and GL_LINE_STRIP or GL_POINTS, 0, len(points)) + glPopMatrix() + + 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 %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, + ) + 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_scalar-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) + return label_str + + 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 + 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 set_waveform(self, channel, samples, color_spec, marker=None): + """! + 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 + """ + self.lock() + if channel not in self._channels.keys(): self.changed(True) + self._channels[channel] = { + SAMPLES_KEY: samples, + COLOR_SPEC_KEY: color_spec, + MARKERY_KEY: marker, + } + 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/gltext.py b/gr-wxgui/src/python/plotter/gltext.py new file mode 100644 index 000000000..67f62ca56 --- /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.Color):
+ """
+ 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/plotter_base.py b/gr-wxgui/src/python/plotter/plotter_base.py new file mode 100644 index 000000000..96a1869da --- /dev/null +++ b/gr-wxgui/src/python/plotter/plotter_base.py @@ -0,0 +1,390 @@ +# +# 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. +# + +import wx +import wx.glcanvas +from OpenGL.GL import * +from gnuradio.wxgui import common +import threading +import gltext +import math +import time + +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 + +################################################## +# OpenGL WX Plotter Canvas +################################################## +class _plotter_base(wx.glcanvas.GLCanvas): + """! + Plotter base class for all plot types. + """ + + def __init__(self, parent): + """! + Create a new plotter base. + Initialize GL and register events. + @param parent the parent widgit + """ + self._semaphore = threading.Semaphore(1) + wx.glcanvas.GLCanvas.__init__(self, parent, -1) + self.changed(False) + self._gl_init_flag = False + self._resized_flag = True + self._update_ts = 0 + self.Bind(wx.EVT_PAINT, self._on_paint) + self.Bind(wx.EVT_SIZE, self._on_size) + + def lock(self): self._semaphore.acquire(True) + def unlock(self): self._semaphore.release() + + def _on_size(self, event): + """! + Flag the resize event. + The paint event will handle the actual resizing. + """ + self._resized_flag = True + + def _on_paint(self, event): + """! + Respond to paint events, call update. + Initialize GL if this is the first paint event. + """ + self.SetCurrent() + #check if gl was initialized + if not self._gl_init_flag: + glClearColor(*BACKGROUND_COLOR_SPEC) + self._gl_init() + self._gl_init_flag = True + #check for a change in window size + if self._resized_flag: + self.lock() + self.width, self.height = self.GetSize() + glViewport(0, 0, self.width, self.height) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + glOrtho(0, self.width, self.height, 0, 1, 0) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + glViewport(0, 0, self.width, self.height) + self._resized_flag = False + self.changed(True) + self.unlock() + self.draw() + + 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) + glBegin(GL_LINE_LOOP) + glVertex3f(self.padding_left, self.padding_top, 0) + glVertex3f(self.width - self.padding_right, self.padding_top, 0) + glVertex3f(self.width - self.padding_right, self.height - self.padding_bottom, 0) + glVertex3f(self.padding_left, self.height - self.padding_bottom, 0) + glEnd() + ################################################## + # 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 new file mode 100644 index 000000000..88e2b4dc1 --- /dev/null +++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py @@ -0,0 +1,282 @@ +# +# 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. +# + +import wx +from plotter_base import grid_plotter_base +from OpenGL.GL import * +from gnuradio.wxgui import common +import numpy +import gltext +import math + +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 +PADDING = 35, 60, 40, 60 #top, right, bottom, left + +ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2))) + +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(map( + lambda pw: int(255*_fcn(i/255.0, pw)), + (red_pts, green_pts, blue_pts, alpha_pts), + ), numpy.uint8).tostring() for i in range(0, 256) + ] + +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, PADDING) + 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]) + + def _gl_init(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() + + 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. + """ + #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) + #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)) + self._pointer = (self._pointer + 1)%self._num_lines + #begin drawing + glEnable(GL_TEXTURE_2D) + glPushMatrix() + #matrix scaling + glTranslatef(self.padding_left+1, self.padding_top, 0) + glScalef( + float(self.width-self.padding_left-self.padding_right-1), + float(self.height-self.padding_top-self.padding_bottom-1), + 1.0, + ) + #draw texture with wrapping + glBegin(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) + + 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 %s'%(self.x_label, common.label_format(x_val), self.x_units) + + 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 = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))] + 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) + 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: + glBindTexture(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) + 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.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.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 = numpy.choose(samples, COLORS[self._color_mode]).tostring() + self._buffer.append(data) + self.unlock() |