summaryrefslogtreecommitdiff
path: root/gr-wxgui/src/python/plotter/waterfall_plotter.py
blob: 6a6bf6330e176d046784c9c8ce299e130b1bf416 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#
# Copyright 2008, 2009, 2010 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# GNU Radio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Radio; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#

import wx
from grid_plotter_base import grid_plotter_base
from OpenGL import GL
import common
import numpy
import gltext
import math
import struct

LEGEND_LEFT_PAD = 7
LEGEND_NUM_BLOCKS = 256
LEGEND_NUM_LABELS = 9
LEGEND_WIDTH = 8
LEGEND_FONT_SIZE = 8
LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left

ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))

pack_color   = lambda x: struct.unpack('I', struct.pack('BBBB', *x))[0]
unpack_color = lambda x: struct.unpack('BBBB', struct.pack('I', int(x)))

def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
	"""
	Get an array of 256 rgba values where each index maps to a color.
	The scaling for red, green, blue, alpha are specified in piece-wise functions.
	The piece-wise functions consist of a set of x, y coordinates.
	The x and y values of the coordinates range from 0 to 1.
	The coordinates must be specified so that x increases with the index value.
	Resulting values are calculated along the line formed between 2 coordinates.
	@param *_pts an array of x,y coordinates for each color element
	@return array of rbga values (4 bytes) each
	"""
	def _fcn(x, pw):
		for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
			#linear interpolation
			if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
		raise Exception
	return numpy.array([pack_color(map(
		lambda pw: int(255*_fcn(i/255.0, pw)),
		(red_pts, green_pts, blue_pts, alpha_pts),
	)) for i in range(0, 256)], numpy.uint32)

COLORS = {
	'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
		red_pts = [(0, 0), (.5, 0), (1, 1)],
		green_pts = [(0, 0), (.5, 1), (1, 0)],
		blue_pts = [(0, 1), (.5, 0), (1, 0)],
	),
	'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
		red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
		green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
		blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
	),
	'rgb3': _get_rbga(
		red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
		green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
		blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
	),
	'gray': _get_rbga(
		red_pts = [(0, 0), (1, 1)],
		green_pts = [(0, 0), (1, 1)],
		blue_pts = [(0, 0), (1, 1)],
	),
}

