2 # Copyright 2008 Free Software Foundation, Inc.
4 # This file is part of GNU Radio
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)
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.
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.
23 from plotter_base import grid_plotter_base
24 from OpenGL.GL import *
25 from gnuradio.wxgui import common
31 LEGEND_NUM_BLOCKS = 256
35 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
36 PADDING = 35, 60, 40, 60 #top, right, bottom, left
38 ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
40 def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
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
52 for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
54 if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
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)
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)],
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)],
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)],
79 red_pts = [(0, 0), (1, 1)],
80 green_pts = [(0, 0), (1, 1)],
81 blue_pts = [(0, 0), (1, 1)],
85 ##################################################
87 ##################################################
88 class waterfall_plotter(grid_plotter_base):
89 def __init__(self, parent):
91 Create a new channel plotter.
94 grid_plotter_base.__init__(self, parent, PADDING)
95 self._resize_texture(False)
102 self.set_num_lines(0)
103 self.set_color_mode(COLORS.keys()[0])
107 Run gl initialization tasks.
109 self._grid_compiled_list_id = glGenLists(1)
110 self._waterfall_texture = glGenTextures(1)
114 Draw the grid and waveforms.
118 self._resize_texture()
119 #store the grid drawing operations
121 glNewList(self._grid_compiled_list_id, GL_COMPILE)
128 glCallList(self._grid_compiled_list_id)
129 self._draw_waterfall()
130 self._draw_point_label()
131 #swap buffer into display
135 def _draw_waterfall(self):
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.
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
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
152 glEnable(GL_TEXTURE_2D)
155 glTranslatef(self.padding_left+1, self.padding_top, 0)
157 float(self.width-self.padding_left-self.padding_right-1),
158 float(self.height-self.padding_top-self.padding_bottom-1),
161 #draw texture with wrapping
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)
168 glTexCoord2f(prop_x, prop_y+1-off)
170 glTexCoord2f(prop_x, prop_y)
172 glTexCoord2f(0, prop_y)
176 glDisable(GL_TEXTURE_2D)
178 def _populate_point_label(self, x_val, y_val):
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
186 return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units)
188 def _draw_legend(self):
190 Draw the color scale legend.
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))
215 def _resize_texture(self, flag=None):
217 Create the texture to fit the fft_size X num_lines.
218 @param flag the set/unset or update flag
221 self._resize_texture_flag = flag
223 if not self._resize_texture_flag: return
224 self._buffer = list()
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
232 def set_color_mode(self, 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
240 if color_mode in COLORS.keys():
241 self._color_mode = color_mode
246 def set_num_lines(self, num_lines):
250 @param num_lines the new number of lines
253 self._num_lines = num_lines
254 self._resize_texture(True)
258 def set_samples(self, samples, minimum, maximum):
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
267 #set the min, max values
268 if self._minimum != minimum or self._maximum != maximum:
269 self._minimum = minimum
270 self._maximum = maximum
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)