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 ('Geonames', elements.GeonamesState()), 

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

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

414 ('HUD Subtitle', elements.HudState( 

415 template='Subtitle')), 

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

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

418 position='top-left')), 

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

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

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

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

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

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

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

426 

427 def wrap_add_element(estate): 

428 def add_element(*args): 

429 new_element = guts.clone(estate) 

430 new_element.element_id = elements.random_id() 

431 self.state.elements.append(new_element) 

432 self.state.sort_elements() 

433 

434 return add_element 

435 

436 mitem = qw.QAction(name, self) 

437 

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

439 

440 menu.addAction(mitem) 

441 

442 menu = mbar.addMenu('Help') 

443 

444 menu.addAction( 

445 'Interactive Tour', 

446 self.start_tour) 

447 

448 menu.addAction( 

449 'Online Manual', 

450 self.open_manual) 

451 

452 self.data_providers = [] 

453 self.elements = {} 

454 

455 self.detached_window = None 

456 

457 self.main_frame = qw.QFrame() 

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

459 

460 self.vtk_frame = CenteringScrollArea() 

461 

462 self.vtk_widget = QVTKWidget(self, self) 

463 self.vtk_frame.setWidget(self.vtk_widget) 

464 

465 self.main_layout = qw.QVBoxLayout() 

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

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

468 

469 pb = Progressbars(self) 

470 self.progressbars = pb 

471 self.main_layout.addWidget(pb) 

472 

473 self.main_frame.setLayout(self.main_layout) 

474 

475 self.vtk_frame_substitute = None 

476 

477 self.add_panel( 

478 'Navigation', 

479 self.controls_navigation(), 

480 visible=True, 

481 scrollable=False, 

482 where=qc.Qt.LeftDockWidgetArea) 

483 

484 self.add_panel( 

485 'Time', 

486 self.controls_time(), 

487 visible=True, 

488 scrollable=False, 

489 where=qc.Qt.LeftDockWidgetArea) 

490 

491 self.add_panel( 

492 'Appearance', 

493 self.controls_appearance(), 

494 visible=True, 

495 scrollable=False, 

496 where=qc.Qt.LeftDockWidgetArea) 

497 

498 snapshots_panel = self.controls_snapshots() 

499 self.snapshots_panel = snapshots_panel 

500 self.add_panel( 

501 'Snapshots', 

502 snapshots_panel, 

503 visible=False, 

504 scrollable=False, 

505 where=qc.Qt.LeftDockWidgetArea) 

506 

507 snapshots_panel.setup_menu(snapshots_menu) 

508 

509 self.setCentralWidget(self.main_frame) 

510 

511 self.mesh = None 

512 

513 ren = vtk.vtkRenderer() 

514 

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

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

517 # ren.TwoSidedLightingOn() 

518 # ren.SetUseShadows(1) 

519 

520 self._lighting = None 

521 self._background = None 

522 

523 self.ren = ren 

524 self.update_render_settings() 

525 self.update_camera() 

526 

527 renwin = self.vtk_widget.GetRenderWindow() 

528 

529 if self._use_depth_peeling: 

530 renwin.SetAlphaBitPlanes(1) 

531 renwin.SetMultiSamples(0) 

532 

533 ren.SetUseDepthPeeling(1) 

534 ren.SetMaximumNumberOfPeels(100) 

535 ren.SetOcclusionRatio(0.1) 

536 

537 ren.SetUseFXAA(1) 

538 # ren.SetUseHiddenLineRemoval(1) 

539 # ren.SetBackingStore(1) 

540 

541 self.renwin = renwin 

542 

543 # renwin.LineSmoothingOn() 

544 # renwin.PointSmoothingOn() 

545 # renwin.PolygonSmoothingOn() 

546 

547 renwin.AddRenderer(ren) 

548 

549 iren = renwin.GetInteractor() 

550 iren.LightFollowCameraOn() 

551 iren.SetInteractorStyle(None) 

552 

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

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

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

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

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

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

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

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

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

562 

563 renwin.Render() 

564 

565 iren.Initialize() 

566 

567 self.iren = iren 

568 

569 self.rotating = False 

570 

571 self._elements = {} 

572 self._elements_active = {} 

573 

