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 collections import defaultdict 

16from subprocess import check_call 

17 

18import numpy as num 

19 

20from pyrocko import cake 

21from pyrocko import guts 

22from pyrocko import geonames 

23from pyrocko import moment_tensor as pmt 

24 

25from pyrocko.gui.util import Progressbars, RangeEdit 

26from pyrocko.gui.talkie import TalkieConnectionOwner 

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

28# from pyrocko.gui import vtk_util 

29 

30from . import common, light, snapshots as snapshots_mod 

31 

32import vtk 

33import vtk.qt 

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

35 

36from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

37 

38from pyrocko import geometry # noqa 

39from . import state as vstate, elements # noqa 

40 

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

42 

43 

44d2r = num.pi/180. 

45km = 1000. 

46 

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

48 g_modifier_key = '\u2318' 

49else: 

50 g_modifier_key = 'Ctrl' 

51 

52 

53class ZeroFrame(qw.QFrame): 

54 

55 def sizeHint(self): 

56 return qc.QSize(0, 0) 

57 

58 

59class LocationChoice(object): 

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

61 self._name = name 

62 self._lat = lat 

63 self._lon = lon 

64 self._depth = depth 

65 

66 def get_lat_lon_depth(self): 

67 return self._lat, self._lon, self._depth 

68 

69 

70def location_to_choices(s): 

71 choices = [] 

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

73 try: 

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

75 if len(vals) == 3: 

76 vals[2] *= km 

77 

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

79 

80 except ValueError: 

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

82 for c in cities: 

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

84 

85 return choices 

86 

87 

88class NoLocationChoices(Exception): 

89 

90 def __init__(self, s): 

91 self._string = s 

92 

93 def __str__(self): 

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

95 

96 

97class QVTKWidget(QVTKRenderWindowInteractor): 

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

99 QVTKRenderWindowInteractor.__init__(self, *args) 

100 self._viewer = viewer 

101 

102 def wheelEvent(self, event): 

103 return self._viewer.myWheelEvent(event) 

104 

105 def container_resized(self, ev): 

106 self._viewer.update_vtk_widget_size() 

107 

108 

109class DetachedViewer(qw.QMainWindow): 

110 

111 def __init__(self, main_window, vtk_frame): 

112 qw.QMainWindow.__init__(self, main_window) 

113 self.main_window = main_window 

114 self.setWindowTitle('Sparrow View') 

115 vtk_frame.setParent(self) 

116 self.setCentralWidget(vtk_frame) 

117 

118 def closeEvent(self, ev): 

119 ev.ignore() 

120 self.main_window.attach() 

121 

122 

123class CenteringScrollArea(qw.QScrollArea): 

124 def __init__(self): 

125 qw.QScrollArea.__init__(self) 

126 self.setAlignment(qc.Qt.AlignCenter) 

127 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

128 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

129 self.setFrameShape(qw.QFrame.NoFrame) 

130 

131 def resizeEvent(self, ev): 

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

133 self.widget().container_resized(ev) 

134 return retval 

135 

136 def recenter(self): 

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

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

139 

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

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

142 

143 

144class YAMLEditor(qw.QTextEdit): 

145 

146 def __init__(self, parent): 

147 qw.QTextEdit.__init__(self) 

148 self._parent = parent 

149 

150 def event(self, ev): 

151 if isinstance(ev, qg.QKeyEvent) \ 

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

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

154 self._parent.state_changed() 

155 return True 

156 

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

158 

159 

160class StateEditor(qw.QFrame, TalkieConnectionOwner): 

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

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

163 TalkieConnectionOwner.__init__(self) 

164 

165 layout = qw.QGridLayout() 

166 

167 self.setLayout(layout) 

168 

169 self.source_editor = YAMLEditor(self) 

170 self.source_editor.setAcceptRichText(False) 

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

172 font = qg.QFont("Monospace") 

173 self.source_editor.setCurrentFont(font) 

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

175 

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

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

178 

179 self.error_display = qw.QTextEdit() 

180 self.error_display.setCurrentFont(font) 

181 self.error_display.setReadOnly(True) 

182 

