1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import copy 

7 

8import numpy as num 

9 

10from pyrocko.guts import Bool, Float, String, StringChoice 

11from pyrocko.gui import util as gui_util 

12from pyrocko.gui.vtk_util import ScatterPipe, BeachballPipe 

13from pyrocko.gui.qt_compat import qw, qc 

14 

15from . import base 

16from .. import common 

17 

18guts_prefix = 'sparrow' 

19km = 1e3 

20 

21 

22def inormalize(x, imin, imax, discrete=True): 

23 

24 xmin = num.nanmin(x) 

25 xmax = num.nanmax(x) 

26 if xmin == xmax: 

27 xmin -= 0.5 

28 xmax += 0.5 

29 

30 rmin = imin - 0.5 

31 rmax = imax + 0.5 

32 

33 if discrete: 

34 return num.clip( 

35 num.round( 

36 rmin + (x - xmin) * ( 

37 (rmax-rmin) / (xmax - xmin))).astype(num.int64), 

38 imin, imax) 

39 else: 

40 return num.clip( 

41 rmin + (x - xmin) * ((rmax-rmin) / (xmax - xmin)), 

42 imin, imax) 

43 

44 

45def string_to_sorted_idx(values): 

46 val_sort = num.sort(values, axis=-1, kind='mergesort') 

47 val_sort_unique = num.unique(val_sort) 

48 

49 val_to_idx = dict([ 

50 (val_sort_unique[i], i) 

51 for i in range(val_sort_unique.shape[0])]) 

52 

53 return num.array([val_to_idx[val] for val in values]) 

54 

55 

56class SymbolChoice(StringChoice): 

57 choices = ['point', 'sphere', 'beachball'] 

58 

59 

60class MaskingShapeChoice(StringChoice): 

61 choices = ['rect', 'linear', 'quadratic'] 

62 

63 

64class MaskingModeChoice(StringChoice): 

65 choices = ['past + future', 'past', 'future'] 

66 

67 @classmethod 

68 def get_factors(cls, mode, value_low): 

69 return { 

70 'past + future': (value_low, 1.0, value_low), 

71 'past': (value_low, 1.0, 0.0), 

72 'future': (0.0, 1.0, value_low)}[mode] 

73 

74 

75class TableState(base.ElementState): 

76 visible = Bool.T(default=True) 

77 size = Float.T(default=3.0) 

78 color_parameter = String.T(optional=True) 

79 cpt = base.CPTState.T(default=base.CPTState.D()) 

80 size_parameter = String.T(optional=True) 

81 depth_min = Float.T(optional=True) 

82 depth_max = Float.T(optional=True) 

83 depth_offset = Float.T(default=0.0) 

84 symbol = SymbolChoice.T(default='sphere') 

85 time_masking_opacity = Float.T(default=0.0) 

86 time_masking_shape = MaskingShapeChoice.T(default='rect') 

87 time_masking_mode = MaskingModeChoice.T(default='past + future') 

88 

89 

90class TableElement(base.Element): 

91 def __init__(self): 

92 base.Element.__init__(self) 

93 self._parent = None 

94 

95 self._table = None 

96 self._istate = 0 

97 self._istate_view = 0 

98 

99 self._controls = None 

100 self._color_combobox = None 

101 self._size_combobox = None 

102 

103 self._pipes = None 

104 self._pipe_maps = None 

105 self._isize_min = 1 

106 self._isize_max = 6 

107 

108 self.cpt_handler = base.CPTHandler() 

109 

110 def bind_state(self, state): 

111 base.Element.bind_state(self, state) 

112 self.talkie_connect(state, ['visible', 'size'], self.update) 

113 

114 self.talkie_connect( 

115 state, 

116 ['depth_min', 'depth_max', 'time_masking_shape', 

117 'time_masking_mode', 'time_masking_opacity'], 

118 self.update_alpha) 

119 

120 self.cpt_handler.bind_state(state.cpt, self.update) 

121 

122 self.talkie_connect( 

123 state, 

124 ['symbol', 'size_parameter', 'color_parameter'], 

125 self.update_sizes) 

126 

127 def unbind_state(self): 

128 self.cpt_handler.unbind_state() 

129 base.Element.unbind_state(self) 

130 

131 def get_name(self): 

132 return 'Table' 

133 

134 def set_parent(self, parent): 

135 self._parent = parent 

136 self._parent.add_panel( 

137 self.get_name(), 

138 self._get_controls(), 

139 visible=True, 

140 title_controls=[ 

141 self.get_title_control_remove(), 

142 self.get_title_control_visible()]) 

143 

144 for var in ['tmin', 'tmax', 'tduration', 'tposition']: 

145 self.talkie_connect( 

146 self._parent.state, var, self.update_alpha) 

147 

148 self._parent.register_data_provider(self) 

