1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import math 

7import signal 

8import gc 

9import logging 

10import time 

11import tempfile 

12import os 

13import shutil 

14import platform 

15from subprocess import check_call 

16 

17import numpy as num 

18 

19from pyrocko import cake 

20from pyrocko import guts 

21from pyrocko import geonames 

22from pyrocko import moment_tensor as pmt 

23 

24from pyrocko.gui.util import Progressbars, RangeEdit 

25from pyrocko.gui.talkie import TalkieConnectionOwner 

26from pyrocko.gui.qt_compat import qw, qc, qg 

27# from pyrocko.gui import vtk_util 

28 

29from . import common, light, snapshots as snapshots_mod 

30 

31import vtk 

32import vtk.qt 

33vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa 

34 

35from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

36 

37from pyrocko import geometry # noqa 

38from . import state as vstate, elements # noqa 

39 

40logger = logging.getLogger('pyrocko.gui.sparrow.main') 

41 

42 

43d2r = num.pi/180. 

44km = 1000. 

45 

46if platform.uname()[0] == 'Darwin': 

47 g_modifier_key = '\u2318' 

48else: 

49 g_modifier_key = 'Ctrl' 

50 

51 

52class ZeroFrame(qw.QFrame): 

53 

54 def sizeHint(self): 

55 return qc.QSize(0, 0) 

56 

57 

58class LocationChoice(object): 

59 def __init__(self, name, lat, lon, depth=0): 

60 self._name = name 

61 self._lat = lat 

62 self._lon = lon 

63 self._depth = depth 

64 

65 def get_lat_lon_depth(self): 

66 return self._lat, self._lon, self._depth 

67 

68 

69def location_to_choices(s): 

70 choices = [] 

71 s_vals = s.replace(',', ' ') 

72 try: 

73 vals = [float(x) for x in s_vals.split()] 

74 if len(vals) == 3: 

75 vals[2] *= km 

76 

77 choices.append(LocationChoice('', *vals)) 

78 

79 except ValueError: 

80 cities = geonames.get_cities_by_name(s.strip()) 

81 for c in cities: 

82 choices.append(LocationChoice(c.asciiname, c.lat, c.lon)) 

83 

84 return choices 

85 

86 

87class NoLocationChoices(Exception): 

88 

89 def __init__(self, s): 

90 self._string = s 

91 

92 def __str__(self): 

93 return 'No location choices for string "%s"' % self._string 

94 

95 

96class QVTKWidget(QVTKRenderWindowInteractor): 

97 def __init__(self, viewer, *args): 

98 QVTKRenderWindowInteractor.__init__(self, *args) 

99 self._viewer = viewer 

100 

101 def wheelEvent(self, event): 

102 return self._viewer.myWheelEvent(event) 

103 

104 def container_resized(self, ev): 

105 self._viewer.update_vtk_widget_size() 

106 

107 

108class DetachedViewer(qw.QMainWindow): 

109 

110 def __init__(self, main_window, vtk_frame): 

111 qw.QMainWindow.__init__(self, main_window) 

112 self.main_window = main_window 

113 self.setWindowTitle('Sparrow View') 

114 vtk_frame.setParent(self) 

115 self.setCentralWidget(vtk_frame) 

116 

117 def closeEvent(self, ev): 

118 ev.ignore() 

119 self.main_window.attach() 

120 

121 

122class CenteringScrollArea(qw.QScrollArea): 

123 def __init__(self): 

124 qw.QScrollArea.__init__(self) 

125 self.setAlignment(qc.Qt.AlignCenter) 

126 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

127 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

128 self.setFrameShape(qw.QFrame.NoFrame) 

129 

130 def resizeEvent(self, ev): 

131 retval = qw.QScrollArea.resizeEvent(self, ev) 

132 self.widget().container_resized(ev) 

133 return retval 

134 

135 def recenter(self): 

136 for sb in (self.verticalScrollBar(), self.horizontalScrollBar()): 

137 sb.setValue(int(round(0.5 * (sb.minimum() + sb.maximum())))) 

138 

139 def wheelEvent(self, *args, **kwargs): 

140 return self.widget().wheelEvent(*args, **kwargs) 

141 

142 

143class YAMLEditor(qw.QTextEdit): 

144 

145 def __init__(self, parent): 

146 qw.QTextEdit.__init__(self) 

147 self._parent = parent 

148 

149 def event(self, ev): 

150 if isinstance(ev, qg.QKeyEvent) \ 

151 and ev.key() == qc.Qt.Key_Return \ 

152 and ev.modifiers() & qc.Qt.ShiftModifier: 

153 self._parent.state_changed() 

154 return True 

155 

156 return qw.QTextEdit.event(self, ev) 

157 

158 

159class StateEditor(qw.QFrame, TalkieConnectionOwner): 

160 def __init__(self, viewer, *args, **kwargs): 

161 qw.QFrame.__init__(self, *args, **kwargs) 

162 TalkieConnectionOwner.__init__(self) 

163 

164 layout = qw.QGridLayout() 

165 

166 self.setLayout(layout) 

167 

168 self.source_editor = YAMLEditor(self) 

169 self.source_editor.setAcceptRichText(False) 

170 self.source_editor.setStatusTip('Press Shift-Return to apply changes') 

171 font = qg.QFont("Monospace") 

172 self.source_editor.setCurrentFont(font) 

173 layout.addWidget(self.source_editor, 0, 0, 1, 2) 

174 

175 self.error_display_label = qw.QLabel('Error') 

176 layout.addWidget(self.error_display_label, 1, 0, 1, 2) 

177 

178 self.error_display = qw.QTextEdit() 

179 self.error_display.setCurrentFont(font) 

180 self.error_display.setReadOnly(True) 

181 

