1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6import math 

7import signal 

8import gc 

9import logging 

10import time 

11import tempfile 

12import os 

13import shutil 

14import platform 

15from collections import defaultdict 

16from subprocess import check_call 

17 

18import numpy as num 

19 

20from pyrocko import cake 

21from pyrocko import guts 

22from pyrocko import geonames 

23from pyrocko import config 

24from pyrocko import moment_tensor as pmt 

25from pyrocko import util 

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 def __init__(self, use_depth_peeling=True, events=None, snapshots=None): 

288 qw.QMainWindow.__init__(self) 

289 TalkieConnectionOwner.__init__(self) 

290 

291 common.get_app().set_main_window(self) 

292 

293 self.state = vstate.ViewerState() 

294 self.gui_state = vstate.ViewerGuiState() 

295 

296 self.setWindowTitle('Sparrow') 

297 

298 self.setTabPosition( 

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

300 

301 self.planet_radius = cake.earthradius 

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

303 

304 self._block_capture = 0 

305 self._undo_stack = [] 

306 self._redo_stack = [] 

307 self._undo_aggregate = None 

308 

309 self._panel_togglers = {} 

310 self._actors = set() 

311 self._actors_2d = set() 

312 self._render_window_size = (0, 0) 

313 self._use_depth_peeling = use_depth_peeling 

314 self._in_update_elements = False 

315 self._update_elements_enabled = True 

316 

317 self._animation_tstart = None 

318 self._animation_iframe = None 

319 self._animation = None 

320 

321 mbar = qw.QMenuBar() 

322 self.setMenuBar(mbar) 

323 

324 menu = mbar.addMenu('File') 

325 

326 menu.addAction( 

327 'Export Image...', 

328 self.export_image, 

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

330 qc.Qt.ApplicationShortcut) 

331 

332 menu.addAction( 

333 'Quit', 

334 self.request_quit, 

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

336 qc.Qt.ApplicationShortcut) 

337 

338 menu = mbar.addMenu('Edit') 

339 

340 menu.addAction( 

341 'Undo', 

342 self.undo, 

343 qg.QKeySequence( 

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

345 qc.Qt.ApplicationShortcut) 

346 

347 menu.addAction( 

348 'Redo', 

349 self.redo, 

350 qg.QKeySequence( 

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

352 qc.Qt.ApplicationShortcut) 

353 

354 menu = mbar.addMenu('View') 

355 menu_sizes = menu.addMenu('Size') 

356 self._add_vtk_widget_size_menu_entries(menu_sizes) 

357 

358 # detached/attached 

359 self.talkie_connect( 

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

361 

362 action = qw.QAction('Detach') 

363 action.setCheckable(True) 

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

365 action.setShortcutContext(qc.Qt.ApplicationShortcut) 

366 

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

368 menu.addAction(action) 

369 

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

371 self.panels_menu.addAction( 

372 'Stack Panels', 

373 self.stack_panels) 

374 self.panels_menu.addSeparator() 

375 

376 snapshots_menu = mbar.addMenu('Snapshots') 

377 

378 menu = mbar.addMenu('Elements') 

379 for name, estate in sorted([ 

380 ('Icosphere', elements.IcosphereState( 

381 level=4, 

382 smooth=True, 

383 opacity=0.5, 

384 ambient=0.1)), 

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

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

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

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

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

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

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

392 ('HUD Subtitle', elements.HudState( 

393 template='Subtitle')), 

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

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

396 position='top-left')), 

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

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

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

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

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

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

403 

404 def wrap_add_element(estate): 

405 def add_element(*args): 

406 new_element = guts.clone(estate) 

407 new_element.element_id = elements.random_id() 

408 self.state.elements.append(new_element) 

409 self.state.sort_elements() 

410 

411 return add_element 

412 

413 mitem = qw.QAction(name, self) 

414 

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

416 

417 menu.addAction(mitem) 

418 

419 menu = mbar.addMenu('Help') 

420 

421 menu.addAction( 

422 'Interactive Tour', 

423 self.start_tour) 

424 

425 menu.addAction( 

426 'Online Manual', 

427 self.open_manual) 

428 

429 self.data_providers = [] 

430 self.elements = {} 

431 

432 self.detached_window = None 

433 

434 self.main_frame = qw.QFrame() 

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

436 

437 self.vtk_frame = CenteringScrollArea() 

438 

439 self.vtk_widget = QVTKWidget(self, self) 

440 self.vtk_frame.setWidget(self.vtk_widget) 

441 

442 self.main_layout = qw.QVBoxLayout() 

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

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

445 

446 pb = Progressbars(self) 

447 self.progressbars = pb 

448 self.main_layout.addWidget(pb) 

449 

450 self.main_frame.setLayout(self.main_layout) 

451 

452 self.vtk_frame_substitute = None 

453 

454 self.add_panel( 

455 'Navigation', 

456 self.controls_navigation(), visible=True, 

457 where=qc.Qt.LeftDockWidgetArea) 

458 

459 self.add_panel( 

460 'Time', 

461 self.controls_time(), visible=True, 

462 where=qc.Qt.LeftDockWidgetArea) 

463 

464 self.add_panel( 

465 'Appearance', 

466 self.controls_appearance(), visible=True, 

467 where=qc.Qt.LeftDockWidgetArea) 

468 

469 snapshots_panel = self.controls_snapshots() 

470 self.snapshots_panel = snapshots_panel 

471 self.add_panel( 

472 'Snapshots', 

473 snapshots_panel, visible=False, 

474 where=qc.Qt.LeftDockWidgetArea) 

475 

476 snapshots_panel.setup_menu(snapshots_menu) 

477 

478 self.setCentralWidget(self.main_frame) 

479 

480 self.mesh = None 

481 

482 ren = vtk.vtkRenderer() 

483 

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

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

486 # ren.TwoSidedLightingOn() 

487 # ren.SetUseShadows(1) 

488 

489 self._lighting = None 

490 self._background = None 

491 

492 self.ren = ren 

493 self.update_render_settings() 

494 self.update_camera() 

495 

496 renwin = self.vtk_widget.GetRenderWindow() 

497 

498 if self._use_depth_peeling: 

499 renwin.SetAlphaBitPlanes(1) 

500 renwin.SetMultiSamples(0) 

501 

502 ren.SetUseDepthPeeling(1) 

503 ren.SetMaximumNumberOfPeels(100) 

504 ren.SetOcclusionRatio(0.1) 

505 

506 ren.SetUseFXAA(1) 

507 # ren.SetUseHiddenLineRemoval(1) 

508 # ren.SetBackingStore(1) 

509 

510 self.renwin = renwin 

511 

512 # renwin.LineSmoothingOn() 

513 # renwin.PointSmoothingOn() 

514 # renwin.PolygonSmoothingOn() 

515 

516 renwin.AddRenderer(ren) 

517 

518 iren = renwin.GetInteractor() 

519 iren.LightFollowCameraOn() 

520 iren.SetInteractorStyle(None) 

521 

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

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

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

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

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

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

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

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

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

531 

532 renwin.Render() 

533 

534 iren.Initialize() 

535 

536 self.iren = iren 

537 

538 self.rotating = False 

539 

540 self._elements = {} 

541 self._elements_active = {} 

542 

543 self.talkie_connect( 

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

545 

546 self.state.elements.append(elements.IcosphereState( 

547 element_id='icosphere', 

548 level=4, 

549 smooth=True, 

550 opacity=0.5, 

551 ambient=0.1)) 

552 

553 self.state.elements.append(elements.GridState( 

554 element_id='grid')) 

555 self.state.elements.append(elements.CoastlinesState( 

556 element_id='coastlines')) 

557 self.state.elements.append(elements.CrosshairState( 

558 element_id='crosshair')) 

559 

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

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

562 # self.state.elements.append( 

563 # elements.CatalogState( 

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

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

566 

567 if events: 

568 self.state.elements.append( 

569 elements.CatalogState( 

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

571 

572 self.state.sort_elements() 

573 

574 if snapshots: 

575 snapshots_ = [] 

576 for obj in snapshots: 

577 if isinstance(obj, str): 

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

579 else: 

580 snapshots_.append(obj) 

581 

582 snapshots_panel.add_snapshots(snapshots_) 

583 self.raise_panel(snapshots_panel) 

584 snapshots_panel.goto_snapshot(1) 

585 

586 self.timer = qc.QTimer(self) 

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

588 self.timer.setInterval(1000) 

589 self.timer.start() 

590 

591 self._animation_saver = None 

592 

593 self.closing = False 

594 self.vtk_widget.setFocus() 

595 

596 self.update_detached() 

597 

598 common.get_app().status('Pyrocko Sparrow - A bird\'s eye view.', 2.0) 

599 common.get_app().status('Let\'s fly.', 2.0) 

600 

601 self.show() 

602 self.windowHandle().showMaximized() 

603 

604 self.talkie_connect( 

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

606 

607 self.update_vtk_widget_size() 

608 

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

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

611 

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

613 self.capture_state() 

614 

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

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

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

618 

619 self.start_tour() 

620 

621 def disable_capture(self): 

622 self._block_capture += 1 

623 

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

625 

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

627 if self._block_capture > 0: 

628 self._block_capture -= 1 

629 

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

631 

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

633 self.capture_state(aggregate=aggregate) 

634 

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

636 if self._block_capture: 

637 return 

638 

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

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

641 

642 if aggregate is not None: 

643 if aggregate == self._undo_aggregate: 

644 self._undo_stack.pop() 

645 

646 self._undo_aggregate = aggregate 

647 else: 

648 self._undo_aggregate = None 

649 

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

651 len(self._undo_stack) + 1, 

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

653 '\n'.join( 

654 ' - %s' % s 

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

656 self.state).splitlines()) 

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

658 

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

660 self._redo_stack.clear() 

661 

662 def undo(self): 

663 self._undo_aggregate = None 

664 

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

666 return 

667 

668 state = self._undo_stack.pop() 

669 self._redo_stack.append(state) 

670 state = self._undo_stack[-1] 

671 

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

673 len(self._undo_stack), 

674 '\n'.join( 

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

676 

677 self.disable_capture() 

678 try: 

679 self.set_state(state) 

680 finally: 

681 self.enable_capture(drop=True) 

682 

683 def redo(self): 

684 self._undo_aggregate = None 

685 

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

687 return 

688 

689 state = self._redo_stack.pop() 

690 self._undo_stack.append(state) 

691 

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

693 len(self._redo_stack), 

694 '\n'.join( 

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

696 

697 self.disable_capture() 

698 try: 

699 self.set_state(state) 

700 finally: 

701 self.enable_capture(drop=True) 

702 

703 def start_tour(self): 

704 snapshots_ = snapshots_mod.load_snapshots( 

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

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

707 self.snapshots_panel.add_snapshots(snapshots_) 

708 self.raise_panel(self.snapshots_panel) 

709 self.snapshots_panel.transition_to_next_snapshot() 

710 

711 def open_manual(self): 

712 import webbrowser 

713 webbrowser.open( 

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

715 

716 def _add_vtk_widget_size_menu_entries(self, menu): 

717 

718 group = qw.QActionGroup(menu) 

719 group.setExclusive(True) 

720 

721 def set_variable_size(): 

722 self.gui_state.fixed_size = False 

723 

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

725 variable_size_action.setCheckable(True) 

726 variable_size_action.setActionGroup(group) 

727 variable_size_action.triggered.connect(set_variable_size) 

728 

729 fixed_size_items = [] 

730 for nx, ny, label in [ 

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

732 (426, 240, ''), 

733 (640, 360, ''), 

734 (854, 480, '(FWVGA)'), 

735 (1280, 720, '(HD)'), 

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

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

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

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

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

741 (640, 480, '(VGA)'), 

742 (800, 600, '(SVGA)'), 

743 (None, None, 'Other'), 

744 (512, 512, ''), 

745 (1024, 1024, '')]: 

746 

747 if None in (nx, ny): 

748 menu.addSection(label) 

749 else: 

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

751 action = menu.addAction(name) 

752 action.setCheckable(True) 

753 action.setActionGroup(group) 

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

755 

756 def make_set_fixed_size(nx, ny): 

757 def set_fixed_size(): 

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

759 

760 return set_fixed_size 

761 

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

763 

764 def update_widget(*args): 

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

766 action.blockSignals(True) 

767 action.setChecked( 

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

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

770 action.blockSignals(False) 

771 

772 variable_size_action.blockSignals(True) 

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

774 variable_size_action.blockSignals(False) 

775 

776 update_widget() 

777 self.talkie_connect( 

778 self.gui_state, 'fixed_size', update_widget) 

779 

780 def update_vtk_widget_size(self, *args): 

781 if self.gui_state.fixed_size: 

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

783 wanted_size = qc.QSize(nx, ny) 

784 else: 

785 wanted_size = qc.QSize( 

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

787 

788 current_size = self.vtk_widget.size() 

789 

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

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

792 

793 self.vtk_widget.setFixedSize(wanted_size) 

794 

795 self.vtk_frame.recenter() 

796 self.check_vtk_resize() 

797 

798 def update_focal_point(self, *args): 

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

800 self.vtk_widget.setStatusTip( 

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

802 'change view plane orientation.' % g_modifier_key) 

803 else: 

804 self.vtk_widget.setStatusTip( 

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

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

807 'reverse sense.' % g_modifier_key) 

808 

809 def update_detached(self, *args): 

810 

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

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

813 

814 self.main_layout.removeWidget(self.vtk_frame) 

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

816 self.detached_window.show() 

817 self.vtk_widget.setFocus() 

818 

819 screens = common.get_app().screens() 

820 if len(screens) > 1: 

821 for screen in screens: 

822 if screen is not self.screen(): 

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

824 # .setScreen() does not work reliably, 

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

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

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

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

829 

830 self.detached_window.windowHandle().showMaximized() 

831 

832 frame = qw.QFrame() 

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

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

835 # frame.setAutoFillBackground(True) 

836 frame.setSizePolicy( 

837 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

838 

839 layout = qw.QGridLayout() 

840 frame.setLayout(layout) 

841 self.main_layout.insertWidget(0, frame) 

842 

843 self.state_editor = StateEditor(self) 

844 

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

846 

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

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

849 # layout.addWidget( 

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

851 

852 self.vtk_frame_substitute = frame 

853 

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

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

856 self.detached_window.hide() 

857 self.vtk_frame.setParent(self) 

858 if self.vtk_frame_substitute: 

859 self.main_layout.removeWidget(self.vtk_frame_substitute) 

860 self.state_editor.unbind_state() 

861 self.vtk_frame_substitute = None 

862 

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

864 self.detached_window = None 

865 self.vtk_widget.setFocus() 

866 

867 def attach(self): 

868 self.gui_state.detached = False 

869 

870 def export_image(self): 

871 

872 caption = 'Export Image' 

873 fn_out, _ = qw.QFileDialog.getSaveFileName( 

874 self, caption, 'image.png', 

875 options=common.qfiledialog_options) 

876 

877 if fn_out: 

878 self.save_image(fn_out) 

879 

880 def save_image(self, path): 

881 

882 original_fixed_size = self.gui_state.fixed_size 

883 if original_fixed_size is None: 

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

885 

886 wif = vtk.vtkWindowToImageFilter() 

887 wif.SetInput(self.renwin) 

888 wif.SetInputBufferTypeToRGBA() 

889 wif.SetScale(1, 1) 

890 wif.ReadFrontBufferOff() 

891 writer = vtk.vtkPNGWriter() 

892 writer.SetInputConnection(wif.GetOutputPort()) 

893 

894 self.renwin.Render() 

895 wif.Modified() 

896 writer.SetFileName(path) 

897 writer.Write() 

898 

899 self.gui_state.fixed_size = original_fixed_size 

900 

901 def update_render_settings(self, *args): 

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

903 self.ren.RemoveAllLights() 

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

905 self.ren.AddLight(li) 

906 

907 self._lighting = self.state.lighting 

908 

909 if self._background is None \ 

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

911 

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

913 self._background = self.state.background 

914 

915 self.update_view() 

916 

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

918 if self._animation: 

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

920 self.stop_animation() 

921 

922 self.disable_capture() 

923 self._animation = interpolator 

924 if output_path is None: 

925 self._animation_tstart = time.time() 

926 self._animation_iframe = None 

927 else: 

928 self._animation_iframe = 0 

929 self.showFullScreen() 

930 self.update_view() 

931 self.gui_state.panels_visible = False 

932 self.update_view() 

933 

934 self._animation_timer = qc.QTimer(self) 

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

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

937 self._animation_timer.start() 

938 if output_path is not None: 

939 original_fixed_size = self.gui_state.fixed_size 

940 if original_fixed_size is None: 

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

942 

943 wif = vtk.vtkWindowToImageFilter() 

944 wif.SetInput(self.renwin) 

945 wif.SetInputBufferTypeToRGBA() 

946 wif.SetScale(1, 1) 

947 wif.ReadFrontBufferOff() 

948 writer = vtk.vtkPNGWriter() 

949 temp_path = tempfile.mkdtemp() 

950 self._animation_saver = ( 

951 wif, writer, temp_path, output_path, original_fixed_size) 

952 writer.SetInputConnection(wif.GetOutputPort()) 

953 

954 def next_animation_frame(self): 

955 

956 ani = self._animation 

957 if not ani: 

958 return 

959 

960 if self._animation_iframe is not None: 

961 state = ani( 

962 ani.tmin 

963 + self._animation_iframe * ani.dt) 

964 

965 self._animation_iframe += 1 

966 else: 

967 tnow = time.time() 

968 state = ani(min( 

969 ani.tmax, 

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

971 

972 self.set_state(state) 

973 self.renwin.Render() 

974 if self._animation_saver: 

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

976 wif.Modified() 

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

978 writer.SetFileName(fn % self._animation_iframe) 

979 writer.Write() 

980 

981 if self._animation_iframe is not None: 

982 t = self._animation_iframe * ani.dt 

983 else: 

984 t = tnow - self._animation_tstart 

985 

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

987 self.stop_animation() 

988 

989 def stop_animation(self): 

990 if self._animation_timer: 

991 self._animation_timer.stop() 

992 

993 if self._animation_saver: 

994 

995 wif, writer, temp_path, output_path, original_fixed_size \ 

996 = self._animation_saver 

997 self.gui_state.fixed_size = original_fixed_size 

998 

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

1000 check_call([ 

1001 'ffmpeg', '-y', 

1002 '-i', fn_path, 

1003 '-c:v', 'libx264', 

1004 '-preset', 'slow', 

1005 '-crf', '17', 

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

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

1008 output_path]) 

1009 shutil.rmtree(temp_path) 

1010 

1011 self._animation_saver = None 

1012 self._animation_saver 

1013 

1014 self.showNormal() 

1015 self.gui_state.panels_visible = True 

1016 

1017 self._animation_tstart = None 

1018 self._animation_iframe = None 

1019 self._animation = None 

1020 self.enable_capture() 

1021 

1022 def set_state(self, state): 

1023 self.disable_capture() 

1024 try: 

1025 self._update_elements_enabled = False 

1026 self.setUpdatesEnabled(False) 

1027 self.state.diff_update(state) 

1028 self.state.sort_elements() 

1029 self.setUpdatesEnabled(True) 

1030 self._update_elements_enabled = True 

1031 self.update_elements() 

1032 finally: 

1033 self.enable_capture() 

1034 

1035 def periodical(self): 

1036 pass 

1037 

1038 def request_quit(self): 

1039 app = common.get_app() 

1040 app.myQuit() 

1041 

1042 def check_vtk_resize(self, *args): 

1043 render_window_size = self.renwin.GetSize() 

1044 if self._render_window_size != render_window_size: 

1045 self._render_window_size = render_window_size 

1046 self.resize_event(*render_window_size) 

1047 

1048 def update_elements(self, *_): 

1049 if not self._update_elements_enabled: 

1050 return 

1051 

1052 if self._in_update_elements: 

1053 return 

1054 

1055 self._in_update_elements = True 

1056 for estate in self.state.elements: 

1057 if estate.element_id not in self._elements: 

1058 new_element = estate.create() 

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

1060 type(new_element).__name__, 

1061 estate.element_id)) 

1062 self._elements[estate.element_id] = new_element 

1063 

1064 element = self._elements[estate.element_id] 

1065 

1066 if estate.element_id not in self._elements_active: 

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

1068 type(element).__name__, 

1069 estate.element_id)) 

1070 element.bind_state(estate) 

1071 element.set_parent(self) 

1072 self._elements_active[estate.element_id] = element 

1073 

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

1075 deactivate = [] 

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

1077 if element_id not in state_element_ids: 

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

1079 type(element).__name__, 

1080 element_id)) 

1081 element.unset_parent() 

1082 deactivate.append(element_id) 

1083 

1084 for element_id in deactivate: 

1085 del self._elements_active[element_id] 

1086 

1087 self._update_crosshair_bindings() 

1088 

1089 self._in_update_elements = False 

1090 

1091 def _update_crosshair_bindings(self): 

1092 

1093 def get_crosshair_element(): 

1094 for element in self.state.elements: 

1095 if element.element_id == 'crosshair': 

1096 return element 

1097 

1098 return None 

1099 

1100 crosshair = get_crosshair_element() 

1101 if crosshair is None or crosshair.is_connected: 

1102 return 

1103 

1104 def to_checkbox(state, widget): 

1105 widget.blockSignals(True) 

1106 widget.setChecked(state.visible) 

1107 widget.blockSignals(False) 

1108 

1109 def to_state(widget, state): 

1110 state.visible = widget.isChecked() 

1111 

1112 cb = self._crosshair_checkbox 

1113 vstate.state_bind( 

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

1115 cb, [cb.toggled], to_checkbox) 

1116 

1117 crosshair.is_connected = True 

1118 

1119 def add_actor_2d(self, actor): 

1120 if actor not in self._actors_2d: 

1121 self.ren.AddActor2D(actor) 

1122 self._actors_2d.add(actor) 

1123 

1124 def remove_actor_2d(self, actor): 

1125 if actor in self._actors_2d: 

1126 self.ren.RemoveActor2D(actor) 

1127 self._actors_2d.remove(actor) 

1128 

1129 def add_actor(self, actor): 

1130 if actor not in self._actors: 

1131 self.ren.AddActor(actor) 

1132 self._actors.add(actor) 

1133 

1134 def add_actor_list(self, actorlist): 

1135 for actor in actorlist: 

1136 self.add_actor(actor) 

1137 

1138 def remove_actor(self, actor): 

1139 if actor in self._actors: 

1140 self.ren.RemoveActor(actor) 

1141 self._actors.remove(actor) 

1142 

1143 def update_view(self): 

1144 self.vtk_widget.update() 

1145 

1146 def resize_event(self, size_x, size_y): 

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

1148 

1149 def button_event(self, obj, event): 

1150 if event == "LeftButtonPressEvent": 

1151 self.rotating = True 

1152 elif event == "LeftButtonReleaseEvent": 

1153 self.rotating = False 

1154 

1155 def mouse_move_event(self, obj, event): 

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

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

1158 

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

1160 center_x = size_x / 2.0 

1161 center_y = size_y / 2.0 

1162 

1163 if self.rotating: 

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

1165 

1166 def myWheelEvent(self, event): 

1167 

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

1169 

1170 if angle > 200: 

1171 angle = 200 

1172 

1173 if angle < -200: 

1174 angle = -200 

1175 

1176 self.disable_capture() 

1177 try: 

1178 self.do_dolly(-angle/100.) 

1179 finally: 

1180 self.enable_capture(aggregate='distance') 

1181 

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

1183 

1184 dx = x0 - x 

1185 dy = y0 - y 

1186 

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

1188 focp = self.gui_state.focal_point 

1189 

1190 if focp == 'center': 

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

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

1193 

1194 lat = self.state.lat 

1195 lon = self.state.lon 

1196 factor = self.state.distance / 10.0 

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

1198 else: 

1199 lat = 90. - self.state.dip 

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

1201 factor = 0.5 

1202 factor_lat = 1.0 

1203 

1204 dlat = dy * factor 

1205 dlon = dx * factor * factor_lat 

1206 

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

1208 lon += dlon 

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

1210 

1211 if focp == 'center': 

1212 self.state.lat = float(lat) 

1213 self.state.lon = float(lon) 

1214 else: 

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

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

1217 

1218 def do_dolly(self, v): 

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

1220 

1221 def key_down_event(self, obj, event): 

1222 k = obj.GetKeyCode() 

1223 if k == 'f': 

1224 self.gui_state.next_focal_point() 

1225 

1226 elif k == 'r': 

1227 self.reset_strike_dip() 

1228 

1229 elif k == 'p': 

1230 print(self.state) 

1231 

1232 elif k == 'i': 

1233 for elem in self.state.elements: 

1234 if isinstance(elem, elements.IcosphereState): 

1235 elem.visible = not elem.visible 

1236 

1237 elif k == 'c': 

1238 for elem in self.state.elements: 

1239 if isinstance(elem, elements.CoastlinesState): 

1240 elem.visible = not elem.visible 

1241 

1242 elif k == 't': 

1243 if not any( 

1244 isinstance(elem, elements.TopoState) 

1245 for elem in self.state.elements): 

1246 

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

1248 else: 

1249 for elem in self.state.elements: 

1250 if isinstance(elem, elements.TopoState): 

1251 elem.visible = not elem.visible 

1252 

1253 elif k == ' ': 

1254 self.toggle_panel_visibility() 

1255 

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

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

1258 

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

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

1261 

1262 def controls_navigation(self): 

1263 frame = qw.QFrame(self) 

1264 frame.setSizePolicy( 

1265 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1266 layout = qw.QGridLayout() 

1267 frame.setLayout(layout) 

1268 

1269 # lat, lon, depth 

1270 

1271 layout.addWidget( 

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

1273 

1274 le = qw.QLineEdit() 

1275 le.setStatusTip( 

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

1277 'Focal point location.') 

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

1279 

1280 def lat_lon_depth_to_lineedit(state, widget): 

1281 sel = str(widget.selectedText()) == str(widget.text()) 

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

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

1284 

1285 if sel: 

1286 widget.selectAll() 

1287 

1288 def lineedit_to_lat_lon_depth(widget, state): 

1289 self.disable_capture() 

1290 try: 

1291 s = str(widget.text()) 

1292 choices = location_to_choices(s) 

1293 if len(choices) > 0: 

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

1295 choices[0].get_lat_lon_depth() 

1296 else: 

1297 raise NoLocationChoices(s) 

1298 

1299 finally: 

1300 self.enable_capture() 

1301 

1302 self._state_bind( 

1303 ['lat', 'lon', 'depth'], 

1304 lineedit_to_lat_lon_depth, 

1305 le, [le.editingFinished, le.returnPressed], 

1306 lat_lon_depth_to_lineedit) 

1307 

1308 self.lat_lon_lineedit = le 

1309 

1310 self.lat_lon_lineedit.returnPressed.connect( 

1311 lambda *args: self.lat_lon_lineedit.selectAll()) 

1312 

1313 # focal point 

1314 

1315 cb = qw.QCheckBox('Fix') 

1316 cb.setStatusTip( 

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

1318 % g_modifier_key) 

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

1320 

1321 def focal_point_to_checkbox(state, widget): 

1322 widget.blockSignals(True) 

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

1324 widget.blockSignals(False) 

1325 

1326 def checkbox_to_focal_point(widget, state): 

1327 self.gui_state.focal_point = \ 

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

1329 

1330 self._gui_state_bind( 

1331 ['focal_point'], checkbox_to_focal_point, 

1332 cb, [cb.toggled], focal_point_to_checkbox) 

1333 

1334 self.focal_point_checkbox = cb 

1335 

1336 self.talkie_connect( 

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

1338 

1339 self.update_focal_point() 

1340 

1341 # strike, dip 

1342 

1343 layout.addWidget( 

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

1345 

1346 le = qw.QLineEdit() 

1347 le.setStatusTip( 

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

1349 'direction.') 

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

1351 

1352 def strike_dip_to_lineedit(state, widget): 

1353 sel = widget.selectedText() == widget.text() 

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

1355 if sel: 

1356 widget.selectAll() 

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 self.strike_dip_lineedit.returnPressed.connect( 

1386 lambda *args: self.strike_dip_lineedit.selectAll()) 

1387 

1388 but = qw.QPushButton('Reset') 

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

1390 but.clicked.connect(self.reset_strike_dip) 

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

1392 

1393 # crosshair 

1394 

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

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

1397 

1398 # camera bindings 

1399 self.talkie_connect( 

1400 self.state, 

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

1402 self.update_camera) 

1403 

1404 self.talkie_connect( 

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

1406 

1407 return frame 

1408 

1409 def controls_time(self): 

1410 frame = qw.QFrame(self) 

1411 frame.setSizePolicy( 

1412 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1413 

1414 layout = qw.QGridLayout() 

1415 frame.setLayout(layout) 

1416 

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

1418 le_tmin = qw.QLineEdit() 

1419 layout.addWidget(le_tmin, 0, 1) 

1420 

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

1422 le_tmax = qw.QLineEdit() 

1423 layout.addWidget(le_tmax, 1, 1) 

1424 

1425 label_tcursor = qw.QLabel() 

1426 

1427 label_tcursor.setSizePolicy( 

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

1429 

1430 layout.addWidget(label_tcursor, 2, 1) 

1431 self._label_tcursor = label_tcursor 

1432 

1433 def time_to_lineedit(state, attribute, widget): 

1434 sel = widget.selectedText() == widget.text() \ 

1435 and widget.text() != '' 

1436 

1437 widget.setText( 

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

1439 

1440 if sel: 

1441 widget.selectAll() 

1442 

1443 def lineedit_to_time(widget, state, attribute): 

1444 from pyrocko.util import str_to_time_fillup 

1445 

1446 s = str(widget.text()) 

1447 if not s.strip(): 

1448 setattr(state, attribute, None) 

1449 else: 

1450 try: 

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

1452 except Exception: 

1453 raise ValueError( 

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

1455 

1456 self._state_bind( 

1457 ['tmin'], lineedit_to_time, le_tmin, 

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

1459 attribute='tmin') 

1460 self._state_bind( 

1461 ['tmax'], lineedit_to_time, le_tmax, 

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

1463 attribute='tmax') 

1464 

1465 self.tmin_lineedit = le_tmin 

1466 self.tmax_lineedit = le_tmax 

1467 

1468 range_edit = RangeEdit() 

1469 range_edit.rangeEditPressed.connect(self.disable_capture) 

1470 range_edit.rangeEditReleased.connect(self.enable_capture) 

1471 range_edit.set_data_provider(self) 

1472 range_edit.set_data_name('time') 

1473 

1474 xblock = [False] 

1475 

1476 def range_to_range_edit(state, widget): 

1477 if not xblock[0]: 

1478 widget.blockSignals(True) 

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

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

1481 widget.blockSignals(False) 

1482 

1483 def range_edit_to_range(widget, state): 

1484 xblock[0] = True 

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

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

1487 xblock[0] = False 

1488 

1489 self._state_bind( 

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

1491 range_edit_to_range, 

1492 range_edit, 

1493 [range_edit.rangeChanged, range_edit.focusChanged], 

1494 range_to_range_edit) 

1495 

1496 def handle_tcursor_changed(): 

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

1498 

1499 range_edit.tcursorChanged.connect(handle_tcursor_changed) 

1500 

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

1502 

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

1504 le_focus = qw.QLineEdit() 

1505 layout.addWidget(le_focus, 4, 1) 

1506 

1507 def focus_to_lineedit(state, widget): 

1508 sel = widget.selectedText() == widget.text() \ 

1509 and widget.text() != '' 

1510 

1511 if state.tduration is None: 

1512 widget.setText('') 

1513 else: 

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

1515 guts.str_duration(state.tduration), 

1516 state.tposition)) 

1517 

1518 if sel: 

1519 widget.selectAll() 

1520 

1521 def lineedit_to_focus(widget, state): 

1522 s = str(widget.text()) 

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

1524 try: 

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

1526 state.tduration = None 

1527 state.tposition = 0.0 

1528 else: 

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

1530 if len(w) > 1: 

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

1532 else: 

1533 state.tposition = 0.0 

1534 

1535 except Exception: 

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

1537 

1538 self._state_bind( 

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

1540 [le_focus.editingFinished, le_focus.returnPressed], 

1541 focus_to_lineedit) 

1542 

1543 label_effective_tmin = qw.QLabel() 

1544 label_effective_tmax = qw.QLabel() 

1545 

1546 label_effective_tmin.setSizePolicy( 

1547 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1548 label_effective_tmax.setSizePolicy( 

1549 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1550 label_effective_tmin.setMinimumSize( 

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

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

1553 

1554 layout.addWidget(label_effective_tmin, 5, 1) 

1555 layout.addWidget(label_effective_tmax, 6, 1) 

1556 

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

1558 self.talkie_connect( 

1559 self.state, var, self.update_effective_time_labels) 

1560 

1561 self._label_effective_tmin = label_effective_tmin 

1562 self._label_effective_tmax = label_effective_tmax 

1563 

1564 self.talkie_connect( 

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

1566 

1567 return frame 

1568 

1569 def controls_appearance(self): 

1570 frame = qw.QFrame(self) 

1571 frame.setSizePolicy( 

1572 qw.QSizePolicy.Minimum, qw.QSizePolicy.Fixed) 

1573 layout = qw.QGridLayout() 

1574 frame.setLayout(layout) 

1575 

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

1577 

1578 cb = common.string_choices_to_combobox(vstate.LightingChoice) 

1579 layout.addWidget(cb, 0, 1) 

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

1581 

1582 self.talkie_connect( 

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

1584 

1585 # background 

1586 

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

1588 

1589 cb = common.strings_to_combobox( 

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

1591 

1592 layout.addWidget(cb, 1, 1) 

1593 vstate.state_bind_combobox_background( 

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

1595 

1596 self.talkie_connect( 

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

1598 

1599 return frame 

1600 

1601 def controls_snapshots(self): 

1602 return snapshots_mod.SnapshotsPanel(self) 

1603 

1604 def update_effective_time_labels(self, *args): 

1605 tmin = self.state.tmin_effective 

1606 tmax = self.state.tmax_effective 

1607 

1608 stmin = common.time_or_none_to_str(tmin) 

1609 stmax = common.time_or_none_to_str(tmax) 

1610 

1611 self._label_effective_tmin.setText(stmin) 

1612 self._label_effective_tmax.setText(stmax) 

1613 

1614 def update_tcursor(self, *args): 

1615 tcursor = self.gui_state.tcursor 

1616 stcursor = common.time_or_none_to_str(tcursor) 

1617 self._label_tcursor.setText(stcursor) 

1618 

1619 def reset_strike_dip(self, *args): 

1620 self.state.strike = 90. 

1621 self.state.dip = 0 

1622 self.gui_state.focal_point = 'center' 

1623 

1624 def get_camera_geometry(self): 

1625 

1626 def rtp2xyz(rtp): 

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

1628 

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

1630 

1631 cam_rtp = num.array([ 

1632 radius+self.state.distance, 

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

1634 self.state.lon * d2r]) 

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

1636 cam, up, foc = \ 

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

1638 

1639 foc_rtp = num.array([ 

1640 radius, 

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

1642 self.state.lon * d2r]) 

1643 

1644 foc = rtp2xyz(foc_rtp) 

1645 

1646 rot_world = pmt.euler_to_matrix( 

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

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

1649 0.0*d2r).T 

1650 

1651 rot_cam = pmt.euler_to_matrix( 

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

1653 

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

1655 

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

1657 up = num.dot(rot, up) 

1658 return cam, up, foc 

1659 

1660 def update_camera(self, *args): 

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

1662 camera = self.ren.GetActiveCamera() 

1663 camera.SetPosition(*cam) 

1664 camera.SetFocalPoint(*foc) 

1665 camera.SetViewUp(*up) 

1666 

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

1668 

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

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

1671 

1672 # if horizon == 0.0: 

1673 # horizon = 2.0 + self.state.distance 

1674 

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

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

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

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

1679 # clip_dist = feature_horizon 

1680 

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

1682 

1683 self.camera_params = ( 

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

1685 

1686 self.update_view() 

1687 

1688 def add_panel( 

1689 self, title_label, panel, 

1690 visible=False, 

1691 # volatile=False, 

1692 tabify=True, 

1693 where=qc.Qt.RightDockWidgetArea, 

1694 remove=None, 

1695 title_controls=[]): 

1696 

1697 dockwidget = common.MyDockWidget( 

1698 self, title_label, title_controls=title_controls) 

1699 

1700 if not visible: 

1701 dockwidget.hide() 

1702 

1703 if not self.gui_state.panels_visible: 

1704 dockwidget.block() 

1705 

1706 dockwidget.setWidget(panel) 

1707 

1708 panel.setParent(dockwidget) 

1709 

1710 dockwidgets = self.findChildren(common.MyDockWidget) 

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

1712 

1713 self.addDockWidget(where, dockwidget) 

1714 

1715 nwrap = 4 

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

1717 self.tabifyDockWidget( 

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

1719 

1720 mitem = dockwidget.toggleViewAction() 

1721 

1722 def update_label(*args): 

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

1724 self.update_slug_abbreviated_lengths() 

1725 

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

1727 dockwidget.titlebar._title_label.title_changed.connect( 

1728 self.update_slug_abbreviated_lengths) 

1729 

1730 update_label() 

1731 

1732 self._panel_togglers[dockwidget] = mitem 

1733 self.panels_menu.addAction(mitem) 

1734 if visible: 

1735 dockwidget.setVisible(True) 

1736 dockwidget.setFocus() 

1737 dockwidget.raise_() 

1738 

1739 def stack_panels(self): 

1740 dockwidgets = self.findChildren(common.MyDockWidget) 

1741 by_area = defaultdict(list) 

1742 for dw in dockwidgets: 

1743 area = self.dockWidgetArea(dw) 

1744 by_area[area].append(dw) 

1745 

1746 for dockwidgets in by_area.values(): 

1747 dw_last = None 

1748 for dw in dockwidgets: 

1749 if dw_last is not None: 

1750 self.tabifyDockWidget(dw_last, dw) 

1751 

1752 dw_last = dw 

1753 

1754 def update_slug_abbreviated_lengths(self): 

1755 dockwidgets = self.findChildren(common.MyDockWidget) 

1756 title_labels = [] 

1757 for dw in dockwidgets: 

1758 title_labels.append(dw.titlebar._title_label) 

1759 

1760 by_title = defaultdict(list) 

1761 for tl in title_labels: 

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

1763 

1764 for group in by_title.values(): 

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

1766 

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

1768 nunique = len(set(slugs)) 

1769 

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

1771 n -= 1 

1772 

1773 if n > 0: 

1774 n = max(3, n) 

1775 

1776 for tl in group: 

1777 tl.set_slug_abbreviated_length(n) 

1778 

1779 def raise_panel(self, panel): 

1780 dockwidget = panel.parent() 

1781 dockwidget.setVisible(True) 

1782 dockwidget.setFocus() 

1783 dockwidget.raise_() 

1784 

1785 def toggle_panel_visibility(self): 

1786 self.gui_state.panels_visible = not self.gui_state.panels_visible 

1787 

1788 def update_panel_visibility(self, *args): 

1789 self.setUpdatesEnabled(False) 

1790 mbar = self.menuBar() 

1791 sbar = self.statusBar() 

1792 dockwidgets = self.findChildren(common.MyDockWidget) 

1793 

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

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

1796 # objects. 

1797 mbar.setFixedHeight( 

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

1799 

1800 sbar.setVisible(self.gui_state.panels_visible) 

1801 for dockwidget in dockwidgets: 

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

1803 

1804 self.setUpdatesEnabled(True) 

1805 

1806 def remove_panel(self, panel): 

1807 dockwidget = panel.parent() 

1808 self.removeDockWidget(dockwidget) 

1809 dockwidget.setParent(None) 

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

1811 

1812 def register_data_provider(self, provider): 

1813 if provider not in self.data_providers: 

1814 self.data_providers.append(provider) 

1815 

1816 def unregister_data_provider(self, provider): 

1817 if provider in self.data_providers: 

1818 self.data_providers.remove(provider) 

1819 

1820 def iter_data(self, name): 

1821 for provider in self.data_providers: 

1822 for data in provider.iter_data(name): 

1823 yield data 

1824 

1825 def closeEvent(self, event): 

1826 self.attach() 

1827 event.accept() 

1828 self.closing = True 

1829 common.get_app().set_main_window(None) 

1830 

1831 def is_closing(self): 

1832 return self.closing 

1833 

1834 

1835class SparrowApp(qw.QApplication): 

1836 def __init__(self): 

1837 qw.QApplication.__init__(self, ['Sparrow']) 

1838 self.lastWindowClosed.connect(self.myQuit) 

1839 self._main_window = None 

1840 self.setApplicationDisplayName('Sparrow') 

1841 self.setDesktopFileName('Sparrow') 

1842 

1843 def install_sigint_handler(self): 

1844 self._old_signal_handler = signal.signal( 

1845 signal.SIGINT, self.myCloseAllWindows) 

1846 

1847 def uninstall_sigint_handler(self): 

1848 signal.signal(signal.SIGINT, self._old_signal_handler) 

1849 

1850 def myQuit(self, *args): 

1851 self.quit() 

1852 

1853 def myCloseAllWindows(self, *args): 

1854 self.closeAllWindows() 

1855 

1856 def set_main_window(self, win): 

1857 self._main_window = win 

1858 

1859 def get_main_window(self): 

1860 return self._main_window 

1861 

1862 def get_progressbars(self): 

1863 if self._main_window: 

1864 return self._main_window.progressbars 

1865 else: 

1866 return None 

1867 

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

1869 win = self.get_main_window() 

1870 if not win: 

1871 return 

1872 

1873 win.statusBar().showMessage( 

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

1875 

1876 

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

1878 

1879 from pyrocko import util 

1880 from pyrocko.gui import util as gui_util 

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

1882 

1883 global win 

1884 

1885 if gui_util.app is None: 

1886 gui_util.app = SparrowApp() 

1887 

1888 # try: 

1889 # from qt_material import apply_stylesheet 

1890 # 

1891 # apply_stylesheet(app, theme='dark_teal.xml') 

1892 # 

1893 # 

1894 # import qdarkgraystyle 

1895 # app.setStyleSheet(qdarkgraystyle.load_stylesheet()) 

1896 # import qdarkstyle 

1897 # 

1898 # app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 

1899 # 

1900 # 

1901 # except ImportError: 

1902 # logger.info( 

1903 # 'Module qdarkgraystyle not available.\n' 

1904 # 'If wanted, install qdarkstyle with "pip install ' 

1905 # 'qdarkgraystyle".') 

1906 # 

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

1908 

1909 gui_util.app.install_sigint_handler() 

1910 gui_util.app.exec_() 

1911 gui_util.app.uninstall_sigint_handler() 

1912 

1913 del win 

1914 

1915 gc.collect() 

1916 

1917 del gui_util.app