1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import logging
8import numpy as num
10from pyrocko import util
11from pyrocko.guts import StringChoice, Float, List, Bool, Timestamp, Tuple, \
12 Duration, Object, get_elements, set_elements, path_to_str, clone
14from pyrocko.color import Color, interpolate as interpolate_color
16from pyrocko.gui import talkie
17from pyrocko.gui import util as gui_util
18from . import common, light
20guts_prefix = 'sparrow'
22logger = logging.getLogger('pyrocko.gui.sparrow.state')
25class FocalPointChoice(StringChoice):
26 choices = ['center', 'target']
29class ShadingChoice(StringChoice):
30 choices = ['flat', 'gouraud', 'phong', 'pbr']
33class LightingChoice(StringChoice):
34 choices = light.get_lighting_theme_names()
37class ViewerGuiState(talkie.TalkieRoot):
38 panels_visible = Bool.T(default=True)
39 size = Tuple.T(2, Float.T(), default=(100., 100.))
40 fixed_size = Tuple.T(2, Float.T(), optional=True)
41 focal_point = FocalPointChoice.T(default='center')
42 detached = Bool.T(default=False)
43 tcursor = Timestamp.T(optional=True)
45 def next_focal_point(self):
46 choices = FocalPointChoice.choices
47 ii = choices.index(self.focal_point)
48 self.focal_point = choices[(ii+1) % len(choices)]
51class Background(Object):
52 color = Color.T(default=Color.D('black'))
54 def vtk_apply(self, ren):
55 ren.GradientBackgroundOff()
56 ren.SetBackground(*self.color.rgb)
58 def __str__(self):
59 return str(self.color)
61 @property
62 def color_top(self):
63 return self.color
65 @property
66 def color_bottom(self):
67 return self.color
69 # def __eq__(self, other):
70 # print('in==', self.color.rgb, other.color.rgb)
71 # return type(self) is type(other) and self.color == other.color
74class BackgroundGradient(Background):
75 color_top = Color.T(default=Color.D('skyblue1'))
76 color_bottom = Color.T(default=Color.D('white'))
78 def vtk_apply(self, ren):
79 ren.GradientBackgroundOn()
80 ren.SetBackground(*self.color_bottom.rgb)
81 ren.SetBackground2(*self.color_top.rgb)
83 def __str__(self):
84 return '%s - %s' % (self.color_top, self.color_bottom)
86 # def __eq__(self, other):
87 # return type(self) is type(other) and \
88 # self.color_top == other.color_top and \
89 # self.color_bottom == other.color_bottom
92def interpolate_background(a, b, blend):
93 if type(a) is Background and type(b) is Background:
94 return Background(color=interpolate_color(a.color, b.color, blend))
95 else:
96 return BackgroundGradient(
97 color_top=interpolate_color(
98 a.color_top, b.color_top, blend),
99 color_bottom=interpolate_color(
100 a.color_bottom, b.color_bottom, blend))
103@talkie.has_computed
104class ViewerState(talkie.TalkieRoot):
105 lat = Float.T(default=0.0)
106 lon = Float.T(default=0.0)
107 depth = Float.T(default=0.0)
108 strike = Float.T(default=90.0)
109 dip = Float.T(default=0.0)
110 distance = Float.T(default=3.0)
111 elements = List.T(talkie.Talkie.T())
112 tmin = Timestamp.T(optional=True)
113 tmax = Timestamp.T(optional=True)
114 tduration = Duration.T(optional=True)
115 tposition = Float.T(default=0.0)
116 lighting = LightingChoice.T(default=LightingChoice.choices[0])
117 background = Background.T(default=Background.D(color=Color('black')))
119 @talkie.computed(['tmin', 'tmax', 'tduration', 'tposition'])
120 def tmin_effective(self):
121 return common.tmin_effective(
122 self.tmin, self.tmax, self.tduration, self.tposition)
124 @talkie.computed(['tmin', 'tmax', 'tduration', 'tposition'])
125 def tmax_effective(self):
126 return common.tmax_effective(
127 self.tmin, self.tmax, self.tduration, self.tposition)
129 def sort_elements(self):
130 self.elements.sort(key=lambda el: el.element_id)
133def state_bind(
134 owner, state, paths, update_state,
135 widget, signals, update_widget, attribute=None):
137 def make_wrappers(widget):
138 def wrap_update_widget(*args):
139 if attribute:
140 update_widget(state, attribute, widget)
141 else:
142 update_widget(state, widget)
143 common.de_errorize(widget)
145 def wrap_update_state(*args):
146 try:
147 if attribute:
148 update_state(widget, state, attribute)
149 else:
150 update_state(widget, state)
151 common.de_errorize(widget)
152 except Exception as e:
153 logger.warn('Caught exception: %s' % e)
154 common.errorize(widget)
156 return wrap_update_widget, wrap_update_state
158 wrap_update_widget, wrap_update_state = make_wrappers(widget)
160 for sig in signals:
161 sig.connect(wrap_update_state)
163 for path in paths:
164 owner.talkie_connect(state, path, wrap_update_widget)
166 wrap_update_widget()
169def state_bind_slider(
170 owner, state, path, widget, factor=1.,
171 dtype=float,
172 min_is_none=False,
173 max_is_none=False):
175 app = common.get_app()
177 viewer = app.get_main_window()
178 widget.sliderPressed.connect(viewer.disable_capture)
179 widget.sliderReleased.connect(viewer.enable_capture)
181 def make_funcs():
182 def update_state(widget, state):
183 val = widget.value()
184 if (min_is_none and val == widget.minimum()) \
185 or (max_is_none and val == widget.maximum()):
186 state.set(path, None)
187 else:
188 app.status('%g' % (val * factor))
189 state.set(path, dtype(val * factor))
191 def update_widget(state, widget):
192 val = state.get(path)
193 widget.blockSignals(True)
194 if min_is_none and val is None:
195 widget.setValue(widget.minimum())
196 elif max_is_none and val is None:
197 widget.setValue(widget.maximum())
198 else:
199 widget.setValue(int(state.get(path) * 1. / factor))
200 widget.blockSignals(False)
202 return update_state, update_widget
204 update_state, update_widget = make_funcs()
206 state_bind(
207 owner, state, [path], update_state, widget, [widget.valueChanged],
208 update_widget)
211def state_bind_slider_float(
212 owner, state, path, widget,
213 min_is_none=False,
214 max_is_none=False):
216 assert isinstance(widget, gui_util.QSliderFloat)
218 app = common.get_app()
220 viewer = app.get_main_window()
221 widget.sliderPressed.connect(viewer.disable_capture)
222 widget.sliderReleased.connect(viewer.enable_capture)
224 def make_funcs():
225 def update_state(widget, state):
226 val = widget.valueFloat()
227 if (min_is_none and val == widget.minimumFloat()) \
228 or (max_is_none and val == widget.maximumFloat()):
229 state.set(path, None)
230 else:
231 app.status('%g' % (val))
232 state.set(path, val)
234 def update_widget(state, widget):
235 val = state.get(path)
236 widget.blockSignals(True)
237 if min_is_none and val is None:
238 widget.setValueFloat(widget.minimumFloat())
239 elif max_is_none and val is None:
240 widget.setValueFloat(widget.maximumFloat())
241 else:
242 widget.setValueFloat(state.get(path))
243 widget.blockSignals(False)
245 return update_state, update_widget
247 update_state, update_widget = make_funcs()
249 state_bind(
250 owner, state, [path], update_state, widget, [widget.valueChanged],
251 update_widget)
254def state_bind_spinbox(owner, state, path, widget, factor=1., dtype=float):
255 return state_bind_slider(owner, state, path, widget, factor, dtype)
258def state_bind_combobox(owner, state, path, widget):
260 def make_funcs():
261 def update_state(widget, state):
262 state.set(path, str(widget.currentText()))
264 def update_widget(state, widget):
265 widget.blockSignals(True)
266 val = state.get(path)
267 for i in range(widget.count()):
268 if str(widget.itemText(i)) == val:
269 widget.setCurrentIndex(i)
270 widget.blockSignals(False)
272 return update_state, update_widget
274 update_state, update_widget = make_funcs()
276 state_bind(
277 owner, state, [path], update_state, widget, [widget.activated],
278 update_widget)
281def state_bind_combobox_background(owner, state, path, widget):
283 def make_funcs():
284 def update_state(widget, state):
285 values = str(widget.currentText()).split(' - ')
286 if len(values) == 1:
287 state.set(
288 path,
289 Background(color=Color(values[0])))
291 elif len(values) == 2:
292 state.set(
293 path,
294 BackgroundGradient(
295 color_top=Color(values[0]),
296 color_bottom=Color(values[1])))
298 def update_widget(state, widget):
299 widget.blockSignals(True)
300 val = str(state.get(path))
301 for i in range(widget.count()):
302 if str(widget.itemText(i)) == val:
303 widget.setCurrentIndex(i)
304 widget.blockSignals(False)
306 return update_state, update_widget
308 update_state, update_widget = make_funcs()
310 state_bind(
311 owner, state, [path], update_state, widget, [widget.activated],
312 update_widget)
315def state_bind_combobox_color(owner, state, path, widget):
317 def make_funcs():
318 def update_state(widget, state):
319 value = str(widget.currentText())
320 state.set(path, Color(value))
322 def update_widget(state, widget):
323 widget.blockSignals(True)
324 val = str(state.get(path))
325 for i in range(widget.count()):
326 if str(widget.itemText(i)) == val:
327 widget.setCurrentIndex(i)
328 widget.blockSignals(False)
330 return update_state, update_widget
332 update_state, update_widget = make_funcs()
334 state_bind(
335 owner, state, [path], update_state, widget, [widget.activated],
336 update_widget)
339def state_bind_checkbox(owner, state, path, widget):
341 def make_funcs():
342 def update_state(widget, state):
343 state.set(path, bool(widget.isChecked()))
345 def update_widget(state, widget):
346 widget.blockSignals(True)
347 widget.setChecked(state.get(path))
348 widget.blockSignals(False)
350 return update_state, update_widget
352 update_state, update_widget = make_funcs()
354 state_bind(
355 owner, state, [path], update_state, widget, [widget.toggled],
356 update_widget)
359def state_bind_lineedit(
360 owner, state, path, widget, from_string=str, to_string=str):
362 def make_funcs():
364 def update_state(widget, state):
365 state.set(path, from_string(widget.text()))
367 def update_widget(state, widget):
368 widget.blockSignals(True)
369 widget.setText(to_string(state.get(path)))
370 widget.blockSignals(False)
372 return update_state, update_widget
374 update_state, update_widget = make_funcs()
376 state_bind(
377 owner,
378 state, [path], update_state,
379 widget, [widget.editingFinished, widget.returnPressed], update_widget)
382def interpolateables(state_a, state_b):
384 animate = []
385 for tag, path, values in state_b.diff(state_a):
386 if tag == 'set':
387 ypath = path_to_str(path)
388 v_new = get_elements(state_b, ypath)[0]
389 v_old = values
390 for type in [float, Color, Background]:
391 if isinstance(v_old, type) and isinstance(v_new, type):
392 animate.append((ypath, v_old, v_new))
394 return animate
397def interpolate(times, states, times_inter):
399 assert len(times) == len(states)
401 states_inter = []
402 for i in range(len(times) - 1):
404 state_a = states[i]
405 state_b = states[i+1]
406 time_a = times[i]
407 time_b = times[i+1]
409 animate = interpolateables(state_a, state_b)
411 if i == 0:
412 times_inter_this = times_inter[num.logical_and(
413 time_a <= times_inter, times_inter <= time_b)]
414 else:
415 times_inter_this = times_inter[num.logical_and(
416 time_a < times_inter, times_inter <= time_b)]
418 for time_inter in times_inter_this:
419 state = clone(state_b)
420 if time_b == time_a:
421 blend = 0.
422 else:
423 blend = (time_inter - time_a) / (time_b - time_a)
425 for ypath, v_old, v_new in animate:
426 if isinstance(v_old, float) and isinstance(v_new, float):
427 if ypath == 'strike':
428 if v_new - v_old > 180.:
429 v_new -= 360.
430 elif v_new - v_old < -180.:
431 v_new += 360.
433 if ypath != 'distance':
434 v_inter = v_old + blend * (v_new - v_old)
435 else:
436 v_old = num.log(v_old)
437 v_new = num.log(v_new)
438 v_inter = v_old + blend * (v_new - v_old)
439 v_inter = num.exp(v_inter)
441 set_elements(state, ypath, v_inter)
442 else:
443 set_elements(state, ypath, v_new)
445 states_inter.append(state)
447 return states_inter
450class Interpolator(object):
452 def __init__(self, times, states, fps=25.):
454 assert len(times) == len(states)
456 self.dt = 1.0 / fps
457 self.tmin = times[0]
458 self.tmax = times[-1]
459 times_inter = util.arange2(
460 self.tmin, self.tmax, self.dt, error='floor')
461 times_inter[-1] = times[-1]
463 states_inter = []
464 for i in range(len(times) - 1):
466 state_a = states[i]
467 state_b = states[i+1]
468 time_a = times[i]
469 time_b = times[i+1]
471 animate = interpolateables(state_a, state_b)
473 if i == 0:
474 times_inter_this = times_inter[num.logical_and(
475 time_a <= times_inter, times_inter <= time_b)]
476 else:
477 times_inter_this = times_inter[num.logical_and(
478 time_a < times_inter, times_inter <= time_b)]
480 for time_inter in times_inter_this:
481 state = clone(state_b)
483 if time_b == time_a:
484 blend = 0.
485 else:
486 blend = (time_inter - time_a) / (time_b - time_a)
488 for ypath, v_old, v_new in animate:
489 if isinstance(v_old, float) and isinstance(v_new, float):
490 if ypath in ('lon', 'strike'):
491 if v_new - v_old > 180.:
492 v_new -= 360.
493 elif v_new - v_old < -180.:
494 v_new += 360.
496 if ypath != 'distance':
497 v_inter = v_old + blend * (v_new - v_old)
498 else:
499 v_old = num.log(v_old)
500 v_new = num.log(v_new)
501 v_inter = v_old + blend * (v_new - v_old)
502 v_inter = num.exp(v_inter)
504 set_elements(state, ypath, v_inter)
506 elif isinstance(v_old, Color) and isinstance(v_new, Color):
507 v_inter = interpolate_color(v_old, v_new, blend)
508 set_elements(state, ypath, v_inter)
510 elif isinstance(v_old, Background) \
511 and isinstance(v_new, Background):
512 v_inter = interpolate_background(v_old, v_new, blend)
513 set_elements(state, ypath, v_inter)
515 else:
516 set_elements(state, ypath, v_new)
518 states_inter.append(state)
520 self._states_inter = states_inter
522 def __call__(self, t):
523 itime = int(round((t - self.tmin) / self.dt))
524 itime = min(max(0, itime), len(self._states_inter)-1)
525 return self._states_inter[itime]