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 config 

24from pyrocko import moment_tensor as pmt 

25from pyrocko import util 

26 

27from pyrocko.gui.util import Progressbars, RangeEdit 

28from pyrocko.gui.talkie import TalkieConnectionOwner, equal as state_equal 

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

30# from pyrocko.gui import vtk_util 

31 

32from . import common, light, snapshots as snapshots_mod 

33 

34import vtk 

35import vtk.qt 

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

37 

38from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

39 

40from pyrocko import geometry # noqa 

41from . import state as vstate, elements # noqa 

42 

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

44 

45 

46d2r = num.pi/180. 

47km = 1000. 

48 

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

50 g_modifier_key = '\u2318' 

51else: 

52 g_modifier_key = 'Ctrl' 

53 

54 

55class ZeroFrame(qw.QFrame): 

56 

57 def sizeHint(self): 

58 return qc.QSize(0, 0) 

59 

60 

61class LocationChoice(object): 

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

63 self._name = name 

64 self._lat = lat 

65 self._lon = lon 

66 self._depth = depth 

67 

68 def get_lat_lon_depth(self): 

69 return self._lat, self._lon, self._depth 

70 

71 

72def location_to_choices(s): 

73 choices = [] 

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

75 try: 

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

77 if len(vals) == 3: 

78 vals[2] *= km 

79 

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

81 

82 except ValueError: 

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

84 for c in cities: 

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

86 

87 return choices 

88 

89 

90class NoLocationChoices(Exception): 

91 

92 def __init__(self, s): 

93 self._string = s 

94 

95 def __str__(self): 

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

97 

98 

99class QVTKWidget(QVTKRenderWindowInteractor): 

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

101 QVTKRenderWindowInteractor.__init__(self, *args) 

102 self._viewer = viewer 

103 self._ctrl_state = False 

104 

105 def wheelEvent(self, event): 

106 return self._viewer.myWheelEvent(event) 

107 

108 def keyPressEvent(self, event): 

109 if event.key() == qc.Qt.Key_Control: 

110 self._update_ctrl_state(True) 

111 QVTKRenderWindowInteractor.keyPressEvent(self, event) 

112 

113 def keyReleaseEvent(self, event): 

114 if event.key() == qc.Qt.Key_Control: 

115 self._update_ctrl_state(False) 

116 QVTKRenderWindowInteractor.keyReleaseEvent(self, event) 

117 

118 def focusInEvent(self, event): 

119 self._update_ctrl_state() 

120 QVTKRenderWindowInteractor.focusInEvent(self, event) 

121 

122 def focusOutEvent(self, event): 

123 self._update_ctrl_state(False) 

124 QVTKRenderWindowInteractor.focusOutEvent(self, event) 

125 

126 def mousePressEvent(self, event): 

127 self._viewer.disable_capture() 

128 QVTKRenderWindowInteractor.mousePressEvent(self, event) 

129 

130 def mouseReleaseEvent(self, event): 

131 self._viewer.enable_capture() 

132 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event) 

133 

134 def _update_ctrl_state(self, state=None): 

135 if state is None: 

136 app = common.get_app() 

137 if not app: 

138 return 

139 state = app.keyboardModifiers() == qc.Qt.ControlModifier 

140 if self._ctrl_state != state: 

141 self._viewer.gui_state.next_focal_point() 

142 self._ctrl_state = state 

143 

144 def container_resized(self, ev): 

145 self._viewer.update_vtk_widget_size() 

146 

147 

148class DetachedViewer(qw.QMainWindow): 

149 

150 def __init__(self, main_window, vtk_frame): 

151 qw.QMainWindow.__init__(self, main_window) 

152 self.main_window = main_window 

153 self.setWindowTitle('Sparrow View') 

154 vtk_frame.setParent(self) 

155 self.setCentralWidget(vtk_frame) 

156 

157 def closeEvent(self, ev): 

158 ev.ignore() 

159 self.main_window.attach() 

160 

161 

162class CenteringScrollArea(qw.QScrollArea): 

163 def __init__(self): 

164 qw.QScrollArea.__init__(self) 

165 self.setAlignment(qc.Qt.AlignCenter) 

166 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

167 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

168 self.setFrameShape(qw.QFrame.NoFrame) 

169 

170 def resizeEvent(self, ev): 

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

172 self.widget().container_resized(ev) 

173 return retval 

174 

175 def recenter(self): 

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

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

178 

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

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

181 

182 

183class YAMLEditor(qw.QTextEdit): 

184 

185 def __init__(self, parent): 

186 qw.QTextEdit.__init__(self) 

187 self._parent = parent 

188 

189 def event(self, ev): 

190 if isinstance(ev, qg.QKeyEvent) \ 

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

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

193 self._parent.state_changed() 

194 return True 

195 

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

197 

198 

199class StateEditor(qw.QFrame, TalkieConnectionOwner): 

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

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

202 TalkieConnectionOwner.__init__(self) 

203 

204 layout = qw.QGridLayout() 

205 

206 self.setLayout(layout) 

207 

208 self.source_editor = YAMLEditor(self) 

209 self.source_editor.setAcceptRichText(False) 

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

211 font = qg.QFont("Monospace") 

212 self.source_editor.setCurrentFont(font) 

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

214 

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

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

217 

218 self.error_display = qw.QTextEdit() 

219 self.error_display.setCurrentFont(font) 

220 self.error_display.setReadOnly(True) 

221 

