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 

81 if low_is_none and default == minimum: 

82 default = None 

83 if high_is_none and default == maximum: 

84 default = None 

85 

86 self.name = name 

87 self.ident = ident 

88 self.default = default 

89 self.minimum = minimum 

90 self.maximum = maximum 

91 self.low_is_none = low_is_none 

92 self.high_is_none = high_is_none 

93 self.low_is_zero = low_is_zero 

94 self._control = None 

95 

96 

97class Switch(object): 

98 ''' 

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

100 may display a checkbox for such a switch. 

101 

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

103 :param ident: identifier of the parameter 

104 :param default: default value 

105 ''' 

106 

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

108 self.name = name 

109 self.ident = ident 

110 self.default = default 

111 

112 

113class Choice(object): 

114 ''' 

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

116 may display a menu for such a choice. 

117 

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

119 :param ident: identifier of the parameter 

120 :param default: default value 

121 :param choices: tuple of other options 

122 ''' 

123 

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

125 self.name = name 

126 self.ident = ident 

127 self.default = default 

128 self.choices = choices 

129 

130 

131class Snuffling(object): 

132 ''' 

133 Base class for user snufflings. 

134 

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

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

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

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

139 

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

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

142 ''' 

143 

144 def __init__(self): 

145 self._path = None 

146 

147 self._name = 'Untitled Snuffling' 

148 self._viewer = None 

149 self._tickets = [] 

150 self._markers = [] 

151 

152 self._delete_panel = None 

153 self._delete_menuitem = None 

154 

155 self._panel_parent = None 

156 self._menu_parent = None 

157 

158 self._panel = None 

159 self._menuitem = None 

160 self._helpmenuitem = None 

161 self._parameters = [] 

162 self._param_controls = {} 

163 

164 self._triggers = [] 

165 

166 self._live_update = True 

167 self._previous_output_filename = None 

168 self._previous_input_filename = None 

169 self._previous_input_directory = None 

170 

171 self._tempdir = None 

172 self._iplot = 0 

173 

174 self._have_pre_process_hook = False 

175 self._have_post_process_hook = False 

176 self._pre_process_hook_enabled = False 

177 self._post_process_hook_enabled = False 

178 

179 self._no_viewer_pile = None 

180 self._cli_params = {} 

181 self._filename = None 

182 self._force_panel = False 

183 self._call_in_progress = False 

184 

185 def setup(self): 

186 ''' 

187 Setup the snuffling. 

188 

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

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

191 ''' 

192 

193 pass 

194 

195 def module_dir(self): 

196 ''' 

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

198 

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

200 ''' 

201 

202 return self._path 

203 

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

205 ''' 

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

207 

208 This method is called from the 

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

210 :py:meth:`setup_gui`. 

211 ''' 

212 

213 self._viewer = viewer 

214 self._panel_parent = panel_parent 

215 self._menu_parent = menu_parent 

216 

217 self.setup_gui(reloaded=reloaded) 

218 

219 def setup_gui(self, reloaded=False): 

220 ''' 

221 Create and add gui elements to the viewer. 

222 

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

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

225 snuffling has been changed. 

226 ''' 

227 

228 if self._panel_parent is not None: 

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

230 if self._panel: 

