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 

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 

30from importlib import reload 

31 

32Marker, load_markers, save_markers # noqa 

33 

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

35 

36 

37class MyFrame(qw.QFrame): 

38 widgetVisibilityChanged = qc.pyqtSignal(bool) 

39 

40 def showEvent(self, ev): 

41 self.widgetVisibilityChanged.emit(True) 

42 

43 def hideEvent(self, ev): 

44 self.widgetVisibilityChanged.emit(False) 

45 

46 

47class Param(object): 

48 ''' 

49 Definition of an adjustable floating point parameter for the 

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

51 such parameters. 

52 

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

54 :param ident: identifier of the parameter 

55 :param default: default value 

56 :param minimum: minimum value for the parameter 

57 :param maximum: maximum value for the parameter 

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

59 of parameter range (optional) 

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

61 of parameter range (optional) 

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

63 value of parameter range (optional) 

64 ''' 

65 

66 def __init__( 

67 self, name, ident, default, minimum, maximum, 

68 low_is_none=None, 

69 high_is_none=None, 

70 low_is_zero=False, 

71 tracking=True, 

72 type=float): 

73 

74 if low_is_none and default == minimum: 

75 default = None 

76 if high_is_none and default == maximum: 

77 default = None 

78 

79 self.name = name 

80 self.ident = ident 

81 self.default = default 

82 self.minimum = minimum 

83 self.maximum = maximum 

84 self.low_is_none = low_is_none 

85 self.high_is_none = high_is_none 

86 self.low_is_zero = low_is_zero 

87 self.tracking = tracking 

88 self.type = type 

89 

90 self._control = None 

91 

92 

93class Switch(object): 

94 ''' 

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

96 may display a checkbox for such a switch. 

97 

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

99 :param ident: identifier of the parameter 

100 :param default: default value 

101 ''' 

102 

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

104 self.name = name 

105 self.ident = ident 

106 self.default = default 

107 

108 

109class Choice(object): 

110 ''' 

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

112 may display a menu for such a choice. 

113 

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

115 :param ident: identifier of the parameter 

116 :param default: default value 

117 :param choices: tuple of other options 

118 ''' 

119 

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

121 self.name = name 

122 self.ident = ident 

123 self.default = default 

124 self.choices = choices 

125 

126 

127class Snuffling(object): 

128 ''' 

129 Base class for user snufflings. 

130 

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

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

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

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

135 

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

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

138 ''' 

139 

140 def __init__(self): 

141 self._path = None 

142 

143 self._name = 'Untitled Snuffling' 

144 self._viewer = None 

145 self._tickets = [] 

146 self._markers = [] 

147 

148 self._delete_panel = None 

149 self._delete_menuitem = None 

150 

151 self._panel_parent = None 

152 self._menu_parent = None 

153 

154 self._panel = None 

155 self._menuitem = None 

156 self._helpmenuitem = None 

157 self._parameters = [] 

158 self._param_controls = {} 

159 

160 self._triggers = [] 

161 

162 self._live_update = True 

163 self._previous_output_filename = None 

164 self._previous_input_filename = None 

165 self._previous_input_directory = None 

166 

167 self._tempdir = None 

168 self._iplot = 0 

169 

170 self._have_pre_process_hook = False 

171 self._have_post_process_hook = False 

172 self._pre_process_hook_enabled = False 

173 self._post_process_hook_enabled = False 

174 

175 self._no_viewer_pile = None 

176 self._cli_params = {} 

177 self._filename = None 

178 self._force_panel = False 

179 self._call_in_progress = False 

180 

181 def setup(self): 

182 ''' 

183 Setup the snuffling. 

184 

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

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

187 ''' 

188 

189 pass 

190 

191 def module_dir(self): 

192 ''' 

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

194 

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

196 ''' 

197 

198 return self._path 

199 

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

201 ''' 

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

203 

204 This method is called from the 

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

206 :py:meth:`setup_gui`. 

207 ''' 

208 

209 self._viewer = viewer 

210 self._panel_parent = panel_parent 

211 self._menu_parent = menu_parent 

212 

213 self.setup_gui(reloaded=reloaded) 

214 

215 def setup_gui(self, reloaded=False): 

216 ''' 

217 Create and add gui elements to the viewer. 

218 

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

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

221 snuffling has been changed. 

222 ''' 

223 

224 if self._panel_parent is not None: 

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

226 if self._panel: 

