Imported Upstream version 3.2.2
[debian/gnuradio] / gr-wxgui / src / python / plotter / waterfall_plotter.py
1 #
2 # Copyright 2008, 2009 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 grid_plotter_base import grid_plotter_base
24 from OpenGL import GL
25 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 MIN_PADDING = 0, 60, 0, 0 #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, MIN_PADDING)
95                 #setup legend cache
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)
102                 self._minimum = 0
103                 self._maximum = 0
104                 self._fft_size = 1
105                 self._buffer = list()
106                 self._pointer = 0
107                 self._counter = 0
108                 self.set_num_lines(0)
109                 self.set_color_mode(COLORS.keys()[0])
110
111         def _init_waterfall(self):
112                 """
113                 Run gl initialization tasks.
114                 """
115                 self._waterfall_texture = GL.glGenTextures(1)
116
117         def _draw_waterfall(self):
118                 """
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.
122                 """
123                 #resize texture
124                 self._resize_texture()
125                 #setup 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
132                 while self._buffer:
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
135                 #begin drawing
136                 GL.glEnable(GL.GL_TEXTURE_2D)
137                 GL.glPushMatrix()
138                 #matrix scaling
139                 GL.glTranslatef(self.padding_left, self.padding_top, 0)
140                 GL.glScalef(
141                         float(self.width-self.padding_left-self.padding_right),
142                         float(self.height-self.padding_top-self.padding_bottom),
143                         1.0,
144                 )
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)
151                 GL.glVertex2f(0, 1)
152                 GL.glTexCoord2f(prop_x, prop_y+1-off)
153                 GL.glVertex2f(1, 1)
154                 GL.glTexCoord2f(prop_x, prop_y)
155                 GL.glVertex2f(1, 0)
156                 GL.glTexCoord2f(0, prop_y)
157                 GL.glVertex2f(0, 0)
158                 GL.glEnd()
159                 GL.glPopMatrix()
160                 GL.glDisable(GL.GL_TEXTURE_2D)
161
162         def _populate_point_label(self, x_val, y_val):
163                 """
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
169                 """
170                 return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
171
172         def _draw_legend(self):
173                 """
174                 Draw the color scale legend.
175                 """
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))
198
199         def _resize_texture(self, flag=None):
200                 """
201                 Create the texture to fit the fft_size X num_lines.
202                 @param flag the set/unset or update flag
203                 """
204                 if flag is not None: 
205                         self._resize_texture_flag = flag
206                         return
207                 if not self._resize_texture_flag: return
208                 self._buffer = list()
209                 self._pointer = 0
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
215
216         def set_color_mode(self, color_mode):
217                 """
218                 Set the 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
222                 """
223                 self.lock()
224                 if color_mode in COLORS.keys():
225                         self._color_mode = color_mode
226                         self._legend_cache.changed(True)
227                 self.update()
228                 self.unlock()
229
230         def set_num_lines(self, num_lines):
231                 """
232                 Set number of lines.
233                 Powers of two only.
234                 @param num_lines the new number of lines
235                 """
236                 self.lock()
237                 self._num_lines = num_lines
238                 self._resize_texture(True)
239                 self.update()
240                 self.unlock()
241
242         def set_samples(self, samples, minimum, maximum):
243                 """
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
249                 """
250                 self.lock()
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)
267                 self.unlock()