231 self._panel_parent.add_panel( 

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

233 

234 if self._menu_parent is not None: 

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

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

237 if self._menuitem: 

238 self._menu_parent.add_snuffling_menuitem(self._menuitem) 

239 

240 if self._helpmenuitem: 

241 self._menu_parent.add_snuffling_help_menuitem( 

242 self._helpmenuitem) 

243 

244 def set_force_panel(self, bool=True): 

245 ''' 

246 Force to create a panel. 

247 

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

249 button. 

250 ''' 

251 

252 self._force_panel = bool 

253 

254 def make_cli_parser1(self): 

255 import optparse 

256 

257 class MyOptionParser(optparse.OptionParser): 

258 def error(self, msg): 

259 logger.error(msg) 

260 self.exit(1) 

261 

262 parser = MyOptionParser() 

263 

264 parser.add_option( 

265 '--format', 

266 dest='format', 

267 default='from_extension', 

268 choices=( 

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

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

271 'from_extension', 'detect'), 

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

273 

274 parser.add_option( 

275 '--pattern', 

276 dest='regex', 

277 metavar='REGEX', 

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

279 

280 self.add_params_to_cli_parser(parser) 

281 self.configure_cli_parser(parser) 

282 return parser 

283 

284 def configure_cli_parser(self, parser): 

285 pass 

286 

287 def cli_usage(self): 

288 return None 

289 

290 def add_params_to_cli_parser(self, parser): 

291 

292 for param in self._parameters: 

293 if isinstance(param, Param): 

294 parser.add_option( 

295 '--' + param.ident, 

296 dest=param.ident, 

297 default=param.default, 

298 type='float', 

299 help=param.name) 

300 

301 def setup_cli(self): 

302 self.setup() 

303 parser = self.make_cli_parser1() 

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

305 

306 for param in self._parameters: 

307 if isinstance(param, Param): 

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

309 

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

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

312 self._cli_params['sources'] = args 

313 

314 return options, args, parser 

315 

316 def delete_gui(self): 

317 ''' 

318 Remove the gui elements of the snuffling. 

319 

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

321 also removes all traces and markers added with the 

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

323 ''' 

324 

325 self.cleanup() 

326 

327 if self._panel is not None: 

328 self._panel_parent.remove_panel(self._panel) 

329 self._panel = None 

330 

331 if self._menuitem is not None: 

332 self._menu_parent.remove_snuffling_menuitem(self._menuitem) 

333 self._menuitem = None 

334 

335 if self._helpmenuitem is not None: 

336 self._menu_parent.remove_snuffling_help_menuitem( 

337 self._helpmenuitem) 

338 

339 def set_name(self, name): 

340 ''' 

341 Set the snuffling's name. 

342 

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

344 ''' 

345 

346 self._name = name 

347 self.reset_gui() 

348 

349 def get_name(self): 

350 ''' 

351 Get the snuffling's name. 

352 ''' 

353 

354 return self._name 

355 

356 def set_have_pre_process_hook(self, bool): 

357 self._have_pre_process_hook = bool 

358 self._live_update = False 

359 self._pre_process_hook_enabled = False 

360 self.reset_gui() 

361 

362 def set_have_post_process_hook(self, bool): 

363 self._have_post_process_hook = bool 

364 self._live_update = False 

365 self._post_process_hook_enabled = False 

366 self.reset_gui() 

367 

368 def set_have_pile_changed_hook(self, bool): 

369 self._pile_ = False 

370 

371 def enable_pile_changed_notifications(self): 

372 ''' 

373 Get informed when pile changed. 

374 

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

376 update in the viewer's pile. 

377 ''' 

378 

379 viewer = self.get_viewer() 

380 viewer.pile_has_changed_signal.connect( 

381 self.pile_changed) 

382 

383 def disable_pile_changed_notifications(self): 

384 ''' 

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

386 ''' 

387 

388 viewer = self.get_viewer() 

389 viewer.pile_has_changed_signal.disconnect( 

390 self.pile_changed) 

391 

392 def pile_changed(self): 

393 ''' 

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

395 

396 Must be activated with a call to 

397 :py:meth:`enable_pile_changed_notifications`. 

398 ''' 

399 

400 pass 

401 

402 def reset_gui(self, reloaded=False): 

403 ''' 

404 Delete and recreate the snuffling's panel. 

405 ''' 

406 

407 if self._panel or self._menuitem: 

408 sett = self.get_settings() 

409 self.delete_gui() 

410 self.setup_gui(reloaded=reloaded) 

411 self.set_settings(sett) 

412 

413 def show_message(self, kind, message): 

414 ''' 

415 Display a message box. 

416 

417 :param kind: string defining kind of message 

418 :param message: the message to be displayed 

419 ''' 

420 

421 try: 

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

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

424 box.exec_() 

425 except NoViewerSet: 

426 pass 

427 

428 def error(self, message): 

429 ''' 

430 Show an error message box. 

431 

432 :param message: specifying the error 

433 ''' 

434 

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

436 self.show_message('error', message) 

437 

438 def warn(self, message): 

439 ''' 

440 Display a warning message. 

441 

442 :param message: specifying the warning 

443 ''' 

444 

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

446 self.show_message('warning', message) 

447 

448 def fail(self, message): 

449 ''' 

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

451 exception. 

452 

453 :param message: specifying the error 

454 ''' 

455 

456 self.error(message) 

457 raise SnufflingCallFailed(message) 

458 

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

460 ''' 

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

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

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

464 

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

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

467 ''' 

468 

469 if name is None: 

470 self._iplot += 1 

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

472 

473 fframe = FigureFrame() 

474 self._panel_parent.add_tab(name, fframe) 

475 if get == 'axes': 

476 return fframe.gca() 

477 elif get == 'figure': 

478 return fframe.gcf() 

479 elif get == 'figure_frame': 

480 return fframe 

481 

482 def figure(self, name=None): 

483 ''' 

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

485 which can be displayed within snuffler by calling 

486 :py:meth:`canvas.draw`. 

487 

488 :param name: labels the tab of the figure 

489 ''' 

490 

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

492 

493 def axes(self, name=None): 

494 ''' 

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

496 

497 :param name: labels the tab of axes 

498 ''' 

499 

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

501 

502 def figure_frame(self, name=None): 

503 ''' 

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

505 

506 :param name: labels the tab figure frame 

507 ''' 

508 

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

510 

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

512 ''' 

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

514 

515 :param name: labels the tab 

516 :param filename: name of file to be displayed 

517 ''' 

518 

519 f = PixmapFrame(filename) 

520 

521 scroll_area = qw.QScrollArea() 

522 scroll_area.setWidget(f) 

523 scroll_area.setWidgetResizable(True) 

524 

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

526 return f 

527 

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

529 ''' 

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

531 within snuffler. 

532 

533 :param url: url to open 

534 :param name: labels the tab 

535 ''' 

536 

537 if name is None: 

538 self._iplot += 1 

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

540 

541 f = WebKitFrame(url) 

542 self._panel_parent.add_tab(name, f) 

543 return f 

544 

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

546 ''' 

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

548 graphics. 

549 

550 :param actors: list of VTKActors 

551 :param name: labels the tab 

552 

553 Initialize the interactive rendering by calling the frames' 

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

555 renderer. 

556 

557 Requires installation of vtk including python wrapper. 

558 ''' 

559 if name is None: 

560 self._iplot += 1 

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

562 

563 try: 

564 f = VTKFrame(actors=actors) 

565 except ImportError as e: 

566 self.fail(e) 

567 

568 self._panel_parent.add_tab(name, f) 

569 return f 

570 

571 def tempdir(self): 

572 ''' 

573 Create a temporary directory and return its absolute path. 

574 

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

576 instance is deleted. 

577 ''' 

578 

579 if self._tempdir is None: 

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

581 

582 return self._tempdir 

583 

584 def set_live_update(self, live_update): 

585 ''' 

586 Enable/disable live updating. 

587 

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

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

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

591 item or pressing the call button. 

592 ''' 

593 

594 self._live_update = live_update 

595 if self._have_pre_process_hook: 

596 self._pre_process_hook_enabled = live_update 

597 if self._have_post_process_hook: 

598 self._post_process_hook_enabled = live_update 

599 

600 try: 

601 self.get_viewer().clean_update() 

602 except NoViewerSet: 

603 pass 

604 

605 def add_parameter(self, param): 

606 ''' 

607 Add an adjustable parameter to the snuffling. 

608 

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

610 :py:class:`Choice`. 

611 

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

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

614 ''' 

615 

616 self._parameters.append(param) 

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

618 

619 if self._panel is not None: 

620 self.delete_gui() 

621 self.setup_gui() 

622 

623 def add_trigger(self, name, method): 

624 ''' 

625 Add a button to the snuffling's panel. 

626 

627 :param name: string that labels the button 

628 :param method: method associated with the button 

629 ''' 

630 

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

632 

633 if self._panel is not None: 

634 self.delete_gui() 

635 self.setup_gui() 

636 

637 def get_parameters(self): 

638 ''' 

639 Get the snuffling's adjustable parameter definitions. 

640 

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

642 ''' 

643 

644 return self._parameters 

645 

646 def get_parameter(self, ident): 

647 ''' 

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

649 

650 :param ident: identifier of the parameter 

651 

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

653 ''' 

654 

655 for param in self._parameters: 

656 if param.ident == ident: 

657 return param 

658 return None 

659 

660 def set_parameter(self, ident, value): 

661 ''' 

662 Set one of the snuffling's adjustable parameters. 

663 

664 :param ident: identifier of the parameter 

665 :param value: new value of the parameter 

666 

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

668 ''' 

669 

670 self._set_parameter_value(ident, value) 

671 

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

673 if control: 

674 control.set_value(value) 

675 

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

677 ''' 

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

679 

680 :param ident: identifier of the parameter 

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

682 

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

684 ''' 

685 

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

687 if control: 

688 control.set_range(vmin, vmax) 

689 

690 def set_parameter_choices(self, ident, choices): 

691 ''' 

692 Update the choices of a Choice parameter. 

693 

694 :param ident: identifier of the parameter 

695 :param choices: list of strings 

696 ''' 

697 

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

699 if control: 

700 selected_choice = control.set_choices(choices) 

701 self._set_parameter_value(ident, selected_choice) 

702 

703 def _set_parameter_value(self, ident, value): 

704 setattr(self, ident, value) 

705 

706 def get_parameter_value(self, ident): 

707 ''' 

708 Get the current value of a parameter. 

709 

710 :param ident: identifier of the parameter 

711 ''' 

712 return getattr(self, ident) 

713 

714 def get_settings(self): 

715 ''' 

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

717 their values as the dictionaries values. 

718 ''' 

719 

720 params = self.get_parameters() 

721 settings = {} 

722 for param in params: 

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

724 

725 return settings 

726 

727 def set_settings(self, settings): 

728 params = self.get_parameters() 

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

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

731 if k in dparams: 

732 self._set_parameter_value(k, v) 

733 if k in self._param_controls: 

734 control = self._param_controls[k] 

735 control.set_value(v) 

736 

737 def get_viewer(self): 

738 ''' 

739 Get the parent viewer. 

740 

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

742 which is the main viewer widget. 

743 

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

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

746 ''' 

747 

748 if self._viewer is None: 

749 raise NoViewerSet() 

750 return self._viewer 

751 

752 def get_pile(self): 

753 ''' 

754 Get the pile. 

755 

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

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

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

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

760 mode. 

761 ''' 

762 

763 try: 

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

765 except NoViewerSet: 

766 if self._no_viewer_pile is None: 

767 self._no_viewer_pile = self.make_pile() 

768 

769 p = self._no_viewer_pile 

770 

771 return p 

772 

773 def get_active_event_and_stations( 

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

775 

776 ''' 

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

778 

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

780 query for available data 

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

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

783 

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

785 ''' 

786 

787 p = self.get_pile() 

788 v = self.get_viewer() 

789 

790 event = v.get_active_event() 

791 if event is None: 

792 self.fail( 

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

794 'it the "active event"') 

795 

796 stations = {} 

797 for traces in p.chopper( 

798 event.time+trange[0], 

799 event.time+trange[1], 

800 load_data=False, 

801 degap=False): 

802 

803 for tr in traces: 

804 try: 

805 for skey in v.station_keys(tr): 

806 if skey in stations: 

807 continue 

808 

809 station = v.get_station(skey) 

810 stations[skey] = station 

811 

812 except KeyError: 

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

814 % '.'.join(skey) 

815 

816 if missing == 'warn': 

817 logger.warning(s) 

818 elif missing == 'raise': 

819 raise MissingStationInformation(s) 

820 elif missing == 'ignore': 

821 pass 

822 else: 

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

824 

825 stations[skey] = None 

826 

827 return event, list(set( 

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

829 

830 def get_stations(self): 

831 ''' 

832 Get all stations known to the viewer. 

833 ''' 

834 

835 v = self.get_viewer() 

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

837 return stations 

838 

839 def get_markers(self): 

840 ''' 

841 Get all markers from the viewer. 

842 ''' 

843 

844 return self.get_viewer().get_markers() 

845 

846 def get_event_markers(self): 

847 ''' 

848 Get all event markers from the viewer. 

849 ''' 

850 

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

852 if isinstance(m, EventMarker)] 

853 

854 def get_selected_markers(self): 

855 ''' 

856 Get all selected markers from the viewer. 

857 ''' 

858 

859 return self.get_viewer().selected_markers() 

860 

861 def get_selected_event_markers(self): 

862 ''' 

863 Get all selected event markers from the viewer. 

864 ''' 

865 

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

867 if isinstance(m, EventMarker)] 

868 

869 def get_active_event_and_phase_markers(self): 

870 ''' 

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

872 ''' 

873 

874 viewer = self.get_viewer() 

875 markers = viewer.get_markers() 

876 event_marker = viewer.get_active_event_marker() 

877 if event_marker is None: 

878 self.fail( 

879 'No active event set. ' 

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

881 

882 event = event_marker.get_event() 

883 

884 selection = [] 

885 for m in markers: 

886 if isinstance(m, PhaseMarker): 

887 if m.get_event() is event: 

888 selection.append(m) 

889 

890 return ( 

891 event_marker, 

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

893 m.get_event() == event]) 

894 

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

896 ''' 

897 Get currently active trace selector from viewer. 

898 

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

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

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

902 to disable any restrictions. 

903 ''' 

904 

905 viewer = self.get_viewer() 

906 

907 def rtrue(tr): 

908 return True 

909 

910 if mode == 'inview': 

911 return viewer.trace_selector or rtrue 

912 elif mode == 'visible': 

913 return viewer.trace_filter or rtrue 

914 elif mode == 'all': 

915 return rtrue 

916 else: 

917 raise Exception('invalid mode argument') 

918 

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

920 mode='inview', main_bandpass=False, 

921 progress=None, responsive=False, 

922 *args, **kwargs): 

923 ''' 

924 Iterate over selected traces. 

925 

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

927 running snuffler. For each selected marker, 

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

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

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

931 *\\*args* and *\\*\\*kwargs*. 

932 

933 :param fallback: 

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

935 currently visible in the viewer. 

936 

937 :param marker_selector: 

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

939 

940 :param mode: 

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

942 shown in the viewer (excluding traces accessible through vertical 

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

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

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

946 restrictions. 

947 

948 :param main_bandpass: 

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

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

951 order Butterworth highpass and lowpass and the signal is always 

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

953 bandpass settings from the graphical interface are not respected 

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

955 artifacts. 

956 

957 :param progress: 

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

959 is used as the label for the progress bar. 

960 

961 :param responsive: 

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

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

964 to be aborted by the user. 

965 ''' 

966 

967 try: 

968 viewer = self.get_viewer() 

969 markers = [ 

970 m for m in viewer.selected_markers() 

971 if not isinstance(m, EventMarker)] 

972 

973 if marker_selector is not None: 

974 markers = [ 

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

976 

977 pile = self.get_pile() 

978 

979 def rtrue(tr): 

980 return True 

981 

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

983 trace_selector_viewer = self.get_viewer_trace_selector(mode) 

984 

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

986 

987 if main_bandpass: 

988 def apply_filters(traces): 

989 for tr in traces: 

990 if viewer.highpass is not None: 

991 tr.highpass(4, viewer.highpass) 

992 if viewer.lowpass is not None: 

993 tr.lowpass(4, viewer.lowpass) 

994 return traces 

995 else: 

996 def apply_filters(traces): 

997 return traces 

998 

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

1000 

1001 time_last = [time.time()] 

1002 

1003 def update_progress(label, batch): 

1004 time_now = time.time() 

1005 if responsive: 

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

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

1008 # changes etc. 

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

1010 qw.qApp.processEvents() 

1011 else: 

1012 # redraw about once a second 

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

1014 viewer.repaint() 

1015 

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

1017 

1018 abort = pb.set_status( 

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

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

1021 

1022 return abort 

1023 

1024 if markers: 

1025 for imarker, marker in enumerate(markers): 

1026 try: 

1027 if progress: 

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

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

1030 

1031 pb.set_status(label, 0, responsive) 

1032 

1033 if not marker.nslc_ids: 

1034 trace_selector_marker = rtrue 

1035 else: 

1036 def trace_selector_marker(tr): 

1037 return marker.match_nslc(tr.nslc_id) 

1038 

1039 def trace_selector(tr): 

1040 return trace_selector_arg(tr) \ 

1041 and trace_selector_viewer(tr) \ 

1042 and trace_selector_marker(tr) 

1043 

1044 for batch in pile.chopper( 

1045 tmin=marker.tmin, 

1046 tmax=marker.tmax, 

1047 trace_selector=trace_selector, 

1048 style='batch', 

1049 *args, 

1050 **kwargs): 

1051 

1052 if progress: 

1053 abort = update_progress(label, batch) 

1054 if abort: 

1055 return 

1056 

1057 batch.traces = apply_filters(batch.traces) 

1058 if style_arg == 'batch': 

1059 yield batch 

1060 else: 

1061 yield batch.traces 

1062 

1063 finally: 

1064 if progress: 

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

1066 

1067 elif fallback: 

1068 def trace_selector(tr): 

1069 return trace_selector_arg(tr) \ 

1070 and trace_selector_viewer(tr) 

1071 

1072 tmin, tmax = viewer.get_time_range() 

1073 

1074 if not pile.is_empty(): 

1075 ptmin = pile.get_tmin() 

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

1077 if ptmin > tmin: 

1078 tmin = ptmin + tpad 

1079 ptmax = pile.get_tmax() 

1080 if ptmax < tmax: 

1081 tmax = ptmax - tpad 

1082 

1083 try: 

1084 if progress: 

1085 label = progress 

1086 pb.set_status(label, 0, responsive) 

1087 

1088 for batch in pile.chopper( 

1089 tmin=tmin, 

1090 tmax=tmax, 

1091 trace_selector=trace_selector, 

1092 style='batch', 

1093 *args, 

1094 **kwargs): 

1095 

1096 if progress: 

1097 abort = update_progress(label, batch) 

1098 

1099 if abort: 

1100 return 

1101 

1102 batch.traces = apply_filters(batch.traces) 

1103 

1104 if style_arg == 'batch': 

1105 yield batch 

1106 else: 

1107 yield batch.traces 

1108 

1109 finally: 

1110 if progress: 

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

1112 

1113 else: 

1114 raise NoTracesSelected() 

1115 

1116 except NoViewerSet: 

1117 pile = self.get_pile() 

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

1119 

1120 def get_selected_time_range(self, fallback=False): 

1121 ''' 

1122 Get the time range spanning all selected markers. 

1123 

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

1125 end of visible time range 

1126 ''' 

1127 

1128 viewer = self.get_viewer() 

1129 markers = viewer.selected_markers() 

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

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

1132 

1133 if mins and maxs: 

1134 tmin = min(mins) 

1135 tmax = max(maxs) 

1136 

1137 elif fallback: 

1138 tmin, tmax = viewer.get_time_range() 

1139 

1140 else: 

1141 raise NoTracesSelected() 

1142 

1143 return tmin, tmax 

1144 

1145 def panel_visibility_changed(self, bool): 

1146 ''' 

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

1148 

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

1150 when the panel is activated the first time. 

1151 ''' 

1152 

1153 pass 

1154 

1155 def make_pile(self): 

1156 ''' 

1157 Create a pile. 

1158 

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

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

1161 arguments. 

1162 ''' 

1163 

1164 cachedirname = config.config().cache_dir 

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

1166 return pile.make_pile( 

1167 sources, 

1168 cachedirname=cachedirname, 

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

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

1171 

1172 def make_panel(self, parent): 

1173 ''' 

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

1175 

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

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

1178 parameters). 

1179 ''' 

1180 

1181 params = self.get_parameters() 

1182 self._param_controls = {} 

1183 if params or self._force_panel: 

1184 sarea = MyScrollArea(parent.get_panel_parent_widget()) 

1185 sarea.setFrameStyle(qw.QFrame.NoFrame) 

1186 sarea.setSizePolicy(qw.QSizePolicy( 

1187 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)) 

1188 frame = MyFrame(sarea) 

1189 frame.widgetVisibilityChanged.connect( 

1190 self.panel_visibility_changed) 

1191 

1192 frame.setSizePolicy(qw.QSizePolicy( 

1193 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1194 frame.setFrameStyle(qw.QFrame.NoFrame) 

1195 sarea.setWidget(frame) 

1196 sarea.setWidgetResizable(True) 

1197 layout = qw.QGridLayout() 

1198 layout.setContentsMargins(0, 0, 0, 0) 

1199 layout.setSpacing(0) 

1200 frame.setLayout(layout) 

1201 

1202 parlayout = qw.QGridLayout() 

1203 

1204 irow = 0 

1205 ipar = 0 

1206 have_switches = False 

1207 have_params = False 

1208 for iparam, param in enumerate(params): 

1209 if isinstance(param, Param): 

1210 if param.minimum <= 0.0: 

1211 param_control = LinValControl( 

1212 high_is_none=param.high_is_none, 

1213 low_is_none=param.low_is_none) 

1214 else: 

1215 param_control = ValControl( 

1216 high_is_none=param.high_is_none, 

1217 low_is_none=param.low_is_none, 

1218 low_is_zero=param.low_is_zero) 

1219 

1220 param_control.setup( 

1221 param.name, 

1222 param.minimum, 

1223 param.maximum, 

1224 param.default, 

1225 iparam) 

1226 

1227 param_control.valchange.connect( 

1228 self.modified_snuffling_panel) 

1229 

1230 self._param_controls[param.ident] = param_control 

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

1232 parlayout.addWidget(w, ipar, iw) 

1233 

1234 ipar += 1 

1235 have_params = True 

1236 

1237 elif isinstance(param, Choice): 

1238 param_widget = ChoiceControl( 

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

1240 param_widget.choosen.connect( 

1241 self.choose_on_snuffling_panel) 

1242 

1243 self._param_controls[param.ident] = param_widget 

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

1245 ipar += 1 

1246 have_params = True 

1247 

1248 elif isinstance(param, Switch): 

1249 have_switches = True 

1250 

1251 if have_params: 

1252 parframe = qw.QFrame(sarea) 

1253 parframe.setSizePolicy(qw.QSizePolicy( 

1254 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1255 parframe.setLayout(parlayout) 

1256 layout.addWidget(parframe, irow, 0) 

1257 irow += 1 

1258 

1259 if have_switches: 

1260 swlayout = qw.QGridLayout() 

1261 isw = 0 

1262 for iparam, param in enumerate(params): 

1263 if isinstance(param, Switch): 

1264 param_widget = SwitchControl( 

1265 param.ident, param.default, param.name) 

1266 param_widget.sw_toggled.connect( 

1267 self.switch_on_snuffling_panel) 

1268 

1269 self._param_controls[param.ident] = param_widget 

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

1271 isw += 1 

1272 

1273 swframe = qw.QFrame(sarea) 

1274 swframe.setSizePolicy(qw.QSizePolicy( 

1275 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1276 swframe.setLayout(swlayout) 

1277 layout.addWidget(swframe, irow, 0) 

1278 irow += 1 

1279 

1280 butframe = qw.QFrame(sarea) 

1281 butframe.setSizePolicy(qw.QSizePolicy( 

1282 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum)) 

1283 butlayout = qw.QHBoxLayout() 

1284 butframe.setLayout(butlayout) 

1285 

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

1287 if self._live_update: 

1288 live_update_checkbox.setCheckState(qc.Qt.Checked) 

1289 

1290 butlayout.addWidget(live_update_checkbox) 

1291 live_update_checkbox.toggled.connect( 

1292 self.live_update_toggled) 

1293 

1294 help_button = qw.QPushButton('Help') 

1295 butlayout.addWidget(help_button) 

1296 help_button.clicked.connect( 

1297 self.help_button_triggered) 

1298 

1299 clear_button = qw.QPushButton('Clear') 

1300 butlayout.addWidget(clear_button) 

1301 clear_button.clicked.connect( 

1302 self.clear_button_triggered) 

1303 

1304 call_button = qw.QPushButton('Run') 

1305 butlayout.addWidget(call_button) 

1306 call_button.clicked.connect( 

1307 self.call_button_triggered) 

1308 

1309 for name, method in self._triggers: 

1310 but = qw.QPushButton(name) 

1311 

1312 def call_and_update(method): 

1313 def f(): 

1314 try: 

1315 method() 

1316 except SnufflingError as e: 

1317 if not isinstance(e, SnufflingCallFailed): 

1318 # those have logged within error() 

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

1320 logger.error( 

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

1322 

1323 self.get_viewer().update() 

1324 return f 

1325 

1326 but.clicked.connect( 

1327 call_and_update(method)) 

1328 

1329 butlayout.addWidget(but) 

1330 

1331 layout.addWidget(butframe, irow, 0) 

1332 

1333 irow += 1 

1334 spacer = qw.QSpacerItem( 

1335 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding) 

1336 

1337 layout.addItem(spacer, irow, 0) 

1338 

1339 return sarea 

1340 

1341 else: 

1342 return None 

1343 

1344 def make_helpmenuitem(self, parent): 

1345 ''' 

1346 Create the help menu item for the snuffling. 

1347 ''' 

1348 

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

1350 

1351 item.triggered.connect( 

1352 self.help_button_triggered) 

1353 

1354 return item 

1355 

1356 def make_menuitem(self, parent): 

1357 ''' 

1358 Create the menu item for the snuffling. 

1359 

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

1361 menu entry is wanted. 

1362 ''' 

1363 

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

1365 item.setCheckable( 

1366 self._have_pre_process_hook or self._have_post_process_hook) 

1367 

1368 item.triggered.connect( 

1369 self.menuitem_triggered) 

1370 

1371 return item 

1372 

1373 def output_filename( 

1374 self, 

1375 caption='Save File', 

1376 dir='', 

1377 filter='', 

1378 selected_filter=None): 

1379 

1380 ''' 

1381 Query user for an output filename. 

1382 

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

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

1385 dialog. 

1386 ''' 

1387 

1388 if not dir and self._previous_output_filename: 

1389 dir = self._previous_output_filename 

1390 

1391 fn = getSaveFileName( 

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

1393 if not fn: 

1394 raise UserCancelled() 

1395 

1396 self._previous_output_filename = fn 

1397 return str(fn) 

1398 

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

1400 ''' 

1401 Query user for an input directory. 

1402 

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

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

1405 dialog. 

1406 ''' 

1407 

1408 if not dir and self._previous_input_directory: 

1409 dir = self._previous_input_directory 

1410 

1411 dn = qw.QFileDialog.getExistingDirectory( 

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

1413 

1414 if not dn: 

1415 raise UserCancelled() 

1416 

1417 self._previous_input_directory = dn 

1418 return str(dn) 

1419 

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

1421 selected_filter=None): 

1422 ''' 

1423 Query user for an input filename. 

1424 

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

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

1427 dialog. 

1428 ''' 

1429 

1430 if not dir and self._previous_input_filename: 

1431 dir = self._previous_input_filename 

1432 

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

1434 self.get_viewer(), 

1435 caption, 

1436 dir, 

1437 filter)) # selected_filter) 

1438 

1439 if not fn: 

1440 raise UserCancelled() 

1441 

1442 self._previous_input_filename = fn 

1443 return str(fn) 

1444 

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

1446 ''' 

1447 Query user for a text input. 

1448 

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

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

1451 dialog. 

1452 ''' 

1453 

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

1455 

1456 if not ok: 

1457 raise UserCancelled() 

1458 

1459 return inp 

1460 

1461 def modified_snuffling_panel(self, value, iparam): 

1462 ''' 

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

1464 

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

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

1467 widget. 

1468 ''' 

1469 

1470 param = self.get_parameters()[iparam] 

1471 self._set_parameter_value(param.ident, value) 

1472 if self._live_update: 

1473 self.check_call() 

1474 self.get_viewer().update() 

1475 

1476 def switch_on_snuffling_panel(self, ident, state): 

1477 ''' 

1478 Called when the user has toggled a switchable parameter. 

1479 ''' 

1480 

1481 self._set_parameter_value(ident, state) 

1482 if self._live_update: 

1483 self.check_call() 

1484 self.get_viewer().update() 

1485 

1486 def choose_on_snuffling_panel(self, ident, state): 

1487 ''' 

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

1489 ''' 

1490 

1491 self._set_parameter_value(ident, state) 

1492 if self._live_update: 

1493 self.check_call() 

1494 self.get_viewer().update() 

1495 

1496 def menuitem_triggered(self, arg): 

1497 ''' 

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

1499 

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

1501 and triggers an update on the viewer widget. 

1502 ''' 

1503 

1504 self.check_call() 

1505 

1506 if self._have_pre_process_hook: 

1507 self._pre_process_hook_enabled = arg 

1508 

1509 if self._have_post_process_hook: 

1510 self._post_process_hook_enabled = arg 

1511 

1512 if self._have_pre_process_hook or self._have_post_process_hook: 

1513 self.get_viewer().clean_update() 

1514 else: 

1515 self.get_viewer().update() 

1516 

1517 def call_button_triggered(self): 

1518 ''' 

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

1520 

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

1522 and triggers an update on the viewer widget. 

1523 ''' 

1524 

1525 self.check_call() 

1526 self.get_viewer().update() 

1527 

1528 def clear_button_triggered(self): 

1529 ''' 

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

1531 

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

1533 viewer widget. 

1534 ''' 

1535 

1536 self.cleanup() 

1537 self.get_viewer().update() 

1538 

1539 def help_button_triggered(self): 

1540 ''' 

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

1542 given in the snufflings' __doc__ string. 

1543 ''' 

1544 

1545 if self.__doc__: 

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

1547 doc = qw.QLabel(self.__doc__) 

1548 else: 

1549 try: 

1550 import markdown 

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

1552 

1553 except ImportError: 

1554 logger.error( 

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

1556 'formatting.') 

1557 

1558 doc = qw.QLabel(self.__doc__) 

1559 else: 

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

1561 

1562 labels = [doc] 

1563 

1564 if self._filename: 

1565 from html import escape 

1566 

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

1568 

1569 doc_src = qw.QLabel( 

1570 '''<html><body> 

1571<hr /> 

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

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

1574<br /> 

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

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

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

1578 % ( 

1579 quote(self._filename), 

1580 escape(self._filename), 

1581 escape(code))) 

1582 

1583 labels.append(doc_src) 

1584 

1585 for h in labels: 

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

1587 h.setWordWrap(True) 

1588 

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

1590 

1591 def live_update_toggled(self, on): 

1592 ''' 

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

1594 ''' 

1595 

1596 self.set_live_update(on) 

1597 

1598 def add_traces(self, traces): 

1599 ''' 

1600 Add traces to the viewer. 

1601 

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

1603 

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

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

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

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

1608 traces are added. 

1609 

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

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

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

1613 method). 

1614 ''' 

1615 

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

1617 self._tickets.append(ticket) 

1618 return ticket 

1619 

1620 def add_trace(self, tr): 

1621 ''' 

1622 Add a trace to the viewer. 

1623 

1624 See :py:meth:`add_traces`. 

1625 ''' 

1626 

1627 self.add_traces([tr]) 

1628 

1629 def add_markers(self, markers): 

1630 ''' 

1631 Add some markers to the display. 

1632 

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

1634 adds these to the viewer. 

1635 ''' 

1636 

1637 self.get_viewer().add_markers(markers) 

1638 self._markers.extend(markers) 

1639 

1640 def add_marker(self, marker): 

1641 ''' 

1642 Add a marker to the display. 

1643 

1644 See :py:meth:`add_markers`. 

1645 ''' 

1646 

1647 self.add_markers([marker]) 

1648 

1649 def cleanup(self): 

1650 ''' 

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

1652 snuffling. 

1653 ''' 

1654 

1655 try: 

1656 viewer = self.get_viewer() 

1657 viewer.release_data(self._tickets) 

1658 viewer.remove_markers(self._markers) 

1659 

1660 except NoViewerSet: 

1661 pass 

1662 

1663 self._tickets = [] 

1664 self._markers = [] 

1665 

1666 def check_call(self): 

1667 

1668 if self._call_in_progress: 

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

1670 return 

1671 

1672 try: 

1673 self._call_in_progress = True 

1674 self.call() 

1675 return 0 

1676 

1677 except SnufflingError as e: 

1678 if not isinstance(e, SnufflingCallFailed): 

1679 # those have logged within error() 

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

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

1682 return 1 

1683 

1684 except Exception as e: 

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

1686 self._name, str(e)) 

1687 

1688 logger.exception(message) 

1689 self.show_message('error', message) 

1690 

1691 finally: 

1692 self._call_in_progress = False 

1693 

1694 def call(self): 

1695 ''' 

1696 Main work routine of the snuffling. 

1697 

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

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

1700 in subclass. The default implementation does nothing useful. 

1701 ''' 

1702 

1703 pass 

1704 

1705 def pre_process_hook(self, traces): 

1706 return traces 

1707 

1708 def post_process_hook(self, traces): 

1709 return traces 

1710 

1711 def get_tpad(self): 

1712 ''' 

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

1714 ''' 

1715 

1716 return 0.0 

1717 

1718 def pre_destroy(self): 

1719 ''' 

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

1721 

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

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

1724 the snuffling`s tempory directory, if needed. 

1725 ''' 

1726 

1727 self.cleanup() 

1728 if self._tempdir is not None: 

1729 import shutil 

1730 shutil.rmtree(self._tempdir) 

1731 

1732 

1733class SnufflingError(Exception): 

1734 pass 

1735 

1736 

1737class NoViewerSet(SnufflingError): 

1738 ''' 

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

1740 ''' 

1741 

1742 def __str__(self): 

1743 return 'No GUI available. ' \ 

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

1745 

1746 

1747class MissingStationInformation(SnufflingError): 

1748 ''' 

1749 Raised when station information is missing. 

1750 ''' 

1751 

1752 

1753class NoTracesSelected(SnufflingError): 

1754 ''' 

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

1756 and we cannot fallback to using the current view. 

1757 ''' 

1758 

1759 def __str__(self): 

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

1761 

1762 

1763class UserCancelled(SnufflingError): 

1764 ''' 

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

1766 ''' 

1767 

1768 def __str__(self): 

1769 return 'The user has cancelled a dialog.' 

1770 

1771 

1772class SnufflingCallFailed(SnufflingError): 

1773 ''' 

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

1775 :py:meth:`Snuffling.call`. 

1776 ''' 

1777 

1778 

1779class InvalidSnufflingFilename(Exception): 

1780 pass 

1781 

1782 

1783class SnufflingModule(object): 

1784 ''' 

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

1786 

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

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

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

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

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

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

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

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

1795 when needed. 

1796 ''' 

1797 

1798 mtimes = {} 

1799 

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

1801 self._path = path 

1802 self._name = name 

1803 self._mtime = None 

1804 self._module = None 

1805 self._snufflings = [] 

1806 self._handler = handler 

1807 

1808 def load_if_needed(self): 

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

1810 

1811 try: 

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

1813 except OSError as e: 

1814 if e.errno == 2: 

1815 logger.error(e) 

1816 raise BrokenSnufflingModule(filename) 

1817 

1818 if self._module is None: 

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

1820 try: 

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

1822 if self._name in sys.modules: 

1823 raise InvalidSnufflingFilename(self._name) 

1824 

1825 self._module = __import__(self._name) 

1826 del sys.modules[self._name] 

1827 

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

1829 snuffling._filename = filename 

1830 self.add_snuffling(snuffling) 

1831 

1832 except Exception: 

1833 logger.error(traceback.format_exc()) 

1834 raise BrokenSnufflingModule(filename) 

1835 

1836 finally: 

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

1838 

1839 elif self._mtime != mtime: 

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

1841 settings = self.remove_snufflings() 

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

1843 try: 

1844 

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

1846 

1847 reload(self._module) 

1848 del sys.modules[self._name] 

1849 

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

1851 snuffling._filename = filename 

1852 self.add_snuffling(snuffling, reloaded=True) 

1853 

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

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

1856 snuf.set_settings(sett) 

1857 

1858 except Exception: 

1859 logger.error(traceback.format_exc()) 

1860 raise BrokenSnufflingModule(filename) 

1861 

1862 finally: 

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

1864 

1865 self._mtime = mtime 

1866 

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

1868 snuffling._path = self._path 

1869 snuffling.setup() 

1870 self._snufflings.append(snuffling) 

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

1872 

1873 def remove_snufflings(self): 

1874 settings = [] 

1875 for snuffling in self._snufflings: 

1876 settings.append(snuffling.get_settings()) 

1877 self._handler.remove_snuffling(snuffling) 

1878 

1879 self._snufflings = [] 

1880 return settings 

1881 

1882 

1883class BrokenSnufflingModule(Exception): 

1884 pass 

1885 

1886 

1887class MyScrollArea(qw.QScrollArea): 

1888 

1889 def sizeHint(self): 

1890 s = qc.QSize() 

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

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

1893 return s 

1894 

1895 

1896class SwitchControl(qw.QCheckBox): 

1897 sw_toggled = qc.pyqtSignal(object, bool) 

1898 

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

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

1901 self.ident = ident 

1902 self.setChecked(default) 

1903 self.toggled.connect(self._sw_toggled) 

1904 

1905 def _sw_toggled(self, state): 

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

1907 

1908 def set_value(self, state): 

1909 self.blockSignals(True) 

1910 self.setChecked(state) 

1911 self.blockSignals(False) 

1912 

1913 

1914class ChoiceControl(qw.QFrame): 

1915 choosen = qc.pyqtSignal(object, object) 

1916 

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

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

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

1920 self.label.setMinimumWidth(120) 

1921 self.cbox = qw.QComboBox(self) 

1922 self.layout = qw.QHBoxLayout(self) 

1923 self.layout.addWidget(self.label) 

1924 self.layout.addWidget(self.cbox) 

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

1926 self.layout.setSpacing(0) 

1927 self.ident = ident 

1928 self.choices = choices 

1929 for ichoice, choice in enumerate(choices): 

1930 self.cbox.addItem(choice) 

1931 

1932 self.set_value(default) 

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

1934 

1935 def set_choices(self, choices): 

1936 icur = self.cbox.currentIndex() 

1937 if icur != -1: 

1938 selected_choice = choices[icur] 

1939 else: 

1940 selected_choice = None 

1941 

1942 self.choices = choices 

1943 self.cbox.clear() 

1944 for ichoice, choice in enumerate(choices): 

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

1946 

1947 if selected_choice is not None and selected_choice in choices: 

1948 self.set_value(selected_choice) 

1949 return selected_choice 

1950 else: 

1951 self.set_value(choices[0]) 

1952 return choices[0] 

1953 

1954 def emit_choosen(self, i): 

1955 self.choosen.emit( 

1956 self.ident, 

1957 self.choices[i]) 

1958 

1959 def set_value(self, v): 

1960 self.cbox.blockSignals(True) 

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

1962 if choice == v: 

1963 self.cbox.setCurrentIndex(i) 

1964 self.cbox.blockSignals(False)