149 

150 self.update() 

151 

152 def iter_data(self, name): 

153 if self._table and self._table.has_col(name): 

154 yield self._table.get_col(name) 

155 

156 def set_table(self, table): 

157 self._table = table 

158 

159 self._istate += 1 

160 

161 if self._pipes is not None and self._istate != self._istate_view: 

162 self._clear_pipes() 

163 

164 self._update_controls() 

165 

166 def get_size_parameter_extra_entries(self): 

167 return [] 

168 

169 def get_color_parameter_extra_entries(self): 

170 return [] 

171 

172 def update_sizes(self, *args): 

173 self._istate += 1 

174 self.update() 

175 

176 def unset_parent(self): 

177 self.unbind_state() 

178 if self._parent: 

179 self._parent.unregister_data_provider(self) 

180 

181 self._clear_pipes() 

182 

183 if self._controls: 

184 self._parent.remove_panel(self._controls) 

185 self._controls = None 

186 

187 self._parent.update_view() 

188 self._parent = None 

189 

190 def _clear_pipes(self): 

191 if self._pipes is not None: 

192 for p in self._pipes: 

193 self._parent.remove_actor(p.actor) 

194 

195 self._pipes = None 

196 

197 if self._pipe_maps is not None: 

198 self._pipe_maps = None 

199 

200 def _init_pipes_scatter(self): 

201 state = self._state 

202 points = self._table.get_col('xyz') 

203 self._pipes = [] 

204 self._pipe_maps = [] 

205 if state.size_parameter: 

206 sizes = self._table.get_col(state.size_parameter) 

207 isizes = inormalize( 

208 sizes, self._isize_min, self._isize_max) 

209 

210 for i in range(self._isize_min, self._isize_max+1): 

211 b = isizes == i 

212 p = ScatterPipe(points[b].copy()) 

213 self._pipes.append(p) 

214 self._pipe_maps.append(b) 

215 else: 

216 self._pipes.append( 

217 ScatterPipe(points)) 

218 self._pipe_maps.append( 

219 num.ones(points.shape[0], dtype=bool)) 

220 

221 def _init_pipes_beachball(self): 

222 state = self._state 

223 self._pipes = [] 

224 

225 tab = self._table 

226 

227 positions = tab.get_col('xyz') 

228 

229 if tab.has_col('m6'): 

230 m6s = tab.get_col('m6') 

231 else: 

232 m6s = num.zeros((tab.get_nrows(), 6)) 

233 m6s[:, 3] = 1.0 

234 

235 if state.size_parameter: 

236 sizes = tab.get_col(state.size_parameter) 

237 else: 

238 sizes = num.ones(tab.get_nrows()) 

239 

240 if state.color_parameter: 

241 values = self._table.get_col(state.color_parameter) 

242 else: 

243 values = num.zeros(tab.get_nrows()) 

244 

245 rsizes = inormalize( 

246 sizes, self._isize_min, self._isize_max, discrete=False) * 0.005 

247 

248 pipe = BeachballPipe(positions, m6s, rsizes, values, self._parent.ren) 

249 self._pipes = [pipe] 

250 

251 def _update_pipes_scatter(self): 

252 state = self._state 

253 for i, p in enumerate(self._pipes): 

254 self._parent.add_actor(p.actor) 

255 p.set_size(state.size * (self._isize_min + i)**1.3) 

256 

257 if state.color_parameter: 

258 values = self._table.get_col(state.color_parameter) 

259 

260 if num.issubdtype(values.dtype, num.string_): 

261 values = string_to_sorted_idx(values) 

262 

263 self.cpt_handler._values = values 

264 self.cpt_handler.update_cpt() 

265 

266 cpt = copy.deepcopy( 

267 self.cpt_handler._cpts[self._state.cpt.cpt_name]) 

268 colors2 = cpt(values) 

269 colors2 = colors2 / 255. 

270 

271 for m, p in zip(self._pipe_maps, self._pipes): 

272 p.set_colors(colors2[m, :]) 

273 

274 for p in self._pipes: 

275 p.set_symbol(state.symbol) 

276 

277 def _update_pipes_beachball(self): 

278 state = self._state 

279 

280 p = self._pipes[0] 

281 

282 self._parent.add_actor(p.actor) 

283 p.set_size_factor(state.size * 0.005) 

284 

285 def update(self, *args): 

286 state = self._state 

287 

288 if self._pipes is not None and self._istate != self._istate_view: 

289 self._clear_pipes() 

290 

291 if not state.visible: 

292 if self._pipes is not None: 

293 for p in self._pipes: 

294 self._parent.remove_actor(p.actor) 

295 

296 else: 

297 if self._istate != self._istate_view and self._table: 

298 if state.symbol == 'beachball': 

299 self._init_pipes_beachball() 

300 else: 