182 self.error_display.setSizePolicy( 

183 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

184 

185 self.error_display_label.hide() 

186 self.error_display.hide() 

187 

188 layout.addWidget(self.error_display, 2, 0, 1, 2) 

189 

190 self.instant_updates = qw.QCheckBox('Instant Updates') 

191 self.instant_updates.toggled.connect(self.state_changed) 

192 layout.addWidget(self.instant_updates, 3, 0) 

193 

194 button = qw.QPushButton('Apply') 

195 button.clicked.connect(self.state_changed) 

196 layout.addWidget(button, 3, 1) 

197 

198 self.viewer = viewer 

199 # recommended way, but resulted in a variable-width font being used: 

200 # font = qg.QFontDatabase.systemFont(qg.QFontDatabase.FixedFont) 

201 self.bind_state() 

202 self.source_editor.textChanged.connect(self.text_changed_handler) 

203 self.destroyed.connect(self.unbind_state) 

204 self.bind_state() 

205 

206 def bind_state(self, *args): 

207 self.talkie_connect(self.viewer.state, '', self.update_state) 

208 self.update_state() 

209 

210 def unbind_state(self): 

211 self.talkie_disconnect_all() 

212 

213 def update_state(self, *args): 

214 cursor = self.source_editor.textCursor() 

215 

216 cursor_position = cursor.position() 

217 vsb_position = self.source_editor.verticalScrollBar().value() 

218 hsb_position = self.source_editor.horizontalScrollBar().value() 

219 

220 self.source_editor.setPlainText(str(self.viewer.state)) 

221 

222 cursor.setPosition(cursor_position) 

223 self.source_editor.setTextCursor(cursor) 

224 self.source_editor.verticalScrollBar().setValue(vsb_position) 

225 self.source_editor.horizontalScrollBar().setValue(hsb_position) 

226 

227 def text_changed_handler(self, *args): 

228 if self.instant_updates.isChecked(): 

229 self.state_changed() 

230 

231 def state_changed(self): 

232 try: 

233 s = self.source_editor.toPlainText() 

234 state = guts.load(string=s) 

235 self.viewer.set_state(state) 

236 self.error_display.setPlainText('') 

237 self.error_display_label.hide() 

238 self.error_display.hide() 

239 

240 except Exception as e: 

241 self.error_display.show() 

242 self.error_display_label.show() 

243 self.error_display.setPlainText(str(e)) 

244 

245 

246class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

247 def __init__(self, use_depth_peeling=True, events=None, snapshots=None): 

248 qw.QMainWindow.__init__(self) 

249 TalkieConnectionOwner.__init__(self) 

250 

251 common.get_app().set_main_window(self) 

252 

253 self.state = vstate.ViewerState() 

254 self.gui_state = vstate.ViewerGuiState() 

255 

256 self.setWindowTitle('Sparrow') 

257 

258 self.setTabPosition( 

259 qc.Qt.AllDockWidgetAreas, qw.QTabWidget.West) 

260 

261 self.planet_radius = cake.earthradius 

262 self.feature_radius_min = cake.earthradius - 1000. * km 

263 

264 self._panel_togglers = {} 

265 self._actors = set() 

266 self._actors_2d = set() 

267 self._render_window_size = (0, 0) 

268 self._use_depth_peeling = use_depth_peeling 

269 self._in_update_elements = False 

270 

271 mbar = qw.QMenuBar() 

272 self.setMenuBar(mbar) 

273 

274 menu = mbar.addMenu('File') 

275 

276 menu.addAction( 

277 'Export Image...', 

278 self.export_image, 

279 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_E)).setShortcutContext( 

280 qc.Qt.ApplicationShortcut) 

281 

282 menu.addAction( 

283 'Quit', 

284 self.request_quit, 

285 qg.QKeySequence(qc.Qt.CTRL | qc.Qt.Key_Q)).setShortcutContext( 

286 qc.Qt.ApplicationShortcut) 

287 

288 menu = mbar.addMenu('View') 

289 menu_sizes = menu.addMenu('Size') 

290 self._add_vtk_widget_size_menu_entries(menu_sizes) 

291 

292 # detached/attached 

293 self.talkie_connect( 

294 self.gui_state, 'detached', self.update_detached) 

295 

296 action = qw.QAction('Detach') 

297 action.setCheckable(True) 

298 action.setShortcut(qc.Qt.CTRL | qc.Qt.Key_D) 

299 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

300 

301 vstate.state_bind_checkbox(self, self.gui_state, 'detached', action) 

302 menu.addAction(action) 

303 

304 self.panels_menu = mbar.addMenu('Panels') 

305 

306 menu = mbar.addMenu('Add') 

307 for name, estate in [ 

308 ('Icosphere', elements.IcosphereState( 

309 level=4, 

310 smooth=True, 

311 opacity=0.5, 

312 ambient=0.1)), 

313 ('Grid', elements.GridState()), 

314 ('Stations', elements.StationsState()), 

315 ('Topography', elements.TopoState()), 

316 ('Custom Topography', elements.CustomTopoState()), 

317 ('Catalog', elements.CatalogState()), 

318 ('Coastlines', elements.CoastlinesState()), 

319 ('Source', elements.SourceState()), 

320 ('HUD (tmax)', elements.HudState( 

321 variables=['tmax'], 

322 template='tmax: {0|date}', 

323 position='top-left')), 

324 ('HUD subtitle', elements.HudState( 

325 template='Awesome')), 

326 ('Volcanoes', elements.VolcanoesState()), 

327 ('Faults', elements.ActiveFaultsState()), 

328 ('Plate bounds', elements.PlatesBoundsState()), 

329 ('InSAR Surface Displacements', elements.KiteState()), 

330 ('Geometry', elements.GeometryState()), 

331 ('Spheroid', elements.SpheroidState()), 

332 ('Rays', elements.RaysState())]: 

333 

334 def wrap_add_element(estate): 

335 def add_element(*args): 

336 new_element = guts.clone(estate) 

337 new_element.element_id = elements.random_id() 

338 self.state.elements.append(new_element) 

339 self.state.sort_elements() 

340 

341 return add_element 

342 

343 mitem = qw.QAction(name, self) 

344 

345 mitem.triggered.connect(wrap_add_element(estate)) 

346 

347 menu.addAction(mitem) 

348 

349 self.data_providers = [] 

350 self.elements = {} 

351 

352 self.detached_window = None 

353 

354 self.main_frame = qw.QFrame() 

355 self.main_frame.setFrameShape(qw.QFrame.NoFrame) 

356 

357 self.vtk_frame = CenteringScrollArea() 

358 

359 self.vtk_widget = QVTKWidget(self, self) 

360 self.vtk_frame.setWidget(self.vtk_widget) 

361 

362 self.main_layout = qw.QVBoxLayout() 

363 self.main_layout.setContentsMargins(0, 0, 0, 0) 

364 self.main_layout.addWidget(self.vtk_frame, qc.Qt.AlignCenter) 

365 

366 pb = Progressbars(self) 

367 self.progressbars = pb 

368 self.main_layout.addWidget(pb) 

369 

370 self.main_frame.setLayout(self.main_layout) 

371 

372 self.vtk_frame_substitute = None 

373 

374 self.add_panel( 

375 'Navigation', 

376 self.controls_navigation(), visible=True, 

377 where=qc.Qt.LeftDockWidgetArea) 

378 

379 self.add_panel( 

380 'Time', 

381 self.controls_time(), visible=True, 

382 where=qc.Qt.LeftDockWidgetArea) 

383 

384 self.add_panel( 

385 'Appearance', 

386 self.controls_appearance(), visible=True, 

387 where=qc.Qt.LeftDockWidgetArea) 

388 

389 snapshots_panel = self.controls_snapshots() 

390 self.add_panel( 

391 'Snapshots', 

392 snapshots_panel, visible=False, 

393 where=qc.Qt.LeftDockWidgetArea) 

394 

395 self.setCentralWidget(self.main_frame) 

396 

397 self.mesh = None 

398 

399 ren = vtk.vtkRenderer() 

400 

401 # ren.SetBackground(0.15, 0.15, 0.15) 

402 # ren.SetBackground(0.0, 0.0, 0.0) 

403 # ren.TwoSidedLightingOn() 

404 # ren.SetUseShadows(1) 

405 

406 self._lighting = None 

407 self._background = None 

408 

409 self.ren = ren 

410 self.update_render_settings() 

411 self.update_camera() 

412 

413 renwin = self.vtk_widget.GetRenderWindow() 

414 

415 if self._use_depth_peeling: 

416 renwin.SetAlphaBitPlanes(1) 

417 renwin.SetMultiSamples(0) 

418 

419 ren.SetUseDepthPeeling(1) 

420 ren.SetMaximumNumberOfPeels(100) 

421 ren.SetOcclusionRatio(0.1) 

422 

423 ren.SetUseFXAA(1) 

424 # ren.SetUseHiddenLineRemoval(1) 

425 # ren.SetBackingStore(1) 

426 

427 self.renwin = renwin 

428 

429 # renwin.LineSmoothingOn() 

430 # renwin.PointSmoothingOn() 

