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
32 LEGEND_NUM_BLOCKS = 256
36 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
37 MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left
39 ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
41 pack_color = lambda x: struct.pack('BBBB', *x)
42 unpack_color = lambda s: struct.unpack('BBBB', s)
44 def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
46 Get an array of 256 rgba values where each index maps to a color.
47 The scaling for red, green, blue, alpha are specified in piece-wise functions.
48 The piece-wise functions consist of a set of x, y coordinates.
49 The x and y values of the coordinates range from 0 to 1.
50 The coordinates must be specified so that x increases with the index value.
51 Resulting values are calculated along the line formed between 2 coordinates.
52 @param *_pts an array of x,y coordinates for each color element
53 @return array of rbga values (4 bytes) each
56 for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
58 if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
60 return [pack_color(map(
61 lambda pw: int(255*_fcn(i/255.0, pw)),
62 (red_pts, green_pts, blue_pts, alpha_pts),
63 )) for i in range(0, 256)
67 'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
68 red_pts = [(0, 0), (.5, 0), (1, 1)],
69 green_pts = [(0, 0), (.5, 1), (1, 0)],
70 blue_pts = [(0, 1), (.5, 0), (1, 0)],
72 'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
73 red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
74 green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
75 blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
78 red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
79 green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
80 blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
83 red_pts = [(0, 0), (1, 1)],
84 green_pts = [(0, 0), (1, 1)],
85 blue_pts = [(0, 0), (1, 1)],
89 ##################################################
91 ##################################################
92 class waterfall_plotter(grid_plotter_base):
93 def __init__(self, parent):
95 Create a new channel plotter.
98 grid_plotter_base.__init__(self, parent, MIN_PADDING)
100 self._legend_cache = self.new_gl_cache(self._draw_legend)
101 #setup waterfall cache
102 self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50)
103 #setup waterfall plotter
104 self.register_init(self._init_waterfall)
105 self._resize_texture(False)
109 self._buffer = list()
112 self.set_num_lines(0)
113 self.set_color_mode(COLORS.keys()[0])
115 def _init_waterfall(self):
117 Run gl initialization tasks.
119 self._waterfall_texture = GL.glGenTextures(1)
121 def _draw_waterfall(self):
123 Draw the waterfall from the texture.
124 The texture is circularly filled and will wrap around.
125 Use matrix modeling to shift and scale the texture onto the coordinate plane.
128 self._resize_texture()
130 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
131 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
132 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
133 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
134 GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE)
135 #write the buffer to the texture
137 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))
138 self._pointer = (self._pointer + 1)%self._num_lines
140 GL.glEnable(GL.GL_TEXTURE_2D)
143 GL.glTranslatef(self.padding_left, self.padding_top, 0)
145 float(self.width-self.padding_left-self.padding_right),
146 float(self.height-self.padding_top-self.padding_bottom),
149 #draw texture with wrapping
150 GL.glBegin(GL.GL_QUADS)
151 prop_y = float(self._pointer)/(self._num_lines-1)
152 prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
153 off = 1.0/(self._num_lines-1)
154 GL.glTexCoord2f(0, prop_y+1-off)
156 GL.glTexCoord2f(prop_x, prop_y+1-off)
158 GL.glTexCoord2f(prop_x, prop_y)
160 GL.glTexCoord2f(0, prop_y)
164 GL.glDisable(GL.GL_TEXTURE_2D)
166 def _populate_point_label(self, x_val, y_val):
168 Get the text the will populate the point label.
169 Give the X value for the current point.
170 @param x_val the current x value
171 @param y_val the current y value
172 @return a value string with units
174 return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
176 def _draw_legend(self):
178 Draw the color scale legend.
180 if not self._color_mode: return
181 legend_height = self.height-self.padding_top-self.padding_bottom
182 #draw each legend block
183 block_height = float(legend_height)/LEGEND_NUM_BLOCKS
184 x = self.width - self.padding_right + LEGEND_LEFT_PAD
185 for i in range(LEGEND_NUM_BLOCKS):
186 color = unpack_color(COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))])
187 GL.glColor4f(*numpy.array(color)/255.0)
188 y = self.height - (i+1)*block_height - self.padding_bottom
189 self._draw_rect(x, y, LEGEND_WIDTH, block_height)
190 #draw rectangle around color scale border
191 GL.glColor3f(*LEGEND_BORDER_COLOR_SPEC)
192 self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
193 #draw each legend label
194 label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
195 x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
196 for i in range(LEGEND_NUM_LABELS):
197 proportion = i/float(LEGEND_NUM_LABELS-1)
198 dB = proportion*(self._maximum - self._minimum) + self._minimum
199 y = self.height - i*label_spacing - self.padding_bottom
200 txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
201 txt.draw_text(wx.Point(x, y))
203 def _resize_texture(self, flag=None):
205 Create the texture to fit the fft_size X num_lines.
206 @param flag the set/unset or update flag
209 self._resize_texture_flag = flag
211 if not self._resize_texture_flag: return
212 self._buffer = list()
214 if self._num_lines and self._fft_size:
215 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
216 data = numpy.zeros(self._num_lines*ceil_log2(self._fft_size)*4, numpy.uint8).tostring()
217 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)
218 self._resize_texture_flag = False
220 def set_color_mode(self, color_mode):
223 New samples will be converted to the new color mode.
224 Old samples will not be recolorized.
225 @param color_mode the new color mode string
228 if color_mode in COLORS.keys():
229 self._color_mode = color_mode
230 self._legend_cache.changed(True)
234 def set_num_lines(self, num_lines):
238 @param num_lines the new number of lines
241 self._num_lines = num_lines
242 self._resize_texture(True)
246 def set_samples(self, samples, minimum, maximum):
248 Set the samples to the waterfall.
249 Convert the samples to color data.
250 @param samples the array of floats
251 @param minimum the minimum value to scale
252 @param maximum the maximum value to scale
255 #set the min, max values
256 if self._minimum != minimum or self._maximum != maximum:
257 self._minimum = minimum
258 self._maximum = maximum
259 self._legend_cache.changed(True)
260 if self._fft_size != len(samples):
261 self._fft_size = len(samples)
262 self._resize_texture(True)
263 #normalize the samples to min/max
264 samples = (samples - minimum)*float(255/(maximum-minimum))
265 samples = numpy.clip(samples, 0, 255) #clip
266 samples = numpy.array(samples, numpy.uint8)
267 #convert the samples to RGBA data
268 data = ''.join([COLORS[self._color_mode][sample] for sample in samples])
269 self._buffer.append(data)
270 self._waterfall_cache.changed(True)