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

186 

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) 

197 

198 return update_state, update_widget 

199 

200 update_state, update_widget = make_funcs() 

201 

202 state_bind( 

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

204 update_widget) 

205 

206 

207def state_bind_slider_float( 

208 owner, state, path, widget, 

209 min_is_none=False, 

210 max_is_none=False): 

211 

212 assert isinstance(widget, gui_util.QSliderFloat) 

213 

214 app = common.get_app() 

215 

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) 

225 

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) 

236 

237 return update_state, update_widget 

238 

239 update_state, update_widget = make_funcs() 

240 

241 state_bind( 

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

243 update_widget) 

244 

245 

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

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

248 

249 

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

251 

252 def make_funcs(): 

253 def update_state(widget, state): 

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

255 

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) 

263 

264 return update_state, update_widget 

265 

266 update_state, update_widget = make_funcs() 

267 

268 state_bind( 

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

270 update_widget) 

271 

272 

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

274 

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

282 

283 elif len(values) == 2: 

284 state.set( 

285 path, 

286 BackgroundGradient( 

287 color_top=Color(values[0]), 

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

289 

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) 

297 

298 return update_state, update_widget 

299 

300 update_state, update_widget = make_funcs() 

301 

302 state_bind( 

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

304 update_widget) 

305 

306 

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

308 

309 def make_funcs(): 

310 def update_state(widget, state): 

311 value = str(widget.currentText()) 

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

313 

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) 

321 

322 return update_state, update_widget 

323 

324 update_state, update_widget = make_funcs() 

325 

326 state_bind( 

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

328 update_widget) 

329 

330 

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

332 

333 def make_funcs(): 

334 def update_state(widget, state): 

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

336 

337 def update_widget(state, widget): 

338 widget.blockSignals(True) 

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

340 widget.blockSignals(False) 

341 

342 return update_state, update_widget 

343 

344 update_state, update_widget = make_funcs() 

345 

346 state_bind( 

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

348 update_widget) 

349 

350 

351def state_bind_lineedit( 

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

353 

354 def make_funcs(): 

355 

356 def update_state(widget, state): 

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

358 

359 def update_widget(state, widget): 

360 widget.blockSignals(True) 

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

362 widget.blockSignals(False) 

363 

364 return update_state, update_widget 

365 

366 update_state, update_widget = make_funcs() 

367 

368 state_bind( 

369 owner, 

370 state, [path], update_state, 

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

372 

373 

374def interpolateables(state_a, state_b): 

375 

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

385 

386 return animate 

387 

388 

389def interpolate(times, states, times_inter): 

390 

391 assert len(times) == len(states) 

392 

393 states_inter = [] 

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

395 

396 state_a = states[i] 

397 state_b = states[i+1] 

398 time_a = times[i] 

399 time_b = times[i+1] 

400 

401 animate = interpolateables(state_a, state_b) 

402 

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

409 

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) 

416 

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. 

424 

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) 

432 

433 set_elements(state, ypath, v_inter) 

434 else: 

435 set_elements(state, ypath, v_new) 

436 

437 states_inter.append(state) 

438 

439 return states_inter 

440 

441 

442class Interpolator(object): 

443 

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

445 

446 assert len(times) == len(states) 

447 

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] 

454 

455 states_inter = [] 

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

457 

458 state_a = states[i] 

459 state_b = states[i+1] 

460 time_a = times[i] 

461 time_b = times[i+1] 

462 

463 animate = interpolateables(state_a, state_b) 

464 

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

471 

472 for time_inter in times_inter_this: 

473 state = clone(state_b) 

474 

475 if time_b == time_a: 

476 blend = 0. 

477 else: 

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

479 

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. 

487 

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) 

495 

496 set_elements(state, ypath, v_inter) 

497 

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) 

501 

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) 

506 

507 else: 

508 set_elements(state, ypath, v_new) 

509 

510 states_inter.append(state) 

511 

512 self._states_inter = states_inter 

513 

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]