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.ReadFrontBufferOff() 

703 writer = vtk.vtkPNGWriter() 

704 writer.SetInputConnection(wif.GetOutputPort()) 

705 

706 self.renwin.Render() 

707 wif.Modified() 

708 writer.SetFileName(path) 

709 writer.Write() 

710 

711 self.vtk_widget.setFixedSize( 

712 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

713 

714 self.gui_state.fixed_size = original_fixed_size 

715 

716 def update_render_settings(self, *args): 

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

718 self.ren.RemoveAllLights() 

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

720 self.ren.AddLight(li) 

721 

722 self._lighting = self.state.lighting 

723 

724 if self._background is None \ 

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

726 

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

728 self._background = self.state.background 

729 

730 self.update_view() 

731 

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

733 self._animation = interpolator 

734 if output_path is None: 

735 self._animation_tstart = time.time() 

736 self._animation_iframe = None 

737 else: 

738 self._animation_iframe = 0 

739 self.showFullScreen() 

740 self.update_view() 

741 self.gui_state.panels_visible = False 

742 self.update_view() 

743 

744 self._animation_timer = qc.QTimer(self) 

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

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

747 self._animation_timer.start() 

748 if output_path is not None: 

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

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

751 

752 wif = vtk.vtkWindowToImageFilter() 

753 wif.SetInput(self.renwin) 

754 wif.SetInputBufferTypeToRGBA() 

755 wif.SetScale(1, 1) 

756 wif.ReadFrontBufferOff() 

757 writer = vtk.vtkPNGWriter() 

758 temp_path = tempfile.mkdtemp() 

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

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 self.vtk_widget.setFixedSize( 

803 qw.QWIDGETSIZE_MAX, qw.QWIDGETSIZE_MAX) 

804 

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

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

807 check_call([ 

808 'ffmpeg', '-y', 

809 '-i', fn_path, 

810 '-c:v', 'libx264', 

811 '-preset', 'slow', 

812 '-crf', '17', 

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

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

815 output_path]) 

816 shutil.rmtree(temp_path) 

817 

818 self._animation_saver = None 

819 self._animation_saver 

820 

821 self.showNormal() 

822 self.gui_state.panels_visible = True 

823 

824 self._animation_tstart = None 

825 self._animation_iframe = None 

826 self._animation = None 

827 

828 def set_state(self, state): 

829 self._update_elements_enabled = False 

830 self.setUpdatesEnabled(False) 

831 self.state.diff_update(state) 

832 self.state.sort_elements() 

833 self.setUpdatesEnabled(True) 

834 self._update_elements_enabled = True 

835 self.update_elements() 

836 

837 def periodical(self): 

838 pass 

839 

840 def request_quit(self): 

841 app = common.get_app() 

842 app.myQuit() 

843 

844 def check_vtk_resize(self, *args): 

845 render_window_size = self.renwin.GetSize() 

846 if self._render_window_size != render_window_size: 

847 self._render_window_size = render_window_size 

848 self.resize_event(*render_window_size) 

849 

850 def update_elements(self, *_): 

851 if not self._update_elements_enabled: 

852 return 

853 

854 if self._in_update_elements: 

855 return 

856 

857 self._in_update_elements = True 

858 for estate in self.state.elements: 

859 if estate.element_id not in self._elements: 

860 new_element = estate.create() 

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

862 type(new_element).__name__, 

863 estate.element_id)) 

864 self._elements[estate.element_id] = new_element 

865 

866 element = self._elements[estate.element_id] 

867 

868 if estate.element_id not in self._elements_active: 

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

870 type(element).__name__, 

871 estate.element_id)) 

872 element.bind_state(estate) 

873 element.set_parent(self) 

874 self._elements_active[estate.element_id] = element 

875 

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

877 deactivate = [] 

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

879 if element_id not in state_element_ids: 

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

881 type(element).__name__, 

882 element_id)) 

883 element.unset_parent() 

884 deactivate.append(element_id) 

885 

886 for element_id in deactivate: 

887 del self._elements_active[element_id] 

888 

889 self._update_crosshair_bindings() 

890 

891 self._in_update_elements = False 

892 

893 def _update_crosshair_bindings(self): 

894 

895 def get_crosshair_element(): 

