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 menu = mbar.addMenu('Snapshots') 

397 menu.addAction( 

398 'Next', 

399 snapshots_panel.transition_to_next_snapshot, 

400 qg.QKeySequence(qc.Qt.Key_PageDown)).setShortcutContext( 

401 qc.Qt.ApplicationShortcut) 

402 

403 menu.addAction( 

404 'Previous', 

405 snapshots_panel.transition_to_previous_snapshot, 

406 qg.QKeySequence(qc.Qt.Key_PageUp)).setShortcutContext( 

407 qc.Qt.ApplicationShortcut) 

408 

409 self.setCentralWidget(self.main_frame) 

410 

411 self.mesh = None 

412 

413 ren = vtk.vtkRenderer() 

414 

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

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

417 # ren.TwoSidedLightingOn() 

418 # ren.SetUseShadows(1) 

419 

420 self._lighting = None 

421 self._background = None 

422 

423 self.ren = ren 

424 self.update_render_settings() 

425 self.update_camera() 

426 

427 renwin = self.vtk_widget.GetRenderWindow() 

428 

429 if self._use_depth_peeling: 

430 renwin.SetAlphaBitPlanes(1) 

431 renwin.SetMultiSamples(0) 

432 

433 ren.SetUseDepthPeeling(1) 

434 ren.SetMaximumNumberOfPeels(100) 

435 ren.SetOcclusionRatio(0.1) 

436 

437 ren.SetUseFXAA(1) 

438 # ren.SetUseHiddenLineRemoval(1) 

439 # ren.SetBackingStore(1) 

440 

441 self.renwin = renwin 

442 

443 # renwin.LineSmoothingOn() 

444 # renwin.PointSmoothingOn() 

445 # renwin.PolygonSmoothingOn() 

446 

447 renwin.AddRenderer(ren) 

448 

449 iren = renwin.GetInteractor() 

450 iren.LightFollowCameraOn() 

451 iren.SetInteractorStyle(None) 

452 

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

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

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

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

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

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

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

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

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

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

463 

464 renwin.Render() 

465 

466 iren.Initialize() 

467 

468 self.iren = iren 

469 

470 self.rotating = False 

471 

472 self._elements = {} 

473 self._elements_active = {} 

474 

