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):
24 xmin = num.nanmin(x)
25 xmax = num.nanmax(x)
26 if xmin == xmax:
27 xmin -= 0.5
28 xmax += 0.5
30 rmin = imin - 0.5
31 rmax = imax + 0.5
33 if discrete:
34 return num.clip(
35 num.round(
36 rmin + (x - xmin) * (
37 (rmax-rmin) / (xmax - xmin))).astype(num.int64),
38 imin, imax)
39 else:
40 return num.clip(
41 rmin + (x - xmin) * ((rmax-rmin) / (xmax - xmin)),
42 imin, imax)
45def string_to_sorted_idx(values):
46 val_sort = num.sort(values, axis=-1, kind='mergesort')
47 val_sort_unique = num.unique(val_sort)
49 val_to_idx = dict([
50 (val_sort_unique[i], i)
51 for i in range(val_sort_unique.shape[0])])
53 return num.array([val_to_idx[val] for val in values])
56class SymbolChoice(StringChoice):
57 choices = ['point', 'sphere', 'beachball']
60class MaskingShapeChoice(StringChoice):
61 choices = ['rect', 'linear', 'quadratic']
64class MaskingModeChoice(StringChoice):
65 choices = ['past + future', 'past', 'future']
67 @classmethod
68 def get_factors(cls, mode, value_low):
69 return {
70 'past + future': (value_low, 1.0, value_low),
71 'past': (value_low, 1.0, 0.0),
72 'future': (0.0, 1.0, value_low)}[mode]
75class TableState(base.ElementState):
76 visible = Bool.T(default=True)
77 size = Float.T(default=3.0)
78 color_parameter = String.T(optional=True)
79 cpt = base.CPTState.T(default=base.CPTState.D())
80 size_parameter = String.T(optional=True)
81 depth_min = Float.T(optional=True)
82 depth_max = Float.T(optional=True)
83 depth_offset = Float.T(default=0.0)
84 symbol = SymbolChoice.T(default='sphere')
85 time_masking_opacity = Float.T(default=0.0)
86 time_masking_shape = MaskingShapeChoice.T(default='rect')
87 time_masking_mode = MaskingModeChoice.T(default='past + future')
90class TableElement(base.Element):
91 def __init__(self):
92 base.Element.__init__(self)
93 self._parent = None
95 self._table = None
96 self._istate = 0
97 self._istate_view = 0
99 self._controls = None
100 self._color_combobox = None
101 self._size_combobox = None
103 self._pipes = None
104 self._pipe_maps = None
105 self._isize_min = 1
106 self._isize_max = 6
108 self.cpt_handler = base.CPTHandler()
110 def bind_state(self, state):
111 base.Element.bind_state(self, state)
112 for var in ['visible', 'size']:
113 self.register_state_listener3(self.update, state, var)
115 for var in [
116 'depth_min', 'depth_max', 'time_masking_shape',
117 'time_masking_mode', 'time_masking_opacity']:
119 self.register_state_listener3(self.update_alpha, state, var)
121 self.cpt_handler.bind_state(state.cpt, self.update)
123 for var in ['symbol', 'size_parameter', 'color_parameter']:
124 self.register_state_listener3(self.update_sizes, state, var)
126 def unbind_state(self):
127 self.cpt_handler.unbind_state()
128 self._listeners = []
129 self._state = None
131 def get_name(self):
132 return 'Table'
134 def set_parent(self, parent):
135 self._parent = parent
136 self._parent.add_panel(
137 self.get_name(),
138 self._get_controls(),
139 visible=True,
140 title_controls=[
141 self.get_title_control_remove(),
142 self.get_title_control_visible()])
144 for var in ['tmin', 'tmax', 'tduration', 'tposition']:
145 self.register_state_listener3(
146 self.update_alpha, self._parent.state, var)
148 self._parent.register_data_provider(self)
150 self.update()
152 def iter_data(self, name):
153 if self._table and self._table.has_col(name):
154 yield self._table.get_col(name)
156 def set_table(self, table):
157 self._table = table
159 self._istate += 1
161 if self._pipes is not None and self._istate != self._istate_view:
162 self._clear_pipes()
164 self._update_controls()
166 def get_size_parameter_extra_entries(self):
167 return []
169 def get_color_parameter_extra_entries(self):
170 return []
172 def update_sizes(self, *args):
173 self._istate += 1
174 self.update()
176 def unset_parent(self):
177 self.unbind_state()
178 if self._parent:
179 self._parent.unregister_data_provider(self)
181 self._clear_pipes()
183 if self._controls:
184 self._parent.remove_panel(self._controls)
185 self._controls = None
187 self._parent.update_view()
188 self._parent = None
190 def _clear_pipes(self):
191 if self._pipes is not None:
192 for p in self._pipes:
193 self._parent.remove_actor(p.actor)
195 self._pipes = None
197 if self._pipe_maps is not None:
198 self._pipe_maps = None
200 def _init_pipes_scatter(self):
201 state = self._state
202 points = self._table.get_col('xyz')
203 self._pipes = []
204 self._pipe_maps = []
205 if state.size_parameter:
206 sizes = self._table.get_col(state.size_parameter)
207 isizes = inormalize(
208 sizes, self._isize_min, self._isize_max)
210 for i in range(self._isize_min, self._isize_max+1):
211 b = isizes == i
212 p = ScatterPipe(points[b].copy())
213 self._pipes.append(p)
214 self._pipe_maps.append(b)
215 else:
216 self._pipes.append(
217 ScatterPipe(points))
218 self._pipe_maps.append(
219 num.ones(points.shape[0], dtype=bool))
221 def _init_pipes_beachball(self):
222 state = self._state
223 self._pipes = []
225 tab = self._table
227 positions = tab.get_col('xyz')
229 if tab.has_col('m6'):
230 m6s = tab.get_col('m6')
231 else:
232 m6s = num.zeros((tab.get_nrows(), 6))
233 m6s[:, 3] = 1.0
235 if state.size_parameter:
236 sizes = tab.get_col(state.size_parameter)
237 else:
238 sizes = num.ones(tab.get_nrows())
240 if state.color_parameter:
241 values = self._table.get_col(state.color_parameter)
242 else:
243 values = num.zeros(tab.get_nrows())
245 rsizes = inormalize(
246 sizes, self._isize_min, self._isize_max, discrete=False) * 0.005
248 pipe = BeachballPipe(positions, m6s, rsizes, values, self._parent.ren)
249 self._pipes = [pipe]
251 def _update_pipes_scatter(self):
252 state = self._state
253 for i, p in enumerate(self._pipes):
254 self._parent.add_actor(p.actor)
255 p.set_size(state.size * (self._isize_min + i)**1.3)
257 if state.color_parameter:
258 values = self._table.get_col(state.color_parameter)
260 if num.issubdtype(values.dtype, num.string_):
261 values = string_to_sorted_idx(values)
263 self.cpt_handler._values = values
264 self.cpt_handler.update_cpt()
266 cpt = copy.deepcopy(
267 self.cpt_handler._cpts[self._state.cpt.cpt_name])
268 colors2 = cpt(values)
269 colors2 = colors2 / 255.
271 for m, p in zip(self._pipe_maps, self._pipes):
272 p.set_colors(colors2[m, :])
274 for p in self._pipes:
275 p.set_symbol(state.symbol)
277 def _update_pipes_beachball(self):
278 state = self._state
280 p = self._pipes[0]
282 self._parent.add_actor(p.actor)
283 p.set_size_factor(state.size * 0.005)
285 def update(self, *args):
286 state = self._state
288 if self._pipes is not None and self._istate != self._istate_view:
289 self._clear_pipes()
291 if not state.visible:
292 if self._pipes is not None:
293 for p in self._pipes:
294 self._parent.remove_actor(p.actor)
296 else:
297 if self._istate != self._istate_view and self._table:
298 if state.symbol == 'beachball':
299 self._init_pipes_beachball()
300 else:
301 self._init_pipes_scatter()
303 self._istate_view = self._istate
305 if self._pipes is not None:
306 if state.symbol == 'beachball':
307 self._update_pipes_beachball()
308 else:
309 self._update_pipes_scatter()
311 self.update_alpha() # TODO: only if needed?
312 self._parent.update_view()
314 def update_alpha(self, *args, mask=None):
315 if self._state.symbol == 'beachball':
316 return
318 if self._pipes is None:
319 return
321 time = self._table.get_col('time')
322 depth = self._table.get_col('depth')
324 depth_mask = num.ones(time.size, dtype=bool)
326 if self._state.depth_min is not None:
327 depth_mask &= depth >= self._state.depth_min
328 if self._state.depth_max is not None:
329 depth_mask &= depth <= self._state.depth_max
331 tmin = self._parent.state.tmin_effective
332 tmax = self._parent.state.tmax_effective
334 if tmin is not None:
335 m1 = time < tmin
336 else:
337 m1 = num.zeros(time.size, dtype=bool)
339 if tmax is not None:
340 m3 = tmax < time
341 else:
342 m3 = num.zeros(time.size, dtype=bool)
344 m2 = num.logical_not(num.logical_or(m1, m3))
346 value_low = self._state.time_masking_opacity
348 f1, f2, f3 = MaskingModeChoice.get_factors(
349 self._state.time_masking_mode, value_low)
351 amp = num.ones(time.size, dtype=num.float64)
352 amp[m1] = f1
353 amp[m3] = f3
354 if None in (tmin, tmax):
355 amp[m2] = 1.0
356 else:
357 if self._state.time_masking_shape == 'rect':
358 amp[m2] == 1.0
359 elif self._state.time_masking_shape == 'linear':
360 amp[m2] = time[m2]
361 amp[m2] -= tmin
362 amp[m2] /= (tmax - tmin)
363 elif self._state.time_masking_shape == 'quadratic':
364 amp[m2] = time[m2]
365 amp[m2] -= tmin
366 amp[m2] /= (tmax - tmin)
367 amp[m2] **= 2
369 if f1 != 0.0:
370 amp[m2] *= (1.0 - value_low)
371 amp[m2] += value_low
373 amp *= depth_mask
375 for m, p in zip(self._pipe_maps, self._pipes):
376 p.set_alpha(amp[m])
378 self._parent.update_view()
380 def _get_table_widgets_start(self):
381 return 0
383 def _get_controls(self):
384 if self._controls is None:
385 from ..state import state_bind_slider, state_bind_slider_float, \
386 state_bind_combobox, state_bind_lineedit
388 frame = qw.QFrame()
389 layout = qw.QGridLayout()
390 frame.setLayout(layout)
392 iy = self._get_table_widgets_start()
394 layout.addWidget(qw.QLabel('Size'), iy, 0)
396 slider = qw.QSlider(qc.Qt.Horizontal)
397 slider.setSizePolicy(
398 qw.QSizePolicy(
399 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
400 slider.setMinimum(0)
401 slider.setMaximum(100)
402 layout.addWidget(slider, iy, 1)
403 state_bind_slider(self, self._state, 'size', slider, factor=0.1)
405 iy += 1
407 layout.addWidget(qw.QLabel('Size Scaling'), iy, 0)
409 cb = qw.QComboBox()
411 layout.addWidget(cb, iy, 1)
412 state_bind_combobox(
413 self, self._state, 'size_parameter', cb)
415 self._size_combobox = cb
417 iy += 1
419 layout.addWidget(qw.QLabel('Color'), iy, 0)
421 cb = qw.QComboBox()
423 layout.addWidget(cb, iy, 1)
424 state_bind_combobox(
425 self, self._state, 'color_parameter', cb)
427 self._color_combobox = cb
429 self.cpt_handler.cpt_controls(
430 self._parent, self._state.cpt, layout)
432 iy = layout.rowCount() + 1
434 layout.addWidget(qw.QLabel('Symbol'), iy, 0)
436 cb = common.string_choices_to_combobox(SymbolChoice)
438 layout.addWidget(cb, iy, 1)
439 state_bind_combobox(
440 self, self._state, 'symbol', cb)
442 iy += 1
444 layout.addWidget(qw.QLabel('Depth Min [km]'), iy, 0)
445 slider = gui_util.QSliderFloat(qc.Qt.Horizontal)
446 slider.setSizePolicy(
447 qw.QSizePolicy(
448 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
449 slider.setMinimumFloat(-60*km)
450 slider.setMaximumFloat(700*km)
451 layout.addWidget(slider, iy, 1)
452 state_bind_slider_float(
453 self, self._state, 'depth_min', slider,
454 min_is_none=True)
455 self._depth_min_slider = slider
457 le = qw.QLineEdit()
458 layout.addWidget(le, iy, 2)
459 state_bind_lineedit(
460 self, self._state, 'depth_min', le,
461 from_string=lambda s: None if s == 'off' else float(s)*1000.,
462 to_string=lambda v: 'off' if v is None else str(v/1000.))
464 self._depth_min_lineedit = le
466 iy += 1
468 layout.addWidget(qw.QLabel('Depth Max [km]'), iy, 0)
469 slider = gui_util.QSliderFloat(qc.Qt.Horizontal)
470 slider.setSizePolicy(
471 qw.QSizePolicy(
472 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
473 slider.setMinimumFloat(-60*km)
474 slider.setMaximumFloat(700*km)
475 layout.addWidget(slider, iy, 1)
476 state_bind_slider_float(
477 self, self._state, 'depth_max', slider,
478 max_is_none=True)
479 self._depth_max_slider = slider
481 le = qw.QLineEdit()
482 layout.addWidget(le, iy, 2)
483 state_bind_lineedit(
484 self, self._state, 'depth_max', le,
485 from_string=lambda s: None if s == 'off' else float(s)*1000.,
486 to_string=lambda v: 'off' if v is None else str(v/1000.))
488 self._depth_max_lineedit = le
490 iy += 1
492 layout.addWidget(qw.QLabel('Time Masking Opacity'), iy, 0)
494 slider = qw.QSlider(qc.Qt.Horizontal)
495 slider.setSizePolicy(
496 qw.QSizePolicy(
497 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed))
498 slider.setMinimum(0)
499 slider.setMaximum(100)
500 layout.addWidget(slider, iy, 1)
501 state_bind_slider(
502 self, self._state, 'time_masking_opacity', slider, factor=0.01)
504 iy += 1
506 layout.addWidget(qw.QLabel('Time Masking Shape'), iy, 0)
507 cb = common.string_choices_to_combobox(MaskingShapeChoice)
508 layout.addWidget(cb, iy, 1)
509 state_bind_combobox(self, self._state, 'time_masking_shape', cb)
511 iy += 1
513 layout.addWidget(qw.QLabel('Time Masking Mode'), iy, 0)
514 cb = common.string_choices_to_combobox(MaskingModeChoice)
515 layout.addWidget(cb, iy, 1)
516 state_bind_combobox(self, self._state, 'time_masking_mode', cb)
518 iy += 1
520 layout.addWidget(qw.QFrame(), iy, 0, 1, 3)
522 self._controls = frame
524 self._update_controls()
526 return self._controls
528 def _update_controls(self):
529 for (cb, get_extra_entries) in [
530 (self._color_combobox, self.get_color_parameter_extra_entries),
531 (self._size_combobox, self.get_size_parameter_extra_entries)]:
533 if cb is not None:
534 cb.clear()
536 have = set()
537 for s in get_extra_entries():
538 if s not in have:
539 cb.insertItem(len(have), s)
540 have.add(s)
542 if self._table is not None:
543 for s in self._table.get_col_names():
544 h = self._table.get_header(s)
545 if h.get_ncols() == 1 and s not in have:
546 cb.insertItem(len(have), s)
547 have.add(s)
549 self.cpt_handler._update_cpt_combobox()
550 self.cpt_handler._update_cptscale_lineedit()
552 if self._table is not None and self._table.has_col('depth'):
553 depth = self._table.get_col('depth')
555 if depth.size > 0:
557 depth_min = depth.min()
558 depth_max = depth.max()
560 for wdg in (self._depth_min_slider, self._depth_max_slider):
561 wdg.setMinimumFloat(depth_min)
562 wdg.setMaximumFloat(depth_max)
565__all__ = [
566 'TableElement',
567 'TableState',
568]