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 ('Volcanoes', elements.VolcanoesState()), 

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

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

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

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

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

403 

404 def wrap_add_element(estate): 

405 def add_element(*args): 

406 new_element = guts.clone(estate) 

407 new_element.element_id = elements.random_id() 

408 self.state.elements.append(new_element) 

409 self.state.sort_elements() 

410 

411 return add_element 

412 

413 mitem = qw.QAction(name, self) 

414 

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

416 

417 menu.addAction(mitem) 

418 

419 menu = mbar.addMenu('Help') 

420 

421 menu.addAction( 

422 'Interactive Tour', 

423 self.start_tour) 

424 

425 menu.addAction( 

426 'Online Manual', 

427 self.open_manual) 

428 

429 self.data_providers = [] 

430 self.elements = {} 

431 

432 self.detached_window = None 

433 

434 self.main_frame = qw.QFrame() 

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

436 

437 self.vtk_frame = CenteringScrollArea() 

438 

439 self.vtk_widget = QVTKWidget(self, self) 

440 self.vtk_frame.setWidget(self.vtk_widget) 

441 

442 self.main_layout = qw.QVBoxLayout() 

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

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

445 

446 pb = Progressbars(self) 

447 self.progressbars = pb 

448 self.main_layout.addWidget(pb) 

449 

450 self.main_frame.setLayout(self.main_layout) 

451 

452 self.vtk_frame_substitute = None 

453 

454 self.add_panel( 

455 'Navigation', 

456 self.controls_navigation(), visible=True, 

457 where=qc.Qt.LeftDockWidgetArea) 

458 

459 self.add_panel( 

460 'Time', 

461 self.controls_time(), visible=True, 

462 where=qc.Qt.LeftDockWidgetArea) 

463 

464 self.add_panel( 

465 'Appearance', 

466 self.controls_appearance(), visible=True, 

467 where=qc.Qt.LeftDockWidgetArea) 

468 

469 snapshots_panel = self.controls_snapshots() 

470 self.snapshots_panel = snapshots_panel 

471 self.add_panel( 

472 'Snapshots', 

473 snapshots_panel, visible=False, 

474 where=qc.Qt.LeftDockWidgetArea) 

475 

476 snapshots_panel.setup_menu(snapshots_menu) 

477 

478 self.setCentralWidget(self.main_frame) 

479 

480 self.mesh = None 

481 

482 ren = vtk.vtkRenderer() 

483 

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

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

486 # ren.TwoSidedLightingOn() 

487 # ren.SetUseShadows(1) 

488 

489 self._lighting = None 

490 self._background = None 

491 

492 self.ren = ren 

493 self.update_render_settings() 

494 self.update_camera() 

495 

496 renwin = self.vtk_widget.GetRenderWindow() 

497 

498 if self._use_depth_peeling: 

499 renwin.SetAlphaBitPlanes(1) 

500 renwin.SetMultiSamples(0) 

501 

502 ren.SetUseDepthPeeling(1) 

503 ren.SetMaximumNumberOfPeels(100) 

504 ren.SetOcclusionRatio(0.1) 

505 

506 ren.SetUseFXAA(1) 

507 # ren.SetUseHiddenLineRemoval(1) 

508 # ren.SetBackingStore(1) 

509 

510 self.renwin = renwin 

511 

512 # renwin.LineSmoothingOn() 

513 # renwin.PointSmoothingOn() 

514 # renwin.PolygonSmoothingOn() 

515 

516 renwin.AddRenderer(ren) 

517 

518 iren = renwin.GetInteractor() 

519 iren.LightFollowCameraOn() 

520 iren.SetInteractorStyle(None) 

521 

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

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

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

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

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

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

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

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

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

531 

532 renwin.Render() 

533 

534 iren.Initialize() 

535 

536 self.iren = iren 

537 

538 self.rotating = False 

539 

540 self._elements = {} 

541 self._elements_active = {} 

542 

