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 self.panels_menu = mbar.addMenu('Panels') 

382 self.panels_menu.addAction( 

383 'Stack Panels', 

384 self.stack_panels) 

385 self.panels_menu.addSeparator() 

386 

387 snapshots_menu = mbar.addMenu('Snapshots') 

388 

389 menu = mbar.addMenu('Elements') 

390 for name, estate in sorted([ 

391 ('Icosphere', elements.IcosphereState( 

392 level=4, 

393 smooth=True, 

394 opacity=0.5, 

395 ambient=0.1)), 

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

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

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

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

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

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

402 ('Source', elements.SourceState()), 

403 ('HUD Subtitle', elements.HudState( 

404 template='Subtitle')), 

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

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

407 position='top-left')), 

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

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

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

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

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

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

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

415 

416 def wrap_add_element(estate): 

417 def add_element(*args): 

418 new_element = guts.clone(estate) 

419 new_element.element_id = elements.random_id() 

420 self.state.elements.append(new_element) 

421 self.state.sort_elements() 

422 

423 return add_element 

424 

425 mitem = qw.QAction(name, self) 

426 

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

428 

429 menu.addAction(mitem) 

430 

431 menu = mbar.addMenu('Help') 

432 

433 menu.addAction( 

434 'Interactive Tour', 

435 self.start_tour) 

436 

437 menu.addAction( 

438 'Online Manual', 

439 self.open_manual) 

440 

441 self.data_providers = [] 

442 self.elements = {} 

443 

444 self.detached_window = None 

445 

446 self.main_frame = qw.QFrame() 

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

448 

449 self.vtk_frame = CenteringScrollArea() 

450 

451 self.vtk_widget = QVTKWidget(self, self) 

452 self.vtk_frame.setWidget(self.vtk_widget) 

453 

454 self.main_layout = qw.QVBoxLayout() 

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

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

457 

458 pb = Progressbars(self) 

459 self.progressbars = pb 

460 self.main_layout.addWidget(pb) 

461 

462 self.main_frame.setLayout(self.main_layout) 

463 

464 self.vtk_frame_substitute = None 

465 

466 self.add_panel( 

467 'Navigation', 

468 self.controls_navigation(), visible=True, 

469 where=qc.Qt.LeftDockWidgetArea) 

470 

471 self.add_panel( 

472 'Time', 

473 self.controls_time(), visible=True, 

474 where=qc.Qt.LeftDockWidgetArea) 

475 

476 self.add_panel( 

477 'Appearance', 

478 self.controls_appearance(), visible=True, 

479 where=qc.Qt.LeftDockWidgetArea) 

480 

481 snapshots_panel = self.controls_snapshots() 

482 self.snapshots_panel = snapshots_panel 

483 self.add_panel( 

484 'Snapshots', 

485 snapshots_panel, visible=False, 

486 where=qc.Qt.LeftDockWidgetArea) 

487 

488 snapshots_panel.setup_menu(snapshots_menu) 

489 

490 self.setCentralWidget(self.main_frame) 

491 

492 self.mesh = None 

493 

494 ren = vtk.vtkRenderer() 

495 

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

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

498 # ren.TwoSidedLightingOn() 

499 # ren.SetUseShadows(1) 

500 

501 self._lighting = None 

502 self._background = None 

503 

504 self.ren = ren 

505 self.update_render_settings() 

506 self.update_camera() 

507 

508 renwin = self.vtk_widget.GetRenderWindow() 

509 

510 if self._use_depth_peeling: 

511 renwin.SetAlphaBitPlanes(1) 

512 renwin.SetMultiSamples(0) 

513 

514 ren.SetUseDepthPeeling(1) 

515 ren.SetMaximumNumberOfPeels(100) 

516 ren.SetOcclusionRatio(0.1) 

517 

518 ren.SetUseFXAA(1) 

519 # ren.SetUseHiddenLineRemoval(1) 

520 # ren.SetBackingStore(1) 

521 

522 self.renwin = renwin 

523 

524 # renwin.LineSmoothingOn() 

525 # renwin.PointSmoothingOn() 

526 # renwin.PolygonSmoothingOn() 

527 

528 renwin.AddRenderer(ren) 

529 

530 iren = renwin.GetInteractor() 

531 iren.LightFollowCameraOn() 

532 iren.SetInteractorStyle(None) 

533 

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

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

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

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

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

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

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

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

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

543 

544 renwin.Render() 

545 

546 iren.Initialize() 

547 

548 self.iren = iren 

549 

550 self.rotating = False 

551 

552 self._elements = {} 

553 self._elements_active = {} 

554 

