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 ('Borders', elements.BordersState()), 

411 ('Rivers', elements.RiversState()), 

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

413 ('HUD Subtitle', elements.HudState( 

414 template='Subtitle')), 

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

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

417 position='top-left')), 

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

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

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

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

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

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

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

425 

426 def wrap_add_element(estate): 

427 def add_element(*args): 

428 new_element = guts.clone(estate) 

429 new_element.element_id = elements.random_id() 

430 self.state.elements.append(new_element) 

431 self.state.sort_elements() 

432 

433 return add_element 

434 

435 mitem = qw.QAction(name, self) 

436 

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

438 

439 menu.addAction(mitem) 

440 

441 menu = mbar.addMenu('Help') 

442 

443 menu.addAction( 

444 'Interactive Tour', 

445 self.start_tour) 

446 

447 menu.addAction( 

448 'Online Manual', 

449 self.open_manual) 

450 

451 self.data_providers = [] 

452 self.elements = {} 

453 

454 self.detached_window = None 

455 

456 self.main_frame = qw.QFrame() 

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

458 

459 self.vtk_frame = CenteringScrollArea() 

460 

461 self.vtk_widget = QVTKWidget(self, self) 

462 self.vtk_frame.setWidget(self.vtk_widget) 

463 

464 self.main_layout = qw.QVBoxLayout() 

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

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

467 

468 pb = Progressbars(self) 

469 self.progressbars = pb 

470 self.main_layout.addWidget(pb) 

471 

472 self.main_frame.setLayout(self.main_layout) 

473 

474 self.vtk_frame_substitute = None 

475 

476 self.add_panel( 

477 'Navigation', 

478 self.controls_navigation(), 

479 visible=True, 

480 scrollable=False, 

481 where=qc.Qt.LeftDockWidgetArea) 

482 

483 self.add_panel( 

484 'Time', 

485 self.controls_time(), 

486 visible=True, 

487 scrollable=False, 

488 where=qc.Qt.LeftDockWidgetArea) 

489 

490 self.add_panel( 

491 'Appearance', 

492 self.controls_appearance(), 

493 visible=True, 

494 scrollable=False, 

495 where=qc.Qt.LeftDockWidgetArea) 

496 

497 snapshots_panel = self.controls_snapshots() 

498 self.snapshots_panel = snapshots_panel 

499 self.add_panel( 

500 'Snapshots', 

501 snapshots_panel, 

502 visible=False, 

503 scrollable=False, 

504 where=qc.Qt.LeftDockWidgetArea) 

505 

506 snapshots_panel.setup_menu(snapshots_menu) 

507 

508 self.setCentralWidget(self.main_frame) 

509 

510 self.mesh = None 

511 

512 ren = vtk.vtkRenderer() 

513 

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

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

516 # ren.TwoSidedLightingOn() 

517 # ren.SetUseShadows(1) 

518 

519 self._lighting = None 

520 self._background = None 

521 

522 self.ren = ren 

523 self.update_render_settings() 

524 self.update_camera() 

525 

526 renwin = self.vtk_widget.GetRenderWindow() 

527 

528 if self._use_depth_peeling: 

529 renwin.SetAlphaBitPlanes(1) 

530 renwin.SetMultiSamples(0) 

531 

532 ren.SetUseDepthPeeling(1) 

533 ren.SetMaximumNumberOfPeels(100) 

534 ren.SetOcclusionRatio(0.1) 

535 

536 ren.SetUseFXAA(1) 

537 # ren.SetUseHiddenLineRemoval(1) 

538 # ren.SetBackingStore(1) 

539 

540 self.renwin = renwin 

541 

542 # renwin.LineSmoothingOn() 

543 # renwin.PointSmoothingOn() 

544 # renwin.PolygonSmoothingOn() 

545 

546 renwin.AddRenderer(ren) 

547 

548 iren = renwin.GetInteractor() 

549 iren.LightFollowCameraOn() 

550 iren.SetInteractorStyle(None) 

551 

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

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

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

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

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

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

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

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

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

561 

562 renwin.Render() 

563 

564 iren.Initialize() 

565 

566 self.iren = iren 

567 

568 self.rotating = False 

569 

570 self._elements = {} 

571 self._elements_active = {} 

572 

