1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import logging 

7 

8import numpy as num 

9 

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 

13 

14from pyrocko.color import Color, interpolate as interpolate_color 

15 

16from pyrocko.gui import talkie 

17from pyrocko.gui import util as gui_util 

18from . import common, light 

19 

20guts_prefix = 'sparrow' 

21 

22logger = logging.getLogger('pyrocko.gui.sparrow.state') 

23 

24 

25class FocalPointChoice(StringChoice): 

26 choices = ['center', 'target'] 

27 

28 

29class ShadingChoice(StringChoice): 

30 choices = ['flat', 'gouraud', 'phong', 'pbr'] 

31 

32 

33class LightingChoice(StringChoice): 

34 choices = light.get_lighting_theme_names() 

35 

36 

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) 

44 

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)] 

49 

50 

51class Background(Object): 

52 color = Color.T(default=Color.D('black')) 

53 

54 def vtk_apply(self, ren): 

55 ren.GradientBackgroundOff() 

56 ren.SetBackground(*self.color.rgb) 

57 

58 def __str__(self): 

59 return str(self.color) 

60 

61 @property 

62 def color_top(self): 

63 return self.color 

64 

65 @property 

66 def color_bottom(self): 

67 return self.color 

68 

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 

72 

73 

74class BackgroundGradient(Background): 

75 color_top = Color.T(default=Color.D('skyblue1')) 

76 color_bottom = Color.T(default=Color.D('white')) 

77 

78 def vtk_apply(self, ren): 

79 ren.GradientBackgroundOn() 

80 ren.SetBackground(*self.color_bottom.rgb) 

81 ren.SetBackground2(*self.color_top.rgb) 

82 

83 def __str__(self): 

84 return '%s - %s' % (self.color_top, self.color_bottom) 

85 

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 

90 

91 

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)) 

101 

102 

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'))) 

118 

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) 

123 

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) 

128 

129 def sort_elements(self): 

130 self.elements.sort(key=lambda el: el.element_id) 

131 

132 

133def state_bind( 

134 owner, state, paths, update_state, 

135 widget, signals, update_widget, attribute=None): 

136 

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) 

144 

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) 

155 

156 return wrap_update_widget, wrap_update_state 

157 

158 wrap_update_widget, wrap_update_state = make_wrappers(widget) 

159 

160 for sig in signals: 

161 sig.connect(wrap_update_state) 

162 

163 for path in paths: 

164 owner.talkie_connect(state, path, wrap_update_widget) 

165 

166 wrap_update_widget() 

167 

168 

169def state_bind_slider( 

170 owner, state, path, widget, factor=1., 

171 dtype=float, 

172 min_is_none=False, 

173 max_is_none=False): 

174 

175 app = common.get_app() 

176 

177 viewer = app.get_main_window() 

178 widget.sliderPressed.connect(viewer.disable_capture) 

179 widget.sliderReleased.connect(viewer.enable_capture) 

180 

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)) 

190 

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) 

201 

202 return update_state, update_widget 

203 

204 update_state, update_widget = make_funcs() 

205 

206 state_bind( 

207 owner, state, [path], update_state, widget, [widget.valueChanged], 

208 update_widget) 

209 

210 

211def state_bind_slider_float( 

212 owner, state, path, widget, 

213 min_is_none=False, 

214 max_is_none=False): 

215 

216 assert isinstance(widget, gui_util.QSliderFloat) 

217 

218 app = common.get_app() 

219 

220 viewer = app.get_main_window() 

221 widget.sliderPressed.connect(viewer.disable_capture) 

222 widget.sliderReleased.connect(viewer.enable_capture) 

223 

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) 

233 

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) 

244 

245 return update_state, update_widget 

246 

247 update_state, update_widget = make_funcs() 

248 

249 state_bind( 

250 owner, state, [path], update_state, widget, [widget.valueChanged], 

251 update_widget) 

252 

253 

254def state_bind_spinbox(owner, state, path, widget, factor=1., dtype=float): 

255 return state_bind_slider(owner, state, path, widget, factor, dtype) 

256 

257 

258def state_bind_combobox(owner, state, path, widget): 

259 

260 def make_funcs(): 

261 def update_state(widget, state): 

262 state.set(path, str(widget.currentText())) 