##################################################
# Waterfall Plotter
##################################################
class waterfall_plotter(grid_plotter_base):
	def __init__(self, parent):
		"""
		Create a new channel plotter.
		"""
		#init
		grid_plotter_base.__init__(self, parent, MIN_PADDING)
		#setup legend cache
		self._legend_cache = self.new_gl_cache(self._draw_legend)
		#setup waterfall cache
		self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50)
		#setup waterfall plotter
		self.register_init(self._init_waterfall)
		self._resize_texture(False)
		self._minimum = 0
		self._maximum = 0
		self._fft_size = 1
		self._buffer = list()
		self._pointer = 0
		self._counter = 0
		self.set_num_lines(0)
		self.set_color_mode(COLORS.keys()[0])
		self.callback = None

	def _init_waterfall(self):
		"""
		Run gl initialization tasks.
		"""
		self._waterfall_texture = GL.glGenTextures(1)

	def _draw_waterfall(self):
		"""
		Draw the waterfall from the texture.
		The texture is circularly filled and will wrap around.
		Use matrix modeling to shift and scale the texture onto the coordinate plane.
		"""
		#resize texture
		self._resize_texture()
		#setup texture
		GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
		GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
		GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
		GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
		GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE)
		#write the buffer to the texture
		while self._buffer:
			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))
			self._pointer = (self._pointer + 1)%self._num_lines
		#begin drawing
		GL.glEnable(GL.GL_TEXTURE_2D)
		GL.glPushMatrix()
		#matrix scaling
		GL.glTranslatef(self.padding_left, self.padding_top, 0)
		GL.glScalef(
			float(self.width-self.padding_left-self.padding_right),
			float(self.height-self.padding_top-self.padding_bottom),
			1.0,
		)
		#draw texture with wrapping
		GL.glBegin(GL.GL_QUADS)
		prop_y = float(self._pointer)/(self._num_lines-1)
		prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
		off = 1.0/(self._num_lines-1)
		GL.glTexCoord2f(0, prop_y+1-off)
		GL.glVertex2f(0, 1)
		GL.glTexCoord2f(prop_x, prop_y+1-off)
		GL.glVertex2f(1, 1)
		GL.glTexCoord2f(prop_x, prop_y)
		GL.glVertex2f(1, 0)
		GL.glTexCoord2f(0, prop_y)
		GL.glVertex2f(0, 0)
		GL.glEnd()
		GL.glPopMatrix()
		GL.glDisable(GL.GL_TEXTURE_2D)

	def _populate_point_label(self, x_val, y_val):
		"""
		Get the text the will populate the point label.
		Give the X value for the current point.
		@param x_val the current x value
		@param y_val the current y value
		@return a value string with units
		"""
		return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units))

	def _call_callback(self, x_val, y_val):
		if self.callback != None:
			self.callback(x_val,y_val)
	
	def set_callback(self,callback):
		self.callback = callback
		
	def _draw_legend(self):
		"""
		Draw the color scale legend.
		"""
		if not self._color_mode: return
		legend_height = self.height-self.padding_top-self.padding_bottom
		#draw each legend block
		block_height = float(legend_height)/LEGEND_NUM_BLOCKS
		x = self.width - self.padding_right + LEGEND_LEFT_PAD
		for i in range(LEGEND_NUM_BLOCKS):
			color = unpack_color(COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))])
			GL.glColor4f(*numpy.array(color)/255.0)
			y = self.height - (i+1)*block_height - self.padding_bottom
			self._draw_rect(x, y, LEGEND_WIDTH, block_height)
		#draw rectangle around color scale border
		GL.glColor3f(*LEGEND_BORDER_COLOR_SPEC)
		self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
		#draw each legend label
		label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
		x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
		for i in range(LEGEND_NUM_LABELS):
			proportion = i/float(LEGEND_NUM_LABELS-1)
			dB = proportion*(self._maximum - self._minimum) + self._minimum
			y = self.height - i*label_spacing - self.padding_bottom
			txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
			txt.draw_text(wx.Point(x, y))

	def _resize_texture(self, flag=None):
		"""
		Create the texture to fit the fft_size X num_lines.
		@param flag the set/unset or update flag
		"""
		if flag is not None:
			self._resize_texture_flag = flag
			return
		if not self._resize_texture_flag: return
		self._buffer = list()
		self._pointer = 0
		if self._num_lines and self._fft_size:
			GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
			data = numpy.zeros(self._num_lines*ceil_log2(self._fft_size)*4, numpy.uint8).tostring()
			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)
		self._resize_texture_flag = False

	def set_color_mode(self, color_mode):
		"""
		Set the color mode.
		New samples will be converted to the new color mode.
		Old samples will not be recolorized.
		@param color_mode the new color mode string
		"""
		self.lock()
		if color_mode in COLORS.keys():
			self._color_mode = color_mode
			self._legend_cache.changed(True)
		self.update()
		self.unlock()

	def set_num_lines(self, num_lines):
		"""
		Set number of lines.
		Powers of two only.
		@param num_lines the new number of lines
		"""
		self.lock()
		self._num_lines = num_lines
		self._resize_texture(True)
		self.update()
		self.unlock()

	def set_samples(self, samples, minimum, maximum):
		"""
		Set the samples to the waterfall.
		Convert the samples to color data.
		@param samples the array of floats
		@param minimum the minimum value to scale
		@param maximum the maximum value to scale
		"""
		self.lock()
		#set the min, max values
		if self._minimum != minimum or self._maximum != maximum:
			self._minimum = minimum
			self._maximum = maximum
			self._legend_cache.changed(True)
		if self._fft_size != len(samples):
			self._fft_size = len(samples)
			self._resize_texture(True)
		#normalize the samples to min/max
		samples = (samples - minimum)*float(255/(maximum-minimum))
		samples = numpy.clip(samples, 0, 255) #clip
		samples = numpy.array(samples, numpy.uint8)
		#convert the samples to RGBA data
		data = COLORS[self._color_mode][samples].tostring()
		self._buffer.append(data)
		self._waterfall_cache.changed(True)
		self.unlock()