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 

25 

26from pyrocko.gui.util import Progressbars, RangeEdit 

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

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

29# from pyrocko.gui import vtk_util 

30 

31from . import common, light, snapshots as snapshots_mod 

32 

33import vtk 

34import vtk.qt 

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

36 

37from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor # noqa 

38 

39from pyrocko import geometry # noqa 

40from . import state as vstate, elements # noqa 

41 

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

43 

44 

45d2r = num.pi/180. 

46km = 1000. 

47 

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

49 g_modifier_key = '\u2318' 

50else: 

51 g_modifier_key = 'Ctrl' 

52 

53 

54class ZeroFrame(qw.QFrame): 

55 

56 def sizeHint(self): 

57 return qc.QSize(0, 0) 

58 

59 

60class LocationChoice(object): 

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

62 self._name = name 

63 self._lat = lat 

64 self._lon = lon 

65 self._depth = depth 

66 

67 def get_lat_lon_depth(self): 

68 return self._lat, self._lon, self._depth 

69 

70 

71def location_to_choices(s): 

72 choices = [] 

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

74 try: 

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

76 if len(vals) == 3: 

77 vals[2] *= km 

78 

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

80 

81 except ValueError: 

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

83 for c in cities: 

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

85 

86 return choices 

87 

88 

89class NoLocationChoices(Exception): 

90 

91 def __init__(self, s): 

92 self._string = s 

93 

94 def __str__(self): 

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

96 

97 

98class QVTKWidget(QVTKRenderWindowInteractor): 

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

100 QVTKRenderWindowInteractor.__init__(self, *args) 

101 self._viewer = viewer 

102 self._ctrl_state = False 

103 

104 def wheelEvent(self, event): 

105 return self._viewer.myWheelEvent(event) 

106 

107 def keyPressEvent(self, event): 

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

109 self._update_ctrl_state(True) 

110 QVTKRenderWindowInteractor.keyPressEvent(self, event) 

111 

112 def keyReleaseEvent(self, event): 

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

114 self._update_ctrl_state(False) 

115 QVTKRenderWindowInteractor.keyReleaseEvent(self, event) 

116 

117 def focusInEvent(self, event): 

118 self._update_ctrl_state() 

119 QVTKRenderWindowInteractor.focusInEvent(self, event) 

120 

121 def focusOutEvent(self, event): 

122 self._update_ctrl_state(False) 

123 QVTKRenderWindowInteractor.focusOutEvent(self, event) 

124 

125 def mousePressEvent(self, event): 

126 self._viewer.disable_capture() 

127 QVTKRenderWindowInteractor.mousePressEvent(self, event) 

128 

129 def mouseReleaseEvent(self, event): 

130 self._viewer.enable_capture() 

131 QVTKRenderWindowInteractor.mouseReleaseEvent(self, event) 

132 

133 def _update_ctrl_state(self, state=None): 

134 if state is None: 

135 app = common.get_app() 

136 if not app: 

137 return 

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

139 if self._ctrl_state != state: 

140 self._viewer.gui_state.next_focal_point() 

141 self._ctrl_state = state 

142 

143 def container_resized(self, ev): 

144 self._viewer.update_vtk_widget_size() 

145 

146 

147class DetachedViewer(qw.QMainWindow): 

148 

149 def __init__(self, main_window, vtk_frame): 

150 qw.QMainWindow.__init__(self, main_window) 

151 self.main_window = main_window 

152 self.setWindowTitle('Sparrow View') 

153 vtk_frame.setParent(self) 

154 self.setCentralWidget(vtk_frame) 

155 

156 def closeEvent(self, ev): 

157 ev.ignore() 

158 self.main_window.attach() 

159 

160 

161class CenteringScrollArea(qw.QScrollArea): 

162 def __init__(self): 

163 qw.QScrollArea.__init__(self) 

164 self.setAlignment(qc.Qt.AlignCenter) 

165 self.setVerticalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

166 self.setHorizontalScrollBarPolicy(qc.Qt.ScrollBarAlwaysOff) 

167 self.setFrameShape(qw.QFrame.NoFrame) 

168 

169 def resizeEvent(self, ev): 

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

171 self.widget().container_resized(ev) 

172 return retval 

173 

174 def recenter(self): 

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

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

177 

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

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

180 

181 

182class YAMLEditor(qw.QTextEdit): 

183 

184 def __init__(self, parent): 

185 qw.QTextEdit.__init__(self) 

186 self._parent = parent 

187 

188 def event(self, ev): 

189 if isinstance(ev, qg.QKeyEvent) \ 

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

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

192 self._parent.state_changed() 

193 return True 

194 

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

196 

197 

198class StateEditor(qw.QFrame, TalkieConnectionOwner): 

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

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

201 TalkieConnectionOwner.__init__(self) 

202 

203 layout = qw.QGridLayout() 

204 

205 self.setLayout(layout) 

206 

207 self.source_editor = YAMLEditor(self) 

208 self.source_editor.setAcceptRichText(False) 

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

210 font = qg.QFont("Monospace") 

211 self.source_editor.setCurrentFont(font) 

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

213 

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

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

216 

217 self.error_display = qw.QTextEdit() 

218 self.error_display.setCurrentFont(font) 

219 self.error_display.setReadOnly(True) 

220 

221 self.error_display.setSizePolicy( 

222 qw.QSizePolicy.Minimum, qw.QSizePolicy.Minimum) 

223 

224 self.error_display_label.hide() 

225 self.error_display.hide() 

226 

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

228 

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

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

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

232 

233 button = qw.QPushButton('Apply') 

234 button.clicked.connect(self.state_changed) 

235 layout.addWidget(button, 3, 1) 

236 

237 self.viewer = viewer 

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

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

240 self.bind_state() 

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

242 self.destroyed.connect(self.unbind_state) 

243 self.bind_state() 

244 

245 def bind_state(self, *args): 

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

247 self.update_state() 

248 

249 def unbind_state(self): 

250 self.talkie_disconnect_all() 

251 

252 def update_state(self, *args): 