222 self.error_display.setSizePolicy( 

223 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

224 

225 self.error_display_label.hide() 

226 self.error_display.hide() 

227 

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

229 

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

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

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

233 

234 button = qw.QPushButton('Apply') 

235 button.clicked.connect(self.state_changed) 

236 layout.addWidget(button, 3, 1) 

237 

238 self.viewer = viewer 

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

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

241 self.bind_state() 

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

243 self.destroyed.connect(self.unbind_state) 

244 self.bind_state() 

245 

246 def bind_state(self, *args): 

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

248 self.update_state() 

249 

250 def unbind_state(self): 

251 self.talkie_disconnect_all() 

252 

253 def update_state(self, *args): 

254 cursor = self.source_editor.textCursor() 

255 

256 cursor_position = cursor.position() 

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

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

259 

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

261 

262 cursor.setPosition(cursor_position) 

263 self.source_editor.setTextCursor(cursor) 

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

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

266 

267 def text_changed_handler(self, *args): 

268 if self.instant_updates.isChecked(): 

269 self.state_changed() 

270 

271 def state_changed(self): 

272 try: 

273 s = self.source_editor.toPlainText() 

274 state = guts.load(string=s) 

275 self.viewer.set_state(state) 

276 self.error_display.setPlainText('') 

277 self.error_display_label.hide() 

278 self.error_display.hide() 

279 

280 except Exception as e: 

281 self.error_display.show() 

282 self.error_display_label.show() 

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

284 

285 

286class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

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

288 qw.QMainWindow.__init__(self) 

289 TalkieConnectionOwner.__init__(self) 

290 

291 common.get_app().set_main_window(self) 

292 

293 self.state = vstate.ViewerState() 

294 self.gui_state = vstate.ViewerGuiState() 

295 

296 self.setWindowTitle('Sparrow') 

297 

298 self.setTabPosition( 

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

300 

301 self.planet_radius = cake.earthradius 

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

303 

304 self._block_capture = 0 

305 self._undo_stack = [] 

306 self._redo_stack = [] 

307 self._undo_aggregate = None 

308 

309 self._panel_togglers = {} 

310 self._actors = set() 

311 self._actors_2d = set() 

312 self._render_window_size = (0, 0) 

313 self._use_depth_peeling = use_depth_peeling 

314 self._in_update_elements = False 

315 self._update_elements_enabled = True 

316 

317 self._animation_tstart = None 

318 self._animation_iframe = None 

319 self._animation = None 

320 

321 mbar = qw.QMenuBar() 

322 self.setMenuBar(mbar) 

323 

324 menu = mbar.addMenu('File') 

325 

326 menu.addAction( 

327 'Export Image...', 

328 self.export_image, 

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

330 qc.Qt.ApplicationShortcut) 

331 

332 menu.addAction( 

333 'Quit', 

334 self.request_quit, 

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

336 qc.Qt.ApplicationShortcut) 

337 

338 menu = mbar.addMenu('Edit') 

339 

340 menu.addAction( 

341 'Undo', 

342 self.undo, 

343 qg.QKeySequence( 

344 qc.Qt.CTRL | qc.Qt.Key_Z)).setShortcutContext( 

345 qc.Qt.ApplicationShortcut) 

346 

347 menu.addAction( 

348 'Redo', 

349 self.redo, 

350 qg.QKeySequence( 

351 qc.Qt.CTRL | qc.Qt.SHIFT | qc.Qt.Key_Z)).setShortcutContext( 

352 qc.Qt.ApplicationShortcut) 

353 

354 menu = mbar.addMenu('View') 

355 menu_sizes = menu.addMenu('Size') 

356 self._add_vtk_widget_size_menu_entries(menu_sizes) 

357 

358 # detached/attached 

359 self.talkie_connect( 

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

361 

362 action = qw.QAction('Detach') 

363 action.setCheckable(True) 

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

365 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

366 

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

368 menu.addAction(action) 

369 

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

371 self.panels_menu.addAction( 

372 'Stack Panels', 

373 self.stack_panels) 

374 self.panels_menu.addSeparator() 

375 

376 snapshots_menu = mbar.addMenu('Snapshots') 

377 

378 menu = mbar.addMenu('Elements') 

379 for name, estate in sorted([ 

380 ('Icosphere', elements.IcosphereState( 

381 level=4, 

382 smooth=True, 

383 opacity=0.5, 

384 ambient=0.1)), 

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

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

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

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

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

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

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

392 ('HUD Subtitle', elements.HudState( 

393 template='Subtitle')), 

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

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

396 position='top-left')), 

397 ('AxesBox', elements.AxesBoxState()), 

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

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

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

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

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

403 ('Spheroid', elements.SpheroidState())]): 

404 

405 def wrap_add_element(estate): 

406 def add_element(*args): 

407 new_element = guts.clone(estate) 

408 new_element.element_id = elements.random_id() 

409 self.state.elements.append(new_element) 

410 self.state.sort_elements() 

411 

412 return add_element 

413 

414 mitem = qw.QAction(name, self) 

415 

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

417 

418 menu.addAction(mitem) 

419 

420 menu = mbar.addMenu('Help') 

421 

422 menu.addAction( 

423 'Interactive Tour', 

424 self.start_tour) 

425 

426 menu.addAction( 

427 'Online Manual', 

428 self.open_manual) 

429 

430 self.data_providers = [] 

431 self.elements = {} 

432 

433 self.detached_window = None 

434 

435 self.main_frame = qw.QFrame() 

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

437 

438 self.vtk_frame = CenteringScrollArea() 

439 

440 self.vtk_widget = QVTKWidget(self, self) 

441 self.vtk_frame.setWidget(self.vtk_widget) 

442 

443 self.main_layout = qw.QVBoxLayout() 

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

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

446 

447 pb = Progressbars(self) 

448 self.progressbars = pb 

449 self.main_layout.addWidget(pb) 

450 

451 self.main_frame.setLayout(self.main_layout) 

452 

453 self.vtk_frame_substitute = None 

454 

455 self.add_panel( 

456 'Navigation', 

457 self.controls_navigation(), visible=True, 

458 where=qc.Qt.LeftDockWidgetArea) 

459 

460 self.add_panel( 

461 'Time', 

462 self.controls_time(), visible=True, 

463 where=qc.Qt.LeftDockWidgetArea) 

464 

465 self.add_panel( 

466 'Appearance', 

467 self.controls_appearance(), visible=True, 

468 where=qc.Qt.LeftDockWidgetArea) 

469 

470 snapshots_panel = self.controls_snapshots() 

471 self.snapshots_panel = snapshots_panel 

472 self.add_panel( 

473 'Snapshots', 

474 snapshots_panel, visible=False, 

475 where=qc.Qt.LeftDockWidgetArea) 

476 

477 snapshots_panel.setup_menu(snapshots_menu) 

478 

479 self.setCentralWidget(self.main_frame) 

480 

481 self.mesh = None 

482 

483 ren = vtk.vtkRenderer() 

484 

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

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

487 # ren.TwoSidedLightingOn() 

488 # ren.SetUseShadows(1) 

489 

490 self._lighting = None 

491 self._background = None 

492 

493 self.ren = ren 

494 self.update_render_settings() 

495 self.update_camera() 

496 

497 renwin = self.vtk_widget.GetRenderWindow() 

498 

499 if self._use_depth_peeling: 

500 renwin.SetAlphaBitPlanes(1) 

501 renwin.SetMultiSamples(0) 

502 

503 ren.SetUseDepthPeeling(1) 

504 ren.SetMaximumNumberOfPeels(100) 

505 ren.SetOcclusionRatio(0.1) 

506 

507 ren.SetUseFXAA(1) 

508 # ren.SetUseHiddenLineRemoval(1) 

509 # ren.SetBackingStore(1) 

510 

511 self.renwin = renwin 

512 

513 # renwin.LineSmoothingOn() 

514 # renwin.PointSmoothingOn() 

515 # renwin.PolygonSmoothingOn() 

516 

517 renwin.AddRenderer(ren) 

518 

519 iren = renwin.GetInteractor() 

520 iren.LightFollowCameraOn() 

521 iren.SetInteractorStyle(None) 

522 

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

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

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

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

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

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

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

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

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

532 

533 renwin.Render() 

534 

535 iren.Initialize() 

536 

537 self.iren = iren 

538 

539 self.rotating = False 

540 

541 self._elements = {} 

542 self._elements_active = {} 

543 

544 self.talkie_connect( 

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

546 

547 self.state.elements.append(elements.IcosphereState( 

548 element_id='icosphere', 

549 level=4, 

550 smooth=True, 

551 opacity=0.5, 

552 ambient=0.1)) 

553 

554 self.state.elements.append(elements.GridState( 

555 element_id='grid')) 

556 self.state.elements.append(elements.CoastlinesState( 

557 element_id='coastlines')) 

558 self.state.elements.append(elements.CrosshairState( 

559 element_id='crosshair')) 

560 

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

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

563 # self.state.elements.append( 

564 # elements.CatalogState( 

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

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

567 

568 if events: 

569 self.state.elements.append( 

570 elements.CatalogState( 

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

572 

573 self.state.sort_elements() 

574 

575 if snapshots: 

576 snapshots_ = [] 

577 for obj in snapshots: 

578 if isinstance(obj, str): 

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

580 else: 

581 snapshots_.append(obj) 

582 

583 snapshots_panel.add_snapshots(snapshots_) 

584 self.raise_panel(snapshots_panel) 

585 snapshots_panel.goto_snapshot(1) 

586 

587 self.timer = qc.QTimer(self) 

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

589 self.timer.setInterval(1000) 

590 self.timer.start() 

591 

592 self._animation_saver = None 

593 

594 self.closing = False 

595 self.vtk_widget.setFocus() 

596 

597 self.update_detached() 

598 

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

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

601 

602 self.show() 

603 self.windowHandle().showMaximized() 

604 

605 self.talkie_connect( 

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

607 

608 self.update_vtk_widget_size() 

609 

610 hatch_path = config.expand(os.path.join( 

611 config.pyrocko_dir_tmpl, '.sparrow-has-hatched')) 

612 

613 self.talkie_connect(self.state, '', self.capture_state) 

614 self.capture_state() 

615 

616 if not os.path.exists(hatch_path): 

617 with open(hatch_path, 'w') as f: 

618 f.write('%s\n' % util.time_to_str(time.time())) 

619 

620 self.start_tour() 

621 

622 def disable_capture(self): 

623 self._block_capture += 1 

624 

625 logger.debug('Undo capture block (+1): %i' % self._block_capture) 

626 

627 def enable_capture(self, drop=False, aggregate=None): 

628 if self._block_capture > 0: 

629 self._block_capture -= 1 

630 

631 logger.debug('Undo capture block (-1): %i' % self._block_capture) 

632 

633 if self._block_capture == 0 and not drop: 

634 self.capture_state(aggregate=aggregate) 

635 

636 def capture_state(self, *args, aggregate=None): 

637 if self._block_capture: 

638 return 

639 

640 if len(self._undo_stack) == 0 or not state_equal( 

641 self.state, self._undo_stack[-1]): 

642 

643 if aggregate is not None: 

644 if aggregate == self._undo_aggregate: 

645 self._undo_stack.pop() 

646 

647 self._undo_aggregate = aggregate 

648 else: 

649 self._undo_aggregate = None 

650 

651 logger.debug('Capture undo state (%i%s)\n%s' % ( 

652 len(self._undo_stack) + 1, 

653 '' if aggregate is None else ', aggregate=%s' % aggregate, 

654 '\n'.join( 

655 ' - %s' % s 

656 for s in self._undo_stack[-1].str_diff( 

657 self.state).splitlines()) 

658 if len(self._undo_stack) > 0 else 'initial')) 

659 

660 self._undo_stack.append(guts.clone(self.state)) 

661 self._redo_stack.clear() 

662 

663 def undo(self): 

664 self._undo_aggregate = None 

665 

666 if len(self._undo_stack) <= 1: 

667 return 

668 

669 state = self._undo_stack.pop() 

670 self._redo_stack.append(state) 

671 state = self._undo_stack[-1] 

672 

673 logger.debug('Undo (%i)\n%s' % ( 

674 len(self._undo_stack), 

675 '\n'.join( 

676 ' - %s' % s for s in self.state.str_diff(state).splitlines()))) 

677 

678 self.disable_capture() 

679 try: 

680 self.set_state(state) 

681 finally: 

682 self.enable_capture(drop=True) 

683 

684 def redo(self): 

685 self._undo_aggregate = None 

686 

687 if len(self._redo_stack) == 0: 

688 return 

689 

690 state = self._redo_stack.pop() 

691 self._undo_stack.append(state) 

692 

693 logger.debug('Redo (%i)\n%s' % ( 

694 len(self._redo_stack), 

695 '\n'.join( 

696 ' - %s' % s for s in self.state.str_diff(state).splitlines()))) 

697 

698 self.disable_capture() 

699 try: 

700 self.set_state(state) 

701 finally: 

702 self.enable_capture(drop=True) 

703 

704 def start_tour(self): 

705 snapshots_ = snapshots_mod.load_snapshots( 

706 'https://data.pyrocko.org/examples/' 

707 'sparrow-tour-v0.1.snapshots.yaml') 

708 self.snapshots_panel.add_snapshots(snapshots_) 

709 self.raise_panel(self.snapshots_panel) 

710 self.snapshots_panel.transition_to_next_snapshot() 

711 

712 def open_manual(self): 

713 import webbrowser 

714 webbrowser.open( 

715 'https://pyrocko.org/docs/current/apps/sparrow/index.html') 

716 

717 def _add_vtk_widget_size_menu_entries(self, menu): 

718 

719 group = qw.QActionGroup(menu) 

720 group.setExclusive(True) 

721 

722 def set_variable_size(): 

723 self.gui_state.fixed_size = False 

724 

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

726 variable_size_action.setCheckable(True) 

727 variable_size_action.setActionGroup(group) 

728 variable_size_action.triggered.connect(set_variable_size) 

729 

730 fixed_size_items = [] 

731 for nx, ny, label in [ 

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

733 (426, 240, ''), 

734 (640, 360, ''), 

735 (854, 480, '(FWVGA)'), 

736 (1280, 720, '(HD)'), 

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

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

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

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

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

742 (640, 480, '(VGA)'), 

743 (800, 600, '(SVGA)'), 

744 (None, None, 'Other'), 

745 (512, 512, ''), 

746 (1024, 1024, '')]: 

747 

748 if None in (nx, ny): 

749 menu.addSection(label) 

750 else: 

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

752 action = menu.addAction(name) 

753 action.setCheckable(True) 

754 action.setActionGroup(group) 

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

756 

757 def make_set_fixed_size(nx, ny): 

758 def set_fixed_size(): 

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

760 

761 return set_fixed_size 

762 

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

764 

765 def update_widget(*args): 

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

767 action.blockSignals(True) 

768 action.setChecked( 

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

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

771 action.blockSignals(False) 

772 

773 variable_size_action.blockSignals(True) 

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

775 variable_size_action.blockSignals(False) 

776 

777 update_widget() 

778 self.talkie_connect( 

779 self.gui_state, 'fixed_size', update_widget) 

780 

781 def update_vtk_widget_size(self, *args): 

782 if self.gui_state.fixed_size: 

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

784 wanted_size = qc.QSize(nx, ny) 

785 else: 

786 wanted_size = qc.QSize( 

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

788 

789 current_size = self.vtk_widget.size() 

790 

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

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

793 

794 self.vtk_widget.setFixedSize(wanted_size) 

795 

796 self.vtk_frame.recenter() 

797 self.check_vtk_resize() 

798 

799 def update_focal_point(self, *args): 

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

801 self.vtk_widget.setStatusTip( 

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

803 'change view plane orientation.' % g_modifier_key) 

804 else: 

805 self.vtk_widget.setStatusTip( 

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

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

808 'reverse sense.' % g_modifier_key) 

809 

810 def update_detached(self, *args): 

811 

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

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

814 

815 self.main_layout.removeWidget(self.vtk_frame) 

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

817 self.detached_window.show() 

818 self.vtk_widget.setFocus() 

819 

820 screens = common.get_app().screens() 

821 if len(screens) > 1: 

822 for screen in screens: 

823 if screen is not self.screen(): 

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

825 # .setScreen() does not work reliably, 

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

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

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

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

830 

831 self.detached_window.windowHandle().showMaximized() 

832 

833 frame = qw.QFrame() 

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

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

836 # frame.setAutoFillBackground(True) 

837 frame.setSizePolicy( 

838 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

839 

840 layout = qw.QGridLayout() 

841 frame.setLayout(layout) 

842 self.main_layout.insertWidget(0, frame) 

843 

844 self.state_editor = StateEditor(self) 

845 

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

847 

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

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

850 # layout.addWidget( 

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

852 

853 self.vtk_frame_substitute = frame 

854 

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

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

857 self.detached_window.hide() 

858 self.vtk_frame.setParent(self) 

859 if self.vtk_frame_substitute: 

860 self.main_layout.removeWidget(self.vtk_frame_substitute) 

861 self.state_editor.unbind_state() 

862 self.vtk_frame_substitute = None 

863 

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

865 self.detached_window = None 

866 self.vtk_widget.setFocus() 

867 

868 def attach(self): 

869 self.gui_state.detached = False 

870 

871 def export_image(self): 

872 

873 caption = 'Export Image' 

874 fn_out, _ = qw.QFileDialog.getSaveFileName( 

875 self, caption, 'image.png', 

876 options=common.qfiledialog_options) 

877 

878 if fn_out: 

879 self.save_image(fn_out) 

880 

881 def save_image(self, path): 

882 

883 original_fixed_size = self.gui_state.fixed_size 

884 if original_fixed_size is None: 

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

886 

887 wif = vtk.vtkWindowToImageFilter() 

888 wif.SetInput(self.renwin) 

889 wif.SetInputBufferTypeToRGBA() 

890 wif.SetScale(1, 1) 

891 wif.ReadFrontBufferOff() 

892 writer = vtk.vtkPNGWriter() 

893 writer.SetInputConnection(wif.GetOutputPort()) 

894 

895 self.renwin.Render() 

896 wif.Modified() 

897 writer.SetFileName(path) 

898 writer.Write() 

899 

900 self.gui_state.fixed_size = original_fixed_size 

901 

902 def update_render_settings(self, *args): 

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

904 self.ren.RemoveAllLights() 

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

906 self.ren.AddLight(li) 

907 

908 self._lighting = self.state.lighting 

909 

910 if self._background is None \ 

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

912 

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

914 self._background = self.state.background 

915 

916 self.update_view() 

917 

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

919 if self._animation: 

920 logger.debug('Aborting animation in progress to start a new one.') 

921 self.stop_animation() 

922 

923 self.disable_capture() 

924 self._animation = interpolator 

925 if output_path is None: 

926 self._animation_tstart = time.time() 

927 self._animation_iframe = None 

928 else: 

929 self._animation_iframe = 0 

930 self.showFullScreen() 

931 self.update_view() 

932 self.gui_state.panels_visible = False 

933 self.update_view() 

934 

935 self._animation_timer = qc.QTimer(self) 

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

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

938 self._animation_timer.start() 

939 if output_path is not None: 

940 original_fixed_size = self.gui_state.fixed_size 

941 if original_fixed_size is None: 

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

943 

944 wif = vtk.vtkWindowToImageFilter() 

945 wif.SetInput(self.renwin) 

946 wif.SetInputBufferTypeToRGBA() 

947 wif.SetScale(1, 1) 

948 wif.ReadFrontBufferOff() 

949 writer = vtk.vtkPNGWriter() 

950 temp_path = tempfile.mkdtemp() 

951 self._animation_saver = ( 

952 wif, writer, temp_path, output_path, original_fixed_size) 

953 writer.SetInputConnection(wif.GetOutputPort()) 

954 

955 def next_animation_frame(self): 

956 

957 ani = self._animation 

958 if not ani: 

959 return 

960 

961 if self._animation_iframe is not None: 

962 state = ani( 

963 ani.tmin 

964 + self._animation_iframe * ani.dt) 

965 

966 self._animation_iframe += 1 

967 else: 

968 tnow = time.time() 

969 state = ani(min( 

970 ani.tmax, 

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

972 

973 self.set_state(state) 

974 self.renwin.Render() 

975 if self._animation_saver: 

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

977 wif.Modified() 

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

979 writer.SetFileName(fn % self._animation_iframe) 

980 writer.Write() 

981 

982 if self._animation_iframe is not None: 

983 t = self._animation_iframe * ani.dt 

984 else: 

985 t = tnow - self._animation_tstart 

986 

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

988 self.stop_animation() 

989 

990 def stop_animation(self): 

991 if self._animation_timer: 

992 self._animation_timer.stop() 

993 

994 if self._animation_saver: 

995 

996 wif, writer, temp_path, output_path, original_fixed_size \ 

997 = self._animation_saver 

998 self.gui_state.fixed_size = original_fixed_size 

999 

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

1001 check_call([ 

1002 'ffmpeg', '-y', 

1003 '-i', fn_path, 

1004 '-c:v', 'libx264', 

1005 '-preset', 'slow', 

1006 '-crf', '17', 

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

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

1009 output_path]) 

1010 shutil.rmtree(temp_path) 

1011 

1012 self._animation_saver = None 

1013 self._animation_saver 

1014 

1015 self.showNormal() 

1016 self.gui_state.panels_visible = True 

1017 

1018 self._animation_tstart = None 

1019 self._animation_iframe = None 

1020 self._animation = None 

1021 self.enable_capture() 

1022 

1023 def set_state(self, state): 

1024 self.disable_capture() 

1025 try: 

1026 self._update_elements_enabled = False 

1027 self.setUpdatesEnabled(False) 

1028 self.state.diff_update(state) 

1029 self.state.sort_elements() 

1030 self.setUpdatesEnabled(True) 

1031 self._update_elements_enabled = True 

1032 self.update_elements() 

1033 finally: 

1034 self.enable_capture() 

1035 

1036 def periodical(self): 

1037 pass 

1038 

1039 def request_quit(self): 

1040 app = common.get_app() 

1041 app.myQuit() 

1042 

1043 def check_vtk_resize(self, *args): 

1044 render_window_size = self.renwin.GetSize() 

1045 if self._render_window_size != render_window_size: 

1046 self._render_window_size = render_window_size 

1047 self.resize_event(*render_window_size) 

1048 

1049 def update_elements(self, *_): 

1050 if not self._update_elements_enabled: 

1051 return 

1052 

1053 if self._in_update_elements: 

1054 return 

1055 

1056 self._in_update_elements = True 

1057 for estate in self.state.elements: 

1058 if estate.element_id not in self._elements: 

1059 new_element = estate.create() 

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

1061 type(new_element).__name__, 

1062 estate.element_id)) 

1063 self._elements[estate.element_id] = new_element 

1064 

1065 element = self._elements[estate.element_id] 

1066 

1067 if estate.element_id not in self._elements_active: 

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

1069 type(element).__name__, 

1070 estate.element_id)) 

1071 element.bind_state(estate) 

1072 element.set_parent(self) 

1073 self._elements_active[estate.element_id] = element 

1074 

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

1076 deactivate = [] 

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

1078 if element_id not in state_element_ids: 

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

1080 type(element).__name__, 

1081 element_id)) 

1082 element.unset_parent() 

1083 deactivate.append(element_id) 

1084 

1085 for element_id in deactivate: 

1086 del self._elements_active[element_id] 

1087 

1088 self._update_crosshair_bindings() 

1089 

1090 self._in_update_elements = False 

1091 

1092 def _update_crosshair_bindings(self): 

1093 

1094 def get_crosshair_element(): 

1095 for element in self.state.elements: 

1096 if element.element_id == 'crosshair': 

1097 return element 

1098 

1099 return None 

1100 

1101 crosshair = get_crosshair_element() 

1102 if crosshair is None or crosshair.is_connected: 

1103 return 

1104 

1105 def to_checkbox(state, widget): 

1106 widget.blockSignals(True) 

1107 widget.setChecked(state.visible) 

1108 widget.blockSignals(False) 

1109 

1110 def to_state(widget, state): 

1111 state.visible = widget.isChecked() 

1112 

1113 cb = self._crosshair_checkbox 

1114 vstate.state_bind( 

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

1116 cb, [cb.toggled], to_checkbox) 

1117 

1118 crosshair.is_connected = True 

1119 

1120 def add_actor_2d(self, actor): 

1121 if actor not in self._actors_2d: 

1122 self.ren.AddActor2D(actor) 

1123 self._actors_2d.add(actor) 

1124 

1125 def remove_actor_2d(self, actor): 

1126 if actor in self._actors_2d: 

1127 self.ren.RemoveActor2D(actor) 

1128 self._actors_2d.remove(actor) 

1129 

1130 def add_actor(self, actor): 

1131 if actor not in self._actors: 

1132 self.ren.AddActor(actor) 

1133 self._actors.add(actor) 

1134 

1135 def add_actor_list(self, actorlist): 

1136 for actor in actorlist: 

1137 self.add_actor(actor) 

1138 

1139 def remove_actor(self, actor): 

1140 if actor in self._actors: 

1141 self.ren.RemoveActor(actor) 

1142 self._actors.remove(actor) 

1143 

1144 def update_view(self): 

1145 self.vtk_widget.update() 

1146 

1147 def resize_event(self, size_x, size_y): 

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

1149 

1150 def button_event(self, obj, event): 

1151 if event == "LeftButtonPressEvent": 

1152 self.rotating = True 

1153 elif event == "LeftButtonReleaseEvent": 

1154 self.rotating = False 

1155 

1156 def mouse_move_event(self, obj, event): 

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

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

1159 

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

1161 center_x = size_x / 2.0 

1162 center_y = size_y / 2.0 

1163 

1164 if self.rotating: 

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

1166 

1167 def myWheelEvent(self, event): 

1168 

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

1170 

1171 if angle > 200: 

1172 angle = 200 

1173 

1174 if angle < -200: 

1175 angle = -200 

1176 

1177 self.disable_capture() 

1178 try: 

1179 self.do_dolly(-angle/100.) 

1180 finally: 

1181 self.enable_capture(aggregate='distance') 

1182 

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

1184 

1185 dx = x0 - x 

1186 dy = y0 - y 

1187 

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

1189 focp = self.gui_state.focal_point 

1190 

1191 if focp == 'center': 

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

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

1194 

1195 lat = self.state.lat 

1196 lon = self.state.lon 

1197 factor = self.state.distance / 10.0 

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

1199 else: 

1200 lat = 90. - self.state.dip 

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

1202 factor = 0.5 

1203 factor_lat = 1.0 

1204 

1205 dlat = dy * factor 

1206 dlon = dx * factor * factor_lat 

1207 

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

1209 lon += dlon 

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

1211 

1212 if focp == 'center': 

1213 self.state.lat = float(lat) 

1214 self.state.lon = float(lon) 

1215 else: 

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

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

1218 

1219 def do_dolly(self, v): 

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

1221 

1222 def key_down_event(self, obj, event): 

1223 k = obj.GetKeyCode() 

1224 if k == 'f': 

1225 self.gui_state.next_focal_point() 

1226 

1227 elif k == 'r': 

1228 self.reset_strike_dip() 

1229 

1230 elif k == 'p': 

1231 print(self.state) 

1232 

1233 elif k == 'i': 

1234 for elem in self.state.elements: 

1235 if isinstance(elem, elements.IcosphereState): 

1236 elem.visible = not elem.visible 

1237 

1238 elif k == 'c': 

1239 for elem in self.state.elements: 

1240 if isinstance(elem, elements.CoastlinesState): 

1241 elem.visible = not elem.visible 

1242 

1243 elif k == 't': 

1244 if not any( 

1245 isinstance(elem, elements.TopoState) 

1246 for elem in self.state.elements): 

1247 

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

1249 else: 

1250 for elem in self.state.elements: 

1251 if isinstance(elem, elements.TopoState): 

1252 elem.visible = not elem.visible 

1253 

1254 elif k == ' ': 

1255 self.toggle_panel_visibility() 

1256 

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

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

1259 

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

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

1262 

1263 def controls_navigation(self): 

1264 frame = qw.QFrame(self) 

1265 frame.setSizePolicy( 

1266 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1267 layout = qw.QGridLayout() 

1268 frame.setLayout(layout) 

1269 

1270 # lat, lon, depth 

1271 

1272 layout.addWidget( 

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

1274 

1275 le = qw.QLineEdit() 

1276 le.setStatusTip( 

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

1278 'Focal point location.') 

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

1280 

1281 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1284 

1285 def lineedit_to_lat_lon_depth(widget, state): 

1286 self.disable_capture() 

1287 try: 

1288 s = str(widget.text()) 

1289 choices = location_to_choices(s) 

1290 if len(choices) > 0: 

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

1292 choices[0].get_lat_lon_depth() 

1293 else: 

1294 raise NoLocationChoices(s) 

1295 

1296 finally: 

1297 self.enable_capture() 

1298 

1299 self._state_bind( 

1300 ['lat', 'lon', 'depth'], 

1301 lineedit_to_lat_lon_depth, 

1302 le, [le.editingFinished, le.returnPressed], 

1303 lat_lon_depth_to_lineedit) 

1304 

1305 self.lat_lon_lineedit = le 

1306 

1307 # focal point 

1308 

1309 cb = qw.QCheckBox('Fix') 

1310 cb.setStatusTip( 

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

1312 % g_modifier_key) 

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

1314 

1315 def focal_point_to_checkbox(state, widget): 

1316 widget.blockSignals(True) 

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

1318 widget.blockSignals(False) 

1319 

1320 def checkbox_to_focal_point(widget, state): 

1321 self.gui_state.focal_point = \ 

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

1323 

1324 self._gui_state_bind( 

1325 ['focal_point'], checkbox_to_focal_point, 

1326 cb, [cb.toggled], focal_point_to_checkbox) 

1327 

1328 self.focal_point_checkbox = cb 

1329 

1330 self.talkie_connect( 

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

1332 

1333 self.update_focal_point() 

1334 

1335 # strike, dip 

1336 

1337 layout.addWidget( 

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

1339 

1340 le = qw.QLineEdit() 

1341 le.setStatusTip( 

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

1343 'direction.') 

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

1345 

1346 def strike_dip_to_lineedit(state, widget): 

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

1348 

1349 def lineedit_to_strike_dip(widget, state): 

1350 s = str(widget.text()) 

1351 string_to_strike_dip = { 

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

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

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

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

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

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

1358 

1359 self.disable_capture() 

1360 if s in string_to_strike_dip: 

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

1362 

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

1364 try: 

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

1366 except Exception: 

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

1368 finally: 

1369 self.enable_capture() 

1370 

1371 self._state_bind( 

1372 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1374 

1375 self.strike_dip_lineedit = le 

1376 

1377 but = qw.QPushButton('Reset') 

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

1379 but.clicked.connect(self.reset_strike_dip) 

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

1381 

1382 # crosshair 

1383 

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

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

1386 

1387 # camera bindings 

1388 self.talkie_connect( 

1389 self.state, 

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

1391 self.update_camera) 

1392 

1393 self.talkie_connect( 

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

1395 

1396 return frame 

1397 

1398 def controls_time(self): 

1399 frame = qw.QFrame(self) 

1400 frame.setSizePolicy( 

1401 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1402 

1403 layout = qw.QGridLayout() 

1404 frame.setLayout(layout) 

1405 

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

1407 le_tmin = qw.QLineEdit() 

1408 layout.addWidget(le_tmin, 0, 1) 

1409 

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

1411 le_tmax = qw.QLineEdit() 

1412 layout.addWidget(le_tmax, 1, 1) 

1413 

1414 label_tcursor = qw.QLabel() 

1415 

1416 label_tcursor.setSizePolicy( 

1417 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1418 

1419 layout.addWidget(label_tcursor, 2, 1) 

1420 self._label_tcursor = label_tcursor 

1421 

1422 def time_to_lineedit(state, attribute, widget): 

1423 widget.setText( 

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

1425 

1426 def lineedit_to_time(widget, state, attribute): 

1427 from pyrocko.util import str_to_time_fillup 

1428 

1429 s = str(widget.text()) 

1430 if not s.strip(): 

1431 setattr(state, attribute, None) 

1432 else: 

1433 try: 

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

1435 except Exception: 

1436 raise ValueError( 

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

1438 

1439 self._state_bind( 

1440 ['tmin'], lineedit_to_time, le_tmin, 

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

1442 attribute='tmin') 

1443 self._state_bind( 

1444 ['tmax'], lineedit_to_time, le_tmax, 

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

1446 attribute='tmax') 

1447 

1448 self.tmin_lineedit = le_tmin 

1449 self.tmax_lineedit = le_tmax 

1450 

1451 range_edit = RangeEdit() 

1452 range_edit.rangeEditPressed.connect(self.disable_capture) 

1453 range_edit.rangeEditReleased.connect(self.enable_capture) 

1454 range_edit.set_data_provider(self) 

1455 range_edit.set_data_name('time') 

1456 

1457 xblock = [False] 

1458 

1459 def range_to_range_edit(state, widget): 

1460 if not xblock[0]: 

1461 widget.blockSignals(True) 

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

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

1464 widget.blockSignals(False) 

1465 

1466 def range_edit_to_range(widget, state): 

1467 xblock[0] = True 

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

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

1470 xblock[0] = False 

1471 

1472 self._state_bind( 

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

1474 range_edit_to_range, 

1475 range_edit, 

1476 [range_edit.rangeChanged, range_edit.focusChanged], 

1477 range_to_range_edit) 

1478 

1479 def handle_tcursor_changed(): 

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

1481 

1482 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1483 

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

1485 

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

1487 le_focus = qw.QLineEdit() 

1488 layout.addWidget(le_focus, 4, 1) 

1489 

1490 def focus_to_lineedit(state, widget): 

1491 if state.tduration is None: 

1492 widget.setText('') 

1493 else: 

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

1495 guts.str_duration(state.tduration), 

1496 state.tposition)) 

1497 

1498 def lineedit_to_focus(widget, state): 

1499 s = str(widget.text()) 

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

1501 try: 

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

1503 state.tduration = None 

1504 state.tposition = 0.0 

1505 else: 

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

1507 if len(w) > 1: 

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

1509 else: 

1510 state.tposition = 0.0 

1511 

1512 except Exception: 

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

1514 

1515 self._state_bind( 

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

1517 [le_focus.editingFinished, le_focus.returnPressed], 

1518 focus_to_lineedit) 

1519 

1520 label_effective_tmin = qw.QLabel() 

1521 label_effective_tmax = qw.QLabel() 

1522 

1523 label_effective_tmin.setSizePolicy( 

1524 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1525 label_effective_tmax.setSizePolicy( 

1526 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1527 label_effective_tmin.setMinimumSize( 

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

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

1530 

1531 layout.addWidget(label_effective_tmin, 5, 1) 

1532 layout.addWidget(label_effective_tmax, 6, 1) 

1533 

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

1535 self.talkie_connect( 

1536 self.state, var, self.update_effective_time_labels) 

1537 

1538 self._label_effective_tmin = label_effective_tmin 

1539 self._label_effective_tmax = label_effective_tmax 

1540 

1541 self.talkie_connect( 

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

1543 

1544 return frame 

1545 

1546 def controls_appearance(self): 

1547 frame = qw.QFrame(self) 

1548 frame.setSizePolicy( 

1549 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1550 layout = qw.QGridLayout() 

1551 frame.setLayout(layout) 

1552 

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

1554 

1555 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1556 layout.addWidget(cb, 0, 1) 

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

1558 

1559 self.talkie_connect( 

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

1561 

1562 # background 

1563 

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

1565 

1566 cb = common.strings_to_combobox( 

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

1568 

1569 layout.addWidget(cb, 1, 1) 

1570 vstate.state_bind_combobox_background( 

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

1572 

1573 self.talkie_connect( 

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

1575 

1576 return frame 

1577 

1578 def controls_snapshots(self): 

1579 return snapshots_mod.SnapshotsPanel(self) 

1580 

1581 def update_effective_time_labels(self, *args): 

1582 tmin = self.state.tmin_effective 

1583 tmax = self.state.tmax_effective 

1584 

1585 stmin = common.time_or_none_to_str(tmin) 

1586 stmax = common.time_or_none_to_str(tmax) 

1587 

1588 self._label_effective_tmin.setText(stmin) 

1589 self._label_effective_tmax.setText(stmax) 

1590 

1591 def update_tcursor(self, *args): 

1592 tcursor = self.gui_state.tcursor 

1593 stcursor = common.time_or_none_to_str(tcursor) 

1594 self._label_tcursor.setText(stcursor) 

1595 

1596 def reset_strike_dip(self, *args): 

1597 self.state.strike = 90. 

1598 self.state.dip = 0 

1599 self.gui_state.focal_point = 'center' 

1600 

1601 def get_camera_geometry(self): 

1602 

1603 def rtp2xyz(rtp): 

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

1605 

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

1607 

1608 cam_rtp = num.array([ 

1609 radius+self.state.distance, 

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

1611 self.state.lon * d2r]) 

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

1613 cam, up, foc = \ 

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

1615 

1616 foc_rtp = num.array([ 

1617 radius, 

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

1619 self.state.lon * d2r]) 

1620 

1621 foc = rtp2xyz(foc_rtp) 

1622 

1623 rot_world = pmt.euler_to_matrix( 

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

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

1626 0.0*d2r).T 

1627 

1628 rot_cam = pmt.euler_to_matrix( 

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

1630 

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

1632 

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

1634 up = num.dot(rot, up) 

1635 return cam, up, foc 

1636 

1637 def update_camera(self, *args): 

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

1639 camera = self.ren.GetActiveCamera() 

1640 camera.SetPosition(*cam) 

1641 camera.SetFocalPoint(*foc) 

1642 camera.SetViewUp(*up) 

1643 

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

1645 

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

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

1648 

1649 # if horizon == 0.0: 

1650 # horizon = 2.0 + self.state.distance 

1651 

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

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

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

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

1656 # clip_dist = feature_horizon 

1657 

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

1659 

1660 self.camera_params = ( 

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

1662 

1663 self.update_view() 

1664 

1665 def add_panel( 

1666 self, title_label, panel, 

1667 visible=False, 

1668 # volatile=False, 

1669 tabify=True, 

1670 where=qc.Qt.RightDockWidgetArea, 

1671 remove=None, 

1672 title_controls=[]): 

1673 

1674 dockwidget = common.MyDockWidget( 

1675 self, title_label, title_controls=title_controls) 

1676 

1677 if not visible: 

1678 dockwidget.hide() 

1679 

1680 if not self.gui_state.panels_visible: 

1681 dockwidget.block() 

1682 

1683 dockwidget.setWidget(panel) 

1684 

1685 panel.setParent(dockwidget) 

1686 

1687 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1689 

1690 self.addDockWidget(where, dockwidget) 

1691 

1692 nwrap = 4 

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

1694 self.tabifyDockWidget( 

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

1696 

1697 mitem = dockwidget.toggleViewAction() 

1698 

1699 def update_label(*args): 

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

1701 self.update_slug_abbreviated_lengths() 

1702 

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

1704 dockwidget.titlebar._title_label.title_changed.connect( 

1705 self.update_slug_abbreviated_lengths) 

1706 

1707 update_label() 

1708 

1709 self._panel_togglers[dockwidget] = mitem 

1710 self.panels_menu.addAction(mitem) 

1711 if visible: 

1712 dockwidget.setVisible(True) 

1713 dockwidget.setFocus() 

1714 dockwidget.raise_() 

1715 

1716 def stack_panels(self): 

1717 dockwidgets = self.findChildren(common.MyDockWidget) 

1718 by_area = defaultdict(list) 

1719 for dw in dockwidgets: 

1720 area = self.dockWidgetArea(dw) 

1721 by_area[area].append(dw) 

1722 

1723 for dockwidgets in by_area.values(): 

1724 dw_last = None 

1725 for dw in dockwidgets: 

1726 if dw_last is not None: 

1727 self.tabifyDockWidget(dw_last, dw) 

1728 

1729 dw_last = dw 

1730 

1731 def update_slug_abbreviated_lengths(self): 

1732 dockwidgets = self.findChildren(common.MyDockWidget) 

1733 title_labels = [] 

1734 for dw in dockwidgets: 

1735 title_labels.append(dw.titlebar._title_label) 

1736 

1737 by_title = defaultdict(list) 

1738 for tl in title_labels: 

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

1740 

1741 for group in by_title.values(): 

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

1743 

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

1745 nunique = len(set(slugs)) 

1746 

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

1748 n -= 1 

1749 

1750 if n > 0: 

1751 n = max(3, n) 

1752 

1753 for tl in group: 

1754 tl.set_slug_abbreviated_length(n) 

1755 

1756 def raise_panel(self, panel): 

1757 dockwidget = panel.parent() 

1758 dockwidget.setVisible(True) 

1759 dockwidget.setFocus() 

1760 dockwidget.raise_() 

1761 

1762 def toggle_panel_visibility(self): 

1763 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1764 

1765 def update_panel_visibility(self, *args): 

1766 self.setUpdatesEnabled(False) 

1767 mbar = self.menuBar() 

1768 sbar = self.statusBar() 

1769 dockwidgets = self.findChildren(common.MyDockWidget) 

1770 

1771 # Set height to zero instead of hiding so that shortcuts still work 

1772 # otherwise one would have to mess around with separate QShortcut 

1773 # objects. 

1774 mbar.setFixedHeight( 

1775 qw.QWIDGETSIZE_MAX if self.gui_state.panels_visible else 0) 

1776 

1777 sbar.setVisible(self.gui_state.panels_visible) 

1778 for dockwidget in dockwidgets: 

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

1780 

1781 self.setUpdatesEnabled(True) 

1782 

1783 def remove_panel(self, panel): 

1784 dockwidget = panel.parent() 

1785 self.removeDockWidget(dockwidget) 

1786 dockwidget.setParent(None) 

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

1788 

1789 def register_data_provider(self, provider): 

1790 if provider not in self.data_providers: 

1791 self.data_providers.append(provider) 

1792 

1793 def unregister_data_provider(self, provider): 

1794 if provider in self.data_providers: 

1795 self.data_providers.remove(provider) 

1796 

1797 def iter_data(self, name): 

1798 for provider in self.data_providers: 

1799 for data in provider.iter_data(name): 

1800 yield data 

1801 

1802 def closeEvent(self, event): 

1803 self.attach() 

1804 event.accept() 

1805 self.closing = True 

1806 common.get_app().set_main_window(None) 

1807 

1808 def is_closing(self): 

1809 return self.closing 

1810 

1811 

1812class SparrowApp(qw.QApplication): 

1813 def __init__(self): 

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

1815 self.lastWindowClosed.connect(self.myQuit) 

1816 self._main_window = None 

1817 self.setApplicationDisplayName('Sparrow') 

1818 self.setDesktopFileName('Sparrow') 

1819 

1820 def install_sigint_handler(self): 

1821 self._old_signal_handler = signal.signal( 

1822 signal.SIGINT, self.myCloseAllWindows) 

1823 

1824 def uninstall_sigint_handler(self): 

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

1826 

1827 def myQuit(self, *args): 

1828 self.quit() 

1829 

1830 def myCloseAllWindows(self, *args): 

1831 self.closeAllWindows() 

1832 

1833 def set_main_window(self, win): 

1834 self._main_window = win 

1835 

1836 def get_main_window(self): 

1837 return self._main_window 

1838 

1839 def get_progressbars(self): 

1840 if self._main_window: 

1841 return self._main_window.progressbars 

1842 else: 

1843 return None 

1844 

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

1846 win = self.get_main_window() 

1847 if not win: 

1848 return 

1849 

1850 win.statusBar().showMessage( 

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

1852 

1853 

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

1855 

1856 from pyrocko import util 

1857 from pyrocko.gui import util as gui_util 

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

1859 

1860 global win 

1861 

1862 if gui_util.app is None: 

1863 gui_util.app = SparrowApp() 

1864 

1865 # try: 

1866 # from qt_material import apply_stylesheet 

1867 # 

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

1869 # 

1870 # 

1871 # import qdarkgraystyle 

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

1873 # import qdarkstyle 

1874 # 

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

1876 # 

1877 # 

1878 # except ImportError: 

1879 # logger.info( 

1880 # 'Module qdarkgraystyle not available.\n' 

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

1882 # 'qdarkgraystyle".') 

1883 # 

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

1885 

1886 gui_util.app.install_sigint_handler() 

1887 gui_util.app.exec_() 

1888 gui_util.app.uninstall_sigint_handler() 

1889 

1890 del win 

1891 

1892 gc.collect() 

1893 

1894 del gui_util.app