896 for element in self.state.elements: 

897 if element.element_id == 'crosshair': 

898 return element 

899 

900 return None 

901 

902 crosshair = get_crosshair_element() 

903 if crosshair is None or crosshair.is_connected: 

904 return 

905 

906 def to_checkbox(state, widget): 

907 widget.blockSignals(True) 

908 widget.setChecked(state.visible) 

909 widget.blockSignals(False) 

910 

911 def to_state(widget, state): 

912 state.visible = widget.isChecked() 

913 

914 cb = self._crosshair_checkbox 

915 vstate.state_bind( 

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

917 cb, [cb.toggled], to_checkbox) 

918 

919 crosshair.is_connected = True 

920 

921 def add_actor_2d(self, actor): 

922 if actor not in self._actors_2d: 

923 self.ren.AddActor2D(actor) 

924 self._actors_2d.add(actor) 

925 

926 def remove_actor_2d(self, actor): 

927 if actor in self._actors_2d: 

928 self.ren.RemoveActor2D(actor) 

929 self._actors_2d.remove(actor) 

930 

931 def add_actor(self, actor): 

932 if actor not in self._actors: 

933 self.ren.AddActor(actor) 

934 self._actors.add(actor) 

935 

936 def add_actor_list(self, actorlist): 

937 for actor in actorlist: 

938 self.add_actor(actor) 

939 

940 def remove_actor(self, actor): 

941 if actor in self._actors: 

942 self.ren.RemoveActor(actor) 

943 self._actors.remove(actor) 

944 

945 def update_view(self): 

946 self.vtk_widget.update() 

947 

948 def resize_event(self, size_x, size_y): 

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

950 

951 def button_event(self, obj, event): 

952 if event == "LeftButtonPressEvent": 

953 self.rotating = True 

954 elif event == "LeftButtonReleaseEvent": 

955 self.rotating = False 

956 

957 def mouse_move_event(self, obj, event): 

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

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

960 

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

962 center_x = size_x / 2.0 

963 center_y = size_y / 2.0 

964 

965 if self.rotating: 

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

967 

968 def myWheelEvent(self, event): 

969 

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

971 

972 if angle > 200: 

973 angle = 200 

974 

975 if angle < -200: 

976 angle = -200 

977 

978 self.do_dolly(-angle/100.) 

979 

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

981 

982 dx = x0 - x 

983 dy = y0 - y 

984 

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

986 focp = self.gui_state.focal_point 

987 

988 if focp == 'center': 

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

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

991 

992 lat = self.state.lat 

993 lon = self.state.lon 

994 factor = self.state.distance / 10.0 

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

996 else: 

997 lat = 90. - self.state.dip 

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

999 factor = 0.5 

1000 factor_lat = 1.0 

1001 

1002 dlat = dy * factor 

1003 dlon = dx * factor * factor_lat 

1004 

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

1006 lon += dlon 

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

1008 

1009 if focp == 'center': 

1010 self.state.lat = float(lat) 

1011 self.state.lon = float(lon) 

1012 else: 

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

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

1015 

1016 def do_dolly(self, v): 

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

1018 

1019 def key_down_event(self, obj, event): 

1020 k = obj.GetKeyCode() 

1021 s = obj.GetKeySym() 

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

1023 self.gui_state.next_focal_point() 

1024 

1025 elif k == 'r': 

1026 self.reset_strike_dip() 

1027 

1028 elif k == 'p': 

1029 print(self.state) 

1030 

1031 elif k == 'i': 

1032 for elem in self.state.elements: 

1033 if isinstance(elem, elements.IcosphereState): 

1034 elem.visible = not elem.visible 

1035 

1036 elif k == 'c': 

1037 for elem in self.state.elements: 

1038 if isinstance(elem, elements.CoastlinesState): 

1039 elem.visible = not elem.visible 

1040 

1041 elif k == 't': 

1042 if not any( 

1043 isinstance(elem, elements.TopoState) 

1044 for elem in self.state.elements): 

1045 

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

1047 else: 

1048 for elem in self.state.elements: 

1049 if isinstance(elem, elements.TopoState): 

1050 elem.visible = not elem.visible 

1051 

1052 elif k == ' ': 

