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

836 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-23 12:34 +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): 

447 ''' 

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

449 exception. 

450 

451 :param message: specifying the error 

452 ''' 

453 

454 self.error(message) 

455 raise SnufflingCallFailed(message) 

456 

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

458 ''' 

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

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

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

462 

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

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

465 ''' 

466 

467 if name is None: 

468 self._iplot += 1 

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

470 

471 fframe = FigureFrame(figure_cls=figure_cls) 

472 self._panel_parent.add_tab(name, fframe) 

473 if get == 'axes': 

474 return fframe.gca() 

475 elif get == 'figure': 

476 return fframe.gcf() 

477 elif get == 'figure_frame': 

478 return fframe 

479 

480 def figure(self, name=None): 

481 ''' 

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

483 

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

485 returned object ``fig``. 

486 

487 :param name: labels the tab of the figure 

488 ''' 

489 

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

491 

492 def axes(self, name=None): 

493 ''' 

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

495 

496 :param name: labels the tab of axes 

497 ''' 

498 

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

500 

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

502 ''' 

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

504 

505 :param name: labels the tab figure frame 

506 ''' 

507 

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

509 

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

511 ''' 

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

513 

514 :param name: labels the tab 

515 :param \\*args: 

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

517 :param \\*kwargs: 

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

519 :param plot_cls: 

520 if given, subclass to be used instead of 

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

522 ''' 

523 frame = SmartplotFrame( 

524 plot_args=args, 

525 plot_cls=plot_cls, 

526 plot_kwargs=kwargs) 

527 

528 self._panel_parent.add_tab(name, frame) 

529 return frame 

530 

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

532 ''' 

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

534 

535 :param name: labels the tab 

536 :param filename: name of file to be displayed 

537 ''' 

538 

539 f = PixmapFrame(filename) 

540 

541 scroll_area = qw.QScrollArea() 

542 scroll_area.setWidget(f) 

543 scroll_area.setWidgetResizable(True) 

544 

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

546 return f 

547 

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

549 ''' 

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

551 used as a browser within Snuffler. 

552 

553 :param url: url to open 

554 :param name: labels the tab 

555 ''' 

556 

557 if name is None: 

558 self._iplot += 1 

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

560 

561 f = WebKitFrame(url) 

562 self._panel_parent.add_tab(name, f) 

563 return f 

564 

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

566 ''' 

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

568 graphics. 

569 

570 :param actors: list of VTKActors 

571 :param name: labels the tab 

572 

573 Initialize the interactive rendering by calling the frames' 

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

575 renderer. 

576 

577 Requires installation of vtk including python wrapper. 

578 ''' 

579 if name is None: 

580 self._iplot += 1 

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

582 

583 try: 

584 f = VTKFrame(actors=actors) 

585 except ImportError as e: 

586 self.fail(e) 

587 

588 self._panel_parent.add_tab(name, f) 

589 return f 

590 

591 def tempdir(self): 

592 ''' 

593 Create a temporary directory and return its absolute path. 

594 

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

596 instance is deleted. 

597 ''' 

598 

599 if self._tempdir is None: 

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

601 

602 return self._tempdir 

603 

604 def set_live_update(self, live_update): 

605 ''' 

606 Enable/disable live updating. 

607 

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

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

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

611 item or pressing the call button. 

612 ''' 

613 

614 self._live_update = live_update 

615 if self._have_pre_process_hook: 

616 self._pre_process_hook_enabled = live_update 

617 if self._have_post_process_hook: 

618 self._post_process_hook_enabled = live_update 

619 

620 try: 

621 self.get_viewer().clean_update() 

622 except NoViewerSet: 

623 pass 

624 

625 def add_parameter(self, param): 

626 ''' 

627 Add an adjustable parameter to the snuffling. 

628 

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

630 :py:class:`Choice`. 

631 

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

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

634 ''' 

635 

636 self._parameters.append(param) 

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

638 

639 if self._panel is not None: 

640 self.delete_gui() 

641 self.setup_gui() 

642 

643 def add_trigger(self, name, method): 

644 ''' 

645 Add a button to the snuffling's panel. 

646 

647 :param name: string that labels the button 

648 :param method: method associated with the button 

649 ''' 

650 

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

652 

653 if self._panel is not None: 

654 self.delete_gui() 

655 self.setup_gui() 

656 

657 def get_parameters(self): 

658 ''' 

659 Get the snuffling's adjustable parameter definitions. 

660 

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

662 ''' 

663 

664 return self._parameters 

665 

666 def get_parameter(self, ident): 

667 ''' 

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

669 

670 :param ident: identifier of the parameter 

671 

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

673 ''' 

