Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffling.py: 45%

849 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-02-27 10:58 +0000

1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5 

6''' 

7Snuffling infrastructure 

8 

9This module provides the base class :py:class:`Snuffling` for user-defined 

10snufflings and some utilities for their handling. 

11''' 

12 

13import os 

14import sys 

15import time 

16import logging 

17import traceback 

18import tempfile 

19 

20from ..qt_compat import qc, qw, getSaveFileName 

21 

22from pyrocko import pile, config 

23from pyrocko.util import quote 

24 

25from ..util import ( 

26 ValControl, LinValControl, FigureFrame, SmartplotFrame, WebKitFrame, 

27 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker, load_markers, 

28 save_markers) 

29 

30from importlib import reload 

31 

32Marker, load_markers, save_markers # noqa 

33 

34logger = logging.getLogger('pyrocko.gui.snuffler.snuffling') 

35 

36 

37class MyFrame(qw.QFrame): 

38 widgetVisibilityChanged = qc.pyqtSignal(bool) 

39 

40 def showEvent(self, ev): 

41 '' 

42 self.widgetVisibilityChanged.emit(True) 

43 

44 def hideEvent(self, ev): 

45 '' 

46 self.widgetVisibilityChanged.emit(False) 

47 

48 

49class Param(object): 

50 ''' 

51 Definition of an adjustable floating point parameter for the 

52 snuffling. The snuffling may display controls for user input for 

53 such parameters. 

54 

55 :param name: labels the parameter on the snuffling's control panel 

56 :param ident: identifier of the parameter 

57 :param default: default value 

58 :param minimum: minimum value for the parameter 

59 :param maximum: maximum value for the parameter 

60 :param low_is_none: if ``True``: parameter is set to None at lowest value 

61 of parameter range (optional) 

62 :param high_is_none: if ``True``: parameter is set to None at highest value 

63 of parameter range (optional) 

64 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest 

65 value of parameter range (optional) 

66 ''' 

67 

68 def __init__( 

69 self, name, ident, default, minimum, maximum, 

70 low_is_none=None, 

71 high_is_none=None, 

72 low_is_zero=False, 

73 tracking=True, 

74 type=float): 

75 

76 if low_is_none and default == minimum: 

77 default = None 

78 if high_is_none and default == maximum: 

79 default = None 

80 

81 self.name = name 

82 self.ident = ident 

83 self.default = default 

84 self.minimum = minimum 

85 self.maximum = maximum 

86 self.low_is_none = low_is_none 

87 self.high_is_none = high_is_none 

88 self.low_is_zero = low_is_zero 

89 self.tracking = tracking 

90 self.type = type 

91 

92 self._control = None 

93 

94 

95class Switch(object): 

96 ''' 

97 Definition of a boolean switch for the snuffling. The snuffling 

98 may display a checkbox for such a switch. 

99 

100 :param name: labels the switch on the snuffling's control panel 

101 :param ident: identifier of the parameter 

102 :param default: default value 

103 ''' 

104 

105 def __init__(self, name, ident, default): 

106 self.name = name 

107 self.ident = ident 

108 self.default = default 

109 

110 

111class Choice(object): 

112 ''' 

113 Definition of a string choice for the snuffling. The snuffling 

114 may display a menu for such a choice. 

115 

116 :param name: labels the menu on the snuffling's control panel 

117 :param ident: identifier of the parameter 

118 :param default: default value 

119 :param choices: tuple of other options 

120 ''' 

121 

122 def __init__(self, name, ident, default, choices): 

123 self.name = name 

124 self.ident = ident 

125 self.default = default 

126 self.choices = choices 

127 

128 

129class Snuffling(object): 

130 ''' 

131 Base class for user snufflings. 

132 

133 Snufflings are plugins for snuffler (and other applications using the 

134 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` class defined in 

135 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and 

136 should provide a simple way of extending the functionality of snuffler. 

137 

138 A snuffling has access to all data available in a pile viewer, can process 

139 this data and can create and add new traces and markers to the viewer. 

140 ''' 

141 

142 def __init__(self): 

143 self._path = None 

144 

145 self._name = 'Untitled Snuffling' 

146 self._viewer = None 

147 self._tickets = [] 

148 self._markers = [] 

149 

150 self._delete_panel = None 

151 self._delete_menuitem = None 

152 

153 self._panel_parent = None 

154 self._menu_parent = None 

155 

156 self._panel = None 

157 self._menuitem = None 

158 self._helpmenuitem = None 

159 self._parameters = [] 

160 self._param_controls = {} 

161 

162 self._triggers = [] 

163 

164 self._live_update = True 

165 self._previous_output_filename = None 

166 self._previous_input_filename = None 

167 self._previous_input_directory = None 

168 

169 self._tempdir = None 

170 self._iplot = 0 

171 

172 self._have_pre_process_hook = False 

173 self._have_post_process_hook = False 

174 self._pre_process_hook_enabled = False 

175 self._post_process_hook_enabled = False 

176 

177 self._no_viewer_pile = None 

178 self._cli_params = {} 

179 self._filename = None 

180 self._force_panel = False 

181 self._call_in_progress = {} 

182 

183 def setup(self): 

184 ''' 

185 Setup the snuffling. 

186 

187 This method should be implemented in subclass and contain e.g. calls to 

188 :py:meth:`set_name` and :py:meth:`add_parameter`. 

189 ''' 

190 

191 pass 

192 

193 def module_dir(self): 

194 ''' 

195 Returns the path of the directory where snufflings are stored. 

196 

197 The default path is ``$HOME/.snufflings``. 

198 ''' 

199 

200 return self._path 

201 

202 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False): 

203 ''' 

204 Set parent viewer and hooks to add panel and menu entry. 

205 

206 This method is called from the 

207 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` object. Calls 

208 :py:meth:`setup_gui`. 

209 ''' 

210 

211 self._viewer = viewer 

212 self._panel_parent = panel_parent 

213 self._menu_parent = menu_parent 

214 

215 self.setup_gui(reloaded=reloaded) 

216 

217 def setup_gui(self, reloaded=False): 

218 ''' 

219 Create and add gui elements to the viewer. 

220 

221 This method is initially called from :py:meth:`init_gui`. It is also 

222 called, e.g. when new parameters have been added or if the name of the 

223 snuffling has been changed. 

224 ''' 

225 

226 if self._panel_parent is not None: 

227 self._panel = self.make_panel(self._panel_parent) 

228 if self._panel: 

229 self._panel_parent.add_panel( 

230 self.get_name(), self._panel, reloaded) 

231 

232 if self._menu_parent is not None: 

233 self._menuitem = self.make_menuitem(self._menu_parent) 

234 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent) 

235 if self._menuitem: 

236 self._menu_parent.add_snuffling_menuitem(self._menuitem) 

237 

238 if self._helpmenuitem: 

239 self._menu_parent.add_snuffling_help_menuitem( 

240 self._helpmenuitem) 

241 

242 def set_force_panel(self, bool=True): 

243 ''' 

244 Force to create a panel. 

245 

246 :param bool: if ``True`` will create a panel with Help, Clear and Run 

247 button. 

248 ''' 

249 

250 self._force_panel = bool 

251 

252 def make_cli_parser1(self): 

253 import optparse 

254 

255 class MyOptionParser(optparse.OptionParser): 

256 def error(self, msg): 

257 logger.error(msg) 

258 self.exit(1) 

259 

260 parser = MyOptionParser() 

261 

262 parser.add_option( 

263 '--format', 

264 dest='format', 

265 default='from_extension', 

266 choices=( 

267 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l', 

268 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube', 

269 'from_extension', 'detect'), 

270 help="assume files are of given FORMAT [default: '%default']") 

271 

272 parser.add_option( 

273 '--pattern', 

274 dest='regex', 

275 metavar='REGEX', 

276 help='only include files whose paths match REGEX') 

277 

278 self.add_params_to_cli_parser(parser) 

279 self.configure_cli_parser(parser) 

280 return parser 

281 