301 self._init_pipes_scatter() 

302 

303 self._istate_view = self._istate 

304 

305 if self._pipes is not None: 

306 if state.symbol == 'beachball': 

307 self._update_pipes_beachball() 

308 else: 

309 self._update_pipes_scatter() 

310 

311 self.update_alpha() # TODO: only if needed? 

312 self._parent.update_view() 

313 

314 def update_alpha(self, *args, mask=None): 

315 if self._state.symbol == 'beachball': 

316 return 

317 

318 if self._pipes is None: 

319 return 

320 

321 time = self._table.get_col('time') 

322 depth = self._table.get_col('depth') 

323 

324 depth_mask = num.ones(time.size, dtype=bool) 

325 

326 if self._state.depth_min is not None: 

327 depth_mask &= depth >= self._state.depth_min 

328 if self._state.depth_max is not None: 

329 depth_mask &= depth <= self._state.depth_max 

330 

331 tmin = self._parent.state.tmin_effective 

332 tmax = self._parent.state.tmax_effective 

333 

334 if tmin is not None: 

335 m1 = time < tmin 

336 else: 

337 m1 = num.zeros(time.size, dtype=bool) 

338 

339 if tmax is not None: 

340 m3 = tmax < time 

341 else: 

342 m3 = num.zeros(time.size, dtype=bool) 

343 

344 m2 = num.logical_not(num.logical_or(m1, m3)) 

345 

346 value_low = self._state.time_masking_opacity 

347 

348 f1, f2, f3 = MaskingModeChoice.get_factors( 

349 self._state.time_masking_mode, value_low) 

350 

351 amp = num.ones(time.size, dtype=num.float64) 

352 amp[m1] = f1 

353 amp[m3] = f3 

354 if None in (tmin, tmax): 

355 amp[m2] = 1.0 

356 else: 

357 if self._state.time_masking_shape == 'rect': 

358 amp[m2] == 1.0 

359 elif self._state.time_masking_shape == 'linear': 

360 amp[m2] = time[m2] 

361 amp[m2] -= tmin 

362 amp[m2] /= (tmax - tmin) 

363 elif self._state.time_masking_shape == 'quadratic': 

364 amp[m2] = time[m2] 

365 amp[m2] -= tmin 

366 amp[m2] /= (tmax - tmin) 

367 amp[m2] **= 2 

368 

369 if f1 != 0.0: 

370 amp[m2] *= (1.0 - value_low) 

371 amp[m2] += value_low 

372 

373 amp *= depth_mask 

374 

375 for m, p in zip(self._pipe_maps, self._pipes): 

376 p.set_alpha(amp[m]) 

377 

378 self._parent.update_view() 

379 

380 def _get_table_widgets_start(self): 

381 return 0 

382 

383 def _get_controls(self): 

384 if self._controls is None: 

385 from ..state import state_bind_slider, state_bind_slider_float, \ 

386 state_bind_combobox, state_bind_lineedit 

387 

388 frame = qw.QFrame() 

389 layout = qw.QGridLayout() 

390 frame.setLayout(layout) 

391 

392 iy = self._get_table_widgets_start() 

393 

394 layout.addWidget(qw.QLabel('Size'), iy, 0) 

395 

396 slider = qw.QSlider(qc.Qt.Horizontal) 

