Merge commit 'v3.3.0' into upstream
[debian/gnuradio] / gr-wxgui / src / python / plotter / waterfall_plotter.py
1 #
2 # Copyright 2008, 2009, 2010 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 import struct
30
31 LEGEND_LEFT_PAD = 7
32 LEGEND_NUM_BLOCKS = 256
33 LEGEND_NUM_LABELS = 9
34 LEGEND_WIDTH = 8
35 LEGEND_FONT_SIZE = 8
36 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
37 MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left
38
39 ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
40
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)))
43
44 def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
45         """
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
54         """
55         def _fcn(x, pw):
56                 for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
57                         #linear interpolation
58                         if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
59                 raise Exception
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)
64
65 COLORS = {
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)],
70         ),
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)],
75         ),
76         'rgb3': _get_rbga(
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)],
80         ),
81         'gray': _get_rbga(
82                 red_pts = [(0, 0), (1, 1)],
83                 green_pts = [(0, 0), (1, 1)],
84                 blue_pts = [(0, 0), (1, 1)],
85         ),
86 }
87
88 ##################################################
89 # Waterfall Plotter
90 ##################################################
91 class waterfall_plotter(grid_plotter_base):
92         def __init__(self, parent):
93                 """
94                 Create a new channel plotter.
95                 """
96                 #init
97                 grid_plotter_base.__init__(self, parent, MIN_PADDING)
98                 #setup legend cache
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)
105                 self._minimum = 0
106                 self._maximum = 0
107                 self._fft_size = 1
108                 self._buffer = list()
109                 self._pointer = 0
110                 self._counter = 0
111                 self.set_num_lines(0)
112                 self.set_color_mode(COLORS.keys()[0])
113
114         def _init_waterfall(self):
115                 """
116                 Run gl initialization tasks.
117                 """
118                 self._waterfall_texture = GL.glGenTextures(1)
119
120         def _draw_waterfall(self):
121                 """
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.
125                 """
126                 #resize texture
127                 self._resize_texture()
128                 #setup 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
135                 while self._buffer:
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
138                 #begin drawing
139                 GL.glEnable(GL.GL_TEXTURE_2D)
140                 GL.glPushMatrix()
141                 #matrix scaling
142                 GL.glTranslatef(self.padding_left, self.padding_top, 0)
143                 GL.glScalef(
144                         float(self.width-self.padding_left-self.padding_right),
145                         float(self.height-self.padding_top-self.padding_bottom),
146                         1.0,
147                 )
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)
154                 GL.glVertex2f(0, 1)
155                 GL.glTexCoord2f(prop_x, prop_y+1-off)
156                 GL.glVertex2f(1, 1)
157                 GL.glTexCoord2f(prop_x, prop_y)
158                 GL.glVertex2f(1, 0)
159                 GL.glTexCoord2f(0, prop_y)
160                 GL.glVertex2f(0, 0)
161                 GL.glEnd()
162                 GL.glPopMatrix()
163                 GL.glDisable(GL.GL_TEXTURE_2D)
164
165         def _populate_point_label(self, x_val, y_val):
166                 """
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
172                 """
173                 return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))
174
175         def _draw_legend(self):
176                 """
177                 Draw the color scale legend.
178                 """
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))
201
202         def _resize_texture(self, flag=None):
203                 """
204                 Create the texture to fit the fft_size X num_lines.
205                 @param flag the set/unset or update flag
206                 """
207                 if flag is not None: 
208                         self._resize_texture_flag = flag
209                         return
210                 if not self._resize_texture_flag: return
211                 self._buffer = list()
212                 self._pointer = 0
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
218
219         def set_color_mode(self, color_mode):
220                 """
221                 Set the 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
225                 """
226                 self.lock()
227                 if color_mode in COLORS.keys():
228                         self._color_mode = color_mode
229                         self._legend_cache.changed(True)
230                 self.update()
231                 self.unlock()
232
233         def set_num_lines(self, num_lines):
234                 """
235                 Set number of lines.
236                 Powers of two only.
237                 @param num_lines the new number of lines
238                 """
239                 self.lock()
240                 self._num_lines = num_lines
241                 self._resize_texture(True)
242                 self.update()
243                 self.unlock()
244
245         def set_samples(self, samples, minimum, maximum):
246                 """
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
252                 """
253                 self.lock()
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)
270                 self.unlock()