253 cursor = self.source_editor.textCursor() 

254 

255 cursor_position = cursor.position() 

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

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

258 

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

260 

261 cursor.setPosition(cursor_position) 

262 self.source_editor.setTextCursor(cursor) 

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

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

265 

266 def text_changed_handler(self, *args): 

267 if self.instant_updates.isChecked(): 

268 self.state_changed() 

269 

270 def state_changed(self): 

271 try: 

272 s = self.source_editor.toPlainText() 

273 state = guts.load(string=s) 

274 self.viewer.set_state(state) 

275 self.error_display.setPlainText('') 

276 self.error_display_label.hide() 

277 self.error_display.hide() 

278 

279 except Exception as e: 

280 self.error_display.show() 

281 self.error_display_label.show() 

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

283 

284 

285class SparrowViewer(qw.QMainWindow, TalkieConnectionOwner): 

286 def __init__( 

287 self, 

288 use_depth_peeling=True, 

289 events=None, 

290 snapshots=None, 

291 instant_close=False): 

292 

293 common.set_viewer(self) 

294 

295 qw.QMainWindow.__init__(self) 

296 TalkieConnectionOwner.__init__(self) 

297 

298 self.instant_close = instant_close 

299 

300 self.state = vstate.ViewerState() 

301 self.gui_state = vstate.ViewerGuiState() 

302 

303 self.setWindowTitle('Sparrow') 

304 