397 slider.setSizePolicy( 

398 qw.QSizePolicy( 

399 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

400 slider.setMinimum(0) 

401 slider.setMaximum(100) 

402 layout.addWidget(slider, iy, 1) 

403 state_bind_slider(self, self._state, 'size', slider, factor=0.1) 

404 

405 iy += 1 

406 

407 layout.addWidget(qw.QLabel('Size Scaling'), iy, 0) 

408 

409 cb = qw.QComboBox() 

410 

411 layout.addWidget(cb, iy, 1) 

412 state_bind_combobox( 

413 self, self._state, 'size_parameter', cb) 

414 

415 self._size_combobox = cb 

416 

417 iy += 1 

418 

419 layout.addWidget(qw.QLabel('Color'), iy, 0) 

420 

421 cb = qw.QComboBox() 

422 

423 layout.addWidget(cb, iy, 1) 

424 state_bind_combobox( 

425 self, self._state, 'color_parameter', cb) 

426 

427 self._color_combobox = cb 

428 

429 self.cpt_handler.cpt_controls( 

430 self._parent, self._state.cpt, layout) 

431 

432 iy = layout.rowCount() + 1 

433 

434 layout.addWidget(qw.QLabel('Symbol'), iy, 0) 

435 

436 cb = common.string_choices_to_combobox(SymbolChoice) 

437 

438 layout.addWidget(cb, iy, 1) 

439 state_bind_combobox( 

440 self, self._state, 'symbol', cb) 

441 

442 iy += 1 

443 

444 layout.addWidget(qw.QLabel('Depth Min [km]'), iy, 0) 

445 slider = gui_util.QSliderFloat(qc.Qt.Horizontal) 

446 slider.setSizePolicy( 

447 qw.QSizePolicy( 

448 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

449 slider.setMinimumFloat(-60*km) 

450 slider.setMaximumFloat(700*km) 

451 layout.addWidget(slider, iy, 1) 

452 state_bind_slider_float( 

453 self, self._state, 'depth_min', slider, 

454 min_is_none=True) 

455 self._depth_min_slider = slider 

456 

457 le = qw.QLineEdit() 

458 layout.addWidget(le, iy, 2) 

459 state_bind_lineedit( 

460 self, self._state, 'depth_min', le, 

461 from_string=lambda s: None if s == 'off' else float(s)*1000., 

462 to_string=lambda v: 'off' if v is None else str(v/1000.)) 

463 

464 self._depth_min_lineedit = le 

465 

466 iy += 1 

467 

468 layout.addWidget(qw.QLabel('Depth Max [km]'), iy, 0) 

469 slider = gui_util.QSliderFloat(qc.Qt.Horizontal) 

470 slider.setSizePolicy( 

471 qw.QSizePolicy( 

472 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

473 slider.setMinimumFloat(-60*km) 

474 slider.setMaximumFloat(700*km) 

475 layout.addWidget(slider, iy, 1) 

476 state_bind_slider_float( 

477 self, self._state, 'depth_max', slider, 

478 max_is_none=True) 

479 self._depth_max_slider = slider 

480 

481 le = qw.QLineEdit() 

482 layout.addWidget(le, iy, 2) 

483 state_bind_lineedit( 

484 self, self._state, 'depth_max', le, 

485 from_string=lambda s: None if s == 'off' else float(s)*1000., 

486 to_string=lambda v: 'off' if v is None else str(v/1000.)) 

487 

488 self._depth_max_lineedit = le 

489 

490 iy += 1 

491 

492 layout.addWidget(qw.QLabel('Time Masking Opacity'), iy, 0) 

493 

494 slider = qw.QSlider(qc.Qt.Horizontal) 

495 slider.setSizePolicy( 

496 qw.QSizePolicy( 

497 qw.QSizePolicy.Expanding, qw.QSizePolicy.Fixed)) 

498 slider.setMinimum(0) 

499 slider.setMaximum(100) 

500 layout.addWidget(slider, iy, 1) 

501 state_bind_slider( 

502 self, self._state, 'time_masking_opacity', slider, factor=0.01) 

503 

504 iy += 1 

505 

506 layout.addWidget(qw.QLabel('Time Masking Shape'), iy, 0) 

507 cb = common.string_choices_to_combobox(MaskingShapeChoice) 

508 layout.addWidget(cb, iy, 1) 

509 state_bind_combobox(self, self._state, 'time_masking_shape', cb) 

510 

511 iy += 1 

512 

513 layout.addWidget(qw.QLabel('Time Masking Mode'), iy, 0) 

514 cb = common.string_choices_to_combobox(MaskingModeChoice) 

515 layout.addWidget(cb, iy, 1) 

516 state_bind_combobox(self, self._state, 'time_masking_mode', cb) 

517 

518 iy += 1 

519 

520 layout.addWidget(qw.QFrame(), iy, 0, 1, 3) 

521 

522 self._controls = frame 

523 

524 self._update_controls() 

525 

526 return self._controls 

527 

528 def _update_controls(self): 

529 for (cb, get_extra_entries) in [ 

530 (self._color_combobox, self.get_color_parameter_extra_entries), 

531 (self._size_combobox, self.get_size_parameter_extra_entries)]: 

532 

533 if cb is not None: 

534 cb.clear() 

535 

536 have = set() 

537 for s in get_extra_entries(): 

538 if s not in have: 

539 cb.insertItem(len(have), s) 

540 have.add(s) 

541 

542 if self._table is not None: 

543 for s in self._table.get_col_names(): 

544 h = self._table.get_header(s) 

545 if h.get_ncols() == 1 and s not in have: 

546 cb.insertItem(len(have), s) 

547 have.add(s) 

548 

549 self.cpt_handler._update_cpt_combobox() 

550 self.cpt_handler._update_cptscale_lineedit() 

551 

552 if self._table is not None and self._table.has_col('depth'): 

553 depth = self._table.get_col('depth') 

554 

555 if depth.size > 0: 

556 

557 depth_min = depth.min() 

558 depth_max = depth.max() 

559 

560 for wdg in (self._depth_min_slider, self._depth_max_slider): 

561 wdg.setMinimumFloat(depth_min) 

562 wdg.setMaximumFloat(depth_max) 

563 

564 

565__all__ = [ 

566 'TableElement', 

567 'TableState', 

568]