543 self.talkie_connect( 

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

545 

546 self.state.elements.append(elements.IcosphereState( 

547 element_id='icosphere', 

548 level=4, 

549 smooth=True, 

550 opacity=0.5, 

551 ambient=0.1)) 

552 

553 self.state.elements.append(elements.GridState( 

554 element_id='grid')) 

555 self.state.elements.append(elements.CoastlinesState( 

556 element_id='coastlines')) 

557 self.state.elements.append(elements.CrosshairState( 

558 element_id='crosshair')) 

559 

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

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

562 # self.state.elements.append( 

563 # elements.CatalogState( 

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

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

566 

567 if events: 

568 self.state.elements.append( 

569 elements.CatalogState( 

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

571 

572 self.state.sort_elements() 

573 

574 if snapshots: 

575 snapshots_ = [] 

576 for obj in snapshots: 

577 if isinstance(obj, str): 

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

579 else: 

580 snapshots_.append(obj) 

581 

582 snapshots_panel.add_snapshots(snapshots_) 

583 self.raise_panel(snapshots_panel) 

584 snapshots_panel.goto_snapshot(1) 

585 

586 self.timer = qc.QTimer(self) 

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

588 self.timer.setInterval(1000) 

589 self.timer.start() 

590 

591 self._animation_saver = None 

592 

593 self.closing = False 

594 self.vtk_widget.setFocus() 

595 

596 self.update_detached() 

597 

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

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

600 

601 self.show() 

602 self.windowHandle().showMaximized() 

603 

604 self.talkie_connect( 

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

606 

607 self.update_vtk_widget_size() 

608 

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

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

611 

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

613 self.capture_state() 

614 

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

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

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

618 

619 self.start_tour() 

620 

621 def disable_capture(self): 

622 self._block_capture += 1 

623 

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

625 

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

627 if self._block_capture > 0: 

628 self._block_capture -= 1 

629 

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

631 

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

633 self.capture_state(aggregate=aggregate) 

634 

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

636 if self._block_capture: 

637 return 

638 

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

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

641 

642 if aggregate is not None: 

643 if aggregate == self._undo_aggregate: 

644 self._undo_stack.pop() 

645 

646 self._undo_aggregate = aggregate 

647 else: 

648 self._undo_aggregate = None 

649 

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

651 len(self._undo_stack) + 1, 

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

653 '\n'.join( 

654 ' - %s' % s 

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

656 self.state).splitlines()) 

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

658 

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

660 self._redo_stack.clear() 

661 

662 def undo(self): 

663 self._undo_aggregate = None 

664 

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

666 return 

667 

668 state = self._undo_stack.pop() 

669 self._redo_stack.append(state) 

670 state = self._undo_stack[-1] 

671 

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

673 len(self._undo_stack), 

674 '\n'.join( 

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

676 

677 self.disable_capture() 

678 try: 

679 self.set_state(state) 

680 finally: 

681 self.enable_capture(drop=True) 

682 

683 def redo(self): 

684 self._undo_aggregate = None 

685 

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

687 return 

688 

689 state = self._redo_stack.pop() 

690 self._undo_stack.append(state) 

691 

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

693 len(self._redo_stack), 

694 '\n'.join( 

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

696 

697 self.disable_capture() 

698 try: 

699 self.set_state(state) 

700 finally: 

701 self.enable_capture(drop=True) 

702 

703 def start_tour(self): 

704 snapshots_ = snapshots_mod.load_snapshots( 

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

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

707 self.snapshots_panel.add_snapshots(snapshots_) 

708 self.raise_panel(self.snapshots_panel) 

709 self.snapshots_panel.transition_to_next_snapshot() 

710 

711 def open_manual(self): 

712 import webbrowser 

713 webbrowser.open( 

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

715 

716 def _add_vtk_widget_size_menu_entries(self, menu): 

717 

718 group = qw.QActionGroup(menu) 

719 group.setExclusive(True) 

720 

721 def set_variable_size(): 

722 self.gui_state.fixed_size = False 

723 

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

725 variable_size_action.setCheckable(True) 

726 variable_size_action.setActionGroup(group) 

727 variable_size_action.triggered.connect(set_variable_size) 

728 

729 fixed_size_items = [] 

730 for nx, ny, label in [ 

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

732 (426, 240, ''), 

733 (640, 360, ''), 

734 (854, 480, '(FWVGA)'), 

735 (1280, 720, '(HD)'), 

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

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

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

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

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

741 (640, 480, '(VGA)'), 

742 (800, 600, '(SVGA)'), 

743 (None, None, 'Other'), 

744 (512, 512, ''), 

745 (1024, 1024, '')]: 

746 

747 if None in (nx, ny): 

748 menu.addSection(label) 

749 else: 

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

751 action = menu.addAction(name) 

752 action.setCheckable(True) 

753 action.setActionGroup(group) 

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

755 

756 def make_set_fixed_size(nx, ny): 

757 def set_fixed_size(): 

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

759 

760 return set_fixed_size 

761 

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

763 

764 def update_widget(*args): 

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

766 action.blockSignals(True) 

767 action.setChecked( 

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

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

770 action.blockSignals(False) 

771 

772 variable_size_action.blockSignals(True) 

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

774 variable_size_action.blockSignals(False) 

775 

776 update_widget() 

777 self.talkie_connect( 

778 self.gui_state, 'fixed_size', update_widget) 

779 

780 def update_vtk_widget_size(self, *args): 

781 if self.gui_state.fixed_size: 

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

783 wanted_size = qc.QSize(nx, ny) 

784 else: 

785 wanted_size = qc.QSize( 

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

787 

788 current_size = self.vtk_widget.size() 

789 

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

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

792 

793 self.vtk_widget.setFixedSize(wanted_size) 

794 

795 self.vtk_frame.recenter() 

796 self.check_vtk_resize() 

797 

798 def update_focal_point(self, *args): 

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

800 self.vtk_widget.setStatusTip( 

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

802 'change view plane orientation.' % g_modifier_key) 

803 else: 

804 self.vtk_widget.setStatusTip( 

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

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

807 'reverse sense.' % g_modifier_key) 

808 

809 def update_detached(self, *args): 

810 

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

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

813 

814 self.main_layout.removeWidget(self.vtk_frame) 

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

816 self.detached_window.show() 

817 self.vtk_widget.setFocus() 

818 

819 screens = common.get_app().screens() 

820 if len(screens) > 1: 

821 for screen in screens: 

822 if screen is not self.screen(): 

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

824 # .setScreen() does not work reliably, 

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

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

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

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

829 

830 self.detached_window.windowHandle().showMaximized() 

831 

832 frame = qw.QFrame() 

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

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

835 # frame.setAutoFillBackground(True) 

836 frame.setSizePolicy( 

837 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

838 

839 layout = qw.QGridLayout() 

840 frame.setLayout(layout) 

841 self.main_layout.insertWidget(0, frame) 

842 

843 self.state_editor = StateEditor(self) 

844 

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

846 

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

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

849 # layout.addWidget( 

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

851 

852 self.vtk_frame_substitute = frame 

853 

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

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

856 self.detached_window.hide() 

857 self.vtk_frame.setParent(self) 

858 if self.vtk_frame_substitute: 

859 self.main_layout.removeWidget(self.vtk_frame_substitute) 

860 self.state_editor.unbind_state() 

861 self.vtk_frame_substitute = None 

862 

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

864 self.detached_window = None 

865 self.vtk_widget.setFocus() 

866 

867 def attach(self): 

868 self.gui_state.detached = False 

869 

870 def export_image(self): 

871 

872 caption = 'Export Image' 

873 fn_out, _ = qw.QFileDialog.getSaveFileName( 

874 self, caption, 'image.png', 

875 options=common.qfiledialog_options) 

876 

877 if fn_out: 

878 self.save_image(fn_out) 

879 

880 def save_image(self, path): 

881 

882 original_fixed_size = self.gui_state.fixed_size 

883 if original_fixed_size is None: 

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

885 

886 wif = vtk.vtkWindowToImageFilter() 

887 wif.SetInput(self.renwin) 

888 wif.SetInputBufferTypeToRGBA() 

889 wif.SetScale(1, 1) 

890 wif.ReadFrontBufferOff() 

891 writer = vtk.vtkPNGWriter() 

892 writer.SetInputConnection(wif.GetOutputPort()) 

893 

894 self.renwin.Render() 

895 wif.Modified() 

896 writer.SetFileName(path) 

897 writer.Write() 

898 

899 self.gui_state.fixed_size = original_fixed_size 

900 

901 def update_render_settings(self, *args): 

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

903 self.ren.RemoveAllLights() 

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

905 self.ren.AddLight(li) 

906 

907 self._lighting = self.state.lighting 

908 

909 if self._background is None \ 

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

911 

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

913 self._background = self.state.background 

914 

915 self.update_view() 

916 

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

918 if self._animation: 

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

920 self.stop_animation() 

921 

922 self.disable_capture() 

923 self._animation = interpolator 

924 if output_path is None: 

925 self._animation_tstart = time.time() 

926 self._animation_iframe = None 

927 else: 

928 self._animation_iframe = 0 

929 self.showFullScreen() 

930 self.update_view() 

931 self.gui_state.panels_visible = False 

932 self.update_view() 

933 

934 self._animation_timer = qc.QTimer(self) 

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

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

937 self._animation_timer.start() 

938 if output_path is not None: 

939 original_fixed_size = self.gui_state.fixed_size 

940 if original_fixed_size is None: 

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

942 

943 wif = vtk.vtkWindowToImageFilter() 

944 wif.SetInput(self.renwin) 

945 wif.SetInputBufferTypeToRGBA() 

946 wif.SetScale(1, 1) 

947 wif.ReadFrontBufferOff() 

948 writer = vtk.vtkPNGWriter() 

949 temp_path = tempfile.mkdtemp() 

950 self._animation_saver = ( 

951 wif, writer, temp_path, output_path, original_fixed_size) 

952 writer.SetInputConnection(wif.GetOutputPort()) 

953 

954 def next_animation_frame(self): 

955 

956 ani = self._animation 

957 if not ani: 

958 return 

959 

960 if self._animation_iframe is not None: 

961 state = ani( 

962 ani.tmin 

963 + self._animation_iframe * ani.dt) 

964 

965 self._animation_iframe += 1 

966 else: 

967 tnow = time.time() 

968 state = ani(min( 

969 ani.tmax, 

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

971 

972 self.set_state(state) 

973 self.renwin.Render() 

974 if self._animation_saver: 

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

976 wif.Modified() 

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

978 writer.SetFileName(fn % self._animation_iframe) 

979 writer.Write() 

980 

981 if self._animation_iframe is not None: 

982 t = self._animation_iframe * ani.dt 

983 else: 

984 t = tnow - self._animation_tstart 

985 

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

987 self.stop_animation() 

988 

989 def stop_animation(self): 

990 if self._animation_timer: 

991 self._animation_timer.stop() 

992 

993 if self._animation_saver: 

994 

995 wif, writer, temp_path, output_path, original_fixed_size \ 

996 = self._animation_saver 

997 self.gui_state.fixed_size = original_fixed_size 

998 

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

1000 check_call([ 

1001 'ffmpeg', '-y', 

1002 '-i', fn_path, 

1003 '-c:v', 'libx264', 

1004 '-preset', 'slow', 

1005 '-crf', '17', 

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

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

1008 output_path]) 

1009 shutil.rmtree(temp_path) 

1010 

1011 self._animation_saver = None 

1012 self._animation_saver 

1013 

1014 self.showNormal() 

1015 self.gui_state.panels_visible = True 

1016 

1017 self._animation_tstart = None 

1018 self._animation_iframe = None 

1019 self._animation = None 

1020 self.enable_capture() 

1021 

1022 def set_state(self, state): 

1023 self.disable_capture() 

1024 try: 

1025 self._update_elements_enabled = False 

1026 self.setUpdatesEnabled(False) 

1027 self.state.diff_update(state) 

1028 self.state.sort_elements() 

1029 self.setUpdatesEnabled(True) 

1030 self._update_elements_enabled = True 

1031 self.update_elements() 

1032 finally: 

1033 self.enable_capture() 

1034 

1035 def periodical(self): 

1036 pass 

1037 

1038 def request_quit(self): 

1039 app = common.get_app() 

1040 app.myQuit() 

1041 

1042 def check_vtk_resize(self, *args): 

1043 render_window_size = self.renwin.GetSize() 

1044 if self._render_window_size != render_window_size: 

1045 self._render_window_size = render_window_size 

1046 self.resize_event(*render_window_size) 

1047 

1048 def update_elements(self, *_): 

1049 if not self._update_elements_enabled: 

1050 return 

1051 

1052 if self._in_update_elements: 

1053 return 

1054 

1055 self._in_update_elements = True 

1056 for estate in self.state.elements: 

1057 if estate.element_id not in self._elements: 

1058 new_element = estate.create() 

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

1060 type(new_element).__name__, 

1061 estate.element_id)) 

1062 self._elements[estate.element_id] = new_element 

1063 

1064 element = self._elements[estate.element_id] 

1065 

1066 if estate.element_id not in self._elements_active: 

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

1068 type(element).__name__, 

1069 estate.element_id)) 

1070 element.bind_state(estate) 

1071 element.set_parent(self) 

1072 self._elements_active[estate.element_id] = element 

1073 

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

1075 deactivate = [] 

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

1077 if element_id not in state_element_ids: 

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

1079 type(element).__name__, 

1080 element_id)) 

1081 element.unset_parent() 

1082 deactivate.append(element_id) 

1083 

1084 for element_id in deactivate: 

1085 del self._elements_active[element_id] 

1086 

1087 self._update_crosshair_bindings() 

1088 

1089 self._in_update_elements = False 

1090 

1091 def _update_crosshair_bindings(self): 

1092 

1093 def get_crosshair_element(): 

1094 for element in self.state.elements: 

1095 if element.element_id == 'crosshair': 

1096 return element 

1097 

1098 return None 

1099 

1100 crosshair = get_crosshair_element() 

1101 if crosshair is None or crosshair.is_connected: 

1102 return 

1103 

1104 def to_checkbox(state, widget): 

1105 widget.blockSignals(True) 

1106 widget.setChecked(state.visible) 

1107 widget.blockSignals(False) 

1108 

1109 def to_state(widget, state): 

1110 state.visible = widget.isChecked() 

1111 

1112 cb = self._crosshair_checkbox 

1113 vstate.state_bind( 

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

1115 cb, [cb.toggled], to_checkbox) 

1116 

1117 crosshair.is_connected = True 

1118 

1119 def add_actor_2d(self, actor): 

1120 if actor not in self._actors_2d: 

1121 self.ren.AddActor2D(actor) 

1122 self._actors_2d.add(actor) 

1123 

1124 def remove_actor_2d(self, actor): 

1125 if actor in self._actors_2d: 

1126 self.ren.RemoveActor2D(actor) 

1127 self._actors_2d.remove(actor) 

1128 

1129 def add_actor(self, actor): 

1130 if actor not in self._actors: 

1131 self.ren.AddActor(actor) 

1132 self._actors.add(actor) 

1133 

1134 def add_actor_list(self, actorlist): 

1135 for actor in actorlist: 

1136 self.add_actor(actor) 

1137 

1138 def remove_actor(self, actor): 

1139 if actor in self._actors: 

1140 self.ren.RemoveActor(actor) 

1141 self._actors.remove(actor) 

1142 

1143 def update_view(self): 

1144 self.vtk_widget.update() 

1145 

1146 def resize_event(self, size_x, size_y): 

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

1148 

1149 def button_event(self, obj, event): 

1150 if event == "LeftButtonPressEvent": 

1151 self.rotating = True 

1152 elif event == "LeftButtonReleaseEvent": 

1153 self.rotating = False 

1154 

1155 def mouse_move_event(self, obj, event): 

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

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

1158 

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

1160 center_x = size_x / 2.0 

1161 center_y = size_y / 2.0 

1162 

1163 if self.rotating: 

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

1165 

1166 def myWheelEvent(self, event): 

1167 

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

1169 

1170 if angle > 200: 

1171 angle = 200 

1172 

1173 if angle < -200: 

1174 angle = -200 

1175 

1176 self.disable_capture() 

1177 try: 

1178 self.do_dolly(-angle/100.) 

1179 finally: 

1180 self.enable_capture(aggregate='distance') 

1181 

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

1183 

1184 dx = x0 - x 

1185 dy = y0 - y 

1186 

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

1188 focp = self.gui_state.focal_point 

1189 

1190 if focp == 'center': 

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

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

1193 

1194 lat = self.state.lat 

1195 lon = self.state.lon 

1196 factor = self.state.distance / 10.0 

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

1198 else: 

1199 lat = 90. - self.state.dip 

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

1201 factor = 0.5 

1202 factor_lat = 1.0 

1203 

1204 dlat = dy * factor 

1205 dlon = dx * factor * factor_lat 

1206 

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

1208 lon += dlon 

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

1210 

1211 if focp == 'center': 

1212 self.state.lat = float(lat) 

1213 self.state.lon = float(lon) 

1214 else: 

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

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

1217 

1218 def do_dolly(self, v): 

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

1220 

1221 def key_down_event(self, obj, event): 

1222 k = obj.GetKeyCode() 

1223 if k == 'f': 

1224 self.gui_state.next_focal_point() 

1225 

1226 elif k == 'r': 

1227 self.reset_strike_dip() 

1228 

1229 elif k == 'p': 

1230 print(self.state) 

1231 

1232 elif k == 'i': 

1233 for elem in self.state.elements: 

1234 if isinstance(elem, elements.IcosphereState): 

1235 elem.visible = not elem.visible 

1236 

1237 elif k == 'c': 

1238 for elem in self.state.elements: 

1239 if isinstance(elem, elements.CoastlinesState): 

1240 elem.visible = not elem.visible 

1241 

1242 elif k == 't': 

1243 if not any( 

1244 isinstance(elem, elements.TopoState) 

1245 for elem in self.state.elements): 

1246 

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

1248 else: 

1249 for elem in self.state.elements: 

1250 if isinstance(elem, elements.TopoState): 

1251 elem.visible = not elem.visible 

1252 

1253 elif k == ' ': 

1254 self.toggle_panel_visibility() 

1255 

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

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

1258 

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

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

1261 

1262 def controls_navigation(self): 

1263 frame = qw.QFrame(self) 

1264 frame.setSizePolicy( 

1265 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1266 layout = qw.QGridLayout() 

1267 frame.setLayout(layout) 

1268 

1269 # lat, lon, depth 

1270 

1271 layout.addWidget( 

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

1273 

1274 le = qw.QLineEdit() 

1275 le.setStatusTip( 

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

1277 'Focal point location.') 

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

1279 

1280 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1283 

1284 def lineedit_to_lat_lon_depth(widget, state): 

1285 self.disable_capture() 

1286 try: 

1287 s = str(widget.text()) 

1288 choices = location_to_choices(s) 

1289 if len(choices) > 0: 

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

1291 choices[0].get_lat_lon_depth() 

1292 else: 

1293 raise NoLocationChoices(s) 

1294 

1295 finally: 

1296 self.enable_capture() 

1297 

1298 self._state_bind( 

1299 ['lat', 'lon', 'depth'], 

1300 lineedit_to_lat_lon_depth, 

1301 le, [le.editingFinished, le.returnPressed], 

1302 lat_lon_depth_to_lineedit) 

1303 

1304 self.lat_lon_lineedit = le 

1305 

1306 # focal point 

1307 

1308 cb = qw.QCheckBox('Fix') 

1309 cb.setStatusTip( 

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

1311 % g_modifier_key) 

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

1313 

1314 def focal_point_to_checkbox(state, widget): 

1315 widget.blockSignals(True) 

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

1317 widget.blockSignals(False) 

1318 

1319 def checkbox_to_focal_point(widget, state): 

1320 self.gui_state.focal_point = \ 

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

1322 

1323 self._gui_state_bind( 

1324 ['focal_point'], checkbox_to_focal_point, 

1325 cb, [cb.toggled], focal_point_to_checkbox) 

1326 

1327 self.focal_point_checkbox = cb 

1328 

1329 self.talkie_connect( 

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

1331 

1332 self.update_focal_point() 

1333 

1334 # strike, dip 

1335 

1336 layout.addWidget( 

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

1338 

1339 le = qw.QLineEdit() 

1340 le.setStatusTip( 

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

1342 'direction.') 

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

1344 

1345 def strike_dip_to_lineedit(state, widget): 

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

1347 

1348 def lineedit_to_strike_dip(widget, state): 

1349 s = str(widget.text()) 

1350 string_to_strike_dip = { 

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

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

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

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

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

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

1357 

1358 self.disable_capture() 

1359 if s in string_to_strike_dip: 

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

1361 

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

1363 try: 

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

1365 except Exception: 

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

1367 finally: 

1368 self.enable_capture() 

1369 

1370 self._state_bind( 

1371 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1373 

1374 self.strike_dip_lineedit = le 

1375 

1376 but = qw.QPushButton('Reset') 

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

1378 but.clicked.connect(self.reset_strike_dip) 

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

1380 

1381 # crosshair 

1382 

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

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

1385 

1386 # camera bindings 

1387 self.talkie_connect( 

1388 self.state, 

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

1390 self.update_camera) 

1391 

1392 self.talkie_connect( 

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

1394 

1395 return frame 

1396 

1397 def controls_time(self): 

1398 frame = qw.QFrame(self) 

1399 frame.setSizePolicy( 

1400 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1401 

1402 layout = qw.QGridLayout() 

1403 frame.setLayout(layout) 

1404 

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

1406 le_tmin = qw.QLineEdit() 

1407 layout.addWidget(le_tmin, 0, 1) 

1408 

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

1410 le_tmax = qw.QLineEdit() 

1411 layout.addWidget(le_tmax, 1, 1) 

1412 

1413 label_tcursor = qw.QLabel() 

1414 

1415 label_tcursor.setSizePolicy( 

1416 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1417 

1418 layout.addWidget(label_tcursor, 2, 1) 

1419 self._label_tcursor = label_tcursor 

1420 

1421 def time_to_lineedit(state, attribute, widget): 

1422 widget.setText( 

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

1424 

1425 def lineedit_to_time(widget, state, attribute): 

1426 from pyrocko.util import str_to_time_fillup 

1427 

1428 s = str(widget.text()) 

1429 if not s.strip(): 

1430 setattr(state, attribute, None) 

1431 else: 

1432 try: 

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

1434 except Exception: 

1435 raise ValueError( 

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

1437 

1438 self._state_bind( 

1439 ['tmin'], lineedit_to_time, le_tmin, 

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

1441 attribute='tmin') 

1442 self._state_bind( 

1443 ['tmax'], lineedit_to_time, le_tmax, 

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

1445 attribute='tmax') 

1446 

1447 self.tmin_lineedit = le_tmin 

1448 self.tmax_lineedit = le_tmax 

1449 

1450 range_edit = RangeEdit() 

1451 range_edit.rangeEditPressed.connect(self.disable_capture) 

1452 range_edit.rangeEditReleased.connect(self.enable_capture) 

1453 range_edit.set_data_provider(self) 

1454 range_edit.set_data_name('time') 

1455 

1456 xblock = [False] 

1457 

1458 def range_to_range_edit(state, widget): 

1459 if not xblock[0]: 

1460 widget.blockSignals(True) 

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

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

1463 widget.blockSignals(False) 

1464 

1465 def range_edit_to_range(widget, state): 

1466 xblock[0] = True 

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

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

1469 xblock[0] = False 

1470 

1471 self._state_bind( 

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

1473 range_edit_to_range, 

1474 range_edit, 

1475 [range_edit.rangeChanged, range_edit.focusChanged], 

1476 range_to_range_edit) 

1477 

1478 def handle_tcursor_changed(): 

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

1480 

1481 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1482 

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

1484 

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

1486 le_focus = qw.QLineEdit() 

1487 layout.addWidget(le_focus, 4, 1) 

1488 

1489 def focus_to_lineedit(state, widget): 

1490 if state.tduration is None: 

1491 widget.setText('') 

1492 else: 

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

1494 guts.str_duration(state.tduration), 

1495 state.tposition)) 

1496 

1497 def lineedit_to_focus(widget, state): 

1498 s = str(widget.text()) 

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

1500 try: 

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

1502 state.tduration = None 

1503 state.tposition = 0.0 

1504 else: 

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

1506 if len(w) > 1: 

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

1508 else: 

1509 state.tposition = 0.0 

1510 

1511 except Exception: 

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

1513 

1514 self._state_bind( 

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

1516 [le_focus.editingFinished, le_focus.returnPressed], 

1517 focus_to_lineedit) 

1518 

1519 label_effective_tmin = qw.QLabel() 

1520 label_effective_tmax = qw.QLabel() 

1521 

1522 label_effective_tmin.setSizePolicy( 

1523 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1524 label_effective_tmax.setSizePolicy( 

1525 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1526 label_effective_tmin.setMinimumSize( 

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

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

1529 

1530 layout.addWidget(label_effective_tmin, 5, 1) 

1531 layout.addWidget(label_effective_tmax, 6, 1) 

1532 

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

1534 self.talkie_connect( 

1535 self.state, var, self.update_effective_time_labels) 

1536 

1537 self._label_effective_tmin = label_effective_tmin 

1538 self._label_effective_tmax = label_effective_tmax 

1539 

1540 self.talkie_connect( 

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

1542 

1543 return frame 

1544 

1545 def controls_appearance(self): 

1546 frame = qw.QFrame(self) 

1547 frame.setSizePolicy( 

1548 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1549 layout = qw.QGridLayout() 

1550 frame.setLayout(layout) 

1551 

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

1553 

1554 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1555 layout.addWidget(cb, 0, 1) 

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

1557 

1558 self.talkie_connect( 

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

1560 

1561 # background 

1562 

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

1564 

1565 cb = common.strings_to_combobox( 

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

1567 

1568 layout.addWidget(cb, 1, 1) 

1569 vstate.state_bind_combobox_background( 

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

1571 

1572 self.talkie_connect( 

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

1574 

1575 return frame 

1576 

1577 def controls_snapshots(self): 

1578 return snapshots_mod.SnapshotsPanel(self) 

1579 

1580 def update_effective_time_labels(self, *args): 

1581 tmin = self.state.tmin_effective 

1582 tmax = self.state.tmax_effective 

1583 

1584 stmin = common.time_or_none_to_str(tmin) 

1585 stmax = common.time_or_none_to_str(tmax) 

1586 

1587 self._label_effective_tmin.setText(stmin) 

1588 self._label_effective_tmax.setText(stmax) 

1589 

1590 def update_tcursor(self, *args): 

1591 tcursor = self.gui_state.tcursor 

1592 stcursor = common.time_or_none_to_str(tcursor) 

1593 self._label_tcursor.setText(stcursor) 

1594 

1595 def reset_strike_dip(self, *args): 

1596 self.state.strike = 90. 

1597 self.state.dip = 0 

1598 self.gui_state.focal_point = 'center' 

1599 

1600 def get_camera_geometry(self): 

1601 

1602 def rtp2xyz(rtp): 

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

1604 

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

1606 

1607 cam_rtp = num.array([ 

1608 radius+self.state.distance, 

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

1610 self.state.lon * d2r]) 

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

1612 cam, up, foc = \ 

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

1614 

1615 foc_rtp = num.array([ 

1616 radius, 

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

1618 self.state.lon * d2r]) 

1619 

1620 foc = rtp2xyz(foc_rtp) 

1621 

1622 rot_world = pmt.euler_to_matrix( 

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

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

1625 0.0*d2r).T 

1626 

1627 rot_cam = pmt.euler_to_matrix( 

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

1629 

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

1631 

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

1633 up = num.dot(rot, up) 

1634 return cam, up, foc 

1635 

1636 def update_camera(self, *args): 

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

1638 camera = self.ren.GetActiveCamera() 

1639 camera.SetPosition(*cam) 

1640 camera.SetFocalPoint(*foc) 

1641 camera.SetViewUp(*up) 

1642 

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

1644 

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

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

1647 

1648 # if horizon == 0.0: 

1649 # horizon = 2.0 + self.state.distance 

1650 

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

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

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

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

1655 # clip_dist = feature_horizon 

1656 

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

1658 

1659 self.camera_params = ( 

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

1661 

1662 self.update_view() 

1663 

1664 def add_panel( 

1665 self, title_label, panel, 

1666 visible=False, 

1667 # volatile=False, 

1668 tabify=True, 

1669 where=qc.Qt.RightDockWidgetArea, 

1670 remove=None, 

1671 title_controls=[]): 

1672 

1673 dockwidget = common.MyDockWidget( 

1674 self, title_label, title_controls=title_controls) 

1675 

1676 if not visible: 

1677 dockwidget.hide() 

1678 

1679 if not self.gui_state.panels_visible: 

1680 dockwidget.block() 

1681 

1682 dockwidget.setWidget(panel) 

1683 

1684 panel.setParent(dockwidget) 

1685 

1686 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1688 

1689 self.addDockWidget(where, dockwidget) 

1690 

1691 nwrap = 4 

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

1693 self.tabifyDockWidget( 

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

1695 

1696 mitem = dockwidget.toggleViewAction() 

1697 

1698 def update_label(*args): 

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

1700 self.update_slug_abbreviated_lengths() 

1701 

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

1703 dockwidget.titlebar._title_label.title_changed.connect( 

1704 self.update_slug_abbreviated_lengths) 

1705 

1706 update_label() 

1707 

1708 self._panel_togglers[dockwidget] = mitem 

1709 self.panels_menu.addAction(mitem) 

1710 if visible: 

1711 dockwidget.setVisible(True) 

1712 dockwidget.setFocus() 

1713 dockwidget.raise_() 

1714 

1715 def stack_panels(self): 

1716 dockwidgets = self.findChildren(common.MyDockWidget) 

1717 by_area = defaultdict(list) 

1718 for dw in dockwidgets: 

1719 area = self.dockWidgetArea(dw) 

1720 by_area[area].append(dw) 

1721 

1722 for dockwidgets in by_area.values(): 

1723 dw_last = None 

1724 for dw in dockwidgets: 

1725 if dw_last is not None: 

1726 self.tabifyDockWidget(dw_last, dw) 

1727 

1728 dw_last = dw 

1729 

1730 def update_slug_abbreviated_lengths(self): 

1731 dockwidgets = self.findChildren(common.MyDockWidget) 

1732 title_labels = [] 

1733 for dw in dockwidgets: 

1734 title_labels.append(dw.titlebar._title_label) 

1735 

1736 by_title = defaultdict(list) 

1737 for tl in title_labels: 

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

1739 

1740 for group in by_title.values(): 

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

1742 

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

1744 nunique = len(set(slugs)) 

1745 

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

1747 n -= 1 

1748 

1749 if n > 0: 

1750 n = max(3, n) 

1751 

1752 for tl in group: 

1753 tl.set_slug_abbreviated_length(n) 

1754 

1755 def raise_panel(self, panel): 

1756 dockwidget = panel.parent() 

1757 dockwidget.setVisible(True) 

1758 dockwidget.setFocus() 

1759 dockwidget.raise_() 

1760 

1761 def toggle_panel_visibility(self): 

1762 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1763 

1764 def update_panel_visibility(self, *args): 

1765 self.setUpdatesEnabled(False) 

1766 mbar = self.menuBar() 

1767 sbar = self.statusBar() 

1768 dockwidgets = self.findChildren(common.MyDockWidget) 

1769 

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

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

1772 # objects. 

1773 mbar.setFixedHeight( 

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

1775 

1776 sbar.setVisible(self.gui_state.panels_visible) 

1777 for dockwidget in dockwidgets: 

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

1779 

1780 self.setUpdatesEnabled(True) 

1781 

1782 def remove_panel(self, panel): 

1783 dockwidget = panel.parent() 

1784 self.removeDockWidget(dockwidget) 

1785 dockwidget.setParent(None) 

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

1787 

1788 def register_data_provider(self, provider): 

1789 if provider not in self.data_providers: 

1790 self.data_providers.append(provider) 

1791 

1792 def unregister_data_provider(self, provider): 

1793 if provider in self.data_providers: 

1794 self.data_providers.remove(provider) 

1795 

1796 def iter_data(self, name): 

1797 for provider in self.data_providers: 

1798 for data in provider.iter_data(name): 

1799 yield data 

1800 

1801 def closeEvent(self, event): 

1802 self.attach() 

1803 event.accept() 

1804 self.closing = True 

1805 common.get_app().set_main_window(None) 

1806 

1807 def is_closing(self): 

1808 return self.closing 

1809 

1810 

1811class SparrowApp(qw.QApplication): 

1812 def __init__(self): 

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

1814 self.lastWindowClosed.connect(self.myQuit) 

1815 self._main_window = None 

1816 self.setApplicationDisplayName('Sparrow') 

1817 self.setDesktopFileName('Sparrow') 

1818 

1819 def install_sigint_handler(self): 

1820 self._old_signal_handler = signal.signal( 

1821 signal.SIGINT, self.myCloseAllWindows) 

1822 

1823 def uninstall_sigint_handler(self): 

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

1825 

1826 def myQuit(self, *args): 

1827 self.quit() 

1828 

1829 def myCloseAllWindows(self, *args): 

1830 self.closeAllWindows() 

1831 

1832 def set_main_window(self, win): 

1833 self._main_window = win 

1834 

1835 def get_main_window(self): 

1836 return self._main_window 

1837 

1838 def get_progressbars(self): 

1839 if self._main_window: 

1840 return self._main_window.progressbars 

1841 else: 

1842 return None 

1843 

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

1845 win = self.get_main_window() 

1846 if not win: 

1847 return 

1848 

1849 win.statusBar().showMessage( 

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

1851 

1852 

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

1854 

1855 from pyrocko import util 

1856 from pyrocko.gui import util as gui_util 

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

1858 

1859 global win 

1860 

1861 if gui_util.app is None: 

1862 gui_util.app = SparrowApp() 

1863 

1864 # try: 

1865 # from qt_material import apply_stylesheet 

1866 # 

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

1868 # 

1869 # 

1870 # import qdarkgraystyle 

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

1872 # import qdarkstyle 

1873 # 

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

1875 # 

1876 # 

1877 # except ImportError: 

1878 # logger.info( 

1879 # 'Module qdarkgraystyle not available.\n' 

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

1881 # 'qdarkgraystyle".') 

1882 # 

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

1884 

1885 gui_util.app.install_sigint_handler() 

1886 gui_util.app.exec_() 

1887 gui_util.app.uninstall_sigint_handler() 

1888 

1889 del win 

1890 

1891 gc.collect() 

1892 

1893 del gui_util.app