282 def configure_cli_parser(self, parser): 

283 pass 

284 

285 def cli_usage(self): 

286 return None 

287 

288 def add_params_to_cli_parser(self, parser): 

289 

290 for param in self._parameters: 

291 if isinstance(param, Param): 

292 parser.add_option( 

293 '--' + param.ident, 

294 dest=param.ident, 

295 default=param.default, 

296 type={float: 'float', int: 'int'}[param.type], 

297 help=param.name) 

298 

299 def setup_cli(self): 

300 self.setup() 

301 parser = self.make_cli_parser1() 

302 (options, args) = parser.parse_args() 

303 

304 for param in self._parameters: 

305 if isinstance(param, Param): 

306 setattr(self, param.ident, getattr(options, param.ident)) 

307 

308 self._cli_params['regex'] = options.regex 

309 self._cli_params['format'] = options.format 

310 self._cli_params['sources'] = args 

311 

312 return options, args, parser 

313 

314 def delete_gui(self): 

315 ''' 

316 Remove the gui elements of the snuffling. 

317 

318 This removes the panel and menu entry of the widget from the viewer and 

319 also removes all traces and markers added with the 

320 :py:meth:`add_traces` and :py:meth:`add_markers` methods. 

321 ''' 

322 

323 self.cleanup() 

324 

325 if self._panel is not None: 

326 self._panel_parent.remove_panel(self._panel) 

327 self._panel = None 

328 

329 if self._menuitem is not None: 

330 self._menu_parent.remove_snuffling_menuitem(self._menuitem) 

331 self._menuitem = None 

332 

333 if self._helpmenuitem is not None: 

334 self._menu_parent.remove_snuffling_help_menuitem( 

335 self._helpmenuitem) 

336 

337 def set_name(self, name): 

338 ''' 

339 Set the snuffling's name. 

340 

341 The snuffling's name is shown as a menu entry and in the panel header. 

342 ''' 

343 

344 self._name = name 

345 self.reset_gui() 

346 

347 def get_name(self): 

348 ''' 

349 Get the snuffling's name. 

350 ''' 

351 

352 return self._name 

353 

354 def set_have_pre_process_hook(self, bool): 

355 self._have_pre_process_hook = bool 

356 self._live_update = False 

357 self._pre_process_hook_enabled = False 

358 self.reset_gui() 

359 

360 def set_have_post_process_hook(self, bool): 

361 self._have_post_process_hook = bool 

362 self._live_update = False 

363 self._post_process_hook_enabled = False 

364 self.reset_gui() 

365 

366 def set_have_pile_changed_hook(self, bool): 

367 self._pile_ = False 

368 

369 def enable_pile_changed_notifications(self): 

370 ''' 

371 Get informed when pile changed. 

372 

373 When activated, the :py:meth:`pile_changed` method is called on every 

374 update in the viewer's pile. 

375 ''' 

376 

377 viewer = self.get_viewer() 

378 viewer.pile_has_changed_signal.connect( 

379 self.pile_changed) 

380 

381 def disable_pile_changed_notifications(self): 

382 ''' 

383 Stop getting informed about changes in viewer's pile. 

384 ''' 

385 

386 viewer = self.get_viewer() 

387 viewer.pile_has_changed_signal.disconnect( 

388 self.pile_changed) 

389 

390 def pile_changed(self): 

391 ''' 

392 Called when the connected viewer's pile has changed. 

393 

394 Must be activated with a call to 

395 :py:meth:`enable_pile_changed_notifications`. 

396 ''' 

397 

398 pass 

399 

400 def reset_gui(self, reloaded=False): 

401 ''' 

402 Delete and recreate the snuffling's panel. 

403 ''' 

404 

405 if self._panel or self._menuitem: 

406 sett = self.get_settings() 

407 self.delete_gui() 

408 self.setup_gui(reloaded=reloaded) 

409 self.set_settings(sett) 

410 

411 def show_message(self, kind, message): 

412 ''' 

413 Display a message box. 

414 

415 :param kind: string defining kind of message 

416 :param message: the message to be displayed 

417 ''' 

418 

419 try: 

420 box = qw.QMessageBox(self.get_viewer()) 

421 box.setText('%s: %s' % (kind.capitalize(), message)) 

422 box.exec_() 

423 except NoViewerSet: 

424 pass 

425 

426 def error(self, message): 

427 ''' 

428 Show an error message box. 

429 

430 :param message: specifying the error 

431 ''' 

432 

433 logger.error('%s: %s' % (self._name, message)) 

434 self.show_message('error', message) 

435 

436 def warn(self, message): 

437 ''' 

438 Display a warning message. 

439 

440 :param message: specifying the warning 

441 ''' 

442 

443 logger.warning('%s: %s' % (self._name, message)) 

444 self.show_message('warning', message) 

445 

446 def fail(self, message, action='error'): 

447 ''' 

448 Show an error message box and raise :py:exc:`SnufflingCallFailed` 

449 exception. 

450 

451 :param message: specifying the error 

452 ''' 

453 

454 if action == 'error': 

455 self.error(message) 

456 elif action == 'warn': 

457 self.warn(message) 

458 elif action == 'log': 

459 logger.error('%s: %s' % (self._name, message)) 

460 elif action == 'status': 

461 logger.warn('%s: %s' % (self._name, message)) 

462 viewer = self.get_viewer().window() 

463 if viewer: 

464 viewer.window().status_messages.set( 

465 'snuffling', message) 

466 

467 raise SnufflingCallFailed(message) 

468 

469 def pylab(self, name=None, get='axes', figure_cls=None): 

470 ''' 

471 Create a :py:class:`pyrocko.gui.util.FigureFrame` and return either the 

472 frame, a :py:class:`matplotlib.figure.Figure` instance or a 

473 :py:class:`matplotlib.axes.Axes` instance. 

474 

475 :param name: labels the figure frame's tab 

476 :param get: 'axes'|'figure'|'frame' (optional) 

477 ''' 

478 

479 if name is None: 

480 self._iplot += 1 

481 name = 'Plot %i (%s)' % (self._iplot, self.get_name()) 

482 

483 fframe = FigureFrame(figure_cls=figure_cls) 

484 self._panel_parent.add_tab(name, fframe) 

485 if get == 'axes': 

486 return fframe.gca() 

487 elif get == 'figure': 

488 return fframe.gcf() 

489 elif get == 'figure_frame': 

490 return fframe 

491 

492 def figure(self, name=None): 

493 ''' 

494 Returns a :py:class:`matplotlib.figure.Figure` instance. 

495 

496 Force drawing of the figure by calling `fig.canvas.draw()` on the 

497 returned object ``fig``. 

498 

499 :param name: labels the tab of the figure 

500 ''' 

501 

502 return self.pylab(name=name, get='figure') 

503 

504 def axes(self, name=None): 

505 ''' 

506 Returns a :py:class:`matplotlib.axes.Axes` instance. 

507 

508 :param name: labels the tab of axes 

509 ''' 

510 

511 return self.pylab(name=name, get='axes') 

512 

513 def figure_frame(self, name=None, figure_cls=None): 

514 ''' 

515 Create a :py:class:`pyrocko.gui.util.FigureFrame`. 

516 

517 :param name: labels the tab figure frame 

518 ''' 

519 

520 return self.pylab(name=name, get='figure_frame', figure_cls=figure_cls) 

521 

522 def smartplot_frame(self, name, *args, plot_cls=None, **kwargs): 

523 ''' 

524 Create a :py:class:`pyrocko.gui.util.SmartplotFrame`. 

525 

526 :param name: labels the tab 

527 :param \\*args: 

528 passed to :py:class:`pyrocko.plot.smartplot.Plot` 

529 :param \\*kwargs: 

530 passed to :py:class:`pyrocko.plot.smartplot.Plot` 

531 :param plot_cls: 

532 if given, subclass to be used instead of 

533 :py:class:`pyrocko.plot.smartplot.Plot` 

534 ''' 

