Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/sparrow/main.py: 72%

1081 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-06 06:59 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import math 

7import gc 

8import logging 

9import time 

10import tempfile 

11import os 

12import shutil 

13import platform 

14from collections import defaultdict 

15from subprocess import check_call 

16 

17import numpy as num 

18 

19from pyrocko import cake 

20from pyrocko import guts 

21from pyrocko.dataset import geonames 

22from pyrocko import config 

23from pyrocko import moment_tensor as pmt 

24from pyrocko import util 

25from pyrocko.dataset.util import set_download_callback 

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 

288 download_progress_update = qc.pyqtSignal() 

289 

290 def __init__( 

291 self, 

292 use_depth_peeling=True, 

293 events=None, 

294 snapshots=None, 

295 instant_close=False): 

296 

297 common.set_viewer(self) 

298 

299 qw.QMainWindow.__init__(self) 

300 TalkieConnectionOwner.__init__(self) 

301 

302 self.instant_close = instant_close 

303 

304 self.state = vstate.ViewerState() 

305 self.gui_state = vstate.ViewerGuiState() 

306 

307 self.setWindowTitle('Sparrow') 

308 

309 self.setTabPosition( 

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

311 

312 self.planet_radius = cake.earthradius 

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

314 

315 self._block_capture = 0 

316 self._undo_stack = [] 

317 self._redo_stack = [] 

318 self._undo_aggregate = None 

319 

320 self._panel_togglers = {} 

321 self._actors = set() 

322 self._actors_2d = set() 

323 self._render_window_size = (0, 0) 

324 self._use_depth_peeling = use_depth_peeling 

325 self._in_update_elements = False 

326 self._update_elements_enabled = True 

327 

328 self._animation_tstart = None 

329 self._animation_iframe = None 

330 self._animation = None 

331 

332 mbar = qw.QMenuBar() 

333 self.setMenuBar(mbar) 

334 

335 menu = mbar.addMenu('File') 

336 

337 menu.addAction( 

338 'Export Image...', 

339 self.export_image, 

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

341 qc.Qt.ApplicationShortcut) 

342 

343 menu.addAction( 

344 'Quit', 

345 self.close, 

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

347 qc.Qt.ApplicationShortcut) 

348 

349 menu = mbar.addMenu('Edit') 

350 

351 menu.addAction( 

352 'Undo', 

353 self.undo, 

354 qg.QKeySequence( 

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

356 qc.Qt.ApplicationShortcut) 

357 

358 menu.addAction( 

359 'Redo', 

360 self.redo, 

361 qg.QKeySequence( 

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

363 qc.Qt.ApplicationShortcut) 

364 

365 menu = mbar.addMenu('View') 

366 menu_sizes = menu.addMenu('Size') 

367 self._add_vtk_widget_size_menu_entries(menu_sizes) 

368 

369 # detached/attached 

370 self.talkie_connect( 

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

372 

373 action = qw.QAction('Detach') 

374 action.setCheckable(True) 

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

376 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

377 

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

379 menu.addAction(action) 

380 

381 # hide controls 

382 action = qw.QAction('Hide Controls', self) 

383 action.setCheckable(True) 

384 action.setShortcut(qc.Qt.Key_Space) 

385 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

386 action.triggered.connect(self.toggle_panel_visibility) 

387 menu.addAction(action) 

388 

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

390 self.panels_menu.addAction( 

391 'Stack Panels', 

392 self.stack_panels) 

393 self.panels_menu.addSeparator() 

394 

395 snapshots_menu = mbar.addMenu('Snapshots') 

396 

397 menu = mbar.addMenu('Elements') 

398 for name, estate in sorted([ 

399 ('Icosphere', elements.IcosphereState( 

400 level=4, 

401 smooth=True, 

402 opacity=0.5, 

403 ambient=0.1)), 

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

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

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

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

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

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

410 ('Rectangular Source', elements.SourceState()), 

411 ('HUD Subtitle', elements.HudState( 

412 template='Subtitle')), 

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

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

415 position='top-left')), 

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

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

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

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

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

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

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

423 

424 def wrap_add_element(estate): 

425 def add_element(*args): 

426 new_element = guts.clone(estate) 

427 new_element.element_id = elements.random_id() 

428 self.state.elements.append(new_element) 

429 self.state.sort_elements() 

430 

431 return add_element 

432 

433 mitem = qw.QAction(name, self) 

434 

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

436 

437 menu.addAction(mitem) 

438 

439 menu = mbar.addMenu('Help') 

440 

441 menu.addAction( 

442 'Interactive Tour', 

443 self.start_tour) 

444 

445 menu.addAction( 

446 'Online Manual', 

447 self.open_manual) 

448 

449 self.data_providers = [] 

450 self.elements = {} 

451 

452 self.detached_window = None 

453 

454 self.main_frame = qw.QFrame() 

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

456 

457 self.vtk_frame = CenteringScrollArea() 

458 

459 self.vtk_widget = QVTKWidget(self, self) 

460 self.vtk_frame.setWidget(self.vtk_widget) 

461 

462 self.main_layout = qw.QVBoxLayout() 

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

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

465 

466 pb = Progressbars(self) 

467 self.progressbars = pb 

468 self.main_layout.addWidget(pb) 

469 

470 self.main_frame.setLayout(self.main_layout) 

471 

472 self.vtk_frame_substitute = None 

473 

474 self.add_panel( 

475 'Navigation', 

476 self.controls_navigation(), 

477 visible=True, 

478 scrollable=False, 

479 where=qc.Qt.LeftDockWidgetArea) 

480 

481 self.add_panel( 

482 'Time', 

483 self.controls_time(), 

484 visible=True, 

485 scrollable=False, 

486 where=qc.Qt.LeftDockWidgetArea) 

487 

488 self.add_panel( 

489 'Appearance', 

490 self.controls_appearance(), 

491 visible=True, 

492 scrollable=False, 

493 where=qc.Qt.LeftDockWidgetArea) 

494 

495 snapshots_panel = self.controls_snapshots() 

496 self.snapshots_panel = snapshots_panel 

497 self.add_panel( 

498 'Snapshots', 

499 snapshots_panel, 

500 visible=False, 

501 scrollable=False, 

502 where=qc.Qt.LeftDockWidgetArea) 

503 

504 snapshots_panel.setup_menu(snapshots_menu) 

505 

506 self.setCentralWidget(self.main_frame) 

507 

508 self.mesh = None 

509 

510 ren = vtk.vtkRenderer() 

511 

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

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

514 # ren.TwoSidedLightingOn() 

515 # ren.SetUseShadows(1) 

516 

517 self._lighting = None 

518 self._background = None 

519 

520 self.ren = ren 

521 self.update_render_settings() 

522 self.update_camera() 

523 

524 renwin = self.vtk_widget.GetRenderWindow() 

525 

526 if self._use_depth_peeling: 

527 renwin.SetAlphaBitPlanes(1) 

528 renwin.SetMultiSamples(0) 

529 

530 ren.SetUseDepthPeeling(1) 

531 ren.SetMaximumNumberOfPeels(100) 

532 ren.SetOcclusionRatio(0.1) 

533 

534 ren.SetUseFXAA(1) 

535 # ren.SetUseHiddenLineRemoval(1) 

536 # ren.SetBackingStore(1) 

537 

538 self.renwin = renwin 

539 

540 # renwin.LineSmoothingOn() 

541 # renwin.PointSmoothingOn() 

542 # renwin.PolygonSmoothingOn() 

543 

544 renwin.AddRenderer(ren) 

545 

546 iren = renwin.GetInteractor() 

547 iren.LightFollowCameraOn() 

548 iren.SetInteractorStyle(None) 

549 

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

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

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

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

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

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

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

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

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

559 

560 renwin.Render() 

561 

562 iren.Initialize() 

563 

564 self.iren = iren 

565 

566 self.rotating = False 

567 

568 self._elements = {} 

569 self._elements_active = {} 

570 

571 self.talkie_connect( 

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

573 

574 self.state.elements.append(elements.IcosphereState( 

575 element_id='icosphere', 

576 level=4, 

577 smooth=True, 

578 opacity=0.5, 

579 ambient=0.1)) 

580 

581 self.state.elements.append(elements.GridState( 

582 element_id='grid')) 

583 self.state.elements.append(elements.CoastlinesState( 

584 element_id='coastlines')) 

585 self.state.elements.append(elements.CrosshairState( 

586 element_id='crosshair')) 

587 

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

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

590 # self.state.elements.append( 

591 # elements.CatalogState( 

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

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

594 

595 if events: 

596 self.state.elements.append( 

597 elements.CatalogState( 

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

599 

600 self.state.sort_elements() 

601 

602 if snapshots: 

603 snapshots_ = [] 

604 for obj in snapshots: 

605 if isinstance(obj, str): 

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

607 else: 

608 snapshots_.append(obj) 

609 

610 snapshots_panel.add_snapshots(snapshots_) 

611 self.raise_panel(snapshots_panel) 

612 snapshots_panel.goto_snapshot(1) 

613 

614 self.timer = qc.QTimer(self) 

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

616 self.timer.setInterval(1000) 

617 self.timer.start() 

618 

619 self._animation_saver = None 

620 

621 self.closing = False 

622 self.vtk_widget.setFocus() 

623 

624 self.update_detached() 

625 

626 self.status( 

627 'Pyrocko Sparrow - A bird\'s eye view.', 2.0) 

628 

629 self.status( 

630 'Let\'s fly.', 2.0) 

631 

632 self.show() 

633 self.windowHandle().showMaximized() 

634 

635 self.talkie_connect( 

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

637 

638 self.update_vtk_widget_size() 

639 

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

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

642 

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

644 self.capture_state() 

645 

646 set_download_callback(self.update_download_progress) 

647 

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

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

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

651 

652 self.start_tour() 

653 

654 def update_download_progress(self, message, args): 

655 self.download_progress_update.emit() 

656 

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

658 self.statusBar().showMessage( 

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

660 

661 def disable_capture(self): 

662 self._block_capture += 1 

663 

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

665 

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

667 if self._block_capture > 0: 

668 self._block_capture -= 1 

669 

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

671 

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

673 self.capture_state(aggregate=aggregate) 

674 

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

676 if self._block_capture: 

677 return 

678 

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

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

681 

682 if aggregate is not None: 

683 if aggregate == self._undo_aggregate: 

684 self._undo_stack.pop() 

685 

686 self._undo_aggregate = aggregate 

687 else: 

688 self._undo_aggregate = None 

689 

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

691 len(self._undo_stack) + 1, 

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

693 '\n'.join( 

694 ' - %s' % s 

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

696 self.state).splitlines()) 

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

698 

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

700 self._redo_stack.clear() 

701 

702 def undo(self): 

703 self._undo_aggregate = None 

704 

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

706 return 

707 

708 state = self._undo_stack.pop() 

709 self._redo_stack.append(state) 

710 state = self._undo_stack[-1] 

711 

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

713 len(self._undo_stack), 

714 '\n'.join( 

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

716 

717 self.disable_capture() 

718 try: 

719 self.set_state(state) 

720 finally: 

721 self.enable_capture(drop=True) 

722 

723 def redo(self): 

724 self._undo_aggregate = None 

725 

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

727 return 

728 

729 state = self._redo_stack.pop() 

730 self._undo_stack.append(state) 

731 

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

733 len(self._redo_stack), 

734 '\n'.join( 

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

736 

737 self.disable_capture() 

738 try: 

739 self.set_state(state) 

740 finally: 

741 self.enable_capture(drop=True) 

742 

743 def start_tour(self): 

744 snapshots_ = snapshots_mod.load_snapshots( 

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

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

747 self.snapshots_panel.add_snapshots(snapshots_) 

748 self.raise_panel(self.snapshots_panel) 

749 self.snapshots_panel.transition_to_next_snapshot() 

750 

751 def open_manual(self): 

752 import webbrowser 

753 webbrowser.open( 

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

755 

756 def _add_vtk_widget_size_menu_entries(self, menu): 

757 

758 group = qw.QActionGroup(menu) 

759 group.setExclusive(True) 

760 

761 def set_variable_size(): 

762 self.gui_state.fixed_size = False 

763 

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

765 variable_size_action.setCheckable(True) 

766 variable_size_action.setActionGroup(group) 

767 variable_size_action.triggered.connect(set_variable_size) 

768 

769 fixed_size_items = [] 

770 for nx, ny, label in [ 

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

772 (426, 240, ''), 

773 (640, 360, ''), 

774 (854, 480, '(FWVGA)'), 

775 (1280, 720, '(HD)'), 

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

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

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

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

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

781 (640, 480, '(VGA)'), 

782 (800, 600, '(SVGA)'), 

783 (None, None, 'Other'), 

784 (512, 512, ''), 

785 (1024, 1024, '')]: 

786 

787 if None in (nx, ny): 

788 menu.addSection(label) 

789 else: 

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

791 action = menu.addAction(name) 

792 action.setCheckable(True) 

793 action.setActionGroup(group) 

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

795 

796 def make_set_fixed_size(nx, ny): 

797 def set_fixed_size(): 

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

799 

800 return set_fixed_size 

801 

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

803 

804 def update_widget(*args): 

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

806 action.blockSignals(True) 

807 action.setChecked( 

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

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

810 action.blockSignals(False) 

811 

812 variable_size_action.blockSignals(True) 

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

814 variable_size_action.blockSignals(False) 

815 

816 update_widget() 

817 self.talkie_connect( 

818 self.gui_state, 'fixed_size', update_widget) 

819 

820 def update_vtk_widget_size(self, *args): 

821 if self.gui_state.fixed_size: 

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

823 wanted_size = qc.QSize(nx, ny) 

824 else: 

825 wanted_size = qc.QSize( 

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

827 

828 current_size = self.vtk_widget.size() 

829 

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

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

832 

833 self.vtk_widget.setFixedSize(wanted_size) 

834 

835 self.vtk_frame.recenter() 

836 self.check_vtk_resize() 

837 

838 def update_focal_point(self, *args): 

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

840 self.vtk_widget.setStatusTip( 

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

842 'change view plane orientation.' % g_modifier_key) 

843 else: 

844 self.vtk_widget.setStatusTip( 

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

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

847 'reverse sense.' % g_modifier_key) 

848 

849 def update_detached(self, *args): 

850 

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

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

853 

854 self.main_layout.removeWidget(self.vtk_frame) 

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

856 self.detached_window.show() 

857 self.vtk_widget.setFocus() 

858 

859 screens = common.get_app().screens() 

860 if len(screens) > 1: 

861 for screen in screens: 

862 if screen is not self.screen(): 

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

864 # .setScreen() does not work reliably, 

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

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

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

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

869 

870 self.detached_window.windowHandle().showMaximized() 

871 

872 frame = qw.QFrame() 

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

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

875 # frame.setAutoFillBackground(True) 

876 frame.setSizePolicy( 

877 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

878 

879 layout = qw.QGridLayout() 

880 frame.setLayout(layout) 

881 self.main_layout.insertWidget(0, frame) 

882 

883 self.state_editor = StateEditor(self) 

884 

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

886 

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

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

889 # layout.addWidget( 

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

891 

892 self.vtk_frame_substitute = frame 

893 

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

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

896 self.detached_window.hide() 

897 self.vtk_frame.setParent(self) 

898 if self.vtk_frame_substitute: 

899 self.main_layout.removeWidget(self.vtk_frame_substitute) 

900 self.state_editor.unbind_state() 

901 self.vtk_frame_substitute = None 

902 

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

904 self.detached_window = None 

905 self.vtk_widget.setFocus() 

906 

907 def attach(self): 

908 self.gui_state.detached = False 

909 

910 def export_image(self): 

911 

912 caption = 'Export Image' 

913 fn_out, _ = qw.QFileDialog.getSaveFileName( 

914 self, caption, 'image.png', 

915 options=common.qfiledialog_options) 

916 

917 if fn_out: 

918 self.save_image(fn_out) 

919 

920 def save_image(self, path): 

921 

922 original_fixed_size = self.gui_state.fixed_size 

923 if original_fixed_size is None: 

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

925 

926 wif = vtk.vtkWindowToImageFilter() 

927 wif.SetInput(self.renwin) 

928 wif.SetInputBufferTypeToRGBA() 

929 wif.SetScale(1, 1) 

930 wif.ReadFrontBufferOff() 

931 writer = vtk.vtkPNGWriter() 

932 writer.SetInputConnection(wif.GetOutputPort()) 

933 

934 self.renwin.Render() 

935 wif.Modified() 

936 writer.SetFileName(path) 

937 writer.Write() 

938 

939 self.gui_state.fixed_size = original_fixed_size 

940 

941 def update_render_settings(self, *args): 

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

943 self.ren.RemoveAllLights() 

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

945 self.ren.AddLight(li) 

946 

947 self._lighting = self.state.lighting 

948 

949 if self._background is None \ 

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

951 

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

953 self._background = self.state.background 

954 

955 self.update_view() 

956 

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

958 if self._animation: 

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

960 self.stop_animation() 

961 

962 self.disable_capture() 

963 self._animation = interpolator 

964 if output_path is None: 

965 self._animation_tstart = time.time() 

966 self._animation_iframe = None 

967 else: 

968 self._animation_iframe = 0 

969 mess = 'Rendering movie' 

970 self.progressbars.set_status(mess, 0, can_abort=True) 

971 

972 self._animation_timer = qc.QTimer(self) 

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

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

975 self._animation_timer.start() 

976 if output_path is not None: 

977 original_fixed_size = self.gui_state.fixed_size 

978 if original_fixed_size is None: 

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

980 

981 wif = vtk.vtkWindowToImageFilter() 

982 wif.SetInput(self.renwin) 

983 wif.SetInputBufferTypeToRGBA() 

984 wif.SetScale(1, 1) 

985 wif.ReadFrontBufferOff() 

986 writer = vtk.vtkPNGWriter() 

987 temp_path = tempfile.mkdtemp() 

988 self._animation_saver = ( 

989 wif, writer, temp_path, output_path, original_fixed_size) 

990 writer.SetInputConnection(wif.GetOutputPort()) 

991 

992 def next_animation_frame(self): 

993 

994 ani = self._animation 

995 if not ani: 

996 return 

997 

998 if self._animation_iframe is not None: 

999 state = ani( 

1000 ani.tmin 

1001 + self._animation_iframe * ani.dt) 

1002 

1003 self._animation_iframe += 1 

1004 else: 

1005 tnow = time.time() 

1006 state = ani(min( 

1007 ani.tmax, 

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

1009 

1010 self.set_state(state) 

1011 self.renwin.Render() 

1012 abort = False 

1013 if self._animation_saver: 

1014 abort = self.progressbars.set_status( 

1015 'Rendering movie', 

1016 100*self._animation_iframe*ani.dt / (ani.tmax - ani.tmin), 

1017 can_abort=True) 

1018 

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

1020 wif.Modified() 

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

1022 writer.SetFileName(fn % self._animation_iframe) 

1023 writer.Write() 

1024 

1025 if self._animation_iframe is not None: 

1026 t = self._animation_iframe * ani.dt 

1027 else: 

1028 t = tnow - self._animation_tstart 

1029 

1030 if t > ani.tmax - ani.tmin or abort: 

1031 self.stop_animation() 

1032 

1033 def stop_animation(self): 

1034 if self._animation_timer: 

1035 self._animation_timer.stop() 

1036 

1037 if self._animation_saver: 

1038 

1039 wif, writer, temp_path, output_path, original_fixed_size \ 

1040 = self._animation_saver 

1041 self.gui_state.fixed_size = original_fixed_size 

1042 

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

1044 check_call([ 

1045 'ffmpeg', '-y', 

1046 '-i', fn_path, 

1047 '-c:v', 'libx264', 

1048 '-preset', 'slow', 

1049 '-crf', '17', 

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

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

1052 output_path]) 

1053 shutil.rmtree(temp_path) 

1054 

1055 self._animation_saver = None 

1056 self._animation_saver 

1057 

1058 self.progressbars.set_status( 

1059 'Rendering movie', 100, can_abort=True) 

1060 

1061 self._animation_tstart = None 

1062 self._animation_iframe = None 

1063 self._animation = None 

1064 self.enable_capture() 

1065 

1066 def set_state(self, state): 

1067 self.disable_capture() 

1068 try: 

1069 self._update_elements_enabled = False 

1070 self.setUpdatesEnabled(False) 

1071 self.state.diff_update(state) 

1072 self.state.sort_elements() 

1073 self.setUpdatesEnabled(True) 

1074 self._update_elements_enabled = True 

1075 self.update_elements() 

1076 finally: 

1077 self.enable_capture() 

1078 

1079 def periodical(self): 

1080 pass 

1081 

1082 def check_vtk_resize(self, *args): 

1083 render_window_size = self.renwin.GetSize() 

1084 if self._render_window_size != render_window_size: 

1085 self._render_window_size = render_window_size 

1086 self.resize_event(*render_window_size) 

1087 

1088 def update_elements(self, *_): 

1089 if not self._update_elements_enabled: 

1090 return 

1091 

1092 if self._in_update_elements: 

1093 return 

1094 

1095 self._in_update_elements = True 

1096 for estate in self.state.elements: 

1097 if estate.element_id not in self._elements: 

1098 new_element = estate.create() 

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

1100 type(new_element).__name__, 

1101 estate.element_id)) 

1102 self._elements[estate.element_id] = new_element 

1103 

1104 element = self._elements[estate.element_id] 

1105 

1106 if estate.element_id not in self._elements_active: 

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

1108 type(element).__name__, 

1109 estate.element_id)) 

1110 element.bind_state(estate) 

1111 element.set_parent(self) 

1112 self._elements_active[estate.element_id] = element 

1113 

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

1115 deactivate = [] 

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

1117 if element_id not in state_element_ids: 

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

1119 type(element).__name__, 

1120 element_id)) 

1121 element.unset_parent() 

1122 deactivate.append(element_id) 

1123 

1124 for element_id in deactivate: 

1125 del self._elements_active[element_id] 

1126 

1127 self._update_crosshair_bindings() 

1128 

1129 self._in_update_elements = False 

1130 

1131 def _update_crosshair_bindings(self): 

1132 

1133 def get_crosshair_element(): 

1134 for element in self.state.elements: 

1135 if element.element_id == 'crosshair': 

1136 return element 

1137 

1138 return None 

1139 

1140 crosshair = get_crosshair_element() 

1141 if crosshair is None or crosshair.is_connected: 

1142 return 

1143 

1144 def to_checkbox(state, widget): 

1145 widget.blockSignals(True) 

1146 widget.setChecked(state.visible) 

1147 widget.blockSignals(False) 

1148 

1149 def to_state(widget, state): 

1150 state.visible = widget.isChecked() 

1151 

1152 cb = self._crosshair_checkbox 

1153 vstate.state_bind( 

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

1155 cb, [cb.toggled], to_checkbox) 

1156 

1157 crosshair.is_connected = True 

1158 

1159 def add_actor_2d(self, actor): 

1160 if actor not in self._actors_2d: 

1161 self.ren.AddActor2D(actor) 

1162 self._actors_2d.add(actor) 

1163 

1164 def remove_actor_2d(self, actor): 

1165 if actor in self._actors_2d: 

1166 self.ren.RemoveActor2D(actor) 

1167 self._actors_2d.remove(actor) 

1168 

1169 def add_actor(self, actor): 

1170 if actor not in self._actors: 

1171 self.ren.AddActor(actor) 

1172 self._actors.add(actor) 

1173 

1174 def add_actor_list(self, actorlist): 

1175 for actor in actorlist: 

1176 self.add_actor(actor) 

1177 

1178 def remove_actor(self, actor): 

1179 if actor in self._actors: 

1180 self.ren.RemoveActor(actor) 

1181 self._actors.remove(actor) 

1182 

1183 def update_view(self): 

1184 self.vtk_widget.update() 

1185 

1186 def resize_event(self, size_x, size_y): 

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

1188 

1189 def button_event(self, obj, event): 

1190 if event == "LeftButtonPressEvent": 

1191 self.rotating = True 

1192 elif event == "LeftButtonReleaseEvent": 

1193 self.rotating = False 

1194 

1195 def mouse_move_event(self, obj, event): 

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

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

1198 

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

1200 center_x = size_x / 2.0 

1201 center_y = size_y / 2.0 

1202 

1203 if self.rotating: 

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

1205 

1206 def myWheelEvent(self, event): 

1207 

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

1209 

1210 if angle > 200: 

1211 angle = 200 

1212 

1213 if angle < -200: 

1214 angle = -200 

1215 

1216 self.disable_capture() 

1217 try: 

1218 self.do_dolly(-angle/100.) 

1219 finally: 

1220 self.enable_capture(aggregate='distance') 

1221 

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

1223 

1224 dx = x0 - x 

1225 dy = y0 - y 

1226 

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

1228 focp = self.gui_state.focal_point 

1229 

1230 if focp == 'center': 

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

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

1233 

1234 lat = self.state.lat 

1235 lon = self.state.lon 

1236 factor = self.state.distance / 10.0 

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

1238 else: 

1239 lat = 90. - self.state.dip 

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

1241 factor = 0.5 

1242 factor_lat = 1.0 

1243 

1244 dlat = dy * factor 

1245 dlon = dx * factor * factor_lat 

1246 

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

1248 lon += dlon 

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

1250 

1251 if focp == 'center': 

1252 self.state.lat = float(lat) 

1253 self.state.lon = float(lon) 

1254 else: 

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

1256 self.state.strike = float(((-(lon + 90.))+180.) % 360. - 180.) 

1257 

1258 def do_dolly(self, v): 

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

1260 

1261 def key_down_event(self, obj, event): 

1262 k = obj.GetKeyCode() 

1263 if k == 'f': 

1264 self.gui_state.next_focal_point() 

1265 

1266 elif k == 'r': 

1267 self.reset_strike_dip() 

1268 

1269 elif k == 'p': 

1270 print(self.state) 

1271 

1272 elif k == 'i': 

1273 for elem in self.state.elements: 

1274 if isinstance(elem, elements.IcosphereState): 

1275 elem.visible = not elem.visible 

1276 

1277 elif k == 'c': 

1278 for elem in self.state.elements: 

1279 if isinstance(elem, elements.CoastlinesState): 

1280 elem.visible = not elem.visible 

1281 

1282 elif k == 't': 

1283 if not any( 

1284 isinstance(elem, elements.TopoState) 

1285 for elem in self.state.elements): 

1286 

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

1288 else: 

1289 for elem in self.state.elements: 

1290 if isinstance(elem, elements.TopoState): 

1291 elem.visible = not elem.visible 

1292 

1293 # elif k == ' ': 

1294 # self.toggle_panel_visibility() 

1295 

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

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

1298 

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

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

1301 

1302 def controls_navigation(self): 

1303 frame = qw.QFrame(self) 

1304 frame.setSizePolicy( 

1305 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1306 layout = qw.QGridLayout() 

1307 frame.setLayout(layout) 

1308 

1309 # lat, lon, depth 

1310 

1311 layout.addWidget( 

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

1313 

1314 le = qw.QLineEdit() 

1315 le.setStatusTip( 

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

1317 'Focal point location.') 

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

1319 

1320 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1323 

1324 def lineedit_to_lat_lon_depth(widget, state): 

1325 self.disable_capture() 

1326 try: 

1327 s = str(widget.text()) 

1328 choices = location_to_choices(s) 

1329 if len(choices) > 0: 

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

1331 choices[0].get_lat_lon_depth() 

1332 else: 

1333 raise NoLocationChoices(s) 

1334 

1335 finally: 

1336 self.enable_capture() 

1337 

1338 self._state_bind( 

1339 ['lat', 'lon', 'depth'], 

1340 lineedit_to_lat_lon_depth, 

1341 le, [le.editingFinished, le.returnPressed], 

1342 lat_lon_depth_to_lineedit) 

1343 

1344 self.lat_lon_lineedit = le 

1345 

1346 # focal point 

1347 

1348 cb = qw.QCheckBox('Fix') 

1349 cb.setStatusTip( 

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

1351 % g_modifier_key) 

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

1353 

1354 def focal_point_to_checkbox(state, widget): 

1355 widget.blockSignals(True) 

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

1357 widget.blockSignals(False) 

1358 

1359 def checkbox_to_focal_point(widget, state): 

1360 self.gui_state.focal_point = \ 

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

1362 

1363 self._gui_state_bind( 

1364 ['focal_point'], checkbox_to_focal_point, 

1365 cb, [cb.toggled], focal_point_to_checkbox) 

1366 

1367 self.focal_point_checkbox = cb 

1368 

1369 self.talkie_connect( 

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

1371 

1372 self.update_focal_point() 

1373 

1374 # strike, dip 

1375 

1376 layout.addWidget( 

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

1378 

1379 le = qw.QLineEdit() 

1380 le.setStatusTip( 

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

1382 'direction.') 

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

1384 

1385 def strike_dip_to_lineedit(state, widget): 

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

1387 

1388 def lineedit_to_strike_dip(widget, state): 

1389 s = str(widget.text()) 

1390 string_to_strike_dip = { 

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

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

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

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

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

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

1397 

1398 self.disable_capture() 

1399 if s in string_to_strike_dip: 

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

1401 

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

1403 try: 

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

1405 except Exception: 

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

1407 finally: 

1408 self.enable_capture() 

1409 

1410 self._state_bind( 

1411 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1413 

1414 self.strike_dip_lineedit = le 

1415 

1416 but = qw.QPushButton('Reset') 

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

1418 but.clicked.connect(self.reset_strike_dip) 

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

1420 

1421 # crosshair 

1422 

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

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

1425 

1426 # camera bindings 

1427 self.talkie_connect( 

1428 self.state, 

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

1430 self.update_camera) 

1431 

1432 self.talkie_connect( 

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

1434 

1435 return frame 

1436 

1437 def controls_time(self): 

1438 frame = qw.QFrame(self) 

1439 frame.setSizePolicy( 

1440 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1441 

1442 layout = qw.QGridLayout() 

1443 frame.setLayout(layout) 

1444 

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

1446 le_tmin = qw.QLineEdit() 

1447 layout.addWidget(le_tmin, 0, 1) 

1448 

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

1450 le_tmax = qw.QLineEdit() 

1451 layout.addWidget(le_tmax, 1, 1) 

1452 

1453 label_tcursor = qw.QLabel() 

1454 

1455 label_tcursor.setSizePolicy( 

1456 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1457 

1458 layout.addWidget(label_tcursor, 2, 1) 

1459 self._label_tcursor = label_tcursor 

1460 

1461 self._state_bind( 

1462 ['tmin'], common.lineedit_to_time, le_tmin, 

1463 [le_tmin.editingFinished, le_tmin.returnPressed], 

1464 common.time_to_lineedit, 

1465 attribute='tmin') 

1466 self._state_bind( 

1467 ['tmax'], common.lineedit_to_time, le_tmax, 

1468 [le_tmax.editingFinished, le_tmax.returnPressed], 

1469 common.time_to_lineedit, 

1470 attribute='tmax') 

1471 

1472 self.tmin_lineedit = le_tmin 

1473 self.tmax_lineedit = le_tmax 

1474 

1475 range_edit = RangeEdit() 

1476 range_edit.rangeEditPressed.connect(self.disable_capture) 

1477 range_edit.rangeEditReleased.connect(self.enable_capture) 

1478 range_edit.set_data_provider(self) 

1479 range_edit.set_data_name('time') 

1480 

1481 xblock = [False] 

1482 

1483 def range_to_range_edit(state, widget): 

1484 if not xblock[0]: 

1485 widget.blockSignals(True) 

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

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

1488 widget.blockSignals(False) 

1489 

1490 def range_edit_to_range(widget, state): 

1491 xblock[0] = True 

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

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

1494 xblock[0] = False 

1495 

1496 self._state_bind( 

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

1498 range_edit_to_range, 

1499 range_edit, 

1500 [range_edit.rangeChanged, range_edit.focusChanged], 

1501 range_to_range_edit) 

1502 

1503 def handle_tcursor_changed(): 

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

1505 

1506 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1507 

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

1509 

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

1511 le_focus = qw.QLineEdit() 

1512 layout.addWidget(le_focus, 4, 1) 

1513 

1514 def focus_to_lineedit(state, widget): 

1515 if state.tduration is None: 

1516 widget.setText('') 

1517 else: 

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

1519 guts.str_duration(state.tduration), 

1520 state.tposition)) 

1521 

1522 def lineedit_to_focus(widget, state): 

1523 s = str(widget.text()) 

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

1525 try: 

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

1527 state.tduration = None 

1528 state.tposition = 0.0 

1529 else: 

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

1531 if len(w) > 1: 

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

1533 else: 

1534 state.tposition = 0.0 

1535 

1536 except Exception: 

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

1538 

1539 self._state_bind( 

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

1541 [le_focus.editingFinished, le_focus.returnPressed], 

1542 focus_to_lineedit) 

1543 

1544 label_effective_tmin = qw.QLabel() 

1545 label_effective_tmax = qw.QLabel() 

1546 

1547 label_effective_tmin.setSizePolicy( 

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

1549 label_effective_tmax.setSizePolicy( 

1550 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1551 label_effective_tmin.setMinimumSize( 

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

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

1554 

1555 layout.addWidget(label_effective_tmin, 5, 1) 

1556 layout.addWidget(label_effective_tmax, 6, 1) 

1557 

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

1559 self.talkie_connect( 

1560 self.state, var, self.update_effective_time_labels) 

1561 

1562 self._label_effective_tmin = label_effective_tmin 

1563 self._label_effective_tmax = label_effective_tmax 

1564 

1565 self.talkie_connect( 

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

1567 

1568 return frame 

1569 

1570 def controls_appearance(self): 

1571 frame = qw.QFrame(self) 

1572 frame.setSizePolicy( 

1573 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1574 layout = qw.QGridLayout() 

1575 frame.setLayout(layout) 

1576 

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

1578 

1579 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1580 layout.addWidget(cb, 0, 1) 

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

1582 

1583 self.talkie_connect( 

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

1585 

1586 # background 

1587 

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

1589 

1590 cb = common.strings_to_combobox( 

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

1592 

1593 layout.addWidget(cb, 1, 1) 

1594 vstate.state_bind_combobox_background( 

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

1596 

1597 self.talkie_connect( 

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

1599 

1600 return frame 

1601 

1602 def controls_snapshots(self): 

1603 return snapshots_mod.SnapshotsPanel(self) 

1604 

1605 def update_effective_time_labels(self, *args): 

1606 tmin = self.state.tmin_effective 

1607 tmax = self.state.tmax_effective 

1608 

1609 stmin = common.time_or_none_to_str(tmin) 

1610 stmax = common.time_or_none_to_str(tmax) 

1611 

1612 self._label_effective_tmin.setText(stmin) 

1613 self._label_effective_tmax.setText(stmax) 

1614 

1615 def update_tcursor(self, *args): 

1616 tcursor = self.gui_state.tcursor 

1617 stcursor = common.time_or_none_to_str(tcursor) 

1618 self._label_tcursor.setText(stcursor) 

1619 

1620 def reset_strike_dip(self, *args): 

1621 self.state.strike = 90. 

1622 self.state.dip = 0 

1623 self.gui_state.focal_point = 'center' 

1624 

1625 def get_camera_geometry(self): 

1626 

1627 def rtp2xyz(rtp): 

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

1629 

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

1631 

1632 cam_rtp = num.array([ 

1633 radius+self.state.distance, 

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

1635 self.state.lon * d2r]) 

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

1637 cam, up, foc = \ 

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

1639 

1640 foc_rtp = num.array([ 

1641 radius, 

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

1643 self.state.lon * d2r]) 

1644 

1645 foc = rtp2xyz(foc_rtp) 

1646 

1647 rot_world = pmt.euler_to_matrix( 

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

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

1650 0.0*d2r).T 

1651 

1652 rot_cam = pmt.euler_to_matrix( 

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

1654 

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

1656 

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

1658 up = num.dot(rot, up) 

1659 return cam, up, foc 

1660 

1661 def update_camera(self, *args): 

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

1663 camera = self.ren.GetActiveCamera() 

1664 camera.SetPosition(*cam) 

1665 camera.SetFocalPoint(*foc) 

1666 camera.SetViewUp(*up) 

1667 

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

1669 

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

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

1672 

1673 # if horizon == 0.0: 

1674 # horizon = 2.0 + self.state.distance 

1675 

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

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

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

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

1680 # clip_dist = feature_horizon 

1681 

1682 camera.SetClippingRange( 

1683 max(clip_dist*0.00001, clip_dist-3.0), clip_dist) 

1684 

1685 self.camera_params = ( 

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

1687 

1688 self.update_view() 

1689 

1690 def add_panel( 

1691 self, title_label, panel, 

1692 visible=False, 

1693 # volatile=False, 

1694 tabify=True, 

1695 where=qc.Qt.RightDockWidgetArea, 

1696 remove=None, 

1697 title_controls=[], 

1698 scrollable=True): 

1699 

1700 dockwidget = common.MyDockWidget( 

1701 self, title_label, title_controls=title_controls) 

1702 

1703 if not visible: 

1704 dockwidget.hide() 

1705 

1706 if not self.gui_state.panels_visible: 

1707 dockwidget.block() 

1708 

1709 if scrollable: 

1710 scrollarea = common.MyScrollArea() 

1711 scrollarea.setWidget(panel) 

1712 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

1713 scrollarea.setSizeAdjustPolicy( 

1714 qw.QAbstractScrollArea.AdjustToContents) 

1715 scrollarea.setFrameShape(qw.QFrame.NoFrame) 

1716 

1717 dockwidget.setWidget(scrollarea) 

1718 else: 

1719 dockwidget.setWidget(panel) 

1720 

1721 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1723 

1724 self.addDockWidget(where, dockwidget) 

1725 

1726 nwrap = 4 

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

1728 self.tabifyDockWidget( 

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

1730 

1731 mitem = dockwidget.toggleViewAction() 

1732 

1733 def update_label(*args): 

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

1735 self.update_slug_abbreviated_lengths() 

1736 

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

1738 dockwidget.titlebar._title_label.title_changed.connect( 

1739 self.update_slug_abbreviated_lengths) 

1740 

1741 update_label() 

1742 

1743 self._panel_togglers[dockwidget] = mitem 

1744 self.panels_menu.addAction(mitem) 

1745 if visible: 

1746 dockwidget.setVisible(True) 

1747 dockwidget.setFocus() 

1748 dockwidget.raise_() 

1749 

1750 def stack_panels(self): 

1751 dockwidgets = self.findChildren(common.MyDockWidget) 

1752 by_area = defaultdict(list) 

1753 for dw in dockwidgets: 

1754 area = self.dockWidgetArea(dw) 

1755 by_area[area].append(dw) 

1756 

1757 for dockwidgets in by_area.values(): 

1758 dw_last = None 

1759 for dw in dockwidgets: 

1760 if dw_last is not None: 

1761 self.tabifyDockWidget(dw_last, dw) 

1762 

1763 dw_last = dw 

1764 

1765 def update_slug_abbreviated_lengths(self): 

1766 dockwidgets = self.findChildren(common.MyDockWidget) 

1767 title_labels = [] 

1768 for dw in dockwidgets: 

1769 title_labels.append(dw.titlebar._title_label) 

1770 

1771 by_title = defaultdict(list) 

1772 for tl in title_labels: 

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

1774 

1775 for group in by_title.values(): 

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

1777 

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

1779 nunique = len(set(slugs)) 

1780 

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

1782 n -= 1 

1783 

1784 if n > 0: 

1785 n = max(3, n) 

1786 

1787 for tl in group: 

1788 tl.set_slug_abbreviated_length(n) 

1789 

1790 def get_dockwidget(self, panel): 

1791 dockwidget = panel 

1792 while not isinstance(dockwidget, qw.QDockWidget): 

1793 dockwidget = dockwidget.parent() 

1794 

1795 return dockwidget 

1796 

1797 def raise_panel(self, panel): 

1798 dockwidget = self.get_dockwidget(panel) 

1799 dockwidget.setVisible(True) 

1800 dockwidget.setFocus() 

1801 dockwidget.raise_() 

1802 

1803 def toggle_panel_visibility(self): 

1804 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1805 

1806 def update_panel_visibility(self, *args): 

1807 self.setUpdatesEnabled(False) 

1808 mbar = self.menuBar() 

1809 sbar = self.statusBar() 

1810 dockwidgets = self.findChildren(common.MyDockWidget) 

1811 

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

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

1814 # objects. 

1815 mbar.setFixedHeight( 

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

1817 

1818 sbar.setVisible(self.gui_state.panels_visible) 

1819 for dockwidget in dockwidgets: 

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

1821 

1822 self.setUpdatesEnabled(True) 

1823 

1824 def remove_panel(self, panel): 

1825 dockwidget = self.get_dockwidget(panel) 

1826 self.removeDockWidget(dockwidget) 

1827 dockwidget.setParent(None) 

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

1829 

1830 def register_data_provider(self, provider): 

1831 if provider not in self.data_providers: 

1832 self.data_providers.append(provider) 

1833 

1834 def unregister_data_provider(self, provider): 

1835 if provider in self.data_providers: 

1836 self.data_providers.remove(provider) 

1837 

1838 def iter_data(self, name): 

1839 for provider in self.data_providers: 

1840 for data in provider.iter_data(name): 

1841 yield data 

1842 

1843 def confirm_close(self): 

1844 ret = qw.QMessageBox.question( 

1845 self, 

1846 'Sparrow', 

1847 'Close Sparrow window?', 

1848 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1849 qw.QMessageBox.Ok) 

1850 

1851 return ret == qw.QMessageBox.Ok 

1852 

1853 def closeEvent(self, event): 

1854 if self.instant_close or self.confirm_close(): 

1855 self.attach() 

1856 self.closing = True 

1857 event.accept() 

1858 else: 

1859 event.ignore() 

1860 

1861 def is_closing(self): 

1862 return self.closing 

1863 

1864 

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

1866 

1867 from pyrocko import util 

1868 from pyrocko.gui import util as gui_util 

1869 from . import common 

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

1871 

1872 global win 

1873 

1874 app = gui_util.get_app() 

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

1876 app.set_main_window(win) 

1877 

1878 gui_util.app.install_sigint_handler() 

1879 

1880 try: 

1881 gui_util.app.exec_() 

1882 finally: 

1883 gui_util.app.uninstall_sigint_handler() 

1884 app.unset_main_window() 

1885 common.set_viewer(None) 

1886 del win 

1887 gc.collect()