574 self.talkie_connect( 

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

576 

577 self.state.elements.append(elements.IcosphereState( 

578 element_id='icosphere', 

579 level=4, 

580 smooth=True, 

581 opacity=0.5, 

582 ambient=0.1)) 

583 

584 self.state.elements.append(elements.GridState( 

585 element_id='grid')) 

586 self.state.elements.append(elements.CoastlinesState( 

587 element_id='coastlines')) 

588 self.state.elements.append(elements.CrosshairState( 

589 element_id='crosshair')) 

590 

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

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

593 # self.state.elements.append( 

594 # elements.CatalogState( 

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

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

597 

598 if events: 

599 self.state.elements.append( 

600 elements.CatalogState( 

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

602 

603 self.state.sort_elements() 

604 

605 if snapshots: 

606 snapshots_ = [] 

607 for obj in snapshots: 

608 if isinstance(obj, str): 

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

610 else: 

611 snapshots_.append(obj) 

612 

613 snapshots_panel.add_snapshots(snapshots_) 

614 self.raise_panel(snapshots_panel) 

615 snapshots_panel.goto_snapshot(1) 

616 

617 self.timer = qc.QTimer(self) 

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

619 self.timer.setInterval(1000) 

620 self.timer.start() 

621 

622 self._animation_saver = None 

623 

624 self.closing = False 

625 self.vtk_widget.setFocus() 

626 

627 self.update_detached() 

628 

629 self.status( 

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

631 

632 self.status( 

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

634 

635 self.show() 

636 self.windowHandle().showMaximized() 

637 

638 self.talkie_connect( 

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

640 

641 self.update_vtk_widget_size() 

642 

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

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

645 

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

647 self.capture_state() 

648 

649 set_download_callback(self.update_download_progress) 

650 

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

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

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

654 

655 self.start_tour() 

656 

657 def update_download_progress(self, message, args): 

658 self.download_progress_update.emit() 

659 

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

661 self.statusBar().showMessage( 

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

663 

664 def disable_capture(self): 

665 self._block_capture += 1 

666 

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

668 

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

670 if self._block_capture > 0: 

671 self._block_capture -= 1 

672 

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

674 

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

676 self.capture_state(aggregate=aggregate) 

677 

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

679 if self._block_capture: 

680 return 

681 

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

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

684 

685 if aggregate is not None: 

686 if aggregate == self._undo_aggregate: 

687 self._undo_stack.pop() 

688 

689 self._undo_aggregate = aggregate 

690 else: 

691 self._undo_aggregate = None 

692 

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

694 len(self._undo_stack) + 1, 

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

696 '\n'.join( 

697 ' - %s' % s 

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

699 self.state).splitlines()) 

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

701 

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

703 self._redo_stack.clear() 

704 

705 def undo(self): 

706 self._undo_aggregate = None 

707 

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

709 return 

710 

711 state = self._undo_stack.pop() 

712 self._redo_stack.append(state) 

713 state = self._undo_stack[-1] 

714 

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

716 len(self._undo_stack), 

717 '\n'.join( 

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

719 

720 self.disable_capture() 

721 try: 

722 self.set_state(state) 

723 finally: 

724 self.enable_capture(drop=True) 

725 

726 def redo(self): 

727 self._undo_aggregate = None 

728 

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

730 return 

731 

732 state = self._redo_stack.pop() 

733 self._undo_stack.append(state) 

734 

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

736 len(self._redo_stack), 

737 '\n'.join( 

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

739 

740 self.disable_capture() 

741 try: 

742 self.set_state(state) 

743 finally: 

744 self.enable_capture(drop=True) 

745 

746 def start_tour(self): 

747 snapshots_ = snapshots_mod.load_snapshots( 

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

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

750 self.snapshots_panel.add_snapshots(snapshots_) 

751 self.raise_panel(self.snapshots_panel) 

752 self.snapshots_panel.transition_to_next_snapshot() 

753 

754 def open_manual(self): 

755 import webbrowser 

756 webbrowser.open( 

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

758 

759 def _add_vtk_widget_size_menu_entries(self, menu): 

760 

761 group = qw.QActionGroup(menu) 

762 group.setExclusive(True) 

763 

764 def set_variable_size(): 

765 self.gui_state.fixed_size = False 

766 

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

768 variable_size_action.setCheckable(True) 

769 variable_size_action.setActionGroup(group) 

770 variable_size_action.triggered.connect(set_variable_size) 

771 

772 fixed_size_items = [] 

773 for nx, ny, label in [ 

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

775 (426, 240, ''), 

776 (640, 360, ''), 

777 (854, 480, '(FWVGA)'), 

778 (1280, 720, '(HD)'), 

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

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

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

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

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

784 (640, 480, '(VGA)'), 

785 (800, 600, '(SVGA)'), 

786 (None, None, 'Other'), 

787 (512, 512, ''), 

788 (1024, 1024, '')]: 

789 

790 if None in (nx, ny): 

791 menu.addSection(label) 

792 else: 

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

794 action = menu.addAction(name) 

795 action.setCheckable(True) 

796 action.setActionGroup(group) 

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

798 

799 def make_set_fixed_size(nx, ny): 

800 def set_fixed_size(): 

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

802 

803 return set_fixed_size 

804 

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

806 

807 def update_widget(*args): 

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

809 action.blockSignals(True) 

810 action.setChecked( 

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

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

813 action.blockSignals(False) 

814 

815 variable_size_action.blockSignals(True) 

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

817 variable_size_action.blockSignals(False) 

818 

819 update_widget() 

820 self.talkie_connect( 

821 self.gui_state, 'fixed_size', update_widget) 

822 

823 def update_vtk_widget_size(self, *args): 

824 if self.gui_state.fixed_size: 

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

826 wanted_size = qc.QSize(nx, ny) 

827 else: 

828 wanted_size = qc.QSize( 

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

830 

831 current_size = self.vtk_widget.size() 

832 

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

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

835 

836 self.vtk_widget.setFixedSize(wanted_size) 

837 

838 self.vtk_frame.recenter() 

839 self.check_vtk_resize() 

840 

841 def update_focal_point(self, *args): 

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

843 self.vtk_widget.setStatusTip( 

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

845 'change view plane orientation.' % g_modifier_key) 

846 else: 

847 self.vtk_widget.setStatusTip( 

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

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

850 'reverse sense.' % g_modifier_key) 

851 

852 def update_detached(self, *args): 

853 

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

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

856 

857 self.main_layout.removeWidget(self.vtk_frame) 

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

859 self.detached_window.show() 

860 self.vtk_widget.setFocus() 

861 

862 screens = common.get_app().screens() 

863 if len(screens) > 1: 

864 for screen in screens: 

865 if screen is not self.screen(): 

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

867 # .setScreen() does not work reliably, 

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

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

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

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

872 

873 self.detached_window.windowHandle().showMaximized() 

874 

875 frame = qw.QFrame() 

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

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

878 # frame.setAutoFillBackground(True) 

879 frame.setSizePolicy( 

880 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

881 

882 layout = qw.QGridLayout() 

883 frame.setLayout(layout) 

884 self.main_layout.insertWidget(0, frame) 

885 

886 self.state_editor = StateEditor(self) 

887 

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

889 

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

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

892 # layout.addWidget( 

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

894 

895 self.vtk_frame_substitute = frame 

896 

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

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

899 self.detached_window.hide() 

900 self.vtk_frame.setParent(self) 

901 if self.vtk_frame_substitute: 

902 self.main_layout.removeWidget(self.vtk_frame_substitute) 

903 self.state_editor.unbind_state() 

904 self.vtk_frame_substitute = None 

905 

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

907 self.detached_window = None 

908 self.vtk_widget.setFocus() 

909 

910 def attach(self): 

911 self.gui_state.detached = False 

912 

913 def export_image(self): 

914 

915 caption = 'Export Image' 

916 fn_out, _ = qw.QFileDialog.getSaveFileName( 

917 self, caption, 'image.png', 

918 options=common.qfiledialog_options) 

919 

920 if fn_out: 

921 self.save_image(fn_out) 

922 

923 def save_image(self, path): 

924 

925 original_fixed_size = self.gui_state.fixed_size 

926 if original_fixed_size is None: 

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

928 

929 wif = vtk.vtkWindowToImageFilter() 

930 wif.SetInput(self.renwin) 

931 wif.SetInputBufferTypeToRGBA() 

932 wif.SetScale(1, 1) 

933 wif.ReadFrontBufferOff() 

934 writer = vtk.vtkPNGWriter() 

935 writer.SetInputConnection(wif.GetOutputPort()) 

936 

937 self.renwin.Render() 

938 wif.Modified() 

939 writer.SetFileName(path) 

940 writer.Write() 

941 

942 self.gui_state.fixed_size = original_fixed_size 

943 

944 def update_render_settings(self, *args): 

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

946 self.ren.RemoveAllLights() 

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

948 self.ren.AddLight(li) 

949 

950 self._lighting = self.state.lighting 

951 

952 if self._background is None \ 

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

954 

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

956 self._background = self.state.background 

957 

958 self.update_view() 

959 

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

961 if self._animation: 

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

963 self.stop_animation() 

964 

965 self.disable_capture() 

966 self._animation = interpolator 

967 if output_path is None: 

968 self._animation_tstart = time.time() 

969 self._animation_iframe = None 

970 else: 

971 self._animation_iframe = 0 

972 mess = 'Rendering movie' 

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

974 

975 self._animation_timer = qc.QTimer(self) 

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

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

978 self._animation_timer.start() 

979 if output_path is not None: 

980 original_fixed_size = self.gui_state.fixed_size 

981 if original_fixed_size is None: 

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

983 

984 wif = vtk.vtkWindowToImageFilter() 

985 wif.SetInput(self.renwin) 

986 wif.SetInputBufferTypeToRGBA() 

987 wif.SetScale(1, 1) 

988 wif.ReadFrontBufferOff() 

989 writer = vtk.vtkPNGWriter() 

990 temp_path = tempfile.mkdtemp() 

991 self._animation_saver = ( 

992 wif, writer, temp_path, output_path, original_fixed_size) 

993 writer.SetInputConnection(wif.GetOutputPort()) 

994 

995 def next_animation_frame(self): 

996 

997 ani = self._animation 

998 if not ani: 

999 return 

1000 

1001 if self._animation_iframe is not None: 

1002 state = ani( 

1003 ani.tmin 

1004 + self._animation_iframe * ani.dt) 

1005 

1006 self._animation_iframe += 1 

1007 else: 

1008 tnow = time.time() 

1009 state = ani(min( 

1010 ani.tmax, 

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

1012 

1013 self.set_state(state) 

1014 self.renwin.Render() 

1015 abort = False 

1016 if self._animation_saver: 

1017 abort = self.progressbars.set_status( 

1018 'Rendering movie', 

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

1020 can_abort=True) 

1021 

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

1023 wif.Modified() 

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

1025 writer.SetFileName(fn % self._animation_iframe) 

1026 writer.Write() 

1027 

1028 if self._animation_iframe is not None: 

1029 t = self._animation_iframe * ani.dt 

1030 else: 

1031 t = tnow - self._animation_tstart 

1032 

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

1034 self.stop_animation() 

1035 

1036 def stop_animation(self): 

1037 if self._animation_timer: 

1038 self._animation_timer.stop() 

1039 

1040 if self._animation_saver: 

1041 

1042 wif, writer, temp_path, output_path, original_fixed_size \ 

1043 = self._animation_saver 

1044 self.gui_state.fixed_size = original_fixed_size 

1045 

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

1047 check_call([ 

1048 'ffmpeg', '-y', 

1049 '-i', fn_path, 

1050 '-c:v', 'libx264', 

1051 '-preset', 'slow', 

1052 '-crf', '17', 

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

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

1055 output_path]) 

1056 shutil.rmtree(temp_path) 

1057 

1058 self._animation_saver = None 

1059 self._animation_saver 

1060 

1061 self.progressbars.set_status( 

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

1063 

1064 self._animation_tstart = None 

1065 self._animation_iframe = None 

1066 self._animation = None 

1067 self.enable_capture() 

1068 

1069 def set_state(self, state): 

1070 self.disable_capture() 

1071 try: 

1072 self._update_elements_enabled = False 

1073 self.setUpdatesEnabled(False) 

1074 self.state.diff_update(state) 

1075 self.state.sort_elements() 

1076 self.setUpdatesEnabled(True) 

1077 self._update_elements_enabled = True 

1078 self.update_elements() 

1079 finally: 

1080 self.enable_capture() 

1081 

1082 def periodical(self): 

1083 pass 

1084 

1085 def check_vtk_resize(self, *args): 

1086 render_window_size = self.renwin.GetSize() 

1087 if self._render_window_size != render_window_size: 

1088 self._render_window_size = render_window_size 

1089 self.resize_event(*render_window_size) 

1090 

1091 def update_elements(self, *_): 

1092 if not self._update_elements_enabled: 

1093 return 

1094 

1095 if self._in_update_elements: 

1096 return 

1097 

1098 self._in_update_elements = True 

1099 for estate in self.state.elements: 

1100 if estate.element_id not in self._elements: 

1101 new_element = estate.create() 

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

1103 type(new_element).__name__, 

1104 estate.element_id)) 

1105 self._elements[estate.element_id] = new_element 

1106 

1107 element = self._elements[estate.element_id] 

1108 

1109 if estate.element_id not in self._elements_active: 

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

1111 type(element).__name__, 

1112 estate.element_id)) 

1113 element.bind_state(estate) 

1114 element.set_parent(self) 

1115 self._elements_active[estate.element_id] = element 

1116 

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

1118 deactivate = [] 

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

1120 if element_id not in state_element_ids: 

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

1122 type(element).__name__, 

1123 element_id)) 

1124 element.unset_parent() 

1125 deactivate.append(element_id) 

1126 

1127 for element_id in deactivate: 

1128 del self._elements_active[element_id] 

1129 

1130 self._update_crosshair_bindings() 

1131 

1132 self._in_update_elements = False 

1133 

1134 def _update_crosshair_bindings(self): 

1135 

1136 def get_crosshair_element(): 

1137 for element in self.state.elements: 

1138 if element.element_id == 'crosshair': 

1139 return element 

1140 

1141 return None 

1142 

1143 crosshair = get_crosshair_element() 

1144 if crosshair is None or crosshair.is_connected: 

1145 return 

1146 

1147 def to_checkbox(state, widget): 

1148 widget.blockSignals(True) 

1149 widget.setChecked(state.visible) 

1150 widget.blockSignals(False) 

1151 

1152 def to_state(widget, state): 

1153 state.visible = widget.isChecked() 

1154 

1155 cb = self._crosshair_checkbox 

1156 vstate.state_bind( 

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

1158 cb, [cb.toggled], to_checkbox) 

1159 

1160 crosshair.is_connected = True 

1161 

1162 def add_actor_2d(self, actor): 

1163 if actor not in self._actors_2d: 

1164 self.ren.AddActor2D(actor) 

1165 self._actors_2d.add(actor) 

1166 

1167 def remove_actor_2d(self, actor): 

1168 if actor in self._actors_2d: 

1169 self.ren.RemoveActor2D(actor) 

1170 self._actors_2d.remove(actor) 

1171 

1172 def add_actor(self, actor): 

1173 if actor not in self._actors: 

1174 self.ren.AddActor(actor) 

1175 self._actors.add(actor) 

1176 

1177 def add_actor_list(self, actorlist): 

1178 for actor in actorlist: 

1179 self.add_actor(actor) 

1180 

1181 def remove_actor(self, actor): 

1182 if actor in self._actors: 

1183 self.ren.RemoveActor(actor) 

1184 self._actors.remove(actor) 

1185 

1186 def update_view(self): 

1187 self.vtk_widget.update() 

1188 

1189 def resize_event(self, size_x, size_y): 

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

1191 

1192 def button_event(self, obj, event): 

1193 if event == "LeftButtonPressEvent": 

1194 self.rotating = True 

1195 elif event == "LeftButtonReleaseEvent": 

1196 self.rotating = False 

1197 

1198 def mouse_move_event(self, obj, event): 

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

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

1201 

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

1203 center_x = size_x / 2.0 

1204 center_y = size_y / 2.0 

1205 

1206 if self.rotating: 

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

1208 

1209 def myWheelEvent(self, event): 

1210 

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

1212 

1213 if angle > 200: 

1214 angle = 200 

1215 

1216 if angle < -200: 

1217 angle = -200 

1218 

1219 self.disable_capture() 

1220 try: 

1221 self.do_dolly(-angle/100.) 

1222 finally: 

1223 self.enable_capture(aggregate='distance') 

1224 

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

1226 

1227 dx = x0 - x 

1228 dy = y0 - y 

1229 

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

1231 focp = self.gui_state.focal_point 

1232 

1233 if focp == 'center': 

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

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

1236 

1237 lat = self.state.lat 

1238 lon = self.state.lon 

1239 factor = self.state.distance / 10.0 

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

1241 else: 

1242 lat = 90. - self.state.dip 

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

1244 factor = 0.5 

1245 factor_lat = 1.0 

1246 

1247 dlat = dy * factor 

1248 dlon = dx * factor * factor_lat 

1249 

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

1251 lon += dlon 

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

1253 

1254 if focp == 'center': 

1255 self.state.lat = float(lat) 

1256 self.state.lon = float(lon) 

1257 else: 

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

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

1260 

1261 def do_dolly(self, v): 

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

1263 

1264 def key_down_event(self, obj, event): 

1265 k = obj.GetKeyCode() 

1266 if k == 'f': 

1267 self.gui_state.next_focal_point() 

1268 

1269 elif k == 'r': 

1270 self.reset_strike_dip() 

1271 

1272 elif k == 'p': 

1273 print(self.state) 

1274 

1275 elif k == 'i': 

1276 for elem in self.state.elements: 

1277 if isinstance(elem, elements.IcosphereState): 

1278 elem.visible = not elem.visible 

1279 

1280 elif k == 'c': 

1281 for elem in self.state.elements: 

1282 if isinstance(elem, elements.CoastlinesState): 

1283 elem.visible = not elem.visible 

1284 

1285 elif k == 't': 

1286 if not any( 

1287 isinstance(elem, elements.TopoState) 

1288 for elem in self.state.elements): 

1289 

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

1291 else: 

1292 for elem in self.state.elements: 

1293 if isinstance(elem, elements.TopoState): 

1294 elem.visible = not elem.visible 

1295 

1296 # elif k == ' ': 

1297 # self.toggle_panel_visibility() 

1298 

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

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

1301 

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

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

1304 

1305 def controls_navigation(self): 

1306 frame = qw.QFrame(self) 

1307 frame.setSizePolicy( 

1308 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1309 layout = qw.QGridLayout() 

1310 frame.setLayout(layout) 

1311 

1312 # lat, lon, depth 

1313 

1314 layout.addWidget( 

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

1316 

1317 le = qw.QLineEdit() 

1318 le.setStatusTip( 

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

1320 'Focal point location.') 

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

1322 

1323 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1326 

1327 def lineedit_to_lat_lon_depth(widget, state): 

1328 self.disable_capture() 

1329 try: 

1330 s = str(widget.text()) 

1331 choices = location_to_choices(s) 

1332 if len(choices) > 0: 

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

1334 choices[0].get_lat_lon_depth() 

1335 else: 

1336 raise NoLocationChoices(s) 

1337 

1338 finally: 

1339 self.enable_capture() 

1340 

1341 self._state_bind( 

1342 ['lat', 'lon', 'depth'], 

1343 lineedit_to_lat_lon_depth, 

1344 le, [le.editingFinished, le.returnPressed], 

1345 lat_lon_depth_to_lineedit) 

1346 

1347 self.lat_lon_lineedit = le 

1348 

1349 # focal point 

1350 

1351 cb = qw.QCheckBox('Fix') 

1352 cb.setStatusTip( 

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

1354 % g_modifier_key) 

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

1356 

1357 def focal_point_to_checkbox(state, widget): 

1358 widget.blockSignals(True) 

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

1360 widget.blockSignals(False) 

1361 

1362 def checkbox_to_focal_point(widget, state): 

1363 self.gui_state.focal_point = \ 

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

1365 

1366 self._gui_state_bind( 

1367 ['focal_point'], checkbox_to_focal_point, 

1368 cb, [cb.toggled], focal_point_to_checkbox) 

1369 

1370 self.focal_point_checkbox = cb 

1371 

1372 self.talkie_connect( 

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

1374 

1375 self.update_focal_point() 

1376 

1377 # strike, dip 

1378 

1379 layout.addWidget( 

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

1381 

1382 le = qw.QLineEdit() 

1383 le.setStatusTip( 

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

1385 'direction.') 

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

1387 

1388 def strike_dip_to_lineedit(state, widget): 

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

1390 

1391 def lineedit_to_strike_dip(widget, state): 

1392 s = str(widget.text()) 

1393 string_to_strike_dip = { 

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

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

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

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

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

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

1400 

1401 self.disable_capture() 

1402 if s in string_to_strike_dip: 

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

1404 

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

1406 try: 

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

1408 except Exception: 

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

1410 finally: 

1411 self.enable_capture() 

1412 

1413 self._state_bind( 

1414 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1416 

1417 self.strike_dip_lineedit = le 

1418 

1419 but = qw.QPushButton('Reset') 

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

1421 but.clicked.connect(self.reset_strike_dip) 

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

1423 

1424 # crosshair 

1425 

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

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

1428 

1429 # camera bindings 

1430 self.talkie_connect( 

1431 self.state, 

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

1433 self.update_camera) 

1434 

1435 self.talkie_connect( 

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

1437 

1438 return frame 

1439 

1440 def controls_time(self): 

1441 frame = qw.QFrame(self) 

1442 frame.setSizePolicy( 

1443 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1444 

1445 layout = qw.QGridLayout() 

1446 frame.setLayout(layout) 

1447 

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

1449 le_tmin = qw.QLineEdit() 

1450 layout.addWidget(le_tmin, 0, 1) 

1451 

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

1453 le_tmax = qw.QLineEdit() 

1454 layout.addWidget(le_tmax, 1, 1) 

1455 

1456 label_tcursor = qw.QLabel() 

1457 

1458 label_tcursor.setSizePolicy( 

1459 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1460 

1461 layout.addWidget(label_tcursor, 2, 1) 

1462 self._label_tcursor = label_tcursor 

1463 

1464 self._state_bind( 

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

1466 [le_tmin.editingFinished, le_tmin.returnPressed], 

1467 common.time_to_lineedit, 

1468 attribute='tmin') 

1469 self._state_bind( 

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

1471 [le_tmax.editingFinished, le_tmax.returnPressed], 

1472 common.time_to_lineedit, 

1473 attribute='tmax') 

1474 

1475 self.tmin_lineedit = le_tmin 

1476 self.tmax_lineedit = le_tmax 

1477 

1478 range_edit = RangeEdit() 

1479 range_edit.rangeEditPressed.connect(self.disable_capture) 

1480 range_edit.rangeEditReleased.connect(self.enable_capture) 

1481 range_edit.set_data_provider(self) 

1482 range_edit.set_data_name('time') 

1483 

1484 xblock = [False] 

1485 

1486 def range_to_range_edit(state, widget): 

1487 if not xblock[0]: 

1488 widget.blockSignals(True) 

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

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

1491 widget.blockSignals(False) 

1492 

1493 def range_edit_to_range(widget, state): 

1494 xblock[0] = True 

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

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

1497 xblock[0] = False 

1498 

1499 self._state_bind( 

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

1501 range_edit_to_range, 

1502 range_edit, 

1503 [range_edit.rangeChanged, range_edit.focusChanged], 

1504 range_to_range_edit) 

1505 

1506 def handle_tcursor_changed(): 

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

1508 

1509 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1510 

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

1512 

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

1514 le_focus = qw.QLineEdit() 

1515 layout.addWidget(le_focus, 4, 1) 

1516 

1517 def focus_to_lineedit(state, widget): 

1518 if state.tduration is None: 

1519 widget.setText('') 

1520 else: 

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

1522 guts.str_duration(state.tduration), 

1523 state.tposition)) 

1524 

1525 def lineedit_to_focus(widget, state): 

1526 s = str(widget.text()) 

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

1528 try: 

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

1530 state.tduration = None 

1531 state.tposition = 0.0 

1532 else: 

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

1534 if len(w) > 1: 

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

1536 else: 

1537 state.tposition = 0.0 

1538 

1539 except Exception: 

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

1541 

1542 self._state_bind( 

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

1544 [le_focus.editingFinished, le_focus.returnPressed], 

1545 focus_to_lineedit) 

1546 

1547 label_effective_tmin = qw.QLabel() 

1548 label_effective_tmax = qw.QLabel() 

1549 

1550 label_effective_tmin.setSizePolicy( 

1551 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1552 label_effective_tmax.setSizePolicy( 

1553 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1554 label_effective_tmin.setMinimumSize( 

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

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

1557 

1558 layout.addWidget(label_effective_tmin, 5, 1) 

1559 layout.addWidget(label_effective_tmax, 6, 1) 

1560 

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

1562 self.talkie_connect( 

1563 self.state, var, self.update_effective_time_labels) 

1564 

1565 self._label_effective_tmin = label_effective_tmin 

1566 self._label_effective_tmax = label_effective_tmax 

1567 

1568 self.talkie_connect( 

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

1570 

1571 return frame 

1572 

1573 def controls_appearance(self): 

1574 frame = qw.QFrame(self) 

1575 frame.setSizePolicy( 

1576 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1577 layout = qw.QGridLayout() 

1578 frame.setLayout(layout) 

1579 

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

1581 

1582 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1583 layout.addWidget(cb, 0, 1) 

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

1585 

1586 self.talkie_connect( 

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

1588 

1589 # background 

1590 

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

1592 

1593 cb = common.strings_to_combobox( 

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

1595 

1596 layout.addWidget(cb, 1, 1) 

1597 vstate.state_bind_combobox_background( 

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

1599 

1600 self.talkie_connect( 

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

1602 

1603 return frame 

1604 

1605 def controls_snapshots(self): 

1606 return snapshots_mod.SnapshotsPanel(self) 

1607 

1608 def update_effective_time_labels(self, *args): 

1609 tmin = self.state.tmin_effective 

1610 tmax = self.state.tmax_effective 

1611 

1612 stmin = common.time_or_none_to_str(tmin) 

1613 stmax = common.time_or_none_to_str(tmax) 

1614 

1615 self._label_effective_tmin.setText(stmin) 

1616 self._label_effective_tmax.setText(stmax) 

1617 

1618 def update_tcursor(self, *args): 

1619 tcursor = self.gui_state.tcursor 

1620 stcursor = common.time_or_none_to_str(tcursor) 

1621 self._label_tcursor.setText(stcursor) 

1622 

1623 def reset_strike_dip(self, *args): 

1624 self.state.strike = 90. 

1625 self.state.dip = 0 

1626 self.gui_state.focal_point = 'center' 

1627 

1628 def get_camera_geometry(self): 

1629 

1630 def rtp2xyz(rtp): 

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

1632 

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

1634 

1635 cam_rtp = num.array([ 

1636 radius+self.state.distance, 

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

1638 self.state.lon * d2r]) 

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

1640 cam, up, foc = \ 

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

1642 

1643 foc_rtp = num.array([ 

1644 radius, 

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

1646 self.state.lon * d2r]) 

1647 

1648 foc = rtp2xyz(foc_rtp) 

1649 

1650 rot_world = pmt.euler_to_matrix( 

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

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

1653 0.0*d2r).T 

1654 

1655 rot_cam = pmt.euler_to_matrix( 

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

1657 

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

1659 

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

1661 up = num.dot(rot, up) 

1662 return cam, up, foc 

1663 

1664 def update_camera(self, *args): 

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

1666 camera = self.ren.GetActiveCamera() 

1667 camera.SetPosition(*cam) 

1668 camera.SetFocalPoint(*foc) 

1669 camera.SetViewUp(*up) 

1670 

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

1672 

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

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

1675 

1676 # if horizon == 0.0: 

1677 # horizon = 2.0 + self.state.distance 

1678 

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

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

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

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

1683 # clip_dist = feature_horizon 

1684 

1685 camera.SetClippingRange( 

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

1687 

1688 self.camera_params = ( 

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

1690 

1691 self.update_view() 

1692 

1693 def add_panel( 

1694 self, title_label, panel, 

1695 visible=False, 

1696 # volatile=False, 

1697 tabify=True, 

1698 where=qc.Qt.RightDockWidgetArea, 

1699 remove=None, 

1700 title_controls=[], 

1701 scrollable=True): 

1702 

1703 dockwidget = common.MyDockWidget( 

1704 self, title_label, title_controls=title_controls) 

1705 

1706 if not visible: 

1707 dockwidget.hide() 

1708 

1709 if not self.gui_state.panels_visible: 

1710 dockwidget.block() 

1711 

1712 if scrollable: 

1713 scrollarea = common.MyScrollArea() 

1714 scrollarea.setWidget(panel) 

1715 scrollarea.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

1716 scrollarea.setSizeAdjustPolicy( 

1717 qw.QAbstractScrollArea.AdjustToContents) 

1718 scrollarea.setFrameShape(qw.QFrame.NoFrame) 

1719 

1720 dockwidget.setWidget(scrollarea) 

1721 else: 

1722 dockwidget.setWidget(panel) 

1723 

1724 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1726 

1727 self.addDockWidget(where, dockwidget) 

1728 

1729 nwrap = 4 

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

1731 self.tabifyDockWidget( 

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

1733 

1734 mitem = dockwidget.toggleViewAction() 

1735 

1736 def update_label(*args): 

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

1738 self.update_slug_abbreviated_lengths() 

1739 

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

1741 dockwidget.titlebar._title_label.title_changed.connect( 

1742 self.update_slug_abbreviated_lengths) 

1743 

1744 update_label() 

1745 

1746 self._panel_togglers[dockwidget] = mitem 

1747 self.panels_menu.addAction(mitem) 

1748 if visible: 

1749 dockwidget.setVisible(True) 

1750 dockwidget.setFocus() 

1751 dockwidget.raise_() 

1752 

1753 def stack_panels(self): 

1754 dockwidgets = self.findChildren(common.MyDockWidget) 

1755 by_area = defaultdict(list) 

1756 for dw in dockwidgets: 

1757 area = self.dockWidgetArea(dw) 

1758 by_area[area].append(dw) 

1759 

1760 for dockwidgets in by_area.values(): 

1761 dw_last = None 

1762 for dw in dockwidgets: 

1763 if dw_last is not None: 

1764 self.tabifyDockWidget(dw_last, dw) 

1765 

1766 dw_last = dw 

1767 

1768 def update_slug_abbreviated_lengths(self): 

1769 dockwidgets = self.findChildren(common.MyDockWidget) 

1770 title_labels = [] 

1771 for dw in dockwidgets: 

1772 title_labels.append(dw.titlebar._title_label) 

1773 

1774 by_title = defaultdict(list) 

1775 for tl in title_labels: 

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

1777 

1778 for group in by_title.values(): 

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

1780 

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

1782 nunique = len(set(slugs)) 

1783 

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

1785 n -= 1 

1786 

1787 if n > 0: 

1788 n = max(3, n) 

1789 

1790 for tl in group: 

1791 tl.set_slug_abbreviated_length(n) 

1792 

1793 def get_dockwidget(self, panel): 

1794 dockwidget = panel 

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

1796 dockwidget = dockwidget.parent() 

1797 

1798 return dockwidget 

1799 

1800 def raise_panel(self, panel): 

1801 dockwidget = self.get_dockwidget(panel) 

1802 dockwidget.setVisible(True) 

1803 dockwidget.setFocus() 

1804 dockwidget.raise_() 

1805 

1806 def toggle_panel_visibility(self): 

1807 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1808 

1809 def update_panel_visibility(self, *args): 

1810 self.setUpdatesEnabled(False) 

1811 mbar = self.menuBar() 

1812 sbar = self.statusBar() 

1813 dockwidgets = self.findChildren(common.MyDockWidget) 

1814 

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

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

1817 # objects. 

1818 mbar.setFixedHeight( 

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

1820 

1821 sbar.setVisible(self.gui_state.panels_visible) 

1822 for dockwidget in dockwidgets: 

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

1824 

1825 self.setUpdatesEnabled(True) 

1826 

1827 def remove_panel(self, panel): 

1828 dockwidget = self.get_dockwidget(panel) 

1829 self.removeDockWidget(dockwidget) 

1830 dockwidget.setParent(None) 

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

1832 

1833 def register_data_provider(self, provider): 

1834 if provider not in self.data_providers: 

1835 self.data_providers.append(provider) 

1836 

1837 def unregister_data_provider(self, provider): 

1838 if provider in self.data_providers: 

1839 self.data_providers.remove(provider) 

1840 

1841 def iter_data(self, name): 

1842 for provider in self.data_providers: 

1843 for data in provider.iter_data(name): 

1844 yield data 

1845 

1846 def confirm_close(self): 

1847 ret = qw.QMessageBox.question( 

1848 self, 

1849 'Sparrow', 

1850 'Close Sparrow window?', 

1851 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1852 qw.QMessageBox.Ok) 

1853 

1854 return ret == qw.QMessageBox.Ok 

1855 

1856 def closeEvent(self, event): 

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

1858 self.attach() 

1859 self.closing = True 

1860 event.accept() 

1861 else: 

1862 event.ignore() 

1863 

1864 def is_closing(self): 

1865 return self.closing 

1866 

1867 

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

1869 

1870 from pyrocko import util 

1871 from pyrocko.gui import util as gui_util 

1872 from . import common 

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

1874 

1875 global win 

1876 

1877 app = gui_util.get_app() 

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

1879 app.set_main_window(win) 

1880 

1881 gui_util.app.install_sigint_handler() 

1882 

1883 try: 

1884 gui_util.app.exec_() 

1885 finally: 

1886 gui_util.app.uninstall_sigint_handler() 

1887 app.unset_main_window() 

1888 common.set_viewer(None) 

1889 del win 

1890 gc.collect()