535 frame = SmartplotFrame( 

536 plot_args=args, 

537 plot_cls=plot_cls, 

538 plot_kwargs=kwargs) 

539 

540 self._panel_parent.add_tab(name, frame) 

541 return frame 

542 

543 def pixmap_frame(self, filename=None, name=None): 

544 ''' 

545 Create a :py:class:`pyrocko.gui.util.PixmapFrame`. 

546 

547 :param name: labels the tab 

548 :param filename: name of file to be displayed 

549 ''' 

550 

551 f = PixmapFrame(filename) 

552 

553 scroll_area = qw.QScrollArea() 

554 scroll_area.setWidget(f) 

555 scroll_area.setWidgetResizable(True) 

556 

557 self._panel_parent.add_tab(name or 'Pixmap', scroll_area) 

558 return f 

559 

560 def web_frame(self, url=None, name=None): 

561 ''' 

562 Creates a :py:class:`~pyrocko.gui.util.WebKitFrame` which can be 

563 used as a browser within Snuffler. 

564 

565 :param url: url to open 

566 :param name: labels the tab 

567 ''' 

568 

569 if name is None: 

570 self._iplot += 1 

571 name = 'Web browser %i (%s)' % (self._iplot, self.get_name()) 

572 

573 f = WebKitFrame(url) 

574 self._panel_parent.add_tab(name, f) 

575 return f 

576 

577 def vtk_frame(self, name=None, actors=None): 

578 ''' 

579 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D 

580 graphics. 

581 

582 :param actors: list of VTKActors 

583 :param name: labels the tab 

584 

585 Initialize the interactive rendering by calling the frames' 

586 :py:meth`initialize` method after having added all actors to the frames 

587 renderer. 

588 

589 Requires installation of vtk including python wrapper. 

590 ''' 

591 if name is None: 

592 self._iplot += 1 

593 name = 'VTK %i (%s)' % (self._iplot, self.get_name()) 

594 

595 try: 

596 f = VTKFrame(actors=actors) 

597 except ImportError as e: 

598 self.fail(e) 

599 

600 self._panel_parent.add_tab(name, f) 

601 return f 

602 

603 def tempdir(self): 

604 ''' 

605 Create a temporary directory and return its absolute path. 

606 

607 The directory and all its contents are removed when the Snuffling 

608 instance is deleted. 

609 ''' 

610 

611 if self._tempdir is None: 

612 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-') 

613 

614 return self._tempdir 

615 

616 def set_live_update(self, live_update): 

617 ''' 

618 Enable/disable live updating. 

619 

620 When live updates are enabled, the :py:meth:`call` method is called 

621 whenever the user changes a parameter. If it is disabled, the user has 

622 to initiate such a call manually by triggering the snuffling's menu 

623 item or pressing the call button. 

624 ''' 

625 

626 self._live_update = live_update 

627 if self._have_pre_process_hook: 

628 self._pre_process_hook_enabled = live_update 

629 if self._have_post_process_hook: 

630 self._post_process_hook_enabled = live_update 

631 

632 try: 

633 self.get_viewer().clean_update() 

634 except NoViewerSet: 

635 pass 

636 

637 def add_parameter(self, param): 

638 ''' 

639 Add an adjustable parameter to the snuffling. 

640 

641 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or 

642 :py:class:`Choice`. 

643 

644 For each parameter added, controls are added to the snuffling's panel, 

645 so that the parameter can be adjusted from the gui. 

646 ''' 

647 

648 self._parameters.append(param) 

649 self._set_parameter_value(param.ident, param.default) 

650 

651 if self._panel is not None: 

652 self.delete_gui() 

653 self.setup_gui() 

654 

655 def add_trigger(self, name, method): 

656 ''' 

657 Add a button to the snuffling's panel. 

658 

659 :param name: string that labels the button 

660 :param method: method associated with the button 

661 ''' 

662 

663 self._triggers.append((name, method)) 

664 

665 if self._panel is not None: 

666 self.delete_gui() 

667 self.setup_gui() 

668 

669 def get_parameters(self): 

670 ''' 

671 Get the snuffling's adjustable parameter definitions. 

672 

673 Returns a list of objects of type :py:class:`Param`. 

674 ''' 

675 

676 return self._parameters 

677 

678 def get_parameter(self, ident): 

679 ''' 

680 Get one of the snuffling's adjustable parameter definitions. 

681 

682 :param ident: identifier of the parameter 

683 

684 Returns an object of type :py:class:`Param` or ``None``. 

685 ''' 

686 

687 for param in self._parameters: 

688 if param.ident == ident: 

689 return param 

690 return None 

691 

692 def set_parameter(self, ident, value): 

693 ''' 

694 Set one of the snuffling's adjustable parameters. 

695 

696 :param ident: identifier of the parameter 

697 :param value: new value of the parameter 

698 

699 Adjusts the control of a parameter without calling :py:meth:`call`. 

700 ''' 

701 

702 self._set_parameter_value(ident, value) 

703 

704 control = self._param_controls.get(ident, None) 

705 if control: 

706 control.set_value(value) 

707 

708 def set_parameter_range(self, ident, vmin, vmax): 

709 ''' 

710 Set the range of one of the snuffling's adjustable parameters. 

711 

712 :param ident: identifier of the parameter 

713 :param vmin,vmax: new minimum and maximum value for the parameter 

714 

715 Adjusts the control of a parameter without calling :py:meth:`call`. 

716 ''' 

717 

718 control = self._param_controls.get(ident, None) 

719 if control: 

720 control.set_range(vmin, vmax) 

721 

722 def set_parameter_choices(self, ident, choices): 

723 ''' 

724 Update the choices of a Choice parameter. 

725 

726 :param ident: identifier of the parameter 

727 :param choices: list of strings 

728 ''' 

729 

730 control = self._param_controls.get(ident, None) 

731 if control: 

732 selected_choice = control.set_choices(choices) 

733 self._set_parameter_value(ident, selected_choice) 

734 

735 def _set_parameter_value(self, ident, value): 

736 setattr(self, ident, value) 

737 

738 def get_parameter_value(self, ident): 

739 ''' 

740 Get the current value of a parameter. 

741 

742 :param ident: identifier of the parameter 

743 ''' 

744 return getattr(self, ident) 

745 

746 def get_settings(self): 

747 ''' 

748 Returns a dictionary with identifiers of all parameters as keys and 

749 their values as the dictionaries values. 

750 ''' 

751 

752 params = self.get_parameters() 

753 settings = {} 

754 for param in params: 

755 settings[param.ident] = self.get_parameter_value(param.ident) 

756 

757 return settings 

758 

759 def set_settings(self, settings): 

760 params = self.get_parameters() 

761 dparams = dict([(param.ident, param) for param in params]) 

762 for k, v in settings.items(): 

763 if k in dparams: 

764 self._set_parameter_value(k, v) 

765 if k in self._param_controls: 

766 control = self._param_controls[k] 

767 control.set_value(v) 

768 

769 def get_viewer(self): 

770 ''' 

771 Get the parent viewer. 

772 

773 Returns a reference to an object of type 

774 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer`, which is the 

775 main viewer widget. 

776 

777 If no gui has been initialized for the snuffling, a 

778 :py:exc:`NoViewerSet` exception is raised. 

779 ''' 

780 

781 if self._viewer is None: 

782 raise NoViewerSet() 

783 return self._viewer 

784 

785 def get_pile(self): 

786 ''' 

787 Get the pile. 

788 

789 If a gui has been initialized, a reference to the viewer's internal 

790 pile is returned. If not, the :py:meth:`make_pile` method (which may be 

791 overloaded in subclass) is called to create a pile. This can be 

792 utilized to make hybrid snufflings, which may work also in a standalone 

793 mode. 

794 ''' 

795 

796 try: 

797 p = self.get_viewer().get_pile() 

798 except NoViewerSet: 

799 if self._no_viewer_pile is None: 

800 self._no_viewer_pile = self.make_pile() 

801 

802 p = self._no_viewer_pile 

803 

804 return p 

805 

