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 

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

117 

118 @property 

119 def tmin_effective(self): 

120 return common.tmin_effective( 

121 self.tmin, self.tmax, self.tduration, self.tposition) 

122 

123 @property 

124 def tmax_effective(self): 

125 return common.tmax_effective( 

126 self.tmin, self.tmax, self.tduration, self.tposition) 

127 

128 def sort_elements(self): 

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

130 

131 

132def state_bind( 

133 owner, state, paths, update_state, 

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

135 

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) 

143 

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) 

154 

155 return wrap_update_widget, wrap_update_state 

156 

157 wrap_update_widget, wrap_update_state = make_wrappers(widget) 

158 

159 for sig in signals: 

160 sig.connect(wrap_update_state) 

161 

162 for path in paths: 

163 owner.talkie_connect(state, path, wrap_update_widget) 

164 

165 wrap_update_widget() 

166 

167 

168def state_bind_slider( 

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

170 dtype=float, 

171 min_is_none=False, 

172 max_is_none=False): 

173 

174 app = common.get_app() 

175 

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

185 

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) 

196 

197 return update_state, update_widget 

198 

199 update_state, update_widget = make_funcs() 

200 

201 state_bind( 

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

203 update_widget) 

204 

205 

206def state_bind_slider_float( 

207 owner, state, path, widget, 

208 min_is_none=False, 

209 max_is_none=False): 

210 

211 assert isinstance(widget, gui_util.QSliderFloat) 

212 

213 app = common.get_app() 

214 

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) 

224 

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) 

235 

236 return update_state, update_widget 

237 

238 update_state, update_widget = make_funcs() 

239 

240 state_bind( 

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

242 update_widget) 

243 

244 

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

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

247 

248 

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

250 

251 def make_funcs(): 

252 def update_state(widget, state): 

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

254 

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) 

262 

263 return update_state, update_widget 

264 

265 update_state, update_widget = make_funcs() 

266 

267 state_bind( 

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

269 update_widget) 

270 

271 

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

273 

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

281 

282 elif len(values) == 2: 

283 state.set( 

284 path, 

285 BackgroundGradient( 

286 color_top=Color(values[0]), 

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

288 

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) 

296 

297 return update_state, update_widget 

298 

299 update_state, update_widget = make_funcs() 

300 

301 state_bind( 

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

303 update_widget) 

304 

305 

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

307 

308 def make_funcs(): 

309 def update_state(widget, state): 

310 value = str(widget.currentText()) 

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

312 

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) 

320 

321 return update_state, update_widget 

322 

323 update_state, update_widget = make_funcs() 

324 

325 state_bind( 

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

327 update_widget) 

328 

329 

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

331 

332 def make_funcs(): 

333 def update_state(widget, state): 

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

335 

336 def update_widget(state, widget): 

337 widget.blockSignals(True) 

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

339 widget.blockSignals(False) 

340 

341 return update_state, update_widget 

342 

343 update_state, update_widget = make_funcs() 

344 

345 state_bind( 

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

347 update_widget) 

348 

349 

350def state_bind_lineedit( 

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

352 

353 def make_funcs(): 

354 

355 def update_state(widget, state): 

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

357 

358 def update_widget(state, widget): 

359 widget.blockSignals(True) 

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

361 widget.blockSignals(False) 

362 

363 return update_state, update_widget 

364 

365 update_state, update_widget = make_funcs() 

366 

367 state_bind( 

368 owner, 

369 state, [path], update_state, 

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

371 

372 

373def interpolateables(state_a, state_b): 

374 

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

384 

385 return animate 

386 

387 

388def interpolate(times, states, times_inter): 

389 

390 assert len(times) == len(states) 

391 

392 states_inter = [] 

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

394 

395 state_a = states[i] 

396 state_b = states[i+1] 

397 time_a = times[i] 

398 time_b = times[i+1] 

399 

400 animate = interpolateables(state_a, state_b) 

401 

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

408 

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) 

415 

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. 

423 

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) 

431 

432 set_elements(state, ypath, v_inter) 

433 else: 

434 set_elements(state, ypath, v_new) 

435 

436 states_inter.append(state) 

437 

438 return states_inter 

439 

440 

441class Interpolator(object): 

442 

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

444 

445 assert len(times) == len(states) 

446 

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] 

453 

454 states_inter = [] 

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

456 

457 state_a = states[i] 

458 state_b = states[i+1] 

459 time_a = times[i] 

460 time_b = times[i+1] 

461 

462 animate = interpolateables(state_a, state_b) 

463 

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

470 

471 for time_inter in times_inter_this: 

472 state = clone(state_b) 

473 

474 if time_b == time_a: 

475 blend = 0. 

476 else: 

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

478 

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. 

486 

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) 

494 

495 set_elements(state, ypath, v_inter) 

496 

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) 

500 

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) 

505 

506 else: 

507 set_elements(state, ypath, v_new) 

508 

509 states_inter.append(state) 

510 

511 self._states_inter = states_inter 

512 

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]