Merged changeset r9241:9289 from jblum/glwxgui into trunk. Adds OpenGL versions...
[debian/gnuradio] / gr-wxgui / src / python / plotter / waterfall_plotter.py
1 #
2 # Copyright 2008 Free Software Foundation, Inc.
3 #
4 # This file is part of GNU Radio
5 #
6 # GNU Radio is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3, or (at your option)
9 # any later version.
10 #
11 # GNU Radio is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with GNU Radio; see the file COPYING.  If not, write to
18 # the Free Software Foundation, Inc., 51 Franklin Street,
19 # Boston, MA 02110-1301, USA.
20 #
21
22 import wx
23 from plotter_base import grid_plotter_base
24 from OpenGL.GL import *
25 from gnuradio.wxgui import common
26 import numpy
27 import gltext
28 import math
29
30 LEGEND_LEFT_PAD = 7
31 LEGEND_NUM_BLOCKS = 256
32 LEGEND_NUM_LABELS = 9
33 LEGEND_WIDTH = 8
34 LEGEND_FONT_SIZE = 8
35 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
36 PADDING = 35, 60, 40, 60 #top, right, bottom, left
37
38 ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
39
40 def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
41         """!
42         Get an array of 256 rgba values where each index maps to a color.
43         The scaling for red, green, blue, alpha are specified in piece-wise functions.
44         The piece-wise functions consist of a set of x, y coordinates.
45         The x and y values of the coordinates range from 0 to 1.
46         The coordinates must be specified so that x increases with the index value.
47         Resulting values are calculated along the line formed between 2 coordinates.
48         @param *_pts an array of x,y coordinates for each color element
49         @return array of rbga values (4 bytes) each
50         """
51         def _fcn(x, pw):
52                 for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
53                         #linear interpolation
54                         if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
55                 raise Exception
56         return [numpy.array(map(
57                         lambda pw: int(255*_fcn(i/255.0, pw)),
58                         (red_pts, green_pts, blue_pts, alpha_pts),
59                 ), numpy.uint8).tostring() for i in range(0, 256)
60         ]
61
62 COLORS = {
63         'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
64                 red_pts = [(0, 0), (.5, 0), (1, 1)],
65                 green_pts = [(0, 0), (.5, 1), (1, 0)],
66                 blue_pts = [(0, 1), (.5, 0), (1, 0)],
67         ),
68         'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
69                 red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
70                 green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
71                 blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
72         ),
73         'rgb3': _get_rbga(
74                 red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
75                 green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
76                 blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
77         ),
78         'gray': _get_rbga(
79                 red_pts = [(0, 0), (1, 1)],
80                 green_pts = [(0, 0), (1, 1)],
81                 blue_pts = [(0, 0), (1, 1)],
82         ),
83 }
84
85 ##################################################
86 # Waterfall Plotter
87 ##################################################
88 class waterfall_plotter(grid_plotter_base):
89         def __init__(self, parent):
90                 """!
91                 Create a new channel plotter.
92                 """
93                 #init
94                 grid_plotter_base.__init__(self, parent, PADDING)
95                 self._resize_texture(False)
96                 self._minimum = 0
97                 self._maximum = 0
98                 self._fft_size = 1
99                 self._buffer = list()
100                 self._pointer = 0
101                 self._counter = 0
102                 self.set_num_lines(0)
103                 self.set_color_mode(COLORS.keys()[0])
104
105         def _gl_init(self):
106                 """!
107                 Run gl initialization tasks.
108                 """
109                 self._grid_compiled_list_id = glGenLists(1)
110                 self._waterfall_texture = glGenTextures(1)
111
112         def draw(self):
113                 """!
114                 Draw the grid and waveforms.
115                 """
116                 self.lock()
117                 #resize texture
118                 self._resize_texture()
119                 #store the grid drawing operations
120                 if self.changed():
121                         glNewList(self._grid_compiled_list_id, GL_COMPILE)
122                         self._draw_grid()
123                         self._draw_legend()
124                         glEndList()
125                         self.changed(False)
126                 self.clear()
127                 #draw the grid
128                 glCallList(self._grid_compiled_list_id)
129                 self._draw_waterfall()
130                 self._draw_point_label()
131                 #swap buffer into display
132                 self.SwapBuffers()
133                 self.unlock()
134
135         def _draw_waterfall(self):
136                 """!
137                 Draw the waterfall from the texture.
138                 The texture is circularly filled and will wrap around.
139                 Use matrix modeling to shift and scale the texture onto the coordinate plane.
140                 """
141                 #setup texture
142                 glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
143                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
144                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
145                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
146                 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
147                 #write the buffer to the texture
148                 while self._buffer:
149                         glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL_RGBA, GL_UNSIGNED_BYTE, self._buffer.pop(0))
150                         self._pointer = (self._pointer + 1)%self._num_lines
151                 #begin drawing
152                 glEnable(GL_TEXTURE_2D)
153                 glPushMatrix()
154                 #matrix scaling
155                 glTranslatef(self.padding_left+1, self.padding_top, 0)
156                 glScalef(
157                         float(self.width-self.padding_left-self.padding_right-1),
158                         float(self.height-self.padding_top-self.padding_bottom-1),
159                         1.0,
160                 )
161                 #draw texture with wrapping
162                 glBegin(GL_QUADS)
163                 prop_y = float(self._pointer)/(self._num_lines-1)
164                 prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
165                 off = 1.0/(self._num_lines-1)
166                 glTexCoord2f(0, prop_y+1-off)
167                 glVertex2f(0, 1)
168                 glTexCoord2f(prop_x, prop_y+1-off)
169                 glVertex2f(1, 1)
170                 glTexCoord2f(prop_x, prop_y)
171                 glVertex2f(1, 0)
172                 glTexCoord2f(0, prop_y)
173                 glVertex2f(0, 0)
174                 glEnd()
175                 glPopMatrix()
176                 glDisable(GL_TEXTURE_2D)
177
178         def _populate_point_label(self, x_val, y_val):
179                 """!
180                 Get the text the will populate the point label.
181                 Give the X value for the current point.
182                 @param x_val the current x value
183                 @param y_val the current y value
184                 @return a value string with units
185                 """
186                 return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units)
187
188         def _draw_legend(self):
189                 """!
190                 Draw the color scale legend.
191                 """
192                 if not self._color_mode: return
193                 legend_height = self.height-self.padding_top-self.padding_bottom
194                 #draw each legend block
195                 block_height = float(legend_height)/LEGEND_NUM_BLOCKS
196                 x = self.width - self.padding_right + LEGEND_LEFT_PAD
197                 for i in range(LEGEND_NUM_BLOCKS):
198                         color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))]
199                         glColor4f(*map(lambda c: ord(c)/255.0, color))
200                         y = self.height - (i+1)*block_height - self.padding_bottom
201                         self._draw_rect(x, y, LEGEND_WIDTH, block_height)
202                 #draw rectangle around color scale border
203                 glColor3f(*LEGEND_BORDER_COLOR_SPEC)
204                 self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
205                 #draw each legend label
206                 label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
207                 x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
208                 for i in range(LEGEND_NUM_LABELS):
209                         proportion = i/float(LEGEND_NUM_LABELS-1)
210                         dB = proportion*(self._maximum - self._minimum) + self._minimum
211                         y = self.height - i*label_spacing - self.padding_bottom
212                         txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
213                         txt.draw_text(wx.Point(x, y))
214
215         def _resize_texture(self, flag=None):
216                 """!
217                 Create the texture to fit the fft_size X num_lines.
218                 @param flag the set/unset or update flag
219                 """
220                 if flag is not None: 
221                         self._resize_texture_flag = flag
222                         return
223                 if not self._resize_texture_flag: return
224                 self._buffer = list()
225                 self._pointer = 0
226                 if self._num_lines and self._fft_size:
227                         glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
228                         data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring()
229                         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
230                 self._resize_texture_flag = False
231
232         def set_color_mode(self, color_mode):
233                 """!
234                 Set the color mode.
235                 New samples will be converted to the new color mode.
236                 Old samples will not be recolorized.
237                 @param color_mode the new color mode string
238                 """
239                 self.lock()
240                 if color_mode in COLORS.keys():
241                         self._color_mode = color_mode
242                         self.changed(True)
243                 self.update()
244                 self.unlock()
245
246         def set_num_lines(self, num_lines):
247                 """!
248                 Set number of lines.
249                 Powers of two only.
250                 @param num_lines the new number of lines
251                 """
252                 self.lock()
253                 self._num_lines = num_lines
254                 self._resize_texture(True)
255                 self.update()
256                 self.unlock()
257
258         def set_samples(self, samples, minimum, maximum):
259                 """!
260                 Set the samples to the waterfall.
261                 Convert the samples to color data.
262                 @param samples the array of floats
263                 @param minimum the minimum value to scale
264                 @param maximum the maximum value to scale
265                 """
266                 self.lock()
267                 #set the min, max values
268                 if self._minimum != minimum or self._maximum != maximum:
269                         self._minimum = minimum
270                         self._maximum = maximum
271                         self.changed(True)
272                 if self._fft_size != len(samples):
273                         self._fft_size = len(samples)
274                         self._resize_texture(True)
275                 #normalize the samples to min/max
276                 samples = (samples - minimum)*float(255/(maximum-minimum))
277                 samples = numpy.clip(samples, 0, 255) #clip
278                 samples = numpy.array(samples, numpy.uint8)
279                 #convert the samples to RGBA data
280                 data = numpy.choose(samples, COLORS[self._color_mode]).tostring()
281                 self._buffer.append(data)
282                 self.unlock()