573 self.talkie_connect( 

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

575 

576 self.state.elements.append(elements.IcosphereState( 

577 element_id='icosphere', 

578 level=4, 

579 smooth=True, 

580 opacity=0.5, 

581 ambient=0.1)) 

582 

583 self.state.elements.append(elements.GridState( 

584 element_id='grid')) 

585 self.state.elements.append(elements.CoastlinesState( 

586 element_id='coastlines')) 

587 self.state.elements.append(elements.CrosshairState( 

588 element_id='crosshair')) 

589 

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

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

592 # self.state.elements.append( 

593 # elements.CatalogState( 

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

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

596 

597 if events: 

598 self.state.elements.append( 

599 elements.CatalogState( 

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

601 

602 self.state.sort_elements() 

603 

604 if snapshots: 

605 snapshots_ = [] 

606 for obj in snapshots: 

607 if isinstance(obj, str): 

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

609 else: 

610 snapshots_.append(obj) 

611 

612 snapshots_panel.add_snapshots(snapshots_) 

613 self.raise_panel(snapshots_panel) 

614 snapshots_panel.goto_snapshot(1) 

615 

616 self.timer = qc.QTimer(self) 

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

618 self.timer.setInterval(1000) 

619 self.timer.start() 

620 

621 self._animation_saver = None 

622 

623 self.closing = False 

624 self.vtk_widget.setFocus() 

625 

626 self.update_detached() 

627 

628 self.status( 

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

630 

631 self.status( 

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

633 

634 self.show() 

635 self.windowHandle().showMaximized() 

636 

637 self.talkie_connect( 

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

639 

640 self.update_vtk_widget_size() 

641 

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

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

644 

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

646 self.capture_state() 

647 

648 set_download_callback(self.update_download_progress) 

649 

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

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

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

653 

654 self.start_tour() 

655 

656 def update_download_progress(self, message, args): 

657 self.download_progress_update.emit() 

658 

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

660 self.statusBar().showMessage( 

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

662 

663 def disable_capture(self): 

664 self._block_capture += 1 

665 

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

667 

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

669 if self._block_capture > 0: 

670 self._block_capture -= 1 

671 

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

673 

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

675 self.capture_state(aggregate=aggregate) 

676 

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

678 if self._block_capture: 

679 return 

680 

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

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

683 

684 if aggregate is not None: 

685 if aggregate == self._undo_aggregate: 

686 self._undo_stack.pop() 

687 

688 self._undo_aggregate = aggregate 

689 else: 

690 self._undo_aggregate = None 

691 

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

693 len(self._undo_stack) + 1, 

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

695 '\n'.join( 

696 ' - %s' % s 

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

698 self.state).splitlines()) 

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

700 

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

702 self._redo_stack.clear() 

703 

704 def undo(self): 

705 self._undo_aggregate = None 

706 

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

708 return 

709 

710 state = self._undo_stack.pop() 

711 self._redo_stack.append(state) 

712 state = self._undo_stack[-1] 

713 

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

715 len(self._undo_stack), 

716 '\n'.join( 

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

718 

719 self.disable_capture() 

720 try: 

721 self.set_state(state) 

722 finally: 

723 self.enable_capture(drop=True) 

724 

725 def redo(self): 

726 self._undo_aggregate = None 

727 

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

729 return 

730 

731 state = self._redo_stack.pop() 

732 self._undo_stack.append(state) 

733 

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

735 len(self._redo_stack), 

736 '\n'.join( 

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

738 

739 self.disable_capture() 

740 try: 

741 self.set_state(state) 

742 finally: 

743 self.enable_capture(drop=True) 

744 

745 def start_tour(self): 

746 snapshots_ = snapshots_mod.load_snapshots( 

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

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

749 self.snapshots_panel.add_snapshots(snapshots_) 

750 self.raise_panel(self.snapshots_panel) 

751 self.snapshots_panel.transition_to_next_snapshot() 

752 

753 def open_manual(self): 

754 import webbrowser 

755 webbrowser.open( 

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

757 

758 def _add_vtk_widget_size_menu_entries(self, menu): 

759 

760 group = qw.QActionGroup(menu) 

761 group.setExclusive(True) 

762 

763 def set_variable_size(): 

764 self.gui_state.fixed_size = False 

765 

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

767 variable_size_action.setCheckable(True) 

768 variable_size_action.setActionGroup(group) 

769 variable_size_action.triggered.connect(set_variable_size) 

770 

771 fixed_size_items = [] 

772 for nx, ny, label in [ 

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

774 (426, 240, ''), 

775 (640, 360, ''), 

776 (854, 480, '(FWVGA)'), 

777 (1280, 720, '(HD)'), 

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

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

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

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

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

783 (640, 480, '(VGA)'), 

784 (800, 600, '(SVGA)'), 

785 (None, None, 'Other'), 

786 (512, 512, ''), 

787 (1024, 1024, '')]: 

788 

789 if None in (nx, ny): 

790 menu.addSection(label) 

791 else: 

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

793 action = menu.addAction(name) 

794 action.setCheckable(True) 

795 action.setActionGroup(group) 

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

797 

798 def make_set_fixed_size(nx, ny): 

799 def set_fixed_size(): 

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

801 

802 return set_fixed_size 

803 

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

805 

806 def update_widget(*args): 

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

808 action.blockSignals(True) 

809 action.setChecked( 

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

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

812 action.blockSignals(False) 

813 

814 variable_size_action.blockSignals(True) 

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

816 variable_size_action.blockSignals(False) 

817 

818 update_widget() 

819 self.talkie_connect( 

820 self.gui_state, 'fixed_size', update_widget) 

821 

822 def update_vtk_widget_size(self, *args): 

823 if self.gui_state.fixed_size: 

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

825 wanted_size = qc.QSize(nx, ny) 

826 else: 

827 wanted_size = qc.QSize( 

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

829 

830 current_size = self.vtk_widget.size() 

831 

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

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

834 

835 self.vtk_widget.setFixedSize(wanted_size) 

836 

837 self.vtk_frame.recenter() 

838 self.check_vtk_resize() 

839 

840 def update_focal_point(self, *args): 

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

842 self.vtk_widget.setStatusTip( 

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

844 'change view plane orientation.' % g_modifier_key) 

845 else: 

846 self.vtk_widget.setStatusTip( 

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

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

849 'reverse sense.' % g_modifier_key) 

850 

851 def update_detached(self, *args): 

852 

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

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

855 

856 self.main_layout.removeWidget(self.vtk_frame) 

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

858 self.detached_window.show() 

859 self.vtk_widget.setFocus() 

860 

861 screens = common.get_app().screens() 

862 if len(screens) > 1: 

863 for screen in screens: 

864 if screen is not self.screen(): 

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

866 # .setScreen() does not work reliably, 

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

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

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

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

871 

872 self.detached_window.windowHandle().showMaximized() 

873 

874 frame = qw.QFrame() 

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

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

877 # frame.setAutoFillBackground(True) 

878 frame.setSizePolicy( 

879 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

880 

881 layout = qw.QGridLayout() 

882 frame.setLayout(layout) 

883 self.main_layout.insertWidget(0, frame) 

884 

885 self.state_editor = StateEditor(self) 

886 

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

888 

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

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

891 # layout.addWidget( 

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

893 

894 self.vtk_frame_substitute = frame 

895 

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

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

898 self.detached_window.hide() 

899 self.vtk_frame.setParent(self) 

900 if self.vtk_frame_substitute: 

901 self.main_layout.removeWidget(self.vtk_frame_substitute) 

902 self.state_editor.unbind_state() 

903 self.vtk_frame_substitute = None 

904 

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

906 self.detached_window = None 

907 self.vtk_widget.setFocus() 

908 

909 def attach(self): 

910 self.gui_state.detached = False 

911 

912 def export_image(self): 

913 

914 caption = 'Export Image' 

915 fn_out, _ = qw.QFileDialog.getSaveFileName( 

916 self, caption, 'image.png', 

917 options=common.qfiledialog_options) 

918 

919 if fn_out: 

920 self.save_image(fn_out) 

921 

922 def save_image(self, path): 

923 

924 original_fixed_size = self.gui_state.fixed_size 

925 if original_fixed_size is None: 

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

927 

928 wif = vtk.vtkWindowToImageFilter() 

929 wif.SetInput(self.renwin) 

930 wif.SetInputBufferTypeToRGBA() 

931 wif.SetScale(1, 1) 

932 wif.ReadFrontBufferOff() 

933 writer = vtk.vtkPNGWriter() 

934 writer.SetInputConnection(wif.GetOutputPort()) 

935 

936 self.renwin.Render() 

937 wif.Modified() 

938 writer.SetFileName(path) 

939 writer.Write() 

940 

941 self.gui_state.fixed_size = original_fixed_size 

942 

943 def update_render_settings(self, *args): 

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

945 self.ren.RemoveAllLights() 

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

947 self.ren.AddLight(li) 

948 

949 self._lighting = self.state.lighting 

950 

951 if self._background is None \ 

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

953 

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

955 self._background = self.state.background 

956 

957 self.update_view() 

958 

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

960 if self._animation: 

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

962 self.stop_animation() 

963 

964 self.disable_capture() 

965 self._animation = interpolator 

966 if output_path is None: 

967 self._animation_tstart = time.time() 

968 self._animation_iframe = None 

969 else: 

970 self._animation_iframe = 0 

971 mess = 'Rendering movie' 

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

973 

974 self._animation_timer = qc.QTimer(self) 

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

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

977 self._animation_timer.start() 

978 if output_path is not None: 

979 original_fixed_size = self.gui_state.fixed_size 

980 if original_fixed_size is None: 

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

982 

983 wif = vtk.vtkWindowToImageFilter() 

984 wif.SetInput(self.renwin) 

985 wif.SetInputBufferTypeToRGBA() 

986 wif.SetScale(1, 1) 

987 wif.ReadFrontBufferOff() 

988 writer = vtk.vtkPNGWriter() 

989 temp_path = tempfile.mkdtemp() 

990 self._animation_saver = ( 

991 wif, writer, temp_path, output_path, original_fixed_size) 

992 writer.SetInputConnection(wif.GetOutputPort()) 

993 

994 def next_animation_frame(self): 

995 

996 ani = self._animation 

997 if not ani: 

998 return 

999 

1000 if self._animation_iframe is not None: 

1001 state = ani( 

1002 ani.tmin 

1003 + self._animation_iframe * ani.dt) 

1004 

1005 self._animation_iframe += 1 

1006 else: 

1007 tnow = time.time() 

1008 state = ani(min( 

1009 ani.tmax, 

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

1011 

1012 self.set_state(state) 

1013 self.renwin.Render() 

1014 abort = False 

1015 if self._animation_saver: 

1016 abort = self.progressbars.set_status( 

1017 'Rendering movie', 

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

1019 can_abort=True) 

1020 

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

1022 wif.Modified() 

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

1024 writer.SetFileName(fn % self._animation_iframe) 

1025 writer.Write() 

1026 

1027 if self._animation_iframe is not None: 

1028 t = self._animation_iframe * ani.dt 

1029 else: 

1030 t = tnow - self._animation_tstart 

1031 

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

1033 self.stop_animation() 

1034 

1035 def stop_animation(self): 

1036 if self._animation_timer: 

1037 self._animation_timer.stop() 

1038 

1039 if self._animation_saver: 

1040 

1041 wif, writer, temp_path, output_path, original_fixed_size \ 

1042 = self._animation_saver 

1043 self.gui_state.fixed_size = original_fixed_size 

1044 

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

1046 check_call([ 

1047 'ffmpeg', '-y', 

1048 '-i', fn_path, 

1049 '-c:v', 'libx264', 

1050 '-preset', 'slow', 

1051 '-crf', '17', 

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

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

1054 output_path]) 

1055 shutil.rmtree(temp_path) 

1056 

1057 self._animation_saver = None 

1058 self._animation_saver 

1059 

1060 self.progressbars.set_status( 

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

1062 

1063 self._animation_tstart = None 

1064 self._animation_iframe = None 

1065 self._animation = None 

1066 self.enable_capture() 

1067 

1068 def set_state(self, state): 

1069 self.disable_capture() 

1070 try: 

1071 self._update_elements_enabled = False 

1072 self.setUpdatesEnabled(False) 

1073 self.state.diff_update(state) 

1074 self.state.sort_elements() 

1075 self.setUpdatesEnabled(True) 

1076 self._update_elements_enabled = True 

1077 self.update_elements() 

1078 finally: 

1079 self.enable_capture() 

1080 

1081 def periodical(self): 

1082 pass 

1083 

1084 def check_vtk_resize(self, *args): 

1085 render_window_size = self.renwin.GetSize() 

1086 if self._render_window_size != render_window_size: 

1087 self._render_window_size = render_window_size 

1088 self.resize_event(*render_window_size) 

1089 

1090 def update_elements(self, *_): 

1091 if not self._update_elements_enabled: 

1092 return 

1093 

1094 if self._in_update_elements: 

1095 return 

1096 

1097 self._in_update_elements = True 

1098 for estate in self.state.elements: 

1099 if estate.element_id not in self._elements: 

1100 new_element = estate.create() 

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

1102 type(new_element).__name__, 

1103 estate.element_id)) 

1104 self._elements[estate.element_id] = new_element 

1105 

1106 element = self._elements[estate.element_id] 

1107 

1108 if estate.element_id not in self._elements_active: 

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

1110 type(element).__name__, 

1111 estate.element_id)) 

1112 element.bind_state(estate) 

1113 element.set_parent(self) 

1114 self._elements_active[estate.element_id] = element 

1115 

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

1117 deactivate = [] 

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

1119 if element_id not in state_element_ids: 

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

1121 type(element).__name__, 

1122 element_id)) 

1123 element.unset_parent() 

1124 deactivate.append(element_id) 

1125 

1126 for element_id in deactivate: 

1127 del self._elements_active[element_id] 

1128 

1129 self._update_crosshair_bindings() 

1130 

1131 self._in_update_elements = False 

1132 

1133 def _update_crosshair_bindings(self): 

1134 

1135 def get_crosshair_element(): 

1136 for element in self.state.elements: 

1137 if element.element_id == 'crosshair': 

1138 return element 

1139 

1140 return None 

1141 

1142 crosshair = get_crosshair_element() 

1143 if crosshair is None or crosshair.is_connected: 

1144 return 

1145 

1146 def to_checkbox(state, widget): 

1147 widget.blockSignals(True) 

1148 widget.setChecked(state.visible) 

1149 widget.blockSignals(False) 

1150 

1151 def to_state(widget, state): 

1152 state.visible = widget.isChecked() 

1153 

1154 cb = self._crosshair_checkbox 

1155 vstate.state_bind( 

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

1157 cb, [cb.toggled], to_checkbox) 

1158 

1159 crosshair.is_connected = True 

1160 

1161 def add_actor_2d(self, actor): 

1162 if actor not in self._actors_2d: 

1163 self.ren.AddActor2D(actor) 

1164 self._actors_2d.add(actor) 

1165 

1166 def remove_actor_2d(self, actor): 

1167 if actor in self._actors_2d: 

1168 self.ren.RemoveActor2D(actor) 

1169 self._actors_2d.remove(actor) 

1170 

1171 def add_actor(self, actor): 

1172 if actor not in self._actors: 

1173 self.ren.AddActor(actor) 

1174 self._actors.add(actor) 

1175 

1176 def add_actor_list(self, actorlist): 

1177 for actor in actorlist: 

1178 self.add_actor(actor) 

1179 

1180 def remove_actor(self, actor): 

1181 if actor in self._actors: 

1182 self.ren.RemoveActor(actor) 

1183 self._actors.remove(actor) 

1184 

1185 def update_view(self): 

1186 self.vtk_widget.update() 

1187 

1188 def resize_event(self, size_x, size_y): 

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

1190 

1191 def button_event(self, obj, event): 

1192 if event == "LeftButtonPressEvent": 

1193 self.rotating = True 

1194 elif event == "LeftButtonReleaseEvent": 

1195 self.rotating = False 

1196 

1197 def mouse_move_event(self, obj, event): 

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

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

1200 

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

1202 center_x = size_x / 2.0 

1203 center_y = size_y / 2.0 

1204 

1205 if self.rotating: 

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

1207 

1208 def myWheelEvent(self, event): 

1209 

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

1211 

1212 if angle > 200: 

1213 angle = 200 

1214 

1215 if angle < -200: 

1216 angle = -200 

1217 

1218 self.disable_capture() 

1219 try: 

1220 self.do_dolly(-angle/100.) 

1221 finally: 

1222 self.enable_capture(aggregate='distance') 

1223 

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

1225 

1226 dx = x0 - x 

1227 dy = y0 - y 

1228 

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

1230 focp = self.gui_state.focal_point 

1231 

1232 if focp == 'center': 

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

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

1235 

1236 lat = self.state.lat 

1237 lon = self.state.lon 

1238 factor = self.state.distance / 10.0 

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

1240 else: 

1241 lat = 90. - self.state.dip 

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

1243 factor = 0.5 

1244 factor_lat = 1.0 

1245 

1246 dlat = dy * factor 

1247 dlon = dx * factor * factor_lat 

1248 

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

1250 lon += dlon 

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

1252 

1253 if focp == 'center': 

1254 self.state.lat = float(lat) 

1255 self.state.lon = float(lon) 

1256 else: 

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

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

1259 

1260 def do_dolly(self, v): 

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

1262 

1263 def key_down_event(self, obj, event): 

1264 k = obj.GetKeyCode() 

1265 if k == 'f': 

1266 self.gui_state.next_focal_point() 

1267 

1268 elif k == 'r': 

1269 self.reset_strike_dip() 

1270 

1271 elif k == 'p': 

1272 print(self.state) 

1273 

1274 elif k == 'i': 

1275 for elem in self.state.elements: 

1276 if isinstance(elem, elements.IcosphereState): 

1277 elem.visible = not elem.visible 

1278 

1279 elif k == 'c': 

1280 for elem in self.state.elements: 

1281 if isinstance(elem, elements.CoastlinesState): 

1282 elem.visible = not elem.visible 

1283 

1284 elif k == 't': 

1285 if not any( 

1286 isinstance(elem, elements.TopoState) 

1287 for elem in self.state.elements): 

1288 

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

1290 else: 

1291 for elem in self.state.elements: 

1292 if isinstance(elem, elements.TopoState): 

1293 elem.visible = not elem.visible 

1294 

1295 # elif k == ' ': 

1296 # self.toggle_panel_visibility() 

1297 

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

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

1300 

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

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

1303 

1304 def controls_navigation(self): 

1305 frame = qw.QFrame(self) 

1306 frame.setSizePolicy( 

1307 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1308 layout = qw.QGridLayout() 

1309 frame.setLayout(layout) 

1310 

1311 # lat, lon, depth 

1312 

1313 layout.addWidget( 

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

1315 

1316 le = qw.QLineEdit() 

1317 le.setStatusTip( 

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

1319 'Focal point location.') 

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

1321 

1322 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1325 

1326 def lineedit_to_lat_lon_depth(widget, state): 

1327 self.disable_capture() 

1328 try: 

1329 s = str(widget.text()) 

1330 choices = location_to_choices(s) 

1331 if len(choices) > 0: 

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

1333 choices[0].get_lat_lon_depth() 

1334 else: 

1335 raise NoLocationChoices(s) 

1336 

1337 finally: 

1338 self.enable_capture() 

1339 

1340 self._state_bind( 

1341 ['lat', 'lon', 'depth'], 

1342 lineedit_to_lat_lon_depth, 

1343 le, [le.editingFinished, le.returnPressed], 

1344 lat_lon_depth_to_lineedit) 

1345 

1346 self.lat_lon_lineedit = le 

1347 

1348 # focal point 

1349 

1350 cb = qw.QCheckBox('Fix') 

1351 cb.setStatusTip( 

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

1353 % g_modifier_key) 

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

1355 

1356 def focal_point_to_checkbox(state, widget): 

1357 widget.blockSignals(True) 

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

1359 widget.blockSignals(False) 

1360 

1361 def checkbox_to_focal_point(widget, state): 

1362 self.gui_state.focal_point = \ 

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

1364 

1365 self._gui_state_bind( 

1366 ['focal_point'], checkbox_to_focal_point, 

1367 cb, [cb.toggled], focal_point_to_checkbox) 

1368 

1369 self.focal_point_checkbox = cb 

1370 

1371 self.talkie_connect( 

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

1373 

1374 self.update_focal_point() 

1375 

1376 # strike, dip 

1377 

1378 layout.addWidget( 

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

1380 

1381 le = qw.QLineEdit() 

1382 le.setStatusTip( 

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

1384 'direction.') 

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

1386 

1387 def strike_dip_to_lineedit(state, widget): 

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

1389 

1390 def lineedit_to_strike_dip(widget, state): 

1391 s = str(widget.text()) 

1392 string_to_strike_dip = { 

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

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

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

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

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

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

1399 

1400 self.disable_capture() 

1401 if s in string_to_strike_dip: 

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

1403 

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

1405 try: 

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

1407 except Exception: 

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

1409 finally: 

1410 self.enable_capture() 

1411 

1412 self._state_bind( 

1413 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1415 

1416 self.strike_dip_lineedit = le 

1417 

1418 but = qw.QPushButton('Reset') 

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

1420 but.clicked.connect(self.reset_strike_dip) 

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

1422 

1423 # crosshair 

1424 

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

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

1427 

1428 # camera bindings 

1429 self.talkie_connect( 

1430 self.state, 

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

1432 self.update_camera) 

1433 

1434 self.talkie_connect( 

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

1436 

1437 return frame 

1438 

1439 def controls_time(self): 

1440 frame = qw.QFrame(self) 

1441 frame.setSizePolicy( 

1442 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1443 

1444 layout = qw.QGridLayout() 

1445 frame.setLayout(layout) 

1446 

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

1448 le_tmin = qw.QLineEdit() 

1449 layout.addWidget(le_tmin, 0, 1) 

1450 

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

1452 le_tmax = qw.QLineEdit() 

1453 layout.addWidget(le_tmax, 1, 1) 

1454 

1455 label_tcursor = qw.QLabel() 

1456 

1457 label_tcursor.setSizePolicy( 

1458 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1459 

1460 layout.addWidget(label_tcursor, 2, 1) 

1461 self._label_tcursor = label_tcursor 

1462 

1463 self._state_bind( 

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

1465 [le_tmin.editingFinished, le_tmin.returnPressed], 

1466 common.time_to_lineedit, 

1467 attribute='tmin') 

1468 self._state_bind( 

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

1470 [le_tmax.editingFinished, le_tmax.returnPressed], 

1471 common.time_to_lineedit, 

1472 attribute='tmax') 

1473 

1474 self.tmin_lineedit = le_tmin 

1475 self.tmax_lineedit = le_tmax 

1476 

1477 range_edit = RangeEdit() 

1478 range_edit.rangeEditPressed.connect(self.disable_capture) 

1479 range_edit.rangeEditReleased.connect(self.enable_capture) 

1480 range_edit.set_data_provider(self) 

1481 range_edit.set_data_name('time') 

1482 

1483 xblock = [False] 

1484 

1485 def range_to_range_edit(state, widget): 

1486 if not xblock[0]: 

1487 widget.blockSignals(True) 

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

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

1490 widget.blockSignals(False) 

1491 

1492 def range_edit_to_range(widget, state): 

1493 xblock[0] = True 

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

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

1496 xblock[0] = False 

1497 

1498 self._state_bind( 

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

1500 range_edit_to_range, 

1501 range_edit, 

1502 [range_edit.rangeChanged, range_edit.focusChanged], 

1503 range_to_range_edit) 

1504 

1505 def handle_tcursor_changed(): 

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

1507 

1508 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1509 

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

1511 

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

1513 le_focus = qw.QLineEdit() 

1514 layout.addWidget(le_focus, 4, 1) 

1515 

1516 def focus_to_lineedit(state, widget): 

1517 if state.tduration is None: 

1518 widget.setText('') 

1519 else: 

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

1521 guts.str_duration(state.tduration), 

1522 state.tposition)) 

1523 

1524 def lineedit_to_focus(widget, state): 

1525 s = str(widget.text()) 

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

1527 try: 

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

1529 state.tduration = None 

1530 state.tposition = 0.0 

1531 else: 

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

1533 if len(w) > 1: 

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

1535 else: 

1536 state.tposition = 0.0 

1537 

1538 except Exception: 

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

1540 

1541 self._state_bind( 

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

1543 [le_focus.editingFinished, le_focus.returnPressed], 

1544 focus_to_lineedit) 

1545 

1546 label_effective_tmin = qw.QLabel() 

1547 label_effective_tmax = qw.QLabel() 

1548 

1549 label_effective_tmin.setSizePolicy( 

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

1551 label_effective_tmax.setSizePolicy( 

1552 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1553 label_effective_tmin.setMinimumSize( 

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

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

1556 

1557 layout.addWidget(label_effective_tmin, 5, 1) 

1558 layout.addWidget(label_effective_tmax, 6, 1) 

1559 

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

1561 self.talkie_connect( 

1562 self.state, var, self.update_effective_time_labels) 

1563 

1564 self._label_effective_tmin = label_effective_tmin 

1565 self._label_effective_tmax = label_effective_tmax 

1566 

1567 self.talkie_connect( 

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

1569 

1570 return frame 

1571 

1572 def controls_appearance(self): 

1573 frame = qw.QFrame(self) 

1574 frame.setSizePolicy( 

1575 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1576 layout = qw.QGridLayout() 

1577 frame.setLayout(layout) 

1578 

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

1580 

1581 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1582 layout.addWidget(cb, 0, 1) 

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

1584 

1585 self.talkie_connect( 

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

1587 

1588 # background 

1589 

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

1591 

1592 cb = common.strings_to_combobox( 

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

1594 

1595 layout.addWidget(cb, 1, 1) 

1596 vstate.state_bind_combobox_background( 

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

1598 

1599 self.talkie_connect( 

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

1601 

1602 return frame 

1603 

1604 def controls_snapshots(self): 

1605 return snapshots_mod.SnapshotsPanel(self) 

1606 

1607 def update_effective_time_labels(self, *args): 

1608 tmin = self.state.tmin_effective 

1609 tmax = self.state.tmax_effective 

1610 

1611 stmin = common.time_or_none_to_str(tmin) 

1612 stmax = common.time_or_none_to_str(tmax) 

1613 

1614 self._label_effective_tmin.setText(stmin) 

1615 self._label_effective_tmax.setText(stmax) 

1616 

1617 def update_tcursor(self, *args): 

1618 tcursor = self.gui_state.tcursor 

1619 stcursor = common.time_or_none_to_str(tcursor) 

1620 self._label_tcursor.setText(stcursor) 

1621 

1622 def reset_strike_dip(self, *args): 

1623 self.state.strike = 90. 

1624 self.state.dip = 0 

1625 self.gui_state.focal_point = 'center' 

1626 

1627 def get_camera_geometry(self): 

1628 

1629 def rtp2xyz(rtp): 

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

1631 

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

1633 

1634 cam_rtp = num.array([ 

1635 radius+self.state.distance, 

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

1637 self.state.lon * d2r]) 

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

1639 cam, up, foc = \ 

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

1641 

1642 foc_rtp = num.array([ 

1643 radius, 

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

1645 self.state.lon * d2r]) 

1646 

1647 foc = rtp2xyz(foc_rtp) 

1648 

1649 rot_world = pmt.euler_to_matrix( 

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

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

1652 0.0*d2r).T 

1653 

1654 rot_cam = pmt.euler_to_matrix( 

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

1656 

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

1658 

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

1660 up = num.dot(rot, up) 

1661 return cam, up, foc 

1662 

1663 def update_camera(self, *args): 

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

1665 camera = self.ren.GetActiveCamera() 

1666 camera.SetPosition(*cam) 

1667 camera.SetFocalPoint(*foc) 

1668 camera.SetViewUp(*up) 

1669 

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

1671 

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

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

1674 

1675 # if horizon == 0.0: 

1676 # horizon = 2.0 + self.state.distance 

1677 

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

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

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

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

1682 # clip_dist = feature_horizon 

1683 

1684 camera.SetClippingRange( 

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

1686 

1687 self.camera_params = ( 

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

1689 

1690 self.update_view() 

1691 

1692 def add_panel( 

1693 self, title_label, panel, 

1694 visible=False, 

1695 # volatile=False, 

1696 tabify=True, 

1697 where=qc.Qt.RightDockWidgetArea, 

1698 remove=None, 

1699 title_controls=[], 

1700 scrollable=True): 

1701 

1702 dockwidget = common.MyDockWidget( 

1703 self, title_label, title_controls=title_controls) 

1704 

1705 if not visible: 

1706 dockwidget.hide() 

1707 

1708 if not self.gui_state.panels_visible: 

1709 dockwidget.block() 

1710 

1711 if scrollable: 

1712 scrollarea = common.MyScrollArea() 

1713 scrollarea.setWidget(panel) 

1714 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

1715 scrollarea.setSizeAdjustPolicy( 

1716 qw.QAbstractScrollArea.AdjustToContents) 

1717 scrollarea.setFrameShape(qw.QFrame.NoFrame) 

1718 

1719 dockwidget.setWidget(scrollarea) 

1720 else: 

1721 dockwidget.setWidget(panel) 

1722 

1723 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1725 

1726 self.addDockWidget(where, dockwidget) 

1727 

1728 nwrap = 4 

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

1730 self.tabifyDockWidget( 

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

1732 

1733 mitem = dockwidget.toggleViewAction() 

1734 

1735 def update_label(*args): 

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

1737 self.update_slug_abbreviated_lengths() 

1738 

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

1740 dockwidget.titlebar._title_label.title_changed.connect( 

1741 self.update_slug_abbreviated_lengths) 

1742 

1743 update_label() 

1744 

1745 self._panel_togglers[dockwidget] = mitem 

1746 self.panels_menu.addAction(mitem) 

1747 if visible: 

1748 dockwidget.setVisible(True) 

1749 dockwidget.setFocus() 

1750 dockwidget.raise_() 

1751 

1752 def stack_panels(self): 

1753 dockwidgets = self.findChildren(common.MyDockWidget) 

1754 by_area = defaultdict(list) 

1755 for dw in dockwidgets: 

1756 area = self.dockWidgetArea(dw) 

1757 by_area[area].append(dw) 

1758 

1759 for dockwidgets in by_area.values(): 

1760 dw_last = None 

1761 for dw in dockwidgets: 

1762 if dw_last is not None: 

1763 self.tabifyDockWidget(dw_last, dw) 

1764 

1765 dw_last = dw 

1766 

1767 def update_slug_abbreviated_lengths(self): 

1768 dockwidgets = self.findChildren(common.MyDockWidget) 

1769 title_labels = [] 

1770 for dw in dockwidgets: 

1771 title_labels.append(dw.titlebar._title_label) 

1772 

1773 by_title = defaultdict(list) 

1774 for tl in title_labels: 

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

1776 

1777 for group in by_title.values(): 

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

1779 

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

1781 nunique = len(set(slugs)) 

1782 

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

1784 n -= 1 

1785 

1786 if n > 0: 

1787 n = max(3, n) 

1788 

1789 for tl in group: 

1790 tl.set_slug_abbreviated_length(n) 

1791 

1792 def get_dockwidget(self, panel): 

1793 dockwidget = panel 

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

1795 dockwidget = dockwidget.parent() 

1796 

1797 return dockwidget 

1798 

1799 def raise_panel(self, panel): 

1800 dockwidget = self.get_dockwidget(panel) 

1801 dockwidget.setVisible(True) 

1802 dockwidget.setFocus() 

1803 dockwidget.raise_() 

1804 

1805 def toggle_panel_visibility(self): 

1806 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1807 

1808 def update_panel_visibility(self, *args): 

1809 self.setUpdatesEnabled(False) 

1810 mbar = self.menuBar() 

1811 sbar = self.statusBar() 

1812 dockwidgets = self.findChildren(common.MyDockWidget) 

1813 

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

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

1816 # objects. 

1817 mbar.setFixedHeight( 

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

1819 

1820 sbar.setVisible(self.gui_state.panels_visible) 

1821 for dockwidget in dockwidgets: 

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

1823 

1824 self.setUpdatesEnabled(True) 

1825 

1826 def remove_panel(self, panel): 

1827 dockwidget = self.get_dockwidget(panel) 

1828 self.removeDockWidget(dockwidget) 

1829 dockwidget.setParent(None) 

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

1831 

1832 def register_data_provider(self, provider): 

1833 if provider not in self.data_providers: 

1834 self.data_providers.append(provider) 

1835 

1836 def unregister_data_provider(self, provider): 

1837 if provider in self.data_providers: 

1838 self.data_providers.remove(provider) 

1839 

1840 def iter_data(self, name): 

1841 for provider in self.data_providers: 

1842 for data in provider.iter_data(name): 

1843 yield data 

1844 

1845 def confirm_close(self): 

1846 ret = qw.QMessageBox.question( 

1847 self, 

1848 'Sparrow', 

1849 'Close Sparrow window?', 

1850 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1851 qw.QMessageBox.Ok) 

1852 

1853 return ret == qw.QMessageBox.Ok 

1854 

1855 def closeEvent(self, event): 

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

1857 self.attach() 

1858 self.closing = True 

1859 event.accept() 

1860 else: 

1861 event.ignore() 

1862 

1863 def is_closing(self): 

1864 return self.closing 

1865 

1866 

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

1868 

1869 from pyrocko import util 

1870 from pyrocko.gui import util as gui_util 

1871 from . import common 

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

1873 

1874 global win 

1875 

1876 app = gui_util.get_app() 

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

1878 app.set_main_window(win) 

1879 

1880 gui_util.app.install_sigint_handler() 

1881 

1882 try: 

1883 gui_util.app.exec_() 

1884 finally: 

1885 gui_util.app.uninstall_sigint_handler() 

1886 app.unset_main_window() 

1887 common.set_viewer(None) 

1888 del win 

1889 gc.collect()