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.qt_compat import qw, qc, qg 

26# from pyrocko.gui import vtk_util 

27 

28from . import common, light, snapshots as snapshots_mod 

29 

30import vtk 

31import vtk.qt 

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

33 

34from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

35 

36from pyrocko import geometry # noqa 

37from . import state as vstate, elements # noqa 

38 

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

40 

41 

42d2r = num.pi/180. 

43km = 1000. 

44 

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

46 g_modifier_key = '\u2318' 

47else: 

48 g_modifier_key = 'Ctrl' 

49 

50 

51class ZeroFrame(qw.QFrame): 

52 

53 def sizeHint(self): 

54 return qc.QSize(0, 0) 

55 

56 

57class LocationChoice(object): 

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

59 self._name = name 

60 self._lat = lat 

61 self._lon = lon 

62 self._depth = depth 

63 

64 def get_lat_lon_depth(self): 

65 return self._lat, self._lon, self._depth 

66 

67 

68def location_to_choices(s): 

69 choices = [] 

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

71 try: 

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

73 if len(vals) == 3: 

74 vals[2] *= km 

75 

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

77 

78 except ValueError: 

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

80 for c in cities: 

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

82 

83 return choices 

84 

85 

86class NoLocationChoices(Exception): 

87 

88 def __init__(self, s): 

89 self._string = s 

90 

91 def __str__(self): 

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

93 

94 

95class QVTKWidget(QVTKRenderWindowInteractor): 

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

97 QVTKRenderWindowInteractor.__init__(self, *args) 

98 self._viewer = viewer 

99 

100 def wheelEvent(self, event): 

101 return self._viewer.myWheelEvent(event) 

102 

103 def container_resized(self, ev): 

104 self._viewer.update_vtk_widget_size() 

105 

106 

107class DetachedViewer(qw.QMainWindow): 

108 

109 def __init__(self, main_window, vtk_frame): 

110 qw.QMainWindow.__init__(self, main_window) 

111 self.main_window = main_window 

112 self.setWindowTitle('Sparrow View') 

113 vtk_frame.setParent(self) 

114 self.setCentralWidget(vtk_frame) 

115 

116 def closeEvent(self, ev): 

117 ev.ignore() 

118 self.main_window.attach() 

119 

120 

121class CenteringScrollArea(qw.QScrollArea): 

122 def __init__(self): 

123 qw.QScrollArea.__init__(self) 

124 self.setAlignment(qc.Qt.AlignCenter) 

125 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

126 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

127 self.setFrameShape(qw.QFrame.NoFrame) 

128 

129 def resizeEvent(self, ev): 

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

131 self.widget().container_resized(ev) 

132 return retval 

133 

134 def recenter(self): 

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

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

137 

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

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

140 

141 

142class YAMLEditor(qw.QTextEdit): 

143 

144 def __init__(self, parent): 

145 qw.QTextEdit.__init__(self) 

146 self._parent = parent 

147 

148 def event(self, ev): 

149 if isinstance(ev, qg.QKeyEvent) \ 

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

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

152 self._parent.state_changed() 

153 return True 

154 

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

156 

157 

158class StateEditor(qw.QFrame): 

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

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

161 self.listeners = [] 

162 

163 layout = qw.QGridLayout() 

164 

165 self.setLayout(layout) 

166 

167 self.source_editor = YAMLEditor(self) 

168 self.source_editor.setAcceptRichText(False) 

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

170 font = qg.QFont("Monospace") 

171 self.source_editor.setCurrentFont(font) 

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

173 

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

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

176 

177 self.error_display = qw.QTextEdit() 

178 self.error_display.setCurrentFont(font) 

179 self.error_display.setReadOnly(True) 

180 