263 

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) 

271 

272 return update_state, update_widget 

273 

274 update_state, update_widget = make_funcs() 

275 

276 state_bind( 

277 owner, state, [path], update_state, widget, [widget.activated], 

278 update_widget) 

279 

280 

281def state_bind_combobox_background(owner, state, path, widget): 

282 

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]))) 

290 

291 elif len(values) == 2: 

292 state.set( 

293 path, 

294 BackgroundGradient( 

295 color_top=Color(values[0]), 

296 color_bottom=Color(values[1]))) 

297 

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) 

305 

306 return update_state, update_widget 

307 

308 update_state, update_widget = make_funcs() 

309 

310 state_bind( 

311 owner, state, [path], update_state, widget, [widget.activated], 

312 update_widget) 

313 

314 

315def state_bind_combobox_color(owner, state, path, widget): 

316 

317 def make_funcs(): 

318 def update_state(widget, state): 

319 value = str(widget.currentText()) 

320 state.set(path, Color(value)) 

321 

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) 

329 

330 return update_state, update_widget 

331 

332 update_state, update_widget = make_funcs() 

333 

334 state_bind( 

335 owner, state, [path], update_state, widget, [widget.activated], 

336 update_widget) 

337 

338 

339def state_bind_checkbox(owner, state, path, widget): 

340 

341 def make_funcs(): 

342 def update_state(widget, state): 

343 state.set(path, bool(widget.isChecked())) 

344 

345 def update_widget(state, widget): 

346 widget.blockSignals(True) 

347 widget.setChecked(state.get(path)) 

348 widget.blockSignals(False) 

349 

350 return update_state, update_widget 

351 

352 update_state, update_widget = make_funcs() 

353 

354 state_bind( 

355 owner, state, [path], update_state, widget, [widget.toggled], 

356 update_widget) 

357 

358 

359def state_bind_lineedit( 

360 owner, state, path, widget, from_string=str, to_string=str): 

361 

362 def make_funcs(): 

363 

364 def update_state(widget, state): 

365 state.set(path, from_string(widget.text())) 

366 

367 def update_widget(state, widget): 

368 widget.blockSignals(True) 

369 widget.setText(to_string(state.get(path))) 

370 widget.blockSignals(False) 

371 

372 return update_state, update_widget 

373 

374 update_state, update_widget = make_funcs() 

375 

376 state_bind( 

377 owner, 

378 state, [path], update_state, 

379 widget, [widget.editingFinished, widget.returnPressed], update_widget) 

380 

381 

382def interpolateables(state_a, state_b): 

383 

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)) 

393 

394 return animate 

395 

396 

397def interpolate(times, states, times_inter): 

398 

399 assert len(times) == len(states) 

400 

401 states_inter = [] 

402 for i in range(len(times) - 1): 

403 

404 state_a = states[i] 

405 state_b = states[i+1] 

406 time_a = times[i] 

407 time_b = times[i+1] 

408 

409 animate = interpolateables(state_a, state_b) 

410 

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)] 

417 

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) 

424 

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. 

432 

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) 

440 

441 set_elements(state, ypath, v_inter) 

442 else: 

443 set_elements(state, ypath, v_new) 

444 

445 states_inter.append(state) 

446 

447 return states_inter 

448 

449 

450class Interpolator(object): 

451 

452 def __init__(self, times, states, fps=25.): 

453 

454 assert len(times) == len(states) 

455 

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] 

462 

463 states_inter = [] 

464 for i in range(len(times) - 1): 

465 

466 state_a = states[i] 

467 state_b = states[i+1] 

468 time_a = times[i] 

469 time_b = times[i+1] 

470 

471 animate = interpolateables(state_a, state_b) 

472 

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)] 

479 

480 for time_inter in times_inter_this: 

481 state = clone(state_b) 

482 

483 if time_b == time_a: 

484 blend = 0. 

485 else: 

486 blend = (time_inter - time_a) / (time_b - time_a) 

487 

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. 

495 

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) 

503 

504 set_elements(state, ypath, v_inter) 

505 

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) 

509 

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) 

514 

515 else: 

516 set_elements(state, ypath, v_new) 

517 

518 states_inter.append(state) 

519 

520 self._states_inter = states_inter 

521 

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]