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))
103class ViewerState(talkie.TalkieRoot):
104 lat = Float.T(default=0.0)
105 lon = Float.T(default=0.0)
106 depth = Float.T(default=0.0)
107 strike = Float.T(default=90.0)
108 dip = Float.T(default=0.0)
109 distance = Float.T(default=3.0)
110 elements = List.T(talkie.Talkie.T())
111 tmin = Timestamp.T(optional=True)
112 tmax = Timestamp.T(optional=True)
113 tduration = Duration.T(optional=True)
114 tposition = Float.T(default=0.0)
115 lighting = LightingChoice.T(default=LightingChoice.choices[0])
116 background = Background.T(default=Background.D(color=Color('black')))
118 @property
119 def tmin_effective(self):
120 return common.tmin_effective(
121 self.tmin, self.tmax, self.tduration, self.tposition)
123 @property
124 def tmax_effective(self):
125 return common.tmax_effective(
126 self.tmin, self.tmax, self.tduration, self.tposition)
128 def sort_elements(self):
129 self.elements.sort(key=lambda el: el.element_id)
132def state_bind(
133 owner, state, paths, update_state,
134 widget, signals, update_widget, attribute=None):
136 def make_wrappers(widget):
137 def wrap_update_widget(*args):
138 if attribute:
139 update_widget(state, attribute, widget)
140 else:
141 update_widget(state, widget)
142 common.de_errorize(widget)
144 def wrap_update_state(*args):
145 try:
146 if attribute:
147 update_state(widget, state, attribute)
148 else:
149 update_state(widget, state)
150 common.de_errorize(widget)
151 except Exception as e:
152 logger.warn('caught exception: %s' % e)
153 common.errorize(widget)
155 return wrap_update_widget, wrap_update_state
157 wrap_update_widget, wrap_update_state = make_wrappers(widget)
159 for sig in signals:
160 sig.connect(wrap_update_state)
162 for path in paths:
163 owner.talkie_connect(state, path, wrap_update_widget)
165 wrap_update_widget()
168def state_bind_slider(
169 owner, state, path, widget, factor=1.,
170 dtype=float,
171 min_is_none=False,
172 max_is_none=False):
174 app = common.get_app()
176 def make_funcs():
177 def update_state(widget, state):
178 val = widget.value()
179 if (min_is_none and val == widget.minimum()) \
180 or (max_is_none and val == widget.maximum()):
181 state.set(path, None)
182 else:
183 app.status('%g' % (val * factor))
184 state.set(path, dtype(val * factor))
186 def update_widget(state, widget):
187 val = state.get(path)
188 widget.blockSignals(True)
189 if min_is_none and val is None:
190 widget.setValue(widget.minimum())
191 elif max_is_none and val is None:
192 widget.setValue(widget.maximum())
193 else:
194 widget.setValue(int(state.get(path) * 1. / factor))
195 widget.blockSignals(False)
197 return update_state, update_widget
199 update_state, update_widget = make_funcs()
201 state_bind(
202 owner, state, [path], update_state, widget, [widget.valueChanged],
203 update_widget)
206def state_bind_slider_float(
207 owner, state, path, widget,
208 min_is_none=False,
209 max_is_none=False):
211 assert isinstance(widget, gui_util.QSliderFloat)
213 app = common.get_app()
215 def make_funcs():
216 def update_state(widget, state):
217 val = widget.valueFloat()
218 if (min_is_none and val == widget.minimumFloat()) \
219 or (max_is_none and val == widget.maximumFloat()):
220 state.set(path, None)
221 else:
222 app.status('%g' % (val))
223 state.set(path, val)
225 def update_widget(state, widget):
226 val = state.get(path)
227 widget.blockSignals(True)
228 if min_is_none and val is None:
229 widget.setValueFloat(widget.minimumFloat())
230 elif max_is_none and val is None:
231 widget.setValueFloat(widget.maximumFloat())
232 else:
233 widget.setValueFloat(state.get(path))
234 widget.blockSignals(False)
236 return update_state, update_widget
238 update_state, update_widget = make_funcs()
240 state_bind(
241 owner, state, [path], update_state, widget, [widget.valueChanged],
242 update_widget)
245def state_bind_spinbox(owner, state, path, widget, factor=1., dtype=float):
246 return state_bind_slider(owner, state, path, widget, factor, dtype)
249def state_bind_combobox(owner, state, path, widget):
251 def make_funcs():
252 def update_state(widget, state):
253 state.set(path, str(widget.currentText()))
255 def update_widget(state, widget):
256 widget.blockSignals(True)
257 val = state.get(path)
258 for i in range(widget.count()):
259 if str(widget.itemText(i)) == val:
260 widget.setCurrentIndex(i)
261 widget.blockSignals(False)
263 return update_state, update_widget
265 update_state, update_widget = make_funcs()
267 state_bind(
268 owner, state, [path], update_state, widget, [widget.activated],
269 update_widget)
272def state_bind_combobox_background(owner, state, path, widget):
274 def make_funcs():
275 def update_state(widget, state):
276 values = str(widget.currentText()).split(' - ')
277 if len(values) == 1:
278 state.set(
279 path,
280 Background(color=Color(values[0])))
282 elif len(values) == 2:
283 state.set(
284 path,
285 BackgroundGradient(
286 color_top=Color(values[0]),
287 color_bottom=Color(values[1])))
289 def update_widget(state, widget):
290 widget.blockSignals(True)
291 val = str(state.get(path))
292 for i in range(widget.count()):
293 if str(widget.itemText(i)) == val:
294 widget.setCurrentIndex(i)
295 widget.blockSignals(False)
297 return update_state, update_widget
299 update_state, update_widget = make_funcs()
301 state_bind(
302 owner, state, [path], update_state, widget, [widget.activated],
303 update_widget)
306def state_bind_combobox_color(owner, state, path, widget):
308 def make_funcs():
309 def update_state(widget, state):
310 value = str(widget.currentText())
311 state.set(path, Color(value))
313 def update_widget(state, widget):
314 widget.blockSignals(True)
315 val = str(state.get(path))
316 for i in range(widget.count()):
317 if str(widget.itemText(i)) == val:
318 widget.setCurrentIndex(i)
319 widget.blockSignals(False)
321 return update_state, update_widget
323 update_state, update_widget = make_funcs()
325 state_bind(
326 owner, state, [path], update_state, widget, [widget.activated],
327 update_widget)
330def state_bind_checkbox(owner, state, path, widget):
332 def make_funcs():
333 def update_state(widget, state):
334 state.set(path, bool(widget.isChecked()))
336 def update_widget(state, widget):
337 widget.blockSignals(True)
338 widget.setChecked(state.get(path))
339 widget.blockSignals(False)
341 return update_state, update_widget
343 update_state, update_widget = make_funcs()
345 state_bind(
346 owner, state, [path], update_state, widget, [widget.toggled],
347 update_widget)
350def state_bind_lineedit(
351 owner, state, path, widget, from_string=str, to_string=str):
353 def make_funcs():
355 def update_state(widget, state):
356 state.set(path, from_string(widget.text()))
358 def update_widget(state, widget):
359 widget.blockSignals(True)
360 widget.setText(to_string(state.get(path)))
361 widget.blockSignals(False)
363 return update_state, update_widget
365 update_state, update_widget = make_funcs()
367 state_bind(
368 owner,
369 state, [path], update_state,
370 widget, [widget.editingFinished, widget.returnPressed], update_widget)
373def interpolateables(state_a, state_b):
375 animate = []
376 for tag, path, values in state_a.diff(state_b):
377 if tag == 'set':
378 ypath = path_to_str(path)
379 v_old = get_elements(state_a, ypath)[0]
380 v_new = values
381 for type in [float, Color, Background]:
382 if isinstance(v_old, type) and isinstance(v_new, type):
383 animate.append((ypath, v_old, v_new))
385 return animate
388def interpolate(times, states, times_inter):
390 assert len(times) == len(states)
392 states_inter = []
393 for i in range(len(times) - 1):
395 state_a = states[i]
396 state_b = states[i+1]
397 time_a = times[i]
398 time_b = times[i+1]
400 animate = interpolateables(state_a, state_b)
402 if i == 0:
403 times_inter_this = times_inter[num.logical_and(
404 time_a <= times_inter, times_inter <= time_b)]
405 else:
406 times_inter_this = times_inter[num.logical_and(
407 time_a < times_inter, times_inter <= time_b)]
409 for time_inter in times_inter_this:
410 state = clone(state_b)
411 if time_b == time_a:
412 blend = 0.
413 else:
414 blend = (time_inter - time_a) / (time_b - time_a)
416 for ypath, v_old, v_new in animate:
417 if isinstance(v_old, float) and isinstance(v_new, float):
418 if ypath == 'strike':
419 if v_new - v_old > 180.:
420 v_new -= 360.
421 elif v_new - v_old < -180.:
422 v_new += 360.
424 if ypath != 'distance':
425 v_inter = v_old + blend * (v_new - v_old)
426 else:
427 v_old = num.log(v_old)
428 v_new = num.log(v_new)
429 v_inter = v_old + blend * (v_new - v_old)
430 v_inter = num.exp(v_inter)
432 set_elements(state, ypath, v_inter)
433 else:
434 set_elements(state, ypath, v_new)
436 states_inter.append(state)
438 return states_inter
441class Interpolator(object):
443 def __init__(self, times, states, fps=25.):
445 assert len(times) == len(states)
447 self.dt = 1.0 / fps
448 self.tmin = times[0]
449 self.tmax = times[-1]
450 times_inter = util.arange2(
451 self.tmin, self.tmax, self.dt, error='floor')
452 times_inter[-1] = times[-1]
454 states_inter = []
455 for i in range(len(times) - 1):
457 state_a = states[i]
458 state_b = states[i+1]
459 time_a = times[i]
460 time_b = times[i+1]
462 animate = interpolateables(state_a, state_b)
464 if i == 0:
465 times_inter_this = times_inter[num.logical_and(
466 time_a <= times_inter, times_inter <= time_b)]
467 else:
468 times_inter_this = times_inter[num.logical_and(
469 time_a < times_inter, times_inter <= time_b)]
471 for time_inter in times_inter_this:
472 state = clone(state_b)
474 if time_b == time_a:
475 blend = 0.
476 else:
477 blend = (time_inter - time_a) / (time_b - time_a)
479 for ypath, v_old, v_new in animate:
480 if isinstance(v_old, float) and isinstance(v_new, float):
481 if ypath == 'strike':
482 if v_new - v_old > 180.:
483 v_new -= 360.
484 elif v_new - v_old < -180.:
485 v_new += 360.
487 if ypath != 'distance':
488 v_inter = v_old + blend * (v_new - v_old)
489 else:
490 v_old = num.log(v_old)
491 v_new = num.log(v_new)
492 v_inter = v_old + blend * (v_new - v_old)
493 v_inter = num.exp(v_inter)
495 set_elements(state, ypath, v_inter)
497 elif isinstance(v_old, Color) and isinstance(v_new, Color):
498 v_inter = interpolate_color(v_old, v_new, blend)
499 set_elements(state, ypath, v_inter)
501 elif isinstance(v_old, Background) \
502 and isinstance(v_new, Background):
503 v_inter = interpolate_background(v_old, v_new, blend)
504 set_elements(state, ypath, v_inter)
506 else:
507 set_elements(state, ypath, v_new)
509 states_inter.append(state)
511 self._states_inter = states_inter
513 def __call__(self, t):
514 itime = int(round((t - self.tmin) / self.dt))
515 itime = min(max(0, itime), len(self._states_inter)-1)
516 return self._states_inter[itime]