674 

675 for param in self._parameters: 

676 if param.ident == ident: 

677 return param 

678 return None 

679 

680 def set_parameter(self, ident, value): 

681 ''' 

682 Set one of the snuffling's adjustable parameters. 

683 

684 :param ident: identifier of the parameter 

685 :param value: new value of the parameter 

686 

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

688 ''' 

689 

690 self._set_parameter_value(ident, value) 

691 

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

693 if control: 

694 control.set_value(value) 

695 

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

697 ''' 

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

699 

700 :param ident: identifier of the parameter 

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

702 

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

704 ''' 

705 

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

707 if control: 

708 control.set_range(vmin, vmax) 

709 

710 def set_parameter_choices(self, ident, choices): 

711 ''' 

712 Update the choices of a Choice parameter. 

713 

714 :param ident: identifier of the parameter 

715 :param choices: list of strings 

716 ''' 

717 

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

719 if control: 

720 selected_choice = control.set_choices(choices) 

721 self._set_parameter_value(ident, selected_choice) 

722 

723 def _set_parameter_value(self, ident, value): 

724 setattr(self, ident, value) 

725 

726 def get_parameter_value(self, ident): 

727 ''' 

728 Get the current value of a parameter. 

729 

730 :param ident: identifier of the parameter 

731 ''' 

732 return getattr(self, ident) 

733 

734 def get_settings(self): 

735 ''' 

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

737 their values as the dictionaries values. 

738 ''' 

739 

740 params = self.get_parameters() 

741 settings = {} 

742 for param in params: 

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

744 

745 return settings 

746 

747 def set_settings(self, settings): 

748 params = self.get_parameters() 

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

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

751 if k in dparams: 

752 self._set_parameter_value(k, v) 

753 if k in self._param_controls: 

754 control = self._param_controls[k] 

755 control.set_value(v) 

756 

757 def get_viewer(self): 

758 ''' 

759 Get the parent viewer. 

760 

761 Returns a reference to an object of type 

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

763 main viewer widget. 

764 

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

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

767 ''' 

768 

769 if self._viewer is None: 

770 raise NoViewerSet() 

771 return self._viewer 

772 

773 def get_pile(self): 

774 ''' 

775 Get the pile. 

776 

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

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

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

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

781 mode. 

782 ''' 

783 

784 try: 

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

786 except NoViewerSet: 

787 if self._no_viewer_pile is None: 

788 self._no_viewer_pile = self.make_pile() 

789 

790 p = self._no_viewer_pile 

791 

792 return p 

793 

794 def get_active_event_and_stations( 

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

796 

797 ''' 

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

799 

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

801 query for available data 

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

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

804 

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

806 ''' 

807 

808 p = self.get_pile() 

809 v = self.get_viewer() 

810 

811 event = v.get_active_event() 

812 if event is None: 

813 self.fail( 

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

815 'it the "active event"') 

816 

817 stations = {} 

818 for traces in p.chopper( 

819 event.time+trange[0], 

820 event.time+trange[1], 

821 load_data=False, 

822 degap=False): 

823 

824 for tr in traces: 

825 try: 

826 for skey in v.station_keys(tr): 

827 if skey in stations: 

828 continue 

829 

830 station = v.get_station(skey) 

831 stations[skey] = station 

832 

833 except KeyError: 

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

835 % '.'.join(skey) 

836 

837 if missing == 'warn': 

838 logger.warning(s) 

839 elif missing == 'raise': 

840 raise MissingStationInformation(s) 

841 elif missing == 'ignore': 

842 pass 

843 else: 

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

845 

846 stations[skey] = None 

847 

848 return event, list(set( 

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

850 

851 def get_stations(self): 

852 ''' 

853 Get all stations known to the viewer. 

854 ''' 

855 

856 v = self.get_viewer() 

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

858 return stations 

859 

860 def get_markers(self): 

861 ''' 

862 Get all markers from the viewer. 

863 ''' 

864 

865 return self.get_viewer().get_markers() 

866 

867 def get_event_markers(self): 

868 ''' 

869 Get all event markers from the viewer. 

870 ''' 

871 

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

873 if isinstance(m, EventMarker)] 

874 

875 def get_selected_markers(self): 

876 ''' 

877 Get all selected markers from the viewer. 

878 ''' 

879 

880 return self.get_viewer().selected_markers() 

881 

882 def get_selected_event_markers(self): 

883 ''' 

884 Get all selected event markers from the viewer. 

885 ''' 

886 

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

888 if isinstance(m, EventMarker)] 

889 

890 def get_active_event_and_phase_markers(self): 

891 ''' 

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

893 ''' 

894 

895 viewer = self.get_viewer() 

896 markers = viewer.get_markers() 

897 event_marker = viewer.get_active_event_marker() 

898 if event_marker is None: 

899 self.fail( 

900 'No active event set. ' 

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

902 

903 event = event_marker.get_event() 

904 

905 selection = [] 

906 for m in markers: 

907 if isinstance(m, PhaseMarker): 

908 if m.get_event() is event: 

909 selection.append(m) 

910 

911 return ( 

912 event_marker, 

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

914 m.get_event() == event]) 

915 

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

917 ''' 

918 Get currently active trace selector from viewer. 

919 

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

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

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

923 to disable any restrictions. 

924 ''' 

925 

926 viewer = self.get_viewer() 

927 

928 def rtrue(tr): 

929 return True 

930 

931 if mode == 'inview': 

932 return viewer.trace_selector or rtrue 

933 elif mode == 'visible': 

934 return viewer.trace_filter or rtrue 

935 elif mode == 'all': 

936 return rtrue 

937 else: 

938 raise Exception('invalid mode argument') 

939 

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

941 mode='inview', main_bandpass=False, 

942 progress=None, responsive=False, 

943 *args, **kwargs): 

