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

1085 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-12-04 10:41 +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 

36import vtkmodules.qt 

37 

38vtkmodules.qt.PyQtImpl = 'PyQt5' 

39vtk.qt.QVTKRWIBase = 'QGLWidget' 

40 

41from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

42 

43from pyrocko import geometry # noqa 

44from . import state as vstate, elements # noqa 

45 

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

47 

48 

49d2r = num.pi/180. 

50km = 1000. 

51 

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

53 g_modifier_key = '\u2318' 

54else: 

55 g_modifier_key = 'Ctrl' 

56 

57 

58class ZeroFrame(qw.QFrame): 

59 

60 def sizeHint(self): 

61 return qc.QSize(0, 0) 

62 

63 

64class LocationChoice(object): 

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

66 self._name = name 

67 self._lat = lat 

68 self._lon = lon 

69 self._depth = depth 

70 

71 def get_lat_lon_depth(self): 

72 return self._lat, self._lon, self._depth 

73 

74 

75def location_to_choices(s): 

76 choices = [] 

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

78 try: 

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

80 if len(vals) == 3: 

81 vals[2] *= km 

82 

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

84 

85 except ValueError: 

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

87 for c in cities: 

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

89 

90 return choices 

91 

92 

93class NoLocationChoices(Exception): 

94 

95 def __init__(self, s): 

96 self._string = s 

97 

98 def __str__(self): 

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

100 

101 

102class QVTKWidget(QVTKRenderWindowInteractor): 

103 def __init__(self, viewer): 

104 QVTKRenderWindowInteractor.__init__(self, parent=viewer) 

105 self._viewer = viewer 

106 self._ctrl_state = False 

107 

108 def wheelEvent(self, event): 

109 return self._viewer.myWheelEvent(event) 

110 

111 def keyPressEvent(self, event): 

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

113 self._update_ctrl_state(True) 

114 QVTKRenderWindowInteractor.keyPressEvent(self, event) 

115 

116 def keyReleaseEvent(self, event): 

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

118 self._update_ctrl_state(False) 

119 QVTKRenderWindowInteractor.keyReleaseEvent(self, event) 

120 

121 def focusInEvent(self, event): 

122 self._update_ctrl_state() 

123 QVTKRenderWindowInteractor.focusInEvent(self, event) 

124 

125 def focusOutEvent(self, event): 

126 self._update_ctrl_state(False) 

127 QVTKRenderWindowInteractor.focusOutEvent(self, event) 

128 

129 def mousePressEvent(self, event): 

130 self._viewer.disable_capture() 

131 QVTKRenderWindowInteractor.mousePressEvent(self, event) 

132 

133 def mouseReleaseEvent(self, event): 

134 self._viewer.enable_capture() 

135 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event) 

136 

137 def _update_ctrl_state(self, state=None): 

138 if state is None: 

139 app = common.get_app() 

140 if not app: 

141 return 

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

143 if self._ctrl_state != state: 

144 self._viewer.gui_state.next_focal_point() 

145 self._ctrl_state = state 

146 

147 def container_resized(self, ev): 

148 self._viewer.update_vtk_widget_size() 

149 

150 

151class DetachedViewer(qw.QMainWindow): 

152 

153 def __init__(self, main_window, vtk_frame): 

154 qw.QMainWindow.__init__(self, main_window) 

155 self.main_window = main_window 

156 self.setWindowTitle('Sparrow View') 

157 vtk_frame.setParent(self) 

158 self.setCentralWidget(vtk_frame) 

159 

160 def closeEvent(self, ev): 

161 ev.ignore() 

162 self.main_window.attach() 

163 

164 

165class CenteringScrollArea(qw.QScrollArea): 

166 def __init__(self): 

167 qw.QScrollArea.__init__(self) 

168 self.setAlignment(qc.Qt.AlignCenter) 

169 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

170 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

171 self.setFrameShape(qw.QFrame.NoFrame) 

172 

173 def resizeEvent(self, ev): 

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

175 self.widget().container_resized(ev) 

176 return retval 

177 

178 def recenter(self): 

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

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

181 

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

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

184 

185 

186class YAMLEditor(qw.QTextEdit): 

187 

188 def __init__(self, parent): 

189 qw.QTextEdit.__init__(self) 

190 self._parent = parent 

191 

192 def event(self, ev): 

193 if isinstance(ev, qg.QKeyEvent) \ 

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

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

196 self._parent.state_changed() 

197 return True 

198 

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

200 

201 

202class StateEditor(qw.QFrame, TalkieConnectionOwner): 

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

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

205 TalkieConnectionOwner.__init__(self) 

206 

207 layout = qw.QGridLayout() 

208 

209 self.setLayout(layout) 

210 

211 self.source_editor = YAMLEditor(self) 

212 self.source_editor.setAcceptRichText(False) 

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

214 font = qg.QFont("Monospace") 

215 self.source_editor.setCurrentFont(font) 

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

217 

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

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

220 

221 self.error_display = qw.QTextEdit() 

222 self.error_display.setCurrentFont(font) 

223 self.error_display.setReadOnly(True) 

224 

