Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/elements/table.py: 91%
350 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-23 12:34 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-23 12:34 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import copy
8import numpy as num
10from pyrocko.guts import Bool, Float, String, StringChoice
11from pyrocko.gui import util as gui_util
12from pyrocko.gui.vtk_util import ScatterPipe, BeachballPipe
13from pyrocko.gui.qt_compat import qw, qc
15from . import base
16from .. import common
18guts_prefix = 'sparrow'
19km = 1e3
22def inormalize(x, imin, imax, discrete=True):
23 if x.size == 0:
24 return num.full(x.shape, imin, dtype=int)
26 xmin = num.nanmin(x)
27 xmax = num.nanmax(x)
28 if xmin == xmax:
29 xmin -= 0.5
30 xmax += 0.5
32 rmin = imin - 0.5
33 rmax = imax + 0.5
35 if discrete:
36 return num.clip(
37 num.round(
38 rmin + (x - xmin) * (
39 (rmax-rmin) / (xmax - xmin))).astype(num.int64),
40 imin, imax)
41 else:
42 return num.clip(
43 rmin + (x - xmin) * ((rmax-rmin) / (xmax - xmin)),
44 imin, imax)
47def string_to_sorted_idx(values):
48 val_sort = num.sort(values, axis=-1, kind='mergesort')
49 val_sort_unique = num.unique(val_sort)
51 val_to_idx = dict([
52 (val_sort_unique[i], i)
53 for i in range(val_sort_unique.shape[0])])
55 return num.array([val_to_idx[val] for val in values])
58class SymbolChoice(StringChoice):
59 choices = ['point', 'sphere', 'beachball']
62class MaskingShapeChoice(StringChoice):
63 choices = ['rect', 'linear', 'quadratic']
66class MaskingModeChoice(StringChoice):
67 choices = ['past + future', 'past', 'future']
69 @classmethod
70 def get_factors(cls, mode, value_low):
71 return {
72 'past + future': (value_low, 1.0, value_low),
73 'past': (value_low, 1.0, 0.0),
74 'future': (0.0, 1.0, value_low)}[mode]
77class TableState(base.ElementState):
78 visible = Bool.T(default=True)
79 size = Float.T(default=3.0)
80 color_parameter = String.T(optional=True)
81 cpt = base.CPTState.T(default=base.CPTState.D())
82 size_parameter = String.T(optional=True)
83 depth_min = Float.T(optional=True)
84 depth_max = Float.T(optional=True)
85 depth_offset = Float.T(default=0.0)
86 symbol = SymbolChoice.T(default='sphere')
87 time_masking_opacity = Float.T(default=0.0)
88 time_masking_shape = MaskingShapeChoice.T(default='rect')
89 time_masking_mode = MaskingModeChoice.T(default='past + future')
92class TableElement(base.Element):
93 def __init__(self):
94 base.Element.__init__(self)
95 self._parent = None
97 self._table = None
98 self._istate = 0
99 self._istate_view = 0
101 self._controls = None
102 self._color_combobox = None
103 self._size_combobox = None
105 self._pipes = None
106 self._pipe_maps = None
107 self._isize_min = 1
108 self._isize_max = 6
110 self.cpt_handler = base.CPTHandler()
112 def bind_state(self, state):
113 base.Element.bind_state(self, state)
114 self.talkie_connect(state, ['visible', 'size'], self.update)
116 self.talkie_connect(
117 state,
118 ['depth_min', 'depth_max', 'time_masking_shape',
119 'time_masking_mode', 'time_masking_opacity'],
120 self.update_alpha)
122 self.cpt_handler.bind_state(state.cpt, self.update)
124 self.talkie_connect(
125 state,
126 ['symbol', 'size_parameter', 'color_parameter'],
127 self.update_sizes)
129 def unbind_state(self):
130 self.cpt_handler.unbind_state()
131 base.Element.unbind_state(self)
133 def get_name(self):
134 return 'Table'
136 def set_parent(self, parent):
137 self._parent = parent
138 self._parent.add_panel(
139 self.get_title_label(),
140 self._get_controls(),
141 visible=True,
142 title_controls=[
143 self.get_title_control_remove(),
144 self.get_title_control_visible()])
146 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
147 self.talkie_connect(
148 self._parent.state, var, self.update_alpha)
150 self._parent.register_data_provider(self)
152 self.update()
154 def iter_data(self, name):
155 if self._table and self._table.has_col(name):
156 yield self._table.get_col(name)
158 def set_table(self, table):
159 self._table = table
161 self._istate += 1
163 if self._pipes is not None and self._istate != self._istate_view:
164 self._clear_pipes()
166 self._update_controls()
168 def get_size_parameter_extra_entries(self):
169 return []
171 def get_color_parameter_extra_entries(self):
172 return []
174 def update_sizes(self, *args):
175 self._istate += 1
176 self.update()
178 def unset_parent(self):
179 self.unbind_state()
180 if self._parent:
181 self._parent.unregister_data_provider(self)
183 self._clear_pipes()
185 if self._controls:
186 self._parent.remove_panel(self._controls)
187 self._controls = None
189 self._parent.update_view()
190 self._parent = None
192 def _clear_pipes(self):
193 if self._pipes is not None:
194 for p in self._pipes:
195 self._parent.remove_actor(p.actor)
197 self._pipes = None
199 if self._pipe_maps is not None:
200 self._pipe_maps = None
202 def _init_pipes_scatter(self):
203 state = self._state
204 points = self._table.get_col('xyz')
205 self._pipes = []
206 self._pipe_maps = []
207 if state.size_parameter:
208 sizes = self._table.get_col(state.size_parameter)
209 isizes = inormalize(
210 sizes, self._isize_min, self._isize_max)
212 for i in range(self._isize_min, self._isize_max+1):
213 b = isizes == i
214 p = ScatterPipe(points[b].copy())
215 self._pipes.append(p)
216 self._pipe_maps.append(b)
217 else:
218 self._pipes.append(
219 ScatterPipe(points))
220 self._pipe_maps.append(
221 num.ones(points.shape[0], dtype=bool))
223 def _init_pipes_beachball(self):
224 state = self._state
225 self._pipes = []
227 tab = self._table
229 positions = tab.get_col('xyz')
231 if tab.has_col('m6'):
232 m6s = tab.get_col('m6')
233 else:
234 m6s = num.zeros((tab.get_nrows(), 6))
235 m6s[:, 3] = 1.0
237 if state.size_parameter:
238 sizes = tab.get_col(state.size_parameter)
239 else:
240 sizes = num.ones(tab.get_nrows())
242 if state.color_parameter:
243 values = self._table.get_col(state.color_parameter)
244 else:
245 values = num.zeros(tab.get_nrows())
247 rsizes = inormalize(
248 sizes, self._isize_min, self._isize_max, discrete=False) * 0.005
250 pipe = BeachballPipe(positions, m6s, rsizes, values, self._parent.ren)
251 self._pipes = [pipe]
253 def _update_pipes_scatter(self):
254 state = self._state
255 for i, p in enumerate(self._pipes):
256 self._parent.add_actor(p.actor)
257 p.set_size(state.size * (self._isize_min + i)**1.3)
259 if state.color_parameter:
260 values = self._table.get_col(state.color_parameter)
262 if num.issubdtype(values.dtype, num.string_):
263 values = string_to_sorted_idx(values)
265 self.cpt_handler._values = values
266 self.cpt_handler.update_cpt()
268 cpt = copy.deepcopy(
269 self.cpt_handler._cpts[self._state.cpt.effective_cpt_name])
270 colors2 = cpt(values)
271 colors2 = colors2 / 255.
273 for m, p in zip(self._pipe_maps, self._pipes):
274 p.set_colors(colors2[m, :])
276 for p in self._pipes:
277 p.set_symbol(state.symbol)
279 def _update_pipes_beachball(self):
280 state = self._state
282 p = self._pipes[0]
284 self._parent.add_actor(p.actor)
285 p.set_size_factor(state.size * 0.005)
287 def _init_pipes(self):
288 if self._state.symbol == 'beachball':
289 self._init_pipes_beachball()
290 else:
291 self._init_pipes_scatter()
293 def _update_pipes(self):
294 if self._state.symbol == 'beachball':
295 self._update_pipes_beachball()
296 else:
297 self._update_pipes_scatter()
299 def update(self, *args):
300 state = self._state
302 if self._pipes is not None and self._istate != self._istate_view:
303 self._clear_pipes()
305 if not state.visible:
306 if self._pipes is not None:
307 for p in self._pipes:
308 self._parent.remove_actor(p.actor)
310 else:
311 if self._istate != self._istate_view and self._table:
312 self._init_pipes()
313 self._istate_view = self._istate
315 if self._pipes is not None:
316 self._update_pipes()
318 self.update_alpha() # TODO: only if needed?
319 self._parent.update_view()
321 def update_alpha(self, *args, mask=None):
322 if self._state.symbol == 'beachball':
323 return
325 if self._pipes is None:
326 return
328 time = self._table.get_col('time')
329 depth = self._table.get_col('depth')
331 depth_mask = num.ones(time.size, dtype=bool)
333 if self._state.depth_min is not None:
334 depth_mask &= depth >= self._state.depth_min
335 if self._state.depth_max is not None:
336 depth_mask &= depth <= self._state.depth_max
338 tmin = self._parent.state.tmin_effective
339 tmax = self._parent.state.tmax_effective
341 if tmin is not None:
342 m1 = time < tmin
343 else:
344 m1 = num.zeros(time.size, dtype=bool)
346 if tmax is not None:
347 m3 = tmax < time
348 else:
349 m3 = num.zeros(time.size, dtype=bool)
351 m2 = num.logical_not(num.logical_or(m1, m3))
353 value_low = self._state.time_masking_opacity
355 f1, f2, f3 = MaskingModeChoice.get_factors(
356 self._state.time_masking_mode, value_low)
358 amp = num.ones(time.size, dtype=num.float64)
359 amp[m1] = f1
360 amp[m3] = f3
361 if None in (tmin, tmax):
362 amp[m2] = 1.0
363 else:
364 if self._state.time_masking_shape == 'rect':
365 amp[m2] == 1.0
366 elif self._state.time_masking_shape == 'linear':
367 amp[m2] = time[m2]
368 amp[m2] -= tmin
369 amp[m2] /= (tmax - tmin)
370 elif self._state.time_masking_shape == 'quadratic':
371 amp[m2] = time[m2]
372 amp[m2] -= tmin
373 amp[m2] /= (tmax - tmin)
374 amp[m2] **= 2
376 if f1 != 0.0:
377 amp[m2] *= (1.0 - value_low)
378 amp[m2] += value_low
380 amp *= depth_mask
382 for m, p in zip(self._pipe_maps, self._pipes):
383 p.set_alpha(amp[m])
385 self._parent.update_view()
387 def _get_table_widgets_start(self):
388 return 0
390 def _get_controls(self):
391 if self._controls is None:
392 from ..state import state_bind_slider, state_bind_slider_float, \
393 state_bind_combobox, state_bind_lineedit
395 frame = qw.QFrame()
396 layout = qw.QGridLayout()
397 frame.setLayout(layout)
399 iy = self._get_table_widgets_start()
401 layout.addWidget(qw.QLabel('Size'), iy, 0)
403 slider = qw.QSlider(qc.Qt.Horizontal)
404 slider.setSizePolicy(
405 qw.QSizePolicy(
406 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
407 slider.setMinimum(0)
408 slider.setMaximum(100)
409 layout.addWidget(slider, iy, 1)
410 state_bind_slider(self, self._state, 'size', slider, factor=0.1)
412 iy += 1
414 layout.addWidget(qw.QLabel('Size Scaling'), iy, 0)
416 cb = qw.QComboBox()
418 layout.addWidget(cb, iy, 1)
419 state_bind_combobox(
420 self, self._state, 'size_parameter', cb)
422 self._size_combobox = cb
424 iy += 1
426 layout.addWidget(qw.QLabel('Color'), iy, 0)
428 cb = qw.QComboBox()
430 layout.addWidget(cb, iy, 1)
431 state_bind_combobox(
432 self, self._state, 'color_parameter', cb)
434 self._color_combobox = cb
436 self.cpt_handler.cpt_controls(
437 self._parent, self._state.cpt, layout)
439 iy = layout.rowCount() + 1
441 layout.addWidget(qw.QLabel('Symbol'), iy, 0)
443 cb = common.string_choices_to_combobox(SymbolChoice)
445 layout.addWidget(cb, iy, 1)
446 state_bind_combobox(
447 self, self._state, 'symbol', cb)
449 iy += 1
451 layout.addWidget(qw.QLabel('Depth Min [km]'), iy, 0)
452 slider = gui_util.QSliderFloat(qc.Qt.Horizontal)
453 slider.setSizePolicy(
454 qw.QSizePolicy(
455 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
456 slider.setMinimumFloat(-60*km)
457 slider.setMaximumFloat(700*km)
458 layout.addWidget(slider, iy, 1)
459 state_bind_slider_float(
460 self, self._state, 'depth_min', slider,
461 min_is_none=True)
462 self._depth_min_slider = slider
464 le = qw.QLineEdit()
465 layout.addWidget(le, iy, 2)
466 state_bind_lineedit(
467 self, self._state, 'depth_min', le,
468 from_string=lambda s: None if s == 'off' else float(s)*1000.,
469 to_string=lambda v: 'off' if v is None else str(v/1000.))
471 self._depth_min_lineedit = le
473 iy += 1
475 layout.addWidget(qw.QLabel('Depth Max [km]'), iy, 0)
476 slider = gui_util.QSliderFloat(qc.Qt.Horizontal)
477 slider.setSizePolicy(
478 qw.QSizePolicy(
479 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
480 slider.setMinimumFloat(-60*km)
481 slider.setMaximumFloat(700*km)
482 layout.addWidget(slider, iy, 1)
483 state_bind_slider_float(
484 self, self._state, 'depth_max', slider,
485 max_is_none=True)
486 self._depth_max_slider = slider
488 le = qw.QLineEdit()
489 layout.addWidget(le, iy, 2)
490 state_bind_lineedit(
491 self, self._state, 'depth_max', le,
492 from_string=lambda s: None if s == 'off' else float(s)*1000.,
493 to_string=lambda v: 'off' if v is None else str(v/1000.))
495 self._depth_max_lineedit = le
497 iy += 1
499 layout.addWidget(qw.QLabel('Time Masking Opacity'), iy, 0)
501 slider = qw.QSlider(qc.Qt.Horizontal)
502 slider.setSizePolicy(
503 qw.QSizePolicy(
504 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
505 slider.setMinimum(0)
506 slider.setMaximum(100)
507 layout.addWidget(slider, iy, 1)
508 state_bind_slider(
509 self, self._state, 'time_masking_opacity', slider, factor=0.01)
511 iy += 1
513 layout.addWidget(qw.QLabel('Time Masking Shape'), iy, 0)
514 cb = common.string_choices_to_combobox(MaskingShapeChoice)
515 layout.addWidget(cb, iy, 1)
516 state_bind_combobox(self, self._state, 'time_masking_shape', cb)
518 iy += 1
520 layout.addWidget(qw.QLabel('Time Masking Mode'), iy, 0)
521 cb = common.string_choices_to_combobox(MaskingModeChoice)
522 layout.addWidget(cb, iy, 1)
523 state_bind_combobox(self, self._state, 'time_masking_mode', cb)
525 iy += 1
527 layout.addWidget(qw.QFrame(), iy, 0, 1, 3)
529 self._controls = frame
531 self._update_controls()
533 return self._controls
535 def _update_controls(self):
536 for (cb, get_extra_entries) in [
537 (self._color_combobox, self.get_color_parameter_extra_entries),
538 (self._size_combobox, self.get_size_parameter_extra_entries)]:
540 if cb is not None:
541 cb.clear()
543 have = set()
544 for s in get_extra_entries():
545 if s not in have:
546 cb.insertItem(len(have), s)
547 have.add(s)
549 if self._table is not None:
550 for s in self._table.get_col_names():
551 h = self._table.get_header(s)
552 if h.get_ncols() == 1 and s not in have:
553 cb.insertItem(len(have), s)
554 have.add(s)
556 self.cpt_handler._update_cpt_combobox()
557 self.cpt_handler._update_cptscale_lineedit()
559 if self._table is not None and self._table.has_col('depth'):
560 depth = self._table.get_col('depth')
562 if depth.size > 0:
564 depth_min = depth.min()
565 depth_max = depth.max()
567 for wdg in (self._depth_min_slider, self._depth_max_slider):
568 wdg.setMinimumFloat(depth_min)
569 wdg.setMaximumFloat(depth_max)
572__all__ = [
573 'TableElement',
574 'TableState',
575]