944 ''' 

945 Iterate over selected traces. 

946 

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

948 running snuffler. For each selected marker, 

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

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

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

952 *\\*args* and *\\*\\*kwargs*. 

953 

954 :param fallback: 

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

956 currently visible in the viewer. 

957 

958 :param marker_selector: 

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

960 

961 :param mode: 

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

963 shown in the viewer (excluding traces accessible through vertical 

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

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

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

967 restrictions. 

968 

969 :param main_bandpass: 

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

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

972 order Butterworth highpass and lowpass and the signal is always 

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

974 bandpass settings from the graphical interface are not respected 

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

976 artifacts. 

977 

978 :param progress: 

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

980 is used as the label for the progress bar. 

981 

982 :param responsive: 

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

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

985 to be aborted by the user. 

986 ''' 

987 

988 try: 

989 viewer = self.get_viewer() 

990 markers = [ 

991 m for m in viewer.selected_markers() 

992 if not isinstance(m, EventMarker)] 

993 

994 if marker_selector is not None: 

995 markers = [ 

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

997 

998 pile = self.get_pile() 

999 

1000 def rtrue(tr): 

1001 return True 

1002 

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

1004 trace_selector_viewer = self.get_viewer_trace_selector(mode) 

1005 

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

1007 

1008 if main_bandpass: 

1009 def apply_filters(traces): 

1010 for tr in traces: 

1011 if viewer.highpass is not None: 

1012 tr.highpass(4, viewer.highpass) 

1013 if viewer.lowpass is not None: 

1014 tr.lowpass(4, viewer.lowpass) 

1015 return traces 

1016 else: 

1017 def apply_filters(traces): 

1018 return traces 

1019 

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

1021 

1022 time_last = [time.time()] 

1023 

1024 def update_progress(label, batch): 

1025 time_now = time.time() 

1026 if responsive: 

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

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

1029 # changes etc. 

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

1031 qw.qApp.processEvents() 

1032 else: 

1033 # redraw about once a second 

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

1035 viewer.repaint() 

1036 

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

1038 

1039 abort = pb.set_status( 

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

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

1042 

1043 return abort 

1044 

1045 if markers: 

1046 for imarker, marker in enumerate(markers): 

1047 try: 

1048 if progress: 

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

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

1051 

1052 pb.set_status(label, 0, responsive) 

1053 

1054 if not marker.nslc_ids: 

1055 trace_selector_marker = rtrue 

1056 else: 

1057 def trace_selector_marker(tr): 

1058 return marker.match_nslc(tr.nslc_id) 

1059 

1060 def trace_selector(tr): 

1061 return trace_selector_arg(tr) \ 

1062 and trace_selector_viewer(tr) \ 

1063 and trace_selector_marker(tr) 

1064 

1065 for batch in pile.chopper( 

1066 tmin=marker.tmin, 

1067 tmax=marker.tmax, 

1068 trace_selector=trace_selector, 

1069 style='batch', 

1070 *args, 

1071 **kwargs): 

1072 

1073 if progress: 

1074 abort = update_progress(label, batch) 

1075 if abort: 

1076 return 

1077 

1078 batch.traces = apply_filters(batch.traces) 

1079 if style_arg == 'batch': 

1080 yield batch 

1081 else: 

1082 yield batch.traces 

1083 

1084 finally: 

1085 if progress: 

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

1087 

1088 elif fallback: 

1089 def trace_selector(tr): 

1090 return trace_selector_arg(tr) \ 

1091 and trace_selector_viewer(tr) 

1092 

1093 tmin, tmax = viewer.get_time_range() 

1094 

1095 if not pile.is_empty(): 

1096 ptmin = pile.get_tmin() 

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

1098 if ptmin > tmin: 

1099 tmin = ptmin + tpad 

1100 ptmax = pile.get_tmax() 

1101 if ptmax < tmax: 

1102 tmax = ptmax - tpad 

1103 

1104 try: 

1105 if progress: 

1106 label = progress 

1107 pb.set_status(label, 0, responsive) 

1108 

1109 for batch in pile.chopper( 

1110 tmin=tmin, 

1111 tmax=tmax, 

1112 trace_selector=trace_selector, 

1113 style='batch', 

1114 *args, 

1115 **kwargs): 

1116 

1117 if progress: 

1118 abort = update_progress(label, batch) 

1119 

1120 if abort: 

1121 return 

1122 

1123 batch.traces = apply_filters(batch.traces) 

1124 

1125 if style_arg == 'batch': 

1126 yield batch 

1127 else: 

1128 yield batch.traces 

1129 

1130 finally: 

1131 if progress: 

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

1133 

1134 else: 

1135 raise NoTracesSelected() 

1136 

1137 except NoViewerSet: 

1138 pile = self.get_pile() 

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

1140 

1141 def get_selected_time_range(self, fallback=False): 

1142 ''' 

1143 Get the time range spanning all selected markers. 

1144 

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

1146 end of visible time range 

1147 ''' 

1148 

1149 viewer = self.get_viewer() 

1150 markers = viewer.selected_markers() 

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

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

1153 

1154 if mins and maxs: 

1155 tmin = min(mins) 

1156 tmax = max(maxs) 

1157 

1158 elif fallback: 

1159 tmin, tmax = viewer.get_time_range() 

1160 

1161 else: 

1162 raise NoTracesSelected() 

1163 

1164 return tmin, tmax 

1165 

1166 def panel_visibility_changed(self, bool): 

1167 ''' 

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

1169 

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

1171 when the panel is activated the first time. 

1172 ''' 

1173 

1174 pass 

1175 

1176 def make_pile(self): 

1177 ''' 

1178 Create a pile. 

1179 

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

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

1182 arguments. 

1183 ''' 

1184 

1185 cachedirname = config.config().cache_dir 

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

1187 return pile.make_pile( 

1188 sources, 

1189 cachedirname=cachedirname, 

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

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

1192 

1193 def make_panel(self, parent): 

1194 ''' 

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

1196 

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

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

1199 parameters). 

1200 ''' 

1201 

1202 params = self.get_parameters() 

1203 self._param_controls = {} 

1204 if params or self._force_panel: 

1205 sarea = MyScrollArea(parent.get_panel_parent_widget()) 

1206 sarea.setFrameStyle(qw.QFrame.NoFrame) 

1207 sarea.setSizePolicy(qw.QSizePolicy( 

1208 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)) 

1209 frame = MyFrame(sarea) 

1210 frame.widgetVisibilityChanged.connect( 

1211 self.panel_visibility_changed) 

1212 

1213 frame.setSizePolicy(qw.QSizePolicy( 

1214 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1215 frame.setFrameStyle(qw.QFrame.NoFrame) 

1216 sarea.setWidget(frame) 

1217 sarea.setWidgetResizable(True) 

1218 layout = qw.QGridLayout() 

1219 layout.setContentsMargins(0, 0, 0, 0) 

1220 layout.setSpacing(0) 

1221 frame.setLayout(layout) 

1222 

1223 parlayout = qw.QGridLayout() 

1224 

1225 irow = 0 

1226 ipar = 0 

1227 have_switches = False 

1228 have_params = False 

1229 for iparam, param in enumerate(params): 

1230 if isinstance(param, Param): 

1231 if param.minimum <= 0.0: 

1232 param_control = LinValControl( 

1233 high_is_none=param.high_is_none, 

1234 low_is_none=param.low_is_none, 

1235 type=param.type) 

1236 else: 

1237 param_control = ValControl( 

1238 high_is_none=param.high_is_none, 

1239 low_is_none=param.low_is_none, 

1240 low_is_zero=param.low_is_zero, 

1241 type=param.type) 

1242 

1243 param_control.setup( 

1244 param.name, 

1245 param.minimum, 

1246 param.maximum, 

1247 param.default, 

1248 iparam) 

1249 

1250 param_control.set_tracking(param.tracking) 

1251 param_control.valchange.connect( 

1252 self.modified_snuffling_panel) 

1253 

1254 self._param_controls[param.ident] = param_control 

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

1256 parlayout.addWidget(w, ipar, iw) 

1257 

1258 ipar += 1 

1259 have_params = True 

1260 

1261 elif isinstance(param, Choice): 

1262 param_widget = ChoiceControl( 

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

1264 param_widget.choosen.connect( 

1265 self.choose_on_snuffling_panel) 

1266 

1267 self._param_controls[param.ident] = param_widget 

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

1269 ipar += 1 

1270 have_params = True 

1271 

1272 elif isinstance(param, Switch): 

1273 have_switches = True 

1274 

1275 if have_params: 

1276 parframe = qw.QFrame(sarea) 

1277 parframe.setSizePolicy(qw.QSizePolicy( 

1278 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1279 parframe.setLayout(parlayout) 

1280 layout.addWidget(parframe, irow, 0) 

1281 irow += 1 

1282 

1283 if have_switches: 

1284 swlayout = qw.QGridLayout() 

1285 isw = 0 

1286 for iparam, param in enumerate(params): 

1287 if isinstance(param, Switch): 

1288 param_widget = SwitchControl( 

1289 param.ident, param.default, param.name) 

1290 param_widget.sw_toggled.connect( 

1291 self.switch_on_snuffling_panel) 

1292 

1293 self._param_controls[param.ident] = param_widget 

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

1295 isw += 1 

1296 

1297 swframe = qw.QFrame(sarea) 

1298 swframe.setSizePolicy(qw.QSizePolicy( 

1299 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1300 swframe.setLayout(swlayout) 

1301 layout.addWidget(swframe, irow, 0) 

1302 irow += 1 

1303 

1304 butframe = qw.QFrame(sarea) 

1305 butframe.setSizePolicy(qw.QSizePolicy( 

1306 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1307 butlayout = qw.QHBoxLayout() 

1308 butframe.setLayout(butlayout) 

1309 

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

1311 if self._live_update: 

1312 live_update_checkbox.setCheckState(qc.Qt.Checked) 

1313 

1314 butlayout.addWidget(live_update_checkbox) 

1315 live_update_checkbox.toggled.connect( 

1316 self.live_update_toggled) 

1317 

1318 help_button = qw.QPushButton('Help') 

1319 butlayout.addWidget(help_button) 

1320 help_button.clicked.connect( 

1321 self.help_button_triggered) 

1322 

1323 clear_button = qw.QPushButton('Clear') 

1324 butlayout.addWidget(clear_button) 

1325 clear_button.clicked.connect( 

1326 self.clear_button_triggered) 

1327 

1328 call_button = qw.QPushButton('Run') 

1329 butlayout.addWidget(call_button) 

1330 call_button.clicked.connect( 

1331 self.call_button_triggered) 

1332 

1333 for name, method in self._triggers: 

1334 but = qw.QPushButton(name) 

1335 

1336 def call_and_update(method): 

1337 def f(): 

1338 self.check_call(method) 

1339 self.get_viewer().update() 

1340 return f 

1341 

1342 but.clicked.connect( 

1343 call_and_update(method)) 

1344 

1345 butlayout.addWidget(but) 

1346 

1347 layout.addWidget(butframe, irow, 0) 

1348 

1349 irow += 1 

1350 spacer = qw.QSpacerItem( 

1351 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

1352 

1353 layout.addItem(spacer, irow, 0) 

1354 

1355 return sarea 

1356 

1357 else: 

1358 return None 

1359 

1360 def make_helpmenuitem(self, parent): 

1361 ''' 

1362 Create the help menu item for the snuffling. 

1363 ''' 

1364 

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

1366 

1367 item.triggered.connect( 

1368 self.help_button_triggered) 

1369 

1370 return item 

1371 

1372 def make_menuitem(self, parent): 

1373 ''' 

1374 Create the menu item for the snuffling. 

1375 

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

1377 menu entry is wanted. 

1378 ''' 

1379 

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

1381 item.setCheckable( 

1382 self._have_pre_process_hook or self._have_post_process_hook) 

1383 

1384 item.triggered.connect( 

1385 self.menuitem_triggered) 

1386 

1387 return item 

1388 

1389 def output_filename( 

1390 self, 

1391 caption='Save File', 

1392 dir='', 

1393 filter='', 

1394 selected_filter=None): 

1395 

1396 ''' 

1397 Query user for an output filename. 

1398 

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

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

1401 dialog. 

1402 ''' 

1403 

1404 if not dir and self._previous_output_filename: 

1405 dir = self._previous_output_filename 

1406 

1407 fn = getSaveFileName( 

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

1409 if not fn: 

1410 raise UserCancelled() 

1411 

1412 self._previous_output_filename = fn 

1413 return str(fn) 

1414 

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

1416 ''' 

1417 Query user for an input directory. 

1418 

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

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

1421 dialog. 

1422 ''' 

1423 

1424 if not dir and self._previous_input_directory: 

1425 dir = self._previous_input_directory 

1426 

1427 dn = qw.QFileDialog.getExistingDirectory( 

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

1429 

1430 if not dn: 

1431 raise UserCancelled() 

1432 

1433 self._previous_input_directory = dn 

1434 return str(dn) 

1435 

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

1437 selected_filter=None): 

1438 ''' 

1439 Query user for an input filename. 

1440 

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

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

1443 dialog. 

1444 ''' 

1445 

1446 if not dir and self._previous_input_filename: 

1447 dir = self._previous_input_filename 

1448 

1449 fn, _ = qw.QFileDialog.getOpenFileName( 

1450 self.get_viewer(), 

1451 caption, 

1452 dir, 

1453 filter) 

1454 

1455 if not fn: 

1456 raise UserCancelled() 

1457 

1458 self._previous_input_filename = fn 

1459 return str(fn) 

1460 

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

1462 ''' 

1463 Query user for a text input. 

1464 

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

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

1467 dialog. 

1468 ''' 

1469 

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

1471 

1472 if not ok: 

1473 raise UserCancelled() 

1474 

1475 return inp 

1476 

1477 def modified_snuffling_panel(self, value, iparam): 

1478 ''' 

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

1480 

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

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

1483 widget. 

1484 ''' 

1485 

1486 param = self.get_parameters()[iparam] 

1487 self._set_parameter_value(param.ident, value) 

1488 if self._live_update: 

1489 self.check_call(self.call) 

1490 self.get_viewer().update() 

1491 

1492 def switch_on_snuffling_panel(self, ident, state): 

1493 ''' 

1494 Called when the user has toggled a switchable parameter. 

1495 ''' 

1496 

1497 self._set_parameter_value(ident, state) 

1498 if self._live_update: 

1499 self.check_call(self.call) 

1500 self.get_viewer().update() 

1501 

1502 def choose_on_snuffling_panel(self, ident, state): 

1503 ''' 

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

1505 ''' 

1506 

1507 self._set_parameter_value(ident, state) 

1508 if self._live_update: 

1509 self.check_call(self.call) 

1510 self.get_viewer().update() 

1511 

1512 def menuitem_triggered(self, arg): 

1513 ''' 

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

1515 

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

1517 and triggers an update on the viewer widget. 

1518 ''' 

1519 

1520 self.check_call(self.call) 

1521 

1522 if self._have_pre_process_hook: 

1523 self._pre_process_hook_enabled = arg 

1524 

1525 if self._have_post_process_hook: 

1526 self._post_process_hook_enabled = arg 

1527 

1528 if self._have_pre_process_hook or self._have_post_process_hook: 

1529 self.get_viewer().clean_update() 

1530 else: 

1531 self.get_viewer().update() 

1532 

1533 def call_button_triggered(self): 

1534 ''' 

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

1536 

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

1538 and triggers an update on the viewer widget. 

1539 ''' 

1540 

1541 self.check_call(self.call) 

1542 self.get_viewer().update() 

1543 

1544 def clear_button_triggered(self): 

1545 ''' 

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

1547 

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

1549 viewer widget. 

1550 ''' 

1551 

1552 self.cleanup() 

1553 self.get_viewer().update() 

1554 

1555 def help(self): 

1556 ''' 

1557 Get help text in html/markdown. 

1558 ''' 

1559 

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

1561 # should overload .help() 

1562 return self.__doc__ or '' 

1563 

1564 def help_button_triggered(self): 

1565 ''' 

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

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

1568 ''' 

1569 

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

1571 

1572 if s: 

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

1574 doc = qw.QLabel(s) 

1575 else: 

1576 try: 

1577 import markdown 

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

1579 

1580 except ImportError: 

1581 logger.error( 

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

1583 'formatting.') 

1584 

1585 doc = qw.QLabel(s) 

1586 else: 

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

1588 

1589 labels = [doc] 

1590 

1591 if self._filename: 

1592 from html import escape 

1593 

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

1595 

1596 doc_src = qw.QLabel( 

1597 '''<html><body> 

1598<hr /> 

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

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

1601<br /> 

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

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

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

1605 % ( 

1606 quote(self._filename), 

1607 escape(self._filename), 

1608 escape(code))) 

1609 

1610 labels.append(doc_src) 

1611 

1612 for h in labels: 

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

1614 h.setWordWrap(True) 

1615 

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

1617 

1618 def live_update_toggled(self, on): 

1619 ''' 

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

1621 ''' 

1622 

1623 self.set_live_update(on) 

1624 

1625 def add_traces(self, traces): 

1626 ''' 

1627 Add traces to the viewer. 

1628 

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

1630 

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

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

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

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

1635 traces are added. 

1636 

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

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

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

1640 method). 

1641 ''' 

1642 

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

1644 self._tickets.append(ticket) 

1645 return ticket 

1646 

1647 def add_trace(self, tr): 

1648 ''' 

1649 Add a trace to the viewer. 

1650 

1651 See :py:meth:`add_traces`. 

1652 ''' 

1653 

1654 self.add_traces([tr]) 

1655 

1656 def add_markers(self, markers): 

1657 ''' 

1658 Add some markers to the display. 

1659 

1660 Takes a list of objects of type 

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

1662 viewer. 

1663 ''' 

1664 

1665 self.get_viewer().add_markers(markers) 

1666 self._markers.extend(markers) 

1667 

1668 def add_marker(self, marker): 

1669 ''' 

1670 Add a marker to the display. 

1671 

1672 See :py:meth:`add_markers`. 

1673 ''' 

1674 

1675 self.add_markers([marker]) 

1676 

1677 def cleanup(self): 

1678 ''' 

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

1680 snuffling. 

1681 ''' 

1682 

1683 try: 

1684 viewer = self.get_viewer() 

1685 viewer.release_data(self._tickets) 

1686 viewer.remove_markers(self._markers) 

1687 

1688 except NoViewerSet: 

1689 pass 

1690 

1691 self._tickets = [] 

1692 self._markers = [] 

1693 

1694 def check_call(self, method): 

1695 

1696 if method in self._call_in_progress: 

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

1698 return 

1699 

1700 try: 

1701 self._call_in_progress[method] = True 

1702 method() 

1703 return 0 

1704 

1705 except SnufflingError as e: 

1706 if not isinstance(e, SnufflingCallFailed): 

1707 # those have logged within error() 

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

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

1710 return 1 

1711 

1712 except Exception as e: 

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

1714 self._name, str(e)) 

1715 

1716 logger.exception(message) 

1717 self.show_message('error', message) 

1718 

1719 finally: 

1720 del self._call_in_progress[method] 

1721 

1722 def call(self): 

1723 ''' 

1724 Main work routine of the snuffling. 

1725 

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

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

1728 in subclass. The default implementation does nothing useful. 

1729 ''' 

1730 

1731 pass 

1732 

1733 def pre_process_hook(self, traces): 

1734 return traces 

1735 

1736 def post_process_hook(self, traces): 

1737 return traces 

1738 

1739 def get_tpad(self): 

1740 ''' 

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

1742 ''' 

1743 

1744 return 0.0 

1745 

1746 def pre_destroy(self): 

1747 ''' 

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

1749 

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

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

1752 the snuffling`s tempory directory, if needed. 

1753 ''' 

1754 

1755 self.cleanup() 

1756 if self._tempdir is not None: 

1757 import shutil 

1758 shutil.rmtree(self._tempdir) 

1759 

1760 

1761class SnufflingError(Exception): 

1762 ''' 

1763 Base exception for Snuffling errors. 

1764 ''' 

1765 pass 

1766 

1767 

1768class NoViewerSet(SnufflingError): 

1769 ''' 

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

1771 ''' 

1772 

1773 def __str__(self): 

1774 return 'No GUI available. ' \ 

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

1776 

1777 

1778class MissingStationInformation(SnufflingError): 

1779 ''' 

1780 Raised when station information is missing. 

1781 ''' 

1782 

1783 

1784class NoTracesSelected(SnufflingError): 

1785 ''' 

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

1787 and we cannot fallback to using the current view. 

1788 ''' 

1789 

1790 def __str__(self): 

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

1792 

1793 

1794class UserCancelled(SnufflingError): 

1795 ''' 

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

1797 ''' 

1798 

1799 def __str__(self): 

1800 return 'The user has cancelled a dialog.' 

1801 

1802 

1803class SnufflingCallFailed(SnufflingError): 

1804 ''' 

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

1806 :py:meth:`Snuffling.call`. 

1807 ''' 

1808 

1809 

1810class InvalidSnufflingFilename(Exception): 

1811 pass 

1812 

1813 

1814class SnufflingModule(object): 

1815 ''' 

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

1817 

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

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

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

1821 use of the snufflings (e.g. 

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

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

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

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

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

1827 when needed. 

1828 ''' 

1829 

1830 mtimes = {} 

1831 

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

1833 self._path = path 

1834 self._name = name 

1835 self._mtime = None 

1836 self._module = None 

1837 self._snufflings = [] 

1838 self._handler = handler 

1839 

1840 def load_if_needed(self): 

1841 ''' 

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

1843 ''' 

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

1845 

1846 try: 

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

1848 except OSError as e: 

1849 if e.errno == 2: 

1850 logger.error(e) 

1851 raise BrokenSnufflingModule(filename) 

1852 

1853 if self._module is None: 

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

1855 try: 

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

1857 if self._name in sys.modules: 

1858 raise InvalidSnufflingFilename(self._name) 

1859 

1860 self._module = __import__(self._name) 

1861 del sys.modules[self._name] 

1862 

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

1864 snuffling._filename = filename 

1865 self.add_snuffling(snuffling) 

1866 

1867 except (Exception, SystemExit): 

1868 logger.error(traceback.format_exc()) 

1869 raise BrokenSnufflingModule(filename) 

1870 

1871 finally: 

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

1873 

1874 elif self._mtime != mtime: 

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

1876 settings = self.remove_snufflings() 

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

1878 try: 

1879 

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

1881 

1882 reload(self._module) 

1883 del sys.modules[self._name] 

1884 

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

1886 snuffling._filename = filename 

1887 self.add_snuffling(snuffling, reloaded=True) 

1888 

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

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

1891 snuf.set_settings(sett) 

1892 

1893 except (Exception, SystemExit): 

1894 logger.error(traceback.format_exc()) 

1895 raise BrokenSnufflingModule(filename) 

1896 

1897 finally: 

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

1899 

1900 self._mtime = mtime 

1901 

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

1903 ''' 

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

1905 ''' 

1906 snuffling._path = self._path 

1907 snuffling.setup() 

1908 self._snufflings.append(snuffling) 

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

1910 

1911 def remove_snufflings(self): 

1912 ''' 

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

1914 ''' 

1915 settings = [] 

1916 for snuffling in self._snufflings: 

1917 settings.append(snuffling.get_settings()) 

1918 self._handler.remove_snuffling(snuffling) 

1919 

1920 self._snufflings = [] 

1921 return settings 

1922 

1923 

1924class BrokenSnufflingModule(Exception): 

1925 pass 

1926 

1927 

1928class MyScrollArea(qw.QScrollArea): 

1929 

1930 def sizeHint(self): 

1931 ''' 

1932 ''' 

1933 

1934 s = qc.QSize() 

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

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

1937 return s 

1938 

1939 

1940class SwitchControl(qw.QCheckBox): 

1941 sw_toggled = qc.pyqtSignal(object, bool) 

1942 

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

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

1945 self.ident = ident 

1946 self.setChecked(default) 

1947 self.toggled.connect(self._sw_toggled) 

1948 

1949 def _sw_toggled(self, state): 

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

1951 

1952 def set_value(self, state): 

1953 self.blockSignals(True) 

1954 self.setChecked(state) 

1955 self.blockSignals(False) 

1956 

1957 

1958class ChoiceControl(qw.QFrame): 

1959 choosen = qc.pyqtSignal(object, object) 

1960 

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

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

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

1964 self.label.setMinimumWidth(120) 

1965 self.cbox = qw.QComboBox(self) 

1966 self.layout = qw.QHBoxLayout(self) 

1967 self.layout.addWidget(self.label) 

1968 self.layout.addWidget(self.cbox) 

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

1970 self.layout.setSpacing(0) 

1971 self.ident = ident 

1972 self.choices = choices 

1973 for ichoice, choice in enumerate(choices): 

1974 self.cbox.addItem(choice) 

1975 

1976 self.set_value(default) 

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

1978 

1979 def set_choices(self, choices): 

1980 icur = self.cbox.currentIndex() 

1981 if icur != -1: 

1982 selected_choice = choices[icur] 

1983 else: 

1984 selected_choice = None 

1985 

1986 self.choices = choices 

1987 self.cbox.clear() 

1988 for ichoice, choice in enumerate(choices): 

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

1990 

1991 if selected_choice is not None and selected_choice in choices: 

1992 self.set_value(selected_choice) 

1993 return selected_choice 

1994 else: 

1995 self.set_value(choices[0]) 

1996 return choices[0] 

1997 

1998 def emit_choosen(self, i): 

1999 self.choosen.emit( 

2000 self.ident, 

2001 self.choices[i]) 

2002 

2003 def set_value(self, v): 

2004 self.cbox.blockSignals(True) 

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

2006 if choice == v: 

2007 self.cbox.setCurrentIndex(i) 

2008 self.cbox.blockSignals(False)