225 self.error_display.setSizePolicy( 

226 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

227 

228 self.error_display_label.hide() 

229 self.error_display.hide() 

230 

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

232 

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

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

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

236 

237 button = qw.QPushButton('Apply') 

238 button.clicked.connect(self.state_changed) 

239 layout.addWidget(button, 3, 1) 

240 

241 self.viewer = viewer 

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

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

244 self.bind_state() 

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

246 self.destroyed.connect(self.unbind_state) 

247 self.bind_state() 

248 

249 def bind_state(self, *args): 

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

251 self.update_state() 

252 

253 def unbind_state(self): 

254 self.talkie_disconnect_all() 

255 

256 def update_state(self, *args): 

257 cursor = self.source_editor.textCursor() 

258 

259 cursor_position = cursor.position() 

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

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

262 

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

264 

265 cursor.setPosition(cursor_position) 

266 self.source_editor.setTextCursor(cursor) 

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

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

269 

270 def text_changed_handler(self, *args): 

271 if self.instant_updates.isChecked(): 

272 self.state_changed() 

273 

274 def state_changed(self): 

275 try: 

276 s = self.source_editor.toPlainText() 

277 state = guts.load(string=s) 

278 self.viewer.set_state(state) 

279 self.error_display.setPlainText('') 

280 self.error_display_label.hide() 

281 self.error_display.hide() 

282 

283 except Exception as e: 

284 self.error_display.show() 

285 self.error_display_label.show() 

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

287 

288 

289class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

290 

291 download_progress_update = qc.pyqtSignal() 

292 

293 def __init__( 

294 self, 

295 use_depth_peeling=True, 

296 events=None, 

297 snapshots=None, 

298 instant_close=False): 

299 

300 common.set_viewer(self) 

301 

302 qw.QMainWindow.__init__(self) 

303 TalkieConnectionOwner.__init__(self) 

304 

305 app = common.get_app() 

306 app.set_main_window(self) 

307 

308 self.instant_close = instant_close 

309 

310 self.state = vstate.ViewerState() 

311 self.gui_state = vstate.ViewerGuiState() 

312 

313 self.setWindowTitle('Sparrow') 

314 

315 self.setTabPosition( 

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

317 

318 self.planet_radius = cake.earthradius 

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

320 

321 self._block_capture = 0 

322 self._undo_stack = [] 

323 self._redo_stack = [] 

324 self._undo_aggregate = None 

325 

326 self._panel_togglers = {} 

327 self._actors = set() 

328 self._actors_2d = set() 

329 self._render_window_size = (0, 0) 

330 self._use_depth_peeling = use_depth_peeling 

331 self._in_update_elements = False 

332 self._update_elements_enabled = True 

333 

334 self._animation_tstart = None 

335 self._animation_iframe = None 

336 self._animation = None 

337 

338 mbar = qw.QMenuBar() 

339 self.setMenuBar(mbar) 

340 

341 menu = mbar.addMenu('File') 

342 

343 menu.addAction( 

344 'Export Image...', 

345 self.export_image, 

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

347 qc.Qt.ApplicationShortcut) 

348 

349 menu.addAction( 

350 'Quit', 

351 self.close, 

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

353 qc.Qt.ApplicationShortcut) 

354 

355 menu = mbar.addMenu('Edit') 

356 

357 menu.addAction( 

358 'Undo', 

359 self.undo, 

360 qg.QKeySequence( 

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

362 qc.Qt.ApplicationShortcut) 

363 

364 menu.addAction( 

365 'Redo', 

366 self.redo, 

367 qg.QKeySequence( 

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

369 qc.Qt.ApplicationShortcut) 

370 

371 menu = mbar.addMenu('View') 

372 menu_sizes = menu.addMenu('Size') 

373 self._add_vtk_widget_size_menu_entries(menu_sizes) 

374 

375 # detached/attached 

376 self.talkie_connect( 

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

378 

379 action = qw.QAction('Detach') 

380 action.setCheckable(True) 

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

382 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

383 

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

385 menu.addAction(action) 

386 

387 # hide controls 

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

389 action.setCheckable(True) 

390 action.setShortcut(qc.Qt.Key_Space) 

391 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

392 action.triggered.connect(self.toggle_panel_visibility) 

393 menu.addAction(action) 

394 

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

396 self.panels_menu.addAction( 

397 'Stack Panels', 

398 self.stack_panels) 

399 self.panels_menu.addSeparator() 

400 

401 snapshots_menu = mbar.addMenu('Snapshots') 

402 

403 menu = mbar.addMenu('Elements') 

404 for name, estate in sorted([ 

405 ('Icosphere', elements.IcosphereState( 

406 level=4, 

407 smooth=True, 

408 opacity=0.5, 

409 ambient=0.1)), 

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

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

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

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

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

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

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

417 ('HUD Subtitle', elements.HudState( 

418 template='Subtitle')), 

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

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

421 position='top-left')), 

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

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

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

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

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

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

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

429 

430 def wrap_add_element(estate): 

431 def add_element(*args): 

432 new_element = guts.clone(estate) 

433 new_element.element_id = elements.random_id() 

434 self.state.elements.append(new_element) 

435 self.state.sort_elements() 

436 

437 return add_element 

438 

439 mitem = qw.QAction(name, self) 

440 

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

442 

443 menu.addAction(mitem) 

444 

445 menu = mbar.addMenu('Help') 

446 

447 menu.addAction( 

448 'Interactive Tour', 

449 self.start_tour) 

450 

451 menu.addAction( 

452 'Online Manual', 

453 self.open_manual) 

454 

455 self.data_providers = [] 

456 self.elements = {} 

457 

458 self.detached_window = None 

459 

460 self.main_frame = qw.QFrame() 

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

462 

463 self.vtk_frame = CenteringScrollArea() 

464 

465 self.vtk_widget = QVTKWidget(self) 

466 self.vtk_frame.setWidget(self.vtk_widget) 

467 

468 self.main_layout = qw.QVBoxLayout() 

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

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

471 

472 pb = Progressbars(self) 

473 self.progressbars = pb 

474 self.main_layout.addWidget(pb) 

475 

476 self.main_frame.setLayout(self.main_layout) 

477 

478 self.vtk_frame_substitute = None 

479 

480 self.add_panel( 

481 'Navigation', 

482 self.controls_navigation(), 

483 visible=True, 

484 scrollable=False, 

485 where=qc.Qt.LeftDockWidgetArea) 

486 

487 self.add_panel( 

488 'Time', 

489 self.controls_time(), 

490 visible=True, 

491 scrollable=False, 

492 where=qc.Qt.LeftDockWidgetArea) 

493 

494 self.add_panel( 

495 'Appearance', 

496 self.controls_appearance(), 

497 visible=True, 

498 scrollable=False, 

499 where=qc.Qt.LeftDockWidgetArea) 

500 

501 snapshots_panel = self.controls_snapshots() 

502 self.snapshots_panel = snapshots_panel 

503 self.add_panel( 

504 'Snapshots', 

505 snapshots_panel, 

506 visible=False, 

507 scrollable=False, 

508 where=qc.Qt.LeftDockWidgetArea) 

509 

510 snapshots_panel.setup_menu(snapshots_menu) 

511 

512 self.setCentralWidget(self.main_frame) 

513 

514 self.mesh = None 

515 

516 ren = vtk.vtkRenderer() 

517 

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

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

520 # ren.TwoSidedLightingOn() 

521 # ren.SetUseShadows(1) 

522 

523 self._lighting = None 

524 self._background = None 

525 

526 self.ren = ren 

527 self.update_render_settings() 

528 self.update_camera() 

529 

530 renwin = self.vtk_widget.GetRenderWindow() 

531 

532 if self._use_depth_peeling: 

533 renwin.SetAlphaBitPlanes(1) 

534 renwin.SetMultiSamples(0) 

535 

536 ren.SetUseDepthPeeling(1) 

537 ren.SetMaximumNumberOfPeels(100) 

538 ren.SetOcclusionRatio(0.1) 

539 

540 ren.SetUseFXAA(1) 

541 # ren.SetUseHiddenLineRemoval(1) 

542 # ren.SetBackingStore(1) 

543 

544 self.renwin = renwin 

545 

546 # renwin.LineSmoothingOn() 

547 # renwin.PointSmoothingOn() 

548 # renwin.PolygonSmoothingOn() 

549 

550 renwin.AddRenderer(ren) 

551 

552 iren = renwin.GetInteractor() 

553 iren.LightFollowCameraOn() 

554 iren.SetInteractorStyle(None) 

555 

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

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

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

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

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

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

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

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

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

565 

566 renwin.Render() 

567 

568 iren.Initialize() 

569 

570 self.iren = iren 

571 

572 self.rotating = False 

573 

574 self._elements = {} 

575 self._elements_active = {} 

576 

577 self.talkie_connect( 

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

579 

580 self.state.elements.append(elements.IcosphereState( 

581 element_id='icosphere', 

582 level=4, 

583 smooth=True, 

584 opacity=0.5, 

585 ambient=0.1)) 

586 

587 self.state.elements.append(elements.GridState( 

588 element_id='grid')) 

589 self.state.elements.append(elements.CoastlinesState( 

590 element_id='coastlines')) 

591 self.state.elements.append(elements.CrosshairState( 

592 element_id='crosshair')) 

593 

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

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

596 # self.state.elements.append( 

597 # elements.CatalogState( 

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

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

600 

601 if events: 

602 self.state.elements.append( 

603 elements.CatalogState( 

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

605 

606 self.state.sort_elements() 

607 

608 if snapshots: 

609 snapshots_ = [] 

610 for obj in snapshots: 

611 if isinstance(obj, str): 

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

613 else: 

614 snapshots_.append(obj) 

615 

616 snapshots_panel.add_snapshots(snapshots_) 

617 self.raise_panel(snapshots_panel) 

618 snapshots_panel.goto_snapshot(1) 

619 

620 self.timer = qc.QTimer(self) 

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

622 self.timer.setInterval(1000) 

623 self.timer.start() 

624 

625 self._animation_saver = None 

626 

627 self.closing = False 

628 self.vtk_widget.setFocus() 

629 

630 self.update_detached() 

631 

632 self.status( 

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

634 

635 self.status( 

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

637 

638 self.show() 

639 self.windowHandle().showMaximized() 

640 

641 self.talkie_connect( 

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

643 

644 self.update_vtk_widget_size() 

645 

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

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

648 

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

650 self.capture_state() 

651 

652 set_download_callback(self.update_download_progress) 

653 

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

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

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

657 

658 self.start_tour() 

659 

660 def update_download_progress(self, message, args): 

661 self.download_progress_update.emit() 

662 

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

664 self.statusBar().showMessage( 

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

666 

667 def disable_capture(self): 

668 self._block_capture += 1 

669 

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

671 

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

673 if self._block_capture > 0: 

674 self._block_capture -= 1 

675 

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

677 

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

679 self.capture_state(aggregate=aggregate) 

680 

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

682 if self._block_capture: 

683 return 

684 

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

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

687 

688 if aggregate is not None: 

689 if aggregate == self._undo_aggregate: 

690 self._undo_stack.pop() 

691 

692 self._undo_aggregate = aggregate 

693 else: 

694 self._undo_aggregate = None 

695 

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

697 len(self._undo_stack) + 1, 

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

699 '\n'.join( 

700 ' - %s' % s 

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

702 self.state).splitlines()) 

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

704 

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

706 self._redo_stack.clear() 

707 

708 def undo(self): 

709 self._undo_aggregate = None 

710 

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

712 return 

713 

714 state = self._undo_stack.pop() 

715 self._redo_stack.append(state) 

716 state = self._undo_stack[-1] 

717 

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

719 len(self._undo_stack), 

720 '\n'.join( 

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

722 

723 self.disable_capture() 

724 try: 

725 self.set_state(state) 

726 finally: 

727 self.enable_capture(drop=True) 

728 

729 def redo(self): 

730 self._undo_aggregate = None 

731 

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

733 return 

734 

735 state = self._redo_stack.pop() 

736 self._undo_stack.append(state) 

737 

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

739 len(self._redo_stack), 

740 '\n'.join( 

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

742 

743 self.disable_capture() 

744 try: 

745 self.set_state(state) 

746 finally: 

747 self.enable_capture(drop=True) 

748 

749 def start_tour(self): 

750 snapshots_ = snapshots_mod.load_snapshots( 

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

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

753 self.snapshots_panel.add_snapshots(snapshots_) 

754 self.raise_panel(self.snapshots_panel) 

755 self.snapshots_panel.transition_to_next_snapshot() 

756 

757 def open_manual(self): 

758 import webbrowser 

759 webbrowser.open( 

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

761 

762 def _add_vtk_widget_size_menu_entries(self, menu): 

763 

764 group = qw.QActionGroup(menu) 

765 group.setExclusive(True) 

766 

767 def set_variable_size(): 

768 self.gui_state.fixed_size = False 

769 

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

771 variable_size_action.setCheckable(True) 

772 variable_size_action.setActionGroup(group) 

773 variable_size_action.triggered.connect(set_variable_size) 

774 

775 fixed_size_items = [] 

776 for nx, ny, label in [ 

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

778 (426, 240, ''), 

779 (640, 360, ''), 

780 (854, 480, '(FWVGA)'), 

781 (1280, 720, '(HD)'), 

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

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

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

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

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

787 (640, 480, '(VGA)'), 

788 (800, 600, '(SVGA)'), 

789 (None, None, 'Other'), 

790 (512, 512, ''), 

791 (1024, 1024, '')]: 

792 

793 if None in (nx, ny): 

794 menu.addSection(label) 

795 else: 

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

797 action = menu.addAction(name) 

798 action.setCheckable(True) 

799 action.setActionGroup(group) 

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

801 

802 def make_set_fixed_size(nx, ny): 

803 def set_fixed_size(): 

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

805 

806 return set_fixed_size 

807 

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

809 

810 def update_widget(*args): 

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

812 action.blockSignals(True) 

813 action.setChecked( 

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

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

816 action.blockSignals(False) 

817 

818 variable_size_action.blockSignals(True) 

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

820 variable_size_action.blockSignals(False) 

821 

822 update_widget() 

823 self.talkie_connect( 

824 self.gui_state, 'fixed_size', update_widget) 

825 

826 def update_vtk_widget_size(self, *args): 

827 if self.gui_state.fixed_size: 

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

829 wanted_size = qc.QSize(nx, ny) 

830 else: 

831 wanted_size = qc.QSize( 

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

833 

834 current_size = self.vtk_widget.size() 

835 

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

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

838 

839 self.vtk_widget.setFixedSize(wanted_size) 

840 

841 self.vtk_frame.recenter() 

842 self.check_vtk_resize() 

843 

844 def update_focal_point(self, *args): 

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

846 self.vtk_widget.setStatusTip( 

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

848 'change view plane orientation.' % g_modifier_key) 

849 else: 

850 self.vtk_widget.setStatusTip( 

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

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

853 'reverse sense.' % g_modifier_key) 

854 

855 def update_detached(self, *args): 

856 

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

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

859 

860 self.main_layout.removeWidget(self.vtk_frame) 

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

862 self.detached_window.show() 

863 self.vtk_widget.setFocus() 

864 

865 screens = common.get_app().screens() 

866 if len(screens) > 1: 

867 for screen in screens: 

868 if screen is not self.screen(): 

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

870 # .setScreen() does not work reliably, 

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

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

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

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

875 

876 self.detached_window.windowHandle().showMaximized() 

877 

878 frame = qw.QFrame() 

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

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

881 # frame.setAutoFillBackground(True) 

882 frame.setSizePolicy( 

883 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

884 

885 layout = qw.QGridLayout() 

886 frame.setLayout(layout) 

887 self.main_layout.insertWidget(0, frame) 

888 

889 self.state_editor = StateEditor(self) 

890 

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

892 

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

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

895 # layout.addWidget( 

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

897 

898 self.vtk_frame_substitute = frame 

899 

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

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

902 self.detached_window.hide() 

903 self.vtk_frame.setParent(self) 

904 if self.vtk_frame_substitute: 

905 self.main_layout.removeWidget(self.vtk_frame_substitute) 

906 self.state_editor.unbind_state() 

907 self.vtk_frame_substitute = None 

908 

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

910 self.detached_window = None 

911 self.vtk_widget.setFocus() 

912 

913 def attach(self): 

914 self.gui_state.detached = False 

915 

916 def export_image(self): 

917 

918 caption = 'Export Image' 

919 fn_out, _ = qw.QFileDialog.getSaveFileName( 

920 self, caption, 'image.png', 

921 options=common.qfiledialog_options) 

922 

923 if fn_out: 

924 self.save_image(fn_out) 

925 

926 def save_image(self, path): 

927 

928 original_fixed_size = self.gui_state.fixed_size 

929 if original_fixed_size is None: 

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

931 

932 wif = vtk.vtkWindowToImageFilter() 

933 wif.SetInput(self.renwin) 

934 wif.SetInputBufferTypeToRGBA() 

935 wif.SetScale(1, 1) 

936 wif.ReadFrontBufferOff() 

937 writer = vtk.vtkPNGWriter() 

938 writer.SetInputConnection(wif.GetOutputPort()) 

939 

940 self.renwin.Render() 

941 wif.Modified() 

942 writer.SetFileName(path) 

943 writer.Write() 

944 

945 self.gui_state.fixed_size = original_fixed_size 

946 

947 def update_render_settings(self, *args): 

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

949 self.ren.RemoveAllLights() 

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

951 self.ren.AddLight(li) 

952 

953 self._lighting = self.state.lighting 

954 

955 if self._background is None \ 

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

957 

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

959 self._background = self.state.background 

960 

961 self.update_view() 

962 

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

964 if self._animation: 

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

966 self.stop_animation() 

967 

968 self.disable_capture() 

969 self._animation = interpolator 

970 if output_path is None: 

971 self._animation_tstart = time.time() 

972 self._animation_iframe = None 

973 else: 

974 self._animation_iframe = 0 

975 mess = 'Rendering movie' 

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

977 

978 self._animation_timer = qc.QTimer(self) 

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

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

981 self._animation_timer.start() 

982 if output_path is not None: 

983 original_fixed_size = self.gui_state.fixed_size 

984 if original_fixed_size is None: 

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

986 

987 wif = vtk.vtkWindowToImageFilter() 

988 wif.SetInput(self.renwin) 

989 wif.SetInputBufferTypeToRGBA() 

990 wif.SetScale(1, 1) 

991 wif.ReadFrontBufferOff() 

992 writer = vtk.vtkPNGWriter() 

993 temp_path = tempfile.mkdtemp() 

994 self._animation_saver = ( 

995 wif, writer, temp_path, output_path, original_fixed_size) 

996 writer.SetInputConnection(wif.GetOutputPort()) 

997 

998 def next_animation_frame(self): 

999 

1000 ani = self._animation 

1001 if not ani: 

1002 return 

1003 

1004 if self._animation_iframe is not None: 

1005 state = ani( 

1006 ani.tmin 

1007 + self._animation_iframe * ani.dt) 

1008 

1009 self._animation_iframe += 1 

1010 else: 

1011 tnow = time.time() 

1012 state = ani(min( 

1013 ani.tmax, 

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

1015 

1016 self.set_state(state) 

1017 self.renwin.Render() 

1018 abort = False 

1019 if self._animation_saver: 

1020 abort = self.progressbars.set_status( 

1021 'Rendering movie', 

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

1023 can_abort=True) 

1024 

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

1026 wif.Modified() 

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

1028 writer.SetFileName(fn % self._animation_iframe) 

1029 writer.Write() 

1030 

1031 if self._animation_iframe is not None: 

1032 t = self._animation_iframe * ani.dt 

1033 else: 

1034 t = tnow - self._animation_tstart 

1035 

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

1037 self.stop_animation() 

1038 

1039 def stop_animation(self): 

1040 if self._animation_timer: 

1041 self._animation_timer.stop() 

1042 

1043 if self._animation_saver: 

1044 

1045 wif, writer, temp_path, output_path, original_fixed_size \ 

1046 = self._animation_saver 

1047 self.gui_state.fixed_size = original_fixed_size 

1048 

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

1050 check_call([ 

1051 'ffmpeg', '-y', 

1052 '-i', fn_path, 

1053 '-c:v', 'libx264', 

1054 '-preset', 'slow', 

1055 '-crf', '17', 

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

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

1058 output_path]) 

1059 shutil.rmtree(temp_path) 

1060 

1061 self._animation_saver = None 

1062 self._animation_saver 

1063 

1064 self.progressbars.set_status( 

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

1066 

1067 self._animation_tstart = None 

1068 self._animation_iframe = None 

1069 self._animation = None 

1070 self.enable_capture() 

1071 

1072 def set_state(self, state): 

1073 self.disable_capture() 

1074 try: 

1075 self._update_elements_enabled = False 

1076 self.setUpdatesEnabled(False) 

1077 self.state.diff_update(state) 

1078 self.state.sort_elements() 

1079 self.setUpdatesEnabled(True) 

1080 self._update_elements_enabled = True 

1081 self.update_elements() 

1082 finally: 

1083 self.enable_capture() 

1084 

1085 def periodical(self): 

1086 pass 

1087 

1088 def check_vtk_resize(self, *args): 

1089 render_window_size = self.renwin.GetSize() 

1090 if self._render_window_size != render_window_size: 

1091 self._render_window_size = render_window_size 

1092 self.resize_event(*render_window_size) 

1093 

1094 def update_elements(self, *_): 

1095 if not self._update_elements_enabled: 

1096 return 

1097 

1098 if self._in_update_elements: 

1099 return 

1100 

1101 self._in_update_elements = True 

1102 for estate in self.state.elements: 

1103 if estate.element_id not in self._elements: 

1104 new_element = estate.create() 

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

1106 type(new_element).__name__, 

1107 estate.element_id)) 

1108 self._elements[estate.element_id] = new_element 

1109 

1110 element = self._elements[estate.element_id] 

1111 

1112 if estate.element_id not in self._elements_active: 

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

1114 type(element).__name__, 

1115 estate.element_id)) 

1116 element.bind_state(estate) 

1117 element.set_parent(self) 

1118 self._elements_active[estate.element_id] = element 

1119 

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

1121 deactivate = [] 

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

1123 if element_id not in state_element_ids: 

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

1125 type(element).__name__, 

1126 element_id)) 

1127 element.unset_parent() 

1128 deactivate.append(element_id) 

1129 

1130 for element_id in deactivate: 

1131 del self._elements_active[element_id] 

1132 

1133 self._update_crosshair_bindings() 

1134 

1135 self._in_update_elements = False 

1136 

1137 def _update_crosshair_bindings(self): 

1138 

1139 def get_crosshair_element(): 

1140 for element in self.state.elements: 

1141 if element.element_id == 'crosshair': 

1142 return element 

1143 

1144 return None 

1145 

1146 crosshair = get_crosshair_element() 

1147 if crosshair is None or crosshair.is_connected: 

1148 return 

1149 

1150 def to_checkbox(state, widget): 

1151 widget.blockSignals(True) 

1152 widget.setChecked(state.visible) 

1153 widget.blockSignals(False) 

1154 

1155 def to_state(widget, state): 

1156 state.visible = widget.isChecked() 

1157 

1158 cb = self._crosshair_checkbox 

1159 vstate.state_bind( 

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

1161 cb, [cb.toggled], to_checkbox) 

1162 

1163 crosshair.is_connected = True 

1164 

1165 def add_actor_2d(self, actor): 

1166 if actor not in self._actors_2d: 

1167 self.ren.AddActor2D(actor) 

1168 self._actors_2d.add(actor) 

1169 

1170 def remove_actor_2d(self, actor): 

1171 if actor in self._actors_2d: 

1172 self.ren.RemoveActor2D(actor) 

1173 self._actors_2d.remove(actor) 

1174 

1175 def add_actor(self, actor): 

1176 if actor not in self._actors: 

1177 self.ren.AddActor(actor) 

1178 self._actors.add(actor) 

1179 

1180 def add_actor_list(self, actorlist): 

1181 for actor in actorlist: 

1182 self.add_actor(actor) 

1183 

1184 def remove_actor(self, actor): 

1185 if actor in self._actors: 

1186 self.ren.RemoveActor(actor) 

1187 self._actors.remove(actor) 

1188 

1189 def update_view(self): 

1190 self.vtk_widget.update() 

1191 

1192 def resize_event(self, size_x, size_y): 

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

1194 

1195 def button_event(self, obj, event): 

1196 if event == "LeftButtonPressEvent": 

1197 self.rotating = True 

1198 elif event == "LeftButtonReleaseEvent": 

1199 self.rotating = False 

1200 

1201 def mouse_move_event(self, obj, event): 

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

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

1204 

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

1206 center_x = size_x / 2.0 

1207 center_y = size_y / 2.0 

1208 

1209 if self.rotating: 

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

1211 

1212 def myWheelEvent(self, event): 

1213 

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

1215 

1216 if angle > 200: 

1217 angle = 200 

1218 

1219 if angle < -200: 

1220 angle = -200 

1221 

1222 self.disable_capture() 

1223 try: 

1224 self.do_dolly(-angle/100.) 

1225 finally: 

1226 self.enable_capture(aggregate='distance') 

1227 

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

1229 

1230 dx = x0 - x 

1231 dy = y0 - y 

1232 

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

1234 focp = self.gui_state.focal_point 

1235 

1236 if focp == 'center': 

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

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

1239 

1240 lat = self.state.lat 

1241 lon = self.state.lon 

1242 factor = self.state.distance / 10.0 

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

1244 else: 

1245 lat = 90. - self.state.dip 

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

1247 factor = 0.5 

1248 factor_lat = 1.0 

1249 

1250 dlat = dy * factor 

1251 dlon = dx * factor * factor_lat 

1252 

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

1254 lon += dlon 

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

1256 

1257 if focp == 'center': 

1258 self.state.lat = float(lat) 

1259 self.state.lon = float(lon) 

1260 else: 

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

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

1263 

1264 def do_dolly(self, v): 

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

1266 

1267 def key_down_event(self, obj, event): 

1268 k = obj.GetKeyCode() 

1269 if k == 'f': 

1270 self.gui_state.next_focal_point() 

1271 

1272 elif k == 'r': 

1273 self.reset_strike_dip() 

1274 

1275 elif k == 'p': 

1276 print(self.state) 

1277 

1278 elif k == 'i': 

1279 for elem in self.state.elements: 

1280 if isinstance(elem, elements.IcosphereState): 

1281 elem.visible = not elem.visible 

1282 

1283 elif k == 'c': 

1284 for elem in self.state.elements: 

1285 if isinstance(elem, elements.CoastlinesState): 

1286 elem.visible = not elem.visible 

1287 

1288 elif k == 't': 

1289 if not any( 

1290 isinstance(elem, elements.TopoState) 

1291 for elem in self.state.elements): 

1292 

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

1294 else: 

1295 for elem in self.state.elements: 

1296 if isinstance(elem, elements.TopoState): 

1297 elem.visible = not elem.visible 

1298 

1299 # elif k == ' ': 

1300 # self.toggle_panel_visibility() 

1301 

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

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

1304 

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

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

1307 

1308 def controls_navigation(self): 

1309 frame = qw.QFrame(self) 

1310 frame.setSizePolicy( 

1311 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1312 layout = qw.QGridLayout() 

1313 frame.setLayout(layout) 

1314 

1315 # lat, lon, depth 

1316 

1317 layout.addWidget( 

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

1319 

1320 le = qw.QLineEdit() 

1321 le.setStatusTip( 

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

1323 'Focal point location.') 

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

1325 

1326 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1329 

1330 def lineedit_to_lat_lon_depth(widget, state): 

1331 self.disable_capture() 

1332 try: 

1333 s = str(widget.text()) 

1334 choices = location_to_choices(s) 

1335 if len(choices) > 0: 

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

1337 choices[0].get_lat_lon_depth() 

1338 else: 

1339 raise NoLocationChoices(s) 

1340 

1341 finally: 

1342 self.enable_capture() 

1343 

1344 self._state_bind( 

1345 ['lat', 'lon', 'depth'], 

1346 lineedit_to_lat_lon_depth, 

1347 le, [le.editingFinished, le.returnPressed], 

1348 lat_lon_depth_to_lineedit) 

1349 

1350 self.lat_lon_lineedit = le 

1351 

1352 # focal point 

1353 

1354 cb = qw.QCheckBox('Fix') 

1355 cb.setStatusTip( 

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

1357 % g_modifier_key) 

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

1359 

1360 def focal_point_to_checkbox(state, widget): 

1361 widget.blockSignals(True) 

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

1363 widget.blockSignals(False) 

1364 

1365 def checkbox_to_focal_point(widget, state): 

1366 self.gui_state.focal_point = \ 

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

1368 

1369 self._gui_state_bind( 

1370 ['focal_point'], checkbox_to_focal_point, 

1371 cb, [cb.toggled], focal_point_to_checkbox) 

1372 

1373 self.focal_point_checkbox = cb 

1374 

1375 self.talkie_connect( 

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

1377 

1378 self.update_focal_point() 

1379 

1380 # strike, dip 

1381 

1382 layout.addWidget( 

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

1384 

1385 le = qw.QLineEdit() 

1386 le.setStatusTip( 

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

1388 'direction.') 

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

1390 

1391 def strike_dip_to_lineedit(state, widget): 

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

1393 

1394 def lineedit_to_strike_dip(widget, state): 

1395 s = str(widget.text()) 

1396 string_to_strike_dip = { 

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

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

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

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

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

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

1403 

1404 self.disable_capture() 

1405 if s in string_to_strike_dip: 

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

1407 

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

1409 try: 

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

1411 except Exception: 

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

1413 finally: 

1414 self.enable_capture() 

1415 

1416 self._state_bind( 

1417 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1419 

1420 self.strike_dip_lineedit = le 

1421 

1422 but = qw.QPushButton('Reset') 

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

1424 but.clicked.connect(self.reset_strike_dip) 

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

1426 

1427 # crosshair 

1428 

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

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

1431 

1432 # camera bindings 

1433 self.talkie_connect( 

1434 self.state, 

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

1436 self.update_camera) 

1437 

1438 self.talkie_connect( 

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

1440 

1441 return frame 

1442 

1443 def controls_time(self): 

1444 frame = qw.QFrame(self) 

1445 frame.setSizePolicy( 

1446 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1447 

1448 layout = qw.QGridLayout() 

1449 frame.setLayout(layout) 

1450 

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

1452 le_tmin = qw.QLineEdit() 

1453 layout.addWidget(le_tmin, 0, 1) 

1454 

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

1456 le_tmax = qw.QLineEdit() 

1457 layout.addWidget(le_tmax, 1, 1) 

1458 

1459 label_tcursor = qw.QLabel() 

1460 

1461 label_tcursor.setSizePolicy( 

1462 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1463 

1464 layout.addWidget(label_tcursor, 2, 1) 

1465 self._label_tcursor = label_tcursor 

1466 

1467 self._state_bind( 

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

1469 [le_tmin.editingFinished, le_tmin.returnPressed], 

1470 common.time_to_lineedit, 

1471 attribute='tmin') 

1472 self._state_bind( 

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

1474 [le_tmax.editingFinished, le_tmax.returnPressed], 

1475 common.time_to_lineedit, 

1476 attribute='tmax') 

1477 

1478 self.tmin_lineedit = le_tmin 

1479 self.tmax_lineedit = le_tmax 

1480 

1481 range_edit = RangeEdit() 

1482 range_edit.rangeEditPressed.connect(self.disable_capture) 

1483 range_edit.rangeEditReleased.connect(self.enable_capture) 

1484 range_edit.set_data_provider(self) 

1485 range_edit.set_data_name('time') 

1486 

1487 xblock = [False] 

1488 

1489 def range_to_range_edit(state, widget): 

1490 if not xblock[0]: 

1491 widget.blockSignals(True) 

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

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

1494 widget.blockSignals(False) 

1495 

1496 def range_edit_to_range(widget, state): 

1497 xblock[0] = True 

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

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

1500 xblock[0] = False 

1501 

1502 self._state_bind( 

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

1504 range_edit_to_range, 

1505 range_edit, 

1506 [range_edit.rangeChanged, range_edit.focusChanged], 

1507 range_to_range_edit) 

1508 

1509 def handle_tcursor_changed(): 

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

1511 

1512 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1513 

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

1515 

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

1517 le_focus = qw.QLineEdit() 

1518 layout.addWidget(le_focus, 4, 1) 

1519 

1520 def focus_to_lineedit(state, widget): 

1521 if state.tduration is None: 

1522 widget.setText('') 

1523 else: 

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

1525 guts.str_duration(state.tduration), 

1526 state.tposition)) 

1527 

1528 def lineedit_to_focus(widget, state): 

1529 s = str(widget.text()) 

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

1531 try: 

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

1533 state.tduration = None 

1534 state.tposition = 0.0 

1535 else: 

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

1537 if len(w) > 1: 

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

1539 else: 

1540 state.tposition = 0.0 

1541 

1542 except Exception: 

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

1544 

1545 self._state_bind( 

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

1547 [le_focus.editingFinished, le_focus.returnPressed], 

1548 focus_to_lineedit) 

1549 

1550 label_effective_tmin = qw.QLabel() 

1551 label_effective_tmax = qw.QLabel() 

1552 

1553 label_effective_tmin.setSizePolicy( 

1554 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1555 label_effective_tmax.setSizePolicy( 

1556 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1557 label_effective_tmin.setMinimumSize( 

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

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

1560 

1561 layout.addWidget(label_effective_tmin, 5, 1) 

1562 layout.addWidget(label_effective_tmax, 6, 1) 

1563 

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

1565 self.talkie_connect( 

1566 self.state, var, self.update_effective_time_labels) 

1567 

1568 self._label_effective_tmin = label_effective_tmin 

1569 self._label_effective_tmax = label_effective_tmax 

1570 

1571 self.talkie_connect( 

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

1573 

1574 return frame 

1575 

1576 def controls_appearance(self): 

1577 frame = qw.QFrame(self) 

1578 frame.setSizePolicy( 

1579 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1580 layout = qw.QGridLayout() 

1581 frame.setLayout(layout) 

1582 

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

1584 

1585 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1586 layout.addWidget(cb, 0, 1) 

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

1588 

1589 self.talkie_connect( 

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

1591 

1592 # background 

1593 

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

1595 

1596 cb = common.strings_to_combobox( 

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

1598 

1599 layout.addWidget(cb, 1, 1) 

1600 vstate.state_bind_combobox_background( 

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

1602 

1603 self.talkie_connect( 

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

1605 

1606 return frame 

1607 

1608 def controls_snapshots(self): 

1609 return snapshots_mod.SnapshotsPanel(self) 

1610 

1611 def update_effective_time_labels(self, *args): 

1612 tmin = self.state.tmin_effective 

1613 tmax = self.state.tmax_effective 

1614 

1615 stmin = common.time_or_none_to_str(tmin) 

1616 stmax = common.time_or_none_to_str(tmax) 

1617 

1618 self._label_effective_tmin.setText(stmin) 

1619 self._label_effective_tmax.setText(stmax) 

1620 

1621 def update_tcursor(self, *args): 

1622 tcursor = self.gui_state.tcursor 

1623 stcursor = common.time_or_none_to_str(tcursor) 

1624 self._label_tcursor.setText(stcursor) 

1625 

1626 def reset_strike_dip(self, *args): 

1627 self.state.strike = 90. 

1628 self.state.dip = 0 

1629 self.gui_state.focal_point = 'center' 

1630 

1631 def get_camera_geometry(self): 

1632 

1633 def rtp2xyz(rtp): 

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

1635 

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

1637 

1638 cam_rtp = num.array([ 

1639 radius+self.state.distance, 

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

1641 self.state.lon * d2r]) 

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

1643 cam, up, foc = \ 

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

1645 

1646 foc_rtp = num.array([ 

1647 radius, 

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

1649 self.state.lon * d2r]) 

1650 

1651 foc = rtp2xyz(foc_rtp) 

1652 

1653 rot_world = pmt.euler_to_matrix( 

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

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

1656 0.0*d2r).T 

1657 

1658 rot_cam = pmt.euler_to_matrix( 

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

1660 

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

1662 

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

1664 up = num.dot(rot, up) 

1665 return cam, up, foc 

1666 

1667 def update_camera(self, *args): 

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

1669 camera = self.ren.GetActiveCamera() 

1670 camera.SetPosition(*cam) 

1671 camera.SetFocalPoint(*foc) 

1672 camera.SetViewUp(*up) 

1673 

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

1675 

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

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

1678 

1679 # if horizon == 0.0: 

1680 # horizon = 2.0 + self.state.distance 

1681 

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

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

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

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

1686 # clip_dist = feature_horizon 

1687 

1688 camera.SetClippingRange( 

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

1690 

1691 self.camera_params = ( 

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

1693 

1694 self.update_view() 

1695 

1696 def add_panel( 

1697 self, title_label, panel, 

1698 visible=False, 

1699 # volatile=False, 

1700 tabify=True, 

1701 where=qc.Qt.RightDockWidgetArea, 

1702 remove=None, 

1703 title_controls=[], 

1704 scrollable=True): 

1705 

1706 dockwidget = common.MyDockWidget( 

1707 self, title_label, title_controls=title_controls) 

1708 

1709 if not visible: 

1710 dockwidget.hide() 

1711 

1712 if not self.gui_state.panels_visible: 

1713 dockwidget.block() 

1714 

1715 if scrollable: 

1716 scrollarea = common.MyScrollArea() 

1717 scrollarea.setWidget(panel) 

1718 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

1719 scrollarea.setSizeAdjustPolicy( 

1720 qw.QAbstractScrollArea.AdjustToContents) 

1721 scrollarea.setFrameShape(qw.QFrame.NoFrame) 

1722 

1723 dockwidget.setWidget(scrollarea) 

1724 else: 

1725 dockwidget.setWidget(panel) 

1726 

1727 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1729 

1730 self.addDockWidget(where, dockwidget) 

1731 

1732 nwrap = 4 

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

1734 self.tabifyDockWidget( 

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

1736 

1737 mitem = dockwidget.toggleViewAction() 

1738 

1739 def update_label(*args): 

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

1741 self.update_slug_abbreviated_lengths() 

1742 

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

1744 dockwidget.titlebar._title_label.title_changed.connect( 

1745 self.update_slug_abbreviated_lengths) 

1746 

1747 update_label() 

1748 

1749 self._panel_togglers[dockwidget] = mitem 

1750 self.panels_menu.addAction(mitem) 

1751 if visible: 

1752 dockwidget.setVisible(True) 

1753 dockwidget.setFocus() 

1754 dockwidget.raise_() 

1755 

1756 def stack_panels(self): 

1757 dockwidgets = self.findChildren(common.MyDockWidget) 

1758 by_area = defaultdict(list) 

1759 for dw in dockwidgets: 

1760 area = self.dockWidgetArea(dw) 

1761 by_area[area].append(dw) 

1762 

1763 for dockwidgets in by_area.values(): 

1764 dw_last = None 

1765 for dw in dockwidgets: 

1766 if dw_last is not None: 

1767 self.tabifyDockWidget(dw_last, dw) 

1768 

1769 dw_last = dw 

1770 

1771 def update_slug_abbreviated_lengths(self): 

1772 dockwidgets = self.findChildren(common.MyDockWidget) 

1773 title_labels = [] 

1774 for dw in dockwidgets: 

1775 title_labels.append(dw.titlebar._title_label) 

1776 

1777 by_title = defaultdict(list) 

1778 for tl in title_labels: 

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

1780 

1781 for group in by_title.values(): 

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

1783 

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

1785 nunique = len(set(slugs)) 

1786 

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

1788 n -= 1 

1789 

1790 if n > 0: 

1791 n = max(3, n) 

1792 

1793 for tl in group: 

1794 tl.set_slug_abbreviated_length(n) 

1795 

1796 def get_dockwidget(self, panel): 

1797 dockwidget = panel 

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

1799 dockwidget = dockwidget.parent() 

1800 

1801 return dockwidget 

1802 

1803 def raise_panel(self, panel): 

1804 dockwidget = self.get_dockwidget(panel) 

1805 dockwidget.setVisible(True) 

1806 dockwidget.setFocus() 

1807 dockwidget.raise_() 

1808 

1809 def toggle_panel_visibility(self): 

1810 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1811 

1812 def update_panel_visibility(self, *args): 

1813 self.setUpdatesEnabled(False) 

1814 mbar = self.menuBar() 

1815 sbar = self.statusBar() 

1816 dockwidgets = self.findChildren(common.MyDockWidget) 

1817 

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

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

1820 # objects. 

1821 mbar.setFixedHeight( 

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

1823 

1824 sbar.setVisible(self.gui_state.panels_visible) 

1825 for dockwidget in dockwidgets: 

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

1827 

1828 self.setUpdatesEnabled(True) 

1829 

1830 def remove_panel(self, panel): 

1831 dockwidget = self.get_dockwidget(panel) 

1832 self.removeDockWidget(dockwidget) 

1833 dockwidget.setParent(None) 

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

1835 

1836 def register_data_provider(self, provider): 

1837 if provider not in self.data_providers: 

1838 self.data_providers.append(provider) 

1839 

1840 def unregister_data_provider(self, provider): 

1841 if provider in self.data_providers: 

1842 self.data_providers.remove(provider) 

1843 

1844 def iter_data(self, name): 

1845 for provider in self.data_providers: 

1846 for data in provider.iter_data(name): 

1847 yield data 

1848 

1849 def confirm_close(self): 

1850 ret = qw.QMessageBox.question( 

1851 self, 

1852 'Sparrow', 

1853 'Close Sparrow window?', 

1854 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1855 qw.QMessageBox.Ok) 

1856 

1857 return ret == qw.QMessageBox.Ok 

1858 

1859 def closeEvent(self, event): 

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

1861 self.attach() 

1862 self.closing = True 

1863 event.accept() 

1864 else: 

1865 event.ignore() 

1866 

1867 def is_closing(self): 

1868 return self.closing 

1869 

1870 

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

1872 

1873 from pyrocko import util 

1874 from pyrocko.gui import util as gui_util 

1875 from . import common 

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

1877 

1878 global win 

1879 

1880 app = gui_util.get_app() 

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

1882 app.set_main_window(win) 

1883 

1884 gui_util.app.install_sigint_handler() 

1885 

1886 try: 

1887 gui_util.app.exec_() 

1888 finally: 

1889 gui_util.app.uninstall_sigint_handler() 

1890 app.unset_main_window() 

1891 common.set_viewer(None) 

1892 del win 

1893 gc.collect()