806 def get_active_event_and_stations( 

807 self, trange=(-3600., 3600.), missing='warn'): 

808 

809 ''' 

810 Get event and stations with available data for active event. 

811 

812 :param trange: (begin, end), time range around event origin time to 

813 query for available data 

814 :param missing: string, what to do in case of missing station 

815 information: ``'warn'``, ``'raise'`` or ``'ignore'``. 

816 

817 :returns: ``(event, stations)`` 

818 ''' 

819 

820 p = self.get_pile() 

821 v = self.get_viewer() 

822 

823 event = v.get_active_event() 

824 if event is None: 

825 self.fail( 

826 'No active event set. Select an event and press "e" to make ' 

827 'it the "active event"') 

828 

829 stations = {} 

830 for traces in p.chopper( 

831 event.time+trange[0], 

832 event.time+trange[1], 

833 load_data=False, 

834 degap=False): 

835 

836 for tr in traces: 

837 try: 

838 for skey in v.station_keys(tr): 

839 if skey in stations: 

840 continue 

841 

842 station = v.get_station(skey) 

843 stations[skey] = station 

844 

845 except KeyError: 

846 s = 'No station information for station key "%s".' \ 

847 % '.'.join(skey) 

848 

849 if missing == 'warn': 

850 logger.warning(s) 

851 elif missing == 'raise': 

852 raise MissingStationInformation(s) 

853 elif missing == 'ignore': 

854 pass 

855 else: 

856 assert False, 'invalid argument to "missing"' 

857 

858 stations[skey] = None 

859 

860 return event, list(set( 

861 st for st in stations.values() if st is not None)) 

862 

863 def get_stations(self): 

864 ''' 

865 Get all stations known to the viewer. 

866 ''' 

867 

868 v = self.get_viewer() 

869 stations = list(v.stations.values()) 

870 return stations 

871 

872 def get_markers(self): 

873 ''' 

874 Get all markers from the viewer. 

875 ''' 

876 

877 return self.get_viewer().get_markers() 

878 

879 def get_event_markers(self): 

880 ''' 

881 Get all event markers from the viewer. 

882 ''' 

883 

884 return [m for m in self.get_viewer().get_markers() 

885 if isinstance(m, EventMarker)] 

886 

887 def get_selected_markers(self): 

888 ''' 

889 Get all selected markers from the viewer. 

890 ''' 

891 

892 return self.get_viewer().selected_markers() 

893 

894 def get_selected_event_markers(self): 

895 ''' 

896 Get all selected event markers from the viewer. 

897 ''' 

898 

899 return [m for m in self.get_viewer().selected_markers() 

900 if isinstance(m, EventMarker)] 

901 

902 def get_active_event_and_phase_markers(self): 

903 ''' 

904 Get the marker of the active event and any associated phase markers 

905 ''' 

906 

907 viewer = self.get_viewer() 

908 markers = viewer.get_markers() 

909 event_marker = viewer.get_active_event_marker() 

910 if event_marker is None: 

911 self.fail( 

912 'No active event set. ' 

913 'Select an event and press "e" to make it the "active event"') 

914 

915 event = event_marker.get_event() 

916 

917 selection = [] 

918 for m in markers: 

919 if isinstance(m, PhaseMarker): 

920 if m.get_event() is event: 

921 selection.append(m) 

922 

923 return ( 

924 event_marker, 

925 [m for m in markers if isinstance(m, PhaseMarker) and 

926 m.get_event() == event]) 

927 

928 def get_viewer_trace_selector(self, mode='inview'): 

929 ''' 

930 Get currently active trace selector from viewer. 

931 

932 :param mode: set to ``'inview'`` (default) to only include selections 

933 currently shown in the viewer, ``'visible' to include all traces 

934 not currenly hidden by hide or quick-select commands, or ``'all'`` 

935 to disable any restrictions. 

936 ''' 

937 

938 viewer = self.get_viewer() 

939 

940 def rtrue(tr): 

941 return True 

942 

943 if mode == 'inview': 

944 return viewer.trace_selector or rtrue 

945 elif mode == 'visible': 

946 return viewer.trace_filter or rtrue 

947 elif mode == 'all': 

948 return rtrue 

949 else: 

950 raise Exception('invalid mode argument') 

951 

952 def chopper_selected_traces(self, fallback=False, marker_selector=None, 

953 mode='inview', main_bandpass=False, 

954 progress=None, responsive=False, 

955 *args, **kwargs): 

956 ''' 

957 Iterate over selected traces. 

958 

959 Shortcut to get all trace data contained in the selected markers in the 

960 running snuffler. For each selected marker, 

961 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments 

962 *tmin*, *tmax*, and *trace_selector* set to values according to the 

963 marker. Additional arguments to the chopper are handed over from 

964 *\\*args* and *\\*\\*kwargs*. 

965 

966 :param fallback: 

967 If ``True``, if no selection has been marked, use the content 

968 currently visible in the viewer. 

969 

970 :param marker_selector: 

971 If not ``None`` a callback to filter markers. 

972 

973 :param mode: 

974 Set to ``'inview'`` (default) to only include selections currently 

975 shown in the viewer (excluding traces accessible through vertical 

976 scrolling), ``'visible'`` to include all traces not currently 

977 hidden by hide or quick-select commands (including traces 

978 accessible through vertical scrolling), or ``'all'`` to disable any 

979 restrictions. 

980 

981 :param main_bandpass: 

982 If ``True``, apply main control high- and lowpass filters to 

983 traces. Note: use with caution. Processing is fixed to use 4th 

984 order Butterworth highpass and lowpass and the signal is always 

985 demeaned before filtering. FFT filtering, rotation, demean and 

986 bandpass settings from the graphical interface are not respected 

987 here. Padding is not automatically adjusted so results may include 

988 artifacts. 

989 

990 :param progress: 

991 If given a string a progress bar is shown to the user. The string 

992 is used as the label for the progress bar. 

993 

994 :param responsive: 

995 If set to ``True``, occasionally allow UI events to be processed. 

996 If used in combination with ``progress``, this allows the iterator 

997 to be aborted by the user. 

998 ''' 

999 

1000 try: 

1001 viewer = self.get_viewer() 

1002 markers = [ 

1003 m for m in viewer.selected_markers() 

1004 if not isinstance(m, EventMarker)] 

1005 

1006 if marker_selector is not None: 

1007 markers = [ 

1008 marker for marker in markers if marker_selector(marker)] 

1009 

1010 pile = self.get_pile() 

1011 

1012 def rtrue(tr): 

1013 return True 

1014 

1015 trace_selector_arg = kwargs.pop('trace_selector', rtrue) 

1016 trace_selector_viewer = self.get_viewer_trace_selector(mode) 

1017 

1018 style_arg = kwargs.pop('style', None) 

1019 

1020 if main_bandpass: 

1021 def apply_filters(traces): 

1022 for tr in traces: 

1023 if viewer.highpass is not None: 

1024 tr.highpass(4, viewer.highpass) 

1025 if viewer.lowpass is not None: 

1026 tr.lowpass(4, viewer.lowpass) 

1027 return traces 

1028 else: 

1029 def apply_filters(traces): 

1030 return traces 

1031 

1032 pb = viewer.parent().get_progressbars() 

1033 

1034 time_last = [time.time()] 

1035 

1036 def update_progress(label, batch): 

1037 time_now = time.time() 

1038 if responsive: 

1039 # start processing events with one second delay, so that 

1040 # e.g. cleanup actions at startup do not cause track number 

1041 # changes etc. 

1042 if time_last[0] + 1. < time_now: 

1043 qw.qApp.processEvents() 

1044 else: 

1045 # redraw about once a second 

1046 if time_last[0] + 1. < time_now: 

1047 viewer.repaint() 

1048 

1049 time_last[0] = time.time() # use time after drawing 

1050 

1051 abort = pb.set_status( 

1052 label, batch.i*100./batch.n, responsive) 

1053 abort |= viewer.window().is_closing() 

1054 

1055 return abort 

