2 # Copyright 2008, 2009 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 grid_plotter_base import grid_plotter_base
31 LEGEND_NUM_BLOCKS = 256
35 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
36 MIN_PADDING = 0, 60, 0, 0 #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, MIN_PADDING)
96 self._legend_cache = self.new_gl_cache(self._draw_legend)
97 #setup waterfall cache
98 self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50)
99 #setup waterfall plotter
100 self.register_init(self._init_waterfall)
101 self._resize_texture(False)
105 self._buffer = list()
108 self.set_num_lines(0)
109 self.set_color_mode(COLORS.keys()[0])
111 def _init_waterfall(self):
113 Run gl initialization tasks.
115 self._waterfall_texture = GL.glGenTextures(1)
117 def _draw_waterfall(self):
119 Draw the waterfall from the texture.
120 The texture is circularly filled and will wrap around.
121 Use matrix modeling to shift and scale the texture onto the coordinate plane.
124 self._resize_texture()
126 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
127 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
128 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
129 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
130 GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE)
131 #write the buffer to the texture
133 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))
134 self._pointer = (self._pointer + 1)%self._num_lines
136 GL.glEnable(GL.GL_TEXTURE_2D)
139 GL.glTranslatef(self.padding_left, self.padding_top, 0)
141 float(self.width-self.padding_left-self.padding_right),
142 float(self.height-self.padding_top-self.padding_bottom),
145 #draw texture with wrapping
146 GL.glBegin(GL.GL_QUADS)
147 prop_y = float(self._pointer)/(self._num_lines-1)
148 prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
149 off = 1.0/(self._num_lines-1)
150 GL.glTexCoord2f(0, prop_y+1-off)
152 GL.glTexCoord2f(prop_x, prop_y+1-off)
154 GL.glTexCoord2f(prop_x, prop_y)
156 GL.glTexCoord2f(0, prop_y)
160 GL.glDisable(GL.GL_TEXTURE_2D)
162 def _populate_point_label(self, x_val, y_val):
164 Get the text the will populate the point label.
165 Give the X value for the current point.
166 @param x_val the current x value
167 @param y_val the current y value
168 @return a value string with units
170 return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
172 def _draw_legend(self):
174 Draw the color scale legend.
176 if not self._color_mode: return
177 legend_height = self.height-self.padding_top-self.padding_bottom
178 #draw each legend block
179 block_height = float(legend_height)/LEGEND_NUM_BLOCKS
180 x = self.width - self.padding_right + LEGEND_LEFT_PAD
181 for i in range(LEGEND_NUM_BLOCKS):
182 color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))]
183 GL.glColor4f(*map(lambda c: ord(c)/255.0, color))
184 y = self.height - (i+1)*block_height - self.padding_bottom
185 self._draw_rect(x, y, LEGEND_WIDTH, block_height)
186 #draw rectangle around color scale border
187 GL.glColor3f(*LEGEND_BORDER_COLOR_SPEC)
188 self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
189 #draw each legend label
190 label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
191 x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
192 for i in range(LEGEND_NUM_LABELS):
193 proportion = i/float(LEGEND_NUM_LABELS-1)
194 dB = proportion*(self._maximum - self._minimum) + self._minimum
195 y = self.height - i*label_spacing - self.padding_bottom
196 txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
197 txt.draw_text(wx.Point(x, y))
199 def _resize_texture(self, flag=None):
201 Create the texture to fit the fft_size X num_lines.
202 @param flag the set/unset or update flag
205 self._resize_texture_flag = flag
207 if not self._resize_texture_flag: return
208 self._buffer = list()
210 if self._num_lines and self._fft_size:
211 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
212 data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring()
213 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)
214 self._resize_texture_flag = False
216 def set_color_mode(self, color_mode):
219 New samples will be converted to the new color mode.
220 Old samples will not be recolorized.
221 @param color_mode the new color mode string
224 if color_mode in COLORS.keys():
225 self._color_mode = color_mode
226 self._legend_cache.changed(True)
230 def set_num_lines(self, num_lines):
234 @param num_lines the new number of lines
237 self._num_lines = num_lines
238 self._resize_texture(True)
242 def set_samples(self, samples, minimum, maximum):
244 Set the samples to the waterfall.
245 Convert the samples to color data.
246 @param samples the array of floats
247 @param minimum the minimum value to scale
248 @param maximum the maximum value to scale
251 #set the min, max values
252 if self._minimum != minimum or self._maximum != maximum:
253 self._minimum = minimum
254 self._maximum = maximum
255 self._legend_cache.changed(True)
256 if self._fft_size != len(samples):
257 self._fft_size = len(samples)
258 self._resize_texture(True)
259 #normalize the samples to min/max
260 samples = (samples - minimum)*float(255/(maximum-minimum))
261 samples = numpy.clip(samples, 0, 255) #clip
262 samples = numpy.array(samples, numpy.uint8)
263 #convert the samples to RGBA data
264 data = numpy.choose(samples, COLORS[self._color_mode]).tostring()
265 self._buffer.append(data)
266 self._waterfall_cache.changed(True)