diff options
Diffstat (limited to 'gr-wxgui/src/python/plotter')
-rw-r--r-- | gr-wxgui/src/python/plotter/__init__.py | 24 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/bar_plotter.py | 144 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/channel_plotter.py | 236 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/common.py | 133 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/gltext.py | 503 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/grid_plotter_base.py | 419 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/plotter_base.py | 214 | ||||
-rw-r--r-- | gr-wxgui/src/python/plotter/waterfall_plotter.py | 278 |
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() |