1056 

1057 if markers: 

1058 for imarker, marker in enumerate(markers): 

1059 try: 

1060 if progress: 

1061 label = '%s: %i/%i' % ( 

1062 progress, imarker+1, len(markers)) 

1063 

1064 pb.set_status(label, 0, responsive) 

1065 

1066 if not marker.nslc_ids: 

1067 trace_selector_marker = rtrue 

1068 else: 

1069 def trace_selector_marker(tr): 

1070 return marker.match_nslc(tr.nslc_id) 

1071 

1072 def trace_selector(tr): 

1073 return trace_selector_arg(tr) \ 

1074 and trace_selector_viewer(tr) \ 

1075 and trace_selector_marker(tr) 

1076 

1077 for batch in pile.chopper( 

1078 tmin=marker.tmin, 

1079 tmax=marker.tmax, 

1080 trace_selector=trace_selector, 

1081 style='batch', 

1082 *args, 

1083 **kwargs): 

1084 

1085 if progress: 

1086 abort = update_progress(label, batch) 

1087 if abort: 

1088 return 

1089 

1090 batch.traces = apply_filters(batch.traces) 

1091 if style_arg == 'batch': 

1092 yield batch 

1093 else: 

1094 yield batch.traces 

1095 

1096 finally: 

1097 if progress: 

1098 pb.set_status(label, 100., responsive) 

1099 

1100 elif fallback: 

1101 def trace_selector(tr): 

1102 return trace_selector_arg(tr) \ 

1103 and trace_selector_viewer(tr) 

1104 

1105 tmin, tmax = viewer.get_time_range() 

1106 

1107 if not pile.is_empty(): 

1108 ptmin = pile.get_tmin() 

1109 tpad = kwargs.get('tpad', 0.0) 

1110 if ptmin > tmin: 

1111 tmin = ptmin + tpad 

1112 ptmax = pile.get_tmax() 

1113 if ptmax < tmax: 

1114 tmax = ptmax - tpad 

1115 

1116 try: 

1117 if progress: 

1118 label = progress 

1119 pb.set_status(label, 0, responsive) 

1120 

1121 for batch in pile.chopper( 

1122 tmin=tmin, 

1123 tmax=tmax, 

1124 trace_selector=trace_selector, 

1125 style='batch', 

1126 *args, 

1127 **kwargs): 

1128 

1129 if progress: 

1130 abort = update_progress(label, batch) 

1131 

1132 if abort: 

1133 return 

1134 

1135 batch.traces = apply_filters(batch.traces) 

1136 

1137 if style_arg == 'batch': 

1138 yield batch 

1139 else: 

1140 yield batch.traces 

1141 

1142 finally: 

1143 if progress: 

1144 pb.set_status(label, 100., responsive) 

1145 

1146 else: 

1147 raise NoTracesSelected() 

1148 

1149 except NoViewerSet: 

1150 pile = self.get_pile() 

1151 return pile.chopper(*args, **kwargs) 

1152 

1153 def get_selected_time_range(self, fallback=False): 

1154 ''' 

1155 Get the time range spanning all selected markers. 

1156 

1157 :param fallback: if ``True`` and no marker is selected return begin and 

1158 end of visible time range 

1159 ''' 

1160 

1161 viewer = self.get_viewer() 

1162 markers = viewer.selected_markers() 

1163 mins = [marker.tmin for marker in markers] 

1164 maxs = [marker.tmax for marker in markers] 

1165 

1166 if mins and maxs: 

1167 tmin = min(mins) 

1168 tmax = max(maxs) 

1169 

1170 elif fallback: 

1171 tmin, tmax = viewer.get_time_range() 

1172 

1173 else: 

1174 raise NoTracesSelected() 

1175 

1176 return tmin, tmax 

1177 

1178 def panel_visibility_changed(self, bool): 

1179 ''' 

1180 Called when the snuffling's panel becomes visible or is hidden. 

1181 

1182 Can be overloaded in subclass, e.g. to perform additional setup actions 

1183 when the panel is activated the first time. 

1184 ''' 

1185 

1186 pass 

1187 

1188 def make_pile(self): 

1189 ''' 

1190 Create a pile. 

1191 

1192 To be overloaded in subclass. The default implementation just calls 

1193 :py:func:`pyrocko.pile.make_pile` to create a pile from command line 

1194 arguments. 

1195 ''' 

1196 

1197 cachedirname = config.config().cache_dir 

1198 sources = self._cli_params.get('sources', sys.argv[1:]) 

1199 return pile.make_pile( 

1200 sources, 

1201 cachedirname=cachedirname, 

1202 regex=self._cli_params['regex'], 

1203 fileformat=self._cli_params['format']) 

1204 

1205 def make_panel(self, parent): 

1206 ''' 

1207 Create a widget for the snuffling's control panel. 

1208 

1209 Normally called from the :py:meth:`setup_gui` method. Returns ``None`` 

1210 if no panel is needed (e.g. if the snuffling has no adjustable 

1211 parameters). 

1212 ''' 

1213 

1214 params = self.get_parameters() 

1215 self._param_controls = {} 

1216 if params or self._force_panel: 

1217 sarea = MyScrollArea(parent.get_panel_parent_widget()) 

1218 sarea.setFrameStyle(qw.QFrame.NoFrame) 

1219 sarea.setSizePolicy(qw.QSizePolicy( 

1220 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)) 

1221 frame = MyFrame(sarea) 

1222 frame.widgetVisibilityChanged.connect( 

1223 self.panel_visibility_changed) 

1224 