1053 self.toggle_panel_visibility() 

1054 

1055 def key_up_event(self, obj, event): 

1056 s = obj.GetKeySym() 

1057 if s == 'Control_L': 

1058 self.gui_state.next_focal_point() 

1059 

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

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

1062 

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

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

1065 

1066 def controls_navigation(self): 

1067 frame = qw.QFrame(self) 

1068 frame.setSizePolicy( 

1069 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1070 layout = qw.QGridLayout() 

1071 frame.setLayout(layout) 

1072 

1073 # lat, lon, depth 

1074 

1075 layout.addWidget( 

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

1077 

1078 le = qw.QLineEdit() 

1079 le.setStatusTip( 

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

1081 'Focal point location.') 

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

1083 

1084 def lat_lon_depth_to_lineedit(state, widget): 

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

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

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

1088 

1089 if sel: 

1090 widget.selectAll() 

1091 

1092 def lineedit_to_lat_lon_depth(widget, state): 

1093 s = str(widget.text()) 

1094 choices = location_to_choices(s) 

1095 if len(choices) > 0: 

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

1097 choices[0].get_lat_lon_depth() 

1098 else: 

1099 raise NoLocationChoices(s) 

1100 

1101 self._state_bind( 

1102 ['lat', 'lon', 'depth'], 

1103 lineedit_to_lat_lon_depth, 

1104 le, [le.editingFinished, le.returnPressed], 

1105 lat_lon_depth_to_lineedit) 

1106 

1107 self.lat_lon_lineedit = le 

1108 

1109 self.lat_lon_lineedit.returnPressed.connect( 

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

1111 

1112 # focal point 

1113 

1114 cb = qw.QCheckBox('Fix') 

1115 cb.setStatusTip( 

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

1117 % g_modifier_key) 

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

1119 

1120 def focal_point_to_checkbox(state, widget): 

1121 widget.blockSignals(True) 

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

1123 widget.blockSignals(False) 

1124 

1125 def checkbox_to_focal_point(widget, state): 

1126 self.gui_state.focal_point = \ 

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

1128 

1129 self._gui_state_bind( 

1130 ['focal_point'], checkbox_to_focal_point, 

1131 cb, [cb.toggled], focal_point_to_checkbox) 

1132 

1133 self.focal_point_checkbox = cb 

1134 

1135 self.talkie_connect( 

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

1137 

1138 self.update_focal_point() 

1139 

1140 # strike, dip 

1141 

1142 layout.addWidget( 

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

1144 

1145 le = qw.QLineEdit() 

1146 le.setStatusTip( 

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

1148 'direction.') 

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

1150 

1151 def strike_dip_to_lineedit(state, widget): 

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

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

1154 if sel: 

1155 widget.selectAll() 

1156 

1157 def lineedit_to_strike_dip(widget, state): 

1158 s = str(widget.text()) 

1159 string_to_strike_dip = { 

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

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

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

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

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

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

1166 

1167 if s in string_to_strike_dip: 

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

1169 

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

1171 try: 

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

1173 except Exception: 

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

1175 

1176 self._state_bind( 

1177 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1179 

1180 self.strike_dip_lineedit = le 

1181 self.strike_dip_lineedit.returnPressed.connect( 

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

1183 

1184 but = qw.QPushButton('Reset') 

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

1186 but.clicked.connect(self.reset_strike_dip) 

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

1188 

1189 # crosshair 

1190 

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

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

1193 

1194 # camera bindings 

1195 self.talkie_connect( 

1196 self.state, 

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

1198 self.update_camera) 

1199 

1200 self.talkie_connect( 

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

1202 

1203 return frame 

1204 

1205 def controls_time(self): 

1206 frame = qw.QFrame(self) 

1207 frame.setSizePolicy( 

1208 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1209 

1210 layout = qw.QGridLayout() 

1211 frame.setLayout(layout) 

1212 

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

1214 le_tmin = qw.QLineEdit() 

1215 layout.addWidget(le_tmin, 0, 1) 

1216 

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

1218 le_tmax = qw.QLineEdit() 

1219 layout.addWidget(le_tmax, 1, 1) 

1220 

1221 label_tcursor = qw.QLabel() 

1222 

1223 label_tcursor.setSizePolicy( 

1224 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1225 

1226 layout.addWidget(label_tcursor, 2, 1) 

1227 self._label_tcursor = label_tcursor 

1228 

1229 def time_to_lineedit(state, attribute, widget): 

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

1231 and widget.text() != '' 

1232 

1233 widget.setText( 

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

1235 

1236 if sel: 

1237 widget.selectAll() 

1238 

1239 def lineedit_to_time(widget, state, attribute): 

1240 from pyrocko.util import str_to_time_fillup 

1241 

1242 s = str(widget.text()) 

1243 if not s.strip(): 

1244 setattr(state, attribute, None) 

1245 else: 

1246 try: 

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

1248 except Exception: 

1249 raise ValueError( 

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

1251 

1252 self._state_bind( 

1253 ['tmin'], lineedit_to_time, le_tmin, 

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

1255 attribute='tmin') 

1256 self._state_bind( 

1257 ['tmax'], lineedit_to_time, le_tmax, 

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

1259 attribute='tmax') 

1260 

1261 self.tmin_lineedit = le_tmin 

1262 self.tmax_lineedit = le_tmax 

1263 

1264 range_edit = RangeEdit() 

1265 range_edit.set_data_provider(self) 

1266 range_edit.set_data_name('time') 

1267 

1268 xblock = [False] 

1269 

1270 def range_to_range_edit(state, widget): 

1271 if not xblock[0]: 

1272 widget.blockSignals(True) 

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

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

1275 widget.blockSignals(False) 

1276 

1277 def range_edit_to_range(widget, state): 

1278 xblock[0] = True 

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

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

1281 xblock[0] = False 

1282 

1283 self._state_bind( 

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

1285 range_edit_to_range, 

1286 range_edit, 

1287 [range_edit.rangeChanged, range_edit.focusChanged], 

1288 range_to_range_edit) 

1289 

1290 def handle_tcursor_changed(): 

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

1292 

1293 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1294 

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

1296 

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

1298 le_focus = qw.QLineEdit() 

1299 layout.addWidget(le_focus, 4, 1) 

1300 

1301 def focus_to_lineedit(state, widget): 

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

1303 and widget.text() != '' 

1304 

1305 if state.tduration is None: 

1306 widget.setText('') 

1307 else: 

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

1309 guts.str_duration(state.tduration), 

1310 state.tposition)) 

1311 

1312 if sel: 

1313 widget.selectAll() 

1314 

1315 def lineedit_to_focus(widget, state): 

1316 s = str(widget.text()) 

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

1318 try: 

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

1320 state.tduration = None 

1321 state.tposition = 0.0 

1322 else: 

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

1324 if len(w) > 1: 

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

1326 else: 

1327 state.tposition = 0.0 

1328 

1329 except Exception: 

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

1331 

1332 self._state_bind( 

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

1334 [le_focus.editingFinished, le_focus.returnPressed], 

1335 focus_to_lineedit) 

1336 

1337 label_effective_tmin = qw.QLabel() 

1338 label_effective_tmax = qw.QLabel() 

1339 

1340 label_effective_tmin.setSizePolicy( 

1341 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1342 label_effective_tmax.setSizePolicy( 

1343 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1344 label_effective_tmin.setMinimumSize( 

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

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

1347 

1348 layout.addWidget(label_effective_tmin, 5, 1) 

1349 layout.addWidget(label_effective_tmax, 6, 1) 

1350 

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

1352 self.talkie_connect( 

1353 self.state, var, self.update_effective_time_labels) 

1354 

1355 self._label_effective_tmin = label_effective_tmin 

1356 self._label_effective_tmax = label_effective_tmax 

1357 

1358 self.talkie_connect( 

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

1360 

1361 return frame 

1362 

1363 def controls_appearance(self): 

1364 frame = qw.QFrame(self) 

1365 frame.setSizePolicy( 

1366 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1367 layout = qw.QGridLayout() 

1368 frame.setLayout(layout) 

1369 

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

1371 

1372 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1373 layout.addWidget(cb, 0, 1) 

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

1375 

1376 self.talkie_connect( 

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

1378 

1379 # background 

1380 

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

1382 

1383 cb = common.strings_to_combobox( 

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

1385 

1386 layout.addWidget(cb, 1, 1) 

1387 vstate.state_bind_combobox_background( 

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

1389 

1390 self.talkie_connect( 

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

1392 

1393 return frame 

1394 

1395 def controls_snapshots(self): 

1396 return snapshots_mod.SnapshotsPanel(self) 

1397 

1398 def update_effective_time_labels(self, *args): 

1399 tmin = self.state.tmin_effective 

1400 tmax = self.state.tmax_effective 

1401 

1402 stmin = common.time_or_none_to_str(tmin) 

1403 stmax = common.time_or_none_to_str(tmax) 

1404 

1405 self._label_effective_tmin.setText(stmin) 

1406 self._label_effective_tmax.setText(stmax) 

1407 

1408 def update_tcursor(self, *args): 

1409 tcursor = self.gui_state.tcursor 

1410 stcursor = common.time_or_none_to_str(tcursor) 

1411 self._label_tcursor.setText(stcursor) 

1412 

1413 def reset_strike_dip(self, *args): 

1414 self.state.strike = 90. 

1415 self.state.dip = 0 

1416 self.gui_state.focal_point = 'center' 

1417 

1418 def get_camera_geometry(self): 

1419 

1420 def rtp2xyz(rtp): 

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

1422 

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

1424 

1425 cam_rtp = num.array([ 

1426 radius+self.state.distance, 

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

1428 self.state.lon * d2r]) 

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

1430 cam, up, foc = \ 

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

1432 

1433 foc_rtp = num.array([ 

1434 radius, 

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

1436 self.state.lon * d2r]) 

1437 

1438 foc = rtp2xyz(foc_rtp) 

1439 

1440 rot_world = pmt.euler_to_matrix( 

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

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

1443 0.0*d2r).T 

1444 

1445 rot_cam = pmt.euler_to_matrix( 

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

1447 

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

1449 

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

1451 up = num.dot(rot, up) 

1452 return cam, up, foc 

1453 

1454 def update_camera(self, *args): 

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

1456 camera = self.ren.GetActiveCamera() 

1457 camera.SetPosition(*cam) 

1458 camera.SetFocalPoint(*foc) 

1459 camera.SetViewUp(*up) 

1460 

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

1462 

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

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

1465 

1466 # if horizon == 0.0: 

1467 # horizon = 2.0 + self.state.distance 

1468 

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

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

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

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

1473 # clip_dist = feature_horizon 

1474 

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

1476 

1477 self.camera_params = ( 

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

1479 

1480 self.update_view() 

1481 

1482 def add_panel( 

1483 self, title_label, panel, 

1484 visible=False, 

1485 # volatile=False, 

1486 tabify=True, 

1487 where=qc.Qt.RightDockWidgetArea, 

1488 remove=None, 

1489 title_controls=[]): 

1490 

1491 dockwidget = common.MyDockWidget( 

1492 self, title_label, title_controls=title_controls) 

1493 

1494 if not visible: 

1495 dockwidget.hide() 

1496 

1497 if not self.gui_state.panels_visible: 

1498 dockwidget.block() 

1499 

1500 dockwidget.setWidget(panel) 

1501 

1502 panel.setParent(dockwidget) 

1503 

1504 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1506 

1507 self.addDockWidget(where, dockwidget) 

1508 

1509 nwrap = 4 

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

1511 self.tabifyDockWidget( 

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

1513 

1514 mitem = dockwidget.toggleViewAction() 

1515 

1516 def update_label(*args): 

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

1518 self.update_slug_abbreviated_lengths() 

1519 

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

1521 dockwidget.titlebar._title_label.title_changed.connect( 

1522 self.update_slug_abbreviated_lengths) 

1523 

1524 update_label() 

1525 

1526 self._panel_togglers[dockwidget] = mitem 

1527 self.panels_menu.addAction(mitem) 

1528 if visible: 

1529 dockwidget.setVisible(True) 

1530 dockwidget.setFocus() 

1531 dockwidget.raise_() 

1532 

1533 def update_slug_abbreviated_lengths(self): 

1534 dockwidgets = self.findChildren(common.MyDockWidget) 

1535 title_labels = [] 

1536 for dw in dockwidgets: 

1537 title_labels.append(dw.titlebar._title_label) 

1538 

1539 by_title = defaultdict(list) 

1540 for tl in title_labels: 

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

1542 

1543 for group in by_title.values(): 

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

1545 

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

1547 nunique = len(set(slugs)) 

1548 

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

1550 n -= 1 

1551 

1552 if n > 0: 

1553 n = max(3, n) 

1554 

1555 for tl in group: 

1556 tl.set_slug_abbreviated_length(n) 

1557 

1558 def raise_panel(self, panel): 

1559 dockwidget = panel.parent() 

1560 dockwidget.setVisible(True) 

1561 dockwidget.setFocus() 

1562 dockwidget.raise_() 

1563 

1564 def toggle_panel_visibility(self): 

1565 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1566 

1567 def update_panel_visibility(self, *args): 

1568 self.setUpdatesEnabled(False) 

1569 mbar = self.menuBar() 

1570 dockwidgets = self.findChildren(common.MyDockWidget) 

1571 

1572 mbar.setVisible(self.gui_state.panels_visible) 

1573 for dockwidget in dockwidgets: 

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

1575 

1576 self.setUpdatesEnabled(True) 

1577 

1578 def remove_panel(self, panel): 

1579 dockwidget = panel.parent() 

1580 self.removeDockWidget(dockwidget) 

1581 dockwidget.setParent(None) 

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

1583 

1584 def register_data_provider(self, provider): 

1585 if provider not in self.data_providers: 

1586 self.data_providers.append(provider) 

1587 

1588 def unregister_data_provider(self, provider): 

1589 if provider in self.data_providers: 

1590 self.data_providers.remove(provider) 

1591 

1592 def iter_data(self, name): 

1593 for provider in self.data_providers: 

1594 for data in provider.iter_data(name): 

1595 yield data 

1596 

1597 def closeEvent(self, event): 

1598 self.attach() 

1599 event.accept() 

1600 self.closing = True 

1601 common.get_app().set_main_window(None) 

1602 

1603 def is_closing(self): 

1604 return self.closing 

1605 

1606 

1607class SparrowApp(qw.QApplication): 

1608 def __init__(self): 

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

1610 self.lastWindowClosed.connect(self.myQuit) 

1611 self._main_window = None 

1612 self.setApplicationDisplayName('Sparrow') 

1613 self.setDesktopFileName('Sparrow') 

1614 

1615 def install_sigint_handler(self): 

1616 self._old_signal_handler = signal.signal( 

1617 signal.SIGINT, self.myCloseAllWindows) 

1618 

1619 def uninstall_sigint_handler(self): 

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

1621 

1622 def myQuit(self, *args): 

1623 self.quit() 

1624 

1625 def myCloseAllWindows(self, *args): 

1626 self.closeAllWindows() 

1627 

1628 def set_main_window(self, win): 

1629 self._main_window = win 

1630 

1631 def get_main_window(self): 

1632 return self._main_window 

1633 

1634 def get_progressbars(self): 

1635 if self._main_window: 

1636 return self._main_window.progressbars 

1637 else: 

1638 return None 

1639 

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

1641 win = self.get_main_window() 

1642 if not win: 

1643 return 

1644 

1645 win.statusBar().showMessage( 

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

1647 

1648 

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

1650 

1651 from pyrocko import util 

1652 from pyrocko.gui import util as gui_util 

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

1654 

1655 global win 

1656 

1657 if gui_util.app is None: 

1658 gui_util.app = SparrowApp() 

1659 

1660 # try: 

1661 # from qt_material import apply_stylesheet 

1662 # 

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

1664 # 

1665 # 

1666 # import qdarkgraystyle 

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

1668 # import qdarkstyle 

1669 # 

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

1671 # 

1672 # 

1673 # except ImportError: 

1674 # logger.info( 

1675 # 'Module qdarkgraystyle not available.\n' 

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

1677 # 'qdarkgraystyle".') 

1678 # 

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

1680 

1681 gui_util.app.install_sigint_handler() 

1682 gui_util.app.exec_() 

1683 gui_util.app.uninstall_sigint_handler() 

1684 

1685 del win 

1686 

1687 gc.collect() 

1688 

1689 del gui_util.app