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 Subtitle', elements.HudState( 

323 template='Subtitle')), 

324 ('HUD (tmax_effective)', elements.HudState( 

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

326 position='top-left')), 

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

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

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

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

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

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

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

334 

335 def wrap_add_element(estate): 

336 def add_element(*args): 

337 new_element = guts.clone(estate) 

338 new_element.element_id = elements.random_id() 

339 self.state.elements.append(new_element) 

340 self.state.sort_elements() 

341 

342 return add_element 

343 

344 mitem = qw.QAction(name, self) 

345 

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

347 

348 menu.addAction(mitem) 

349 

350 self.data_providers = [] 

351 self.elements = {} 

352 

353 self.detached_window = None 

354 

355 self.main_frame = qw.QFrame() 

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

357 

358 self.vtk_frame = CenteringScrollArea() 

359 

360 self.vtk_widget = QVTKWidget(self, self) 

361 self.vtk_frame.setWidget(self.vtk_widget) 

362 

363 self.main_layout = qw.QVBoxLayout() 

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

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

366 

367 pb = Progressbars(self) 

368 self.progressbars = pb 

369 self.main_layout.addWidget(pb) 

370 

371 self.main_frame.setLayout(self.main_layout) 

372 

373 self.vtk_frame_substitute = None 

374 

375 self.add_panel( 

376 'Navigation', 

377 self.controls_navigation(), visible=True, 

378 where=qc.Qt.LeftDockWidgetArea) 

379 

380 self.add_panel( 

381 'Time', 

382 self.controls_time(), visible=True, 

383 where=qc.Qt.LeftDockWidgetArea) 

384 

385 self.add_panel( 

386 'Appearance', 

387 self.controls_appearance(), visible=True, 

388 where=qc.Qt.LeftDockWidgetArea) 

389 

390 snapshots_panel = self.controls_snapshots() 

391 self.add_panel( 

392 'Snapshots', 

393 snapshots_panel, visible=False, 

394 where=qc.Qt.LeftDockWidgetArea) 

395 

396 self.setCentralWidget(self.main_frame) 

397 

398 self.mesh = None 

399 

400 ren = vtk.vtkRenderer() 

401 

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

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

404 # ren.TwoSidedLightingOn() 

405 # ren.SetUseShadows(1) 

406 

407 self._lighting = None 

408 self._background = None 

409 

410 self.ren = ren 

411 self.update_render_settings() 

412 self.update_camera() 

413 

414 renwin = self.vtk_widget.GetRenderWindow() 

415 

416 if self._use_depth_peeling: 

417 renwin.SetAlphaBitPlanes(1) 

418 renwin.SetMultiSamples(0) 

419 

420 ren.SetUseDepthPeeling(1) 

421 ren.SetMaximumNumberOfPeels(100) 

422 ren.SetOcclusionRatio(0.1) 

423 

424 ren.SetUseFXAA(1) 

425 # ren.SetUseHiddenLineRemoval(1) 

426 # ren.SetBackingStore(1) 

427 

428 self.renwin = renwin 

429 

430 # renwin.LineSmoothingOn() 

431 # renwin.PointSmoothingOn() 

432 # renwin.PolygonSmoothingOn() 

433 

434 renwin.AddRenderer(ren) 

435 

436 iren = renwin.GetInteractor() 

437 iren.LightFollowCameraOn() 

438 iren.SetInteractorStyle(None) 

439 

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

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

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

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

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

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

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

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

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

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

450 

451 renwin.Render() 

452 

453 iren.Initialize() 

454 

455 self.iren = iren 

456 

457 self.rotating = False 

458 

459 self._elements = {} 

460 self._elements_active = {} 

461 

