1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

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

5''' 

6Snuffling infrastructure 

7 

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

9snufflings and some utilities for their handling. 

10''' 

11from __future__ import absolute_import 

12 

13import os 

14import sys 

15import time 

16import logging 

17import traceback 

18import tempfile 

19 

20from .qt_compat import qc, qw, getSaveFileName, use_pyqt5 

21 

22from pyrocko import pile, config 

23from pyrocko.util import quote 

24 

25from .util import (ValControl, LinValControl, FigureFrame, WebKitFrame, 

26 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker, 

27 load_markers, save_markers) 

28 

29 

30if sys.version_info >= (3, 0): 

31 from importlib import reload 

32 

33 

34Marker, load_markers, save_markers # noqa 

35 

36logger = logging.getLogger('pyrocko.gui.snuffling') 

37 

38 

39def fnpatch(x): 

40 if use_pyqt5: 

41 return x 

42 else: 

43 return x, None 

44 

45 

46class MyFrame(qw.QFrame): 

47 widgetVisibilityChanged = qc.pyqtSignal(bool) 

48 

49 def showEvent(self, ev): 

50 self.widgetVisibilityChanged.emit(True) 

51 

52 def hideEvent(self, ev): 

53 self.widgetVisibilityChanged.emit(False) 

54 

55 

56class Param(object): 

57 ''' 

58 Definition of an adjustable floating point parameter for the 

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

60 such parameters. 

61 

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

63 :param ident: identifier of the parameter 

64 :param default: default value 

65 :param minimum: minimum value for the parameter 

66 :param maximum: maximum value for the parameter 

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

68 of parameter range (optional) 

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

70 of parameter range (optional) 

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

72 value of parameter range (optional) 

73 ''' 

74 

75 def __init__( 

76 self, name, ident, default, minimum, maximum, 

77 low_is_none=None, 

78 high_is_none=None, 

79 low_is_zero=False, 

80 tracking=True, 

81 type=float): 

82 

83 if low_is_none and default == minimum: 

84 default = None 

85 if high_is_none and default == maximum: 

86 default = None 

87 

88 self.name = name 

89 self.ident = ident 

90 self.default = default 

91 self.minimum = minimum 

92 self.maximum = maximum 

93 self.low_is_none = low_is_none 

94 self.high_is_none = high_is_none 

95 self.low_is_zero = low_is_zero 

96 self.tracking = tracking 

97 self.type = type 

98 

99 self._control = None 

100 

101 

102class Switch(object): 

103 ''' 

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

105 may display a checkbox for such a switch. 

106 

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

108 :param ident: identifier of the parameter 

109 :param default: default value 

110 ''' 

111 

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

113 self.name = name 

114 self.ident = ident 

115 self.default = default 

116 

117 

118class Choice(object): 

119 ''' 

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

121 may display a menu for such a choice. 

122 

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

124 :param ident: identifier of the parameter 

125 :param default: default value 

126 :param choices: tuple of other options 

127 ''' 

128 

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

130 self.name = name 

131 self.ident = ident 

132 self.default = default 

133 self.choices = choices 

134 

135 

136class Snuffling(object): 

137 ''' 

138 Base class for user snufflings. 

139 

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

141 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in 

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

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

144 

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

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

147 ''' 

148 

149 def __init__(self): 

150 self._path = None 

151 

152 self._name = 'Untitled Snuffling' 

153 self._viewer = None 

154 self._tickets = [] 

155 self._markers = [] 

156 

157 self._delete_panel = None 

158 self._delete_menuitem = None 

159 

160 self._panel_parent = None 

161 self._menu_parent = None 

162 

163 self._panel = None 

164 self._menuitem = None 

165 self._helpmenuitem = None 

166 self._parameters = [] 

167 self._param_controls = {} 

168 

169 self._triggers = [] 

170 

171 self._live_update = True 

172 self._previous_output_filename = None 

173 self._previous_input_filename = None 

174 self._previous_input_directory = None 

175 

176 self._tempdir = None 

177 self._iplot = 0 

178 

179 self._have_pre_process_hook = False 

180 self._have_post_process_hook = False 

181 self._pre_process_hook_enabled = False 

182 self._post_process_hook_enabled = False 

183 

184 self._no_viewer_pile = None 

185 self._cli_params = {} 

186 self._filename = None 

187 self._force_panel = False 

188 self._call_in_progress = False 

189 

190 def setup(self): 

191 ''' 

192 Setup the snuffling. 

193 

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

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

196 ''' 

197 

198 pass 

199 

200 def module_dir(self): 

201 ''' 

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

203 

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

205 ''' 

206 

207 return self._path 

208 

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

210 ''' 

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

212 

213 This method is called from the 

214 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls 

215 :py:meth:`setup_gui`. 

216 ''' 

217 

218 self._viewer = viewer 

219 self._panel_parent = panel_parent 

220 self._menu_parent = menu_parent 

221 

222 self.setup_gui(reloaded=reloaded) 

223 

224 def setup_gui(self, reloaded=False): 

225 ''' 

226 Create and add gui elements to the viewer. 

227 

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

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

230 snuffling has been changed. 

231 ''' 

232 

233 if self._panel_parent is not None: 

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

235 if self._panel: 