227 self._panel_parent.add_panel( 

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

229 

230 if self._menu_parent is not None: 

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

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

233 if self._menuitem: 

234 self._menu_parent.add_snuffling_menuitem(self._menuitem) 

235 

236 if self._helpmenuitem: 

237 self._menu_parent.add_snuffling_help_menuitem( 

238 self._helpmenuitem) 

239 

240 def set_force_panel(self, bool=True): 

241 ''' 

242 Force to create a panel. 

243 

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

245 button. 

246 ''' 

247 

248 self._force_panel = bool 

249 

250 def make_cli_parser1(self): 

251 import optparse 

252 

253 class MyOptionParser(optparse.OptionParser): 

254 def error(self, msg): 

255 logger.error(msg) 

256 self.exit(1) 

257 

258 parser = MyOptionParser() 

259 

260 parser.add_option( 

261 '--format', 

262 dest='format', 

263 default='from_extension', 

264 choices=( 

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

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

267 'from_extension', 'detect'), 

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

269 

270 parser.add_option( 

271 '--pattern', 

272 dest='regex', 

273 metavar='REGEX', 

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

275 

276 self.add_params_to_cli_parser(parser) 

277 self.configure_cli_parser(parser) 

278 return parser 

279 

280 def configure_cli_parser(self, parser): 

281 pass 

282 

283 def cli_usage(self): 

284 return None 

285 

286 def add_params_to_cli_parser(self, parser): 

287 

288 for param in self._parameters: 

289 if isinstance(param, Param): 

290 parser.add_option( 

291 '--' + param.ident, 

292 dest=param.ident, 

293 default=param.default, 

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

295 help=param.name) 

296 

297 def setup_cli(self): 

298 self.setup() 

299 parser = self.make_cli_parser1() 

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

301 

302 for param in self._parameters: 

303 if isinstance(param, Param): 

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

305 

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

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

308 self._cli_params['sources'] = args 

309 

310 return options, args, parser 

311 

312 def delete_gui(self): 

313 ''' 

314 Remove the gui elements of the snuffling. 

315 

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

317 also removes all traces and markers added with the 

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

319 ''' 

320 

321 self.cleanup() 

322 

323 if self._panel is not None: 

324 self._panel_parent.remove_panel(self._panel) 

325 self._panel = None 

326 

327 if self._menuitem is not None: 

328 self._menu_parent.remove_snuffling_menuitem(self._menuitem) 

329 self._menuitem = None 

330 

331 if self._helpmenuitem is not None: 

332 self._menu_parent.remove_snuffling_help_menuitem( 

333 self._helpmenuitem) 

334 

335 def set_name(self, name): 

336 ''' 

337 Set the snuffling's name. 

338 

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

340 ''' 

341 

342 self._name = name 

343 self.reset_gui() 

344 

345 def get_name(self): 

346 ''' 

347 Get the snuffling's name. 

348 ''' 

349 

350 return self._name 

351 

352 def set_have_pre_process_hook(self, bool): 

353 self._have_pre_process_hook = bool 

354 self._live_update = False 

355 self._pre_process_hook_enabled = False 

356 self.reset_gui() 

357 

358 def set_have_post_process_hook(self, bool): 

359 self._have_post_process_hook = bool 

360 self._live_update = False 

361 self._post_process_hook_enabled = False 

362 self.reset_gui() 

363 

364 def set_have_pile_changed_hook(self, bool): 

365 self._pile_ = False 

366 

367 def enable_pile_changed_notifications(self): 

368 ''' 

369 Get informed when pile changed. 

370 

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

372 update in the viewer's pile. 

373 ''' 

374 

375 viewer = self.get_viewer() 

376 viewer.pile_has_changed_signal.connect( 

377 self.pile_changed) 

378 

379 def disable_pile_changed_notifications(self): 

380 ''' 

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

382 ''' 

383 

384 viewer = self.get_viewer() 

385 viewer.pile_has_changed_signal.disconnect( 

386 self.pile_changed) 

387 

388 def pile_changed(self): 

389 ''' 

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

391 

392 Must be activated with a call to 

393 :py:meth:`enable_pile_changed_notifications`. 

394 ''' 

395 

396 pass 

397 

398 def reset_gui(self, reloaded=False): 

399 ''' 

400 Delete and recreate the snuffling's panel. 

401 ''' 

402 

403 if self._panel or self._menuitem: 

404 sett = self.get_settings() 

405 self.delete_gui() 

406 self.setup_gui(reloaded=reloaded) 

407 self.set_settings(sett) 

408 

409 def show_message(self, kind, message): 

410 ''' 

411 Display a message box. 

412 

413 :param kind: string defining kind of message 

414 :param message: the message to be displayed 

415 ''' 

416 

417 try: 

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

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

420 box.exec_() 

421 except NoViewerSet: 

422 pass 

423 

424 def error(self, message): 

425 ''' 

426 Show an error message box. 

427 

428 :param message: specifying the error 

429 ''' 

430 

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

432 self.show_message('error', message) 

433 

434 def warn(self, message): 

435 ''' 

436 Display a warning message. 

437 

438 :param message: specifying the warning 

439 ''' 

440 

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

442 self.show_message('warning', message) 

443 

444 def fail(self, message): 

445 ''' 

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

447 exception. 

448 

449 :param message: specifying the error 

450 ''' 

451 

452 self.error(message) 

453 raise SnufflingCallFailed(message) 

454 

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

456 ''' 

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

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

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

460 

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

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

463 ''' 

464 

465 if name is None: 

466 self._iplot += 1 

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

468 

469 fframe = FigureFrame() 

470 self._panel_parent.add_tab(name, fframe) 

471 if get == 'axes': 

472 return fframe.gca() 

473 elif get == 'figure': 

474 return fframe.gcf() 

475 elif get == 'figure_frame': 

476 return fframe 

477 

478 def figure(self, name=None): 

479 ''' 

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

481 which can be displayed within snuffler by calling 

482 :py:meth:`canvas.draw`. 

483 

484 :param name: labels the tab of the figure 

485 ''' 

486 

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

488 

489 def axes(self, name=None): 

490 ''' 

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

492 

493 :param name: labels the tab of axes 

494 ''' 

495 

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

497 

498 def figure_frame(self, name=None): 

499 ''' 

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

501 

502 :param name: labels the tab figure frame 

503 ''' 

504 

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

506 

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

508 ''' 

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

510 

511 :param name: labels the tab 

512 :param filename: name of file to be displayed 

513 ''' 

514 

515 f = PixmapFrame(filename) 

516 

517 scroll_area = qw.QScrollArea() 

518 scroll_area.setWidget(f) 

519 scroll_area.setWidgetResizable(True) 

520 

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

522 return f 

523 

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

525 ''' 

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

527 within snuffler. 

528 

529 :param url: url to open 

530 :param name: labels the tab 

531 ''' 

532 

533 if name is None: 

534 self._iplot += 1 

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

536 

537 f = WebKitFrame(url) 

538 self._panel_parent.add_tab(name, f) 

539 return f 

540 

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

542 ''' 

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

544 graphics. 

545 

546 :param actors: list of VTKActors 

547 :param name: labels the tab 

548 

549 Initialize the interactive rendering by calling the frames' 

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

551 renderer. 

552 

553 Requires installation of vtk including python wrapper. 

554 ''' 

555 if name is None: 

556 self._iplot += 1 

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

558 

559 try: 

560 f = VTKFrame(actors=actors) 

561 except ImportError as e: 

562 self.fail(e) 

563 

564 self._panel_parent.add_tab(name, f) 

565 return f 

566 

567 def tempdir(self): 

568 ''' 

569 Create a temporary directory and return its absolute path. 

570 

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

572 instance is deleted. 

573 ''' 

574 

575 if self._tempdir is None: 

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

577 

578 return self._tempdir 

579 

580 def set_live_update(self, live_update): 

581 ''' 

582 Enable/disable live updating. 

583 

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

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

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

587 item or pressing the call button. 

588 ''' 

589 

590 self._live_update = live_update 

591 if self._have_pre_process_hook: 

592 self._pre_process_hook_enabled = live_update 

593 if self._have_post_process_hook: 

594 self._post_process_hook_enabled = live_update 

595 

596 try: 

597 self.get_viewer().clean_update() 

598 except NoViewerSet: 

599 pass 

600 

601 def add_parameter(self, param): 

602 ''' 

603 Add an adjustable parameter to the snuffling. 

604 

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

606 :py:class:`Choice`. 

607 

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

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

610 ''' 

611 

612 self._parameters.append(param) 

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

614 

615 if self._panel is not None: 

616 self.delete_gui() 

617 self.setup_gui() 

618 

619 def add_trigger(self, name, method): 

620 ''' 

621 Add a button to the snuffling's panel. 

622 

623 :param name: string that labels the button 

624 :param method: method associated with the button 

625 ''' 

626 

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

628 

629 if self._panel is not None: 

630 self.delete_gui() 

631 self.setup_gui() 

632 

633 def get_parameters(self): 

634 ''' 

635 Get the snuffling's adjustable parameter definitions. 

636 

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

638 ''' 

639 

640 return self._parameters 

641 

642 def get_parameter(self, ident): 

643 ''' 

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

645 

646 :param ident: identifier of the parameter 

647 

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

649 ''' 

650 

651 for param in self._parameters: 

652 if param.ident == ident: 

653 return param 

654 return None 

655 

656 def set_parameter(self, ident, value): 

657 ''' 

658 Set one of the snuffling's adjustable parameters. 

659 

660 :param ident: identifier of the parameter 

661 :param value: new value of the parameter 

662 

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

664 ''' 

665 

666 self._set_parameter_value(ident, value) 

667 

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

669 if control: 

670 control.set_value(value) 

671 

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

673 ''' 

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

675 

676 :param ident: identifier of the parameter 

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

678 

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

680 ''' 

681 

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

683 if control: 

684 control.set_range(vmin, vmax) 

685 

686 def set_parameter_choices(self, ident, choices): 

687 ''' 

688 Update the choices of a Choice parameter. 

689 

690 :param ident: identifier of the parameter 

691 :param choices: list of strings 

692 ''' 

693 

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

695 if control: 

696 selected_choice = control.set_choices(choices) 

697 self._set_parameter_value(ident, selected_choice) 

698 

699 def _set_parameter_value(self, ident, value): 

700 setattr(self, ident, value) 

701 

702 def get_parameter_value(self, ident): 

703 ''' 

704 Get the current value of a parameter. 

705 

706 :param ident: identifier of the parameter 

707 ''' 

708 return getattr(self, ident) 

709 

710 def get_settings(self): 

711 ''' 

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

713 their values as the dictionaries values. 

714 ''' 

715 

716 params = self.get_parameters() 

717 settings = {} 

718 for param in params: 

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

720 

721 return settings 

722 

723 def set_settings(self, settings): 

724 params = self.get_parameters() 

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

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

727 if k in dparams: 

728 self._set_parameter_value(k, v) 

729 if k in self._param_controls: 

730 control = self._param_controls[k] 

731 control.set_value(v) 

732 

733 def get_viewer(self): 

734 ''' 

735 Get the parent viewer. 

736 

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

738 which is the main viewer widget. 

739 

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

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

742 ''' 

743 

744 if self._viewer is None: 

745 raise NoViewerSet() 

746 return self._viewer 

747 

748 def get_pile(self): 

749 ''' 

750 Get the pile. 

751 

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

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

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

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

756 mode. 

757 ''' 

758 

759 try: 

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

761 except NoViewerSet: 

762 if self._no_viewer_pile is None: 

763 self._no_viewer_pile = self.make_pile() 

764 

765 p = self._no_viewer_pile 

766 

767 return p 

768 

769 def get_active_event_and_stations( 

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

771 

772 ''' 

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

774 

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

776 query for available data 

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

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

779 

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

781 ''' 

782 

783 p = self.get_pile() 

784 v = self.get_viewer() 

785 

786 event = v.get_active_event() 

787 if event is None: 

788 self.fail( 

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

790 'it the "active event"') 

791 

792 stations = {} 

793 for traces in p.chopper( 

794 event.time+trange[0], 

795 event.time+trange[1], 

796 load_data=False, 

797 degap=False): 

798 

799 for tr in traces: 

800 try: 

801 for skey in v.station_keys(tr): 

802 if skey in stations: 

803 continue 

804 

805 station = v.get_station(skey) 

806 stations[skey] = station 

807 

808 except KeyError: 

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

810 % '.'.join(skey) 

811 

812 if missing == 'warn': 

813 logger.warning(s) 

814 elif missing == 'raise': 

815 raise MissingStationInformation(s) 

816 elif missing == 'ignore': 

817 pass 

818 else: 

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

820 

821 stations[skey] = None 

822 

823 return event, list(set( 

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

825 

826 def get_stations(self): 

827 ''' 

828 Get all stations known to the viewer. 

829 ''' 

830 

831 v = self.get_viewer() 

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

833 return stations 

834 

835 def get_markers(self): 

836 ''' 

837 Get all markers from the viewer. 

838 ''' 

839 

840 return self.get_viewer().get_markers() 

841 

842 def get_event_markers(self): 

843 ''' 

844 Get all event markers from the viewer. 

845 ''' 

846 

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

848 if isinstance(m, EventMarker)] 

849 

850 def get_selected_markers(self): 

851 ''' 

852 Get all selected markers from the viewer. 

853 ''' 

854 

855 return self.get_viewer().selected_markers() 

856 

857 def get_selected_event_markers(self): 

858 ''' 

859 Get all selected event markers from the viewer. 

860 ''' 

861 

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

863 if isinstance(m, EventMarker)] 

864 

865 def get_active_event_and_phase_markers(self): 

866 ''' 

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

868 ''' 

869 

870 viewer = self.get_viewer() 

871 markers = viewer.get_markers() 

872 event_marker = viewer.get_active_event_marker() 

873 if event_marker is None: 

874 self.fail( 

875 'No active event set. ' 

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

877 

878 event = event_marker.get_event() 

879 

880 selection = [] 

881 for m in markers: 

882 if isinstance(m, PhaseMarker): 

883 if m.get_event() is event: 

884 selection.append(m) 

885 

886 return ( 

887 event_marker, 

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

889 m.get_event() == event]) 

890 

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

892 ''' 

893 Get currently active trace selector from viewer. 

894 

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

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

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

898 to disable any restrictions. 

899 ''' 

900 

901 viewer = self.get_viewer() 

902 

903 def rtrue(tr): 

904 return True 

905 

906 if mode == 'inview': 

907 return viewer.trace_selector or rtrue 

908 elif mode == 'visible': 

909 return viewer.trace_filter or rtrue 

910 elif mode == 'all': 

911 return rtrue 

912 else: 

913 raise Exception('invalid mode argument') 

914 

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

916 mode='inview', main_bandpass=False, 

917 progress=None, responsive=False, 

918 *args, **kwargs): 

919 ''' 

920 Iterate over selected traces. 

921 

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

923 running snuffler. For each selected marker, 

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

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

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

927 *\\*args* and *\\*\\*kwargs*. 

928 

929 :param fallback: 

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

931 currently visible in the viewer. 

932 

933 :param marker_selector: 

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

935 

936 :param mode: 

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

938 shown in the viewer (excluding traces accessible through vertical 

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

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

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

942 restrictions. 

943 

944 :param main_bandpass: 

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

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

947 order Butterworth highpass and lowpass and the signal is always 

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

949 bandpass settings from the graphical interface are not respected 

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

951 artifacts. 

952 

953 :param progress: 

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

955 is used as the label for the progress bar. 

956 

957 :param responsive: 

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

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

960 to be aborted by the user. 

961 ''' 

962 

963 try: 

964 viewer = self.get_viewer() 

965 markers = [ 

966 m for m in viewer.selected_markers() 

967 if not isinstance(m, EventMarker)] 

968 

969 if marker_selector is not None: 

970 markers = [ 

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

972 

973 pile = self.get_pile() 

974 

975 def rtrue(tr): 

976 return True 

977 

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

979 trace_selector_viewer = self.get_viewer_trace_selector(mode) 

980 

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

982 

983 if main_bandpass: 

984 def apply_filters(traces): 

985 for tr in traces: 

986 if viewer.highpass is not None: 

987 tr.highpass(4, viewer.highpass) 

988 if viewer.lowpass is not None: 

989 tr.lowpass(4, viewer.lowpass) 

990 return traces 

991 else: 

992 def apply_filters(traces): 

993 return traces 

994 

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

996 

997 time_last = [time.time()] 

998 

999 def update_progress(label, batch): 

1000 time_now = time.time() 

1001 if responsive: 

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

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

1004 # changes etc. 

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

1006 qw.qApp.processEvents() 

1007 else: 

1008 # redraw about once a second 

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

1010 viewer.repaint() 

1011 

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

1013 

1014 abort = pb.set_status( 

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

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

1017 

1018 return abort 

1019 

1020 if markers: 

1021 for imarker, marker in enumerate(markers): 

1022 try: 

1023 if progress: 

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

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

1026 

1027 pb.set_status(label, 0, responsive) 

1028 

1029 if not marker.nslc_ids: 

1030 trace_selector_marker = rtrue 

1031 else: 

1032 def trace_selector_marker(tr): 

1033 return marker.match_nslc(tr.nslc_id) 

1034 

1035 def trace_selector(tr): 

1036 return trace_selector_arg(tr) \ 

1037 and trace_selector_viewer(tr) \ 

1038 and trace_selector_marker(tr) 

1039 

1040 for batch in pile.chopper( 

1041 tmin=marker.tmin, 

1042 tmax=marker.tmax, 

1043 trace_selector=trace_selector, 

1044 style='batch', 

1045 *args, 

1046 **kwargs): 

1047 

1048 if progress: 

1049 abort = update_progress(label, batch) 

1050 if abort: 

1051 return 

1052 

1053 batch.traces = apply_filters(batch.traces) 

1054 if style_arg == 'batch': 

1055 yield batch 

1056 else: 

1057 yield batch.traces 

1058 

1059 finally: 

1060 if progress: 

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

1062 

1063 elif fallback: 

1064 def trace_selector(tr): 

1065 return trace_selector_arg(tr) \ 

1066 and trace_selector_viewer(tr) 

1067 

1068 tmin, tmax = viewer.get_time_range() 

1069 

1070 if not pile.is_empty(): 

1071 ptmin = pile.get_tmin() 

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

1073 if ptmin > tmin: 

1074 tmin = ptmin + tpad 

1075 ptmax = pile.get_tmax() 

1076 if ptmax < tmax: 

1077 tmax = ptmax - tpad 

1078 

1079 try: 

1080 if progress: 

1081 label = progress 

1082 pb.set_status(label, 0, responsive) 

1083 

1084 for batch in pile.chopper( 

1085 tmin=tmin, 

1086 tmax=tmax, 

1087 trace_selector=trace_selector, 

1088 style='batch', 

1089 *args, 

1090 **kwargs): 

1091 

1092 if progress: 

1093 abort = update_progress(label, batch) 

1094 

1095 if abort: 

1096 return 

1097 

1098 batch.traces = apply_filters(batch.traces) 

1099 

1100 if style_arg == 'batch': 

1101 yield batch 

1102 else: 

1103 yield batch.traces 

1104 

1105 finally: 

1106 if progress: 

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

1108 

1109 else: 

1110 raise NoTracesSelected() 

1111 

1112 except NoViewerSet: 

1113 pile = self.get_pile() 

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

1115 

1116 def get_selected_time_range(self, fallback=False): 

1117 ''' 

1118 Get the time range spanning all selected markers. 

1119 

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

1121 end of visible time range 

1122 ''' 

1123 

1124 viewer = self.get_viewer() 

1125 markers = viewer.selected_markers() 

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

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

1128 

1129 if mins and maxs: 

1130 tmin = min(mins) 

1131 tmax = max(maxs) 

1132 

1133 elif fallback: 

1134 tmin, tmax = viewer.get_time_range() 

1135 

1136 else: 

1137 raise NoTracesSelected() 

1138 

1139 return tmin, tmax 

1140 

1141 def panel_visibility_changed(self, bool): 

1142 ''' 

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

1144 

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

1146 when the panel is activated the first time. 

1147 ''' 

1148 

1149 pass 

1150 

1151 def make_pile(self): 

1152 ''' 

1153 Create a pile. 

1154 

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

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

1157 arguments. 

1158 ''' 

1159 

1160 cachedirname = config.config().cache_dir 

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

1162 return pile.make_pile( 

1163 sources, 

1164 cachedirname=cachedirname, 

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

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

1167 

1168 def make_panel(self, parent): 

1169 ''' 

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

1171 

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

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

1174 parameters). 

1175 ''' 

1176 

1177 params = self.get_parameters() 

1178 self._param_controls = {} 

1179 if params or self._force_panel: 

1180 sarea = MyScrollArea(parent.get_panel_parent_widget()) 

1181 sarea.setFrameStyle(qw.QFrame.NoFrame) 

1182 sarea.setSizePolicy(qw.QSizePolicy( 

1183 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)) 

1184 frame = MyFrame(sarea) 

1185 frame.widgetVisibilityChanged.connect( 

1186 self.panel_visibility_changed) 

1187 

1188 frame.setSizePolicy(qw.QSizePolicy( 

1189 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1190 frame.setFrameStyle(qw.QFrame.NoFrame) 

1191 sarea.setWidget(frame) 

1192 sarea.setWidgetResizable(True) 

1193 layout = qw.QGridLayout() 

1194 layout.setContentsMargins(0, 0, 0, 0) 

1195 layout.setSpacing(0) 

1196 frame.setLayout(layout) 

1197 

1198 parlayout = qw.QGridLayout() 

1199 

1200 irow = 0 

1201 ipar = 0 

1202 have_switches = False 

1203 have_params = False 

1204 for iparam, param in enumerate(params): 

1205 if isinstance(param, Param): 

1206 if param.minimum <= 0.0: 

1207 param_control = LinValControl( 

1208 high_is_none=param.high_is_none, 

1209 low_is_none=param.low_is_none, 

1210 type=param.type) 

1211 else: 

1212 param_control = ValControl( 

1213 high_is_none=param.high_is_none, 

1214 low_is_none=param.low_is_none, 

1215 low_is_zero=param.low_is_zero, 

1216 type=param.type) 

1217 

1218 param_control.setup( 

1219 param.name, 

1220 param.minimum, 

1221 param.maximum, 

1222 param.default, 

1223 iparam) 

1224 

1225 param_control.set_tracking(param.tracking) 

1226 param_control.valchange.connect( 

1227 self.modified_snuffling_panel) 

1228 

1229 self._param_controls[param.ident] = param_control 

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

1231 parlayout.addWidget(w, ipar, iw) 

1232 

1233 ipar += 1 

1234 have_params = True 

1235 

1236 elif isinstance(param, Choice): 

1237 param_widget = ChoiceControl( 

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

1239 param_widget.choosen.connect( 

1240 self.choose_on_snuffling_panel) 

1241 

1242 self._param_controls[param.ident] = param_widget 

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

1244 ipar += 1 

1245 have_params = True 

1246 

1247 elif isinstance(param, Switch): 

1248 have_switches = True 

1249 

1250 if have_params: 

1251 parframe = qw.QFrame(sarea) 

1252 parframe.setSizePolicy(qw.QSizePolicy( 

1253 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1254 parframe.setLayout(parlayout) 

1255 layout.addWidget(parframe, irow, 0) 

1256 irow += 1 

1257 

1258 if have_switches: 

1259 swlayout = qw.QGridLayout() 

1260 isw = 0 

1261 for iparam, param in enumerate(params): 

1262 if isinstance(param, Switch): 

1263 param_widget = SwitchControl( 

1264 param.ident, param.default, param.name) 

1265 param_widget.sw_toggled.connect( 

1266 self.switch_on_snuffling_panel) 

1267 

1268 self._param_controls[param.ident] = param_widget 

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

1270 isw += 1 

1271 

1272 swframe = qw.QFrame(sarea) 

1273 swframe.setSizePolicy(qw.QSizePolicy( 

1274 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1275 swframe.setLayout(swlayout) 

1276 layout.addWidget(swframe, irow, 0) 

1277 irow += 1 

1278 

1279 butframe = qw.QFrame(sarea) 

1280 butframe.setSizePolicy(qw.QSizePolicy( 

1281 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1282 butlayout = qw.QHBoxLayout() 

1283 butframe.setLayout(butlayout) 

1284 

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

1286 if self._live_update: 

1287 live_update_checkbox.setCheckState(qc.Qt.Checked) 

1288 

1289 butlayout.addWidget(live_update_checkbox) 

1290 live_update_checkbox.toggled.connect( 

1291 self.live_update_toggled) 

1292 

1293 help_button = qw.QPushButton('Help') 

1294 butlayout.addWidget(help_button) 

1295 help_button.clicked.connect( 

1296 self.help_button_triggered) 

1297 

1298 clear_button = qw.QPushButton('Clear') 

1299 butlayout.addWidget(clear_button) 

1300 clear_button.clicked.connect( 

1301 self.clear_button_triggered) 

1302 

1303 call_button = qw.QPushButton('Run') 

1304 butlayout.addWidget(call_button) 

1305 call_button.clicked.connect( 

1306 self.call_button_triggered) 

1307 

1308 for name, method in self._triggers: 

1309 but = qw.QPushButton(name) 

1310 

1311 def call_and_update(method): 

1312 def f(): 

1313 try: 

1314 method() 

1315 except SnufflingError as e: 

1316 if not isinstance(e, SnufflingCallFailed): 

1317 # those have logged within error() 

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

1319 logger.error( 

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

1321 

1322 self.get_viewer().update() 

1323 return f 

1324 

1325 but.clicked.connect( 

1326 call_and_update(method)) 

1327 

1328 butlayout.addWidget(but) 

1329 

1330 layout.addWidget(butframe, irow, 0) 

1331 

1332 irow += 1 

1333 spacer = qw.QSpacerItem( 

1334 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

1335 

1336 layout.addItem(spacer, irow, 0) 

1337 

1338 return sarea 

1339 

1340 else: 

1341 return None 

1342 

1343 def make_helpmenuitem(self, parent): 

1344 ''' 

1345 Create the help menu item for the snuffling. 

1346 ''' 

1347 

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

1349 

1350 item.triggered.connect( 

1351 self.help_button_triggered) 

1352 

1353 return item 

1354 

1355 def make_menuitem(self, parent): 

1356 ''' 

1357 Create the menu item for the snuffling. 

1358 

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

1360 menu entry is wanted. 

1361 ''' 

1362 

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

1364 item.setCheckable( 

1365 self._have_pre_process_hook or self._have_post_process_hook) 

1366 

1367 item.triggered.connect( 

1368 self.menuitem_triggered) 

1369 

1370 return item 

1371 

1372 def output_filename( 

1373 self, 

1374 caption='Save File', 

1375 dir='', 

1376 filter='', 

1377 selected_filter=None): 

1378 

1379 ''' 

1380 Query user for an output filename. 

1381 

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

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

1384 dialog. 

1385 ''' 

1386 

1387 if not dir and self._previous_output_filename: 

1388 dir = self._previous_output_filename 

1389 

1390 fn = getSaveFileName( 

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

1392 if not fn: 

1393 raise UserCancelled() 

1394 

1395 self._previous_output_filename = fn 

1396 return str(fn) 

1397 

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

1399 ''' 

1400 Query user for an input directory. 

1401 

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

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

1404 dialog. 

1405 ''' 

1406 

1407 if not dir and self._previous_input_directory: 

1408 dir = self._previous_input_directory 

1409 

1410 dn = qw.QFileDialog.getExistingDirectory( 

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

1412 

1413 if not dn: 

1414 raise UserCancelled() 

1415 

1416 self._previous_input_directory = dn 

1417 return str(dn) 

1418 

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

1420 selected_filter=None): 

1421 ''' 

1422 Query user for an input filename. 

1423 

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

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

1426 dialog. 

1427 ''' 

1428 

1429 if not dir and self._previous_input_filename: 

1430 dir = self._previous_input_filename 

1431 

1432 fn, _ = qw.QFileDialog.getOpenFileName( 

1433 self.get_viewer(), 

1434 caption, 

1435 dir, 

1436 filter) 

1437 

1438 if not fn: 

1439 raise UserCancelled() 

1440 

1441 self._previous_input_filename = fn 

1442 return str(fn) 

1443 

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

1445 ''' 

1446 Query user for a text input. 

1447 

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

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

1450 dialog. 

1451 ''' 

1452 

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

1454 

1455 if not ok: 

1456 raise UserCancelled() 

1457 

1458 return inp 

1459 

1460 def modified_snuffling_panel(self, value, iparam): 

1461 ''' 

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

1463 

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

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

1466 widget. 

1467 ''' 

1468 

1469 param = self.get_parameters()[iparam] 

1470 self._set_parameter_value(param.ident, value) 

1471 if self._live_update: 

1472 self.check_call() 

1473 self.get_viewer().update() 

1474 

1475 def switch_on_snuffling_panel(self, ident, state): 

1476 ''' 

1477 Called when the user has toggled a switchable parameter. 

1478 ''' 

1479 

1480 self._set_parameter_value(ident, state) 

1481 if self._live_update: 

1482 self.check_call() 

1483 self.get_viewer().update() 

1484 

1485 def choose_on_snuffling_panel(self, ident, state): 

1486 ''' 

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

1488 ''' 

1489 

1490 self._set_parameter_value(ident, state) 

1491 if self._live_update: 

1492 self.check_call() 

1493 self.get_viewer().update() 

1494 

1495 def menuitem_triggered(self, arg): 

1496 ''' 

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

1498 

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

1500 and triggers an update on the viewer widget. 

1501 ''' 

1502 

1503 self.check_call() 

1504 

1505 if self._have_pre_process_hook: 

1506 self._pre_process_hook_enabled = arg 

1507 

1508 if self._have_post_process_hook: 

1509 self._post_process_hook_enabled = arg 

1510 

1511 if self._have_pre_process_hook or self._have_post_process_hook: 

1512 self.get_viewer().clean_update() 

1513 else: 

1514 self.get_viewer().update() 

1515 

1516 def call_button_triggered(self): 

1517 ''' 

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

1519 

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

1521 and triggers an update on the viewer widget. 

1522 ''' 

1523 

1524 self.check_call() 

1525 self.get_viewer().update() 

1526 

1527 def clear_button_triggered(self): 

1528 ''' 

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

1530 

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

1532 viewer widget. 

1533 ''' 

1534 

1535 self.cleanup() 

1536 self.get_viewer().update() 

1537 

1538 def help_button_triggered(self): 

1539 ''' 

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

1541 given in the snufflings' __doc__ string. 

1542 ''' 

1543 

1544 if self.__doc__: 

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

1546 doc = qw.QLabel(self.__doc__) 

1547 else: 

1548 try: 

1549 import markdown 

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

1551 

1552 except ImportError: 

1553 logger.error( 

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

1555 'formatting.') 

1556 

1557 doc = qw.QLabel(self.__doc__) 

1558 else: 

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

1560 

1561 labels = [doc] 

1562 

1563 if self._filename: 

1564 from html import escape 

1565 

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

1567 

1568 doc_src = qw.QLabel( 

1569 '''<html><body> 

1570<hr /> 

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

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

1573<br /> 

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

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

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

1577 % ( 

1578 quote(self._filename), 

1579 escape(self._filename), 

1580 escape(code))) 

1581 

1582 labels.append(doc_src) 

1583 

1584 for h in labels: 

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

1586 h.setWordWrap(True) 

1587 

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

1589 

1590 def live_update_toggled(self, on): 

1591 ''' 

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

1593 ''' 

1594 

1595 self.set_live_update(on) 

1596 

1597 def add_traces(self, traces): 

1598 ''' 

1599 Add traces to the viewer. 

1600 

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

1602 

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

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

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

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

1607 traces are added. 

1608 

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

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

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

1612 method). 

1613 ''' 

1614 

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

1616 self._tickets.append(ticket) 

1617 return ticket 

1618 

1619 def add_trace(self, tr): 

1620 ''' 

1621 Add a trace to the viewer. 

1622 

1623 See :py:meth:`add_traces`. 

1624 ''' 

1625 

1626 self.add_traces([tr]) 

1627 

1628 def add_markers(self, markers): 

1629 ''' 

1630 Add some markers to the display. 

1631 

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

1633 adds these to the viewer. 

1634 ''' 

1635 

1636 self.get_viewer().add_markers(markers) 

1637 self._markers.extend(markers) 

1638 

1639 def add_marker(self, marker): 

1640 ''' 

1641 Add a marker to the display. 

1642 

1643 See :py:meth:`add_markers`. 

1644 ''' 

1645 

1646 self.add_markers([marker]) 

1647 

1648 def cleanup(self): 

1649 ''' 

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

1651 snuffling. 

1652 ''' 

1653 

1654 try: 

1655 viewer = self.get_viewer() 

1656 viewer.release_data(self._tickets) 

1657 viewer.remove_markers(self._markers) 

1658 

1659 except NoViewerSet: 

1660 pass 

1661 

1662 self._tickets = [] 

1663 self._markers = [] 

1664 

1665 def check_call(self): 

1666 

1667 if self._call_in_progress: 

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

1669 return 

1670 

1671 try: 

1672 self._call_in_progress = True 

1673 self.call() 

1674 return 0 

1675 

1676 except SnufflingError as e: 

1677 if not isinstance(e, SnufflingCallFailed): 

1678 # those have logged within error() 

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

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

1681 return 1 

1682 

1683 except Exception as e: 

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

1685 self._name, str(e)) 

1686 

1687 logger.exception(message) 

1688 self.show_message('error', message) 

1689 

1690 finally: 

1691 self._call_in_progress = False 

1692 

1693 def call(self): 

1694 ''' 

1695 Main work routine of the snuffling. 

1696 

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

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

1699 in subclass. The default implementation does nothing useful. 

1700 ''' 

1701 

1702 pass 

1703 

1704 def pre_process_hook(self, traces): 

1705 return traces 

1706 

1707 def post_process_hook(self, traces): 

1708 return traces 

1709 

1710 def get_tpad(self): 

1711 ''' 

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

1713 ''' 

1714 

1715 return 0.0 

1716 

1717 def pre_destroy(self): 

1718 ''' 

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

1720 

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

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

1723 the snuffling`s tempory directory, if needed. 

1724 ''' 

1725 

1726 self.cleanup() 

1727 if self._tempdir is not None: 

1728 import shutil 

1729 shutil.rmtree(self._tempdir) 

1730 

1731 

1732class SnufflingError(Exception): 

1733 pass 

1734 

1735 

1736class NoViewerSet(SnufflingError): 

1737 ''' 

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

1739 ''' 

1740 

1741 def __str__(self): 

1742 return 'No GUI available. ' \ 

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

1744 

1745 

1746class MissingStationInformation(SnufflingError): 

1747 ''' 

1748 Raised when station information is missing. 

1749 ''' 

1750 

1751 

1752class NoTracesSelected(SnufflingError): 

1753 ''' 

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

1755 and we cannot fallback to using the current view. 

1756 ''' 

1757 

1758 def __str__(self): 

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

1760 

1761 

1762class UserCancelled(SnufflingError): 

1763 ''' 

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

1765 ''' 

1766 

1767 def __str__(self): 

1768 return 'The user has cancelled a dialog.' 

1769 

1770 

1771class SnufflingCallFailed(SnufflingError): 

1772 ''' 

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

1774 :py:meth:`Snuffling.call`. 

1775 ''' 

1776 

1777 

1778class InvalidSnufflingFilename(Exception): 

1779 pass 

1780 

1781 

1782class SnufflingModule(object): 

1783 ''' 

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

1785 

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

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

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

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

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

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

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

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

1794 when needed. 

1795 ''' 

1796 

1797 mtimes = {} 

1798 

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

1800 self._path = path 

1801 self._name = name 

1802 self._mtime = None 

1803 self._module = None 

1804 self._snufflings = [] 

1805 self._handler = handler 

1806 

1807 def load_if_needed(self): 

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

1809 

1810 try: 

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

1812 except OSError as e: 

1813 if e.errno == 2: 

1814 logger.error(e) 

1815 raise BrokenSnufflingModule(filename) 

1816 

1817 if self._module is None: 

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

1819 try: 

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

1821 if self._name in sys.modules: 

1822 raise InvalidSnufflingFilename(self._name) 

1823 

1824 self._module = __import__(self._name) 

1825 del sys.modules[self._name] 

1826 

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

1828 snuffling._filename = filename 

1829 self.add_snuffling(snuffling) 

1830 

1831 except Exception: 

1832 logger.error(traceback.format_exc()) 

1833 raise BrokenSnufflingModule(filename) 

1834 

1835 finally: 

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

1837 

1838 elif self._mtime != mtime: 

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

1840 settings = self.remove_snufflings() 

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

1842 try: 

1843 

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

1845 

1846 reload(self._module) 

1847 del sys.modules[self._name] 

1848 

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

1850 snuffling._filename = filename 

1851 self.add_snuffling(snuffling, reloaded=True) 

1852 

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

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

1855 snuf.set_settings(sett) 

1856 

1857 except Exception: 

1858 logger.error(traceback.format_exc()) 

1859 raise BrokenSnufflingModule(filename) 

1860 

1861 finally: 

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

1863 

1864 self._mtime = mtime 

1865 

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

1867 snuffling._path = self._path 

1868 snuffling.setup() 

1869 self._snufflings.append(snuffling) 

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

1871 

1872 def remove_snufflings(self): 

1873 settings = [] 

1874 for snuffling in self._snufflings: 

1875 settings.append(snuffling.get_settings()) 

1876 self._handler.remove_snuffling(snuffling) 

1877 

1878 self._snufflings = [] 

1879 return settings 

1880 

1881 

1882class BrokenSnufflingModule(Exception): 

1883 pass 

1884 

1885 

1886class MyScrollArea(qw.QScrollArea): 

1887 

1888 def sizeHint(self): 

1889 s = qc.QSize() 

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

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

1892 return s 

1893 

1894 

1895class SwitchControl(qw.QCheckBox): 

1896 sw_toggled = qc.pyqtSignal(object, bool) 

1897 

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

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

1900 self.ident = ident 

1901 self.setChecked(default) 

1902 self.toggled.connect(self._sw_toggled) 

1903 

1904 def _sw_toggled(self, state): 

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

1906 

1907 def set_value(self, state): 

1908 self.blockSignals(True) 

1909 self.setChecked(state) 

1910 self.blockSignals(False) 

1911 

1912 

1913class ChoiceControl(qw.QFrame): 

1914 choosen = qc.pyqtSignal(object, object) 

1915 

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

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

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

1919 self.label.setMinimumWidth(120) 

1920 self.cbox = qw.QComboBox(self) 

1921 self.layout = qw.QHBoxLayout(self) 

1922 self.layout.addWidget(self.label) 

1923 self.layout.addWidget(self.cbox) 

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

1925 self.layout.setSpacing(0) 

1926 self.ident = ident 

1927 self.choices = choices 

1928 for ichoice, choice in enumerate(choices): 

1929 self.cbox.addItem(choice) 

1930 

1931 self.set_value(default) 

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

1933 

1934 def set_choices(self, choices): 

1935 icur = self.cbox.currentIndex() 

1936 if icur != -1: 

1937 selected_choice = choices[icur] 

1938 else: 

1939 selected_choice = None 

1940 

1941 self.choices = choices 

1942 self.cbox.clear() 

1943 for ichoice, choice in enumerate(choices): 

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

1945 

1946 if selected_choice is not None and selected_choice in choices: 

1947 self.set_value(selected_choice) 

1948 return selected_choice 

1949 else: 

1950 self.set_value(choices[0]) 

1951 return choices[0] 

1952 

1953 def emit_choosen(self, i): 

1954 self.choosen.emit( 

1955 self.ident, 

1956 self.choices[i]) 

1957 

1958 def set_value(self, v): 

1959 self.cbox.blockSignals(True) 

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

1961 if choice == v: 

1962 self.cbox.setCurrentIndex(i) 

1963 self.cbox.blockSignals(False)