462 self.talkie_connect( 

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

464 

465 self.state.elements.append(elements.IcosphereState( 

466 element_id='icosphere', 

467 level=4, 

468 smooth=True, 

469 opacity=0.5, 

470 ambient=0.1)) 

471 

472 self.state.elements.append(elements.GridState( 

473 element_id='grid')) 

474 self.state.elements.append(elements.CoastlinesState( 

475 element_id='coastlines')) 

476 self.state.elements.append(elements.CrosshairState( 

477 element_id='crosshair')) 

478 

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

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

481 # self.state.elements.append( 

482 # elements.CatalogState( 

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

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

485 

486 if events: 

487 self.state.elements.append( 

488 elements.CatalogState( 

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

490 

491 self.state.sort_elements() 

492 

493 if snapshots: 

494 snapshots_ = [] 

495 for obj in snapshots: 

496 if isinstance(obj, str): 

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

498 else: 

499 snapshots_.append(obj) 

500 

501 snapshots_panel.add_snapshots(snapshots_) 

502 self.raise_panel(snapshots_panel) 

503 snapshots_panel.goto_snapshot(1) 

504 

505 self.timer = qc.QTimer(self) 

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

507 self.timer.setInterval(1000) 

508 self.timer.start() 

509 

510 self._animation_saver = None 

511 

512 self.closing = False 

513 self.vtk_widget.setFocus() 

514 

515 self.update_detached() 

516 

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

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

519 

520 self.show() 

521 self.windowHandle().showMaximized() 

522 

523 self.talkie_connect( 

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

525 

526 self.update_vtk_widget_size() 

527 

528 def _add_vtk_widget_size_menu_entries(self, menu): 

529 

530 group = qw.QActionGroup(menu) 

531 group.setExclusive(True) 

532 

533 def set_variable_size(): 

534 self.gui_state.fixed_size = False 

535 

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

537 variable_size_action.setCheckable(True) 

538 variable_size_action.setActionGroup(group) 

539 variable_size_action.triggered.connect(set_variable_size) 

540 

541 fixed_size_items = [] 

542 for nx, ny, label in [ 

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

544 (426, 240, ''), 

545 (640, 360, ''), 

546 (854, 480, '(FWVGA)'), 

547 (1280, 720, '(HD)'), 

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

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

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

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

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

553 (640, 480, '(VGA)'), 

554 (800, 600, '(SVGA)'), 

555 (None, None, 'Other'), 

556 (512, 512, ''), 

557 (1024, 1024, '')]: 

558 

559 if None in (nx, ny): 

560 menu.addSection(label) 

561 else: 

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

563 action = menu.addAction(name) 

564 action.setCheckable(True) 

565 action.setActionGroup(group) 

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

567 

568 def make_set_fixed_size(nx, ny): 

569 def set_fixed_size(): 

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

571 

572 return set_fixed_size 

573 

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

575 

576 def update_widget(*args): 

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

578 action.blockSignals(True) 

579 action.setChecked( 

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

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

582 action.blockSignals(False) 

583 

584 variable_size_action.blockSignals(True) 

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

586 variable_size_action.blockSignals(False) 

587 

588 update_widget() 

589 self.talkie_connect( 

590 self.gui_state, 'fixed_size', update_widget) 

591 

592 def update_vtk_widget_size(self, *args): 

593 if self.gui_state.fixed_size: 

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

595 wanted_size = qc.QSize(nx, ny) 

596 else: 

597 wanted_size = qc.QSize( 

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

599 

600 current_size = self.vtk_widget.size() 

601 

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

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

604 

605 self.vtk_widget.setFixedSize(wanted_size) 

606 

607 self.vtk_frame.recenter() 

608 self.check_vtk_resize() 

609 

610 def update_focal_point(self, *args): 

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

612 self.vtk_widget.setStatusTip( 

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

614 'change view plane orientation.' % g_modifier_key) 

615 else: 

616 self.vtk_widget.setStatusTip( 

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

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

619 'reverse sense.' % g_modifier_key) 

620 

621 def update_detached(self, *args): 

622 

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

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

625 

626 self.main_layout.removeWidget(self.vtk_frame) 

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

628 self.detached_window.show() 

629 self.vtk_widget.setFocus() 

630 

631 screens = common.get_app().screens() 

632 if len(screens) > 1: 

633 for screen in screens: 

634 if screen is not self.screen(): 

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

636 # .setScreen() does not work reliably, 

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

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

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

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

641 

642 self.detached_window.windowHandle().showMaximized() 

643 

644 frame = qw.QFrame() 

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

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

647 # frame.setAutoFillBackground(True) 

648 frame.setSizePolicy( 

649 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

650 

651 layout = qw.QGridLayout() 

652 frame.setLayout(layout) 

653 self.main_layout.insertWidget(0, frame) 

654 

655 self.state_editor = StateEditor(self) 

656 

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

658 

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

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

661 # layout.addWidget( 

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

663 

664 self.vtk_frame_substitute = frame 

665 

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

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

668 self.detached_window.hide() 

669 self.vtk_frame.setParent(self) 

670 if self.vtk_frame_substitute: 

671 self.main_layout.removeWidget(self.vtk_frame_substitute) 

672 self.state_editor.unbind_state() 

673 self.vtk_frame_substitute = None 

674 

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

676 self.detached_window = None 

677 self.vtk_widget.setFocus() 

678 

679 def attach(self): 

680 self.gui_state.detached = False 

681 

682 def export_image(self): 

683 

684 caption = 'Export Image' 

685 fn_out, _ = qw.QFileDialog.getSaveFileName( 

686 self, caption, 'image.png', 

687 options=common.qfiledialog_options) 

688 

689 if fn_out: 

690 self.save_image(fn_out) 

691 

692 def save_image(self, path): 

693 

694 original_fixed_size = self.gui_state.fixed_size 

695 if original_fixed_size is None: 

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

697 

698 wif = vtk.vtkWindowToImageFilter() 

699 wif.SetInput(self.renwin) 

700 wif.SetInputBufferTypeToRGBA() 

701 wif.SetScale(1, 1) 

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

712 

713 def update_render_settings(self, *args): 

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

715 self.ren.RemoveAllLights() 

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

717 self.ren.AddLight(li) 

718 

719 self._lighting = self.state.lighting 

720 

721 if self._background is None \ 

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

723 

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

725 self._background = self.state.background 

726 

727 self.update_view() 

728 

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

730 self._animation = interpolator 

731 if output_path is None: 

732 self._animation_tstart = time.time() 

733 self._animation_iframe = None 

734 else: 

735 self._animation_iframe = 0 

736 self.showFullScreen() 

737 self.update_view() 

738 self.gui_state.panels_visible = False 

739 self.update_view() 

740 

741 self._animation_timer = qc.QTimer(self) 

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

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

744 self._animation_timer.start() 

745 if output_path is not None: 

746 original_fixed_size = self.gui_state.fixed_size 

747 if original_fixed_size is None: 

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

749 

750 wif = vtk.vtkWindowToImageFilter() 

751 wif.SetInput(self.renwin) 

752 wif.SetInputBufferTypeToRGBA() 

753 wif.SetScale(1, 1) 

754 wif.ReadFrontBufferOff() 

755 writer = vtk.vtkPNGWriter() 

756 temp_path = tempfile.mkdtemp() 

757 self._animation_saver = ( 

758 wif, writer, temp_path, output_path, original_fixed_size) 

759 writer.SetInputConnection(wif.GetOutputPort()) 

760 

761 def next_animation_frame(self): 

762 

763 ani = self._animation 

764 if not ani: 

765 return 

766 

767 if self._animation_iframe is not None: 

768 state = ani( 

769 ani.tmin 

770 + self._animation_iframe * ani.dt) 

771 

772 self._animation_iframe += 1 

773 else: 

774 tnow = time.time() 

775 state = ani(min( 

776 ani.tmax, 

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

778 

779 self.set_state(state) 

780 self.renwin.Render() 

781 if self._animation_saver: 

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

783 wif.Modified() 

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

785 writer.SetFileName(fn % self._animation_iframe) 

786 writer.Write() 

787 

788 if self._animation_iframe is not None: 

789 t = self._animation_iframe * ani.dt 

790 else: 

791 t = tnow - self._animation_tstart 

792 

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

794 self.stop_animation() 

795 

796 def stop_animation(self): 

797 if self._animation_timer: 

798 self._animation_timer.stop() 

799 

800 if self._animation_saver: 

801 

802 wif, writer, temp_path, output_path, original_fixed_size \ 

803 = self._animation_saver 

804 self.gui_state.fixed_size = original_fixed_size 

805 

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