555 self.talkie_connect( 

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

557 

558 self.state.elements.append(elements.IcosphereState( 

559 element_id='icosphere', 

560 level=4, 

561 smooth=True, 

562 opacity=0.5, 

563 ambient=0.1)) 

564 

565 self.state.elements.append(elements.GridState( 

566 element_id='grid')) 

567 self.state.elements.append(elements.CoastlinesState( 

568 element_id='coastlines')) 

569 self.state.elements.append(elements.CrosshairState( 

570 element_id='crosshair')) 

571 

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

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

574 # self.state.elements.append( 

575 # elements.CatalogState( 

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

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

578 

579 if events: 

580 self.state.elements.append( 

581 elements.CatalogState( 

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

583 

584 self.state.sort_elements() 

585 

586 if snapshots: 

587 snapshots_ = [] 

588 for obj in snapshots: 

589 if isinstance(obj, str): 

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

591 else: 

592 snapshots_.append(obj) 

593 

594 snapshots_panel.add_snapshots(snapshots_) 

595 self.raise_panel(snapshots_panel) 

596 snapshots_panel.goto_snapshot(1) 

597 

598 self.timer = qc.QTimer(self) 

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

600 self.timer.setInterval(1000) 

601 self.timer.start() 

602 

603 self._animation_saver = None 

604 

605 self.closing = False 

606 self.vtk_widget.setFocus() 

607 

608 self.update_detached() 

609 

610 self.status( 

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

612 

613 self.status( 

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

615 

616 self.show() 

617 self.windowHandle().showMaximized() 

618 

619 self.talkie_connect( 

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

621 

622 self.update_vtk_widget_size() 

623 

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

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

626 

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

628 self.capture_state() 

629 

630 set_download_callback(self.update_download_progress) 

631 

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

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

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

635 

636 self.start_tour() 

637 

638 def update_download_progress(self, message, args): 

639 self.download_progress_update.emit() 

640 

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

642 self.statusBar().showMessage( 

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

644 

645 def disable_capture(self): 

646 self._block_capture += 1 

647 

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

649 

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

651 if self._block_capture > 0: 

652 self._block_capture -= 1 

653 

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

655 

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

657 self.capture_state(aggregate=aggregate) 

658 

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

660 if self._block_capture: 

661 return 

662 

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

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

665 

666 if aggregate is not None: 

667 if aggregate == self._undo_aggregate: 

668 self._undo_stack.pop() 

669 

670 self._undo_aggregate = aggregate 

671 else: 

672 self._undo_aggregate = None 

673 

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

675 len(self._undo_stack) + 1, 

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

677 '\n'.join( 

678 ' - %s' % s 

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

680 self.state).splitlines()) 

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

682 

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

684 self._redo_stack.clear() 

685 

686 def undo(self): 

687 self._undo_aggregate = None 

688 

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

690 return 

691 

692 state = self._undo_stack.pop() 

693 self._redo_stack.append(state) 

694 state = self._undo_stack[-1] 

695 

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

697 len(self._undo_stack), 

698 '\n'.join( 

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

700 

701 self.disable_capture() 

702 try: 

703 self.set_state(state) 

704 finally: 

705 self.enable_capture(drop=True) 

706 

707 def redo(self): 

708 self._undo_aggregate = None 

709 

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

711 return 

712 

713 state = self._redo_stack.pop() 

714 self._undo_stack.append(state) 

715 

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

717 len(self._redo_stack), 

718 '\n'.join( 

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

720 

721 self.disable_capture() 

722 try: 

723 self.set_state(state) 

724 finally: 

725 self.enable_capture(drop=True) 

726 

727 def start_tour(self): 

728 snapshots_ = snapshots_mod.load_snapshots( 

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

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

731 self.snapshots_panel.add_snapshots(snapshots_) 

732 self.raise_panel(self.snapshots_panel) 

733 self.snapshots_panel.transition_to_next_snapshot() 

734 

735 def open_manual(self): 

736 import webbrowser 

737 webbrowser.open( 

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

739 

740 def _add_vtk_widget_size_menu_entries(self, menu): 

741 

742 group = qw.QActionGroup(menu) 

743 group.setExclusive(True) 

744 

745 def set_variable_size(): 

746 self.gui_state.fixed_size = False 

747 

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

749 variable_size_action.setCheckable(True) 

750 variable_size_action.setActionGroup(group) 

751 variable_size_action.triggered.connect(set_variable_size) 

752 

753 fixed_size_items = [] 

754 for nx, ny, label in [ 

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

756 (426, 240, ''), 

757 (640, 360, ''), 

758 (854, 480, '(FWVGA)'), 

759 (1280, 720, '(HD)'), 

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

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

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

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

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

765 (640, 480, '(VGA)'), 

766 (800, 600, '(SVGA)'), 

767 (None, None, 'Other'), 

768 (512, 512, ''), 

769 (1024, 1024, '')]: 

770 

771 if None in (nx, ny): 

772 menu.addSection(label) 

773 else: 

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

775 action = menu.addAction(name) 

776 action.setCheckable(True) 

777 action.setActionGroup(group) 

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

779 

780 def make_set_fixed_size(nx, ny): 

781 def set_fixed_size(): 

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

783 

784 return set_fixed_size 

785 

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

787 

788 def update_widget(*args): 

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

790 action.blockSignals(True) 

791 action.setChecked( 

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

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

794 action.blockSignals(False) 

795 

796 variable_size_action.blockSignals(True) 

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

798 variable_size_action.blockSignals(False) 

799 

800 update_widget() 

801 self.talkie_connect( 

802 self.gui_state, 'fixed_size', update_widget) 

803 

804 def update_vtk_widget_size(self, *args): 

805 if self.gui_state.fixed_size: 

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

807 wanted_size = qc.QSize(nx, ny) 

808 else: 

809 wanted_size = qc.QSize( 

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

811 

812 current_size = self.vtk_widget.size() 

813 

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

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

816 

817 self.vtk_widget.setFixedSize(wanted_size) 

818 

819 self.vtk_frame.recenter() 

820 self.check_vtk_resize() 

821 

822 def update_focal_point(self, *args): 

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

824 self.vtk_widget.setStatusTip( 

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

826 'change view plane orientation.' % g_modifier_key) 

827 else: 

828 self.vtk_widget.setStatusTip( 

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

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

831 'reverse sense.' % g_modifier_key) 

832 

833 def update_detached(self, *args): 

834 

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

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

837 

838 self.main_layout.removeWidget(self.vtk_frame) 

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

840 self.detached_window.show() 

841 self.vtk_widget.setFocus() 

842 

843 screens = common.get_app().screens() 

844 if len(screens) > 1: 

845 for screen in screens: 

846 if screen is not self.screen(): 

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

848 # .setScreen() does not work reliably, 

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

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

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

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

853 

854 self.detached_window.windowHandle().showMaximized() 

855 

856 frame = qw.QFrame() 

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

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

859 # frame.setAutoFillBackground(True) 

860 frame.setSizePolicy( 

861 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

862 

863 layout = qw.QGridLayout() 

864 frame.setLayout(layout) 

865 self.main_layout.insertWidget(0, frame) 

866 

867 self.state_editor = StateEditor(self) 

868 

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

870 

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

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

873 # layout.addWidget( 

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

875 

876 self.vtk_frame_substitute = frame 

877 

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

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

880 self.detached_window.hide() 

881 self.vtk_frame.setParent(self) 

882 if self.vtk_frame_substitute: 

883 self.main_layout.removeWidget(self.vtk_frame_substitute) 

884 self.state_editor.unbind_state() 

885 self.vtk_frame_substitute = None 

886 

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

888 self.detached_window = None 

889 self.vtk_widget.setFocus() 

890 

891 def attach(self): 

892 self.gui_state.detached = False 

893 

894 def export_image(self): 

895 

896 caption = 'Export Image' 

897 fn_out, _ = qw.QFileDialog.getSaveFileName( 

898 self, caption, 'image.png', 

899 options=common.qfiledialog_options) 

900 

901 if fn_out: 

902 self.save_image(fn_out) 

903 

904 def save_image(self, path): 

905 

906 original_fixed_size = self.gui_state.fixed_size 

907 if original_fixed_size is None: 

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

909 

910 wif = vtk.vtkWindowToImageFilter() 

911 wif.SetInput(self.renwin) 

912 wif.SetInputBufferTypeToRGBA() 

913 wif.SetScale(1, 1) 

914 wif.ReadFrontBufferOff() 

915 writer = vtk.vtkPNGWriter() 

916 writer.SetInputConnection(wif.GetOutputPort()) 

917 

918 self.renwin.Render() 

919 wif.Modified() 

920 writer.SetFileName(path) 

921 writer.Write() 

922 

923 self.gui_state.fixed_size = original_fixed_size 

924 

925 def update_render_settings(self, *args): 

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

927 self.ren.RemoveAllLights() 

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

929 self.ren.AddLight(li) 

930 

931 self._lighting = self.state.lighting 

932 

933 if self._background is None \ 

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

935 

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

937 self._background = self.state.background 

938 

939 self.update_view() 

940 

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

942 if self._animation: 

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

944 self.stop_animation() 

945 

946 self.disable_capture() 

947 self._animation = interpolator 

948 if output_path is None: 

949 self._animation_tstart = time.time() 

950 self._animation_iframe = None 

951 else: 

952 self._animation_iframe = 0 

953 self.showFullScreen() 

954 self.update_view() 

955 self.gui_state.panels_visible = False 

956 self.update_view() 

957 

958 self._animation_timer = qc.QTimer(self) 

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

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

961 self._animation_timer.start() 

962 if output_path is not None: 

963 original_fixed_size = self.gui_state.fixed_size 

964 if original_fixed_size is None: 

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

966 

967 wif = vtk.vtkWindowToImageFilter() 

968 wif.SetInput(self.renwin) 

969 wif.SetInputBufferTypeToRGBA() 

970 wif.SetScale(1, 1) 

971 wif.ReadFrontBufferOff() 

972 writer = vtk.vtkPNGWriter() 

973 temp_path = tempfile.mkdtemp() 

974 self._animation_saver = ( 

975 wif, writer, temp_path, output_path, original_fixed_size) 

976 writer.SetInputConnection(wif.GetOutputPort()) 

977 

978 def next_animation_frame(self): 

979 

980 ani = self._animation 

981 if not ani: 

982 return 

983 

984 if self._animation_iframe is not None: 

985 state = ani( 

986 ani.tmin 

987 + self._animation_iframe * ani.dt) 

988 

989 self._animation_iframe += 1 

990 else: 

991 tnow = time.time() 

992 state = ani(min( 

993 ani.tmax, 

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

995 

996 self.set_state(state) 

997 self.renwin.Render() 

998 if self._animation_saver: 

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

1000 wif.Modified() 

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

1002 writer.SetFileName(fn % self._animation_iframe) 

1003 writer.Write() 

1004 

1005 if self._animation_iframe is not None: 

1006 t = self._animation_iframe * ani.dt 

1007 else: 

1008 t = tnow - self._animation_tstart 

1009 

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

1011 self.stop_animation() 

1012 

1013 def stop_animation(self): 

1014 if self._animation_timer: 

1015 self._animation_timer.stop() 

1016 

1017 if self._animation_saver: 

1018 

1019 wif, writer, temp_path, output_path, original_fixed_size \ 

1020 = self._animation_saver 

1021 self.gui_state.fixed_size = original_fixed_size 

1022 

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

1024 check_call([ 

1025 'ffmpeg', '-y', 

1026 '-i', fn_path, 

1027 '-c:v', 'libx264', 

1028 '-preset', 'slow', 

1029 '-crf', '17', 

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

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

1032 output_path]) 

1033 shutil.rmtree(temp_path) 

1034 

1035 self._animation_saver = None 

1036 self._animation_saver 

1037 

1038 self.showNormal() 

1039 self.gui_state.panels_visible = True 

1040 

1041 self._animation_tstart = None 

1042 self._animation_iframe = None 

1043 self._animation = None 

1044 self.enable_capture() 

1045 

1046 def set_state(self, state): 

1047 self.disable_capture() 

1048 try: 

1049 self._update_elements_enabled = False 

1050 self.setUpdatesEnabled(False) 

1051 self.state.diff_update(state) 

1052 self.state.sort_elements() 

1053 self.setUpdatesEnabled(True) 

1054 self._update_elements_enabled = True 

1055 self.update_elements() 

1056 finally: 

1057 self.enable_capture() 

1058 

1059 def periodical(self): 

1060 pass 

1061 

1062 def check_vtk_resize(self, *args): 

1063 render_window_size = self.renwin.GetSize() 

1064 if self._render_window_size != render_window_size: 

1065 self._render_window_size = render_window_size 

1066 self.resize_event(*render_window_size) 

1067 

1068 def update_elements(self, *_): 

1069 if not self._update_elements_enabled: 

1070 return 

1071 

1072 if self._in_update_elements: 

1073 return 

1074 

1075 self._in_update_elements = True 

1076 for estate in self.state.elements: 

1077 if estate.element_id not in self._elements: 

1078 new_element = estate.create() 

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

1080 type(new_element).__name__, 

1081 estate.element_id)) 

1082 self._elements[estate.element_id] = new_element 

1083 

1084 element = self._elements[estate.element_id] 

1085 

1086 if estate.element_id not in self._elements_active: 

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

1088 type(element).__name__, 

1089 estate.element_id)) 

1090 element.bind_state(estate) 

1091 element.set_parent(self) 

1092 self._elements_active[estate.element_id] = element 

1093 

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

1095 deactivate = [] 

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

1097 if element_id not in state_element_ids: 

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

1099 type(element).__name__, 

1100 element_id)) 

1101 element.unset_parent() 

1102 deactivate.append(element_id) 

1103 

1104 for element_id in deactivate: 

1105 del self._elements_active[element_id] 

1106 

1107 self._update_crosshair_bindings() 

1108 

1109 self._in_update_elements = False 

1110 

1111 def _update_crosshair_bindings(self): 

1112 

1113 def get_crosshair_element(): 

1114 for element in self.state.elements: 

1115 if element.element_id == 'crosshair': 

1116 return element 

1117 

1118 return None 

1119 

1120 crosshair = get_crosshair_element() 

1121 if crosshair is None or crosshair.is_connected: 

1122 return 

1123 

1124 def to_checkbox(state, widget): 

1125 widget.blockSignals(True) 

1126 widget.setChecked(state.visible) 

1127 widget.blockSignals(False) 

1128 

1129 def to_state(widget, state): 

1130 state.visible = widget.isChecked() 

1131 

1132 cb = self._crosshair_checkbox 

1133 vstate.state_bind( 

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

1135 cb, [cb.toggled], to_checkbox) 

1136 

1137 crosshair.is_connected = True 

1138 

1139 def add_actor_2d(self, actor): 

1140 if actor not in self._actors_2d: 

1141 self.ren.AddActor2D(actor) 

1142 self._actors_2d.add(actor) 

1143 

1144 def remove_actor_2d(self, actor): 

1145 if actor in self._actors_2d: 

1146 self.ren.RemoveActor2D(actor) 

1147 self._actors_2d.remove(actor) 

1148 

1149 def add_actor(self, actor): 

1150 if actor not in self._actors: 

1151 self.ren.AddActor(actor) 

1152 self._actors.add(actor) 

1153 

1154 def add_actor_list(self, actorlist): 

1155 for actor in actorlist: 

1156 self.add_actor(actor) 

1157 

1158 def remove_actor(self, actor): 

1159 if actor in self._actors: 

1160 self.ren.RemoveActor(actor) 

1161 self._actors.remove(actor) 

1162 

1163 def update_view(self): 

1164 self.vtk_widget.update() 

1165 

1166 def resize_event(self, size_x, size_y): 

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

1168 

1169 def button_event(self, obj, event): 

1170 if event == "LeftButtonPressEvent": 

1171 self.rotating = True 

1172 elif event == "LeftButtonReleaseEvent": 

1173 self.rotating = False 

1174 

1175 def mouse_move_event(self, obj, event): 

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

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

1178 

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

1180 center_x = size_x / 2.0 

1181 center_y = size_y / 2.0 

1182 

1183 if self.rotating: 

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

1185 

1186 def myWheelEvent(self, event): 

1187 

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

1189 

1190 if angle > 200: 

1191 angle = 200 

1192 

1193 if angle < -200: 

1194 angle = -200 

1195 

1196 self.disable_capture() 

1197 try: 

1198 self.do_dolly(-angle/100.) 

1199 finally: 

1200 self.enable_capture(aggregate='distance') 

1201 

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

1203 

1204 dx = x0 - x 

1205 dy = y0 - y 

1206 

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

1208 focp = self.gui_state.focal_point 

1209 

1210 if focp == 'center': 

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

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

1213 

1214 lat = self.state.lat 

1215 lon = self.state.lon 

1216 factor = self.state.distance / 10.0 

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

1218 else: 

1219 lat = 90. - self.state.dip 

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

1221 factor = 0.5 

1222 factor_lat = 1.0 

1223 

1224 dlat = dy * factor 

1225 dlon = dx * factor * factor_lat 

1226 

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

1228 lon += dlon 

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

1230 

1231 if focp == 'center': 

1232 self.state.lat = float(lat) 

1233 self.state.lon = float(lon) 

1234 else: 

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

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

1237 

1238 def do_dolly(self, v): 

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

1240 

1241 def key_down_event(self, obj, event): 

1242 k = obj.GetKeyCode() 

1243 if k == 'f': 

1244 self.gui_state.next_focal_point() 

1245 

1246 elif k == 'r': 

1247 self.reset_strike_dip() 

1248 

1249 elif k == 'p': 

1250 print(self.state) 

1251 

1252 elif k == 'i': 

1253 for elem in self.state.elements: 

1254 if isinstance(elem, elements.IcosphereState): 

1255 elem.visible = not elem.visible 

1256 

1257 elif k == 'c': 

1258 for elem in self.state.elements: 

1259 if isinstance(elem, elements.CoastlinesState): 

1260 elem.visible = not elem.visible 

1261 

1262 elif k == 't': 

1263 if not any( 

1264 isinstance(elem, elements.TopoState) 

1265 for elem in self.state.elements): 

1266 

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

1268 else: 

1269 for elem in self.state.elements: 

1270 if isinstance(elem, elements.TopoState): 

1271 elem.visible = not elem.visible 

1272 

1273 elif k == ' ': 

1274 self.toggle_panel_visibility() 

1275 

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

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

1278 

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

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

1281 

1282 def controls_navigation(self): 

1283 frame = qw.QFrame(self) 

1284 frame.setSizePolicy( 

1285 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1286 layout = qw.QGridLayout() 

1287 frame.setLayout(layout) 

1288 

1289 # lat, lon, depth 

1290 

1291 layout.addWidget( 

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

1293 

1294 le = qw.QLineEdit() 

1295 le.setStatusTip( 

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

1297 'Focal point location.') 

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

1299 

1300 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1303 

1304 def lineedit_to_lat_lon_depth(widget, state): 

1305 self.disable_capture() 

1306 try: 

1307 s = str(widget.text()) 

1308 choices = location_to_choices(s) 

1309 if len(choices) > 0: 

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

1311 choices[0].get_lat_lon_depth() 

1312 else: 

1313 raise NoLocationChoices(s) 

1314 

1315 finally: 

1316 self.enable_capture() 

1317 

1318 self._state_bind( 

1319 ['lat', 'lon', 'depth'], 

1320 lineedit_to_lat_lon_depth, 

1321 le, [le.editingFinished, le.returnPressed], 

1322 lat_lon_depth_to_lineedit) 

1323 

1324 self.lat_lon_lineedit = le 

1325 

1326 # focal point 

1327 

1328 cb = qw.QCheckBox('Fix') 

1329 cb.setStatusTip( 

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

1331 % g_modifier_key) 

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

1333 

1334 def focal_point_to_checkbox(state, widget): 

1335 widget.blockSignals(True) 

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

1337 widget.blockSignals(False) 

1338 

1339 def checkbox_to_focal_point(widget, state): 

1340 self.gui_state.focal_point = \ 

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

1342 

1343 self._gui_state_bind( 

1344 ['focal_point'], checkbox_to_focal_point, 

1345 cb, [cb.toggled], focal_point_to_checkbox) 

1346 

1347 self.focal_point_checkbox = cb 

1348 

1349 self.talkie_connect( 

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

1351 

1352 self.update_focal_point() 

1353 

1354 # strike, dip 

1355 

1356 layout.addWidget( 

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

1358 

1359 le = qw.QLineEdit() 

1360 le.setStatusTip( 

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

1362 'direction.') 

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

1364 

1365 def strike_dip_to_lineedit(state, widget): 

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

1367 

1368 def lineedit_to_strike_dip(widget, state): 

1369 s = str(widget.text()) 

1370 string_to_strike_dip = { 

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

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

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

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

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

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

1377 

1378 self.disable_capture() 

1379 if s in string_to_strike_dip: 

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

1381 

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

1383 try: 

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

1385 except Exception: 

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

1387 finally: 

1388 self.enable_capture() 

1389 

1390 self._state_bind( 

1391 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1393 

1394 self.strike_dip_lineedit = le 

1395 

1396 but = qw.QPushButton('Reset') 

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

1398 but.clicked.connect(self.reset_strike_dip) 

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

1400 

1401 # crosshair 

1402 

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

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

1405 

1406 # camera bindings 

1407 self.talkie_connect( 

1408 self.state, 

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

1410 self.update_camera) 

1411 

1412 self.talkie_connect( 

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

1414 

1415 return frame 

1416 

1417 def controls_time(self): 

1418 frame = qw.QFrame(self) 

1419 frame.setSizePolicy( 

1420 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1421 

1422 layout = qw.QGridLayout() 

1423 frame.setLayout(layout) 

1424 

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

1426 le_tmin = qw.QLineEdit() 

1427 layout.addWidget(le_tmin, 0, 1) 

1428 

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

1430 le_tmax = qw.QLineEdit() 

1431 layout.addWidget(le_tmax, 1, 1) 

1432 

1433 label_tcursor = qw.QLabel() 

1434 

1435 label_tcursor.setSizePolicy( 

1436 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1437 

1438 layout.addWidget(label_tcursor, 2, 1) 

1439 self._label_tcursor = label_tcursor 

1440 

1441 def time_to_lineedit(state, attribute, widget): 

1442 widget.setText( 

1443 common.time_or_none_to_str(getattr(state, attribute))) 

1444 

1445 def lineedit_to_time(widget, state, attribute): 

1446 from pyrocko.util import str_to_time_fillup 

1447 

1448 s = str(widget.text()) 

1449 if not s.strip(): 

1450 setattr(state, attribute, None) 

1451 else: 

1452 try: 

1453 setattr(state, attribute, str_to_time_fillup(s)) 

1454 except Exception: 

1455 raise ValueError( 

1456 'Use time format: YYYY-MM-DD HH:MM:SS.FFF') 

1457 

1458 self._state_bind( 

1459 ['tmin'], lineedit_to_time, le_tmin, 

1460 [le_tmin.editingFinished, le_tmin.returnPressed], time_to_lineedit, 

1461 attribute='tmin') 

1462 self._state_bind( 

1463 ['tmax'], lineedit_to_time, le_tmax, 

1464 [le_tmax.editingFinished, le_tmax.returnPressed], time_to_lineedit, 

1465 attribute='tmax') 

1466 

1467 self.tmin_lineedit = le_tmin 

1468 self.tmax_lineedit = le_tmax 

1469 

1470 range_edit = RangeEdit() 

1471 range_edit.rangeEditPressed.connect(self.disable_capture) 

1472 range_edit.rangeEditReleased.connect(self.enable_capture) 

1473 range_edit.set_data_provider(self) 

1474 range_edit.set_data_name('time') 

1475 

1476 xblock = [False] 

1477 

1478 def range_to_range_edit(state, widget): 

1479 if not xblock[0]: 

1480 widget.blockSignals(True) 

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

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

1483 widget.blockSignals(False) 

1484 

1485 def range_edit_to_range(widget, state): 

1486 xblock[0] = True 

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

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

1489 xblock[0] = False 

1490 

1491 self._state_bind( 

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

1493 range_edit_to_range, 

1494 range_edit, 

1495 [range_edit.rangeChanged, range_edit.focusChanged], 

1496 range_to_range_edit) 

1497 

1498 def handle_tcursor_changed(): 

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

1500 

1501 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1502 

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

1504 

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

1506 le_focus = qw.QLineEdit() 

1507 layout.addWidget(le_focus, 4, 1) 

1508 

1509 def focus_to_lineedit(state, widget): 

1510 if state.tduration is None: 

1511 widget.setText('') 

1512 else: 

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

1514 guts.str_duration(state.tduration), 

1515 state.tposition)) 

1516 

1517 def lineedit_to_focus(widget, state): 

1518 s = str(widget.text()) 

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

1520 try: 

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

1522 state.tduration = None 

1523 state.tposition = 0.0 

1524 else: 

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

1526 if len(w) > 1: 

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

1528 else: 

1529 state.tposition = 0.0 

1530 

1531 except Exception: 

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

1533 

1534 self._state_bind( 

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

1536 [le_focus.editingFinished, le_focus.returnPressed], 

1537 focus_to_lineedit) 

1538 

1539 label_effective_tmin = qw.QLabel() 

1540 label_effective_tmax = qw.QLabel() 

1541 

1542 label_effective_tmin.setSizePolicy( 

1543 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1544 label_effective_tmax.setSizePolicy( 

1545 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1546 label_effective_tmin.setMinimumSize( 

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

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

1549 

1550 layout.addWidget(label_effective_tmin, 5, 1) 

1551 layout.addWidget(label_effective_tmax, 6, 1) 

1552 

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

1554 self.talkie_connect( 

1555 self.state, var, self.update_effective_time_labels) 

1556 

1557 self._label_effective_tmin = label_effective_tmin 

1558 self._label_effective_tmax = label_effective_tmax 

1559 

1560 self.talkie_connect( 

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

1562 

1563 return frame 

1564 

1565 def controls_appearance(self): 

1566 frame = qw.QFrame(self) 

1567 frame.setSizePolicy( 

1568 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1569 layout = qw.QGridLayout() 

1570 frame.setLayout(layout) 

1571 

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

1573 

1574 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1575 layout.addWidget(cb, 0, 1) 

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

1577 

1578 self.talkie_connect( 

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

1580 

1581 # background 

1582 

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

1584 

1585 cb = common.strings_to_combobox( 

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

1587 

1588 layout.addWidget(cb, 1, 1) 

1589 vstate.state_bind_combobox_background( 

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

1591 

1592 self.talkie_connect( 

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

1594 

1595 return frame 

1596 

1597 def controls_snapshots(self): 

1598 return snapshots_mod.SnapshotsPanel(self) 

1599 

1600 def update_effective_time_labels(self, *args): 

1601 tmin = self.state.tmin_effective 

1602 tmax = self.state.tmax_effective 

1603 

1604 stmin = common.time_or_none_to_str(tmin) 

1605 stmax = common.time_or_none_to_str(tmax) 

1606 

1607 self._label_effective_tmin.setText(stmin) 

1608 self._label_effective_tmax.setText(stmax) 

1609 

1610 def update_tcursor(self, *args): 

1611 tcursor = self.gui_state.tcursor 

1612 stcursor = common.time_or_none_to_str(tcursor) 

1613 self._label_tcursor.setText(stcursor) 

1614 

1615 def reset_strike_dip(self, *args): 

1616 self.state.strike = 90. 

1617 self.state.dip = 0 

1618 self.gui_state.focal_point = 'center' 

1619 

1620 def get_camera_geometry(self): 

1621 

1622 def rtp2xyz(rtp): 

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

1624 

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

1626 

1627 cam_rtp = num.array([ 

1628 radius+self.state.distance, 

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

1630 self.state.lon * d2r]) 

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

1632 cam, up, foc = \ 

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

1634 

1635 foc_rtp = num.array([ 

1636 radius, 

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

1638 self.state.lon * d2r]) 

1639 

1640 foc = rtp2xyz(foc_rtp) 

1641 

1642 rot_world = pmt.euler_to_matrix( 

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

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

1645 0.0*d2r).T 

1646 

1647 rot_cam = pmt.euler_to_matrix( 

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

1649 

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

1651 

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

1653 up = num.dot(rot, up) 

1654 return cam, up, foc 

1655 

1656 def update_camera(self, *args): 

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

1658 camera = self.ren.GetActiveCamera() 

1659 camera.SetPosition(*cam) 

1660 camera.SetFocalPoint(*foc) 

1661 camera.SetViewUp(*up) 

1662 

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

1664 

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

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

1667 

1668 # if horizon == 0.0: 

1669 # horizon = 2.0 + self.state.distance 

1670 

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

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

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

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

1675 # clip_dist = feature_horizon 

1676 

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

1678 

1679 self.camera_params = ( 

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

1681 

1682 self.update_view() 

1683 

1684 def add_panel( 

1685 self, title_label, panel, 

1686 visible=False, 

1687 # volatile=False, 

1688 tabify=True, 

1689 where=qc.Qt.RightDockWidgetArea, 

1690 remove=None, 

1691 title_controls=[]): 

1692 

1693 dockwidget = common.MyDockWidget( 

1694 self, title_label, title_controls=title_controls) 

1695 

1696 if not visible: 

1697 dockwidget.hide() 

1698 

1699 if not self.gui_state.panels_visible: 

1700 dockwidget.block() 

1701 

1702 dockwidget.setWidget(panel) 

1703 

1704 panel.setParent(dockwidget) 

1705 

1706 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1708 

1709 self.addDockWidget(where, dockwidget) 

1710 

1711 nwrap = 4 

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

1713 self.tabifyDockWidget( 

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

1715 

1716 mitem = dockwidget.toggleViewAction() 

1717 

1718 def update_label(*args): 

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

1720 self.update_slug_abbreviated_lengths() 

1721 

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

1723 dockwidget.titlebar._title_label.title_changed.connect( 

1724 self.update_slug_abbreviated_lengths) 

1725 

1726 update_label() 

1727 

1728 self._panel_togglers[dockwidget] = mitem 

1729 self.panels_menu.addAction(mitem) 

1730 if visible: 

1731 dockwidget.setVisible(True) 

1732 dockwidget.setFocus() 

1733 dockwidget.raise_() 

1734 

1735 def stack_panels(self): 

1736 dockwidgets = self.findChildren(common.MyDockWidget) 

1737 by_area = defaultdict(list) 

1738 for dw in dockwidgets: 

1739 area = self.dockWidgetArea(dw) 

1740 by_area[area].append(dw) 

1741 

1742 for dockwidgets in by_area.values(): 

1743 dw_last = None 

1744 for dw in dockwidgets: 

1745 if dw_last is not None: 

1746 self.tabifyDockWidget(dw_last, dw) 

1747 

1748 dw_last = dw 

1749 

1750 def update_slug_abbreviated_lengths(self): 

1751 dockwidgets = self.findChildren(common.MyDockWidget) 

1752 title_labels = [] 

1753 for dw in dockwidgets: 

1754 title_labels.append(dw.titlebar._title_label) 

1755 

1756 by_title = defaultdict(list) 

1757 for tl in title_labels: 

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

1759 

1760 for group in by_title.values(): 

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

1762 

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

1764 nunique = len(set(slugs)) 

1765 

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

1767 n -= 1 

1768 

1769 if n > 0: 

1770 n = max(3, n) 

1771 

1772 for tl in group: 

1773 tl.set_slug_abbreviated_length(n) 

1774 

1775 def raise_panel(self, panel): 

1776 dockwidget = panel.parent() 

1777 dockwidget.setVisible(True) 

1778 dockwidget.setFocus() 

1779 dockwidget.raise_() 

1780 

1781 def toggle_panel_visibility(self): 

1782 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1783 

1784 def update_panel_visibility(self, *args): 

1785 self.setUpdatesEnabled(False) 

1786 mbar = self.menuBar() 

1787 sbar = self.statusBar() 

1788 dockwidgets = self.findChildren(common.MyDockWidget) 

1789 

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

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

1792 # objects. 

1793 mbar.setFixedHeight( 

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

1795 

1796 sbar.setVisible(self.gui_state.panels_visible) 

1797 for dockwidget in dockwidgets: 

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

1799 

1800 self.setUpdatesEnabled(True) 

1801 

1802 def remove_panel(self, panel): 

1803 dockwidget = panel.parent() 

1804 self.removeDockWidget(dockwidget) 

1805 dockwidget.setParent(None) 

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

1807 

1808 def register_data_provider(self, provider): 

1809 if provider not in self.data_providers: 

1810 self.data_providers.append(provider) 

1811 

1812 def unregister_data_provider(self, provider): 

1813 if provider in self.data_providers: 

1814 self.data_providers.remove(provider) 

1815 

1816 def iter_data(self, name): 

1817 for provider in self.data_providers: 

1818 for data in provider.iter_data(name): 

1819 yield data 

1820 

1821 def confirm_close(self): 

1822 ret = qw.QMessageBox.question( 

1823 self, 

1824 'Sparrow', 

1825 'Close Sparrow window?', 

1826 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1827 qw.QMessageBox.Ok) 

1828 

1829 return ret == qw.QMessageBox.Ok 

1830 

1831 def closeEvent(self, event): 

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

1833 self.attach() 

1834 self.closing = True 

1835 event.accept() 

1836 else: 

1837 event.ignore() 

1838 

1839 def is_closing(self): 

1840 return self.closing 

1841 

1842 

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

1844 

1845 from pyrocko import util 

1846 from pyrocko.gui import util as gui_util 

1847 from . import common 

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

1849 

1850 global win 

1851 

1852 app = gui_util.get_app() 

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

1854 app.set_main_window(win) 

1855 

1856 gui_util.app.install_sigint_handler() 

1857 

1858 try: 

1859 gui_util.app.exec_() 

1860 finally: 

1861 gui_util.app.uninstall_sigint_handler() 

1862 app.unset_main_window() 

1863 common.set_viewer(None) 

1864 del win 

1865 gc.collect()