1225 frame.setSizePolicy(qw.QSizePolicy( 

1226 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1227 frame.setFrameStyle(qw.QFrame.NoFrame) 

1228 sarea.setWidget(frame) 

1229 sarea.setWidgetResizable(True) 

1230 layout = qw.QGridLayout() 

1231 layout.setContentsMargins(0, 0, 0, 0) 

1232 layout.setSpacing(0) 

1233 frame.setLayout(layout) 

1234 

1235 parlayout = qw.QGridLayout() 

1236 

1237 irow = 0 

1238 ipar = 0 

1239 have_switches = False 

1240 have_params = False 

1241 for iparam, param in enumerate(params): 

1242 if isinstance(param, Param): 

1243 if param.minimum <= 0.0: 

1244 param_control = LinValControl( 

1245 high_is_none=param.high_is_none, 

1246 low_is_none=param.low_is_none, 

1247 type=param.type) 

1248 else: 

1249 param_control = ValControl( 

1250 high_is_none=param.high_is_none, 

1251 low_is_none=param.low_is_none, 

1252 low_is_zero=param.low_is_zero, 

1253 type=param.type) 

1254 

1255 param_control.setup( 

1256 param.name, 

1257 param.minimum, 

1258 param.maximum, 

1259 param.default, 

1260 iparam) 

1261 

1262 param_control.set_tracking(param.tracking) 

1263 param_control.valchange.connect( 

1264 self.modified_snuffling_panel) 

1265 

1266 self._param_controls[param.ident] = param_control 

1267 for iw, w in enumerate(param_control.widgets()): 

1268 parlayout.addWidget(w, ipar, iw) 

1269 

1270 ipar += 1 

1271 have_params = True 

1272 

1273 elif isinstance(param, Choice): 

1274 param_widget = ChoiceControl( 

1275 param.ident, param.default, param.choices, param.name) 

1276 param_widget.choosen.connect( 

1277 self.choose_on_snuffling_panel) 

1278 

1279 self._param_controls[param.ident] = param_widget 

1280 parlayout.addWidget(param_widget, ipar, 0, 1, 3) 

1281 ipar += 1 

1282 have_params = True 

1283 

1284 elif isinstance(param, Switch): 

1285 have_switches = True 

1286 

1287 if have_params: 

1288 parframe = qw.QFrame(sarea) 

1289 parframe.setSizePolicy(qw.QSizePolicy( 

1290 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1291 parframe.setLayout(parlayout) 

1292 layout.addWidget(parframe, irow, 0) 

1293 irow += 1 

1294 

1295 if have_switches: 

1296 swlayout = qw.QGridLayout() 

1297 isw = 0 

1298 for iparam, param in enumerate(params): 

1299 if isinstance(param, Switch): 

1300 param_widget = SwitchControl( 

1301 param.ident, param.default, param.name) 

1302 param_widget.sw_toggled.connect( 

1303 self.switch_on_snuffling_panel) 

1304 

1305 self._param_controls[param.ident] = param_widget 

1306 swlayout.addWidget(param_widget, isw//10, isw % 10) 

1307 isw += 1 

1308 

1309 swframe = qw.QFrame(sarea) 

1310 swframe.setSizePolicy(qw.QSizePolicy( 

1311 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1312 swframe.setLayout(swlayout) 

1313 layout.addWidget(swframe, irow, 0) 

1314 irow += 1 

1315 

1316 butframe = qw.QFrame(sarea) 

1317 butframe.setSizePolicy(qw.QSizePolicy( 

1318 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1319 butlayout = qw.QHBoxLayout() 

1320 butframe.setLayout(butlayout) 

1321 

1322 live_update_checkbox = qw.QCheckBox('Auto-Run') 

1323 if self._live_update: 

1324 live_update_checkbox.setCheckState(qc.Qt.Checked) 

1325 

1326 butlayout.addWidget(live_update_checkbox) 

1327 live_update_checkbox.toggled.connect( 

1328 self.live_update_toggled) 

1329 

1330 help_button = qw.QPushButton('Help') 

1331 butlayout.addWidget(help_button) 

1332 help_button.clicked.connect( 

1333 self.help_button_triggered) 

1334 

1335 clear_button = qw.QPushButton('Clear') 

1336 butlayout.addWidget(clear_button) 

1337 clear_button.clicked.connect( 

1338 self.clear_button_triggered) 

1339 

1340 call_button = qw.QPushButton('Run') 

1341 butlayout.addWidget(call_button) 

1342 call_button.clicked.connect( 

1343 self.call_button_triggered) 

1344 

1345 for name, method in self._triggers: 

1346 but = qw.QPushButton(name) 

1347 

1348 def call_and_update(method): 

1349 def f(): 

1350 self.check_call(method) 

1351 self.get_viewer().update() 

1352 return f 

1353 

1354 but.clicked.connect( 

1355 call_and_update(method)) 

1356 

1357 butlayout.addWidget(but) 

1358 

1359 layout.addWidget(butframe, irow, 0) 

1360 

1361 irow += 1 

1362 spacer = qw.QSpacerItem( 

1363 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

1364 

1365 layout.addItem(spacer, irow, 0) 

1366 

1367 return sarea 

1368 

1369 else: 

1370 return None 

1371 

1372 def make_helpmenuitem(self, parent): 

1373 ''' 

1374 Create the help menu item for the snuffling. 

1375 ''' 

1376 

1377 item = qw.QAction(self.get_name(), None) 

1378 

1379 item.triggered.connect( 

1380 self.help_button_triggered) 

1381 

1382 return item 

1383 

1384 def make_menuitem(self, parent): 

1385 ''' 

1386 Create the menu item for the snuffling. 

1387 

1388 This method may be overloaded in subclass and return ``None``, if no 

1389 menu entry is wanted. 

1390 ''' 

1391 

1392 item = qw.QAction(self.get_name(), None) 

1393 item.setCheckable( 

1394 self._have_pre_process_hook or self._have_post_process_hook) 

1395 

1396 item.triggered.connect( 

1397 self.menuitem_triggered) 

1398 

1399 return item 

1400 

1401 def output_filename( 

1402 self, 

1403 caption='Save File', 

1404 dir='', 

1405 filter='', 

1406 selected_filter=None): 

1407 

1408 ''' 

1409 Query user for an output filename. 

1410 

1411 This is currently a wrapper to ``QFileDialog.getSaveFileName``. 

1412 :py:exc:`UserCancelled` exception is raised if the user cancels the 

1413 dialog. 

1414 ''' 

1415 

1416 if not dir and self._previous_output_filename: 

1417 dir = self._previous_output_filename 

1418 

1419 fn = getSaveFileName( 

1420 self.get_viewer(), caption, dir, filter, selected_filter) 

1421 if not fn: 

1422 raise UserCancelled() 

1423 

1424 self._previous_output_filename = fn 

1425 return str(fn) 

1426 

1427 def input_directory(self, caption='Open Directory', dir=''): 

1428 ''' 

1429 Query user for an input directory. 

1430 

1431 This is a wrapper to ``QFileDialog.getExistingDirectory``. A 

1432 :py:exc:`UserCancelled` exception is raised if the user cancels the 

1433 dialog. 

1434 ''' 

1435 

1436 if not dir and self._previous_input_directory: 

1437 dir = self._previous_input_directory 

1438 

1439 dn = qw.QFileDialog.getExistingDirectory( 

1440 None, caption, dir, qw.QFileDialog.ShowDirsOnly) 

1441 

1442 if not dn: 

1443 raise UserCancelled() 

1444 

1445 self._previous_input_directory = dn 

1446 return str(dn) 

1447 

1448 def input_filename(self, caption='Open File', dir='', filter='', 

1449 selected_filter=None): 

1450 ''' 

1451 Query user for an input filename. 

1452 

1453 This is currently a wrapper to ``QFileDialog.getOpenFileName``. A 

1454 :py:exc:`UserCancelled` exception is raised if the user cancels the 

1455 dialog. 

1456 ''' 

1457 

1458 if not dir and self._previous_input_filename: 

1459 dir = self._previous_input_filename 

1460 

1461 fn, _ = qw.QFileDialog.getOpenFileName( 

1462 self.get_viewer(), 

1463 caption, 

1464 dir, 

1465 filter) 

1466 

1467 if not fn: 

1468 raise UserCancelled() 

1469 

1470 self._previous_input_filename = fn 

1471 return str(fn) 

1472 

1473 def input_dialog(self, caption='', request='', directory=False): 

1474 ''' 

1475 Query user for a text input. 

1476 

1477 This is currently a wrapper to ``QInputDialog.getText``. 

1478 A :py:exc:`UserCancelled` exception is raised if the user cancels the 

1479 dialog. 

1480 ''' 

1481 

1482 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption) 

1483 

1484 if not ok: 

1485 raise UserCancelled() 

1486 

1487 return inp 

1488 

1489 def modified_snuffling_panel(self, value, iparam): 

1490 ''' 

1491 Called when the user has played with an adjustable parameter. 

1492 

1493 The default implementation sets the parameter, calls the snuffling's 

1494 :py:meth:`call` method and finally triggers an update on the viewer 

1495 widget. 

1496 ''' 

1497 

1498 param = self.get_parameters()[iparam] 

1499 self._set_parameter_value(param.ident, value) 

1500 if self._live_update: 

1501 self.check_call(self.call) 

1502 self.get_viewer().update() 

1503 

1504 def switch_on_snuffling_panel(self, ident, state): 

1505 ''' 

1506 Called when the user has toggled a switchable parameter. 

1507 ''' 

1508 

1509 self._set_parameter_value(ident, state) 

1510 if self._live_update: 

1511 self.check_call(self.call) 

1512 self.get_viewer().update() 

1513 

1514 def choose_on_snuffling_panel(self, ident, state): 

1515 ''' 

1516 Called when the user has made a choice about a choosable parameter. 

1517 ''' 

1518 

1519 self._set_parameter_value(ident, state) 

1520 if self._live_update: 

1521 self.check_call(self.call) 

1522 self.get_viewer().update() 

1523 

1524 def menuitem_triggered(self, arg): 

1525 ''' 

1526 Called when the user has triggered the snuffling's menu. 

1527 

1528 The default implementation calls the snuffling's :py:meth:`call` method 

1529 and triggers an update on the viewer widget. 

1530 ''' 

1531 

1532 self.check_call(self.call) 

1533 

1534 if self._have_pre_process_hook: 

1535 self._pre_process_hook_enabled = arg 

1536 

1537 if self._have_post_process_hook: 

1538 self._post_process_hook_enabled = arg 

1539 

1540 if self._have_pre_process_hook or self._have_post_process_hook: 

1541 self.get_viewer().clean_update() 

1542 else: 

1543 self.get_viewer().update() 

1544 

1545 def call_button_triggered(self): 

1546 ''' 

1547 Called when the user has clicked the snuffling's call button. 

1548 

1549 The default implementation calls the snuffling's :py:meth:`call` method 

1550 and triggers an update on the viewer widget. 

1551 ''' 

1552 

1553 self.check_call(self.call) 

1554 self.get_viewer().update() 

1555 

1556 def clear_button_triggered(self): 

1557 ''' 

1558 Called when the user has clicked the snuffling's clear button. 

1559 

1560 This calls the :py:meth:`cleanup` method and triggers an update on the 

1561 viewer widget. 

1562 ''' 

1563 

1564 self.cleanup() 

1565 self.get_viewer().update() 

1566 

1567 def help(self): 

1568 ''' 

1569 Get help text in html/markdown. 

1570 ''' 

1571 

1572 # Older snuffling used to provide this through __doc__, newer code 

1573 # should overload .help() 

1574 return self.__doc__ or '' 

1575 

1576 def help_button_triggered(self): 

1577 ''' 

1578 Creates a :py:class:`QLabel` which contains the documentation as 

1579 given in the snufflings :py:meth:`help`. 

1580 ''' 

1581 

1582 s = self.help().strip() 

1583 

1584 if s: 

1585 if s.startswith('<html>'): 

1586 doc = qw.QLabel(s) 

1587 else: 

1588 try: 

1589 import markdown 

1590 doc = qw.QLabel(markdown.markdown(s)) 

1591 

1592 except ImportError: 

1593 logger.error( 

1594 'Install Python module "markdown" for pretty help ' 

1595 'formatting.') 

1596 

1597 doc = qw.QLabel(s) 

1598 else: 

1599 doc = qw.QLabel('This snuffling does not provide any online help.') 

1600 

1601 labels = [doc] 

1602 

1603 if self._filename: 

1604 from html import escape 

1605 

1606 code = open(self._filename, 'r').read() 

1607 

1608 doc_src = qw.QLabel( 

1609 '''<html><body> 

1610<hr /> 

1611<center><em>May the source be with you, young Skywalker!</em><br /><br /> 

1612<a href="file://%s"><code>%s</code></a></center> 

1613<br /> 

1614<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;"> 

1615<pre style="white-space: pre-wrap"><code>%s 

1616</code></pre></p></body></html>''' 

1617 % ( 

1618 quote(self._filename), 

1619 escape(self._filename), 

1620 escape(code))) 

1621 

1622 labels.append(doc_src) 

1623 

1624 for h in labels: 

1625 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft) 

1626 h.setWordWrap(True) 

1627 

1628 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel') 

1629 

1630 def live_update_toggled(self, on): 

1631 ''' 

1632 Called when the checkbox for live-updates has been toggled. 

1633 ''' 

1634 

1635 self.set_live_update(on) 

1636 

1637 def add_traces(self, traces): 

1638 ''' 

1639 Add traces to the viewer. 

1640 

1641 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace` 

1642 

1643 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and 

1644 added to the viewer's internal pile for display. Note, that unlike with 

1645 the traces from the files given on the command line, these traces are 

1646 kept in memory and so may quickly occupy a lot of ram if a lot of 

1647 traces are added. 

1648 

1649 This method should be preferred over modifying the viewer's internal 

1650 pile directly, because this way, the snuffling has a chance to 

1651 automatically remove its private traces again (see :py:meth:`cleanup` 

1652 method). 

1653 ''' 

1654 

1655 ticket = self.get_viewer().add_traces(traces) 

1656 self._tickets.append(ticket) 

1657 return ticket 

1658 

1659 def add_trace(self, tr): 

1660 ''' 

1661 Add a trace to the viewer. 

1662 

1663 See :py:meth:`add_traces`. 

1664 ''' 

1665 

1666 self.add_traces([tr]) 

1667 

1668 def add_markers(self, markers): 

1669 ''' 

1670 Add some markers to the display. 

1671 

1672 Takes a list of objects of type 

1673 :py:class:`pyrocko.gui.snuffler.marker.Marker` and adds these to the 

1674 viewer. 

1675 ''' 

1676 

1677 self.get_viewer().add_markers(markers) 

1678 self._markers.extend(markers) 

1679 

1680 def add_marker(self, marker): 

1681 ''' 

1682 Add a marker to the display. 

1683 

1684 See :py:meth:`add_markers`. 

1685 ''' 

1686 

1687 self.add_markers([marker]) 

1688 

1689 def cleanup(self): 

1690 ''' 

1691 Remove all traces and markers which have been added so far by the 

1692 snuffling. 

1693 ''' 

1694 

1695 try: 

1696 viewer = self.get_viewer() 

1697 viewer.release_data(self._tickets) 

1698 viewer.remove_markers(self._markers) 

1699 

1700 except NoViewerSet: 

1701 pass 

1702 

1703 self._tickets = [] 

1704 self._markers = [] 

1705 

1706 def check_call(self, method): 

1707 

1708 viewer = self.get_viewer() 

1709 if viewer: 

1710 sb = viewer.window().statusBar() 

1711 sb.clearMessage() 

1712 

1713 if method in self._call_in_progress: 

1714 self.show_message('error', 'Previous action still in progress.') 

1715 return 

1716 

1717 try: 

1718 self._call_in_progress[method] = True 

1719 method() 

1720 return 0 

1721 

1722 except SnufflingError as e: 

1723 if not isinstance(e, SnufflingCallFailed): 

1724 # those have logged within error() 

1725 logger.error('%s: %s' % (self._name, e)) 

1726 logger.error('%s: Snuffling action failed' % self._name) 

1727 return 1 

1728 

1729 except Exception as e: 

1730 message = '%s: Snuffling action raised an exception: %s' % ( 

1731 self._name, str(e)) 

1732 

1733 logger.exception(message) 

1734 self.show_message('error', message) 

1735 

1736 finally: 

1737 del self._call_in_progress[method] 

1738 

1739 def call(self): 

1740 ''' 

1741 Main work routine of the snuffling. 

1742 

1743 This method is called when the snuffling's menu item has been triggered 

1744 or when the user has played with the panel controls. To be overloaded 

1745 in subclass. The default implementation does nothing useful. 

1746 ''' 

1747 

1748 pass 

1749 

1750 def pre_process_hook(self, traces): 

1751 return traces 

1752 

1753 def post_process_hook(self, traces): 

1754 return traces 

1755 

1756 def get_tpad(self): 

1757 ''' 

1758 Return current amount of extra padding needed by live processing hooks. 

1759 ''' 

1760 

1761 return 0.0 

1762 

1763 def pre_destroy(self): 

1764 ''' 

1765 Called when the snuffling instance is about to be deleted. 

1766 

1767 Can be overloaded to do user-defined cleanup actions. The 

1768 default implementation calls :py:meth:`cleanup` and deletes 

1769 the snuffling`s tempory directory, if needed. 

1770 ''' 

1771 

1772 self.cleanup() 

1773 if self._tempdir is not None: 

1774 import shutil 

1775 shutil.rmtree(self._tempdir) 

1776 

1777 

1778class SnufflingError(Exception): 

1779 ''' 

1780 Base exception for Snuffling errors. 

1781 ''' 

1782 pass 

1783 

1784 

1785class NoViewerSet(SnufflingError): 

1786 ''' 

1787 This exception is raised, when no viewer has been set on a Snuffling. 

1788 ''' 

1789 

1790 def __str__(self): 

1791 return 'No GUI available. ' \ 

1792 'Maybe this Snuffling cannot be run in command line mode?' 

1793 

1794 

1795class MissingStationInformation(SnufflingError): 

1796 ''' 

1797 Raised when station information is missing. 

1798 ''' 

1799 

1800 

1801class NoTracesSelected(SnufflingError): 

1802 ''' 

1803 This exception is raised, when no traces have been selected in the viewer 

1804 and we cannot fallback to using the current view. 

1805 ''' 

1806 

1807 def __str__(self): 

1808 return 'No traces have been selected / are available.' 

1809 

1810 

1811class UserCancelled(SnufflingError): 

1812 ''' 

1813 This exception is raised, when the user has cancelled a snuffling dialog. 

1814 ''' 

1815 

1816 def __str__(self): 

1817 return 'The user has cancelled a dialog.' 

1818 

1819 

1820class SnufflingCallFailed(SnufflingError): 

1821 ''' 

1822 This exception is raised, when :py:meth:`Snuffling.fail` is called from 

1823 :py:meth:`Snuffling.call`. 

1824 ''' 

1825 

1826 

1827class InvalidSnufflingFilename(Exception): 

1828 pass 

1829 

1830 

1831class SnufflingModule(object): 

1832 ''' 

1833 Utility class to load/reload snufflings from a file. 

1834 

1835 The snufflings are created by user modules which have the special function 

1836 ``__snufflings__`` which return the snuffling instances to be 

1837 exported. The snuffling module is attached to a handler class, which makes 

1838 use of the snufflings (e.g. 

1839 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` from 

1840 ``pile_viewer.py``). The handler class must implement the methods 

1841 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks. 

1842 The callbacks are utilized from the methods :py:meth:`load_if_needed` and 

1843 :py:meth:`remove_snufflings` which may be called from the handler class, 

1844 when needed. 

1845 ''' 

1846 

1847 mtimes = {} 

1848 

1849 def __init__(self, path, name, handler): 

1850 self._path = path 

1851 self._name = name 

1852 self._mtime = None 

1853 self._module = None 

1854 self._snufflings = [] 

1855 self._handler = handler 

1856 

1857 def load_if_needed(self): 

1858 ''' 

1859 Called by Snuffler to check whether it has to reload the module. 

1860 ''' 

1861 filename = os.path.join(self._path, self._name+'.py') 

1862 

1863 try: 

1864 mtime = os.stat(filename)[8] 

1865 except OSError as e: 

1866 if e.errno == 2: 

1867 logger.error(e) 

1868 raise BrokenSnufflingModule(filename) 

1869 

1870 if self._module is None: 

1871 sys.path[0:0] = [self._path] 

1872 try: 

1873 logger.debug('Loading snuffling module %s' % filename) 

1874 if self._name in sys.modules: 

1875 raise InvalidSnufflingFilename(self._name) 

1876 

1877 self._module = __import__(self._name) 

1878 del sys.modules[self._name] 

1879 

1880 for snuffling in self._module.__snufflings__(): 

1881 snuffling._filename = filename 

1882 self.add_snuffling(snuffling) 

1883 

1884 except (Exception, SystemExit): 

1885 logger.error(traceback.format_exc()) 

1886 raise BrokenSnufflingModule(filename) 

1887 

1888 finally: 

1889 sys.path[0:1] = [] 

1890 

1891 elif self._mtime != mtime: 

1892 logger.warning('Reloading snuffling module %s' % filename) 

1893 settings = self.remove_snufflings() 

1894 sys.path[0:0] = [self._path] 

1895 try: 

1896 

1897 sys.modules[self._name] = self._module 

1898 

1899 reload(self._module) 

1900 del sys.modules[self._name] 

1901 

1902 for snuffling in self._module.__snufflings__(): 

1903 snuffling._filename = filename 

1904 self.add_snuffling(snuffling, reloaded=True) 

1905 

1906 if len(self._snufflings) == len(settings): 

1907 for sett, snuf in zip(settings, self._snufflings): 

1908 snuf.set_settings(sett) 

1909 

1910 except (Exception, SystemExit): 

1911 logger.error(traceback.format_exc()) 

1912 raise BrokenSnufflingModule(filename) 

1913 

1914 finally: 

1915 sys.path[0:1] = [] 

1916 

1917 self._mtime = mtime 

1918 

1919 def add_snuffling(self, snuffling, reloaded=False): 

1920 ''' 

1921 Called by :py:meth:`load_if_needed` to add a snuffling. 

1922 ''' 

1923 snuffling._path = self._path 

1924 snuffling.setup() 

1925 self._snufflings.append(snuffling) 

1926 self._handler.add_snuffling(snuffling, reloaded=reloaded) 

1927 

1928 def remove_snufflings(self): 

1929 ''' 

1930 Called by :py:meth:`load_if_needed` to remove all snufflings. 

1931 ''' 

1932 settings = [] 

1933 for snuffling in self._snufflings: 

1934 settings.append(snuffling.get_settings()) 

1935 self._handler.remove_snuffling(snuffling) 

1936 

1937 self._snufflings = [] 

1938 return settings 

1939 

1940 

1941class BrokenSnufflingModule(Exception): 

1942 pass 

1943 

1944 

1945class MyScrollArea(qw.QScrollArea): 

1946 

1947 def sizeHint(self): 

1948 ''' 

1949 ''' 

1950 

1951 s = qc.QSize() 

1952 s.setWidth(self.widget().sizeHint().width()) 

1953 s.setHeight(self.widget().sizeHint().height()) 

1954 return s 

1955 

1956 

1957class SwitchControl(qw.QCheckBox): 

1958 sw_toggled = qc.pyqtSignal(object, bool) 

1959 

1960 def __init__(self, ident, default, *args): 

1961 qw.QCheckBox.__init__(self, *args) 

1962 self.ident = ident 

1963 self.setChecked(default) 

1964 self.toggled.connect(self._sw_toggled) 

1965 

1966 def _sw_toggled(self, state): 

1967 self.sw_toggled.emit(self.ident, state) 

1968 

1969 def set_value(self, state): 

1970 self.blockSignals(True) 

1971 self.setChecked(state) 

1972 self.blockSignals(False) 

1973 

1974 

1975class ChoiceControl(qw.QFrame): 

1976 choosen = qc.pyqtSignal(object, object) 

1977 

1978 def __init__(self, ident, default, choices, name, *args): 

1979 qw.QFrame.__init__(self, *args) 

1980 self.label = qw.QLabel(name, self) 

1981 self.label.setMinimumWidth(120) 

1982 self.cbox = qw.QComboBox(self) 

1983 self.layout = qw.QHBoxLayout(self) 

1984 self.layout.addWidget(self.label) 

1985 self.layout.addWidget(self.cbox) 

1986 self.layout.setContentsMargins(0, 0, 0, 0) 

1987 self.layout.setSpacing(0) 

1988 self.ident = ident 

1989 self.choices = choices 

1990 for ichoice, choice in enumerate(choices): 

1991 self.cbox.addItem(choice) 

1992 

1993 self.set_value(default) 

1994 self.cbox.activated.connect(self.emit_choosen) 

1995 

1996 def set_choices(self, choices): 

1997 icur = self.cbox.currentIndex() 

1998 if icur != -1: 

1999 selected_choice = choices[icur] 

2000 else: 

2001 selected_choice = None 

2002 

2003 self.choices = choices 

2004 self.cbox.clear() 

2005 for ichoice, choice in enumerate(choices): 

2006 self.cbox.addItem(qc.QString(choice)) 

2007 

2008 if selected_choice is not None and selected_choice in choices: 

2009 self.set_value(selected_choice) 

2010 return selected_choice 

2011 else: 

2012 self.set_value(choices[0]) 

2013 return choices[0] 

2014 

2015 def emit_choosen(self, i): 

2016 self.choosen.emit( 

2017 self.ident, 

2018 self.choices[i]) 

2019 

2020 def set_value(self, v): 

2021 self.cbox.blockSignals(True) 

2022 for i, choice in enumerate(self.choices): 

2023 if choice == v: 

2024 self.cbox.setCurrentIndex(i) 

2025 self.cbox.blockSignals(False)