475 self.talkie_connect( 

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

477 

478 self.state.elements.append(elements.IcosphereState( 

479 element_id='icosphere', 

480 level=4, 

481 smooth=True, 

482 opacity=0.5, 

483 ambient=0.1)) 

484 

485 self.state.elements.append(elements.GridState( 

486 element_id='grid')) 

487 self.state.elements.append(elements.CoastlinesState( 

488 element_id='coastlines')) 

489 self.state.elements.append(elements.CrosshairState( 

490 element_id='crosshair')) 

491 

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

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

494 # self.state.elements.append( 

495 # elements.CatalogState( 

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

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

498 

499 if events: 

500 self.state.elements.append( 

501 elements.CatalogState( 

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

503 

504 self.state.sort_elements() 

505 

506 if snapshots: 

507 snapshots_ = [] 

508 for obj in snapshots: 

509 if isinstance(obj, str): 

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

511 else: 

512 snapshots_.append(obj) 

513 

514 snapshots_panel.add_snapshots(snapshots_) 

515 self.raise_panel(snapshots_panel) 

516 snapshots_panel.goto_snapshot(1) 

517 

518 self.timer = qc.QTimer(self) 

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

520 self.timer.setInterval(1000) 

521 self.timer.start() 

522 

523 self._animation_saver = None 

524 

525 self.closing = False 

526 self.vtk_widget.setFocus() 

527 

528 self.update_detached() 

529 

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

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

532 

533 self.show() 

534 self.windowHandle().showMaximized() 

535 

536 self.talkie_connect( 

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

538 

539 self.update_vtk_widget_size() 

540 

541 def _add_vtk_widget_size_menu_entries(self, menu): 

542 

543 group = qw.QActionGroup(menu) 

544 group.setExclusive(True) 

545 

546 def set_variable_size(): 

547 self.gui_state.fixed_size = False 

548 

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

550 variable_size_action.setCheckable(True) 

551 variable_size_action.setActionGroup(group) 

552 variable_size_action.triggered.connect(set_variable_size) 

553 

554 fixed_size_items = [] 

555 for nx, ny, label in [ 

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

557 (426, 240, ''), 

558 (640, 360, ''), 

559 (854, 480, '(FWVGA)'), 

560 (1280, 720, '(HD)'), 

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

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

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

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

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

566 (640, 480, '(VGA)'), 

567 (800, 600, '(SVGA)'), 

568 (None, None, 'Other'), 

569 (512, 512, ''), 

570 (1024, 1024, '')]: 

571 

572 if None in (nx, ny): 

573 menu.addSection(label) 

574 else: 

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

576 action = menu.addAction(name) 

577 action.setCheckable(True) 

578 action.setActionGroup(group) 

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

580 

581 def make_set_fixed_size(nx, ny): 

582 def set_fixed_size(): 

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

584 

585 return set_fixed_size 

586 

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

588 

589 def update_widget(*args): 

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

591 action.blockSignals(True) 

592 action.setChecked( 

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

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

595 action.blockSignals(False) 

596 

597 variable_size_action.blockSignals(True) 

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

599 variable_size_action.blockSignals(False) 

600 

601 update_widget() 

602 self.talkie_connect( 

603 self.gui_state, 'fixed_size', update_widget) 

604 

605 def update_vtk_widget_size(self, *args): 

606 if self.gui_state.fixed_size: 

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

608 wanted_size = qc.QSize(nx, ny) 

609 else: 

610 wanted_size = qc.QSize( 

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

612 

613 current_size = self.vtk_widget.size() 

614 

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

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

617 

618 self.vtk_widget.setFixedSize(wanted_size) 

619 

620 self.vtk_frame.recenter() 

621 self.check_vtk_resize() 

622 

623 def update_focal_point(self, *args): 

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

625 self.vtk_widget.setStatusTip( 

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

627 'change view plane orientation.' % g_modifier_key) 

628 else: 

629 self.vtk_widget.setStatusTip( 

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

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

632 'reverse sense.' % g_modifier_key) 

633 

634 def update_detached(self, *args): 

635 

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

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

638 

639 self.main_layout.removeWidget(self.vtk_frame) 

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

641 self.detached_window.show() 

642 self.vtk_widget.setFocus() 

643 

644 screens = common.get_app().screens() 

645 if len(screens) > 1: 

646 for screen in screens: 

647 if screen is not self.screen(): 

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

649 # .setScreen() does not work reliably, 

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

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

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

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

654 

655 self.detached_window.windowHandle().showMaximized() 

656 

657 frame = qw.QFrame() 

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

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

660 # frame.setAutoFillBackground(True) 

661 frame.setSizePolicy( 

662 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

663 

664 layout = qw.QGridLayout() 

665 frame.setLayout(layout) 

666 self.main_layout.insertWidget(0, frame) 

667 

668 self.state_editor = StateEditor(self) 

669 

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

671 

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

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

674 # layout.addWidget( 

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

676 

677 self.vtk_frame_substitute = frame 

678 

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

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

681 self.detached_window.hide() 

682 self.vtk_frame.setParent(self) 

683 if self.vtk_frame_substitute: 

684 self.main_layout.removeWidget(self.vtk_frame_substitute) 

685 self.state_editor.unbind_state() 

686 self.vtk_frame_substitute = None 

687 

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

689 self.detached_window = None 

690 self.vtk_widget.setFocus() 

691 

692 def attach(self): 

693 self.gui_state.detached = False 

694 

695 def export_image(self): 

696 

697 caption = 'Export Image' 

698 fn_out, _ = qw.QFileDialog.getSaveFileName( 

699 self, caption, 'image.png', 

700 options=common.qfiledialog_options) 

701 

702 if fn_out: 

703 self.save_image(fn_out) 

704 

705 def save_image(self, path): 

706 

707 original_fixed_size = self.gui_state.fixed_size 

708 if original_fixed_size is None: 

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

710 

711 wif = vtk.vtkWindowToImageFilter() 

712 wif.SetInput(self.renwin) 

713 wif.SetInputBufferTypeToRGBA() 

714 wif.SetScale(1, 1) 

715 wif.ReadFrontBufferOff() 

716 writer = vtk.vtkPNGWriter() 

717 writer.SetInputConnection(wif.GetOutputPort()) 

718 

719 self.renwin.Render() 

720 wif.Modified() 

721 writer.SetFileName(path) 

722 writer.Write() 

723 

724 self.gui_state.fixed_size = original_fixed_size 

725 

726 def update_render_settings(self, *args): 

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

728 self.ren.RemoveAllLights() 

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

730 self.ren.AddLight(li) 

731 

732 self._lighting = self.state.lighting 

733 

734 if self._background is None \ 

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

736 

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

738 self._background = self.state.background 

739 

740 self.update_view() 

741 

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

743 self._animation = interpolator 

744 if output_path is None: 

745 self._animation_tstart = time.time() 

746 self._animation_iframe = None 

747 else: 

748 self._animation_iframe = 0 

749 self.showFullScreen() 

750 self.update_view() 

751 self.gui_state.panels_visible = False 

752 self.update_view() 

753 

754 self._animation_timer = qc.QTimer(self) 

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

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

757 self._animation_timer.start() 

758 if output_path is not None: 

759 original_fixed_size = self.gui_state.fixed_size 

760 if original_fixed_size is None: 

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

762 

763 wif = vtk.vtkWindowToImageFilter() 

764 wif.SetInput(self.renwin) 

765 wif.SetInputBufferTypeToRGBA() 

766 wif.SetScale(1, 1) 

767 wif.ReadFrontBufferOff() 

768 writer = vtk.vtkPNGWriter() 

769 temp_path = tempfile.mkdtemp() 

770 self._animation_saver = ( 

771 wif, writer, temp_path, output_path, original_fixed_size) 

772 writer.SetInputConnection(wif.GetOutputPort()) 

773 

774 def next_animation_frame(self): 

775 

776 ani = self._animation 

777 if not ani: 

778 return 

779 

780 if self._animation_iframe is not None: 

781 state = ani( 

782 ani.tmin 

783 + self._animation_iframe * ani.dt) 

784 

785 self._animation_iframe += 1 

786 else: 

787 tnow = time.time() 

788 state = ani(min( 

789 ani.tmax, 

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

791 

792 self.set_state(state) 

793 self.renwin.Render() 

794 if self._animation_saver: 

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

796 wif.Modified() 

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

798 writer.SetFileName(fn % self._animation_iframe) 

799 writer.Write() 

800 

801 if self._animation_iframe is not None: 

802 t = self._animation_iframe * ani.dt 

803 else: 

804 t = tnow - self._animation_tstart 

805 

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

807 self.stop_animation() 

808 

809 def stop_animation(self): 

810 if self._animation_timer: 

811 self._animation_timer.stop() 

812 

813 if self._animation_saver: 

814 

815 wif, writer, temp_path, output_path, original_fixed_size \ 

816 = self._animation_saver 

817 self.gui_state.fixed_size = original_fixed_size 

818 

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

820 check_call([ 

821 'ffmpeg', '-y', 

822 '-i', fn_path, 

823 '-c:v', 'libx264', 

824 '-preset', 'slow', 

825 '-crf', '17', 

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

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

828 output_path]) 

829 shutil.rmtree(temp_path) 

830 

831 self._animation_saver = None 

832 self._animation_saver 

833 

834 self.showNormal() 

835 self.gui_state.panels_visible = True 

836 

837 self._animation_tstart = None 

838 self._animation_iframe = None 

839 self._animation = None 

840 

841 def set_state(self, state): 

842 self._update_elements_enabled = False 

843 self.setUpdatesEnabled(False) 

844 self.state.diff_update(state) 

845 self.state.sort_elements() 

846 self.setUpdatesEnabled(True) 

847 self._update_elements_enabled = True 

848 self.update_elements() 

849 

850 def periodical(self): 

851 pass 

852 

853 def request_quit(self): 

854 app = common.get_app() 

855 app.myQuit() 

856 

857 def check_vtk_resize(self, *args): 

858 render_window_size = self.renwin.GetSize() 

859 if self._render_window_size != render_window_size: 

860 self._render_window_size = render_window_size 

861 self.resize_event(*render_window_size) 

862 

863 def update_elements(self, *_): 

864 if not self._update_elements_enabled: 

865 return 

866 

867 if self._in_update_elements: 

868 return 

869 

870 self._in_update_elements = True 

871 for estate in self.state.elements: 

872 if estate.element_id not in self._elements: 

873 new_element = estate.create() 

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

875 type(new_element).__name__, 

876 estate.element_id)) 

877 self._elements[estate.element_id] = new_element 

878 

879 element = self._elements[estate.element_id] 

880 

881 if estate.element_id not in self._elements_active: 

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

883 type(element).__name__, 

884 estate.element_id)) 

885 element.bind_state(estate) 

886 element.set_parent(self) 

887 self._elements_active[estate.element_id] = element 

888 

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

890 deactivate = [] 

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

892 if element_id not in state_element_ids: 

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

894 type(element).__name__, 

895 element_id)) 

896 element.unset_parent() 

897 deactivate.append(element_id) 

898 

899 for element_id in deactivate: 

900 del self._elements_active[element_id] 

901 

902 self._update_crosshair_bindings() 

903 

904 self._in_update_elements = False 

905 

906 def _update_crosshair_bindings(self): 

907 

908 def get_crosshair_element(): 

909 for element in self.state.elements: 

910 if element.element_id == 'crosshair': 

911 return element 

912 

913 return None 

914 

915 crosshair = get_crosshair_element() 

916 if crosshair is None or crosshair.is_connected: 

917 return 

918 

919 def to_checkbox(state, widget): 

920 widget.blockSignals(True) 

921 widget.setChecked(state.visible) 

922 widget.blockSignals(False) 

923 

924 def to_state(widget, state): 

925 state.visible = widget.isChecked() 

926 

927 cb = self._crosshair_checkbox 

928 vstate.state_bind( 

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

930 cb, [cb.toggled], to_checkbox) 

931 

932 crosshair.is_connected = True 

933 

934 def add_actor_2d(self, actor): 

935 if actor not in self._actors_2d: 

936 self.ren.AddActor2D(actor) 

937 self._actors_2d.add(actor) 

938 

939 def remove_actor_2d(self, actor): 

940 if actor in self._actors_2d: 

941 self.ren.RemoveActor2D(actor) 

942 self._actors_2d.remove(actor) 

943 

944 def add_actor(self, actor): 

945 if actor not in self._actors: 

946 self.ren.AddActor(actor) 

947 self._actors.add(actor) 

948 

949 def add_actor_list(self, actorlist): 

950 for actor in actorlist: 

951 self.add_actor(actor) 

952 

953 def remove_actor(self, actor): 

954 if actor in self._actors: 

955 self.ren.RemoveActor(actor) 

956 self._actors.remove(actor) 

957 

958 def update_view(self): 

959 self.vtk_widget.update() 

960 

961 def resize_event(self, size_x, size_y): 

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

963 

964 def button_event(self, obj, event): 

965 if event == "LeftButtonPressEvent": 

966 self.rotating = True 

967 elif event == "LeftButtonReleaseEvent": 

968 self.rotating = False 

969 

970 def mouse_move_event(self, obj, event): 

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

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

973 

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

975 center_x = size_x / 2.0 

976 center_y = size_y / 2.0 

977 

978 if self.rotating: 

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

980 

981 def myWheelEvent(self, event): 

982 

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

984 

985 if angle > 200: 

986 angle = 200 

987 

988 if angle < -200: 

989 angle = -200 

990 

991 self.do_dolly(-angle/100.) 

992 

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

994 

995 dx = x0 - x 

996 dy = y0 - y 

997 

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

999 focp = self.gui_state.focal_point 

1000 

1001 if focp == 'center': 

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

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

1004 

1005 lat = self.state.lat 

1006 lon = self.state.lon 

1007 factor = self.state.distance / 10.0 

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

1009 else: 

1010 lat = 90. - self.state.dip 

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

1012 factor = 0.5 

1013 factor_lat = 1.0 

1014 

1015 dlat = dy * factor 

1016 dlon = dx * factor * factor_lat 

1017 

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

1019 lon += dlon 

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

1021 

1022 if focp == 'center': 

1023 self.state.lat = float(lat) 

1024 self.state.lon = float(lon) 

1025 else: 

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

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

1028 

1029 def do_dolly(self, v): 

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

1031 

1032 def key_down_event(self, obj, event): 

1033 k = obj.GetKeyCode() 

1034 s = obj.GetKeySym() 

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

1036 self.gui_state.next_focal_point() 

1037 

1038 elif k == 'r': 

1039 self.reset_strike_dip() 

1040 

1041 elif k == 'p': 

1042 print(self.state) 

1043 

1044 elif k == 'i': 

1045 for elem in self.state.elements: 

1046 if isinstance(elem, elements.IcosphereState): 

1047 elem.visible = not elem.visible 

1048 

1049 elif k == 'c': 

1050 for elem in self.state.elements: 

1051 if isinstance(elem, elements.CoastlinesState): 

1052 elem.visible = not elem.visible 

1053 

1054 elif k == 't': 

1055 if not any( 

1056 isinstance(elem, elements.TopoState) 

1057 for elem in self.state.elements): 

1058 

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

1060 else: 

1061 for elem in self.state.elements: 

1062 if isinstance(elem, elements.TopoState): 

1063 elem.visible = not elem.visible 

1064 

1065 elif k == ' ': 

1066 self.toggle_panel_visibility() 

1067 

1068 def key_up_event(self, obj, event): 

1069 s = obj.GetKeySym() 

1070 if s == 'Control_L': 

1071 self.gui_state.next_focal_point() 

1072 

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

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

1075 

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

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

1078 

1079 def controls_navigation(self): 

1080 frame = qw.QFrame(self) 

1081 frame.setSizePolicy( 

1082 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1083 layout = qw.QGridLayout() 

1084 frame.setLayout(layout) 

1085 

1086 # lat, lon, depth 

1087 

1088 layout.addWidget( 

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

1090 

1091 le = qw.QLineEdit() 

1092 le.setStatusTip( 

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

1094 'Focal point location.') 

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

1096 

1097 def lat_lon_depth_to_lineedit(state, widget): 

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

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

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

1101 

1102 if sel: 

1103 widget.selectAll() 

1104 

1105 def lineedit_to_lat_lon_depth(widget, state): 

1106 s = str(widget.text()) 

1107 choices = location_to_choices(s) 

1108 if len(choices) > 0: 

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

1110 choices[0].get_lat_lon_depth() 

1111 else: 

1112 raise NoLocationChoices(s) 

1113 

1114 self._state_bind( 

1115 ['lat', 'lon', 'depth'], 

1116 lineedit_to_lat_lon_depth, 

1117 le, [le.editingFinished, le.returnPressed], 

1118 lat_lon_depth_to_lineedit) 

1119 

1120 self.lat_lon_lineedit = le 

1121 

1122 self.lat_lon_lineedit.returnPressed.connect( 

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

1124 

1125 # focal point 

1126 

1127 cb = qw.QCheckBox('Fix') 

1128 cb.setStatusTip( 

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

1130 % g_modifier_key) 

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

1132 

1133 def focal_point_to_checkbox(state, widget): 

1134 widget.blockSignals(True) 

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

1136 widget.blockSignals(False) 

1137 

1138 def checkbox_to_focal_point(widget, state): 

1139 self.gui_state.focal_point = \ 

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

1141 

1142 self._gui_state_bind( 

1143 ['focal_point'], checkbox_to_focal_point, 

1144 cb, [cb.toggled], focal_point_to_checkbox) 

1145 

1146 self.focal_point_checkbox = cb 

1147 

1148 self.talkie_connect( 

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

1150 

1151 self.update_focal_point() 

1152 

1153 # strike, dip 

1154 

1155 layout.addWidget( 

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

1157 

1158 le = qw.QLineEdit() 

1159 le.setStatusTip( 

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

1161 'direction.') 

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

1163 

1164 def strike_dip_to_lineedit(state, widget): 

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

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

1167 if sel: 

1168 widget.selectAll() 

1169 

1170 def lineedit_to_strike_dip(widget, state): 

1171 s = str(widget.text()) 

1172 string_to_strike_dip = { 

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

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

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

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

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

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

1179 

1180 if s in string_to_strike_dip: 

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

1182 

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

1184 try: 

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

1186 except Exception: 

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

1188 

1189 self._state_bind( 

1190 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1192 

1193 self.strike_dip_lineedit = le 

1194 self.strike_dip_lineedit.returnPressed.connect( 

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

1196 

1197 but = qw.QPushButton('Reset') 

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

1199 but.clicked.connect(self.reset_strike_dip) 

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

1201 

1202 # crosshair 

1203 

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

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

1206 

1207 # camera bindings 

1208 self.talkie_connect( 

1209 self.state, 

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

1211 self.update_camera) 

1212 

1213 self.talkie_connect( 

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

1215 

1216 return frame 

1217 

1218 def controls_time(self): 

1219 frame = qw.QFrame(self) 

1220 frame.setSizePolicy( 

1221 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1222 

1223 layout = qw.QGridLayout() 

1224 frame.setLayout(layout) 

1225 

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

1227 le_tmin = qw.QLineEdit() 

1228 layout.addWidget(le_tmin, 0, 1) 

1229 

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

1231 le_tmax = qw.QLineEdit() 

1232 layout.addWidget(le_tmax, 1, 1) 

1233 

1234 label_tcursor = qw.QLabel() 

1235 

1236 label_tcursor.setSizePolicy( 

1237 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1238 

1239 layout.addWidget(label_tcursor, 2, 1) 

1240 self._label_tcursor = label_tcursor 

1241 

1242 def time_to_lineedit(state, attribute, widget): 

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

1244 and widget.text() != '' 

1245 

1246 widget.setText( 

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

1248 

1249 if sel: 

1250 widget.selectAll() 

1251 

1252 def lineedit_to_time(widget, state, attribute): 

1253 from pyrocko.util import str_to_time_fillup 

1254 

1255 s = str(widget.text()) 

1256 if not s.strip(): 

1257 setattr(state, attribute, None) 

1258 else: 

1259 try: 

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

1261 except Exception: 

1262 raise ValueError( 

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

1264 

1265 self._state_bind( 

1266 ['tmin'], lineedit_to_time, le_tmin, 

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

1268 attribute='tmin') 

1269 self._state_bind( 

1270 ['tmax'], lineedit_to_time, le_tmax, 

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

1272 attribute='tmax') 

1273 

1274 self.tmin_lineedit = le_tmin 

1275 self.tmax_lineedit = le_tmax 

1276 

1277 range_edit = RangeEdit() 

1278 range_edit.set_data_provider(self) 

1279 range_edit.set_data_name('time') 

1280 

1281 xblock = [False] 

1282 

1283 def range_to_range_edit(state, widget): 

1284 if not xblock[0]: 

1285 widget.blockSignals(True) 

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

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

1288 widget.blockSignals(False) 

1289 

1290 def range_edit_to_range(widget, state): 

1291 xblock[0] = True 

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

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

1294 xblock[0] = False 

1295 

1296 self._state_bind( 

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

1298 range_edit_to_range, 

1299 range_edit, 

1300 [range_edit.rangeChanged, range_edit.focusChanged], 

1301 range_to_range_edit) 

1302 

1303 def handle_tcursor_changed(): 

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

1305 

1306 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1307 

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

1309 

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

1311 le_focus = qw.QLineEdit() 

1312 layout.addWidget(le_focus, 4, 1) 

1313 

1314 def focus_to_lineedit(state, widget): 

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

1316 and widget.text() != '' 

1317 

1318 if state.tduration is None: 

1319 widget.setText('') 

1320 else: 

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

1322 guts.str_duration(state.tduration), 

1323 state.tposition)) 

1324 

1325 if sel: 

1326 widget.selectAll() 

1327 

1328 def lineedit_to_focus(widget, state): 

1329 s = str(widget.text()) 

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

1331 try: 

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

1333 state.tduration = None 

1334 state.tposition = 0.0 

1335 else: 

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

1337 if len(w) > 1: 

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

1339 else: 

1340 state.tposition = 0.0 

1341 

1342 except Exception: 

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

1344 

1345 self._state_bind( 

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

1347 [le_focus.editingFinished, le_focus.returnPressed], 

1348 focus_to_lineedit) 

1349 

1350 label_effective_tmin = qw.QLabel() 

1351 label_effective_tmax = qw.QLabel() 

1352 

1353 label_effective_tmin.setSizePolicy( 

1354 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1355 label_effective_tmax.setSizePolicy( 

1356 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1357 label_effective_tmin.setMinimumSize( 

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

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

1360 

1361 layout.addWidget(label_effective_tmin, 5, 1) 

1362 layout.addWidget(label_effective_tmax, 6, 1) 

1363 

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

1365 self.talkie_connect( 

1366 self.state, var, self.update_effective_time_labels) 

1367 

1368 self._label_effective_tmin = label_effective_tmin 

1369 self._label_effective_tmax = label_effective_tmax 

1370 

1371 self.talkie_connect( 

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

1373 

1374 return frame 

1375 

1376 def controls_appearance(self): 

1377 frame = qw.QFrame(self) 

1378 frame.setSizePolicy( 

1379 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1380 layout = qw.QGridLayout() 

1381 frame.setLayout(layout) 

1382 

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

1384 

1385 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1386 layout.addWidget(cb, 0, 1) 

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

1388 

1389 self.talkie_connect( 

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

1391 

1392 # background 

1393 

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

1395 

1396 cb = common.strings_to_combobox( 

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

1398 

1399 layout.addWidget(cb, 1, 1) 

1400 vstate.state_bind_combobox_background( 

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

1402 

1403 self.talkie_connect( 

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

1405 

1406 return frame 

1407 

1408 def controls_snapshots(self): 

1409 return snapshots_mod.SnapshotsPanel(self) 

1410 

1411 def update_effective_time_labels(self, *args): 

1412 tmin = self.state.tmin_effective 

1413 tmax = self.state.tmax_effective 

1414 

1415 stmin = common.time_or_none_to_str(tmin) 

1416 stmax = common.time_or_none_to_str(tmax) 

1417 

1418 self._label_effective_tmin.setText(stmin) 

1419 self._label_effective_tmax.setText(stmax) 

1420 

1421 def update_tcursor(self, *args): 

1422 tcursor = self.gui_state.tcursor 

1423 stcursor = common.time_or_none_to_str(tcursor) 

1424 self._label_tcursor.setText(stcursor) 

1425 

1426 def reset_strike_dip(self, *args): 

1427 self.state.strike = 90. 

1428 self.state.dip = 0 

1429 self.gui_state.focal_point = 'center' 

1430 

1431 def get_camera_geometry(self): 

1432 

1433 def rtp2xyz(rtp): 

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

1435 

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

1437 

1438 cam_rtp = num.array([ 

1439 radius+self.state.distance, 

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

1441 self.state.lon * d2r]) 

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

1443 cam, up, foc = \ 

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

1445 

1446 foc_rtp = num.array([ 

1447 radius, 

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

1449 self.state.lon * d2r]) 

1450 

1451 foc = rtp2xyz(foc_rtp) 

1452 

1453 rot_world = pmt.euler_to_matrix( 

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

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

1456 0.0*d2r).T 

1457 

1458 rot_cam = pmt.euler_to_matrix( 

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

1460 

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

1462 

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

1464 up = num.dot(rot, up) 

1465 return cam, up, foc 

1466 

1467 def update_camera(self, *args): 

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

1469 camera = self.ren.GetActiveCamera() 

1470 camera.SetPosition(*cam) 

1471 camera.SetFocalPoint(*foc) 

1472 camera.SetViewUp(*up) 

1473 

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

1475 

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

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

1478 

1479 # if horizon == 0.0: 

1480 # horizon = 2.0 + self.state.distance 

1481 

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

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

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

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

1486 # clip_dist = feature_horizon 

1487 

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

1489 

1490 self.camera_params = ( 

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

1492 

1493 self.update_view() 

1494 

1495 def add_panel( 

1496 self, title_label, panel, 

1497 visible=False, 

1498 # volatile=False, 

1499 tabify=True, 

1500 where=qc.Qt.RightDockWidgetArea, 

1501 remove=None, 

1502 title_controls=[]): 

1503 

1504 dockwidget = common.MyDockWidget( 

1505 self, title_label, title_controls=title_controls) 

1506 

1507 if not visible: 

1508 dockwidget.hide() 

1509 

1510 if not self.gui_state.panels_visible: 

1511 dockwidget.block() 

1512 

1513 dockwidget.setWidget(panel) 

1514 

1515 panel.setParent(dockwidget) 

1516 

1517 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1519 

1520 self.addDockWidget(where, dockwidget) 

1521 

1522 nwrap = 4 

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

1524 self.tabifyDockWidget( 

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

1526 

1527 mitem = dockwidget.toggleViewAction() 

1528 

1529 def update_label(*args): 

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

1531 self.update_slug_abbreviated_lengths() 

1532 

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

1534 dockwidget.titlebar._title_label.title_changed.connect( 

1535 self.update_slug_abbreviated_lengths) 

1536 

1537 update_label() 

1538 

1539 self._panel_togglers[dockwidget] = mitem 

1540 self.panels_menu.addAction(mitem) 

1541 if visible: 

1542 dockwidget.setVisible(True) 

1543 dockwidget.setFocus() 

1544 dockwidget.raise_() 

1545 

1546 def update_slug_abbreviated_lengths(self): 

1547 dockwidgets = self.findChildren(common.MyDockWidget) 

1548 title_labels = [] 

1549 for dw in dockwidgets: 

1550 title_labels.append(dw.titlebar._title_label) 

1551 

1552 by_title = defaultdict(list) 

1553 for tl in title_labels: 

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

1555 

1556 for group in by_title.values(): 

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

1558 

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

1560 nunique = len(set(slugs)) 

1561 

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

1563 n -= 1 

1564 

1565 if n > 0: 

1566 n = max(3, n) 

1567 

1568 for tl in group: 

1569 tl.set_slug_abbreviated_length(n) 

1570 

1571 def raise_panel(self, panel): 

1572 dockwidget = panel.parent() 

1573 dockwidget.setVisible(True) 

1574 dockwidget.setFocus() 

1575 dockwidget.raise_() 

1576 

1577 def toggle_panel_visibility(self): 

1578 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1579 

1580 def update_panel_visibility(self, *args): 

1581 self.setUpdatesEnabled(False) 

1582 mbar = self.menuBar() 

1583 sbar = self.statusBar() 

1584 dockwidgets = self.findChildren(common.MyDockWidget) 

1585 

1586 mbar.setVisible(self.gui_state.panels_visible) 

1587 sbar.setVisible(self.gui_state.panels_visible) 

1588 for dockwidget in dockwidgets: 

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

1590 

1591 self.setUpdatesEnabled(True) 

1592 

1593 def remove_panel(self, panel): 

1594 dockwidget = panel.parent() 

1595 self.removeDockWidget(dockwidget) 

1596 dockwidget.setParent(None) 

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

1598 

1599 def register_data_provider(self, provider): 

1600 if provider not in self.data_providers: 

1601 self.data_providers.append(provider) 

1602 

1603 def unregister_data_provider(self, provider): 

1604 if provider in self.data_providers: 

1605 self.data_providers.remove(provider) 

1606 

1607 def iter_data(self, name): 

1608 for provider in self.data_providers: 

1609 for data in provider.iter_data(name): 

1610 yield data 

1611 

1612 def closeEvent(self, event): 

1613 self.attach() 

1614 event.accept() 

1615 self.closing = True 

1616 common.get_app().set_main_window(None) 

1617 

1618 def is_closing(self): 

1619 return self.closing 

1620 

1621 

1622class SparrowApp(qw.QApplication): 

1623 def __init__(self): 

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

1625 self.lastWindowClosed.connect(self.myQuit) 

1626 self._main_window = None 

1627 self.setApplicationDisplayName('Sparrow') 

1628 self.setDesktopFileName('Sparrow') 

1629 

1630 def install_sigint_handler(self): 

1631 self._old_signal_handler = signal.signal( 

1632 signal.SIGINT, self.myCloseAllWindows) 

1633 

1634 def uninstall_sigint_handler(self): 

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

1636 

1637 def myQuit(self, *args): 

1638 self.quit() 

1639 

1640 def myCloseAllWindows(self, *args): 

1641 self.closeAllWindows() 

1642 

1643 def set_main_window(self, win): 

1644 self._main_window = win 

1645 

1646 def get_main_window(self): 

1647 return self._main_window 

1648 

1649 def get_progressbars(self): 

1650 if self._main_window: 

1651 return self._main_window.progressbars 

1652 else: 

1653 return None 

1654 

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

1656 win = self.get_main_window() 

1657 if not win: 

1658 return 

1659 

1660 win.statusBar().showMessage( 

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

1662 

1663 

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

1665 

1666 from pyrocko import util 

1667 from pyrocko.gui import util as gui_util 

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

1669 

1670 global win 

1671 

1672 if gui_util.app is None: 

1673 gui_util.app = SparrowApp() 

1674 

1675 # try: 

1676 # from qt_material import apply_stylesheet 

1677 # 

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

1679 # 

1680 # 

1681 # import qdarkgraystyle 

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

1683 # import qdarkstyle 

1684 # 

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

1686 # 

1687 # 

1688 # except ImportError: 

1689 # logger.info( 

1690 # 'Module qdarkgraystyle not available.\n' 

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

1692 # 'qdarkgraystyle".') 

1693 # 

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

1695 

1696 gui_util.app.install_sigint_handler() 

1697 gui_util.app.exec_() 

1698 gui_util.app.uninstall_sigint_handler() 

1699 

1700 del win 

1701 

1702 gc.collect() 

1703 

1704 del gui_util.app