431 # renwin.PolygonSmoothingOn() 

432 

433 renwin.AddRenderer(ren) 

434 

435 iren = renwin.GetInteractor() 

436 iren.LightFollowCameraOn() 

437 iren.SetInteractorStyle(None) 

438 

439 iren.AddObserver('LeftButtonPressEvent', self.button_event) 

440 iren.AddObserver('LeftButtonReleaseEvent', self.button_event) 

441 iren.AddObserver('MiddleButtonPressEvent', self.button_event) 

442 iren.AddObserver('MiddleButtonReleaseEvent', self.button_event) 

443 iren.AddObserver('RightButtonPressEvent', self.button_event) 

444 iren.AddObserver('RightButtonReleaseEvent', self.button_event) 

445 iren.AddObserver('MouseMoveEvent', self.mouse_move_event) 

446 iren.AddObserver('KeyPressEvent', self.key_down_event) 

447 iren.AddObserver('KeyReleaseEvent', self.key_up_event) 

448 iren.AddObserver('ModifiedEvent', self.check_vtk_resize) 

449 

450 renwin.Render() 

451 

452 iren.Initialize() 

453 

454 self.iren = iren 

455 

456 self.rotating = False 

457 

458 self._elements = {} 

459 self._elements_active = {} 

460 

461 self.talkie_connect( 

462 self.state, 'elements', self.update_elements) 

463 

464 self.state.elements.append(elements.IcosphereState( 

465 element_id='icosphere', 

466 level=4, 

467 smooth=True, 

468 opacity=0.5, 

469 ambient=0.1)) 

470 

471 self.state.elements.append(elements.GridState( 

472 element_id='grid')) 

473 self.state.elements.append(elements.CoastlinesState( 

474 element_id='coastlines')) 

475 self.state.elements.append(elements.CrosshairState( 

476 element_id='crosshair')) 

477 

478 # self.state.elements.append(elements.StationsState()) 

479 # self.state.elements.append(elements.SourceState()) 

480 # self.state.elements.append( 

481 # elements.CatalogState( 

482 # selection=elements.FileCatalogSelection(paths=['japan.dat']))) 

483 # selection=elements.FileCatalogSelection(paths=['excerpt.dat']))) 

484 

485 if events: 

486 self.state.elements.append( 

487 elements.CatalogState( 

488 selection=elements.MemoryCatalogSelection(events=events))) 

489 

490 self.state.sort_elements() 

491 

492 if snapshots: 

493 snapshots_ = [] 

494 for obj in snapshots: 

495 if isinstance(obj, str): 

496 snapshots_.extend(snapshots_mod.load_snapshots(obj)) 

497 else: 

498 snapshots.append(obj) 

499 

500 snapshots_panel.add_snapshots(snapshots_) 

501 self.raise_panel(snapshots_panel) 

502 snapshots_panel.goto_snapshot(1) 

503 

504 self.timer = qc.QTimer(self) 

505 self.timer.timeout.connect(self.periodical) 

506 self.timer.setInterval(1000) 

507 self.timer.start() 

508 

509 self._animation_saver = None 

510 

511 self.closing = False 

512 self.vtk_widget.setFocus() 

513 

514 self.update_detached() 

515 

516 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0) 

517 common.get_app().status('Let\'s fly.', 2.0) 

518 

519 self.show() 

520 self.windowHandle().showMaximized() 

521 

522 self.talkie_connect( 

523 self.gui_state, 'fixed_size', self.update_vtk_widget_size) 

524 

525 self.update_vtk_widget_size() 

526 

527 def _add_vtk_widget_size_menu_entries(self, menu): 

528 

529 group = qw.QActionGroup(menu) 

530 group.setExclusive(True) 

531 

532 def set_variable_size(): 

533 self.gui_state.fixed_size = False 

534 

535 variable_size_action = menu.addAction('Fit Window Size') 

536 variable_size_action.setCheckable(True) 

537 variable_size_action.setActionGroup(group) 

538 variable_size_action.triggered.connect(set_variable_size) 

539 

540 fixed_size_items = [] 

541 for nx, ny, label in [ 

542 (None, None, 'Aspect 16:9 (e.g. for YouTube)'), 

543 (426, 240, ''), 

544 (640, 360, ''), 

545 (854, 480, '(FWVGA)'), 

546 (1280, 720, '(HD)'), 

547 (1920, 1080, '(Full HD)'), 

548 (2560, 1440, '(Quad HD)'), 

549 (3840, 2160, '(4K UHD)'), 

550 (3840*2, 2160*2, '',), 

551 (None, None, 'Aspect 4:3'), 

552 (640, 480, '(VGA)'), 

553 (800, 600, '(SVGA)'), 

554 (None, None, 'Other'), 

555 (512, 512, ''), 

556 (1024, 1024, '')]: 

557 

558 if None in (nx, ny): 

559 menu.addSection(label) 

560 else: 

561 name = '%i x %i%s' % (nx, ny, ' %s' % label if label else '') 

562 action = menu.addAction(name) 

563 action.setCheckable(True) 

564 action.setActionGroup(group) 

565 fixed_size_items.append((action, (nx, ny))) 

566 

567 def make_set_fixed_size(nx, ny): 

568 def set_fixed_size(): 

569 self.gui_state.fixed_size = (float(nx), float(ny)) 

570 

571 return set_fixed_size 

572 

573 action.triggered.connect(make_set_fixed_size(nx, ny)) 

574 

575 def update_widget(*args): 

576 for action, (nx, ny) in fixed_size_items: 

577 action.blockSignals(True) 

578 action.setChecked( 

579 bool(self.gui_state.fixed_size and (nx, ny) == tuple( 

580 int(z) for z in self.gui_state.fixed_size))) 

581 action.blockSignals(False) 

582 

583 variable_size_action.blockSignals(True) 

584 variable_size_action.setChecked(not self.gui_state.fixed_size) 

585 variable_size_action.blockSignals(False) 

586 

587 update_widget() 

588 self.talkie_connect( 

589 self.gui_state, 'fixed_size', update_widget) 

590 

591 def update_vtk_widget_size(self, *args): 

592 if self.gui_state.fixed_size: 

593 nx, ny = (int(round(x)) for x in self.gui_state.fixed_size) 

594 wanted_size = qc.QSize(nx, ny) 

595 else: 

596 wanted_size = qc.QSize( 

597 self.vtk_frame.window().width(), self.vtk_frame.height()) 

598 

599 current_size = self.vtk_widget.size() 

600 

601 if current_size.width() != wanted_size.width() \ 

602 or current_size.height() != wanted_size.height(): 

603 

604 self.vtk_widget.setFixedSize(wanted_size) 

605 

606 self.vtk_frame.recenter() 

607 self.check_vtk_resize() 

608 

609 def update_focal_point(self, *args): 

610 if self.gui_state.focal_point == 'center': 

611 self.vtk_widget.setStatusTip( 

612 'Click and drag: change location. %s-click and drag: ' 

613 'change view plane orientation.' % g_modifier_key) 

614 else: 

615 self.vtk_widget.setStatusTip( 

616 '%s-click and drag: change location. Click and drag: ' 

617 'change view plane orientation. Uncheck "Navigation: Fix" to ' 

618 'reverse sense.' % g_modifier_key) 

619 

620 def update_detached(self, *args): 

621 

622 if self.gui_state.detached and not self.detached_window: # detach 

623 logger.debug('Detaching VTK view.') 

624 