236 self._panel_parent.add_panel( 

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

238 

239 if self._menu_parent is not None: 

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

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

242 if self._menuitem: 

243 self._menu_parent.add_snuffling_menuitem(self._menuitem) 

244 

245 if self._helpmenuitem: 

246 self._menu_parent.add_snuffling_help_menuitem( 

247 self._helpmenuitem) 

248 

249 def set_force_panel(self, bool=True): 

250 ''' 

251 Force to create a panel. 

252 

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

254 button. 

255 ''' 

256 

257 self._force_panel = bool 

258 

259 def make_cli_parser1(self): 

260 import optparse 

261 

262 class MyOptionParser(optparse.OptionParser): 

263 def error(self, msg): 

264 logger.error(msg) 

265 self.exit(1) 

266 

267 parser = MyOptionParser() 

268 

269 parser.add_option( 

270 '--format', 

271 dest='format', 

272 default='from_extension', 

273 choices=( 

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

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

276 'from_extension', 'detect'), 

277 help='assume files are of given FORMAT [default: \'%default\']') 

278 

279 parser.add_option( 

280 '--pattern', 

281 dest='regex', 

282 metavar='REGEX', 

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

284 

285 self.add_params_to_cli_parser(parser) 

286 self.configure_cli_parser(parser) 

287 return parser 

288 

289 def configure_cli_parser(self, parser): 

290 pass 

291 

292 def cli_usage(self): 

293 return None 

294 

295 def add_params_to_cli_parser(self, parser): 

296 

297 for param in self._parameters: 

298 if isinstance(param, Param): 

299 parser.add_option( 

300 '--' + param.ident, 

301 dest=param.ident, 

302 default=param.default, 

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

304 help=param.name) 

305 

306 def setup_cli(self): 

307 self.setup() 

308 parser = self.make_cli_parser1() 

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

310 

311 for param in self._parameters: 

312 if isinstance(param, Param): 

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

314 

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

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

317 self._cli_params['sources'] = args 

318 

319 return options, args, parser 

320 

321 def delete_gui(self): 

322 ''' 

323 Remove the gui elements of the snuffling. 

324 

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

326 also removes all traces and markers added with the 

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

328 ''' 

329 

330 self.cleanup() 

331 

332 if self._panel is not None: 

333 self._panel_parent.remove_panel(self._panel) 

334 self._panel = None 

335 

336 if self._menuitem is not None: 

337 self._menu_parent.remove_snuffling_menuitem(self._menuitem) 

338 self._menuitem = None 

339 

340 if self._helpmenuitem is not None: 

341 self._menu_parent.remove_snuffling_help_menuitem( 

342 self._helpmenuitem) 

343 

344 def set_name(self, name): 

345 ''' 

346 Set the snuffling's name. 

347 

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

349 ''' 

350 

351 self._name = name 

352 self.reset_gui() 

353 

354 def get_name(self): 

355 ''' 

356 Get the snuffling's name. 

357 ''' 

358 

359 return self._name 

360 

361 def set_have_pre_process_hook(self, bool): 

362 self._have_pre_process_hook = bool 

363 self._live_update = False 

364 self._pre_process_hook_enabled = False 

365 self.reset_gui() 

366 

367 def set_have_post_process_hook(self, bool): 

368 self._have_post_process_hook = bool 

369 self._live_update = False 

370 self._post_process_hook_enabled = False 

371 self.reset_gui() 

372 

373 def set_have_pile_changed_hook(self, bool): 

374 self._pile_ = False 

375 

376 def enable_pile_changed_notifications(self): 

377 ''' 

378 Get informed when pile changed. 

379 

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

381 update in the viewer's pile. 

382 ''' 

383 

384 viewer = self.get_viewer() 

385 viewer.pile_has_changed_signal.connect( 

386 self.pile_changed) 

387 

388 def disable_pile_changed_notifications(self): 

389 ''' 

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

391 ''' 

392 

393 viewer = self.get_viewer() 

394 viewer.pile_has_changed_signal.disconnect( 

395 self.pile_changed) 

396 

397 def pile_changed(self): 

398 ''' 

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

400 

401 Must be activated with a call to 

402 :py:meth:`enable_pile_changed_notifications`. 

403 ''' 

404 

405 pass 

406 

407 def reset_gui(self, reloaded=False): 

408 ''' 

409 Delete and recreate the snuffling's panel. 

410 ''' 

411 

412 if self._panel or self._menuitem: 

413 sett = self.get_settings() 

414 self.delete_gui() 

415 self.setup_gui(reloaded=reloaded) 

416 self.set_settings(sett) 

417 

418 def show_message(self, kind, message): 

419 ''' 

420 Display a message box. 

421 

422 :param kind: string defining kind of message 

423 :param message: the message to be displayed 

424 ''' 

425 

426 try: 

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

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

429 box.exec_() 

430 except NoViewerSet: 

431 pass 

432 

433 def error(self, message): 

434 ''' 

435 Show an error message box. 

436 

437 :param message: specifying the error 

438 ''' 

439 

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

441 self.show_message('error', message) 

442 

443 def warn(self, message): 

444 ''' 

445 Display a warning message. 

446 

447 :param message: specifying the warning 

448 ''' 

449 

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

451 self.show_message('warning', message) 

452 

453 def fail(self, message): 

454 ''' 

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

456 exception. 

457 

458 :param message: specifying the error 

459 ''' 

460 

461 self.error(message) 

462 raise SnufflingCallFailed(message) 

463 

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

465 ''' 

466 Create a :py:class:`FigureFrame` and return either the frame, 

467 a :py:class:`matplotlib.figure.Figure` instance or a 

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

469 

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

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

472 ''' 

473 

474 if name is None: 

475 self._iplot += 1 

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

477 

478 fframe = FigureFrame() 

479 self._panel_parent.add_tab(name, fframe) 

480 if get == 'axes': 

481 return fframe.gca() 

482 elif get == 'figure': 

483 return fframe.gcf() 

484 elif get == 'figure_frame': 

485 return fframe 

486 

487 def figure(self, name=None): 

488 ''' 

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

490 which can be displayed within snuffler by calling 

491 :py:meth:`canvas.draw`. 

492 

493 :param name: labels the tab of the figure 

494 ''' 

495 

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

497 

498 def axes(self, name=None): 

499 ''' 

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

501 

502 :param name: labels the tab of axes 

503 ''' 

504 

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

506 

507 def figure_frame(self, name=None): 

508 ''' 

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

510 

511 :param name: labels the tab figure frame 

512 ''' 

513 

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

515 

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

517 ''' 

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

519 

520 :param name: labels the tab 

521 :param filename: name of file to be displayed 

522 ''' 

523 

524 f = PixmapFrame(filename) 

525 

526 scroll_area = qw.QScrollArea() 

527 scroll_area.setWidget(f) 

528 scroll_area.setWidgetResizable(True) 

529 

530 self._panel_parent.add_tab(name or "Pixmap", scroll_area) 

531 return f 

532 

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

534 ''' 

535 Creates a :py:class:`WebKitFrame` which can be used as a browser 

536 within snuffler. 

537 

538 :param url: url to open 

539 :param name: labels the tab 

540 ''' 

541 

542 if name is None: 

543 self._iplot += 1 

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

545 

546 f = WebKitFrame(url) 

547 self._panel_parent.add_tab(name, f) 

548 return f 

549 

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

551 ''' 

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

553 graphics. 

554 

555 :param actors: list of VTKActors 

556 :param name: labels the tab 

557 

558 Initialize the interactive rendering by calling the frames' 

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

560 renderer. 

561 

562 Requires installation of vtk including python wrapper. 

563 ''' 

564 if name is None: 

565 self._iplot += 1 

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

567 

568 try: 

569 f = VTKFrame(actors=actors) 

570 except ImportError as e: 

571 self.fail(e) 

572 

573 self._panel_parent.add_tab(name, f) 

574 return f 

575 

576 def tempdir(self): 

577 ''' 

578 Create a temporary directory and return its absolute path. 

579 

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

581 instance is deleted. 

582 ''' 

583 

584 if self._tempdir is None: 

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

586 

587 return self._tempdir 

588 

589 def set_live_update(self, live_update): 

590 ''' 

591 Enable/disable live updating. 

592 

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

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

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

596 item or pressing the call button. 

597 ''' 

598 

599 self._live_update = live_update 

600 if self._have_pre_process_hook: 

601 self._pre_process_hook_enabled = live_update 

602 if self._have_post_process_hook: 

603 self._post_process_hook_enabled = live_update 

604 

605 try: 

606 self.get_viewer().clean_update() 

607 except NoViewerSet: 

608 pass 

609 

610 def add_parameter(self, param): 

611 ''' 

612 Add an adjustable parameter to the snuffling. 

613 

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

615 :py:class:`Choice`. 

616 

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

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

619 ''' 

620 

621 self._parameters.append(param) 

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

623 

624 if self._panel is not None: 

625 self.delete_gui() 

626 self.setup_gui() 

627 

628 def add_trigger(self, name, method): 

629 ''' 

630 Add a button to the snuffling's panel. 

631 

632 :param name: string that labels the button 

633 :param method: method associated with the button 

634 ''' 

635 

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

637 

638 if self._panel is not None: 

639 self.delete_gui() 

640 self.setup_gui() 

641 

642 def get_parameters(self): 

643 ''' 

644 Get the snuffling's adjustable parameter definitions. 

645 

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

647 ''' 

648 

649 return self._parameters 

650 

651 def get_parameter(self, ident): 

652 ''' 

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

654 

655 :param ident: identifier of the parameter 

656 

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

658 ''' 

659 

660 for param in self._parameters: 

661 if param.ident == ident: 

662 return param 

663 return None 

664 

665 def set_parameter(self, ident, value): 

666 ''' 

667 Set one of the snuffling's adjustable parameters. 

668 

669 :param ident: identifier of the parameter 

670 :param value: new value of the parameter 

671 

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

673 ''' 

674 

675 self._set_parameter_value(ident, value) 

676 

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

678 if control: 

679 control.set_value(value) 

680 

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

682 ''' 

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

684 

685 :param ident: identifier of the parameter 

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

687 

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

689 ''' 

690 

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

692 if control: 

693 control.set_range(vmin, vmax) 

694 

695 def set_parameter_choices(self, ident, choices): 

696 ''' 

697 Update the choices of a Choice parameter. 

698 

699 :param ident: identifier of the parameter 

700 :param choices: list of strings 

701 ''' 

702 

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

704 if control: 

705 selected_choice = control.set_choices(choices) 

706 self._set_parameter_value(ident, selected_choice) 

707 

708 def _set_parameter_value(self, ident, value): 

709 setattr(self, ident, value) 

710 

711 def get_parameter_value(self, ident): 

712 ''' 

713 Get the current value of a parameter. 

714 

715 :param ident: identifier of the parameter 

716 ''' 

717 return getattr(self, ident) 

718 

719 def get_settings(self): 

720 ''' 

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

722 their values as the dictionaries values. 

723 ''' 

724 

725 params = self.get_parameters() 

726 settings = {} 

727 for param in params: 

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

729 

730 return settings 

731 

732 def set_settings(self, settings): 

733 params = self.get_parameters() 

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

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

736 if k in dparams: 

737 self._set_parameter_value(k, v) 

738 if k in self._param_controls: 

739 control = self._param_controls[k] 

740 control.set_value(v) 

741 

742 def get_viewer(self): 

743 ''' 

744 Get the parent viewer. 

745 

746 Returns a reference to an object of type :py:class:`PileOverview`, 

747 which is the main viewer widget. 

748 

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

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

751 ''' 

752 

753 if self._viewer is None: 

754 raise NoViewerSet() 

755 return self._viewer 

756 

757 def get_pile(self): 

758 ''' 

759 Get the pile. 

760 

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

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

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

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

765 mode. 

766 ''' 

767 

768 try: 

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

770 except NoViewerSet: 

771 if self._no_viewer_pile is None: 

772 self._no_viewer_pile = self.make_pile() 

773 

774 p = self._no_viewer_pile 

775 

776 return p 

777 

778 def get_active_event_and_stations( 

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

780 

781 ''' 

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

783 

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

785 query for available data 

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

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

788 

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

790 ''' 

791 

792 p = self.get_pile() 

793 v = self.get_viewer() 

794 

795 event = v.get_active_event() 

796 if event is None: 

797 self.fail( 

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

799 'it the "active event"') 

800 

801 stations = {} 

802 for traces in p.chopper( 

803 event.time+trange[0], 

804 event.time+trange[1], 

805 load_data=False, 

806 degap=False): 

807 

808 for tr in traces: 

809 try: 

810 for skey in v.station_keys(tr): 

811 if skey in stations: 

812 continue 

813 

814 station = v.get_station(skey) 

815 stations[skey] = station 

816 

817 except KeyError: 

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

819 % '.'.join(skey) 

820 

821 if missing == 'warn': 

822 logger.warning(s) 

823 elif missing == 'raise': 

824 raise MissingStationInformation(s) 

825 elif missing == 'ignore': 

826 pass 

827 else: 

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

829 

830 stations[skey] = None 

831 

832 return event, list(set( 

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

834 

835 def get_stations(self): 

836 ''' 

837 Get all stations known to the viewer. 

838 ''' 

839 

840 v = self.get_viewer() 

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

842 return stations 

843 

844 def get_markers(self): 

845 ''' 

846 Get all markers from the viewer. 

847 ''' 

848 

849 return self.get_viewer().get_markers() 

850 

851 def get_event_markers(self): 

852 ''' 

853 Get all event markers from the viewer. 

854 ''' 

855 

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

857 if isinstance(m, EventMarker)] 

858 

859 def get_selected_markers(self): 

860 ''' 

861 Get all selected markers from the viewer. 

862 ''' 

863 

864 return self.get_viewer().selected_markers() 

865 

866 def get_selected_event_markers(self): 

867 ''' 

868 Get all selected event markers from the viewer. 

869 ''' 

870 

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

872 if isinstance(m, EventMarker)] 

873 

874 def get_active_event_and_phase_markers(self): 

875 ''' 

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

877 ''' 

878 

879 viewer = self.get_viewer() 

880 markers = viewer.get_markers() 

881 event_marker = viewer.get_active_event_marker() 

882 if event_marker is None: 

883 self.fail( 

884 'No active event set. ' 

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

886 

887 event = event_marker.get_event() 

888 

889 selection = [] 

890 for m in markers: 

891 if isinstance(m, PhaseMarker): 

892 if m.get_event() is event: 

893 selection.append(m) 

894 

895 return ( 

896 event_marker, 

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

898 m.get_event() == event]) 

899 

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

901 ''' 

902 Get currently active trace selector from viewer. 

903 

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

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

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

907 to disable any restrictions. 

908 ''' 

909 

910 viewer = self.get_viewer() 

911 

912 def rtrue(tr): 

913 return True 

914 

915 if mode == 'inview': 

916 return viewer.trace_selector or rtrue 

917 elif mode == 'visible': 

918 return viewer.trace_filter or rtrue 

919 elif mode == 'all': 

920 return rtrue 

921 else: 

922 raise Exception('invalid mode argument') 

923 

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

925 mode='inview', main_bandpass=False, 

926 progress=None, responsive=False, 

927 *args, **kwargs): 

928 ''' 

929 Iterate over selected traces. 

930 

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

932 running snuffler. For each selected marker, 

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

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

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

936 *\\*args* and *\\*\\*kwargs*. 

937 

938 :param fallback: 

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

940 currently visible in the viewer. 

941 

942 :param marker_selector: 

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

944 

945 :param mode: 

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

947 shown in the viewer (excluding traces accessible through vertical 

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

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

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

951 restrictions. 

952 

953 :param main_bandpass: 

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

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

956 order Butterworth highpass and lowpass and the signal is always 

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

958 bandpass settings from the graphical interface are not respected 

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

960 artifacts. 

961 

962 :param progress: 

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

964 is used as the label for the progress bar. 

965 

966 :param responsive: 

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

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

969 to be aborted by the user. 

970 ''' 

971 

972 try: 

973 viewer = self.get_viewer() 

974 markers = [ 

975 m for m in viewer.selected_markers() 

976 if not isinstance(m, EventMarker)] 

977 

978 if marker_selector is not None: 

979 markers = [ 

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

981 

982 pile = self.get_pile() 

983 

984 def rtrue(tr): 

985 return True 

986 

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

988 trace_selector_viewer = self.get_viewer_trace_selector(mode) 

989 

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

991 

992 if main_bandpass: 

993 def apply_filters(traces): 

994 for tr in traces: 

995 if viewer.highpass is not None: 

996 tr.highpass(4, viewer.highpass) 

997 if viewer.lowpass is not None: 

998 tr.lowpass(4, viewer.lowpass) 

999 return traces 

1000 else: 

1001 def apply_filters(traces): 

1002 return traces 

1003 

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

1005 

1006 time_last = [time.time()] 

1007 

1008 def update_progress(label, batch): 

1009 time_now = time.time() 

1010 if responsive: 

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

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

1013 # changes etc. 

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

1015 qw.qApp.processEvents() 

1016 else: 

1017 # redraw about once a second 

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

1019 viewer.repaint() 

1020 

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

1022 

1023 abort = pb.set_status( 

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

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

1026 

1027 return abort 

1028 

1029 if markers: 

1030 for imarker, marker in enumerate(markers): 

1031 try: 

1032 if progress: 

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

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

1035 

1036 pb.set_status(label, 0, responsive) 

1037 

1038 if not marker.nslc_ids: 

1039 trace_selector_marker = rtrue 

1040 else: 

1041 def trace_selector_marker(tr): 

1042 return marker.match_nslc(tr.nslc_id) 

1043 

1044 def trace_selector(tr): 

1045 return trace_selector_arg(tr) \ 

1046 and trace_selector_viewer(tr) \ 

1047 and trace_selector_marker(tr) 

1048 

1049 for batch in pile.chopper( 

1050 tmin=marker.tmin, 

1051 tmax=marker.tmax, 

1052 trace_selector=trace_selector, 

1053 style='batch', 

1054 *args, 

1055 **kwargs): 

1056 

1057 if progress: 

1058 abort = update_progress(label, batch) 

1059 if abort: 

1060 return 

1061 

1062 batch.traces = apply_filters(batch.traces) 

1063 if style_arg == 'batch': 

1064 yield batch 

1065 else: 

1066 yield batch.traces 

1067 

1068 finally: 

1069 if progress: 

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

1071 

1072 elif fallback: 

1073 def trace_selector(tr): 

1074 return trace_selector_arg(tr) \ 

1075 and trace_selector_viewer(tr) 

1076 

1077 tmin, tmax = viewer.get_time_range() 

1078 

1079 if not pile.is_empty(): 

1080 ptmin = pile.get_tmin() 

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

1082 if ptmin > tmin: 

1083 tmin = ptmin + tpad 

1084 ptmax = pile.get_tmax() 

1085 if ptmax < tmax: 

1086 tmax = ptmax - tpad 

1087 

1088 try: 

1089 if progress: 

1090 label = progress 

1091 pb.set_status(label, 0, responsive) 

1092 

1093 for batch in pile.chopper( 

1094 tmin=tmin, 

1095 tmax=tmax, 

1096 trace_selector=trace_selector, 

1097 style='batch', 

1098 *args, 

1099 **kwargs): 

1100 

1101 if progress: 

1102 abort = update_progress(label, batch) 

1103 

1104 if abort: 

1105 return 

1106 

1107 batch.traces = apply_filters(batch.traces) 

1108 

1109 if style_arg == 'batch': 

1110 yield batch 

1111 else: 

1112 yield batch.traces 

1113 

1114 finally: 

1115 if progress: 

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

1117 

1118 else: 

1119 raise NoTracesSelected() 

1120 

1121 except NoViewerSet: 

1122 pile = self.get_pile() 

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

1124 

1125 def get_selected_time_range(self, fallback=False): 

1126 ''' 

1127 Get the time range spanning all selected markers. 

1128 

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

1130 end of visible time range 

1131 ''' 

1132 

1133 viewer = self.get_viewer() 

1134 markers = viewer.selected_markers() 

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

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

1137 

1138 if mins and maxs: 

1139 tmin = min(mins) 

1140 tmax = max(maxs) 

1141 

1142 elif fallback: 

1143 tmin, tmax = viewer.get_time_range() 

1144 

1145 else: 

1146 raise NoTracesSelected() 

1147 

1148 return tmin, tmax 

1149 

1150 def panel_visibility_changed(self, bool): 

1151 ''' 

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

1153 

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

1155 when the panel is activated the first time. 

1156 ''' 

1157 

1158 pass 

1159 

1160 def make_pile(self): 

1161 ''' 

1162 Create a pile. 

1163 

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

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

1166 arguments. 

1167 ''' 

1168 

1169 cachedirname = config.config().cache_dir 

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

1171 return pile.make_pile( 

1172 sources, 

1173 cachedirname=cachedirname, 

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

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

1176 

1177 def make_panel(self, parent): 

1178 ''' 

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

1180 

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

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

1183 parameters). 

1184 ''' 

1185 

1186 params = self.get_parameters() 

1187 self._param_controls = {} 

1188 if params or self._force_panel: 

1189 sarea = MyScrollArea(parent.get_panel_parent_widget()) 

1190 sarea.setFrameStyle(qw.QFrame.NoFrame) 

1191 sarea.setSizePolicy(qw.QSizePolicy( 

1192 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)) 

1193 frame = MyFrame(sarea) 

1194 frame.widgetVisibilityChanged.connect( 

1195 self.panel_visibility_changed) 

1196 

1197 frame.setSizePolicy(qw.QSizePolicy( 

1198 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1199 frame.setFrameStyle(qw.QFrame.NoFrame) 

1200 sarea.setWidget(frame) 

1201 sarea.setWidgetResizable(True) 

1202 layout = qw.QGridLayout() 

1203 layout.setContentsMargins(0, 0, 0, 0) 

1204 layout.setSpacing(0) 

1205 frame.setLayout(layout) 

1206 

1207 parlayout = qw.QGridLayout() 

1208 

1209 irow = 0 

1210 ipar = 0 

1211 have_switches = False 

1212 have_params = False 

1213 for iparam, param in enumerate(params): 

1214 if isinstance(param, Param): 

1215 if param.minimum <= 0.0: 

1216 param_control = LinValControl( 

1217 high_is_none=param.high_is_none, 

1218 low_is_none=param.low_is_none, 

1219 type=param.type) 

1220 else: 

1221 param_control = ValControl( 

1222 high_is_none=param.high_is_none, 

1223 low_is_none=param.low_is_none, 

1224 low_is_zero=param.low_is_zero, 

1225 type=param.type) 

1226 

1227 param_control.setup( 

1228 param.name, 

1229 param.minimum, 

1230 param.maximum, 

1231 param.default, 

1232 iparam) 

1233 

1234 param_control.set_tracking(param.tracking) 

1235 param_control.valchange.connect( 

1236 self.modified_snuffling_panel) 

1237 

1238 self._param_controls[param.ident] = param_control 

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

1240 parlayout.addWidget(w, ipar, iw) 

1241 

1242 ipar += 1 

1243 have_params = True 

1244 

1245 elif isinstance(param, Choice): 

1246 param_widget = ChoiceControl( 

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

1248 param_widget.choosen.connect( 

1249 self.choose_on_snuffling_panel) 

1250 

1251 self._param_controls[param.ident] = param_widget 

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

1253 ipar += 1 

1254 have_params = True 

1255 

1256 elif isinstance(param, Switch): 

1257 have_switches = True 

1258 

1259 if have_params: 

1260 parframe = qw.QFrame(sarea) 

1261 parframe.setSizePolicy(qw.QSizePolicy( 

1262 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1263 parframe.setLayout(parlayout) 

1264 layout.addWidget(parframe, irow, 0) 

1265 irow += 1 

1266 

1267 if have_switches: 

1268 swlayout = qw.QGridLayout() 

1269 isw = 0 

1270 for iparam, param in enumerate(params): 

1271 if isinstance(param, Switch): 

1272 param_widget = SwitchControl( 

1273 param.ident, param.default, param.name) 

1274 param_widget.sw_toggled.connect( 

1275 self.switch_on_snuffling_panel) 

1276 

1277 self._param_controls[param.ident] = param_widget 

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

1279 isw += 1 

1280 

1281 swframe = qw.QFrame(sarea) 

1282 swframe.setSizePolicy(qw.QSizePolicy( 

1283 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1284 swframe.setLayout(swlayout) 

1285 layout.addWidget(swframe, irow, 0) 

1286 irow += 1 

1287 

1288 butframe = qw.QFrame(sarea) 

1289 butframe.setSizePolicy(qw.QSizePolicy( 

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

1291 butlayout = qw.QHBoxLayout() 

1292 butframe.setLayout(butlayout) 

1293 

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

1295 if self._live_update: 

1296 live_update_checkbox.setCheckState(qc.Qt.Checked) 

1297 

1298 butlayout.addWidget(live_update_checkbox) 

1299 live_update_checkbox.toggled.connect( 

1300 self.live_update_toggled) 

1301 

1302 help_button = qw.QPushButton('Help') 

1303 butlayout.addWidget(help_button) 

1304 help_button.clicked.connect( 

1305 self.help_button_triggered) 

1306 

1307 clear_button = qw.QPushButton('Clear') 

1308 butlayout.addWidget(clear_button) 

1309 clear_button.clicked.connect( 

1310 self.clear_button_triggered) 

1311 

1312 call_button = qw.QPushButton('Run') 

1313 butlayout.addWidget(call_button) 

1314 call_button.clicked.connect( 

1315 self.call_button_triggered) 

1316 

1317 for name, method in self._triggers: 

1318 but = qw.QPushButton(name) 

1319 

1320 def call_and_update(method): 

1321 def f(): 

1322 try: 

1323 method() 

1324 except SnufflingError as e: 

1325 if not isinstance(e, SnufflingCallFailed): 

1326 # those have logged within error() 

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

1328 logger.error( 

1329 '%s: Snuffling action failed' % self._name) 

1330 

1331 self.get_viewer().update() 

1332 return f 

1333 

1334 but.clicked.connect( 

1335 call_and_update(method)) 

1336 

1337 butlayout.addWidget(but) 

1338 

1339 layout.addWidget(butframe, irow, 0) 

1340 

1341 irow += 1 

1342 spacer = qw.QSpacerItem( 

1343 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

1344 

1345 layout.addItem(spacer, irow, 0) 

1346 

1347 return sarea 

1348 

1349 else: 

1350 return None 

1351 

1352 def make_helpmenuitem(self, parent): 

1353 ''' 

1354 Create the help menu item for the snuffling. 

1355 ''' 

1356 

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

1358 

1359 item.triggered.connect( 

1360 self.help_button_triggered) 

1361 

1362 return item 

1363 

1364 def make_menuitem(self, parent): 

1365 ''' 

1366 Create the menu item for the snuffling. 

1367 

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

1369 menu entry is wanted. 

1370 ''' 

1371 

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

1373 item.setCheckable( 

1374 self._have_pre_process_hook or self._have_post_process_hook) 

1375 

1376 item.triggered.connect( 

1377 self.menuitem_triggered) 

1378 

1379 return item 

1380 

1381 def output_filename( 

1382 self, 

1383 caption='Save File', 

1384 dir='', 

1385 filter='', 

1386 selected_filter=None): 

1387 

1388 ''' 

1389 Query user for an output filename. 

1390 

1391 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`. 

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

1393 dialog. 

1394 ''' 

1395 

1396 if not dir and self._previous_output_filename: 

1397 dir = self._previous_output_filename 

1398 

1399 fn = getSaveFileName( 

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

1401 if not fn: 

1402 raise UserCancelled() 

1403 

1404 self._previous_output_filename = fn 

1405 return str(fn) 

1406 

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

1408 ''' 

1409 Query user for an input directory. 

1410 

1411 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`. 

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

1413 dialog. 

1414 ''' 

1415 

1416 if not dir and self._previous_input_directory: 

1417 dir = self._previous_input_directory 

1418 

1419 dn = qw.QFileDialog.getExistingDirectory( 

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

1421 

1422 if not dn: 

1423 raise UserCancelled() 

1424 

1425 self._previous_input_directory = dn 

1426 return str(dn) 

1427 

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

1429 selected_filter=None): 

1430 ''' 

1431 Query user for an input filename. 

1432 

1433 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`. 

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

1435 dialog. 

1436 ''' 

1437 

1438 if not dir and self._previous_input_filename: 

1439 dir = self._previous_input_filename 

1440 

1441 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName( 

1442 self.get_viewer(), 

1443 caption, 

1444 dir, 

1445 filter)) # selected_filter) 

1446 

1447 if not fn: 

1448 raise UserCancelled() 

1449 

1450 self._previous_input_filename = fn 

1451 return str(fn) 

1452 

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

1454 ''' 

1455 Query user for a text input. 

1456 

1457 This is currently a wrapper to :py:func:`QInputDialog.getText`. 

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

1459 dialog. 

1460 ''' 

1461 

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

1463 

1464 if not ok: 

1465 raise UserCancelled() 

1466 

1467 return inp 

1468 

1469 def modified_snuffling_panel(self, value, iparam): 

1470 ''' 

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

1472 

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

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

1475 widget. 

1476 ''' 

1477 

1478 param = self.get_parameters()[iparam] 

1479 self._set_parameter_value(param.ident, value) 

1480 if self._live_update: 

1481 self.check_call() 

1482 self.get_viewer().update() 

1483 

1484 def switch_on_snuffling_panel(self, ident, state): 

1485 ''' 

1486 Called when the user has toggled a switchable parameter. 

1487 ''' 

1488 

1489 self._set_parameter_value(ident, state) 

1490 if self._live_update: 

1491 self.check_call() 

1492 self.get_viewer().update() 

1493 

1494 def choose_on_snuffling_panel(self, ident, state): 

1495 ''' 

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

1497 ''' 

1498 

1499 self._set_parameter_value(ident, state) 

1500 if self._live_update: 

1501 self.check_call() 

1502 self.get_viewer().update() 

1503 

1504 def menuitem_triggered(self, arg): 

1505 ''' 

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

1507 

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

1509 and triggers an update on the viewer widget. 

1510 ''' 

1511 

1512 self.check_call() 

1513 

1514 if self._have_pre_process_hook: 

1515 self._pre_process_hook_enabled = arg 

1516 

1517 if self._have_post_process_hook: 

1518 self._post_process_hook_enabled = arg 

1519 

1520 if self._have_pre_process_hook or self._have_post_process_hook: 

1521 self.get_viewer().clean_update() 

1522 else: 

1523 self.get_viewer().update() 

1524 

1525 def call_button_triggered(self): 

1526 ''' 

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

1528 

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

1530 and triggers an update on the viewer widget. 

1531 ''' 

1532 

1533 self.check_call() 

1534 self.get_viewer().update() 

1535 

1536 def clear_button_triggered(self): 

1537 ''' 

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

1539 

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

1541 viewer widget. 

1542 ''' 

1543 

1544 self.cleanup() 

1545 self.get_viewer().update() 

1546 

1547 def help_button_triggered(self): 

1548 ''' 

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

1550 given in the snufflings' __doc__ string. 

1551 ''' 

1552 

1553 if self.__doc__: 

1554 if self.__doc__.strip().startswith('<html>'): 

1555 doc = qw.QLabel(self.__doc__) 

1556 else: 

1557 try: 

1558 import markdown 

1559 doc = qw.QLabel(markdown.markdown(self.__doc__)) 

1560 

1561 except ImportError: 

1562 logger.error( 

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

1564 'formatting.') 

1565 

1566 doc = qw.QLabel(self.__doc__) 

1567 else: 

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

1569 

1570 labels = [doc] 

1571 

1572 if self._filename: 

1573 from html import escape 

1574 

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

1576 

1577 doc_src = qw.QLabel( 

1578 '''<html><body> 

1579<hr /> 

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

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

1582<br /> 

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

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

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

1586 % ( 

1587 quote(self._filename), 

1588 escape(self._filename), 

1589 escape(code))) 

1590 

1591 labels.append(doc_src) 

1592 

1593 for h in labels: 

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

1595 h.setWordWrap(True) 

1596 

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

1598 

1599 def live_update_toggled(self, on): 

1600 ''' 

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

1602 ''' 

1603 

1604 self.set_live_update(on) 

1605 

1606 def add_traces(self, traces): 

1607 ''' 

1608 Add traces to the viewer. 

1609 

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

1611 

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

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

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

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

1616 traces are added. 

1617 

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

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

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

1621 method). 

1622 ''' 

1623 

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

1625 self._tickets.append(ticket) 

1626 return ticket 

1627 

1628 def add_trace(self, tr): 

1629 ''' 

1630 Add a trace to the viewer. 

1631 

1632 See :py:meth:`add_traces`. 

1633 ''' 

1634 

1635 self.add_traces([tr]) 

1636 

1637 def add_markers(self, markers): 

1638 ''' 

1639 Add some markers to the display. 

1640 

1641 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and 

1642 adds these to the viewer. 

1643 ''' 

1644 

1645 self.get_viewer().add_markers(markers) 

1646 self._markers.extend(markers) 

1647 

1648 def add_marker(self, marker): 

1649 ''' 

1650 Add a marker to the display. 

1651 

1652 See :py:meth:`add_markers`. 

1653 ''' 

1654 

1655 self.add_markers([marker]) 

1656 

1657 def cleanup(self): 

1658 ''' 

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

1660 snuffling. 

1661 ''' 

1662 

1663 try: 

1664 viewer = self.get_viewer() 

1665 viewer.release_data(self._tickets) 

1666 viewer.remove_markers(self._markers) 

1667 

1668 except NoViewerSet: 

1669 pass 

1670 

1671 self._tickets = [] 

1672 self._markers = [] 

1673 

1674 def check_call(self): 

1675 

1676 if self._call_in_progress: 

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

1678 return 

1679 

1680 try: 

1681 self._call_in_progress = True 

1682 self.call() 

1683 return 0 

1684 

1685 except SnufflingError as e: 

1686 if not isinstance(e, SnufflingCallFailed): 

1687 # those have logged within error() 

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

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

1690 return 1 

1691 

1692 except Exception as e: 

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

1694 self._name, str(e)) 

1695 

1696 logger.exception(message) 

1697 self.show_message('error', message) 

1698 

1699 finally: 

1700 self._call_in_progress = False 

1701 

1702 def call(self): 

1703 ''' 

1704 Main work routine of the snuffling. 

1705 

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

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

1708 in subclass. The default implementation does nothing useful. 

1709 ''' 

1710 

1711 pass 

1712 

1713 def pre_process_hook(self, traces): 

1714 return traces 

1715 

1716 def post_process_hook(self, traces): 

1717 return traces 

1718 

1719 def get_tpad(self): 

1720 ''' 

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

1722 ''' 

1723 

1724 return 0.0 

1725 

1726 def pre_destroy(self): 

1727 ''' 

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

1729 

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

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

1732 the snuffling`s tempory directory, if needed. 

1733 ''' 

1734 

1735 self.cleanup() 

1736 if self._tempdir is not None: 

1737 import shutil 

1738 shutil.rmtree(self._tempdir) 

1739 

1740 

1741class SnufflingError(Exception): 

1742 pass 

1743 

1744 

1745class NoViewerSet(SnufflingError): 

1746 ''' 

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

1748 ''' 

1749 

1750 def __str__(self): 

1751 return 'No GUI available. ' \ 

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

1753 

1754 

1755class MissingStationInformation(SnufflingError): 

1756 ''' 

1757 Raised when station information is missing. 

1758 ''' 

1759 

1760 

1761class NoTracesSelected(SnufflingError): 

1762 ''' 

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

1764 and we cannot fallback to using the current view. 

1765 ''' 

1766 

1767 def __str__(self): 

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

1769 

1770 

1771class UserCancelled(SnufflingError): 

1772 ''' 

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

1774 ''' 

1775 

1776 def __str__(self): 

1777 return 'The user has cancelled a dialog.' 

1778 

1779 

1780class SnufflingCallFailed(SnufflingError): 

1781 ''' 

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

1783 :py:meth:`Snuffling.call`. 

1784 ''' 

1785 

1786 

1787class InvalidSnufflingFilename(Exception): 

1788 pass 

1789 

1790 

1791class SnufflingModule(object): 

1792 ''' 

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

1794 

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

1796 :py:func:`__snufflings__` which return the snuffling instances to be 

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

1798 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew` 

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

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

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

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

1803 when needed. 

1804 ''' 

1805 

1806 mtimes = {} 

1807 

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

1809 self._path = path 

1810 self._name = name 

1811 self._mtime = None 

1812 self._module = None 

1813 self._snufflings = [] 

1814 self._handler = handler 

1815 

1816 def load_if_needed(self): 

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

1818 

1819 try: 

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

1821 except OSError as e: 

1822 if e.errno == 2: 

1823 logger.error(e) 

1824 raise BrokenSnufflingModule(filename) 

1825 

1826 if self._module is None: 

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

1828 try: 

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

1830 if self._name in sys.modules: 

1831 raise InvalidSnufflingFilename(self._name) 

1832 

1833 self._module = __import__(self._name) 

1834 del sys.modules[self._name] 

1835 

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

1837 snuffling._filename = filename 

1838 self.add_snuffling(snuffling) 

1839 

1840 except Exception: 

1841 logger.error(traceback.format_exc()) 

1842 raise BrokenSnufflingModule(filename) 

1843 

1844 finally: 

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

1846 

1847 elif self._mtime != mtime: 

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

1849 settings = self.remove_snufflings() 

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

1851 try: 

1852 

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

1854 

1855 reload(self._module) 

1856 del sys.modules[self._name] 

1857 

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

1859 snuffling._filename = filename 

1860 self.add_snuffling(snuffling, reloaded=True) 

1861 

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

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

1864 snuf.set_settings(sett) 

1865 

1866 except Exception: 

1867 logger.error(traceback.format_exc()) 

1868 raise BrokenSnufflingModule(filename) 

1869 

1870 finally: 

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

1872 

1873 self._mtime = mtime 

1874 

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

1876 snuffling._path = self._path 

1877 snuffling.setup() 

1878 self._snufflings.append(snuffling) 

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

1880 

1881 def remove_snufflings(self): 

1882 settings = [] 

1883 for snuffling in self._snufflings: 

1884 settings.append(snuffling.get_settings()) 

1885 self._handler.remove_snuffling(snuffling) 

1886 

1887 self._snufflings = [] 

1888 return settings 

1889 

1890 

1891class BrokenSnufflingModule(Exception): 

1892 pass 

1893 

1894 

1895class MyScrollArea(qw.QScrollArea): 

1896 

1897 def sizeHint(self): 

1898 s = qc.QSize() 

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

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

1901 return s 

1902 

1903 

1904class SwitchControl(qw.QCheckBox): 

1905 sw_toggled = qc.pyqtSignal(object, bool) 

1906 

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

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

1909 self.ident = ident 

1910 self.setChecked(default) 

1911 self.toggled.connect(self._sw_toggled) 

1912 

1913 def _sw_toggled(self, state): 

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

1915 

1916 def set_value(self, state): 

1917 self.blockSignals(True) 

1918 self.setChecked(state) 

1919 self.blockSignals(False) 

1920 

1921 

1922class ChoiceControl(qw.QFrame): 

1923 choosen = qc.pyqtSignal(object, object) 

1924 

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

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

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

1928 self.label.setMinimumWidth(120) 

1929 self.cbox = qw.QComboBox(self) 

1930 self.layout = qw.QHBoxLayout(self) 

1931 self.layout.addWidget(self.label) 

1932 self.layout.addWidget(self.cbox) 

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

1934 self.layout.setSpacing(0) 

1935 self.ident = ident 

1936 self.choices = choices 

1937 for ichoice, choice in enumerate(choices): 

1938 self.cbox.addItem(choice) 

1939 

1940 self.set_value(default) 

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

1942 

1943 def set_choices(self, choices): 

1944 icur = self.cbox.currentIndex() 

1945 if icur != -1: 

1946 selected_choice = choices[icur] 

1947 else: 

1948 selected_choice = None 

1949 

1950 self.choices = choices 

1951 self.cbox.clear() 

1952 for ichoice, choice in enumerate(choices): 

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

1954 

1955 if selected_choice is not None and selected_choice in choices: 

1956 self.set_value(selected_choice) 

1957 return selected_choice 

1958 else: 

1959 self.set_value(choices[0]) 

1960 return choices[0] 

1961 

1962 def emit_choosen(self, i): 

1963 self.choosen.emit( 

1964 self.ident, 

1965 self.choices[i]) 

1966 

1967 def set_value(self, v): 

1968 self.cbox.blockSignals(True) 

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

1970 if choice == v: 

1971 self.cbox.setCurrentIndex(i) 

1972 self.cbox.blockSignals(False)