1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import math 

7import gc 

8import logging 

9import time 

10import tempfile 

11import os 

12import shutil 

13import platform 

14from collections import defaultdict 

15from subprocess import check_call 

16 

17import numpy as num 

18 

19from pyrocko import cake 

20from pyrocko import guts 

21from pyrocko.dataset import geonames 

22from pyrocko import config 

23from pyrocko import moment_tensor as pmt 

24from pyrocko import util 

25from pyrocko.dataset.util import set_download_callback 

26 

27from pyrocko.gui.util import Progressbars, RangeEdit 

28from pyrocko.gui.talkie import TalkieConnectionOwner, equal as state_equal 

29from pyrocko.gui.qt_compat import qw, qc, qg 

30# from pyrocko.gui import vtk_util 

31 

32from . import common, light, snapshots as snapshots_mod 

33 

34import vtk 

35import vtk.qt 

36vtk.qt.QVTKRWIBase = 'QGLWidget' # noqa 

37 

38from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

39 

40from pyrocko import geometry # noqa 

41from . import state as vstate, elements # noqa 

42 

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

44 

45 

46d2r = num.pi/180. 

47km = 1000. 

48 

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

50 g_modifier_key = '\u2318' 

51else: 

52 g_modifier_key = 'Ctrl' 

53 

54 

55class ZeroFrame(qw.QFrame): 

56 

57 def sizeHint(self): 

58 return qc.QSize(0, 0) 

59 

60 

61class LocationChoice(object): 

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

63 self._name = name 

64 self._lat = lat 

65 self._lon = lon 

66 self._depth = depth 

67 

68 def get_lat_lon_depth(self): 

69 return self._lat, self._lon, self._depth 

70 

71 

72def location_to_choices(s): 

73 choices = [] 

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

75 try: 

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

77 if len(vals) == 3: 

78 vals[2] *= km 

79 

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

81 

82 except ValueError: 

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

84 for c in cities: 

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

86 

87 return choices 

88 

89 

90class NoLocationChoices(Exception): 

91 

92 def __init__(self, s): 

93 self._string = s 

94 

95 def __str__(self): 

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

97 

98 

99class QVTKWidget(QVTKRenderWindowInteractor): 

100 def __init__(self, viewer, *args): 

101 QVTKRenderWindowInteractor.__init__(self, *args) 

102 self._viewer = viewer 

103 self._ctrl_state = False 

104 

105 def wheelEvent(self, event): 

106 return self._viewer.myWheelEvent(event) 

107 

108 def keyPressEvent(self, event): 

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

110 self._update_ctrl_state(True) 

111 QVTKRenderWindowInteractor.keyPressEvent(self, event) 

112 

113 def keyReleaseEvent(self, event): 

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

115 self._update_ctrl_state(False) 

116 QVTKRenderWindowInteractor.keyReleaseEvent(self, event) 

117 

118 def focusInEvent(self, event): 

119 self._update_ctrl_state() 

120 QVTKRenderWindowInteractor.focusInEvent(self, event) 

121 

122 def focusOutEvent(self, event): 

123 self._update_ctrl_state(False) 

124 QVTKRenderWindowInteractor.focusOutEvent(self, event) 

125 

126 def mousePressEvent(self, event): 

127 self._viewer.disable_capture() 

128 QVTKRenderWindowInteractor.mousePressEvent(self, event) 

129 

130 def mouseReleaseEvent(self, event): 

131 self._viewer.enable_capture() 

132 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event) 

133 

134 def _update_ctrl_state(self, state=None): 

135 if state is None: 

136 app = common.get_app() 

137 if not app: 

138 return 

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

140 if self._ctrl_state != state: 

141 self._viewer.gui_state.next_focal_point() 

142 self._ctrl_state = state 

143 

144 def container_resized(self, ev): 

145 self._viewer.update_vtk_widget_size() 

146 

147 

148class DetachedViewer(qw.QMainWindow): 

149 

150 def __init__(self, main_window, vtk_frame): 

151 qw.QMainWindow.__init__(self, main_window) 

152 self.main_window = main_window 

153 self.setWindowTitle('Sparrow View') 

154 vtk_frame.setParent(self) 

155 self.setCentralWidget(vtk_frame) 

156 

157 def closeEvent(self, ev): 

158 ev.ignore() 

159 self.main_window.attach() 

160 

161 

162class CenteringScrollArea(qw.QScrollArea): 

163 def __init__(self): 

164 qw.QScrollArea.__init__(self) 

165 self.setAlignment(qc.Qt.AlignCenter) 

166 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

167 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

168 self.setFrameShape(qw.QFrame.NoFrame) 

169 

170 def resizeEvent(self, ev): 

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

172 self.widget().container_resized(ev) 

173 return retval 

174 

175 def recenter(self): 

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

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

178 

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

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

181 

182 

183class YAMLEditor(qw.QTextEdit): 

184 

185 def __init__(self, parent): 

186 qw.QTextEdit.__init__(self) 

187 self._parent = parent 

188 

189 def event(self, ev): 

190 if isinstance(ev, qg.QKeyEvent) \ 

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

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

193 self._parent.state_changed() 

194 return True 

195 

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

197 

198 

199class StateEditor(qw.QFrame, TalkieConnectionOwner): 

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

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

202 TalkieConnectionOwner.__init__(self) 

203 

204 layout = qw.QGridLayout() 

205 

206 self.setLayout(layout) 

207 

208 self.source_editor = YAMLEditor(self) 

209 self.source_editor.setAcceptRichText(False) 

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

211 font = qg.QFont("Monospace") 

212 self.source_editor.setCurrentFont(font) 

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

214 

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

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

217 

218 self.error_display = qw.QTextEdit() 

219 self.error_display.setCurrentFont(font) 

220 self.error_display.setReadOnly(True) 

221 