305 self.setTabPosition( 

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

307 

308 self.planet_radius = cake.earthradius 

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

310 

311 self._block_capture = 0 

312 self._undo_stack = [] 

313 self._redo_stack = [] 

314 self._undo_aggregate = None 

315 

316 self._panel_togglers = {} 

317 self._actors = set() 

318 self._actors_2d = set() 

319 self._render_window_size = (0, 0) 

320 self._use_depth_peeling = use_depth_peeling 

321 self._in_update_elements = False 

322 self._update_elements_enabled = True 

323 

324 self._animation_tstart = None 

325 self._animation_iframe = None 

326 self._animation = None 

327 

328 mbar = qw.QMenuBar() 

329 self.setMenuBar(mbar) 

330 

331 menu = mbar.addMenu('File') 

332 

333 menu.addAction( 

334 'Export Image...', 

335 self.export_image, 

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

337 qc.Qt.ApplicationShortcut) 

338 

339 menu.addAction( 

340 'Quit', 

341 self.close, 

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

343 qc.Qt.ApplicationShortcut) 

344 

345 menu = mbar.addMenu('Edit') 

346 

347 menu.addAction( 

348 'Undo', 

349 self.undo, 

350 qg.QKeySequence( 

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

352 qc.Qt.ApplicationShortcut) 

353 

354 menu.addAction( 

355 'Redo', 

356 self.redo, 

357 qg.QKeySequence( 

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

359 qc.Qt.ApplicationShortcut) 

360 

361 menu = mbar.addMenu('View') 

362 menu_sizes = menu.addMenu('Size') 

363 self._add_vtk_widget_size_menu_entries(menu_sizes) 

364 

365 # detached/attached 

366 self.talkie_connect( 

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

368 

369 action = qw.QAction('Detach') 

370 action.setCheckable(True) 

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

372 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

373 

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

375 menu.addAction(action) 

376 

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

378 self.panels_menu.addAction( 

379 'Stack Panels', 

380 self.stack_panels) 

381 self.panels_menu.addSeparator() 

382 

383 snapshots_menu = mbar.addMenu('Snapshots') 

384 

385 menu = mbar.addMenu('Elements') 

386 for name, estate in sorted([ 

387 ('Icosphere', elements.IcosphereState( 

388 level=4, 

389 smooth=True, 

390 opacity=0.5, 

391 ambient=0.1)), 

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

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

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

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

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

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

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

399 ('HUD Subtitle', elements.HudState( 

400 template='Subtitle')), 

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

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

403 position='top-left')), 

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

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

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

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

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

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

410 

411 def wrap_add_element(estate): 

412 def add_element(*args): 

413 new_element = guts.clone(estate) 

414 new_element.element_id = elements.random_id() 

415 self.state.elements.append(new_element) 

416 self.state.sort_elements() 

417 

418 return add_element 

419 

420 mitem = qw.QAction(name, self) 

421 

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

423 

424 menu.addAction(mitem) 

425 

426 menu = mbar.addMenu('Help') 

427 

428 menu.addAction( 

429 'Interactive Tour', 

430 self.start_tour) 

431 

432 menu.addAction( 

433 'Online Manual', 

434 self.open_manual) 

435 

436 self.data_providers = [] 

437 self.elements = {} 

438 

439 self.detached_window = None 

440 

441 self.main_frame = qw.QFrame() 

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

443 

444 self.vtk_frame = CenteringScrollArea() 

445 

446 self.vtk_widget = QVTKWidget(self, self) 

447 self.vtk_frame.setWidget(self.vtk_widget) 

448 

449 self.main_layout = qw.QVBoxLayout() 

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

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

452 

453 pb = Progressbars(self) 

454 self.progressbars = pb 

455 self.main_layout.addWidget(pb) 

456 

457 self.main_frame.setLayout(self.main_layout) 

458 

459 self.vtk_frame_substitute = None 

460 

461 self.add_panel( 

462 'Navigation', 

463 self.controls_navigation(), visible=True, 

464 where=qc.Qt.LeftDockWidgetArea) 

465 

466 self.add_panel( 

467 'Time', 

468 self.controls_time(), visible=True, 

469 where=qc.Qt.LeftDockWidgetArea) 

470 

471 self.add_panel( 

472 'Appearance', 

473 self.controls_appearance(), visible=True, 

474 where=qc.Qt.LeftDockWidgetArea) 

475 

476 snapshots_panel = self.controls_snapshots() 

477 self.snapshots_panel = snapshots_panel 

478 self.add_panel( 

479 'Snapshots', 

480 snapshots_panel, visible=False, 

481 where=qc.Qt.LeftDockWidgetArea) 

482 

483 snapshots_panel.setup_menu(snapshots_menu) 

484 

485 self.setCentralWidget(self.main_frame) 

486 

487 self.mesh = None 

488 

489 ren = vtk.vtkRenderer() 

490 

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

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

493 # ren.TwoSidedLightingOn() 

494 # ren.SetUseShadows(1) 

495 

496 self._lighting = None 

497 self._background = None 

498 

499 self.ren = ren 

500 self.update_render_settings() 

501 self.update_camera() 

502 

503 renwin = self.vtk_widget.GetRenderWindow() 

504 

505 if self._use_depth_peeling: 

506 renwin.SetAlphaBitPlanes(1) 

507 renwin.SetMultiSamples(0) 

508 

509 ren.SetUseDepthPeeling(1) 

510 ren.SetMaximumNumberOfPeels(100) 

511 ren.SetOcclusionRatio(0.1) 

512 

513 ren.SetUseFXAA(1) 

514 # ren.SetUseHiddenLineRemoval(1) 

515 # ren.SetBackingStore(1) 

516 

517 self.renwin = renwin 

518 

519 # renwin.LineSmoothingOn() 

520 # renwin.PointSmoothingOn() 

521 # renwin.PolygonSmoothingOn() 

522 

523 renwin.AddRenderer(ren) 

524 

525 iren = renwin.GetInteractor() 

526 iren.LightFollowCameraOn() 

527 iren.SetInteractorStyle(None) 

528 

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

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

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

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

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

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

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

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

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

538 

539 renwin.Render() 

540 

541 iren.Initialize() 

542 

543 self.iren = iren 

544 

545 self.rotating = False 

546 

547 self._elements = {} 

548 self._elements_active = {} 

549 

550 self.talkie_connect( 

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

552 

553 self.state.elements.append(elements.IcosphereState( 

554 element_id='icosphere', 

555 level=4, 

556 smooth=True, 

557 opacity=0.5, 

558 ambient=0.1)) 

559 

560 self.state.elements.append(elements.GridState( 

561 element_id='grid')) 

562 self.state.elements.append(elements.CoastlinesState( 

563 element_id='coastlines')) 

564 self.state.elements.append(elements.CrosshairState( 

565 element_id='crosshair')) 

566 

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

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

569 # self.state.elements.append( 

570 # elements.CatalogState( 

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

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

573 

574 if events: 

575 self.state.elements.append( 

576 elements.CatalogState( 

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

578 

579 self.state.sort_elements() 

580 

581 if snapshots: 

582 snapshots_ = [] 

583 for obj in snapshots: 

584 if isinstance(obj, str): 

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

586 else: 

587 snapshots_.append(obj) 

588 

589 snapshots_panel.add_snapshots(snapshots_) 

590 self.raise_panel(snapshots_panel) 

591 snapshots_panel.goto_snapshot(1) 

592 

593 self.timer = qc.QTimer(self) 

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

595 self.timer.setInterval(1000) 

596 self.timer.start() 

597 

598 self._animation_saver = None 

599 

600 self.closing = False 

601 self.vtk_widget.setFocus() 

602 

603 self.update_detached() 

604 

605 self.status( 

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

607 

608 self.status( 

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

610 

611 self.show() 

612 self.windowHandle().showMaximized() 

613 

614 self.talkie_connect( 

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

616 

617 self.update_vtk_widget_size() 

618 

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

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

621 

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

623 self.capture_state() 

624 

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

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

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

628 

629 self.start_tour() 

630 

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

632 self.statusBar().showMessage( 

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

634 

635 def disable_capture(self): 

636 self._block_capture += 1 

637 

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

639 

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

641 if self._block_capture > 0: 

642 self._block_capture -= 1 

643 

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

645 

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

647 self.capture_state(aggregate=aggregate) 

648 

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

650 if self._block_capture: 

651 return 

652 

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

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

655 

656 if aggregate is not None: 

657 if aggregate == self._undo_aggregate: 

658 self._undo_stack.pop() 

659 

660 self._undo_aggregate = aggregate 

661 else: 

662 self._undo_aggregate = None 

663 

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

665 len(self._undo_stack) + 1, 

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

667 '\n'.join( 

668 ' - %s' % s 

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

670 self.state).splitlines()) 

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

672 

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

674 self._redo_stack.clear() 

675 

676 def undo(self): 

677 self._undo_aggregate = None 

678 

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

680 return 

681 

682 state = self._undo_stack.pop() 

683 self._redo_stack.append(state) 

684 state = self._undo_stack[-1] 

685 

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

687 len(self._undo_stack), 

688 '\n'.join( 

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

690 

691 self.disable_capture() 

692 try: 

693 self.set_state(state) 

694 finally: 

695 self.enable_capture(drop=True) 

696 

697 def redo(self): 

698 self._undo_aggregate = None 

699 

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

701 return 

702 

703 state = self._redo_stack.pop() 

704 self._undo_stack.append(state) 

705 

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

707 len(self._redo_stack), 

708 '\n'.join( 

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

710 

711 self.disable_capture() 

712 try: 

713 self.set_state(state) 

714 finally: 

715 self.enable_capture(drop=True) 

716 

717 def start_tour(self): 

718 snapshots_ = snapshots_mod.load_snapshots( 

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

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

721 self.snapshots_panel.add_snapshots(snapshots_) 

722 self.raise_panel(self.snapshots_panel) 

723 self.snapshots_panel.transition_to_next_snapshot() 

724 

725 def open_manual(self): 

726 import webbrowser 

727 webbrowser.open( 

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

729 

730 def _add_vtk_widget_size_menu_entries(self, menu): 

731 

732 group = qw.QActionGroup(menu) 

733 group.setExclusive(True) 

734 

735 def set_variable_size(): 

736 self.gui_state.fixed_size = False 

737 

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

739 variable_size_action.setCheckable(True) 

740 variable_size_action.setActionGroup(group) 

741 variable_size_action.triggered.connect(set_variable_size) 

742 

743 fixed_size_items = [] 

744 for nx, ny, label in [ 

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

746 (426, 240, ''), 

747 (640, 360, ''), 

748 (854, 480, '(FWVGA)'), 

749 (1280, 720, '(HD)'), 

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

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

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

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

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

755 (640, 480, '(VGA)'), 

756 (800, 600, '(SVGA)'), 

757 (None, None, 'Other'), 

758 (512, 512, ''), 

759 (1024, 1024, '')]: 

760 

761 if None in (nx, ny): 

762 menu.addSection(label) 

763 else: 

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

765 action = menu.addAction(name) 

766 action.setCheckable(True) 

767 action.setActionGroup(group) 

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

769 

770 def make_set_fixed_size(nx, ny): 

771 def set_fixed_size(): 

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

773 

774 return set_fixed_size 

775 

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

777 

778 def update_widget(*args): 

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

780 action.blockSignals(True) 

781 action.setChecked( 

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

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

784 action.blockSignals(False) 

785 

786 variable_size_action.blockSignals(True) 

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

788 variable_size_action.blockSignals(False) 

789 

790 update_widget() 

791 self.talkie_connect( 

792 self.gui_state, 'fixed_size', update_widget) 

793 

794 def update_vtk_widget_size(self, *args): 

795 if self.gui_state.fixed_size: 

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

797 wanted_size = qc.QSize(nx, ny) 

798 else: 

799 wanted_size = qc.QSize( 

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

801 

802 current_size = self.vtk_widget.size() 

803 

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

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

806 

807 self.vtk_widget.setFixedSize(wanted_size) 

808 

809 self.vtk_frame.recenter() 

810 self.check_vtk_resize() 

811 

812 def update_focal_point(self, *args): 

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

814 self.vtk_widget.setStatusTip( 

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

816 'change view plane orientation.' % g_modifier_key) 

817 else: 

818 self.vtk_widget.setStatusTip( 

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

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

821 'reverse sense.' % g_modifier_key) 

822 

823 def update_detached(self, *args): 

824 

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

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

827 

828 self.main_layout.removeWidget(self.vtk_frame) 

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

830 self.detached_window.show() 

831 self.vtk_widget.setFocus() 

832 

833 screens = common.get_app().screens() 

834 if len(screens) > 1: 

835 for screen in screens: 

836 if screen is not self.screen(): 

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

838 # .setScreen() does not work reliably, 

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

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

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

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

843 

844 self.detached_window.windowHandle().showMaximized() 

845 

846 frame = qw.QFrame() 

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

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

849 # frame.setAutoFillBackground(True) 

850 frame.setSizePolicy( 

851 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

852 

853 layout = qw.QGridLayout() 

854 frame.setLayout(layout) 

855 self.main_layout.insertWidget(0, frame) 

856 

857 self.state_editor = StateEditor(self) 

858 

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

860 

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

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

863 # layout.addWidget( 

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

865 

866 self.vtk_frame_substitute = frame 

867 

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

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

870 self.detached_window.hide() 

871 self.vtk_frame.setParent(self) 

872 if self.vtk_frame_substitute: 

873 self.main_layout.removeWidget(self.vtk_frame_substitute) 

874 self.state_editor.unbind_state() 

875 self.vtk_frame_substitute = None 

876 

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

878 self.detached_window = None 

879 self.vtk_widget.setFocus() 

880 

881 def attach(self): 

882 self.gui_state.detached = False 

883 

884 def export_image(self): 

885 

886 caption = 'Export Image' 

887 fn_out, _ = qw.QFileDialog.getSaveFileName( 

888 self, caption, 'image.png', 

889 options=common.qfiledialog_options) 

890 

891 if fn_out: 

892 self.save_image(fn_out) 

893 

894 def save_image(self, path): 

895 

896 original_fixed_size = self.gui_state.fixed_size 

897 if original_fixed_size is None: 

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

899 

900 wif = vtk.vtkWindowToImageFilter() 

901 wif.SetInput(self.renwin) 

902 wif.SetInputBufferTypeToRGBA() 

903 wif.SetScale(1, 1) 

904 wif.ReadFrontBufferOff() 

905 writer = vtk.vtkPNGWriter() 

906 writer.SetInputConnection(wif.GetOutputPort()) 

907 

908 self.renwin.Render() 

909 wif.Modified() 

910 writer.SetFileName(path) 

911 writer.Write() 

912 

913 self.gui_state.fixed_size = original_fixed_size 

914 

915 def update_render_settings(self, *args): 

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

917 self.ren.RemoveAllLights() 

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

919 self.ren.AddLight(li) 

920 

921 self._lighting = self.state.lighting 

922 

923 if self._background is None \ 

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

925 

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

927 self._background = self.state.background 

928 

929 self.update_view() 

930 

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

932 if self._animation: 

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

934 self.stop_animation() 

935 

936 self.disable_capture() 

937 self._animation = interpolator 

938 if output_path is None: 

939 self._animation_tstart = time.time() 

940 self._animation_iframe = None 

941 else: 

942 self._animation_iframe = 0 

943 self.showFullScreen() 

944 self.update_view() 

945 self.gui_state.panels_visible = False 

946 self.update_view() 

947 

948 self._animation_timer = qc.QTimer(self) 

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

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

951 self._animation_timer.start() 

952 if output_path is not None: 

953 original_fixed_size = self.gui_state.fixed_size 

954 if original_fixed_size is None: 

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

956 

957 wif = vtk.vtkWindowToImageFilter() 

958 wif.SetInput(self.renwin) 

959 wif.SetInputBufferTypeToRGBA() 

960 wif.SetScale(1, 1) 

961 wif.ReadFrontBufferOff() 

962 writer = vtk.vtkPNGWriter() 

963 temp_path = tempfile.mkdtemp() 

964 self._animation_saver = ( 

965 wif, writer, temp_path, output_path, original_fixed_size) 

966 writer.SetInputConnection(wif.GetOutputPort()) 

967 

968 def next_animation_frame(self): 

969 

970 ani = self._animation 

971 if not ani: 

972 return 

973 

974 if self._animation_iframe is not None: 

975 state = ani( 

976 ani.tmin 

977 + self._animation_iframe * ani.dt) 

978 

979 self._animation_iframe += 1 

980 else: 

981 tnow = time.time() 

982 state = ani(min( 

983 ani.tmax, 

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

985 

986 self.set_state(state) 

987 self.renwin.Render() 

988 if self._animation_saver: 

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

990 wif.Modified() 

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

992 writer.SetFileName(fn % self._animation_iframe) 

993 writer.Write() 

994 

995 if self._animation_iframe is not None: 

996 t = self._animation_iframe * ani.dt 

997 else: 

998 t = tnow - self._animation_tstart 

999 

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

1001 self.stop_animation() 

1002 

1003 def stop_animation(self): 

1004 if self._animation_timer: 

1005 self._animation_timer.stop() 

1006 

1007 if self._animation_saver: 

1008 

1009 wif, writer, temp_path, output_path, original_fixed_size \ 

1010 = self._animation_saver 

1011 self.gui_state.fixed_size = original_fixed_size 

1012 

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

1014 check_call([ 

1015 'ffmpeg', '-y', 

1016 '-i', fn_path, 

1017 '-c:v', 'libx264', 

1018 '-preset', 'slow', 

1019 '-crf', '17', 

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

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

1022 output_path]) 

1023 shutil.rmtree(temp_path) 

1024 

1025 self._animation_saver = None 

1026 self._animation_saver 

1027 

1028 self.showNormal() 

1029 self.gui_state.panels_visible = True 

1030 

1031 self._animation_tstart = None 

1032 self._animation_iframe = None 

1033 self._animation = None 

1034 self.enable_capture() 

1035 

1036 def set_state(self, state): 

1037 self.disable_capture() 

1038 try: 

1039 self._update_elements_enabled = False 

1040 self.setUpdatesEnabled(False) 

1041 self.state.diff_update(state) 

1042 self.state.sort_elements() 

1043 self.setUpdatesEnabled(True) 

1044 self._update_elements_enabled = True 

1045 self.update_elements() 

1046 finally: 

1047 self.enable_capture() 

1048 

1049 def periodical(self): 

1050 pass 

1051 

1052 def check_vtk_resize(self, *args): 

1053 render_window_size = self.renwin.GetSize() 

1054 if self._render_window_size != render_window_size: 

1055 self._render_window_size = render_window_size 

1056 self.resize_event(*render_window_size) 

1057 

1058 def update_elements(self, *_): 

1059 if not self._update_elements_enabled: 

1060 return 

1061 

1062 if self._in_update_elements: 

1063 return 

1064 

1065 self._in_update_elements = True 

1066 for estate in self.state.elements: 

1067 if estate.element_id not in self._elements: 

1068 new_element = estate.create() 

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

1070 type(new_element).__name__, 

1071 estate.element_id)) 

1072 self._elements[estate.element_id] = new_element 

1073 

1074 element = self._elements[estate.element_id] 

1075 

1076 if estate.element_id not in self._elements_active: 

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

1078 type(element).__name__, 

1079 estate.element_id)) 

1080 element.bind_state(estate) 

1081 element.set_parent(self) 

1082 self._elements_active[estate.element_id] = element 

1083 

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

1085 deactivate = [] 

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

1087 if element_id not in state_element_ids: 

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

1089 type(element).__name__, 

1090 element_id)) 

1091 element.unset_parent() 

1092 deactivate.append(element_id) 

1093 

1094 for element_id in deactivate: 

1095 del self._elements_active[element_id] 

1096 

1097 self._update_crosshair_bindings() 

1098 

1099 self._in_update_elements = False 

1100 

1101 def _update_crosshair_bindings(self): 

1102 

1103 def get_crosshair_element(): 

1104 for element in self.state.elements: 

1105 if element.element_id == 'crosshair': 

1106 return element 

1107 

1108 return None 

1109 

1110 crosshair = get_crosshair_element() 

1111 if crosshair is None or crosshair.is_connected: 

1112 return 

1113 

1114 def to_checkbox(state, widget): 

1115 widget.blockSignals(True) 

1116 widget.setChecked(state.visible) 

1117 widget.blockSignals(False) 

1118 

1119 def to_state(widget, state): 

1120 state.visible = widget.isChecked() 

1121 

1122 cb = self._crosshair_checkbox 

1123 vstate.state_bind( 

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

1125 cb, [cb.toggled], to_checkbox) 

1126 

1127 crosshair.is_connected = True 

1128 

1129 def add_actor_2d(self, actor): 

1130 if actor not in self._actors_2d: 

1131 self.ren.AddActor2D(actor) 

1132 self._actors_2d.add(actor) 

1133 

1134 def remove_actor_2d(self, actor): 

1135 if actor in self._actors_2d: 

1136 self.ren.RemoveActor2D(actor) 

1137 self._actors_2d.remove(actor) 

1138 

1139 def add_actor(self, actor): 

1140 if actor not in self._actors: 

1141 self.ren.AddActor(actor) 

1142 self._actors.add(actor) 

1143 

1144 def add_actor_list(self, actorlist): 

1145 for actor in actorlist: 

1146 self.add_actor(actor) 

1147 

1148 def remove_actor(self, actor): 

1149 if actor in self._actors: 

1150 self.ren.RemoveActor(actor) 

1151 self._actors.remove(actor) 

1152 

1153 def update_view(self): 

1154 self.vtk_widget.update() 

1155 

1156 def resize_event(self, size_x, size_y): 

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

1158 

1159 def button_event(self, obj, event): 

1160 if event == "LeftButtonPressEvent": 

1161 self.rotating = True 

1162 elif event == "LeftButtonReleaseEvent": 

1163 self.rotating = False 

1164 

1165 def mouse_move_event(self, obj, event): 

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

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

1168 

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

1170 center_x = size_x / 2.0 

1171 center_y = size_y / 2.0 

1172 

1173 if self.rotating: 

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

1175 

1176 def myWheelEvent(self, event): 

1177 

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

1179 

1180 if angle > 200: 

1181 angle = 200 

1182 

1183 if angle < -200: 

1184 angle = -200 

1185 

1186 self.disable_capture() 

1187 try: 

1188 self.do_dolly(-angle/100.) 

1189 finally: 

1190 self.enable_capture(aggregate='distance') 

1191 

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

1193 

1194 dx = x0 - x 

1195 dy = y0 - y 

1196 

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

1198 focp = self.gui_state.focal_point 

1199 

1200 if focp == 'center': 

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

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

1203 

1204 lat = self.state.lat 

1205 lon = self.state.lon 

1206 factor = self.state.distance / 10.0 

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

1208 else: 

1209 lat = 90. - self.state.dip 

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

1211 factor = 0.5 

1212 factor_lat = 1.0 

1213 

1214 dlat = dy * factor 

1215 dlon = dx * factor * factor_lat 

1216 

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

1218 lon += dlon 

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

1220 

1221 if focp == 'center': 

1222 self.state.lat = float(lat) 

1223 self.state.lon = float(lon) 

1224 else: 

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

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

1227 

1228 def do_dolly(self, v): 

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

1230 

1231 def key_down_event(self, obj, event): 

1232 k = obj.GetKeyCode() 

1233 if k == 'f': 

1234 self.gui_state.next_focal_point() 

1235 

1236 elif k == 'r': 

1237 self.reset_strike_dip() 

1238 

1239 elif k == 'p': 

1240 print(self.state) 

1241 

1242 elif k == 'i': 

1243 for elem in self.state.elements: 

1244 if isinstance(elem, elements.IcosphereState): 

1245 elem.visible = not elem.visible 

1246 

1247 elif k == 'c': 

1248 for elem in self.state.elements: 

1249 if isinstance(elem, elements.CoastlinesState): 

1250 elem.visible = not elem.visible 

1251 

1252 elif k == 't': 

1253 if not any( 

1254 isinstance(elem, elements.TopoState) 

1255 for elem in self.state.elements): 

1256 

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

1258 else: 

1259 for elem in self.state.elements: 

1260 if isinstance(elem, elements.TopoState): 

1261 elem.visible = not elem.visible 

1262 

1263 elif k == ' ': 

1264 self.toggle_panel_visibility() 

1265 

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

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

1268 

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

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

1271 

1272 def controls_navigation(self): 

1273 frame = qw.QFrame(self) 

1274 frame.setSizePolicy( 

1275 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1276 layout = qw.QGridLayout() 

1277 frame.setLayout(layout) 

1278 

1279 # lat, lon, depth 

1280 

1281 layout.addWidget( 

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

1283 

1284 le = qw.QLineEdit() 

1285 le.setStatusTip( 

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

1287 'Focal point location.') 

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

1289 

1290 def lat_lon_depth_to_lineedit(state, widget): 

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

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

1293 

1294 def lineedit_to_lat_lon_depth(widget, state): 

1295 self.disable_capture() 

1296 try: 

1297 s = str(widget.text()) 

1298 choices = location_to_choices(s) 

1299 if len(choices) > 0: 

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

1301 choices[0].get_lat_lon_depth() 

1302 else: 

1303 raise NoLocationChoices(s) 

1304 

1305 finally: 

1306 self.enable_capture() 

1307 

1308 self._state_bind( 

1309 ['lat', 'lon', 'depth'], 

1310 lineedit_to_lat_lon_depth, 

1311 le, [le.editingFinished, le.returnPressed], 

1312 lat_lon_depth_to_lineedit) 

1313 

1314 self.lat_lon_lineedit = le 

1315 

1316 # focal point 

1317 

1318 cb = qw.QCheckBox('Fix') 

1319 cb.setStatusTip( 

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

1321 % g_modifier_key) 

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

1323 

1324 def focal_point_to_checkbox(state, widget): 

1325 widget.blockSignals(True) 

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

1327 widget.blockSignals(False) 

1328 

1329 def checkbox_to_focal_point(widget, state): 

1330 self.gui_state.focal_point = \ 

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

1332 

1333 self._gui_state_bind( 

1334 ['focal_point'], checkbox_to_focal_point, 

1335 cb, [cb.toggled], focal_point_to_checkbox) 

1336 

1337 self.focal_point_checkbox = cb 

1338 

1339 self.talkie_connect( 

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

1341 

1342 self.update_focal_point() 

1343 

1344 # strike, dip 

1345 

1346 layout.addWidget( 

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

1348 

1349 le = qw.QLineEdit() 

1350 le.setStatusTip( 

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

1352 'direction.') 

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

1354 

1355 def strike_dip_to_lineedit(state, widget): 

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

1357 

1358 def lineedit_to_strike_dip(widget, state): 

1359 s = str(widget.text()) 

1360 string_to_strike_dip = { 

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

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

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

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

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

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

1367 

1368 self.disable_capture() 

1369 if s in string_to_strike_dip: 

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

1371 

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

1373 try: 

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

1375 except Exception: 

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

1377 finally: 

1378 self.enable_capture() 

1379 

1380 self._state_bind( 

1381 ['strike', 'dip'], lineedit_to_strike_dip, 

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

1383 

1384 self.strike_dip_lineedit = le 

1385 

1386 but = qw.QPushButton('Reset') 

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

1388 but.clicked.connect(self.reset_strike_dip) 

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

1390 

1391 # crosshair 

1392 

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

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

1395 

1396 # camera bindings 

1397 self.talkie_connect( 

1398 self.state, 

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

1400 self.update_camera) 

1401 

1402 self.talkie_connect( 

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

1404 

1405 return frame 

1406 

1407 def controls_time(self): 

1408 frame = qw.QFrame(self) 

1409 frame.setSizePolicy( 

1410 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1411 

1412 layout = qw.QGridLayout() 

1413 frame.setLayout(layout) 

1414 

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

1416 le_tmin = qw.QLineEdit() 

1417 layout.addWidget(le_tmin, 0, 1) 

1418 

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

1420 le_tmax = qw.QLineEdit() 

1421 layout.addWidget(le_tmax, 1, 1) 

1422 

1423 label_tcursor = qw.QLabel() 

1424 

1425 label_tcursor.setSizePolicy( 

1426 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1427 

1428 layout.addWidget(label_tcursor, 2, 1) 

1429 self._label_tcursor = label_tcursor 

1430 

1431 def time_to_lineedit(state, attribute, widget): 

1432 widget.setText( 

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

1434 

1435 def lineedit_to_time(widget, state, attribute): 

1436 from pyrocko.util import str_to_time_fillup 

1437 

1438 s = str(widget.text()) 

1439 if not s.strip(): 

1440 setattr(state, attribute, None) 

1441 else: 

1442 try: 

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

1444 except Exception: 

1445 raise ValueError( 

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

1447 

1448 self._state_bind( 

1449 ['tmin'], lineedit_to_time, le_tmin, 

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

1451 attribute='tmin') 

1452 self._state_bind( 

1453 ['tmax'], lineedit_to_time, le_tmax, 

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

1455 attribute='tmax') 

1456 

1457 self.tmin_lineedit = le_tmin 

1458 self.tmax_lineedit = le_tmax 

1459 

1460 range_edit = RangeEdit() 

1461 range_edit.rangeEditPressed.connect(self.disable_capture) 

1462 range_edit.rangeEditReleased.connect(self.enable_capture) 

1463 range_edit.set_data_provider(self) 

1464 range_edit.set_data_name('time') 

1465 

1466 xblock = [False] 

1467 

1468 def range_to_range_edit(state, widget): 

1469 if not xblock[0]: 

1470 widget.blockSignals(True) 

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

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

1473 widget.blockSignals(False) 

1474 

1475 def range_edit_to_range(widget, state): 

1476 xblock[0] = True 

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

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

1479 xblock[0] = False 

1480 

1481 self._state_bind( 

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

1483 range_edit_to_range, 

1484 range_edit, 

1485 [range_edit.rangeChanged, range_edit.focusChanged], 

1486 range_to_range_edit) 

1487 

1488 def handle_tcursor_changed(): 

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

1490 

1491 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1492 

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

1494 

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

1496 le_focus = qw.QLineEdit() 

1497 layout.addWidget(le_focus, 4, 1) 

1498 

1499 def focus_to_lineedit(state, widget): 

1500 if state.tduration is None: 

1501 widget.setText('') 

1502 else: 

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

1504 guts.str_duration(state.tduration), 

1505 state.tposition)) 

1506 

1507 def lineedit_to_focus(widget, state): 

1508 s = str(widget.text()) 

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

1510 try: 

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

1512 state.tduration = None 

1513 state.tposition = 0.0 

1514 else: 

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

1516 if len(w) > 1: 

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

1518 else: 

1519 state.tposition = 0.0 

1520 

1521 except Exception: 

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

1523 

1524 self._state_bind( 

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

1526 [le_focus.editingFinished, le_focus.returnPressed], 

1527 focus_to_lineedit) 

1528 

1529 label_effective_tmin = qw.QLabel() 

1530 label_effective_tmax = qw.QLabel() 

1531 

1532 label_effective_tmin.setSizePolicy( 

1533 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1534 label_effective_tmax.setSizePolicy( 

1535 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1536 label_effective_tmin.setMinimumSize( 

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

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

1539 

1540 layout.addWidget(label_effective_tmin, 5, 1) 

1541 layout.addWidget(label_effective_tmax, 6, 1) 

1542 

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

1544 self.talkie_connect( 

1545 self.state, var, self.update_effective_time_labels) 

1546 

1547 self._label_effective_tmin = label_effective_tmin 

1548 self._label_effective_tmax = label_effective_tmax 

1549 

1550 self.talkie_connect( 

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

1552 

1553 return frame 

1554 

1555 def controls_appearance(self): 

1556 frame = qw.QFrame(self) 

1557 frame.setSizePolicy( 

1558 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1559 layout = qw.QGridLayout() 

1560 frame.setLayout(layout) 

1561 

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

1563 

1564 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1565 layout.addWidget(cb, 0, 1) 

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

1567 

1568 self.talkie_connect( 

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

1570 

1571 # background 

1572 

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

1574 

1575 cb = common.strings_to_combobox( 

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

1577 

1578 layout.addWidget(cb, 1, 1) 

1579 vstate.state_bind_combobox_background( 

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

1581 

1582 self.talkie_connect( 

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

1584 

1585 return frame 

1586 

1587 def controls_snapshots(self): 

1588 return snapshots_mod.SnapshotsPanel(self) 

1589 

1590 def update_effective_time_labels(self, *args): 

1591 tmin = self.state.tmin_effective 

1592 tmax = self.state.tmax_effective 

1593 

1594 stmin = common.time_or_none_to_str(tmin) 

1595 stmax = common.time_or_none_to_str(tmax) 

1596 

1597 self._label_effective_tmin.setText(stmin) 

1598 self._label_effective_tmax.setText(stmax) 

1599 

1600 def update_tcursor(self, *args): 

1601 tcursor = self.gui_state.tcursor 

1602 stcursor = common.time_or_none_to_str(tcursor) 

1603 self._label_tcursor.setText(stcursor) 

1604 

1605 def reset_strike_dip(self, *args): 

1606 self.state.strike = 90. 

1607 self.state.dip = 0 

1608 self.gui_state.focal_point = 'center' 

1609 

1610 def get_camera_geometry(self): 

1611 

1612 def rtp2xyz(rtp): 

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

1614 

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

1616 

1617 cam_rtp = num.array([ 

1618 radius+self.state.distance, 

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

1620 self.state.lon * d2r]) 

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

1622 cam, up, foc = \ 

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

1624 

1625 foc_rtp = num.array([ 

1626 radius, 

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

1628 self.state.lon * d2r]) 

1629 

1630 foc = rtp2xyz(foc_rtp) 

1631 

1632 rot_world = pmt.euler_to_matrix( 

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

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

1635 0.0*d2r).T 

1636 

1637 rot_cam = pmt.euler_to_matrix( 

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

1639 

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

1641 

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

1643 up = num.dot(rot, up) 

1644 return cam, up, foc 

1645 

1646 def update_camera(self, *args): 

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

1648 camera = self.ren.GetActiveCamera() 

1649 camera.SetPosition(*cam) 

1650 camera.SetFocalPoint(*foc) 

1651 camera.SetViewUp(*up) 

1652 

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

1654 

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

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

1657 

1658 # if horizon == 0.0: 

1659 # horizon = 2.0 + self.state.distance 

1660 

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

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

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

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

1665 # clip_dist = feature_horizon 

1666 

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

1668 

1669 self.camera_params = ( 

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

1671 

1672 self.update_view() 

1673 

1674 def add_panel( 

1675 self, title_label, panel, 

1676 visible=False, 

1677 # volatile=False, 

1678 tabify=True, 

1679 where=qc.Qt.RightDockWidgetArea, 

1680 remove=None, 

1681 title_controls=[]): 

1682 

1683 dockwidget = common.MyDockWidget( 

1684 self, title_label, title_controls=title_controls) 

1685 

1686 if not visible: 

1687 dockwidget.hide() 

1688 

1689 if not self.gui_state.panels_visible: 

1690 dockwidget.block() 

1691 

1692 dockwidget.setWidget(panel) 

1693 

1694 panel.setParent(dockwidget) 

1695 

1696 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1698 

1699 self.addDockWidget(where, dockwidget) 

1700 

1701 nwrap = 4 

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

1703 self.tabifyDockWidget( 

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

1705 

1706 mitem = dockwidget.toggleViewAction() 

1707 

1708 def update_label(*args): 

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

1710 self.update_slug_abbreviated_lengths() 

1711 

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

1713 dockwidget.titlebar._title_label.title_changed.connect( 

1714 self.update_slug_abbreviated_lengths) 

1715 

1716 update_label() 

1717 

1718 self._panel_togglers[dockwidget] = mitem 

1719 self.panels_menu.addAction(mitem) 

1720 if visible: 

1721 dockwidget.setVisible(True) 

1722 dockwidget.setFocus() 

1723 dockwidget.raise_() 

1724 

1725 def stack_panels(self): 

1726 dockwidgets = self.findChildren(common.MyDockWidget) 

1727 by_area = defaultdict(list) 

1728 for dw in dockwidgets: 

1729 area = self.dockWidgetArea(dw) 

1730 by_area[area].append(dw) 

1731 

1732 for dockwidgets in by_area.values(): 

1733 dw_last = None 

1734 for dw in dockwidgets: 

1735 if dw_last is not None: 

1736 self.tabifyDockWidget(dw_last, dw) 

1737 

1738 dw_last = dw 

1739 

1740 def update_slug_abbreviated_lengths(self): 

1741 dockwidgets = self.findChildren(common.MyDockWidget) 

1742 title_labels = [] 

1743 for dw in dockwidgets: 

1744 title_labels.append(dw.titlebar._title_label) 

1745 

1746 by_title = defaultdict(list) 

1747 for tl in title_labels: 

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

1749 

1750 for group in by_title.values(): 

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

1752 

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

1754 nunique = len(set(slugs)) 

1755 

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

1757 n -= 1 

1758 

1759 if n > 0: 

1760 n = max(3, n) 

1761 

1762 for tl in group: 

1763 tl.set_slug_abbreviated_length(n) 

1764 

1765 def raise_panel(self, panel): 

1766 dockwidget = panel.parent() 

1767 dockwidget.setVisible(True) 

1768 dockwidget.setFocus() 

1769 dockwidget.raise_() 

1770 

1771 def toggle_panel_visibility(self): 

1772 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1773 

1774 def update_panel_visibility(self, *args): 

1775 self.setUpdatesEnabled(False) 

1776 mbar = self.menuBar() 

1777 sbar = self.statusBar() 

1778 dockwidgets = self.findChildren(common.MyDockWidget) 

1779 

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

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

1782 # objects. 

1783 mbar.setFixedHeight( 

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

1785 

1786 sbar.setVisible(self.gui_state.panels_visible) 

1787 for dockwidget in dockwidgets: 

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

1789 

1790 self.setUpdatesEnabled(True) 

1791 

1792 def remove_panel(self, panel): 

1793 dockwidget = panel.parent() 

1794 self.removeDockWidget(dockwidget) 

1795 dockwidget.setParent(None) 

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

1797 

1798 def register_data_provider(self, provider): 

1799 if provider not in self.data_providers: 

1800 self.data_providers.append(provider) 

1801 

1802 def unregister_data_provider(self, provider): 

1803 if provider in self.data_providers: 

1804 self.data_providers.remove(provider) 

1805 

1806 def iter_data(self, name): 

1807 for provider in self.data_providers: 

1808 for data in provider.iter_data(name): 

1809 yield data 

1810 

1811 def confirm_close(self): 

1812 ret = qw.QMessageBox.question( 

1813 self, 

1814 'Sparrow', 

1815 'Close Sparrow window?', 

1816 qw.QMessageBox.Cancel | qw.QMessageBox.Ok, 

1817 qw.QMessageBox.Ok) 

1818 

1819 return ret == qw.QMessageBox.Ok 

1820 

1821 def closeEvent(self, event): 

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

1823 self.attach() 

1824 self.closing = True 

1825 event.accept() 

1826 else: 

1827 event.ignore() 

1828 

1829 def is_closing(self): 

1830 return self.closing 

1831 

1832 

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

1834 

1835 from pyrocko import util 

1836 from pyrocko.gui import util as gui_util 

1837 from . import common 

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

1839 

1840 global win 

1841 

1842 app = gui_util.get_app() 

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

1844 app.set_main_window(win) 

1845 

1846 gui_util.app.install_sigint_handler() 

1847 

1848 try: 

1849 gui_util.app.exec_() 

1850 finally: 

1851 gui_util.app.uninstall_sigint_handler() 

1852 app.unset_main_window() 

1853 common.set_viewer(None) 

1854 del win 

1855 gc.collect()