2 # Copyright 2008, 2009, 2010 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.unpack('I', struct.pack('BBBB', *x))[0]
42 unpack_color = lambda x: struct.unpack('BBBB', struct.pack('I', int(x)))
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 numpy.array([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)], numpy.uint32)
66 'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
67 red_pts = [(0, 0), (.5, 0), (1, 1)],
68 green_pts = [(0, 0), (.5, 1), (1, 0)],
69 blue_pts = [(0, 1), (.5, 0), (1, 0)],
71 'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
72 red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
73 green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
74 blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
77 red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
78 green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
79 blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
82 red_pts = [(0, 0), (1, 1)],
83 green_pts = [(0, 0), (1, 1)],
84 blue_pts = [(0, 0), (1, 1)],
88 ##################################################
90 ##################################################
91 class waterfall_plotter(grid_plotter_base):
92 def __init__(self, parent):
94 Create a new channel plotter.
97 grid_plotter_base.__init__(self, parent, MIN_PADDING)
99 self._legend_cache = self.new_gl_cache(self._draw_legend)
100 #setup waterfall cache
101 self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50)
102 #setup waterfall plotter
103 self.register_init(self._init_waterfall)
104 self._resize_texture(False)
108 self._buffer = list()
111 self.set_num_lines(0)
112 self.set_color_mode(COLORS.keys()[0])
114 def _init_waterfall(self):
116 Run gl initialization tasks.
118 self._waterfall_texture = GL.glGenTextures(1)
120 def _draw_waterfall(self):
122 Draw the waterfall from the texture.
123 The texture is circularly filled and will wrap around.
124 Use matrix modeling to shift and scale the texture onto the coordinate plane.
127 self._resize_texture()
129 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
130 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
131 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
132 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
133 GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE)
134 #write the buffer to the texture
136 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))
137 self._pointer = (self._pointer + 1)%self._num_lines
139 GL.glEnable(GL.GL_TEXTURE_2D)
142 GL.glTranslatef(self.padding_left, self.padding_top, 0)
144 float(self.width-self.padding_left-self.padding_right),
145 float(self.height-self.padding_top-self.padding_bottom),
148 #draw texture with wrapping
149 GL.glBegin(GL.GL_QUADS)
150 prop_y = float(self._pointer)/(self._num_lines-1)
151 prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
152 off = 1.0/(self._num_lines-1)
153 GL.glTexCoord2f(0, prop_y+1-off)
155 GL.glTexCoord2f(prop_x, prop_y+1-off)
157 GL.glTexCoord2f(prop_x, prop_y)
159 GL.glTexCoord2f(0, prop_y)
163 GL.glDisable(GL.GL_TEXTURE_2D)
165 def _populate_point_label(self, x_val, y_val):
167 Get the text the will populate the point label.
168 Give the X value for the current point.
169 @param x_val the current x value
170 @param y_val the current y value
171 @return a value string with units
173 return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
175 def _draw_legend(self):
177 Draw the color scale legend.
179 if not self._color_mode: return
180 legend_height = self.height-self.padding_top-self.padding_bottom
181 #draw each legend block
182 block_height = float(legend_height)/LEGEND_NUM_BLOCKS
183 x = self.width - self.padding_right + LEGEND_LEFT_PAD
184 for i in range(LEGEND_NUM_BLOCKS):
185 color = unpack_color(COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))])
186 GL.glColor4f(*numpy.array(color)/255.0)
187 y = self.height - (i+1)*block_height - self.padding_bottom
188 self._draw_rect(x, y, LEGEND_WIDTH, block_height)
189 #draw rectangle around color scale border
190 GL.glColor3f(*LEGEND_BORDER_COLOR_SPEC)
191 self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
192 #draw each legend label
193 label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
194 x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
195 for i in range(LEGEND_NUM_LABELS):
196 proportion = i/float(LEGEND_NUM_LABELS-1)
197 dB = proportion*(self._maximum - self._minimum) + self._minimum
198 y = self.height - i*label_spacing - self.padding_bottom
199 txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
200 txt.draw_text(wx.Point(x, y))
202 def _resize_texture(self, flag=None):
204 Create the texture to fit the fft_size X num_lines.
205 @param flag the set/unset or update flag
208 self._resize_texture_flag = flag
210 if not self._resize_texture_flag: return
211 self._buffer = list()
213 if self._num_lines and self._fft_size:
214 GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
215 data = numpy.zeros(self._num_lines*ceil_log2(self._fft_size)*4, numpy.uint8).tostring()
216 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)
217 self._resize_texture_flag = False
219 def set_color_mode(self, color_mode):
222 New samples will be converted to the new color mode.
223 Old samples will not be recolorized.
224 @param color_mode the new color mode string
227 if color_mode in COLORS.keys():
228 self._color_mode = color_mode
229 self._legend_cache.changed(True)
233 def set_num_lines(self, num_lines):
237 @param num_lines the new number of lines
240 self._num_lines = num_lines
241 self._resize_texture(True)
245 def set_samples(self, samples, minimum, maximum):
247 Set the samples to the waterfall.
248 Convert the samples to color data.
249 @param samples the array of floats
250 @param minimum the minimum value to scale
251 @param maximum the maximum value to scale
254 #set the min, max values
255 if self._minimum != minimum or self._maximum != maximum:
256 self._minimum = minimum
257 self._maximum = maximum
258 self._legend_cache.changed(True)
259 if self._fft_size != len(samples):
260 self._fft_size = len(samples)
261 self._resize_texture(True)
262 #normalize the samples to min/max
263 samples = (samples - minimum)*float(255/(maximum-minimum))
264 samples = numpy.clip(samples, 0, 255) #clip
265 samples = numpy.array(samples, numpy.uint8)
266 #convert the samples to RGBA data
267 data = COLORS[self._color_mode][samples].tostring()
268 self._buffer.append(data)
269 self._waterfall_cache.changed(True)