222 self.error_display.setSizePolicy( 

223 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

224 

225 self.error_display_label.hide() 

226 self.error_display.hide() 

227 

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

229 

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

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

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

233 

234 button = qw.QPushButton('Apply') 

235 button.clicked.connect(self.state_changed) 

236 layout.addWidget(button, 3, 1) 

237 

238 self.viewer = viewer 

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

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

241 self.bind_state() 

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

243 self.destroyed.connect(self.unbind_state) 

244 self.bind_state() 

245 

246 def bind_state(self, *args): 

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

248 self.update_state() 

249 

250 def unbind_state(self): 

251 self.talkie_disconnect_all() 

252 

253 def update_state(self, *args): 

254 cursor = self.source_editor.textCursor() 

255 

256 cursor_position = cursor.position() 

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

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

259 

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

261 

262 cursor.setPosition(cursor_position) 

263 self.source_editor.setTextCursor(cursor) 

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

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

266 

267 def text_changed_handler(self, *args): 

268 if self.instant_updates.isChecked(): 

269 self.state_changed() 

270 

271 def state_changed(self): 

272 try: 

273 s = self.source_editor.toPlainText() 

274 state = guts.load(string=s) 

275 self.viewer.set_state(state) 

276 self.error_display.setPlainText('') 

277 self.error_display_label.hide() 

278 self.error_display.hide() 

279 

280 except Exception as e: 

281 self.error_display.show() 

282 self.error_display_label.show() 

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

284 

285 

286class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

287 

288 download_progress_update = qc.pyqtSignal() 

289 

290 def __init__( 

291 self, 

292 use_depth_peeling=True, 

293 events=None, 

294 snapshots=None, 

295 instant_close=False): 

296 

297 common.set_viewer(self) 

298 

299 qw.QMainWindow.__init__(self) 

300 TalkieConnectionOwner.__init__(self) 

301 

302 self.instant_close = instant_close 

303 

304 self.state = vstate.ViewerState() 

305 self.gui_state = vstate.ViewerGuiState() 

306 

307 self.setWindowTitle('Sparrow') 

308 

309 self.setTabPosition( 

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

311 

312 self.planet_radius = cake.earthradius 

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

314 

315 self._block_capture = 0 

316 self._undo_stack = [] 

317 self._redo_stack = [] 

318 self._undo_aggregate = None 

319 

320 self._panel_togglers = {} 

321 self._actors = set() 

322 self._actors_2d = set() 

323 self._render_window_size = (0, 0) 

324 self._use_depth_peeling = use_depth_peeling 

325 self._in_update_elements = False 

326 self._update_elements_enabled = True 

327 

328 self._animation_tstart = None 

329 self._animation_iframe = None 

330 self._animation = None 

331 

332 mbar = qw.QMenuBar() 

333 self.setMenuBar(mbar) 

334 

335 menu = mbar.addMenu('File') 

336 

337 menu.addAction( 

338 'Export Image...', 

339 self.export_image, 

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

341 qc.Qt.ApplicationShortcut) 

342 

343 menu.addAction( 

344 'Quit', 

345 self.close, 

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

347 qc.Qt.ApplicationShortcut) 

348 

349 menu = mbar.addMenu('Edit') 

350 

351 menu.addAction( 

352 'Undo', 

353 self.undo, 

354 qg.QKeySequence( 

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

356 qc.Qt.ApplicationShortcut) 

357 

358 menu.addAction( 

359 'Redo', 

360 self.redo, 

361 qg.QKeySequence( 

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

363 qc.Qt.ApplicationShortcut) 

364 

365 menu = mbar.addMenu('View') 

366 menu_sizes = menu.addMenu('Size') 

367 self._add_vtk_widget_size_menu_entries(menu_sizes) 

368 

369 # detached/attached 

370 self.talkie_connect( 

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

372 

373 action = qw.QAction('Detach') 

374 action.setCheckable(True) 

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

376 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

377 

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

379 menu.addAction(action) 

380 

381 # hide controls 

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

383 action.setCheckable(True) 

384 action.setShortcut(qc.Qt.Key_Space) 

385 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

386 action.triggered.connect(self.toggle_panel_visibility) 

387 menu.addAction(action) 

388 

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

390 self.panels_menu.addAction( 

391 'Stack Panels', 

392 self.stack_panels) 

393 self.panels_menu.addSeparator() 

394 

395 snapshots_menu = mbar.addMenu('Snapshots') 

396 

397 menu = mbar.addMenu('Elements') 

398 for name, estate in sorted([ 

399 ('Icosphere', elements.IcosphereState( 

400 level=4, 

401 smooth=True, 

402 opacity=0.5, 

403 ambient=0.1)), 

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

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

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

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

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

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

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

411 ('HUD Subtitle', elements.HudState( 

412 template='Subtitle')), 

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

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

415 position='top-left')), 

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

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

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

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

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

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

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

423 

424 def wrap_add_element(estate): 

425 def add_element(*args): 

426 new_element = guts.clone(estate) 

427 new_element.element_id = elements.random_id() 

428 self.state.elements.append(new_element) 

429 self.state.sort_elements() 

430 

431 return add_element 

432 

433 mitem = qw.QAction(name, self) 

434 

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

436 

437 menu.addAction(mitem) 

438 

439 menu = mbar.addMenu('Help') 

440 

441 menu.addAction( 

442 'Interactive Tour', 

443 self.start_tour) 

444 

445 menu.addAction( 

446 'Online Manual', 

447 self.open_manual) 

448 

449 self.data_providers = [] 

450 self.elements = {} 

451 

452 self.detached_window = None 

453 

454 self.main_frame = qw.QFrame() 

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

456 

457 self.vtk_frame = CenteringScrollArea() 

458 

459 self.vtk_widget = QVTKWidget(self, self) 

460 self.vtk_frame.setWidget(self.vtk_widget) 

461 

462 self.main_layout = qw.QVBoxLayout() 

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

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

465 

466 pb = Progressbars(self) 

467 self.progressbars = pb 

468 self.main_layout.addWidget(pb) 

469 

470 self.main_frame.setLayout(self.main_layout) 

471 

472 self.vtk_frame_substitute = None 

473 

474 self.add_panel( 

475 'Navigation', 

476 self.controls_navigation(), visible=True, 

477 where=qc.Qt.LeftDockWidgetArea) 

478 

479 self.add_panel( 

480 'Time', 

481 self.controls_time(), visible=True, 

482 where=qc.Qt.LeftDockWidgetArea) 

483 

484 self.add_panel( 

485 'Appearance', 

486 self.controls_appearance(), visible=True, 

487 where=qc.Qt.LeftDockWidgetArea) 

488 

489 snapshots_panel = self.controls_snapshots() 

490 self.snapshots_panel = snapshots_panel 

491 self.add_panel( 

492 'Snapshots', 

493 snapshots_panel, visible=False, 

494 where=qc.Qt.LeftDockWidgetArea) 

495 

496 snapshots_panel.setup_menu(snapshots_menu) 

497 

498 self.setCentralWidget(self.main_frame) 

499 

500 self.mesh = None 

501 

502 ren = vtk.vtkRenderer() 

503 

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

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

506 # ren.TwoSidedLightingOn() 

507 # ren.SetUseShadows(1) 

508 

509 self._lighting = None 

510 self._background = None 

511 

512 self.ren = ren 

513 self.update_render_settings() 

514 self.update_camera() 

515 

516 renwin = self.vtk_widget.GetRenderWindow() 

517 

518 if self._use_depth_peeling: 

519 renwin.SetAlphaBitPlanes(1) 

520 renwin.SetMultiSamples(0) 

521 

522 ren.SetUseDepthPeeling(1) 

523 ren.SetMaximumNumberOfPeels(100) 

524 ren.SetOcclusionRatio(0.1) 

525 

526 ren.SetUseFXAA(1) 

527 # ren.SetUseHiddenLineRemoval(1) 

528 # ren.SetBackingStore(1) 

529 

530 self.renwin = renwin 

531 

532 # renwin.LineSmoothingOn() 

533 # renwin.PointSmoothingOn() 

534 # renwin.PolygonSmoothingOn() 

535 

536 renwin.AddRenderer(ren) 

537 

538 iren = renwin.GetInteractor() 

539 iren.LightFollowCameraOn() 

540 iren.SetInteractorStyle(None) 

541 

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

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

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

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

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

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

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

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

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

551 

552 renwin.Render() 

553 

554 iren.Initialize() 

555 

556 self.iren = iren 

557 

558 self.rotating = False 

559 

560 self._elements = {} 

561 self._elements_active = {} 

562 

563 self.talkie_connect( 

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

565 

566 self.state.elements.append(elements.IcosphereState( 

567 element_id='icosphere', 

568 level=4, 

569 smooth=True, 

570 opacity=0.5, 

571 ambient=0.1)) 

572 

573 self.state.elements.append(elements.GridState( 

574 element_id='grid')) 

575 self.state.elements.append(elements.CoastlinesState( 

576 element_id='coastlines')) 

577 self.state.elements.append(elements.CrosshairState( 

578 element_id='crosshair')) 

579 

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

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

582 # self.state.elements.append( 

583 # elements.CatalogState( 

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

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

586 

587 if events: 

588 self.state.elements.append( 

589 elements.CatalogState( 

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

591 

592 self.state.sort_elements() 

593 

594 if snapshots: 

595 snapshots_ = [] 

596 for obj in snapshots: 

597 if isinstance(obj, str): 

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

599 else: 

600 snapshots_.append(obj) 

601 

602 snapshots_panel.add_snapshots(snapshots_) 

603 self.raise_panel(snapshots_panel) 

604 snapshots_panel.goto_snapshot(1) 

605 

606 self.timer = qc.QTimer(self) 

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

608 self.timer.setInterval(1000) 

609 self.timer.start() 

610 

611 self._animation_saver = None 

612 

613 self.closing = False 

614 self.vtk_widget.setFocus() 

615 

616 self.update_detached() 

617 

618 self.status( 

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

620 

621 self.status( 

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

623 

624 self.show() 

625 self.windowHandle().showMaximized() 

626 

627 self.talkie_connect( 

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

629 

630 self.update_vtk_widget_size() 

631 

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

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

634 

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

636 self.capture_state() 

637 

638 set_download_callback(self.update_download_progress) 

639 

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

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

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

643 

644 self.start_tour() 

645 

646 def update_download_progress(self, message, args): 

647 self.download_progress_update.emit() 

648 

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

650 self.statusBar().showMessage( 

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

652 

653 def disable_capture(self): 

654 self._block_capture += 1 

655 

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

657 

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

659 if self._block_capture > 0: 

660 self._block_capture -= 1 

661 

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

663 

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

665 self.capture_state(aggregate=aggregate) 

666 

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

668 if self._block_capture: 

669 return 

670 

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

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

673 

674 if aggregate is not None: 

675 if aggregate == self._undo_aggregate: 

676 self._undo_stack.pop() 

677 

678 self._undo_aggregate = aggregate 

679 else: 

680 self._undo_aggregate = None 

681 

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

683 len(self._undo_stack) + 1, 

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

685 '\n'.join( 

686 ' - %s' % s 

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

688 self.state).splitlines()) 

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

690 

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

692 self._redo_stack.clear() 

693 

694 def undo(self): 

695 self._undo_aggregate = None 

696 

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

698 return 

699 

700 state = self._undo_stack.pop() 

701 self._redo_stack.append(state) 

702 state = self._undo_stack[-1] 

703 

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

705 len(self._undo_stack), 

706 '\n'.join( 

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

708 

709 self.disable_capture() 

710 try: 

711 self.set_state(state) 

712 finally: 

713 self.enable_capture(drop=True) 

714 

715 def redo(self): 

716 self._undo_aggregate = None 

717 

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

719 return 

720 

721 state = self._redo_stack.pop() 

722 self._undo_stack.append(state) 

723 

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

725 len(self._redo_stack), 

726 '\n'.join( 

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

728 

729 self.disable_capture() 

730 try: 

731 self.set_state(state) 

732 finally: 

733 self.enable_capture(drop=True) 

734 

735 def start_tour(self): 

736 snapshots_ = snapshots_mod.load_snapshots( 

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

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

739 self.snapshots_panel.add_snapshots(snapshots_) 

740 self.raise_panel(self.snapshots_panel) 

741 self.snapshots_panel.transition_to_next_snapshot() 

742 

743 def open_manual(self): 

744 import webbrowser 

745 webbrowser.open( 

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

747 

748 def _add_vtk_widget_size_menu_entries(self, menu): 

749 

750 group = qw.QActionGroup(menu) 

751 group.setExclusive(True) 

752 

753 def set_variable_size(): 

754 self.gui_state.fixed_size = False 

755 

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

757 variable_size_action.setCheckable(True) 

758 variable_size_action.setActionGroup(group) 

759 variable_size_action.triggered.connect(set_variable_size) 

760 

761 fixed_size_items = [] 

762 for nx, ny, label in [ 

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

764 (426, 240, ''), 

765 (640, 360, ''), 

766 (854, 480, '(FWVGA)'), 

767 (1280, 720, '(HD)'), 

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

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

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

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

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

773 (640, 480, '(VGA)'), 

774 (800, 600, '(SVGA)'), 

775 (None, None, 'Other'), 

776 (512, 512, ''), 

777 (1024, 1024, '')]: 

778 

779 if None in (nx, ny): 

780 menu.addSection(label) 

781 else: 

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

783 action = menu.addAction(name) 

784 action.setCheckable(True) 

785 action.setActionGroup(group) 

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

787 

788 def make_set_fixed_size(nx, ny): 

789 def set_fixed_size(): 

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

791 

792 return set_fixed_size 

793 

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

795 

796 def update_widget(*args): 

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

798 action.blockSignals(True) 

799 action.setChecked( 

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

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

802 action.blockSignals(False) 

803 

804 variable_size_action.blockSignals(True) 

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

806 variable_size_action.blockSignals(False) 

807 

808 update_widget() 

809 self.talkie_connect( 

810 self.gui_state, 'fixed_size', update_widget) 

811 

812 def update_vtk_widget_size(self, *args): 

813 if self.gui_state.fixed_size: 

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

815 wanted_size = qc.QSize(nx, ny) 

816 else: 

817 wanted_size = qc.QSize( 

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

819 

820 current_size = self.vtk_widget.size() 

821 

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

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

824 

825 self.vtk_widget.setFixedSize(wanted_size) 

826 

827 self.vtk_frame.recenter() 

828 self.check_vtk_resize() 

829 

830 def update_focal_point(self, *args): 

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

832 self.vtk_widget.setStatusTip( 

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

834 'change view plane orientation.' % g_modifier_key) 

835 else: 

836 self.vtk_widget.setStatusTip( 

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

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

839 'reverse sense.' % g_modifier_key) 

840 

841 def update_detached(self, *args): 

842 

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

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

845 

846 self.main_layout.removeWidget(self.vtk_frame) 

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

848 self.detached_window.show() 

849 self.vtk_widget.setFocus() 

850 

851 screens = common.get_app().screens() 

852 if len(screens) > 1: 

853 for screen in screens: 

854 if screen is not self.screen(): 

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

856 # .setScreen() does not work reliably, 

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

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

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

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

861 

862 self.detached_window.windowHandle().showMaximized() 

863 

864 frame = qw.QFrame() 

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

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

867 # frame.setAutoFillBackground(True) 

868 frame.setSizePolicy( 

869 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

870 

871 layout = qw.QGridLayout() 

872 frame.setLayout(layout) 

873 self.main_layout.insertWidget(0, frame) 

874 

875 self.state_editor = StateEditor(self) 

876 

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

878 

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

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

881 # layout.addWidget( 

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

883 

884 self.vtk_frame_substitute = frame 

885 

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

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

888 self.detached_window.hide() 

889 self.vtk_frame.setParent(self) 

890 if self.vtk_frame_substitute: 

891 self.main_layout.removeWidget(self.vtk_frame_substitute) 

892 self.state_editor.unbind_state() 

893 self.vtk_frame_substitute = None 

894 

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

896 self.detached_window = None 

897 self.vtk_widget.setFocus() 

898 

899 def attach(self): 

900 self.gui_state.detached = False 

901 

902 def export_image(self): 

903 

904 caption = 'Export Image' 

905 fn_out, _ = qw.QFileDialog.getSaveFileName( 

906 self, caption, 'image.png', 

907 options=common.qfiledialog_options) 

908 

909 if fn_out: 

910 self.save_image(fn_out) 

911 

912 def save_image(self, path): 

913 

914 original_fixed_size = self.gui_state.fixed_size 

915 if original_fixed_size is None: 

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

917 

918 wif = vtk.vtkWindowToImageFilter() 

919 wif.SetInput(self.renwin) 

920 wif.SetInputBufferTypeToRGBA() 

921 wif.SetScale(1, 1) 

922 wif.ReadFrontBufferOff() 

923 writer = vtk.vtkPNGWriter() 

924 writer.SetInputConnection(wif.GetOutputPort()) 

925 

926 self.renwin.Render() 

927 wif.Modified() 

928 writer.SetFileName(path) 

929 writer.Write() 

930 

931 self.gui_state.fixed_size = original_fixed_size 

932 

933 def update_render_settings(self, *args): 

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

935 self.ren.RemoveAllLights() 

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

937 self.ren.AddLight(li) 

938 

939 self._lighting = self.state.lighting 

940 

941 if self._background is None \ 

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

943 

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

945 self._background = self.state.background 

946 

947 self.update_view() 

948 

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

950 if self._animation: 

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

952 self.stop_animation() 

953 

954 self.disable_capture() 

955 self._animation = interpolator 

956 if output_path is None: 

957 self._animation_tstart = time.time() 

958 self._animation_iframe = None 

959 else: 

960 self._animation_iframe = 0 

961 self.showFullScreen() 

962 self.update_view() 

963 self.gui_state.panels_visible = False 

964 self.update_view() 

965 

966 self._animation_timer = qc.QTimer(self) 

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

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

969 self._animation_timer.start() 

970 if output_path is not None: 

971 original_fixed_size = self.gui_state.fixed_size 

972 if original_fixed_size is None: 

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

974 

975 wif = vtk.vtkWindowToImageFilter() 

976 wif.SetInput(self.renwin) 

977 wif.SetInputBufferTypeToRGBA() 

978 wif.SetScale(1, 1) 

979 wif.ReadFrontBufferOff() 

980 writer = vtk.vtkPNGWriter() 

981 temp_path = tempfile.mkdtemp() 

982 self._animation_saver = ( 

983 wif, writer, temp_path, output_path, original_fixed_size) 

984 writer.SetInputConnection(wif.GetOutputPort()) 

985 

986 def next_animation_frame(self): 

987 

988 ani = self._animation 

989 if not ani: 

990 return 

991 

992 if self._animation_iframe is not None: 

993 state = ani( 

994 ani.tmin 

995 + self._animation_iframe * ani.dt) 

996 

997 self._animation_iframe += 1 

998 else: 

999 tnow = time.time() 

1000 state = ani(min( 

1001 ani.tmax, 

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

1003 

1004 self.set_state(state) 

1005 self.renwin.Render() 

1006 if self._animation_saver: 

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

1008 wif.Modified() 

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

1010 writer.SetFileName(fn % self._animation_iframe) 

1011 writer.Write() 

1012 

1013 if self._animation_iframe is not None: 

1014 t = self._animation_iframe * ani.dt 

1015 else: 

1016 t = tnow - self._animation_tstart 

1017 

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

1019 self.stop_animation() 

1020 

1021 def stop_animation(self): 

1022 if self._animation_timer: 

1023 self._animation_timer.stop() 

1024 

1025 if self._animation_saver: 

1026 

1027 wif, writer, temp_path, output_path, original_fixed_size \ 

1028 = self._animation_saver 

1029 self.gui_state.fixed_size = original_fixed_size 

1030 

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

1032 check_call([ 

1033 'ffmpeg', '-y', 

1034 '-i', fn_path, 

1035 '-c:v', 'libx264', 

1036 '-preset', 'slow', 

1037 '-crf', '17', 

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

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

1040 output_path]) 

1041 shutil.rmtree(temp_path) 

1042 

1043 self._animation_saver = None 

1044 self._animation_saver 

1045 

1046 self.showNormal() 

1047 self.gui_state.panels_visible = True 

1048 

1049 self._animation_tstart = None 

1050 self._animation_iframe = None 

1051 self._animation = None 

1052 self.enable_capture() 

1053 

1054 def set_state(self, state): 

1055 self.disable_capture() 

1056 try: 

1057 self._update_elements_enabled = False 

1058 self.setUpdatesEnabled(False) 

1059 self.state.diff_update(state) 

1060 self.state.sort_elements() 

1061 self.setUpdatesEnabled(True) 

1062 self._update_elements_enabled = True 

1063 self.update_elements() 

1064 finally: 

1065 self.enable_capture() 

1066 

1067 def periodical(self): 

1068 pass 

1069 

1070 def check_vtk_resize(self, *args): 

1071 render_window_size = self.renwin.GetSize() 

1072 if self._render_window_size != render_window_size: 

1073 self._render_window_size = render_window_size 

1074 self.resize_event(*render_window_size) 

1075 

1076 def update_elements(self, *_): 

1077 if not self._update_elements_enabled: 

1078 return 

1079 

1080 if self._in_update_elements: 

1081 return 

1082 

1083 self._in_update_elements = True 

1084 for estate in self.state.elements: 

1085 if estate.element_id not in self._elements: 

1086 new_element = estate.create() 

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

1088 type(new_element).__name__, 

1089 estate.element_id)) 

1090 self._elements[estate.element_id] = new_element 

1091 

1092 element = self._elements[estate.element_id] 

1093 

1094 if estate.element_id not in self._elements_active: 

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

1096 type(element).__name__, 

1097 estate.element_id)) 

1098 element.bind_state(estate) 

1099 element.set_parent(self) 

1100 self._elements_active[estate.element_id] = element 

1101 

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

1103 deactivate = [] 

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

1105 if element_id not in state_element_ids: 

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

1107 type(element).__name__, 

1108 element_id)) 

1109 element.unset_parent() 

1110 deactivate.append(element_id) 

1111 

1112 for element_id in deactivate: 

1113 del self._elements_active[element_id] 

1114 

1115 self._update_crosshair_bindings() 

1116 

1117 self._in_update_elements = False 

1118 

1119 def _update_crosshair_bindings(self): 

1120 

1121 def get_crosshair_element(): 

1122 for element in self.state.elements: 

1123 if element.element_id == 'crosshair': 

1124 return element 

1125 

1126 return None 

1127 

1128 crosshair = get_crosshair_element() 

1129 if crosshair is None or crosshair.is_connected: 

1130 return 

1131 

1132 def to_checkbox(state, widget): 

1133 widget.blockSignals(True) 

1134 widget.setChecked(state.visible) 

1135 widget.blockSignals(False) 

1136 

1137 def to_state(widget, state): 

1138 state.visible = widget.isChecked() 

1139 

1140 cb = self._crosshair_checkbox 

1141 vstate.state_bind( 

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

1143 cb, [cb.toggled], to_checkbox) 

1144 

1145 crosshair.is_connected = True 

1146 

1147 def add_actor_2d(self, actor): 

1148 if actor not in self._actors_2d: 

1149 self.ren.AddActor2D(actor) 

1150 self._actors_2d.add(actor) 

1151 

1152 def remove_actor_2d(self, actor): 

1153 if actor in self._actors_2d: 

1154 self.ren.RemoveActor2D(actor) 

1155 self._actors_2d.remove(actor) 

1156 

1157 def add_actor(self, actor): 

1158 if actor not in self._actors: 

1159 self.ren.AddActor(actor) 

1160 self._actors.add(actor) 

1161 

1162 def add_actor_list(self, actorlist): 

1163 for actor in actorlist: 

1164 self.add_actor(actor) 

1165 

1166 def remove_actor(self, actor): 

1167 if actor in self._actors: 

1168 self.ren.RemoveActor(actor) 

1169 self._actors.remove(actor) 

1170 

1171 def update_view(self): 

1172 self.vtk_widget.update() 

1173 

1174 def resize_event(self, size_x, size_y): 

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

1176 

1177 def button_event(self, obj, event): 

1178 if event == "LeftButtonPressEvent": 

1179 self.rotating = True 

1180 elif event == "LeftButtonReleaseEvent": 

1181 self.rotating = False 

1182 

1183 def mouse_move_event(self, obj, event): 

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

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

1186 

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

1188 center_x = size_x / 2.0 

1189 center_y = size_y / 2.0 

1190 

1191 if self.rotating: 

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

1193 

1194 def myWheelEvent(self, event): 

1195 

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

1197 

1198 if angle > 200: 

1199 angle = 200 

1200 

1201 if angle < -200: 

1202 angle = -200 

1203 

1204 self.disable_capture() 

1205 try: 

1206 self.do_dolly(-angle/100.) 

1207 finally: 

1208 self.enable_capture(aggregate='distance') 

1209 

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

1211 

1212 dx = x0 - x 

1213 dy = y0 - y 

1214 

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

1216 focp = self.gui_state.focal_point 

1217 

1218 if focp == 'center': 

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

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

1221 

1222 lat = self.state.lat 

1223 lon = self.state.lon 

1224 factor = self.state.distance / 10.0 

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

1226 else: 

1227 lat = 90. - self.state.dip 

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

1229 factor = 0.5 

1230 factor_lat = 1.0 

1231 

1232 dlat = dy * factor 

1233 dlon = dx * factor * factor_lat 

1234 

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

1236 lon += dlon 

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

1238 

1239 if focp == 'center': 

1240 self.state.lat = float(lat) 

1241 self.state.lon = float(lon) 

1242 else: 

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

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

1245 

1246 def do_dolly(self, v): 

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

1248 

1249 def key_down_event(self, obj, event): 

1250 k = obj.GetKeyCode() 

1251 if k == 'f': 

1252 self.gui_state.next_focal_point() 

1253 

1254 elif k == 'r': 

1255 self.reset_strike_dip() 

1256 

1257 elif k == 'p': 

1258 print(self.state) 

1259 

1260 elif k == 'i': 

1261 for elem in self.state.elements: 

1262 if isinstance(elem, elements.IcosphereState): 

1263 elem.visible = not elem.visible 

1264 

1265 elif k == 'c': 

1266 for elem in self.state.elements: 

1267 if isinstance(elem, elements.CoastlinesState): 

1268 elem.visible = not elem.visible 

1269 

1270 elif k == 't': 

1271 if not any( 

1272 isinstance(elem, elements.TopoState) 

1273 for elem in self.state.elements): 

1274 

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

1276 else: 

1277 for elem in self.state.elements: 

1278 if isinstance(elem, elements.TopoState): 

1279 elem.visible = not elem.visible 

1280 

1281 # elif k == ' ': 

1282 # self.toggle_panel_visibility() 

1283 

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

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

1286 

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

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

1289 

1290 def controls_navigation(self): 

1291 frame = qw.QFrame(self) 

1292 frame.setSizePolicy( 

1293 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1294 layout = qw.QGridLayout() 

1295 frame.setLayout(layout) 

1296 

1297 # lat, lon, depth 

1298 

1299 layout.addWidget( 

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

1301 

1302 le = qw.QLineEdit() 

1303 le.setStatusTip( 

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

1305 'Focal point location.') 

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

1307 

1308 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1311 

1312 def lineedit_to_lat_lon_depth(widget, state): 

1313 self.disable_capture() 

1314 try: 

1315 s = str(widget.text()) 

1316 choices = location_to_choices(s) 

1317 if len(choices) > 0: 

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

1319 choices[0].get_lat_lon_depth() 

1320 else: 

1321 raise NoLocationChoices(s) 

1322 

1323 finally: 

1324 self.enable_capture() 

1325 

1326 self._state_bind( 

1327 ['lat', 'lon', 'depth'], 

1328 lineedit_to_lat_lon_depth, 

1329 le, [le.editingFinished, le.returnPressed], 

1330 lat_lon_depth_to_lineedit) 

1331 

1332 self.lat_lon_lineedit = le 

1333 

1334 # focal point 

1335 

1336 cb = qw.QCheckBox('Fix') 

1337 cb.setStatusTip( 

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

1339 % g_modifier_key) 

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

1341 

1342 def focal_point_to_checkbox(state, widget): 

1343 widget.blockSignals(True) 

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

1345 widget.blockSignals(False) 

1346 

1347 def checkbox_to_focal_point(widget, state): 

1348 self.gui_state.focal_point = \ 

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

1350 

1351 self._gui_state_bind( 

1352 ['focal_point'], checkbox_to_focal_point, 

1353 cb, [cb.toggled], focal_point_to_checkbox) 

1354 

1355 self.focal_point_checkbox = cb 

1356 

1357 self.talkie_connect( 

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

1359 

1360 self.update_focal_point() 

1361 

1362 # strike, dip 

1363 

1364 layout.addWidget( 

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

1366 

1367 le = qw.QLineEdit() 

1368 le.setStatusTip( 

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

1370 'direction.') 

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

1372 

1373 def strike_dip_to_lineedit(state, widget): 

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

1375 

1376 def lineedit_to_strike_dip(widget, state): 

1377 s = str(widget.text()) 

1378 string_to_strike_dip = { 

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

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

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

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

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

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

1385 

1386 self.disable_capture() 

1387 if s in string_to_strike_dip: 

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

1389 

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

1391 try: 

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

1393 except Exception: 

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

1395 finally: 

1396 self.enable_capture() 

1397 

1398 self._state_bind( 

1399 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1401 

1402 self.strike_dip_lineedit = le 

1403 

1404 but = qw.QPushButton('Reset') 

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

1406 but.clicked.connect(self.reset_strike_dip) 

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

1408 

1409 # crosshair 

1410 

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

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

1413 

1414 # camera bindings 

1415 self.talkie_connect( 

1416 self.state, 

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

1418 self.update_camera) 

1419 

1420 self.talkie_connect( 

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

1422 

1423 return frame 

1424 

1425 def controls_time(self): 

1426 frame = qw.QFrame(self) 

1427 frame.setSizePolicy( 

1428 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1429 

1430 layout = qw.QGridLayout() 

1431 frame.setLayout(layout) 

1432 

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

1434 le_tmin = qw.QLineEdit() 

1435 layout.addWidget(le_tmin, 0, 1) 

1436 

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

1438 le_tmax = qw.QLineEdit() 

1439 layout.addWidget(le_tmax, 1, 1) 

1440 

1441 label_tcursor = qw.QLabel() 

1442 

1443 label_tcursor.setSizePolicy( 

1444 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1445 

1446 layout.addWidget(label_tcursor, 2, 1) 

1447 self._label_tcursor = label_tcursor 

1448 

1449 self._state_bind( 

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

1451 [le_tmin.editingFinished, le_tmin.returnPressed], 

1452 common.time_to_lineedit, 

1453 attribute='tmin') 

1454 self._state_bind( 

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

1456 [le_tmax.editingFinished, le_tmax.returnPressed], 

1457 common.time_to_lineedit, 

1458 attribute='tmax') 

1459 

1460 self.tmin_lineedit = le_tmin 

1461 self.tmax_lineedit = le_tmax 

1462 

1463 range_edit = RangeEdit() 

1464 range_edit.rangeEditPressed.connect(self.disable_capture) 

1465 range_edit.rangeEditReleased.connect(self.enable_capture) 

1466 range_edit.set_data_provider(self) 

1467 range_edit.set_data_name('time') 

1468 

1469 xblock = [False] 

1470 

1471 def range_to_range_edit(state, widget): 

1472 if not xblock[0]: 

1473 widget.blockSignals(True) 

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

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

1476 widget.blockSignals(False) 

1477 

1478 def range_edit_to_range(widget, state): 

1479 xblock[0] = True 

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

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

1482 xblock[0] = False 

1483 

1484 self._state_bind( 

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

1486 range_edit_to_range, 

1487 range_edit, 

1488 [range_edit.rangeChanged, range_edit.focusChanged], 

1489 range_to_range_edit) 

1490 

1491 def handle_tcursor_changed(): 

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

1493 

1494 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1495 

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

1497 

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

1499 le_focus = qw.QLineEdit() 

1500 layout.addWidget(le_focus, 4, 1) 

1501 

1502 def focus_to_lineedit(state, widget): 

1503 if state.tduration is None: 

1504 widget.setText('') 

1505 else: 

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

1507 guts.str_duration(state.tduration), 

1508 state.tposition)) 

1509 

1510 def lineedit_to_focus(widget, state): 

1511 s = str(widget.text()) 

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

1513 try: 

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

1515 state.tduration = None 

1516 state.tposition = 0.0 

1517 else: 

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

1519 if len(w) > 1: 

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

1521 else: 

1522 state.tposition = 0.0 

1523 

1524 except Exception: 

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

1526 

1527 self._state_bind( 

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

1529 [le_focus.editingFinished, le_focus.returnPressed], 

1530 focus_to_lineedit) 

1531 

1532 label_effective_tmin = qw.QLabel() 

1533 label_effective_tmax = qw.QLabel() 

1534 

1535 label_effective_tmin.setSizePolicy( 

1536 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1537 label_effective_tmax.setSizePolicy( 

1538 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1539 label_effective_tmin.setMinimumSize( 

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

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

1542 

1543 layout.addWidget(label_effective_tmin, 5, 1) 

1544 layout.addWidget(label_effective_tmax, 6, 1) 

1545 

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

1547 self.talkie_connect( 

1548 self.state, var, self.update_effective_time_labels) 

1549 

1550 self._label_effective_tmin = label_effective_tmin 

1551 self._label_effective_tmax = label_effective_tmax 

1552 

1553 self.talkie_connect( 

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

1555 

1556 return frame 

1557 

1558 def controls_appearance(self): 

1559 frame = qw.QFrame(self) 

1560 frame.setSizePolicy( 

1561 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1562 layout = qw.QGridLayout() 

1563 frame.setLayout(layout) 

1564 

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

1566 

1567 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1568 layout.addWidget(cb, 0, 1) 

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

1570 

1571 self.talkie_connect( 

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

1573 

1574 # background 

1575 

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

1577 

1578 cb = common.strings_to_combobox( 

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

1580 

1581 layout.addWidget(cb, 1, 1) 

1582 vstate.state_bind_combobox_background( 

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

1584 

1585 self.talkie_connect( 

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

1587 

1588 return frame 

1589 

1590 def controls_snapshots(self): 

1591 return snapshots_mod.SnapshotsPanel(self) 

1592 

1593 def update_effective_time_labels(self, *args): 

1594 tmin = self.state.tmin_effective 

1595 tmax = self.state.tmax_effective 

1596 

1597 stmin = common.time_or_none_to_str(tmin) 

1598 stmax = common.time_or_none_to_str(tmax) 

1599 

1600 self._label_effective_tmin.setText(stmin) 

1601 self._label_effective_tmax.setText(stmax) 

1602 

1603 def update_tcursor(self, *args): 

1604 tcursor = self.gui_state.tcursor 

1605 stcursor = common.time_or_none_to_str(tcursor) 

1606 self._label_tcursor.setText(stcursor) 

1607 

1608 def reset_strike_dip(self, *args): 

1609 self.state.strike = 90. 

1610 self.state.dip = 0 

1611 self.gui_state.focal_point = 'center' 

1612 

1613 def get_camera_geometry(self): 

1614 

1615 def rtp2xyz(rtp): 

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

1617 

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

1619 

1620 cam_rtp = num.array([ 

1621 radius+self.state.distance, 

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

1623 self.state.lon * d2r]) 

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

1625 cam, up, foc = \ 

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

1627 

1628 foc_rtp = num.array([ 

1629 radius, 

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

1631 self.state.lon * d2r]) 

1632 

1633 foc = rtp2xyz(foc_rtp) 

1634 

1635 rot_world = pmt.euler_to_matrix( 

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

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

1638 0.0*d2r).T 

1639 

1640 rot_cam = pmt.euler_to_matrix( 

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

1642 

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

1644 

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

1646 up = num.dot(rot, up) 

1647 return cam, up, foc 

1648 

1649 def update_camera(self, *args): 

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

1651 camera = self.ren.GetActiveCamera() 

1652 camera.SetPosition(*cam) 

1653 camera.SetFocalPoint(*foc) 

1654 camera.SetViewUp(*up) 

1655 

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

1657 

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

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

1660 

1661 # if horizon == 0.0: 

1662 # horizon = 2.0 + self.state.distance 

1663 

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

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

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

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

1668 # clip_dist = feature_horizon 

1669 

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

1671 

1672 self.camera_params = ( 

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

1674 

1675 self.update_view() 

1676 

1677 def add_panel( 

1678 self, title_label, panel, 

1679 visible=False, 

1680 # volatile=False, 

1681 tabify=True, 

1682 where=qc.Qt.RightDockWidgetArea, 

1683 remove=None, 

1684 title_controls=[]): 

1685 

1686 dockwidget = common.MyDockWidget( 

1687 self, title_label, title_controls=title_controls) 

1688 

1689 if not visible: 

1690 dockwidget.hide() 

1691 

1692 if not self.gui_state.panels_visible: 

1693 dockwidget.block() 

1694 

1695 dockwidget.setWidget(panel) 

1696 

1697 panel.setParent(dockwidget) 

1698 

1699 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1701 

1702 self.addDockWidget(where, dockwidget) 

1703 

1704 nwrap = 4 

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

1706 self.tabifyDockWidget( 

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

1708 

1709 mitem = dockwidget.toggleViewAction() 

1710 

1711 def update_label(*args): 

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

1713 self.update_slug_abbreviated_lengths() 

1714 

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

1716 dockwidget.titlebar._title_label.title_changed.connect( 

1717 self.update_slug_abbreviated_lengths) 

1718 

1719 update_label() 

1720 

1721 self._panel_togglers[dockwidget] = mitem 

1722 self.panels_menu.addAction(mitem) 

1723 if visible: 

1724 dockwidget.setVisible(True) 

1725 dockwidget.setFocus() 

1726 dockwidget.raise_() 

1727 

1728 def stack_panels(self): 

1729 dockwidgets = self.findChildren(common.MyDockWidget) 

1730 by_area = defaultdict(list) 

1731 for dw in dockwidgets: 

1732 area = self.dockWidgetArea(dw) 

1733 by_area[area].append(dw) 

1734 

1735 for dockwidgets in by_area.values(): 

1736 dw_last = None 

1737 for dw in dockwidgets: 

1738 if dw_last is not None: 

1739 self.tabifyDockWidget(dw_last, dw) 

1740 

1741 dw_last = dw 

1742 

1743 def update_slug_abbreviated_lengths(self): 

1744 dockwidgets = self.findChildren(common.MyDockWidget) 

1745 title_labels = [] 

1746 for dw in dockwidgets: 

1747 title_labels.append(dw.titlebar._title_label) 

1748 

1749 by_title = defaultdict(list) 

1750 for tl in title_labels: 

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

1752 

1753 for group in by_title.values(): 

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

1755 

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

1757 nunique = len(set(slugs)) 

1758 

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

1760 n -= 1 

1761 

1762 if n > 0: 

1763 n = max(3, n) 

1764 

1765 for tl in group: 

1766 tl.set_slug_abbreviated_length(n) 

1767 

1768 def raise_panel(self, panel): 

1769 dockwidget = panel.parent() 

1770 dockwidget.setVisible(True) 

1771 dockwidget.setFocus() 

1772 dockwidget.raise_() 

1773 

1774 def toggle_panel_visibility(self): 

1775 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1776 

1777 def update_panel_visibility(self, *args): 

1778 self.setUpdatesEnabled(False) 

1779 mbar = self.menuBar() 

1780 sbar = self.statusBar() 

1781 dockwidgets = self.findChildren(common.MyDockWidget) 

1782 

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

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

1785 # objects. 

1786 mbar.setFixedHeight( 

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

1788 

1789 sbar.setVisible(self.gui_state.panels_visible) 

1790 for dockwidget in dockwidgets: 

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

1792 

1793 self.setUpdatesEnabled(True) 

1794 

1795 def remove_panel(self, panel): 

1796 dockwidget = panel.parent() 

1797 self.removeDockWidget(dockwidget) 

1798 dockwidget.setParent(None) 

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

1800 

1801 def register_data_provider(self, provider): 

1802 if provider not in self.data_providers: 

1803 self.data_providers.append(provider) 

1804 

1805 def unregister_data_provider(self, provider): 

1806 if provider in self.data_providers: 

1807 self.data_providers.remove(provider) 

1808 

1809 def iter_data(self, name): 

1810 for provider in self.data_providers: 

1811 for data in provider.iter_data(name): 

1812 yield data 

1813 

1814 def confirm_close(self): 

1815 ret = qw.QMessageBox.question( 

1816 self, 

1817 'Sparrow', 

1818 'Close Sparrow window?', 

1819 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1820 qw.QMessageBox.Ok) 

1821 

1822 return ret == qw.QMessageBox.Ok 

1823 

1824 def closeEvent(self, event): 

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

1826 self.attach() 

1827 self.closing = True 

1828 event.accept() 

1829 else: 

1830 event.ignore() 

1831 

1832 def is_closing(self): 

1833 return self.closing 

1834 

1835 

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

1837 

1838 from pyrocko import util 

1839 from pyrocko.gui import util as gui_util 

1840 from . import common 

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

1842 

1843 global win 

1844 

1845 app = gui_util.get_app() 

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

1847 app.set_main_window(win) 

1848 

1849 gui_util.app.install_sigint_handler() 

1850 

1851 try: 

1852 gui_util.app.exec_() 

1853 finally: 

1854 gui_util.app.uninstall_sigint_handler() 

1855 app.unset_main_window() 

1856 common.set_viewer(None) 

1857 del win 

1858 gc.collect()