625 self.main_layout.removeWidget(self.vtk_frame) 

626 self.detached_window = DetachedViewer(self, self.vtk_frame) 

627 self.detached_window.show() 

628 self.vtk_widget.setFocus() 

629 

630 screens = common.get_app().screens() 

631 if len(screens) > 1: 

632 for screen in screens: 

633 if screen is not self.screen(): 

634 self.detached_window.windowHandle().setScreen(screen) 

635 # .setScreen() does not work reliably, 

636 # therefore trying also with .move()... 

637 p = screen.geometry().topLeft() 

638 self.detached_window.move(p.x() + 50, p.y() + 50) 

639 # ... but also does not work in notion window manager. 

640 

641 self.detached_window.windowHandle().showMaximized() 

642 

643 frame = qw.QFrame() 

644 # frame.setFrameShape(qw.QFrame.NoFrame) 

645 # frame.setBackgroundRole(qg.QPalette.Mid) 

646 # frame.setAutoFillBackground(True) 

647 frame.setSizePolicy( 

648 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

649 

650 layout = qw.QGridLayout() 

651 frame.setLayout(layout) 

652 self.main_layout.insertWidget(0, frame) 

653 

654 self.state_editor = StateEditor(self) 

655 

656 layout.addWidget(self.state_editor, 0, 0) 

657 

658 # attach_button = qw.QPushButton('Attach View') 

659 # attach_button.clicked.connect(self.attach) 

660 # layout.addWidget( 

661 # attach_button, 0, 0, alignment=qc.Qt.AlignCenter) 

662 

663 self.vtk_frame_substitute = frame 

664 

665 if not self.gui_state.detached and self.detached_window: # attach 

666 logger.debug('Attaching VTK view.') 

667 self.detached_window.hide() 

668 self.vtk_frame.setParent(self) 

669 if self.vtk_frame_substitute: 

670 self.main_layout.removeWidget(self.vtk_frame_substitute) 

671 self.state_editor.unbind_state() 

672 self.vtk_frame_substitute = None 

673 

674 self.main_layout.insertWidget(0, self.vtk_frame) 

675 self.detached_window = None 

676 self.vtk_widget.setFocus() 

677 

678 def attach(self): 

679 self.gui_state.detached = False 

680 

681 def export_image(self): 

682 

683 caption = 'Export Image' 

684 fn_out, _ = qw.QFileDialog.getSaveFileName( 

685 self, caption, 'image.png', 

686 options=common.qfiledialog_options) 

687 

688 if fn_out: 

689 self.save_image(fn_out) 

690 

691 def save_image(self, path): 

692 

693 original_fixed_size = self.gui_state.fixed_size 

694 if original_fixed_size is None: 

695 self.gui_state.fixed_size = (1920., 1080.) 

696 

697 wif = vtk.vtkWindowToImageFilter() 

698 wif.SetInput(self.renwin) 

699 wif.SetInputBufferTypeToRGBA() 

700 wif.ReadFrontBufferOff() 

701 writer = vtk.vtkPNGWriter() 

702 writer.SetInputConnection(wif.GetOutputPort()) 

703 

704 self.renwin.Render() 

705 wif.Modified() 

706 writer.SetFileName(path) 

707 writer.Write() 

708 

709 self.vtk_widget.setFixedSize( 

710 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

711 

712 self.gui_state.fixed_size = original_fixed_size 

713 

714 def update_render_settings(self, *args): 

715 if self._lighting is None or self._lighting != self.state.lighting: 

716 self.ren.RemoveAllLights() 

717 for li in light.get_lights(self.state.lighting): 

718 self.ren.AddLight(li) 

719 

720 self._lighting = self.state.lighting 

721 

722 if self._background is None \ 

723 or self._background != self.state.background: 

724 

725 self.state.background.vtk_apply(self.ren) 

726 self._background = self.state.background 

727 

728 self.update_view() 

729 

730 def start_animation(self, interpolator, output_path=None): 

731 self._animation = interpolator 

732 if output_path is None: 

733 self._animation_tstart = time.time() 

734 self._animation_iframe = None 

735 else: 

736 self._animation_iframe = 0 

737 self.showFullScreen() 

738 self.update_view() 

739 self.gui_state.panels_visible = False 

740 self.update_view() 

741 

742 self._animation_timer = qc.QTimer(self) 

743 self._animation_timer.timeout.connect(self.next_animation_frame) 

744 self._animation_timer.setInterval(int(round(interpolator.dt * 1000.))) 

745 self._animation_timer.start() 

746 if output_path is not None: 

747 self.vtk_widget.setFixedSize(qc.QSize(1920, 1080)) 

748 # self.vtk_widget.setFixedSize(qc.QSize(960, 540)) 

749 

750 wif = vtk.vtkWindowToImageFilter() 

751 wif.SetInput(self.renwin) 

752 wif.SetInputBufferTypeToRGBA() 

753 wif.SetScale(1, 1) 

754 wif.ReadFrontBufferOff() 

755 writer = vtk.vtkPNGWriter() 

756 temp_path = tempfile.mkdtemp() 

757 self._animation_saver = (wif, writer, temp_path, output_path) 

758 writer.SetInputConnection(wif.GetOutputPort()) 

759 

760 def next_animation_frame(self): 

761 

762 ani = self._animation 

763 if not ani: 

764 return 

765 

766 if self._animation_iframe is not None: 

767 state = ani( 

768 ani.tmin 

769 + self._animation_iframe * ani.dt) 

770 

771 self._animation_iframe += 1 

772 else: 

773 tnow = time.time() 

774 state = ani(min( 

775 ani.tmax, 

776 ani.tmin + (tnow - self._animation_tstart))) 

777 

778 self.set_state(state) 

779 self.renwin.Render() 

780 if self._animation_saver: 

781 wif, writer, temp_path, _ = self._animation_saver 

782 wif.Modified() 

783 fn = os.path.join(temp_path, 'f%09i.png') 

784 writer.SetFileName(fn % self._animation_iframe) 

785 writer.Write() 

786 

787 if self._animation_iframe is not None: 

788 t = self._animation_iframe * ani.dt 

789 else: 

790 t = tnow - self._animation_tstart 

791 

792 if t > ani.tmax - ani.tmin: 

793 self.stop_animation() 

794 

795 def stop_animation(self): 

796 if self._animation_timer: 

797 self._animation_timer.stop() 

798 

799 if self._animation_saver: 

800 self.vtk_widget.setFixedSize( 

801 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

802 

803 wif, writer, temp_path, output_path = self._animation_saver 

804 fn_path = os.path.join(temp_path, 'f%09d.png') 

805 check_call([ 

806 'ffmpeg', '-y', 

807 '-i', fn_path, 

808 '-c:v', 'libx264', 

809 '-preset', 'slow', 

810 '-crf', '17', 

811 '-vf', 'format=yuv420p,fps=%i' % ( 

812 int(round(1.0/self._animation.dt))), 

813 output_path]) 

814 shutil.rmtree(temp_path) 

815 

816 self._animation_saver = None 

817 self._animation_saver 

818 

819 self.showNormal() 

820 self.gui_state.panels_visible = True 

821 

822 self._animation_tstart = None 

823 self._animation_iframe = None 

824 self._animation = None 

825 

826 def set_state(self, state): 

827 self.setUpdatesEnabled(False) 

828 self.state.diff_update(state) 

829 self.setUpdatesEnabled(True) 

830 

831 def periodical(self): 

832 pass 

833 

834 def request_quit(self): 

835 app = common.get_app() 

836 app.myQuit() 

837 

838 def check_vtk_resize(self, *args): 

839 render_window_size = self.renwin.GetSize() 

840 if self._render_window_size != render_window_size: 

841 self._render_window_size = render_window_size 

842 self.resize_event(*render_window_size) 

843 

844 def update_elements(self, path, value): 

845 if self._in_update_elements: 

846 return 

847 

848 self._in_update_elements = True 

849 for estate in self.state.elements: 

850 if estate.element_id not in self._elements: 

851 new_element = estate.create() 

852 logger.debug('Creating "%s".' % type(new_element).__name__) 

853 self._elements[estate.element_id] = new_element 

854 

855 element = self._elements[estate.element_id] 

856 

857 if estate.element_id not in self._elements_active: 

858 logger.debug('Adding "%s".' % type(element).__name__) 

859 element.bind_state(estate) 

860 element.set_parent(self) 

861 self._elements_active[estate.element_id] = element 

862 

863 state_element_ids = [el.element_id for el in self.state.elements] 

864 deactivate = [] 

865 for element_id, element in self._elements_active.items(): 

866 if element_id not in state_element_ids: 

867 logger.debug('Removing "%s".' % type(element).__name__) 

868 element.unset_parent() 

869 deactivate.append(element_id) 

870 

871 for element_id in deactivate: 

872 del self._elements_active[element_id] 

873 

874 self._update_crosshair_bindings() 

875 

876 self._in_update_elements = False 

877 

878 def _update_crosshair_bindings(self): 

879 

880 def get_crosshair_element(): 

881 for element in self.state.elements: 

882 if element.element_id == 'crosshair': 

883 return element 

884 

885 return None 

886 

887 crosshair = get_crosshair_element() 

888 if crosshair is None or crosshair.is_connected: 

889 return 

890 

891 def to_checkbox(state, widget): 

892 widget.blockSignals(True) 

893 widget.setChecked(state.visible) 

894 widget.blockSignals(False) 

895 

896 def to_state(widget, state): 

897 state.visible = widget.isChecked() 

898 

899 cb = self._crosshair_checkbox 

900 vstate.state_bind( 

901 self, crosshair, ['visible'], to_state, 

902 cb, [cb.toggled], to_checkbox) 

903 

904 crosshair.is_connected = True 

905 

906 def add_actor_2d(self, actor): 

907 if actor not in self._actors_2d: 

908 self.ren.AddActor2D(actor) 

909 self._actors_2d.add(actor) 

910 

911 def remove_actor_2d(self, actor): 

912 if actor in self._actors_2d: 

913 self.ren.RemoveActor2D(actor) 

914 self._actors_2d.remove(actor) 

915 

916 def add_actor(self, actor): 

917 if actor not in self._actors: 

918 self.ren.AddActor(actor) 

919 self._actors.add(actor) 

920 

921 def add_actor_list(self, actorlist): 

922 for actor in actorlist: 

923 self.add_actor(actor) 

924 

925 def remove_actor(self, actor): 

926 if actor in self._actors: 

927 self.ren.RemoveActor(actor) 

928 self._actors.remove(actor) 

929 

930 def update_view(self): 

931 self.vtk_widget.update() 

932 

933 def resize_event(self, size_x, size_y): 

934 self.gui_state.size = (size_x, size_y) 

935 

936 def button_event(self, obj, event): 

937 if event == "LeftButtonPressEvent": 

938 self.rotating = True 

939 elif event == "LeftButtonReleaseEvent": 

940 self.rotating = False 

941 

942 def mouse_move_event(self, obj, event): 

943 x0, y0 = self.iren.GetLastEventPosition() 

944 x, y = self.iren.GetEventPosition() 

945 

946 size_x, size_y = self.renwin.GetSize() 

947 center_x = size_x / 2.0 

948 center_y = size_y / 2.0 

949 

950 if self.rotating: 

951 self.do_rotate(x, y, x0, y0, center_x, center_y) 

952 

953 def myWheelEvent(self, event): 

954 

955 angle = event.angleDelta().y() 

956 

957 if angle > 200: 

958 angle = 200 

959 

960 if angle < -200: 

961 angle = -200 

962 

963 self.do_dolly(-angle/100.) 

964 

965 def do_rotate(self, x, y, x0, y0, center_x, center_y): 

966 

967 dx = x0 - x 

968 dy = y0 - y 

969 

970 phi = d2r*(self.state.strike - 90.) 

971 focp = self.gui_state.focal_point 

972 

973 if focp == 'center': 

974 dx, dy = math.cos(phi) * dx + math.sin(phi) * dy, \ 

975 - math.sin(phi) * dx + math.cos(phi) * dy 

976 

977 lat = self.state.lat 

978 lon = self.state.lon 

979 factor = self.state.distance / 10.0 

980 factor_lat = 1.0/(num.cos(lat*d2r) + (0.1 * self.state.distance)) 

981 else: 

982 lat = 90. - self.state.dip 

983 lon = -self.state.strike - 90. 

984 factor = 0.5 

985 factor_lat = 1.0 

986 

987 dlat = dy * factor 

988 dlon = dx * factor * factor_lat 

989 

990 lat = max(min(lat + dlat, 90.), -90.) 

991 lon += dlon 

992 lon = (lon + 180.) % 360. - 180. 

993 

994 if focp == 'center': 

995 self.state.lat = float(lat) 

996 self.state.lon = float(lon) 

997 else: 

998 self.state.dip = float(90. - lat) 

999 self.state.strike = float(-(lon + 90.)) 

1000 

1001 def do_dolly(self, v): 

1002 self.state.distance *= float(1.0 + 0.1*v) 

1003 

1004 def key_down_event(self, obj, event): 

1005 k = obj.GetKeyCode() 

1006 s = obj.GetKeySym() 

1007 if k == 'f' or s == 'Control_L': 

1008 self.gui_state.next_focal_point() 

1009 

1010 elif k == 'r': 

1011 self.reset_strike_dip() 

1012 

1013 elif k == 'p': 

1014 print(self.state) 

1015 

1016 elif k == 'i': 

1017 for elem in self.state.elements: 

1018 if isinstance(elem, elements.IcosphereState): 

1019 elem.visible = not elem.visible 

1020 

1021 elif k == 'c': 

1022 for elem in self.state.elements: 

1023 if isinstance(elem, elements.CoastlinesState): 

1024 elem.visible = not elem.visible 

1025 

1026 elif k == 't': 

1027 if not any( 

1028 isinstance(elem, elements.TopoState) 

1029 for elem in self.state.elements): 

1030 

1031 self.state.elements.append(elements.TopoState()) 

1032 else: 

1033 for elem in self.state.elements: 

1034 if isinstance(elem, elements.TopoState): 

1035 elem.visible = not elem.visible 

1036 

1037 elif k == ' ': 

1038 self.toggle_panel_visibility() 

1039 

1040 def key_up_event(self, obj, event): 

1041 s = obj.GetKeySym() 

1042 if s == 'Control_L': 

1043 self.gui_state.next_focal_point() 

1044 

1045 def _state_bind(self, *args, **kwargs): 

1046 vstate.state_bind(self, self.state, *args, **kwargs) 

1047 

1048 def _gui_state_bind(self, *args, **kwargs): 

1049 vstate.state_bind(self, self.gui_state, *args, **kwargs) 

1050 

1051 def controls_navigation(self): 

1052 frame = qw.QFrame(self) 

1053 frame.setSizePolicy( 

1054 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1055 layout = qw.QGridLayout() 

1056 frame.setLayout(layout) 

1057 

1058 # lat, lon, depth 

1059 

1060 layout.addWidget( 

1061 qw.QLabel('Location'), 0, 0, 1, 2) 

1062 

1063 le = qw.QLineEdit() 

1064 le.setStatusTip( 

1065 'Latitude, Longitude, Depth [km] or city name: ' 

1066 'Focal point location.') 

1067 layout.addWidget(le, 1, 0, 1, 1) 

1068 

1069 def lat_lon_depth_to_lineedit(state, widget): 

1070 sel = str(widget.selectedText()) == str(widget.text()) 

1071 widget.setText('%g, %g, %g' % ( 

1072 state.lat, state.lon, state.depth / km)) 

1073 

1074 if sel: 

1075 widget.selectAll() 

1076 

1077 def lineedit_to_lat_lon_depth(widget, state): 

1078 s = str(widget.text()) 

1079 choices = location_to_choices(s) 

1080 if len(choices) > 0: 

1081 self.state.lat, self.state.lon, self.state.depth = \ 

1082 choices[0].get_lat_lon_depth() 

1083 else: 

1084 raise NoLocationChoices(s) 

1085 

1086 self._state_bind( 

1087 ['lat', 'lon', 'depth'], 

1088 lineedit_to_lat_lon_depth, 

1089 le, [le.editingFinished, le.returnPressed], 

1090 lat_lon_depth_to_lineedit) 

1091 

1092 self.lat_lon_lineedit = le 

1093 

1094 self.lat_lon_lineedit.returnPressed.connect( 

1095 lambda *args: self.lat_lon_lineedit.selectAll()) 

1096 

1097 # focal point 

1098 

1099 cb = qw.QCheckBox('Fix') 

1100 cb.setStatusTip( 

1101 'Fix location. Orbit focal point without pressing %s.' 

1102 % g_modifier_key) 

1103 layout.addWidget(cb, 1, 1, 1, 1) 

1104 

1105 def focal_point_to_checkbox(state, widget): 

1106 widget.blockSignals(True) 

1107 widget.setChecked(self.gui_state.focal_point != 'center') 

1108 widget.blockSignals(False) 

1109 

1110 def checkbox_to_focal_point(widget, state): 

1111 self.gui_state.focal_point = \ 

1112 'target' if widget.isChecked() else 'center' 

1113 

1114 self._gui_state_bind( 

1115 ['focal_point'], checkbox_to_focal_point, 

1116 cb, [cb.toggled], focal_point_to_checkbox) 

1117 

1118 self.focal_point_checkbox = cb 

1119 

1120 self.talkie_connect( 

1121 self.gui_state, 'focal_point', self.update_focal_point) 

1122 

1123 self.update_focal_point() 

1124 

1125 # strike, dip 

1126 

1127 layout.addWidget( 

1128 qw.QLabel('View Plane'), 2, 0, 1, 2) 

1129 

1130 le = qw.QLineEdit() 

1131 le.setStatusTip( 

1132 'Strike, Dip [deg]: View plane orientation, perpendicular to view ' 

1133 'direction.') 

1134 layout.addWidget(le, 3, 0, 1, 1) 

1135 

1136 def strike_dip_to_lineedit(state, widget): 

1137 sel = widget.selectedText() == widget.text() 

1138 widget.setText('%g, %g' % (state.strike, state.dip)) 

1139 if sel: 

1140 widget.selectAll() 

1141 

1142 def lineedit_to_strike_dip(widget, state): 

1143 s = str(widget.text()) 

1144 string_to_strike_dip = { 

1145 'east': (0., 90.), 

1146 'west': (180., 90.), 

1147 'south': (90., 90.), 

1148 'north': (270., 90.), 

1149 'top': (90., 0.), 

1150 'bottom': (90., 180.)} 

1151 

1152 if s in string_to_strike_dip: 

1153 state.strike, state.dip = string_to_strike_dip[s] 

1154 

1155 s = s.replace(',', ' ') 

1156 try: 

1157 state.strike, state.dip = map(float, s.split()) 

1158 except Exception: 

1159 raise ValueError('need two numerical values: <strike>, <dip>') 

1160 

1161 self._state_bind( 

1162 ['strike', 'dip'], lineedit_to_strike_dip, 

1163 le, [le.editingFinished, le.returnPressed], strike_dip_to_lineedit) 

1164 

1165 self.strike_dip_lineedit = le 

1166 self.strike_dip_lineedit.returnPressed.connect( 

1167 lambda *args: self.strike_dip_lineedit.selectAll()) 

1168 

1169 but = qw.QPushButton('Reset') 

1170 but.setStatusTip('Reset to north-up map view.') 

1171 but.clicked.connect(self.reset_strike_dip) 

1172 layout.addWidget(but, 3, 1, 1, 1) 

1173 

1174 # crosshair 

1175 

1176 self._crosshair_checkbox = qw.QCheckBox('Crosshair') 

1177 layout.addWidget(self._crosshair_checkbox, 4, 0, 1, 2) 

1178 

1179 # camera bindings 

1180 self.talkie_connect( 

1181 self.state, 

1182 ['lat', 'lon', 'depth', 'strike', 'dip', 'distance'], 

1183 self.update_camera) 

1184 

1185 self.talkie_connect( 

1186 self.gui_state, 'panels_visible', self.update_panel_visibility) 

1187 

1188 return frame 

1189 

1190 def controls_time(self): 

1191 frame = qw.QFrame(self) 

1192 frame.setSizePolicy( 

1193 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1194 

1195 layout = qw.QGridLayout() 

1196 frame.setLayout(layout) 

1197 

1198 layout.addWidget(qw.QLabel('Min'), 0, 0) 

1199 le_tmin = qw.QLineEdit() 

1200 layout.addWidget(le_tmin, 0, 1) 

1201 

1202 layout.addWidget(qw.QLabel('Max'), 1, 0) 

1203 le_tmax = qw.QLineEdit() 

1204 layout.addWidget(le_tmax, 1, 1) 

1205 

1206 label_tcursor = qw.QLabel() 

1207 

1208 label_tcursor.setSizePolicy( 

1209 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1210 

1211 layout.addWidget(label_tcursor, 2, 1) 

1212 self._label_tcursor = label_tcursor 

1213 

1214 def time_to_lineedit(state, attribute, widget): 

1215 sel = widget.selectedText() == widget.text() \ 

1216 and widget.text() != '' 

1217 

1218 widget.setText( 

1219 common.time_or_none_to_str(getattr(state, attribute))) 

1220 

1221 if sel: 

1222 widget.selectAll() 

1223 

1224 def lineedit_to_time(widget, state, attribute): 

1225 from pyrocko.util import str_to_time_fillup 

1226 

1227 s = str(widget.text()) 

1228 if not s.strip(): 

1229 setattr(state, attribute, None) 

1230 else: 

1231 try: 

1232 setattr(state, attribute, str_to_time_fillup(s)) 

1233 except Exception: 

1234 raise ValueError( 

1235 'Use time format: YYYY-MM-DD HH:MM:SS.FFF') 

1236 

1237 self._state_bind( 

1238 ['tmin'], lineedit_to_time, le_tmin, 

1239 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit, 

1240 attribute='tmin') 

1241 self._state_bind( 

1242 ['tmax'], lineedit_to_time, le_tmax, 

1243 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit, 

1244 attribute='tmax') 

1245 

1246 self.tmin_lineedit = le_tmin 

1247 self.tmax_lineedit = le_tmax 

1248 

1249 range_edit = RangeEdit() 

1250 range_edit.set_data_provider(self) 

1251 range_edit.set_data_name('time') 

1252 

1253 xblock = [False] 

1254 

1255 def range_to_range_edit(state, widget): 

1256 if not xblock[0]: 

1257 widget.blockSignals(True) 

1258 widget.set_focus(state.tduration, state.tposition) 

1259 widget.set_range(state.tmin, state.tmax) 

1260 widget.blockSignals(False) 

1261 

1262 def range_edit_to_range(widget, state): 

1263 xblock[0] = True 

1264 self.state.tduration, self.state.tposition = widget.get_focus() 

1265 self.state.tmin, self.state.tmax = widget.get_range() 

1266 xblock[0] = False 

1267 

1268 self._state_bind( 

1269 ['tmin', 'tmax', 'tduration', 'tposition'], 

1270 range_edit_to_range, 

1271 range_edit, 

1272 [range_edit.rangeChanged, range_edit.focusChanged], 

1273 range_to_range_edit) 

1274 

1275 def handle_tcursor_changed(): 

1276 self.gui_state.tcursor = range_edit.get_tcursor() 

1277 

1278 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1279 

1280 layout.addWidget(range_edit, 3, 0, 1, 2) 

1281 

1282 layout.addWidget(qw.QLabel('Focus'), 4, 0) 

1283 le_focus = qw.QLineEdit() 

1284 layout.addWidget(le_focus, 4, 1) 

1285 

1286 def focus_to_lineedit(state, widget): 

1287 sel = widget.selectedText() == widget.text() \ 

1288 and widget.text() != '' 

1289 

1290 if state.tduration is None: 

1291 widget.setText('') 

1292 else: 

1293 widget.setText('%s, %g' % ( 

1294 guts.str_duration(state.tduration), 

1295 state.tposition)) 

1296 

1297 if sel: 

1298 widget.selectAll() 

1299 

1300 def lineedit_to_focus(widget, state): 

1301 s = str(widget.text()) 

1302 w = [x.strip() for x in s.split(',')] 

1303 try: 

1304 if len(w) == 0 or not w[0]: 

1305 state.tduration = None 

1306 state.tposition = 0.0 

1307 else: 

1308 state.tduration = guts.parse_duration(w[0]) 

1309 if len(w) > 1: 

1310 state.tposition = float(w[1]) 

1311 else: 

1312 state.tposition = 0.0 

1313 

1314 except Exception: 

1315 raise ValueError('need two values: <duration>, <position>') 

1316 

1317 self._state_bind( 

1318 ['tduration', 'tposition'], lineedit_to_focus, le_focus, 

1319 [le_focus.editingFinished, le_focus.returnPressed], 

1320 focus_to_lineedit) 

1321 

1322 label_effective_tmin = qw.QLabel() 

1323 label_effective_tmax = qw.QLabel() 

1324 

1325 label_effective_tmin.setSizePolicy( 

1326 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1327 label_effective_tmax.setSizePolicy( 

1328 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1329 label_effective_tmin.setMinimumSize( 

1330 qg.QFontMetrics(label_effective_tmin.font()).width( 

1331 '0000-00-00 00:00:00.000 '), 0) 

1332 

1333 layout.addWidget(label_effective_tmin, 5, 1) 

1334 layout.addWidget(label_effective_tmax, 6, 1) 

1335 

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

1337 self.talkie_connect( 

1338 self.state, var, self.update_effective_time_labels) 

1339 

1340 self._label_effective_tmin = label_effective_tmin 

1341 self._label_effective_tmax = label_effective_tmax 

1342 

1343 self.talkie_connect( 

1344 self.gui_state, 'tcursor', self.update_tcursor) 

1345 

1346 return frame 

1347 

1348 def controls_appearance(self): 

1349 frame = qw.QFrame(self) 

1350 frame.setSizePolicy( 

1351 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1352 layout = qw.QGridLayout() 

1353 frame.setLayout(layout) 

1354 

1355 layout.addWidget(qw.QLabel('Lighting'), 0, 0) 

1356 

1357 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1358 layout.addWidget(cb, 0, 1) 

1359 vstate.state_bind_combobox(self, self.state, 'lighting', cb) 

1360 

1361 self.talkie_connect( 

1362 self.state, 'lighting', self.update_render_settings) 

1363 

1364 # background 

1365 

1366 layout.addWidget(qw.QLabel('Background'), 1, 0) 

1367 

1368 cb = common.strings_to_combobox( 

1369 ['black', 'white', 'skyblue1 - white']) 

1370 

1371 layout.addWidget(cb, 1, 1) 

1372 vstate.state_bind_combobox_background( 

1373 self, self.state, 'background', cb) 

1374 

1375 self.talkie_connect( 

1376 self.state, 'background', self.update_render_settings) 

1377 

1378 return frame 

1379 

1380 def controls_snapshots(self): 

1381 return snapshots_mod.SnapshotsPanel(self) 

1382 

1383 def update_effective_time_labels(self, *args): 

1384 tmin = self.state.tmin_effective 

1385 tmax = self.state.tmax_effective 

1386 

1387 stmin = common.time_or_none_to_str(tmin) 

1388 stmax = common.time_or_none_to_str(tmax) 

1389 

1390 self._label_effective_tmin.setText(stmin) 

1391 self._label_effective_tmax.setText(stmax) 

1392 

1393 def update_tcursor(self, *args): 

1394 tcursor = self.gui_state.tcursor 

1395 stcursor = common.time_or_none_to_str(tcursor) 

1396 self._label_tcursor.setText(stcursor) 

1397 

1398 def reset_strike_dip(self, *args): 

1399 self.state.strike = 90. 

1400 self.state.dip = 0 

1401 self.gui_state.focal_point = 'center' 

1402 

1403 def get_camera_geometry(self): 

1404 

1405 def rtp2xyz(rtp): 

1406 return geometry.rtp2xyz(rtp[num.newaxis, :])[0] 

1407 

1408 radius = 1.0 - self.state.depth / self.planet_radius 

1409 

1410 cam_rtp = num.array([ 

1411 radius+self.state.distance, 

1412 self.state.lat * d2r + 0.5*num.pi, 

1413 self.state.lon * d2r]) 

1414 up_rtp = cam_rtp + num.array([0., 0.5*num.pi, 0.]) 

1415 cam, up, foc = \ 

1416 rtp2xyz(cam_rtp), rtp2xyz(up_rtp), num.array([0., 0., 0.]) 

1417 

1418 foc_rtp = num.array([ 

1419 radius, 

1420 self.state.lat * d2r + 0.5*num.pi, 

1421 self.state.lon * d2r]) 

1422 

1423 foc = rtp2xyz(foc_rtp) 

1424 

1425 rot_world = pmt.euler_to_matrix( 

1426 -(self.state.lat-90.)*d2r, 

1427 (self.state.lon+90.)*d2r, 

1428 0.0*d2r).T 

1429 

1430 rot_cam = pmt.euler_to_matrix( 

1431 self.state.dip*d2r, -(self.state.strike-90)*d2r, 0.0*d2r).T 

1432 

1433 rot = num.dot(rot_world, num.dot(rot_cam, rot_world.T)) 

1434 

1435 cam = foc + num.dot(rot, cam - foc) 

1436 up = num.dot(rot, up) 

1437 return cam, up, foc 

1438 

1439 def update_camera(self, *args): 

1440 cam, up, foc = self.get_camera_geometry() 

1441 camera = self.ren.GetActiveCamera() 

1442 camera.SetPosition(*cam) 

1443 camera.SetFocalPoint(*foc) 

1444 camera.SetViewUp(*up) 

1445 

1446 planet_horizon = math.sqrt(max(0., num.sum(cam**2) - 1.0)) 

1447 

1448 feature_horizon = math.sqrt(max(0., num.sum(cam**2) - ( 

1449 self.feature_radius_min / self.planet_radius)**2)) 

1450 

1451 # if horizon == 0.0: 

1452 # horizon = 2.0 + self.state.distance 

1453 

1454 # clip_dist = max(min(self.state.distance*5., max( 

1455 # 1.0, num.sqrt(num.sum(cam**2)))), feature_horizon) 

1456 # , math.sqrt(num.sum(cam**2))) 

1457 clip_dist = max(1.0, feature_horizon) # , math.sqrt(num.sum(cam**2))) 

1458 # clip_dist = feature_horizon 

1459 

1460 camera.SetClippingRange(max(clip_dist*0.001, clip_dist-3.0), clip_dist) 

1461 

1462 self.camera_params = ( 

1463 cam, up, foc, planet_horizon, feature_horizon, clip_dist) 

1464 

1465 self.update_view() 

1466 

1467 def add_panel( 

1468 self, name, panel, 

1469 visible=False, 

1470 # volatile=False, 

1471 tabify=True, 

1472 where=qc.Qt.RightDockWidgetArea, 

1473 remove=None, 

1474 title_controls=[]): 

1475 

1476 dockwidget = common.MyDockWidget( 

1477 name, self, title_controls=title_controls) 

1478 

1479 if not visible: 

1480 dockwidget.hide() 

1481 

1482 if not self.gui_state.panels_visible: 

1483 dockwidget.block() 

1484 

1485 dockwidget.setWidget(panel) 

1486 

1487 panel.setParent(dockwidget) 

1488 

1489 dockwidgets = self.findChildren(common.MyDockWidget) 

1490 dws = [x for x in dockwidgets if self.dockWidgetArea(x) == where] 

1491 

1492 self.addDockWidget(where, dockwidget) 

1493 

1494 nwrap = 4 

1495 if dws and len(dws) >= nwrap and tabify: 

1496 self.tabifyDockWidget( 

1497 dws[len(dws) - nwrap + len(dws) % nwrap], dockwidget) 

1498 

1499 mitem = dockwidget.toggleViewAction() 

1500 self._panel_togglers[dockwidget] = mitem 

1501 self.panels_menu.addAction(mitem) 

1502 if visible: 

1503 dockwidget.setVisible(True) 

1504 dockwidget.setFocus() 

1505 dockwidget.raise_() 

1506 

1507 def raise_panel(self, panel): 

1508 dockwidget = panel.parent() 

1509 dockwidget.setVisible(True) 

1510 dockwidget.setFocus() 

1511 dockwidget.raise_() 

1512 

1513 def toggle_panel_visibility(self): 

1514 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1515 

1516 def update_panel_visibility(self, *args): 

1517 self.setUpdatesEnabled(False) 

1518 mbar = self.menuBar() 

1519 dockwidgets = self.findChildren(common.MyDockWidget) 

1520 

1521 mbar.setVisible(self.gui_state.panels_visible) 

1522 for dockwidget in dockwidgets: 

1523 dockwidget.setBlocked(not self.gui_state.panels_visible) 

1524 

1525 self.setUpdatesEnabled(True) 

1526 

1527 def remove_panel(self, panel): 

1528 dockwidget = panel.parent() 

1529 self.removeDockWidget(dockwidget) 

1530 dockwidget.setParent(None) 

1531 self.panels_menu.removeAction(self._panel_togglers[dockwidget]) 

1532 

1533 def register_data_provider(self, provider): 

1534 if provider not in self.data_providers: 

1535 self.data_providers.append(provider) 

1536 

1537 def unregister_data_provider(self, provider): 

1538 if provider in self.data_providers: 

1539 self.data_providers.remove(provider) 

1540 

1541 def iter_data(self, name): 

1542 for provider in self.data_providers: 

1543 for data in provider.iter_data(name): 

1544 yield data 

1545 

1546 def closeEvent(self, event): 

1547 self.attach() 

1548 event.accept() 

1549 self.closing = True 

1550 common.get_app().set_main_window(None) 

1551 

1552 def is_closing(self): 

1553 return self.closing 

1554 

1555 

1556class SparrowApp(qw.QApplication): 

1557 def __init__(self): 

1558 qw.QApplication.__init__(self, ['Sparrow']) 

1559 self.lastWindowClosed.connect(self.myQuit) 

1560 self._main_window = None 

1561 self.setApplicationDisplayName('Sparrow') 

1562 self.setDesktopFileName('Sparrow') 

1563 

1564 def install_sigint_handler(self): 

1565 self._old_signal_handler = signal.signal( 

1566 signal.SIGINT, self.myCloseAllWindows) 

1567 

1568 def uninstall_sigint_handler(self): 

1569 signal.signal(signal.SIGINT, self._old_signal_handler) 

1570 

1571 def myQuit(self, *args): 

1572 self.quit() 

1573 

1574 def myCloseAllWindows(self, *args): 

1575 self.closeAllWindows() 

1576 

1577 def set_main_window(self, win): 

1578 self._main_window = win 

1579 

1580 def get_main_window(self): 

1581 return self._main_window 

1582 

1583 def get_progressbars(self): 

1584 if self._main_window: 

1585 return self._main_window.progressbars 

1586 else: 

1587 return None 

1588 

1589 def status(self, message, duration=None): 

1590 win = self.get_main_window() 

1591 if not win: 

1592 return 

1593 

1594 win.statusBar().showMessage( 

1595 message, int((duration or 0) * 1000)) 

1596 

1597 

1598def main(*args, **kwargs): 

1599 

1600 from pyrocko import util 

1601 from pyrocko.gui import util as gui_util 

1602 util.setup_logging('sparrow', 'info') 

1603 

1604 global win 

1605 

1606 if gui_util.app is None: 

1607 gui_util.app = SparrowApp() 

1608 

1609 # try: 

1610 # from qt_material import apply_stylesheet 

1611 # 

1612 # apply_stylesheet(app, theme='dark_teal.xml') 

1613 # 

1614 # 

1615 # import qdarkgraystyle 

1616 # app.setStyleSheet(qdarkgraystyle.load_stylesheet()) 

1617 # import qdarkstyle 

1618 # 

1619 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 

1620 # 

1621 # 

1622 # except ImportError: 

1623 # logger.info( 

1624 # 'Module qdarkgraystyle not available.\n' 

1625 # 'If wanted, install qdarkstyle with "pip install ' 

1626 # 'qdarkgraystyle".') 

1627 # 

1628 win = SparrowViewer(*args, **kwargs) 

1629 

1630 gui_util.app.install_sigint_handler() 

1631 gui_util.app.exec_() 

1632 gui_util.app.uninstall_sigint_handler() 

1633 

1634 del win 

1635 

1636 gc.collect() 

1637 

1638 del gui_util.app