183 self.error_display.setSizePolicy( 

184 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

185 

186 self.error_display_label.hide() 

187 self.error_display.hide() 

188 

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

190 

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

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

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

194 

195 button = qw.QPushButton('Apply') 

196 button.clicked.connect(self.state_changed) 

197 layout.addWidget(button, 3, 1) 

198 

199 self.viewer = viewer 

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

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

202 self.bind_state() 

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

204 self.destroyed.connect(self.unbind_state) 

205 self.bind_state() 

206 

207 def bind_state(self, *args): 

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

209 self.update_state() 

210 

211 def unbind_state(self): 

212 self.talkie_disconnect_all() 

213 

214 def update_state(self, *args): 

215 cursor = self.source_editor.textCursor() 

216 

217 cursor_position = cursor.position() 

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

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

220 

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

222 

223 cursor.setPosition(cursor_position) 

224 self.source_editor.setTextCursor(cursor) 

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

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

227 

228 def text_changed_handler(self, *args): 

229 if self.instant_updates.isChecked(): 

230 self.state_changed() 

231 

232 def state_changed(self): 

233 try: 

234 s = self.source_editor.toPlainText() 

235 state = guts.load(string=s) 

236 self.viewer.set_state(state) 

237 self.error_display.setPlainText('') 

238 self.error_display_label.hide() 

239 self.error_display.hide() 

240 

241 except Exception as e: 

242 self.error_display.show() 

243 self.error_display_label.show() 

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

245 

246 

247class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

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

249 qw.QMainWindow.__init__(self) 

250 TalkieConnectionOwner.__init__(self) 

251 

252 common.get_app().set_main_window(self) 

253 

254 self.state = vstate.ViewerState() 

255 self.gui_state = vstate.ViewerGuiState() 

256 

257 self.setWindowTitle('Sparrow') 

258 

259 self.setTabPosition( 

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

261 

262 self.planet_radius = cake.earthradius 

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

264 

265 self._panel_togglers = {} 

266 self._actors = set() 

267 self._actors_2d = set() 

268 self._render_window_size = (0, 0) 

269 self._use_depth_peeling = use_depth_peeling 

270 self._in_update_elements = False 

271 self._update_elements_enabled = True 

272 

273 mbar = qw.QMenuBar() 

274 self.setMenuBar(mbar) 

275 

276 menu = mbar.addMenu('File') 

277 

278 menu.addAction( 

279 'Export Image...', 

280 self.export_image, 

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

282 qc.Qt.ApplicationShortcut) 

283 

284 menu.addAction( 

285 'Quit', 

286 self.request_quit, 

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

288 qc.Qt.ApplicationShortcut) 

289 

290 menu = mbar.addMenu('View') 

291 menu_sizes = menu.addMenu('Size') 

292 self._add_vtk_widget_size_menu_entries(menu_sizes) 

293 

294 # detached/attached 

295 self.talkie_connect( 

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

297 

298 action = qw.QAction('Detach') 

299 action.setCheckable(True) 

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

301 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

302 

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

304 menu.addAction(action) 

305 

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

307 

308 menu = mbar.addMenu('Add') 

309 for name, estate in [ 

310 ('Icosphere', elements.IcosphereState( 

311 level=4, 

312 smooth=True, 

313 opacity=0.5, 

314 ambient=0.1)), 

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

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

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

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

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

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

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

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

323 variables=['tmax'], 

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

325 position='top-left')), 

326 ('HUD subtitle', elements.HudState( 

327 template='Awesome')), 

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

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

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

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

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

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

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

335 

336 def wrap_add_element(estate): 

337 def add_element(*args): 

338 new_element = guts.clone(estate) 

339 new_element.element_id = elements.random_id() 

340 self.state.elements.append(new_element) 

341 self.state.sort_elements() 

342 

343 return add_element 

344 

345 mitem = qw.QAction(name, self) 

346 

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

348 

349 menu.addAction(mitem) 

350 

351 self.data_providers = [] 

352 self.elements = {} 

353 

354 self.detached_window = None 

355 

356 self.main_frame = qw.QFrame() 

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

358 

359 self.vtk_frame = CenteringScrollArea() 

360 

361 self.vtk_widget = QVTKWidget(self, self) 

362 self.vtk_frame.setWidget(self.vtk_widget) 

363 

364 self.main_layout = qw.QVBoxLayout() 

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

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

367 

368 pb = Progressbars(self) 

369 self.progressbars = pb 

370 self.main_layout.addWidget(pb) 

371 

372 self.main_frame.setLayout(self.main_layout) 

373 

374 self.vtk_frame_substitute = None 

375 

376 self.add_panel( 

377 'Navigation', 

378 self.controls_navigation(), visible=True, 

379 where=qc.Qt.LeftDockWidgetArea) 

380 

381 self.add_panel( 

382 'Time', 

383 self.controls_time(), visible=True, 

384 where=qc.Qt.LeftDockWidgetArea) 

385 

386 self.add_panel( 

387 'Appearance', 

388 self.controls_appearance(), visible=True, 

389 where=qc.Qt.LeftDockWidgetArea) 

390 

391 snapshots_panel = self.controls_snapshots() 

392 self.add_panel( 

393 'Snapshots', 

394 snapshots_panel, visible=False, 

395 where=qc.Qt.LeftDockWidgetArea) 

396 

397 self.setCentralWidget(self.main_frame) 

398 

399 self.mesh = None 

400 

401 ren = vtk.vtkRenderer() 

402 

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

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

405 # ren.TwoSidedLightingOn() 

406 # ren.SetUseShadows(1) 

407 

408 self._lighting = None 

409 self._background = None 

410 

411 self.ren = ren 

412 self.update_render_settings() 

413 self.update_camera() 

414 

415 renwin = self.vtk_widget.GetRenderWindow() 

416 

417 if self._use_depth_peeling: 

418 renwin.SetAlphaBitPlanes(1) 

419 renwin.SetMultiSamples(0) 

420 

421 ren.SetUseDepthPeeling(1) 

422 ren.SetMaximumNumberOfPeels(100) 

423 ren.SetOcclusionRatio(0.1) 

424 

425 ren.SetUseFXAA(1) 

426 # ren.SetUseHiddenLineRemoval(1) 

427 # ren.SetBackingStore(1) 

428 

429 self.renwin = renwin 

430 

431 # renwin.LineSmoothingOn() 

432 # renwin.PointSmoothingOn() 

433 # renwin.PolygonSmoothingOn() 

434 

435 renwin.AddRenderer(ren) 

436 

437 iren = renwin.GetInteractor() 

438 iren.LightFollowCameraOn() 

439 iren.SetInteractorStyle(None) 

440 

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

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

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

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

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

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

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

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

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

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

451 

452 renwin.Render() 

453 

454 iren.Initialize() 

455 

456 self.iren = iren 

457 

458 self.rotating = False 

459 

460 self._elements = {} 

461 self._elements_active = {} 

462 

463 self.talkie_connect( 

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

465 

466 self.state.elements.append(elements.IcosphereState( 

467 element_id='icosphere', 

468 level=4, 

469 smooth=True, 

470 opacity=0.5, 

471 ambient=0.1)) 

472 

473 self.state.elements.append(elements.GridState( 

474 element_id='grid')) 

475 self.state.elements.append(elements.CoastlinesState( 

476 element_id='coastlines')) 

477 self.state.elements.append(elements.CrosshairState( 

478 element_id='crosshair')) 

479 

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

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

482 # self.state.elements.append( 

483 # elements.CatalogState( 

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

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

486 

487 if events: 

488 self.state.elements.append( 

489 elements.CatalogState( 

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

491 

492 self.state.sort_elements() 

493 

494 if snapshots: 

495 snapshots_ = [] 

496 for obj in snapshots: 

497 if isinstance(obj, str): 

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

499 else: 

500 snapshots_.append(obj) 

501 

502 snapshots_panel.add_snapshots(snapshots_) 

503 self.raise_panel(snapshots_panel) 

504 snapshots_panel.goto_snapshot(1) 

505 

506 self.timer = qc.QTimer(self) 

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

508 self.timer.setInterval(1000) 

509 self.timer.start() 

510 

511 self._animation_saver = None 

512 

513 self.closing = False 

514 self.vtk_widget.setFocus() 

515 

516 self.update_detached() 

517 

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

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

520 

521 self.show() 

522 self.windowHandle().showMaximized() 

523 

524 self.talkie_connect( 

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

526 

527 self.update_vtk_widget_size() 

528 

529 def _add_vtk_widget_size_menu_entries(self, menu): 

530 

531 group = qw.QActionGroup(menu) 

532 group.setExclusive(True) 

533 

534 def set_variable_size(): 

535 self.gui_state.fixed_size = False 

536 

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

538 variable_size_action.setCheckable(True) 

539 variable_size_action.setActionGroup(group) 

540 variable_size_action.triggered.connect(set_variable_size) 

541 

542 fixed_size_items = [] 

543 for nx, ny, label in [ 

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

545 (426, 240, ''), 

546 (640, 360, ''), 

547 (854, 480, '(FWVGA)'), 

548 (1280, 720, '(HD)'), 

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

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

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

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

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

554 (640, 480, '(VGA)'), 

555 (800, 600, '(SVGA)'), 

556 (None, None, 'Other'), 

557 (512, 512, ''), 

558 (1024, 1024, '')]: 

559 

560 if None in (nx, ny): 

561 menu.addSection(label) 

562 else: 

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

564 action = menu.addAction(name) 

565 action.setCheckable(True) 

566 action.setActionGroup(group) 

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

568 

569 def make_set_fixed_size(nx, ny): 

570 def set_fixed_size(): 

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

572 

573 return set_fixed_size 

574 

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

576 

577 def update_widget(*args): 

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

579 action.blockSignals(True) 

580 action.setChecked( 

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

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

583 action.blockSignals(False) 

584 

585 variable_size_action.blockSignals(True) 

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

587 variable_size_action.blockSignals(False) 

588 

589 update_widget() 

590 self.talkie_connect( 

591 self.gui_state, 'fixed_size', update_widget) 

592 

593 def update_vtk_widget_size(self, *args): 

594 if self.gui_state.fixed_size: 

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

596 wanted_size = qc.QSize(nx, ny) 

597 else: 

598 wanted_size = qc.QSize( 

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

600 

601 current_size = self.vtk_widget.size() 

602 

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

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

605 

606 self.vtk_widget.setFixedSize(wanted_size) 

607 

608 self.vtk_frame.recenter() 

609 self.check_vtk_resize() 

610 

611 def update_focal_point(self, *args): 

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

613 self.vtk_widget.setStatusTip( 

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

615 'change view plane orientation.' % g_modifier_key) 

616 else: 

617 self.vtk_widget.setStatusTip( 

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

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

620 'reverse sense.' % g_modifier_key) 

621 

622 def update_detached(self, *args): 

623 

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

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

626 

627 self.main_layout.removeWidget(self.vtk_frame) 

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

629 self.detached_window.show() 

630 self.vtk_widget.setFocus() 

631 

632 screens = common.get_app().screens() 

633 if len(screens) > 1: 

634 for screen in screens: 

635 if screen is not self.screen(): 

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

637 # .setScreen() does not work reliably, 

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

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

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

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

642 

643 self.detached_window.windowHandle().showMaximized() 

644 

645 frame = qw.QFrame() 

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

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

648 # frame.setAutoFillBackground(True) 

649 frame.setSizePolicy( 

650 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

651 

652 layout = qw.QGridLayout() 

653 frame.setLayout(layout) 

654 self.main_layout.insertWidget(0, frame) 

655 

656 self.state_editor = StateEditor(self) 

657 

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

659 

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

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

662 # layout.addWidget( 

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

664 

665 self.vtk_frame_substitute = frame 

666 

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

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

669 self.detached_window.hide() 

670 self.vtk_frame.setParent(self) 

671 if self.vtk_frame_substitute: 

672 self.main_layout.removeWidget(self.vtk_frame_substitute) 

673 self.state_editor.unbind_state() 

674 self.vtk_frame_substitute = None 

675 

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

677 self.detached_window = None 

678 self.vtk_widget.setFocus() 

679 

680 def attach(self): 

681 self.gui_state.detached = False 

682 

683 def export_image(self): 

684 

685 caption = 'Export Image' 

686 fn_out, _ = qw.QFileDialog.getSaveFileName( 

687 self, caption, 'image.png', 

688 options=common.qfiledialog_options) 

689 

690 if fn_out: 

691 self.save_image(fn_out) 

692 

693 def save_image(self, path): 

694 

695 original_fixed_size = self.gui_state.fixed_size 

696 if original_fixed_size is None: 

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

698 

699 wif = vtk.vtkWindowToImageFilter() 

700 wif.SetInput(self.renwin) 

701 wif.SetInputBufferTypeToRGBA() 

702 wif.SetScale(1, 1) 

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.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 original_fixed_size = self.gui_state.fixed_size 

748 if original_fixed_size is None: 

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

750 

751 wif = vtk.vtkWindowToImageFilter() 

752 wif.SetInput(self.renwin) 

753 wif.SetInputBufferTypeToRGBA() 

754 wif.SetScale(1, 1) 

755 wif.ReadFrontBufferOff() 

756 writer = vtk.vtkPNGWriter() 

757 temp_path = tempfile.mkdtemp() 

758 self._animation_saver = ( 

759 wif, writer, temp_path, output_path, original_fixed_size) 

760 writer.SetInputConnection(wif.GetOutputPort()) 

761 

762 def next_animation_frame(self): 

763 

764 ani = self._animation 

765 if not ani: 

766 return 

767 

768 if self._animation_iframe is not None: 

769 state = ani( 

770 ani.tmin 

771 + self._animation_iframe * ani.dt) 

772 

773 self._animation_iframe += 1 

774 else: 

775 tnow = time.time() 

776 state = ani(min( 

777 ani.tmax, 

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

779 

780 self.set_state(state) 

781 self.renwin.Render() 

782 if self._animation_saver: 

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

784 wif.Modified() 

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

786 writer.SetFileName(fn % self._animation_iframe) 

787 writer.Write() 

788 

789 if self._animation_iframe is not None: 

790 t = self._animation_iframe * ani.dt 

791 else: 

792 t = tnow - self._animation_tstart 

793 

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

795 self.stop_animation() 

796 

797 def stop_animation(self): 

798 if self._animation_timer: 

799 self._animation_timer.stop() 

800 

801 if self._animation_saver: 

802 

803 wif, writer, temp_path, output_path, original_fixed_size \ 

804 = self._animation_saver 

805 self.gui_state.fixed_size = original_fixed_size 

806 

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._update_elements_enabled = False 

831 self.setUpdatesEnabled(False) 

832 self.state.diff_update(state) 

833 self.state.sort_elements() 

834 self.setUpdatesEnabled(True) 

835 self._update_elements_enabled = True 

836 self.update_elements() 

837 

838 def periodical(self): 

839 pass 

840 

841 def request_quit(self): 

842 app = common.get_app() 

843 app.myQuit() 

844 

845 def check_vtk_resize(self, *args): 

846 render_window_size = self.renwin.GetSize() 

847 if self._render_window_size != render_window_size: 

848 self._render_window_size = render_window_size 

849 self.resize_event(*render_window_size) 

850 

851 def update_elements(self, *_): 

852 if not self._update_elements_enabled: 

853 return 

854 

855 if self._in_update_elements: 

856 return 

857 

858 self._in_update_elements = True 

859 for estate in self.state.elements: 

860 if estate.element_id not in self._elements: 

861 new_element = estate.create() 

862 logger.debug('Creating "%s" ("%s").' % ( 

863 type(new_element).__name__, 

864 estate.element_id)) 

865 self._elements[estate.element_id] = new_element 

866 

867 element = self._elements[estate.element_id] 

868 

869 if estate.element_id not in self._elements_active: 

870 logger.debug('Adding "%s" ("%s")' % ( 

871 type(element).__name__, 

872 estate.element_id)) 

873 element.bind_state(estate) 

874 element.set_parent(self) 

875 self._elements_active[estate.element_id] = element 

876 

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

878 deactivate = [] 

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

880 if element_id not in state_element_ids: 

881 logger.debug('Removing "%s" ("%s").' % ( 

882 type(element).__name__, 

883 element_id)) 

884 element.unset_parent() 

885 deactivate.append(element_id) 

886 

887 for element_id in deactivate: 

888 del self._elements_active[element_id] 

889 

890 self._update_crosshair_bindings() 

891 

892 self._in_update_elements = False 

893 

894 def _update_crosshair_bindings(self): 

895 

896 def get_crosshair_element(): 

897 for element in self.state.elements: 

898 if element.element_id == 'crosshair': 

899 return element 

900 

901 return None 

902 

903 crosshair = get_crosshair_element() 

904 if crosshair is None or crosshair.is_connected: 

905 return 

906 

907 def to_checkbox(state, widget): 

908 widget.blockSignals(True) 

909 widget.setChecked(state.visible) 

910 widget.blockSignals(False) 

911 

912 def to_state(widget, state): 

913 state.visible = widget.isChecked() 

914 

915 cb = self._crosshair_checkbox 

916 vstate.state_bind( 

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

918 cb, [cb.toggled], to_checkbox) 

919 

920 crosshair.is_connected = True 

921 

922 def add_actor_2d(self, actor): 

923 if actor not in self._actors_2d: 

924 self.ren.AddActor2D(actor) 

925 self._actors_2d.add(actor) 

926 

927 def remove_actor_2d(self, actor): 

928 if actor in self._actors_2d: 

929 self.ren.RemoveActor2D(actor) 

930 self._actors_2d.remove(actor) 

931 

932 def add_actor(self, actor): 

933 if actor not in self._actors: 

934 self.ren.AddActor(actor) 

935 self._actors.add(actor) 

936 

937 def add_actor_list(self, actorlist): 

938 for actor in actorlist: 

939 self.add_actor(actor) 

940 

941 def remove_actor(self, actor): 

942 if actor in self._actors: 

943 self.ren.RemoveActor(actor) 

944 self._actors.remove(actor) 

945 

946 def update_view(self): 

947 self.vtk_widget.update() 

948 

949 def resize_event(self, size_x, size_y): 

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

951 

952 def button_event(self, obj, event): 

953 if event == "LeftButtonPressEvent": 

954 self.rotating = True 

955 elif event == "LeftButtonReleaseEvent": 

956 self.rotating = False 

957 

958 def mouse_move_event(self, obj, event): 

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

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

961 

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

963 center_x = size_x / 2.0 

964 center_y = size_y / 2.0 

965 

966 if self.rotating: 

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

968 

969 def myWheelEvent(self, event): 

970 

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

972 

973 if angle > 200: 

974 angle = 200 

975 

976 if angle < -200: 

977 angle = -200 

978 

979 self.do_dolly(-angle/100.) 

980 

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

982 

983 dx = x0 - x 

984 dy = y0 - y 

985 

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

987 focp = self.gui_state.focal_point 

988 

989 if focp == 'center': 

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

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

992 

993 lat = self.state.lat 

994 lon = self.state.lon 

995 factor = self.state.distance / 10.0 

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

997 else: 

998 lat = 90. - self.state.dip 

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

1000 factor = 0.5 

1001 factor_lat = 1.0 

1002 

1003 dlat = dy * factor 

1004 dlon = dx * factor * factor_lat 

1005 

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

1007 lon += dlon 

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

1009 

1010 if focp == 'center': 

1011 self.state.lat = float(lat) 

1012 self.state.lon = float(lon) 

1013 else: 

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

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

1016 

1017 def do_dolly(self, v): 

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

1019 

1020 def key_down_event(self, obj, event): 

1021 k = obj.GetKeyCode() 

1022 s = obj.GetKeySym() 

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

1024 self.gui_state.next_focal_point() 

1025 

1026 elif k == 'r': 

1027 self.reset_strike_dip() 

1028 

1029 elif k == 'p': 

1030 print(self.state) 

1031 

1032 elif k == 'i': 

1033 for elem in self.state.elements: 

1034 if isinstance(elem, elements.IcosphereState): 

1035 elem.visible = not elem.visible 

1036 

1037 elif k == 'c': 

1038 for elem in self.state.elements: 

1039 if isinstance(elem, elements.CoastlinesState): 

1040 elem.visible = not elem.visible 

1041 

1042 elif k == 't': 

1043 if not any( 

1044 isinstance(elem, elements.TopoState) 

1045 for elem in self.state.elements): 

1046 

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

1048 else: 

1049 for elem in self.state.elements: 

1050 if isinstance(elem, elements.TopoState): 

1051 elem.visible = not elem.visible 

1052 

1053 elif k == ' ': 

1054 self.toggle_panel_visibility() 

1055 

1056 def key_up_event(self, obj, event): 

1057 s = obj.GetKeySym() 

1058 if s == 'Control_L': 

1059 self.gui_state.next_focal_point() 

1060 

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

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

1063 

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

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

1066 

1067 def controls_navigation(self): 

1068 frame = qw.QFrame(self) 

1069 frame.setSizePolicy( 

1070 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1071 layout = qw.QGridLayout() 

1072 frame.setLayout(layout) 

1073 

1074 # lat, lon, depth 

1075 

1076 layout.addWidget( 

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

1078 

1079 le = qw.QLineEdit() 

1080 le.setStatusTip( 

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

1082 'Focal point location.') 

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

1084 

1085 def lat_lon_depth_to_lineedit(state, widget): 

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

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

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

1089 

1090 if sel: 

1091 widget.selectAll() 

1092 

1093 def lineedit_to_lat_lon_depth(widget, state): 

1094 s = str(widget.text()) 

1095 choices = location_to_choices(s) 

1096 if len(choices) > 0: 

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

1098 choices[0].get_lat_lon_depth() 

1099 else: 

1100 raise NoLocationChoices(s) 

1101 

1102 self._state_bind( 

1103 ['lat', 'lon', 'depth'], 

1104 lineedit_to_lat_lon_depth, 

1105 le, [le.editingFinished, le.returnPressed], 

1106 lat_lon_depth_to_lineedit) 

1107 

1108 self.lat_lon_lineedit = le 

1109 

1110 self.lat_lon_lineedit.returnPressed.connect( 

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

1112 

1113 # focal point 

1114 

1115 cb = qw.QCheckBox('Fix') 

1116 cb.setStatusTip( 

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

1118 % g_modifier_key) 

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

1120 

1121 def focal_point_to_checkbox(state, widget): 

1122 widget.blockSignals(True) 

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

1124 widget.blockSignals(False) 

1125 

1126 def checkbox_to_focal_point(widget, state): 

1127 self.gui_state.focal_point = \ 

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

1129 

1130 self._gui_state_bind( 

1131 ['focal_point'], checkbox_to_focal_point, 

1132 cb, [cb.toggled], focal_point_to_checkbox) 

1133 

1134 self.focal_point_checkbox = cb 

1135 

1136 self.talkie_connect( 

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

1138 

1139 self.update_focal_point() 

1140 

1141 # strike, dip 

1142 

1143 layout.addWidget( 

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

1145 

1146 le = qw.QLineEdit() 

1147 le.setStatusTip( 

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

1149 'direction.') 

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

1151 

1152 def strike_dip_to_lineedit(state, widget): 

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

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

1155 if sel: 

1156 widget.selectAll() 

1157 

1158 def lineedit_to_strike_dip(widget, state): 

1159 s = str(widget.text()) 

1160 string_to_strike_dip = { 

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

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

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

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

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

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

1167 

1168 if s in string_to_strike_dip: 

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

1170 

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

1172 try: 

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

1174 except Exception: 

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

1176 

1177 self._state_bind( 

1178 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1180 

1181 self.strike_dip_lineedit = le 

1182 self.strike_dip_lineedit.returnPressed.connect( 

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

1184 

1185 but = qw.QPushButton('Reset') 

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

1187 but.clicked.connect(self.reset_strike_dip) 

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

1189 

1190 # crosshair 

1191 

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

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

1194 

1195 # camera bindings 

1196 self.talkie_connect( 

1197 self.state, 

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

1199 self.update_camera) 

1200 

1201 self.talkie_connect( 

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

1203 

1204 return frame 

1205 

1206 def controls_time(self): 

1207 frame = qw.QFrame(self) 

1208 frame.setSizePolicy( 

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

1210 

1211 layout = qw.QGridLayout() 

1212 frame.setLayout(layout) 

1213 

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

1215 le_tmin = qw.QLineEdit() 

1216 layout.addWidget(le_tmin, 0, 1) 

1217 

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

1219 le_tmax = qw.QLineEdit() 

1220 layout.addWidget(le_tmax, 1, 1) 

1221 

1222 label_tcursor = qw.QLabel() 

1223 

1224 label_tcursor.setSizePolicy( 

1225 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1226 

1227 layout.addWidget(label_tcursor, 2, 1) 

1228 self._label_tcursor = label_tcursor 

1229 

1230 def time_to_lineedit(state, attribute, widget): 

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

1232 and widget.text() != '' 

1233 

1234 widget.setText( 

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

1236 

1237 if sel: 

1238 widget.selectAll() 

1239 

1240 def lineedit_to_time(widget, state, attribute): 

1241 from pyrocko.util import str_to_time_fillup 

1242 

1243 s = str(widget.text()) 

1244 if not s.strip(): 

1245 setattr(state, attribute, None) 

1246 else: 

1247 try: 

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

1249 except Exception: 

1250 raise ValueError( 

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

1252 

1253 self._state_bind( 

1254 ['tmin'], lineedit_to_time, le_tmin, 

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

1256 attribute='tmin') 

1257 self._state_bind( 

1258 ['tmax'], lineedit_to_time, le_tmax, 

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

1260 attribute='tmax') 

1261 

1262 self.tmin_lineedit = le_tmin 

1263 self.tmax_lineedit = le_tmax 

1264 

1265 range_edit = RangeEdit() 

1266 range_edit.set_data_provider(self) 

1267 range_edit.set_data_name('time') 

1268 

1269 xblock = [False] 

1270 

1271 def range_to_range_edit(state, widget): 

1272 if not xblock[0]: 

1273 widget.blockSignals(True) 

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

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

1276 widget.blockSignals(False) 

1277 

1278 def range_edit_to_range(widget, state): 

1279 xblock[0] = True 

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

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

1282 xblock[0] = False 

1283 

1284 self._state_bind( 

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

1286 range_edit_to_range, 

1287 range_edit, 

1288 [range_edit.rangeChanged, range_edit.focusChanged], 

1289 range_to_range_edit) 

1290 

1291 def handle_tcursor_changed(): 

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

1293 

1294 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1295 

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

1297 

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

1299 le_focus = qw.QLineEdit() 

1300 layout.addWidget(le_focus, 4, 1) 

1301 

1302 def focus_to_lineedit(state, widget): 

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

1304 and widget.text() != '' 

1305 

1306 if state.tduration is None: 

1307 widget.setText('') 

1308 else: 

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

1310 guts.str_duration(state.tduration), 

1311 state.tposition)) 

1312 

1313 if sel: 

1314 widget.selectAll() 

1315 

1316 def lineedit_to_focus(widget, state): 

1317 s = str(widget.text()) 

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

1319 try: 

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

1321 state.tduration = None 

1322 state.tposition = 0.0 

1323 else: 

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

1325 if len(w) > 1: 

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

1327 else: 

1328 state.tposition = 0.0 

1329 

1330 except Exception: 

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

1332 

1333 self._state_bind( 

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

1335 [le_focus.editingFinished, le_focus.returnPressed], 

1336 focus_to_lineedit) 

1337 

1338 label_effective_tmin = qw.QLabel() 

1339 label_effective_tmax = qw.QLabel() 

1340 

1341 label_effective_tmin.setSizePolicy( 

1342 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1343 label_effective_tmax.setSizePolicy( 

1344 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1345 label_effective_tmin.setMinimumSize( 

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

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

1348 

1349 layout.addWidget(label_effective_tmin, 5, 1) 

1350 layout.addWidget(label_effective_tmax, 6, 1) 

1351 

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

1353 self.talkie_connect( 

1354 self.state, var, self.update_effective_time_labels) 

1355 

1356 self._label_effective_tmin = label_effective_tmin 

1357 self._label_effective_tmax = label_effective_tmax 

1358 

1359 self.talkie_connect( 

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

1361 

1362 return frame 

1363 

1364 def controls_appearance(self): 

1365 frame = qw.QFrame(self) 

1366 frame.setSizePolicy( 

1367 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1368 layout = qw.QGridLayout() 

1369 frame.setLayout(layout) 

1370 

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

1372 

1373 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1374 layout.addWidget(cb, 0, 1) 

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

1376 

1377 self.talkie_connect( 

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

1379 

1380 # background 

1381 

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

1383 

1384 cb = common.strings_to_combobox( 

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

1386 

1387 layout.addWidget(cb, 1, 1) 

1388 vstate.state_bind_combobox_background( 

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

1390 

1391 self.talkie_connect( 

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

1393 

1394 return frame 

1395 

1396 def controls_snapshots(self): 

1397 return snapshots_mod.SnapshotsPanel(self) 

1398 

1399 def update_effective_time_labels(self, *args): 

1400 tmin = self.state.tmin_effective 

1401 tmax = self.state.tmax_effective 

1402 

1403 stmin = common.time_or_none_to_str(tmin) 

1404 stmax = common.time_or_none_to_str(tmax) 

1405 

1406 self._label_effective_tmin.setText(stmin) 

1407 self._label_effective_tmax.setText(stmax) 

1408 

1409 def update_tcursor(self, *args): 

1410 tcursor = self.gui_state.tcursor 

1411 stcursor = common.time_or_none_to_str(tcursor) 

1412 self._label_tcursor.setText(stcursor) 

1413 

1414 def reset_strike_dip(self, *args): 

1415 self.state.strike = 90. 

1416 self.state.dip = 0 

1417 self.gui_state.focal_point = 'center' 

1418 

1419 def get_camera_geometry(self): 

1420 

1421 def rtp2xyz(rtp): 

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

1423 

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

1425 

1426 cam_rtp = num.array([ 

1427 radius+self.state.distance, 

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

1429 self.state.lon * d2r]) 

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

1431 cam, up, foc = \ 

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

1433 

1434 foc_rtp = num.array([ 

1435 radius, 

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

1437 self.state.lon * d2r]) 

1438 

1439 foc = rtp2xyz(foc_rtp) 

1440 

1441 rot_world = pmt.euler_to_matrix( 

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

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

1444 0.0*d2r).T 

1445 

1446 rot_cam = pmt.euler_to_matrix( 

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

1448 

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

1450 

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

1452 up = num.dot(rot, up) 

1453 return cam, up, foc 

1454 

1455 def update_camera(self, *args): 

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

1457 camera = self.ren.GetActiveCamera() 

1458 camera.SetPosition(*cam) 

1459 camera.SetFocalPoint(*foc) 

1460 camera.SetViewUp(*up) 

1461 

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

1463 

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

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

1466 

1467 # if horizon == 0.0: 

1468 # horizon = 2.0 + self.state.distance 

1469 

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

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

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

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

1474 # clip_dist = feature_horizon 

1475 

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

1477 

1478 self.camera_params = ( 

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

1480 

1481 self.update_view() 

1482 

1483 def add_panel( 

1484 self, title_label, panel, 

1485 visible=False, 

1486 # volatile=False, 

1487 tabify=True, 

1488 where=qc.Qt.RightDockWidgetArea, 

1489 remove=None, 

1490 title_controls=[]): 

1491 

1492 dockwidget = common.MyDockWidget( 

1493 self, title_label, title_controls=title_controls) 

1494 

1495 if not visible: 

1496 dockwidget.hide() 

1497 

1498 if not self.gui_state.panels_visible: 

1499 dockwidget.block() 

1500 

1501 dockwidget.setWidget(panel) 

1502 

1503 panel.setParent(dockwidget) 

1504 

1505 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1507 

1508 self.addDockWidget(where, dockwidget) 

1509 

1510 nwrap = 4 

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

1512 self.tabifyDockWidget( 

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

1514 

1515 mitem = dockwidget.toggleViewAction() 

1516 

1517 def update_label(*args): 

1518 mitem.setText(dockwidget.titlebar._title_label.get_full_title()) 

1519 self.update_slug_abbreviated_lengths() 

1520 

1521 dockwidget.titlebar._title_label.title_changed.connect(update_label) 

1522 dockwidget.titlebar._title_label.title_changed.connect( 

1523 self.update_slug_abbreviated_lengths) 

1524 

1525 update_label() 

1526 

1527 self._panel_togglers[dockwidget] = mitem 

1528 self.panels_menu.addAction(mitem) 

1529 if visible: 

1530 dockwidget.setVisible(True) 

1531 dockwidget.setFocus() 

1532 dockwidget.raise_() 

1533 

1534 def update_slug_abbreviated_lengths(self): 

1535 dockwidgets = self.findChildren(common.MyDockWidget) 

1536 title_labels = [] 

1537 for dw in dockwidgets: 

1538 title_labels.append(dw.titlebar._title_label) 

1539 

1540 by_title = defaultdict(list) 

1541 for tl in title_labels: 

1542 by_title[tl.get_title()].append(tl) 

1543 

1544 for group in by_title.values(): 

1545 slugs = [tl.get_slug() for tl in group] 

1546 

1547 n = max(len(slug) for slug in slugs) 

1548 nunique = len(set(slugs)) 

1549 

1550 while n > 0 and len(set(slug[:n-1] for slug in slugs)) == nunique: 

1551 n -= 1 

1552 

1553 if n > 0: 

1554 n = max(3, n) 

1555 

1556 for tl in group: 

1557 tl.set_slug_abbreviated_length(n) 

1558 

1559 def raise_panel(self, panel): 

1560 dockwidget = panel.parent() 

1561 dockwidget.setVisible(True) 

1562 dockwidget.setFocus() 

1563 dockwidget.raise_() 

1564 

1565 def toggle_panel_visibility(self): 

1566 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1567 

1568 def update_panel_visibility(self, *args): 

1569 self.setUpdatesEnabled(False) 

1570 mbar = self.menuBar() 

1571 dockwidgets = self.findChildren(common.MyDockWidget) 

1572 

1573 mbar.setVisible(self.gui_state.panels_visible) 

1574 for dockwidget in dockwidgets: 

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

1576 

1577 self.setUpdatesEnabled(True) 

1578 

1579 def remove_panel(self, panel): 

1580 dockwidget = panel.parent() 

1581 self.removeDockWidget(dockwidget) 

1582 dockwidget.setParent(None) 

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

1584 

1585 def register_data_provider(self, provider): 

1586 if provider not in self.data_providers: 

1587 self.data_providers.append(provider) 

1588 

1589 def unregister_data_provider(self, provider): 

1590 if provider in self.data_providers: 

1591 self.data_providers.remove(provider) 

1592 

1593 def iter_data(self, name): 

1594 for provider in self.data_providers: 

1595 for data in provider.iter_data(name): 

1596 yield data 

1597 

1598 def closeEvent(self, event): 

1599 self.attach() 

1600 event.accept() 

1601 self.closing = True 

1602 common.get_app().set_main_window(None) 

1603 

1604 def is_closing(self): 

1605 return self.closing 

1606 

1607 

1608class SparrowApp(qw.QApplication): 

1609 def __init__(self): 

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

1611 self.lastWindowClosed.connect(self.myQuit) 

1612 self._main_window = None 

1613 self.setApplicationDisplayName('Sparrow') 

1614 self.setDesktopFileName('Sparrow') 

1615 

1616 def install_sigint_handler(self): 

1617 self._old_signal_handler = signal.signal( 

1618 signal.SIGINT, self.myCloseAllWindows) 

1619 

1620 def uninstall_sigint_handler(self): 

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

1622 

1623 def myQuit(self, *args): 

1624 self.quit() 

1625 

1626 def myCloseAllWindows(self, *args): 

1627 self.closeAllWindows() 

1628 

1629 def set_main_window(self, win): 

1630 self._main_window = win 

1631 

1632 def get_main_window(self): 

1633 return self._main_window 

1634 

1635 def get_progressbars(self): 

1636 if self._main_window: 

1637 return self._main_window.progressbars 

1638 else: 

1639 return None 

1640 

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

1642 win = self.get_main_window() 

1643 if not win: 

1644 return 

1645 

1646 win.statusBar().showMessage( 

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

1648 

1649 

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

1651 

1652 from pyrocko import util 

1653 from pyrocko.gui import util as gui_util 

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

1655 

1656 global win 

1657 

1658 if gui_util.app is None: 

1659 gui_util.app = SparrowApp() 

1660 

1661 # try: 

1662 # from qt_material import apply_stylesheet 

1663 # 

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

1665 # 

1666 # 

1667 # import qdarkgraystyle 

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

1669 # import qdarkstyle 

1670 # 

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

1672 # 

1673 # 

1674 # except ImportError: 

1675 # logger.info( 

1676 # 'Module qdarkgraystyle not available.\n' 

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

1678 # 'qdarkgraystyle".') 

1679 # 

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

1681 

1682 gui_util.app.install_sigint_handler() 

1683 gui_util.app.exec_() 

1684 gui_util.app.uninstall_sigint_handler() 

1685 

1686 del win 

1687 

1688 gc.collect() 

1689 

1690 del gui_util.app