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 def make_funcs():
178 def update_state(widget, state):
179 val = widget.value()
180 if (min_is_none and val == widget.minimum()) \
181 or (max_is_none and val == widget.maximum()):
182 state.set(path, None)
183 else:
184 app.status('%g' % (val * factor))
185 state.set(path, dtype(val * factor))
187 def update_widget(state, widget):
188 val = state.get(path)
189 widget.blockSignals(True)
190 if min_is_none and val is None:
191 widget.setValue(widget.minimum())
192 elif max_is_none and val is None:
193 widget.setValue(widget.maximum())
194 else:
195 widget.setValue(int(state.get(path) * 1. / factor))
196 widget.blockSignals(False)
198 return update_state, update_widget
200 update_state, update_widget = make_funcs()
202 state_bind(
203 owner, state, [path], update_state, widget, [widget.valueChanged],
204 update_widget)
207def state_bind_slider_float(
208 owner, state, path, widget,
209 min_is_none=False,
210 max_is_none=False):
212 assert isinstance(widget, gui_util.QSliderFloat)
214 app = common.get_app()
216 def make_funcs():
217 def update_state(widget, state):
218 val = widget.valueFloat()
219 if (min_is_none and val == widget.minimumFloat()) \
220 or (max_is_none and val == widget.maximumFloat()):
221 state.set(path, None)
222 else:
223 app.status('%g' % (val))
224 state.set(path, val)
226 def update_widget(state, widget):
227 val = state.get(path)
228 widget.blockSignals(True)
229 if min_is_none and val is None:
230 widget.setValueFloat(widget.minimumFloat())
231 elif max_is_none and val is None:
232 widget.setValueFloat(widget.maximumFloat())
233 else:
234 widget.setValueFloat(state.get(path))
235 widget.blockSignals(False)
237 return update_state, update_widget
239 update_state, update_widget = make_funcs()
241 state_bind(
242 owner, state, [path], update_state, widget, [widget.valueChanged],
243 update_widget)
246def state_bind_spinbox(owner, state, path, widget, factor=1., dtype=float):
247 return state_bind_slider(owner, state, path, widget, factor, dtype)
250def state_bind_combobox(owner, state, path, widget):
252 def make_funcs():
253 def update_state(widget, state):
254 state.set(path, str(widget.currentText()))
256 def update_widget(state, widget):
257 widget.blockSignals(True)
258 val = state.get(path)
259 for i in range(widget.count()):
260 if str(widget.itemText(i)) == val:
261 widget.setCurrentIndex(i)
262 widget.blockSignals(False)
264 return update_state, update_widget
266 update_state, update_widget = make_funcs()
268 state_bind(
269 owner, state, [path], update_state, widget, [widget.activated],
270 update_widget)
273def state_bind_combobox_background(owner, state, path, widget):
275 def make_funcs():
276 def update_state(widget, state):
277 values = str(widget.currentText()).split(' - ')
278 if len(values) == 1:
279 state.set(
280 path,
281 Background(color=Color(values[0])))
283 elif len(values) == 2:
284 state.set(
285 path,
286 BackgroundGradient(
287 color_top=Color(values[0]),
288 color_bottom=Color(values[1])))
290 def update_widget(state, widget):
291 widget.blockSignals(True)
292 val = str(state.get(path))
293 for i in range(widget.count()):
294 if str(widget.itemText(i)) == val:
295 widget.setCurrentIndex(i)
296 widget.blockSignals(False)
298 return update_state, update_widget
300 update_state, update_widget = make_funcs()
302 state_bind(
303 owner, state, [path], update_state, widget, [widget.activated],
304 update_widget)
307def state_bind_combobox_color(owner, state, path, widget):
309 def make_funcs():
310 def update_state(widget, state):
311 value = str(widget.currentText())
312 state.set(path, Color(value))
314 def update_widget(state, widget):
315 widget.blockSignals(True)
316 val = str(state.get(path))
317 for i in range(widget.count()):
318 if str(widget.itemText(i)) == val:
319 widget.setCurrentIndex(i)
320 widget.blockSignals(False)
322 return update_state, update_widget
324 update_state, update_widget = make_funcs()
326 state_bind(
327 owner, state, [path], update_state, widget, [widget.activated],
328 update_widget)
331def state_bind_checkbox(owner, state, path, widget):
333 def make_funcs():
334 def update_state(widget, state):
335 state.set(path, bool(widget.isChecked()))
337 def update_widget(state, widget):
338 widget.blockSignals(True)
339 widget.setChecked(state.get(path))
340 widget.blockSignals(False)
342 return update_state, update_widget
344 update_state, update_widget = make_funcs()
346 state_bind(
347 owner, state, [path], update_state, widget, [widget.toggled],
348 update_widget)
351def state_bind_lineedit(
352 owner, state, path, widget, from_string=str, to_string=str):
354 def make_funcs():
356 def update_state(widget, state):
357 state.set(path, from_string(widget.text()))
359 def update_widget(state, widget):
360 widget.blockSignals(True)
361 widget.setText(to_string(state.get(path)))
362 widget.blockSignals(False)
364 return update_state, update_widget
366 update_state, update_widget = make_funcs()
368 state_bind(
369 owner,
370 state, [path], update_state,
371 widget, [widget.editingFinished, widget.returnPressed], update_widget)
374def interpolateables(state_a, state_b):
376 animate = []
377 for tag, path, values in state_a.diff(state_b):
378 if tag == 'set':
379 ypath = path_to_str(path)
380 v_old = get_elements(state_a, ypath)[0]
381 v_new = values
382 for type in [float, Color, Background]:
383 if isinstance(v_old, type) and isinstance(v_new, type):
384 animate.append((ypath, v_old, v_new))
386 return animate
389def interpolate(times, states, times_inter):
391 assert len(times) == len(states)
393 states_inter = []
394 for i in range(len(times) - 1):
396 state_a = states[i]
397 state_b = states[i+1]
398 time_a = times[i]
399 time_b = times[i+1]
401 animate = interpolateables(state_a, state_b)
403 if i == 0:
404 times_inter_this = times_inter[num.logical_and(
405 time_a <= times_inter, times_inter <= time_b)]
406 else:
407 times_inter_this = times_inter[num.logical_and(
408 time_a < times_inter, times_inter <= time_b)]
410 for time_inter in times_inter_this:
411 state = clone(state_b)
412 if time_b == time_a:
413 blend = 0.
414 else:
415 blend = (time_inter - time_a) / (time_b - time_a)
417 for ypath, v_old, v_new in animate:
418 if isinstance(v_old, float) and isinstance(v_new, float):
419 if ypath == 'strike':
420 if v_new - v_old > 180.:
421 v_new -= 360.
422 elif v_new - v_old < -180.:
423 v_new += 360.
425 if ypath != 'distance':
426 v_inter = v_old + blend * (v_new - v_old)
427 else:
428 v_old = num.log(v_old)
429 v_new = num.log(v_new)
430 v_inter = v_old + blend * (v_new - v_old)
431 v_inter = num.exp(v_inter)
433 set_elements(state, ypath, v_inter)
434 else:
435 set_elements(state, ypath, v_new)
437 states_inter.append(state)
439 return states_inter
442class Interpolator(object):
444 def __init__(self, times, states, fps=25.):
446 assert len(times) == len(states)
448 self.dt = 1.0 / fps
449 self.tmin = times[0]
450 self.tmax = times[-1]
451 times_inter = util.arange2(
452 self.tmin, self.tmax, self.dt, error='floor')
453 times_inter[-1] = times[-1]
455 states_inter = []
456 for i in range(len(times) - 1):
458 state_a = states[i]
459 state_b = states[i+1]
460 time_a = times[i]
461 time_b = times[i+1]
463 animate = interpolateables(state_a, state_b)
465 if i == 0:
466 times_inter_this = times_inter[num.logical_and(
467 time_a <= times_inter, times_inter <= time_b)]
468 else:
469 times_inter_this = times_inter[num.logical_and(
470 time_a < times_inter, times_inter <= time_b)]
472 for time_inter in times_inter_this:
473 state = clone(state_b)
475 if time_b == time_a:
476 blend = 0.
477 else:
478 blend = (time_inter - time_a) / (time_b - time_a)
480 for ypath, v_old, v_new in animate:
481 if isinstance(v_old, float) and isinstance(v_new, float):
482 if ypath == 'strike':
483 if v_new - v_old > 180.:
484 v_new -= 360.
485 elif v_new - v_old < -180.:
486 v_new += 360.
488 if ypath != 'distance':
489 v_inter = v_old + blend * (v_new - v_old)
490 else:
491 v_old = num.log(v_old)
492 v_new = num.log(v_new)
493 v_inter = v_old + blend * (v_new - v_old)
494 v_inter = num.exp(v_inter)
496 set_elements(state, ypath, v_inter)
498 elif isinstance(v_old, Color) and isinstance(v_new, Color):
499 v_inter = interpolate_color(v_old, v_new, blend)
500 set_elements(state, ypath, v_inter)
502 elif isinstance(v_old, Background) \
503 and isinstance(v_new, Background):
504 v_inter = interpolate_background(v_old, v_new, blend)
505 set_elements(state, ypath, v_inter)
507 else:
508 set_elements(state, ypath, v_new)
510 states_inter.append(state)
512 self._states_inter = states_inter
514 def __call__(self, t):
515 itime = int(round((t - self.tmin) / self.dt))
516 itime = min(max(0, itime), len(self._states_inter)-1)
517 return self._states_inter[itime]