181 self.error_display.setSizePolicy( 

182 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

183 

184 self.error_display_label.hide() 

185 self.error_display.hide() 

186 

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

188 

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

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

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

192 

193 button = qw.QPushButton('Apply') 

194 button.clicked.connect(self.state_changed) 

195 layout.addWidget(button, 3, 1) 

196 

197 self.viewer = viewer 

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

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

200 self.bind_state() 

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

202 self.destroyed.connect(self.unbind_state) 

203 self.bind_state() 

204 

205 def bind_state(self, *args): 

206 ref = self.viewer.state.add_listener(self.update_state) 

207 self.listeners.append(ref) 

208 self.update_state() 

209 

210 def unbind_state(self): 

211 while self.listeners: 

212 listener = self.listeners.pop() 

213 self.viewer.state.remove_listener(listener) 

214 

215 def update_state(self, *args): 

216 cursor = self.source_editor.textCursor() 

217 

218 cursor_position = cursor.position() 

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

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

221 

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

223 

224 cursor.setPosition(cursor_position) 

225 self.source_editor.setTextCursor(cursor) 

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

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

228 

229 def text_changed_handler(self, *args): 

230 if self.instant_updates.isChecked(): 

231 self.state_changed() 

232 

233 def state_changed(self): 

234 try: 

235 s = self.source_editor.toPlainText() 

236 state = guts.load(string=s) 

237 self.viewer.set_state(state) 

238 self.error_display.setPlainText('') 

239 self.error_display_label.hide() 

240 self.error_display.hide() 

241 

242 except Exception as e: 

243 self.error_display.show() 

244 self.error_display_label.show() 

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

246 

247 

248class SparrowViewer(qw.QMainWindow): 

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

250 qw.QMainWindow.__init__(self) 

251 

252 common.get_app().set_main_window(self) 

253 

254 self.listeners = [] 

255 

256 self.state = vstate.ViewerState() 

257 self.gui_state = vstate.ViewerGuiState() 

258 

259 self.setWindowTitle('Sparrow') 

260 

261 self.setTabPosition( 

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

263 

264 self.planet_radius = cake.earthradius 

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

266 

267 self._panel_togglers = {} 

268 self._actors = set() 

269 self._actors_2d = set() 

270 self._render_window_size = (0, 0) 

271 self._use_depth_peeling = use_depth_peeling 

272 self._in_update_elements = False 

273 

274 mbar = qw.QMenuBar() 

275 self.setMenuBar(mbar) 

276 

277 menu = mbar.addMenu('File') 

278 

279 menu.addAction( 

280 'Export Image...', 

281 self.export_image, 

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

283 qc.Qt.ApplicationShortcut) 

284 

285 menu.addAction( 

286 'Quit', 

287 self.request_quit, 

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

289 qc.Qt.ApplicationShortcut) 

290 

291 menu = mbar.addMenu('View') 

292 menu_sizes = menu.addMenu('Size') 

293 self._add_vtk_widget_size_menu_entries(menu_sizes) 

294 

295 # detached/attached 

296 self.register_state_listener3( 

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

298 

299 action = qw.QAction('Detach') 

300 action.setCheckable(True) 

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

302 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

303 

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

305 menu.addAction(action) 

306 

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

308 

309 menu = mbar.addMenu('Add') 

310 for name, estate in [ 

311 ('Icosphere', elements.IcosphereState( 

312 level=4, 

313 smooth=True, 

314 opacity=0.5, 

315 ambient=0.1)), 

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

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

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

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

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

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

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

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

324 variables=['tmax'], 

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

326 position='top-left')), 

327 ('HUD subtitle', elements.HudState( 

328 template='Awesome')), 

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

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

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

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

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

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

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

336 

337 def wrap_add_element(estate): 

338 def add_element(*args): 

339 new_element = guts.clone(estate) 

340 new_element.element_id = elements.random_id() 

341 self.state.elements.append(new_element) 

342 self.state.sort_elements() 

343 

344 return add_element 

345 

346 mitem = qw.QAction(name, self) 

347 

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

349 

350 menu.addAction(mitem) 

351 

352 self.data_providers = [] 

353 self.elements = {} 

354 

355 self.detached_window = None 

356 

357 self.main_frame = qw.QFrame() 

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

359 

360 self.vtk_frame = CenteringScrollArea() 

361 

362 self.vtk_widget = QVTKWidget(self, self) 

363 self.vtk_frame.setWidget(self.vtk_widget) 

364 

365 self.main_layout = qw.QVBoxLayout() 

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

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

368 

369 pb = Progressbars(self) 

370 self.progressbars = pb 

371 self.main_layout.addWidget(pb) 

372 

373 self.main_frame.setLayout(self.main_layout) 

374 

375 self.vtk_frame_substitute = None 

376 

377 self.add_panel( 

378 'Navigation', 

379 self.controls_navigation(), visible=True, 

380 where=qc.Qt.LeftDockWidgetArea) 

381 

382 self.add_panel( 

383 'Time', 

384 self.controls_time(), visible=True, 

385 where=qc.Qt.LeftDockWidgetArea) 

386 

387 self.add_panel( 

388 'Appearance', 

389 self.controls_appearance(), visible=True, 

390 where=qc.Qt.LeftDockWidgetArea) 

391 

392 snapshots_panel = self.controls_snapshots() 

393 self.add_panel( 

394 'Snapshots', 

395 snapshots_panel, visible=False, 

396 where=qc.Qt.LeftDockWidgetArea) 

397 

398 self.setCentralWidget(self.main_frame) 

399 

400 self.mesh = None 

401 

402 ren = vtk.vtkRenderer() 

403 

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

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

406 # ren.TwoSidedLightingOn() 

407 # ren.SetUseShadows(1) 

408 

409 self._lighting = None 

410 self._background = None 

411 

412 self.ren = ren 

413 self.update_render_settings() 

414 self.update_camera() 

415 

416 renwin = self.vtk_widget.GetRenderWindow() 

417 

418 if self._use_depth_peeling: 

419 renwin.SetAlphaBitPlanes(1) 

420 renwin.SetMultiSamples(0) 

421 

422 ren.SetUseDepthPeeling(1) 

423 ren.SetMaximumNumberOfPeels(100) 

424 ren.SetOcclusionRatio(0.1) 

425 

426 ren.SetUseFXAA(1) 

427 # ren.SetUseHiddenLineRemoval(1) 

428 # ren.SetBackingStore(1) 

429 

430 self.renwin = renwin 

431 

432 # renwin.LineSmoothingOn() 

433 # renwin.PointSmoothingOn() 

434 # renwin.PolygonSmoothingOn() 

435 

436 renwin.AddRenderer(ren) 

437 

438 iren = renwin.GetInteractor() 

439 iren.LightFollowCameraOn() 

440 iren.SetInteractorStyle(None) 

441 

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

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

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

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

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

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

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

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

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

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

452 

453 renwin.Render() 

454 

455 iren.Initialize() 

456 

457 self.iren = iren 

458 

459 self.rotating = False 

460 

461 self._elements = {} 

462 self._elements_active = {} 

463 

464 self.register_state_listener3( 

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

466 

467 self.state.elements.append(elements.IcosphereState( 

468 element_id='icosphere', 

469 level=4, 

470 smooth=True, 

471 opacity=0.5, 

472 ambient=0.1)) 

473 

474 self.state.elements.append(elements.GridState( 

475 element_id='grid')) 

476 self.state.elements.append(elements.CoastlinesState( 

477 element_id='coastlines')) 

478 self.state.elements.append(elements.CrosshairState( 

479 element_id='crosshair')) 

480 

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

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

483 # self.state.elements.append( 

484 # elements.CatalogState( 

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

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

487 

488 if events: 

489 self.state.elements.append( 

490 elements.CatalogState( 

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

492 

493 self.state.sort_elements() 

494 

495 if snapshots: 

496 snapshots_ = [] 

497 for obj in snapshots: 

498 if isinstance(obj, str): 

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

500 else: 

501 snapshots.append(obj) 

502 

503 snapshots_panel.add_snapshots(snapshots_) 

504 self.raise_panel(snapshots_panel) 

505 snapshots_panel.goto_snapshot(1) 

506 

507 self.timer = qc.QTimer(self) 

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

509 self.timer.setInterval(1000) 

510 self.timer.start() 

511 

512 self._animation_saver = None 

513 

514 self.closing = False 

515 self.vtk_widget.setFocus() 

516 

517 self.update_detached() 

518 

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

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

521 

522 self.show() 

523 self.windowHandle().showMaximized() 

524 

525 self.register_state_listener3( 

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

527 

528 self.update_vtk_widget_size() 

529 

530 def _add_vtk_widget_size_menu_entries(self, menu): 

531 

532 group = qw.QActionGroup(menu) 

533 group.setExclusive(True) 

534 

535 def set_variable_size(): 

536 self.gui_state.fixed_size = False 

537 

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

539 variable_size_action.setCheckable(True) 

540 variable_size_action.setActionGroup(group) 

541 variable_size_action.triggered.connect(set_variable_size) 

542 

543 fixed_size_items = [] 

544 for nx, ny, label in [ 

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

546 (426, 240, ''), 

547 (640, 360, ''), 

548 (854, 480, '(FWVGA)'), 

549 (1280, 720, '(HD)'), 

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

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

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

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

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

555 (640, 480, '(VGA)'), 

556 (800, 600, '(SVGA)'), 

557 (None, None, 'Other'), 

558 (512, 512, ''), 

559 (1024, 1024, '')]: 

560 

561 if None in (nx, ny): 

562 menu.addSection(label) 

563 else: 

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

565 action = menu.addAction(name) 

566 action.setCheckable(True) 

567 action.setActionGroup(group) 

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

569 

570 def make_set_fixed_size(nx, ny): 

571 def set_fixed_size(): 

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

573 

574 return set_fixed_size 

575 

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

577 

578 def update_widget(*args): 

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

580 action.blockSignals(True) 

581 action.setChecked( 

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

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

584 action.blockSignals(False) 

585 

586 variable_size_action.blockSignals(True) 

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

588 variable_size_action.blockSignals(False) 

589 

590 update_widget() 

591 self.register_state_listener3( 

592 update_widget, self.gui_state, 'fixed_size') 

593 

594 def update_vtk_widget_size(self, *args): 

595 if self.gui_state.fixed_size: 

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

597 wanted_size = qc.QSize(nx, ny) 

598 else: 

599 wanted_size = qc.QSize( 

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

601 

602 current_size = self.vtk_widget.size() 

603 

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

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

606 

607 self.vtk_widget.setFixedSize(wanted_size) 

608 

609 self.vtk_frame.recenter() 

610 self.check_vtk_resize() 

611 

612 def update_focal_point(self, *args): 

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

614 self.vtk_widget.setStatusTip( 

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

616 'change view plane orientation.' % g_modifier_key) 

617 else: 

618 self.vtk_widget.setStatusTip( 

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

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

621 'reverse sense.' % g_modifier_key) 

622 

623 def update_detached(self, *args): 

624 

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

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

627 

628 self.main_layout.removeWidget(self.vtk_frame) 

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

630 self.detached_window.show() 

631 self.vtk_widget.setFocus() 

632 

633 screens = common.get_app().screens() 

634 if len(screens) > 1: 

635 for screen in screens: 

636 if screen is not self.screen(): 

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

638 # .setScreen() does not work reliably, 

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

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

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

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

643 

644 self.detached_window.windowHandle().showMaximized() 

645 

646 frame = qw.QFrame() 

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

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

649 # frame.setAutoFillBackground(True) 

650 frame.setSizePolicy( 

651 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

652 

653 layout = qw.QGridLayout() 

654 frame.setLayout(layout) 

655 self.main_layout.insertWidget(0, frame) 

656 

657 self.state_editor = StateEditor(self) 

658 

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

660 

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

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

663 # layout.addWidget( 

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

665 

666 self.vtk_frame_substitute = frame 

667 

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

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

670 self.detached_window.hide() 

671 self.vtk_frame.setParent(self) 

672 if self.vtk_frame_substitute: 

673 self.main_layout.removeWidget(self.vtk_frame_substitute) 

674 self.state_editor.unbind_state() 

675 self.vtk_frame_substitute = None 

676 

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

678 self.detached_window = None 

679 self.vtk_widget.setFocus() 

680 

681 def attach(self): 

682 self.gui_state.detached = False 

683 

684 def export_image(self): 

685 

686 caption = 'Export Image' 

687 fn_out, _ = qw.QFileDialog.getSaveFileName( 

688 self, caption, 'image.png', 

689 options=common.qfiledialog_options) 

690 

691 if fn_out: 

692 self.save_image(fn_out) 

693 

694 def save_image(self, path): 

695 

696 original_fixed_size = self.gui_state.fixed_size 

697 if original_fixed_size is None: 

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

699 

700 wif = vtk.vtkWindowToImageFilter() 

701 wif.SetInput(self.renwin) 

702 wif.SetInputBufferTypeToRGBA() 

703 wif.ReadFrontBufferOff() 

704 writer = vtk.vtkPNGWriter() 

705 writer.SetInputConnection(wif.GetOutputPort()) 

706 

707 self.renwin.Render() 

708 wif.Modified() 

709 writer.SetFileName(path) 

710 writer.Write() 

711 

712 self.vtk_widget.setFixedSize( 

713 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

714 

715 self.gui_state.fixed_size = original_fixed_size 

716 

717 def update_render_settings(self, *args): 

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

719 self.ren.RemoveAllLights() 

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

721 self.ren.AddLight(li) 

722 

723 self._lighting = self.state.lighting 

724 

725 if self._background is None \ 

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

727 

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

729 self._background = self.state.background 

730 

731 self.update_view() 

732 

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

734 self._animation = interpolator 

735 if output_path is None: 

736 self._animation_tstart = time.time() 

737 self._animation_iframe = None 

738 else: 

739 self._animation_iframe = 0 

740 self.showFullScreen() 

741 self.update_view() 

742 self.gui_state.panels_visible = False 

743 self.update_view() 

744 

745 self._animation_timer = qc.QTimer(self) 

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

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

748 self._animation_timer.start() 

749 if output_path is not None: 

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

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

752 

753 wif = vtk.vtkWindowToImageFilter() 

754 wif.SetInput(self.renwin) 

755 wif.SetInputBufferTypeToRGBA() 

756 wif.SetScale(1, 1) 

757 wif.ReadFrontBufferOff() 

758 writer = vtk.vtkPNGWriter() 

759 temp_path = tempfile.mkdtemp() 

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

761 writer.SetInputConnection(wif.GetOutputPort()) 

762 

763 def next_animation_frame(self): 

764 

765 ani = self._animation 

766 if not ani: 

767 return 

768 

769 if self._animation_iframe is not None: 

770 state = ani( 

771 ani.tmin 

772 + self._animation_iframe * ani.dt) 

773 

774 self._animation_iframe += 1 

775 else: 

776 tnow = time.time() 

777 state = ani(min( 

778 ani.tmax, 

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

780 

781 self.set_state(state) 

782 self.renwin.Render() 

783 if self._animation_saver: 

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

785 wif.Modified() 

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

787 writer.SetFileName(fn % self._animation_iframe) 

788 writer.Write() 

789 

790 if self._animation_iframe is not None: 

791 t = self._animation_iframe * ani.dt 

792 else: 

793 t = tnow - self._animation_tstart 

794 

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

796 self.stop_animation() 

797 

798 def stop_animation(self): 

799 if self._animation_timer: 

800 self._animation_timer.stop() 

801 

802 if self._animation_saver: 

803 self.vtk_widget.setFixedSize( 

804 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

805 

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

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

808 check_call([ 

809 'ffmpeg', '-y', 

810 '-i', fn_path, 

811 '-c:v', 'libx264', 

812 '-preset', 'slow', 

813 '-crf', '17', 

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

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

816 output_path]) 

817 shutil.rmtree(temp_path) 

818 

819 self._animation_saver = None 

820 self._animation_saver 

821 

822 self.showNormal() 

823 self.gui_state.panels_visible = True 

824 

825 self._animation_tstart = None 

826 self._animation_iframe = None 

827 self._animation = None 

828 

829 def set_state(self, state): 

830 self.setUpdatesEnabled(False) 

831 self.state.diff_update(state) 

832 self.setUpdatesEnabled(True) 

833 

834 def periodical(self): 

835 pass 

836 

837 def request_quit(self): 

838 app = common.get_app() 

839 app.myQuit() 

840 

841 def check_vtk_resize(self, *args): 

842 render_window_size = self.renwin.GetSize() 

843 if self._render_window_size != render_window_size: 

844 self._render_window_size = render_window_size 

845 self.resize_event(*render_window_size) 

846 

847 def update_elements(self, path, value): 

848 if self._in_update_elements: 

849 return 

850 

851 self._in_update_elements = True 

852 for estate in self.state.elements: 

853 if estate.element_id not in self._elements: 

854 new_element = estate.create() 

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

856 self._elements[estate.element_id] = new_element 

857 

858 element = self._elements[estate.element_id] 

859 

860 if estate.element_id not in self._elements_active: 

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

862 element.bind_state(estate) 

863 element.set_parent(self) 

864 self._elements_active[estate.element_id] = element 

865 

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

867 deactivate = [] 

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

869 if element_id not in state_element_ids: 

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

871 element.unset_parent() 

872 deactivate.append(element_id) 

873 

874 for element_id in deactivate: 

875 del self._elements_active[element_id] 

876 

877 self._update_crosshair_bindings() 

878 

879 self._in_update_elements = False 

880 

881 def _update_crosshair_bindings(self): 

882 

883 def get_crosshair_element(): 

884 for element in self.state.elements: 

885 if element.element_id == 'crosshair': 

886 return element 

887 

888 return None 

889 

890 crosshair = get_crosshair_element() 

891 if crosshair is None or crosshair.is_connected: 

892 return 

893 

894 def to_checkbox(state, widget): 

895 widget.blockSignals(True) 

896 widget.setChecked(state.visible) 

897 widget.blockSignals(False) 

898 

899 def to_state(widget, state): 

900 state.visible = widget.isChecked() 

901 

902 cb = self._crosshair_checkbox 

903 vstate.state_bind( 

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

905 cb, [cb.toggled], to_checkbox) 

906 

907 crosshair.is_connected = True 

908 

909 def add_actor_2d(self, actor): 

910 if actor not in self._actors_2d: 

911 self.ren.AddActor2D(actor) 

912 self._actors_2d.add(actor) 

913 

914 def remove_actor_2d(self, actor): 

915 if actor in self._actors_2d: 

916 self.ren.RemoveActor2D(actor) 

917 self._actors_2d.remove(actor) 

918 

919 def add_actor(self, actor): 

920 if actor not in self._actors: 

921 self.ren.AddActor(actor) 

922 self._actors.add(actor) 

923 

924 def add_actor_list(self, actorlist): 

925 for actor in actorlist: 

926 self.add_actor(actor) 

927 

928 def remove_actor(self, actor): 

929 if actor in self._actors: 

930 self.ren.RemoveActor(actor) 

931 self._actors.remove(actor) 

932 

933 def update_view(self): 

934 self.vtk_widget.update() 

935 

936 def resize_event(self, size_x, size_y): 

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

938 

939 def button_event(self, obj, event): 

940 if event == "LeftButtonPressEvent": 

941 self.rotating = True 

942 elif event == "LeftButtonReleaseEvent": 

943 self.rotating = False 

944 

945 def mouse_move_event(self, obj, event): 

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

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

948 

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

950 center_x = size_x / 2.0 

951 center_y = size_y / 2.0 

952 

953 if self.rotating: 

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

955 

956 def myWheelEvent(self, event): 

957 

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

959 

960 if angle > 200: 

961 angle = 200 

962 

963 if angle < -200: 

964 angle = -200 

965 

966 self.do_dolly(-angle/100.) 

967 

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

969 

970 dx = x0 - x 

971 dy = y0 - y 

972 

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

974 focp = self.gui_state.focal_point 

975 

976 if focp == 'center': 

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

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

979 

980 lat = self.state.lat 

981 lon = self.state.lon 

982 factor = self.state.distance / 10.0 

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

984 else: 

985 lat = 90. - self.state.dip 

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

987 factor = 0.5 

988 factor_lat = 1.0 

989 

990 dlat = dy * factor 

991 dlon = dx * factor * factor_lat 

992 

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

994 lon += dlon 

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

996 

997 if focp == 'center': 

998 self.state.lat = float(lat) 

999 self.state.lon = float(lon) 

1000 else: 

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

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

1003 

1004 def do_dolly(self, v): 

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

1006 

1007 def key_down_event(self, obj, event): 

1008 k = obj.GetKeyCode() 

1009 s = obj.GetKeySym() 

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

1011 self.gui_state.next_focal_point() 

1012 

1013 elif k == 'r': 

1014 self.reset_strike_dip() 

1015 

1016 elif k == 'p': 

1017 print(self.state) 

1018 

1019 elif k == 'i': 

1020 for elem in self.state.elements: 

1021 if isinstance(elem, elements.IcosphereState): 

1022 elem.visible = not elem.visible 

1023 

1024 elif k == 'c': 

1025 for elem in self.state.elements: 

1026 if isinstance(elem, elements.CoastlinesState): 

1027 elem.visible = not elem.visible 

1028 

1029 elif k == 't': 

1030 if not any( 

1031 isinstance(elem, elements.TopoState) 

1032 for elem in self.state.elements): 

1033 

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

1035 else: 

1036 for elem in self.state.elements: 

1037 if isinstance(elem, elements.TopoState): 

1038 elem.visible = not elem.visible 

1039 

1040 elif k == ' ': 

1041 self.toggle_panel_visibility() 

1042 

1043 def key_up_event(self, obj, event): 

1044 s = obj.GetKeySym() 

1045 if s == 'Control_L': 

1046 self.gui_state.next_focal_point() 

1047 

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

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

1050 

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

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

1053 

1054 def controls_navigation(self): 

1055 frame = qw.QFrame(self) 

1056 frame.setSizePolicy( 

1057 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1058 layout = qw.QGridLayout() 

1059 frame.setLayout(layout) 

1060 

1061 # lat, lon, depth 

1062 

1063 layout.addWidget( 

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

1065 

1066 le = qw.QLineEdit() 

1067 le.setStatusTip( 

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

1069 'Focal point location.') 

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

1071 

1072 def lat_lon_depth_to_lineedit(state, widget): 

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

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

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

1076 

1077 if sel: 

1078 widget.selectAll() 

1079 

1080 def lineedit_to_lat_lon_depth(widget, state): 

1081 s = str(widget.text()) 

1082 choices = location_to_choices(s) 

1083 if len(choices) > 0: 

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

1085 choices[0].get_lat_lon_depth() 

1086 else: 

1087 raise NoLocationChoices(s) 

1088 

1089 self._state_bind( 

1090 ['lat', 'lon', 'depth'], 

1091 lineedit_to_lat_lon_depth, 

1092 le, [le.editingFinished, le.returnPressed], 

1093 lat_lon_depth_to_lineedit) 

1094 

1095 self.lat_lon_lineedit = le 

1096 

1097 self.lat_lon_lineedit.returnPressed.connect( 

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

1099 

1100 # focal point 

1101 

1102 cb = qw.QCheckBox('Fix') 

1103 cb.setStatusTip( 

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

1105 % g_modifier_key) 

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

1107 

1108 def focal_point_to_checkbox(state, widget): 

1109 widget.blockSignals(True) 

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

1111 widget.blockSignals(False) 

1112 

1113 def checkbox_to_focal_point(widget, state): 

1114 self.gui_state.focal_point = \ 

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

1116 

1117 self._gui_state_bind( 

1118 ['focal_point'], checkbox_to_focal_point, 

1119 cb, [cb.toggled], focal_point_to_checkbox) 

1120 

1121 self.focal_point_checkbox = cb 

1122 

1123 self.register_state_listener3( 

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

1125 

1126 self.update_focal_point() 

1127 

1128 # strike, dip 

1129 

1130 layout.addWidget( 

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

1132 

1133 le = qw.QLineEdit() 

1134 le.setStatusTip( 

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

1136 'direction.') 

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

1138 

1139 def strike_dip_to_lineedit(state, widget): 

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

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

1142 if sel: 

1143 widget.selectAll() 

1144 

1145 def lineedit_to_strike_dip(widget, state): 

1146 s = str(widget.text()) 

1147 string_to_strike_dip = { 

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

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

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

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

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

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

1154 

1155 if s in string_to_strike_dip: 

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

1157 

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

1159 try: 

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

1161 except Exception: 

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

1163 

1164 self._state_bind( 

1165 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1167 

1168 self.strike_dip_lineedit = le 

1169 self.strike_dip_lineedit.returnPressed.connect( 

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

1171 

1172 but = qw.QPushButton('Reset') 

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

1174 but.clicked.connect(self.reset_strike_dip) 

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

1176 

1177 # crosshair 

1178 

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

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

1181 

1182 # camera bindings 

1183 for var in ['lat', 'lon', 'depth', 'strike', 'dip', 'distance']: 

1184 self.register_state_listener3(self.update_camera, self.state, var) 

1185 

1186 self.register_state_listener3( 

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

1188 

1189 return frame 

1190 

1191 def controls_time(self): 

1192 frame = qw.QFrame(self) 

1193 frame.setSizePolicy( 

1194 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1195 

1196 layout = qw.QGridLayout() 

1197 frame.setLayout(layout) 

1198 

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

1200 le_tmin = qw.QLineEdit() 

1201 layout.addWidget(le_tmin, 0, 1) 

1202 

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

1204 le_tmax = qw.QLineEdit() 

1205 layout.addWidget(le_tmax, 1, 1) 

1206 

1207 label_tcursor = qw.QLabel() 

1208 

1209 label_tcursor.setSizePolicy( 

1210 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1211 

1212 layout.addWidget(label_tcursor, 2, 1) 

1213 self._label_tcursor = label_tcursor 

1214 

1215 def time_to_lineedit(state, attribute, widget): 

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

1217 and widget.text() != '' 

1218 

1219 widget.setText( 

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

1221 

1222 if sel: 

1223 widget.selectAll() 

1224 

1225 def lineedit_to_time(widget, state, attribute): 

1226 from pyrocko.util import str_to_time_fillup 

1227 

1228 s = str(widget.text()) 

1229 if not s.strip(): 

1230 setattr(state, attribute, None) 

1231 else: 

1232 try: 

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

1234 except Exception: 

1235 raise ValueError( 

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

1237 

1238 self._state_bind( 

1239 ['tmin'], lineedit_to_time, le_tmin, 

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

1241 attribute='tmin') 

1242 self._state_bind( 

1243 ['tmax'], lineedit_to_time, le_tmax, 

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

1245 attribute='tmax') 

1246 

1247 self.tmin_lineedit = le_tmin 

1248 self.tmax_lineedit = le_tmax 

1249 

1250 range_edit = RangeEdit() 

1251 range_edit.set_data_provider(self) 

1252 range_edit.set_data_name('time') 

1253 

1254 xblock = [False] 

1255 

1256 def range_to_range_edit(state, widget): 

1257 if not xblock[0]: 

1258 widget.blockSignals(True) 

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

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

1261 widget.blockSignals(False) 

1262 

1263 def range_edit_to_range(widget, state): 

1264 xblock[0] = True 

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

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

1267 xblock[0] = False 

1268 

1269 self._state_bind( 

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

1271 range_edit_to_range, 

1272 range_edit, 

1273 [range_edit.rangeChanged, range_edit.focusChanged], 

1274 range_to_range_edit) 

1275 

1276 def handle_tcursor_changed(): 

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

1278 

1279 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1280 

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

1282 

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

1284 le_focus = qw.QLineEdit() 

1285 layout.addWidget(le_focus, 4, 1) 

1286 

1287 def focus_to_lineedit(state, widget): 

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

1289 and widget.text() != '' 

1290 

1291 if state.tduration is None: 

1292 widget.setText('') 

1293 else: 

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

1295 guts.str_duration(state.tduration), 

1296 state.tposition)) 

1297 

1298 if sel: 

1299 widget.selectAll() 

1300 

1301 def lineedit_to_focus(widget, state): 

1302 s = str(widget.text()) 

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

1304 try: 

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

1306 state.tduration = None 

1307 state.tposition = 0.0 

1308 else: 

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

1310 if len(w) > 1: 

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

1312 else: 

1313 state.tposition = 0.0 

1314 

1315 except Exception: 

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

1317 

1318 self._state_bind( 

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

1320 [le_focus.editingFinished, le_focus.returnPressed], 

1321 focus_to_lineedit) 

1322 

1323 label_effective_tmin = qw.QLabel() 

1324 label_effective_tmax = qw.QLabel() 

1325 

1326 label_effective_tmin.setSizePolicy( 

1327 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1328 label_effective_tmax.setSizePolicy( 

1329 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1330 label_effective_tmin.setMinimumSize( 

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

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

1333 

1334 layout.addWidget(label_effective_tmin, 5, 1) 

1335 layout.addWidget(label_effective_tmax, 6, 1) 

1336 

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

1338 self.register_state_listener3( 

1339 self.update_effective_time_labels, self.state, var) 

1340 

1341 self._label_effective_tmin = label_effective_tmin 

1342 self._label_effective_tmax = label_effective_tmax 

1343 

1344 self.register_state_listener3( 

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

1346 

1347 return frame 

1348 

1349 def controls_appearance(self): 

1350 frame = qw.QFrame(self) 

1351 frame.setSizePolicy( 

1352 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1353 layout = qw.QGridLayout() 

1354 frame.setLayout(layout) 

1355 

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

1357 

1358 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1359 layout.addWidget(cb, 0, 1) 

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

1361 

1362 self.register_state_listener3( 

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

1364 

1365 # background 

1366 

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

1368 

1369 cb = common.strings_to_combobox( 

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

1371 

1372 layout.addWidget(cb, 1, 1) 

1373 vstate.state_bind_combobox_background( 

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

1375 

1376 self.register_state_listener3( 

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

1378 

1379 return frame 

1380 

1381 def controls_snapshots(self): 

1382 return snapshots_mod.SnapshotsPanel(self) 

1383 

1384 def update_effective_time_labels(self, *args): 

1385 tmin = self.state.tmin_effective 

1386 tmax = self.state.tmax_effective 

1387 

1388 stmin = common.time_or_none_to_str(tmin) 

1389 stmax = common.time_or_none_to_str(tmax) 

1390 

1391 self._label_effective_tmin.setText(stmin) 

1392 self._label_effective_tmax.setText(stmax) 

1393 

1394 def update_tcursor(self, *args): 

1395 tcursor = self.gui_state.tcursor 

1396 stcursor = common.time_or_none_to_str(tcursor) 

1397 self._label_tcursor.setText(stcursor) 

1398 

1399 def reset_strike_dip(self, *args): 

1400 self.state.strike = 90. 

1401 self.state.dip = 0 

1402 self.gui_state.focal_point = 'center' 

1403 

1404 def register_state_listener(self, listener): 

1405 self.listeners.append(listener) # keep listeners alive 

1406 

1407 def register_state_listener3(self, listener, state, path): 

1408 self.register_state_listener(state.add_listener(listener, path)) 

1409 

1410 def get_camera_geometry(self): 

1411 

1412 def rtp2xyz(rtp): 

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

1414 

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

1416 

1417 cam_rtp = num.array([ 

1418 radius+self.state.distance, 

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

1420 self.state.lon * d2r]) 

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

1422 cam, up, foc = \ 

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

1424 

1425 foc_rtp = num.array([ 

1426 radius, 

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

1428 self.state.lon * d2r]) 

1429 

1430 foc = rtp2xyz(foc_rtp) 

1431 

1432 rot_world = pmt.euler_to_matrix( 

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

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

1435 0.0*d2r).T 

1436 

1437 rot_cam = pmt.euler_to_matrix( 

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

1439 

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

1441 

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

1443 up = num.dot(rot, up) 

1444 return cam, up, foc 

1445 

1446 def update_camera(self, *args): 

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

1448 camera = self.ren.GetActiveCamera() 

1449 camera.SetPosition(*cam) 

1450 camera.SetFocalPoint(*foc) 

1451 camera.SetViewUp(*up) 

1452 

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

1454 

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

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

1457 

1458 # if horizon == 0.0: 

1459 # horizon = 2.0 + self.state.distance 

1460 

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

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

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

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

1465 # clip_dist = feature_horizon 

1466 

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

1468 

1469 self.camera_params = ( 

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

1471 

1472 self.update_view() 

1473 

1474 def add_panel( 

1475 self, name, panel, 

1476 visible=False, 

1477 # volatile=False, 

1478 tabify=True, 

1479 where=qc.Qt.RightDockWidgetArea, 

1480 remove=None, 

1481 title_controls=[]): 

1482 

1483 dockwidget = common.MyDockWidget( 

1484 name, self, title_controls=title_controls) 

1485 

1486 if not visible: 

1487 dockwidget.hide() 

1488 

1489 if not self.gui_state.panels_visible: 

1490 dockwidget.block() 

1491 

1492 dockwidget.setWidget(panel) 

1493 

1494 panel.setParent(dockwidget) 

1495 

1496 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1498 

1499 self.addDockWidget(where, dockwidget) 

1500 

1501 nwrap = 4 

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

1503 self.tabifyDockWidget( 

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

1505 

1506 mitem = dockwidget.toggleViewAction() 

1507 self._panel_togglers[dockwidget] = mitem 

1508 self.panels_menu.addAction(mitem) 

1509 if visible: 

1510 dockwidget.setVisible(True) 

1511 dockwidget.setFocus() 

1512 dockwidget.raise_() 

1513 

1514 def raise_panel(self, panel): 

1515 dockwidget = panel.parent() 

1516 dockwidget.setVisible(True) 

1517 dockwidget.setFocus() 

1518 dockwidget.raise_() 

1519 

1520 def toggle_panel_visibility(self): 

1521 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1522 

1523 def update_panel_visibility(self, *args): 

1524 self.setUpdatesEnabled(False) 

1525 mbar = self.menuBar() 

1526 dockwidgets = self.findChildren(common.MyDockWidget) 

1527 

1528 mbar.setVisible(self.gui_state.panels_visible) 

1529 for dockwidget in dockwidgets: 

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

1531 

1532 self.setUpdatesEnabled(True) 

1533 

1534 def remove_panel(self, panel): 

1535 dockwidget = panel.parent() 

1536 self.removeDockWidget(dockwidget) 

1537 dockwidget.setParent(None) 

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

1539 

1540 def register_data_provider(self, provider): 

1541 if provider not in self.data_providers: 

1542 self.data_providers.append(provider) 

1543 

1544 def unregister_data_provider(self, provider): 

1545 if provider in self.data_providers: 

1546 self.data_providers.remove(provider) 

1547 

1548 def iter_data(self, name): 

1549 for provider in self.data_providers: 

1550 for data in provider.iter_data(name): 

1551 yield data 

1552 

1553 def closeEvent(self, event): 

1554 self.attach() 

1555 event.accept() 

1556 self.closing = True 

1557 common.get_app().set_main_window(None) 

1558 

1559 def is_closing(self): 

1560 return self.closing 

1561 

1562 

1563class SparrowApp(qw.QApplication): 

1564 def __init__(self): 

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

1566 self.lastWindowClosed.connect(self.myQuit) 

1567 self._main_window = None 

1568 self.setApplicationDisplayName('Sparrow') 

1569 self.setDesktopFileName('Sparrow') 

1570 

1571 def install_sigint_handler(self): 

1572 self._old_signal_handler = signal.signal( 

1573 signal.SIGINT, self.myCloseAllWindows) 

1574 

1575 def uninstall_sigint_handler(self): 

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

1577 

1578 def myQuit(self, *args): 

1579 self.quit() 

1580 

1581 def myCloseAllWindows(self, *args): 

1582 self.closeAllWindows() 

1583 

1584 def set_main_window(self, win): 

1585 self._main_window = win 

1586 

1587 def get_main_window(self): 

1588 return self._main_window 

1589 

1590 def get_progressbars(self): 

1591 if self._main_window: 

1592 return self._main_window.progressbars 

1593 else: 

1594 return None 

1595 

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

1597 win = self.get_main_window() 

1598 if not win: 

1599 return 

1600 

1601 win.statusBar().showMessage( 

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

1603 

1604 

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

1606 

1607 from pyrocko import util 

1608 from pyrocko.gui import util as gui_util 

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

1610 

1611 global win 

1612 

1613 if gui_util.app is None: 

1614 gui_util.app = SparrowApp() 

1615 

1616 # try: 

1617 # from qt_material import apply_stylesheet 

1618 # 

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

1620 # 

1621 # 

1622 # import qdarkgraystyle 

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

1624 # import qdarkstyle 

1625 # 

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

1627 # 

1628 # 

1629 # except ImportError: 

1630 # logger.info( 

1631 # 'Module qdarkgraystyle not available.\n' 

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

1633 # 'qdarkgraystyle".') 

1634 # 

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

1636 

1637 gui_util.app.install_sigint_handler() 

1638 gui_util.app.exec_() 

1639 gui_util.app.uninstall_sigint_handler() 

1640 

1641 del win 

1642 

1643 gc.collect() 

1644 

1645 del gui_util.app