Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffling.py: 45%
849 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-03-07 11:54 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Snuffling infrastructure
9This module provides the base class :py:class:`Snuffling` for user-defined
10snufflings and some utilities for their handling.
11'''
13import os
14import sys
15import time
16import logging
17import traceback
18import tempfile
20from ..qt_compat import qc, qw, getSaveFileName
22from pyrocko import pile, config
23from pyrocko.util import quote
25from ..util import (
26 ValControl, LinValControl, FigureFrame, SmartplotFrame, WebKitFrame,
27 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker, load_markers,
28 save_markers)
30from importlib import reload
32Marker, load_markers, save_markers # noqa
34logger = logging.getLogger('pyrocko.gui.snuffler.snuffling')
37class MyFrame(qw.QFrame):
38 widgetVisibilityChanged = qc.pyqtSignal(bool)
40 def showEvent(self, ev):
41 ''
42 self.widgetVisibilityChanged.emit(True)
44 def hideEvent(self, ev):
45 ''
46 self.widgetVisibilityChanged.emit(False)
49class Param(object):
50 '''
51 Definition of an adjustable floating point parameter for the
52 snuffling. The snuffling may display controls for user input for
53 such parameters.
55 :param name: labels the parameter on the snuffling's control panel
56 :param ident: identifier of the parameter
57 :param default: default value
58 :param minimum: minimum value for the parameter
59 :param maximum: maximum value for the parameter
60 :param low_is_none: if ``True``: parameter is set to None at lowest value
61 of parameter range (optional)
62 :param high_is_none: if ``True``: parameter is set to None at highest value
63 of parameter range (optional)
64 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
65 value of parameter range (optional)
66 '''
68 def __init__(
69 self, name, ident, default, minimum, maximum,
70 low_is_none=None,
71 high_is_none=None,
72 low_is_zero=False,
73 tracking=True,
74 type=float):
76 if low_is_none and default == minimum:
77 default = None
78 if high_is_none and default == maximum:
79 default = None
81 self.name = name
82 self.ident = ident
83 self.default = default
84 self.minimum = minimum
85 self.maximum = maximum
86 self.low_is_none = low_is_none
87 self.high_is_none = high_is_none
88 self.low_is_zero = low_is_zero
89 self.tracking = tracking
90 self.type = type
92 self._control = None
95class Switch(object):
96 '''
97 Definition of a boolean switch for the snuffling. The snuffling
98 may display a checkbox for such a switch.
100 :param name: labels the switch on the snuffling's control panel
101 :param ident: identifier of the parameter
102 :param default: default value
103 '''
105 def __init__(self, name, ident, default):
106 self.name = name
107 self.ident = ident
108 self.default = default
111class Choice(object):
112 '''
113 Definition of a string choice for the snuffling. The snuffling
114 may display a menu for such a choice.
116 :param name: labels the menu on the snuffling's control panel
117 :param ident: identifier of the parameter
118 :param default: default value
119 :param choices: tuple of other options
120 '''
122 def __init__(self, name, ident, default, choices):
123 self.name = name
124 self.ident = ident
125 self.default = default
126 self.choices = choices
129class Snuffling(object):
130 '''
131 Base class for user snufflings.
133 Snufflings are plugins for snuffler (and other applications using the
134 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` class defined in
135 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
136 should provide a simple way of extending the functionality of snuffler.
138 A snuffling has access to all data available in a pile viewer, can process
139 this data and can create and add new traces and markers to the viewer.
140 '''
142 def __init__(self):
143 self._path = None
145 self._name = 'Untitled Snuffling'
146 self._viewer = None
147 self._tickets = []
148 self._markers = []
150 self._delete_panel = None
151 self._delete_menuitem = None
153 self._panel_parent = None
154 self._menu_parent = None
156 self._panel = None
157 self._menuitem = None
158 self._helpmenuitem = None
159 self._parameters = []
160 self._param_controls = {}
162 self._triggers = []
164 self._live_update = True
165 self._previous_output_filename = None
166 self._previous_input_filename = None
167 self._previous_input_directory = None
169 self._tempdir = None
170 self._iplot = 0
172 self._have_pre_process_hook = False
173 self._have_post_process_hook = False
174 self._pre_process_hook_enabled = False
175 self._post_process_hook_enabled = False
177 self._no_viewer_pile = None
178 self._cli_params = {}
179 self._filename = None
180 self._force_panel = False
181 self._call_in_progress = {}
183 def setup(self):
184 '''
185 Setup the snuffling.
187 This method should be implemented in subclass and contain e.g. calls to
188 :py:meth:`set_name` and :py:meth:`add_parameter`.
189 '''
191 pass
193 def module_dir(self):
194 '''
195 Returns the path of the directory where snufflings are stored.
197 The default path is ``$HOME/.snufflings``.
198 '''
200 return self._path
202 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
203 '''
204 Set parent viewer and hooks to add panel and menu entry.
206 This method is called from the
207 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` object. Calls
208 :py:meth:`setup_gui`.
209 '''
211 self._viewer = viewer
212 self._panel_parent = panel_parent
213 self._menu_parent = menu_parent
215 self.setup_gui(reloaded=reloaded)
217 def setup_gui(self, reloaded=False):
218 '''
219 Create and add gui elements to the viewer.
221 This method is initially called from :py:meth:`init_gui`. It is also
222 called, e.g. when new parameters have been added or if the name of the
223 snuffling has been changed.
224 '''
226 if self._panel_parent is not None:
227 self._panel = self.make_panel(self._panel_parent)
228 if self._panel:
229 self._panel_parent.add_panel(
230 self.get_name(), self._panel, reloaded)
232 if self._menu_parent is not None:
233 self._menuitem = self.make_menuitem(self._menu_parent)
234 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
235 if self._menuitem:
236 self._menu_parent.add_snuffling_menuitem(self._menuitem)
238 if self._helpmenuitem:
239 self._menu_parent.add_snuffling_help_menuitem(
240 self._helpmenuitem)
242 def set_force_panel(self, bool=True):
243 '''
244 Force to create a panel.
246 :param bool: if ``True`` will create a panel with Help, Clear and Run
247 button.
248 '''
250 self._force_panel = bool
252 def make_cli_parser1(self):
253 import optparse
255 class MyOptionParser(optparse.OptionParser):
256 def error(self, msg):
257 logger.error(msg)
258 self.exit(1)
260 parser = MyOptionParser()
262 parser.add_option(
263 '--format',
264 dest='format',
265 default='from_extension',
266 choices=(
267 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
268 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
269 'from_extension', 'detect'),
270 help="assume files are of given FORMAT [default: '%default']")
272 parser.add_option(
273 '--pattern',
274 dest='regex',
275 metavar='REGEX',
276 help='only include files whose paths match REGEX')
278 self.add_params_to_cli_parser(parser)
279 self.configure_cli_parser(parser)
280 return parser
282 def configure_cli_parser(self, parser):
283 pass
285 def cli_usage(self):
286 return None
288 def add_params_to_cli_parser(self, parser):
290 for param in self._parameters:
291 if isinstance(param, Param):
292 parser.add_option(
293 '--' + param.ident,
294 dest=param.ident,
295 default=param.default,
296 type={float: 'float', int: 'int'}[param.type],
297 help=param.name)
299 def setup_cli(self):
300 self.setup()
301 parser = self.make_cli_parser1()
302 (options, args) = parser.parse_args()
304 for param in self._parameters:
305 if isinstance(param, Param):
306 setattr(self, param.ident, getattr(options, param.ident))
308 self._cli_params['regex'] = options.regex
309 self._cli_params['format'] = options.format
310 self._cli_params['sources'] = args
312 return options, args, parser
314 def delete_gui(self):
315 '''
316 Remove the gui elements of the snuffling.
318 This removes the panel and menu entry of the widget from the viewer and
319 also removes all traces and markers added with the
320 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
321 '''
323 self.cleanup()
325 if self._panel is not None:
326 self._panel_parent.remove_panel(self._panel)
327 self._panel = None
329 if self._menuitem is not None:
330 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
331 self._menuitem = None
333 if self._helpmenuitem is not None:
334 self._menu_parent.remove_snuffling_help_menuitem(
335 self._helpmenuitem)
337 def set_name(self, name):
338 '''
339 Set the snuffling's name.
341 The snuffling's name is shown as a menu entry and in the panel header.
342 '''
344 self._name = name
345 self.reset_gui()
347 def get_name(self):
348 '''
349 Get the snuffling's name.
350 '''
352 return self._name
354 def set_have_pre_process_hook(self, bool):
355 self._have_pre_process_hook = bool
356 self._live_update = False
357 self._pre_process_hook_enabled = False
358 self.reset_gui()
360 def set_have_post_process_hook(self, bool):
361 self._have_post_process_hook = bool
362 self._live_update = False
363 self._post_process_hook_enabled = False
364 self.reset_gui()
366 def set_have_pile_changed_hook(self, bool):
367 self._pile_ = False
369 def enable_pile_changed_notifications(self):
370 '''
371 Get informed when pile changed.
373 When activated, the :py:meth:`pile_changed` method is called on every
374 update in the viewer's pile.
375 '''
377 viewer = self.get_viewer()
378 viewer.pile_has_changed_signal.connect(
379 self.pile_changed)
381 def disable_pile_changed_notifications(self):
382 '''
383 Stop getting informed about changes in viewer's pile.
384 '''
386 viewer = self.get_viewer()
387 viewer.pile_has_changed_signal.disconnect(
388 self.pile_changed)
390 def pile_changed(self):
391 '''
392 Called when the connected viewer's pile has changed.
394 Must be activated with a call to
395 :py:meth:`enable_pile_changed_notifications`.
396 '''
398 pass
400 def reset_gui(self, reloaded=False):
401 '''
402 Delete and recreate the snuffling's panel.
403 '''
405 if self._panel or self._menuitem:
406 sett = self.get_settings()
407 self.delete_gui()
408 self.setup_gui(reloaded=reloaded)
409 self.set_settings(sett)
411 def show_message(self, kind, message):
412 '''
413 Display a message box.
415 :param kind: string defining kind of message
416 :param message: the message to be displayed
417 '''
419 try:
420 box = qw.QMessageBox(self.get_viewer())
421 box.setText('%s: %s' % (kind.capitalize(), message))
422 box.exec_()
423 except NoViewerSet:
424 pass
426 def error(self, message):
427 '''
428 Show an error message box.
430 :param message: specifying the error
431 '''
433 logger.error('%s: %s' % (self._name, message))
434 self.show_message('error', message)
436 def warn(self, message):
437 '''
438 Display a warning message.
440 :param message: specifying the warning
441 '''
443 logger.warning('%s: %s' % (self._name, message))
444 self.show_message('warning', message)
446 def fail(self, message, action='error'):
447 '''
448 Show an error message box and raise :py:exc:`SnufflingCallFailed`
449 exception.
451 :param message: specifying the error
452 '''
454 if action == 'error':
455 self.error(message)
456 elif action == 'warn':
457 self.warn(message)
458 elif action == 'log':
459 logger.error('%s: %s' % (self._name, message))
460 elif action == 'status':
461 logger.warn('%s: %s' % (self._name, message))
462 viewer = self.get_viewer().window()
463 if viewer:
464 viewer.window().status_messages.set(
465 'snuffling', message)
467 raise SnufflingCallFailed(message)
469 def pylab(self, name=None, get='axes', figure_cls=None):
470 '''
471 Create a :py:class:`pyrocko.gui.util.FigureFrame` and return either the
472 frame, a :py:class:`matplotlib.figure.Figure` instance or a
473 :py:class:`matplotlib.axes.Axes` instance.
475 :param name: labels the figure frame's tab
476 :param get: 'axes'|'figure'|'frame' (optional)
477 '''
479 if name is None:
480 self._iplot += 1
481 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
483 fframe = FigureFrame(figure_cls=figure_cls)
484 self._panel_parent.add_tab(name, fframe)
485 if get == 'axes':
486 return fframe.gca()
487 elif get == 'figure':
488 return fframe.gcf()
489 elif get == 'figure_frame':
490 return fframe
492 def figure(self, name=None):
493 '''
494 Returns a :py:class:`matplotlib.figure.Figure` instance.
496 Force drawing of the figure by calling `fig.canvas.draw()` on the
497 returned object ``fig``.
499 :param name: labels the tab of the figure
500 '''
502 return self.pylab(name=name, get='figure')
504 def axes(self, name=None):
505 '''
506 Returns a :py:class:`matplotlib.axes.Axes` instance.
508 :param name: labels the tab of axes
509 '''
511 return self.pylab(name=name, get='axes')
513 def figure_frame(self, name=None, figure_cls=None):
514 '''
515 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
517 :param name: labels the tab figure frame
518 '''
520 return self.pylab(name=name, get='figure_frame', figure_cls=figure_cls)
522 def smartplot_frame(self, name, *args, plot_cls=None, **kwargs):
523 '''
524 Create a :py:class:`pyrocko.gui.util.SmartplotFrame`.
526 :param name: labels the tab
527 :param \\*args:
528 passed to :py:class:`pyrocko.plot.smartplot.Plot`
529 :param \\*kwargs:
530 passed to :py:class:`pyrocko.plot.smartplot.Plot`
531 :param plot_cls:
532 if given, subclass to be used instead of
533 :py:class:`pyrocko.plot.smartplot.Plot`
534 '''
535 frame = SmartplotFrame(
536 plot_args=args,
537 plot_cls=plot_cls,
538 plot_kwargs=kwargs)
540 self._panel_parent.add_tab(name, frame)
541 return frame
543 def pixmap_frame(self, filename=None, name=None):
544 '''
545 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
547 :param name: labels the tab
548 :param filename: name of file to be displayed
549 '''
551 f = PixmapFrame(filename)
553 scroll_area = qw.QScrollArea()
554 scroll_area.setWidget(f)
555 scroll_area.setWidgetResizable(True)
557 self._panel_parent.add_tab(name or 'Pixmap', scroll_area)
558 return f
560 def web_frame(self, url=None, name=None):
561 '''
562 Creates a :py:class:`~pyrocko.gui.util.WebKitFrame` which can be
563 used as a browser within Snuffler.
565 :param url: url to open
566 :param name: labels the tab
567 '''
569 if name is None:
570 self._iplot += 1
571 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
573 f = WebKitFrame(url)
574 self._panel_parent.add_tab(name, f)
575 return f
577 def vtk_frame(self, name=None, actors=None):
578 '''
579 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
580 graphics.
582 :param actors: list of VTKActors
583 :param name: labels the tab
585 Initialize the interactive rendering by calling the frames'
586 :py:meth`initialize` method after having added all actors to the frames
587 renderer.
589 Requires installation of vtk including python wrapper.
590 '''
591 if name is None:
592 self._iplot += 1
593 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
595 try:
596 f = VTKFrame(actors=actors)
597 except ImportError as e:
598 self.fail(e)
600 self._panel_parent.add_tab(name, f)
601 return f
603 def tempdir(self):
604 '''
605 Create a temporary directory and return its absolute path.
607 The directory and all its contents are removed when the Snuffling
608 instance is deleted.
609 '''
611 if self._tempdir is None:
612 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
614 return self._tempdir
616 def set_live_update(self, live_update):
617 '''
618 Enable/disable live updating.
620 When live updates are enabled, the :py:meth:`call` method is called
621 whenever the user changes a parameter. If it is disabled, the user has
622 to initiate such a call manually by triggering the snuffling's menu
623 item or pressing the call button.
624 '''
626 self._live_update = live_update
627 if self._have_pre_process_hook:
628 self._pre_process_hook_enabled = live_update
629 if self._have_post_process_hook:
630 self._post_process_hook_enabled = live_update
632 try:
633 self.get_viewer().clean_update()
634 except NoViewerSet:
635 pass
637 def add_parameter(self, param):
638 '''
639 Add an adjustable parameter to the snuffling.
641 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
642 :py:class:`Choice`.
644 For each parameter added, controls are added to the snuffling's panel,
645 so that the parameter can be adjusted from the gui.
646 '''
648 self._parameters.append(param)
649 self._set_parameter_value(param.ident, param.default)
651 if self._panel is not None:
652 self.delete_gui()
653 self.setup_gui()
655 def add_trigger(self, name, method):
656 '''
657 Add a button to the snuffling's panel.
659 :param name: string that labels the button
660 :param method: method associated with the button
661 '''
663 self._triggers.append((name, method))
665 if self._panel is not None:
666 self.delete_gui()
667 self.setup_gui()
669 def get_parameters(self):
670 '''
671 Get the snuffling's adjustable parameter definitions.
673 Returns a list of objects of type :py:class:`Param`.
674 '''
676 return self._parameters
678 def get_parameter(self, ident):
679 '''
680 Get one of the snuffling's adjustable parameter definitions.
682 :param ident: identifier of the parameter
684 Returns an object of type :py:class:`Param` or ``None``.
685 '''
687 for param in self._parameters:
688 if param.ident == ident:
689 return param
690 return None
692 def set_parameter(self, ident, value):
693 '''
694 Set one of the snuffling's adjustable parameters.
696 :param ident: identifier of the parameter
697 :param value: new value of the parameter
699 Adjusts the control of a parameter without calling :py:meth:`call`.
700 '''
702 self._set_parameter_value(ident, value)
704 control = self._param_controls.get(ident, None)
705 if control:
706 control.set_value(value)
708 def set_parameter_range(self, ident, vmin, vmax):
709 '''
710 Set the range of one of the snuffling's adjustable parameters.
712 :param ident: identifier of the parameter
713 :param vmin,vmax: new minimum and maximum value for the parameter
715 Adjusts the control of a parameter without calling :py:meth:`call`.
716 '''
718 control = self._param_controls.get(ident, None)
719 if control:
720 control.set_range(vmin, vmax)
722 def set_parameter_choices(self, ident, choices):
723 '''
724 Update the choices of a Choice parameter.
726 :param ident: identifier of the parameter
727 :param choices: list of strings
728 '''
730 control = self._param_controls.get(ident, None)
731 if control:
732 selected_choice = control.set_choices(choices)
733 self._set_parameter_value(ident, selected_choice)
735 def _set_parameter_value(self, ident, value):
736 setattr(self, ident, value)
738 def get_parameter_value(self, ident):
739 '''
740 Get the current value of a parameter.
742 :param ident: identifier of the parameter
743 '''
744 return getattr(self, ident)
746 def get_settings(self):
747 '''
748 Returns a dictionary with identifiers of all parameters as keys and
749 their values as the dictionaries values.
750 '''
752 params = self.get_parameters()
753 settings = {}
754 for param in params:
755 settings[param.ident] = self.get_parameter_value(param.ident)
757 return settings
759 def set_settings(self, settings):
760 params = self.get_parameters()
761 dparams = dict([(param.ident, param) for param in params])
762 for k, v in settings.items():
763 if k in dparams:
764 self._set_parameter_value(k, v)
765 if k in self._param_controls:
766 control = self._param_controls[k]
767 control.set_value(v)
769 def get_viewer(self):
770 '''
771 Get the parent viewer.
773 Returns a reference to an object of type
774 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer`, which is the
775 main viewer widget.
777 If no gui has been initialized for the snuffling, a
778 :py:exc:`NoViewerSet` exception is raised.
779 '''
781 if self._viewer is None:
782 raise NoViewerSet()
783 return self._viewer
785 def get_pile(self):
786 '''
787 Get the pile.
789 If a gui has been initialized, a reference to the viewer's internal
790 pile is returned. If not, the :py:meth:`make_pile` method (which may be
791 overloaded in subclass) is called to create a pile. This can be
792 utilized to make hybrid snufflings, which may work also in a standalone
793 mode.
794 '''
796 try:
797 p = self.get_viewer().get_pile()
798 except NoViewerSet:
799 if self._no_viewer_pile is None:
800 self._no_viewer_pile = self.make_pile()
802 p = self._no_viewer_pile
804 return p
806 def get_active_event_and_stations(
807 self, trange=(-3600., 3600.), missing='warn'):
809 '''
810 Get event and stations with available data for active event.
812 :param trange: (begin, end), time range around event origin time to
813 query for available data
814 :param missing: string, what to do in case of missing station
815 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
817 :returns: ``(event, stations)``
818 '''
820 p = self.get_pile()
821 v = self.get_viewer()
823 event = v.get_active_event()
824 if event is None:
825 self.fail(
826 'No active event set. Select an event and press "e" to make '
827 'it the "active event"')
829 stations = {}
830 for traces in p.chopper(
831 event.time+trange[0],
832 event.time+trange[1],
833 load_data=False,
834 degap=False):
836 for tr in traces:
837 try:
838 for skey in v.station_keys(tr):
839 if skey in stations:
840 continue
842 station = v.get_station(skey)
843 stations[skey] = station
845 except KeyError:
846 s = 'No station information for station key "%s".' \
847 % '.'.join(skey)
849 if missing == 'warn':
850 logger.warning(s)
851 elif missing == 'raise':
852 raise MissingStationInformation(s)
853 elif missing == 'ignore':
854 pass
855 else:
856 assert False, 'invalid argument to "missing"'
858 stations[skey] = None
860 return event, list(set(
861 st for st in stations.values() if st is not None))
863 def get_stations(self):
864 '''
865 Get all stations known to the viewer.
866 '''
868 v = self.get_viewer()
869 stations = list(v.stations.values())
870 return stations
872 def get_markers(self):
873 '''
874 Get all markers from the viewer.
875 '''
877 return self.get_viewer().get_markers()
879 def get_event_markers(self):
880 '''
881 Get all event markers from the viewer.
882 '''
884 return [m for m in self.get_viewer().get_markers()
885 if isinstance(m, EventMarker)]
887 def get_selected_markers(self):
888 '''
889 Get all selected markers from the viewer.
890 '''
892 return self.get_viewer().selected_markers()
894 def get_selected_event_markers(self):
895 '''
896 Get all selected event markers from the viewer.
897 '''
899 return [m for m in self.get_viewer().selected_markers()
900 if isinstance(m, EventMarker)]
902 def get_active_event_and_phase_markers(self):
903 '''
904 Get the marker of the active event and any associated phase markers
905 '''
907 viewer = self.get_viewer()
908 markers = viewer.get_markers()
909 event_marker = viewer.get_active_event_marker()
910 if event_marker is None:
911 self.fail(
912 'No active event set. '
913 'Select an event and press "e" to make it the "active event"')
915 event = event_marker.get_event()
917 selection = []
918 for m in markers:
919 if isinstance(m, PhaseMarker):
920 if m.get_event() is event:
921 selection.append(m)
923 return (
924 event_marker,
925 [m for m in markers if isinstance(m, PhaseMarker) and
926 m.get_event() == event])
928 def get_viewer_trace_selector(self, mode='inview'):
929 '''
930 Get currently active trace selector from viewer.
932 :param mode: set to ``'inview'`` (default) to only include selections
933 currently shown in the viewer, ``'visible' to include all traces
934 not currenly hidden by hide or quick-select commands, or ``'all'``
935 to disable any restrictions.
936 '''
938 viewer = self.get_viewer()
940 def rtrue(tr):
941 return True
943 if mode == 'inview':
944 return viewer.trace_selector or rtrue
945 elif mode == 'visible':
946 return viewer.trace_filter or rtrue
947 elif mode == 'all':
948 return rtrue
949 else:
950 raise Exception('invalid mode argument')
952 def chopper_selected_traces(self, fallback=False, marker_selector=None,
953 mode='inview', main_bandpass=False,
954 progress=None, responsive=False,
955 *args, **kwargs):
956 '''
957 Iterate over selected traces.
959 Shortcut to get all trace data contained in the selected markers in the
960 running snuffler. For each selected marker,
961 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
962 *tmin*, *tmax*, and *trace_selector* set to values according to the
963 marker. Additional arguments to the chopper are handed over from
964 *\\*args* and *\\*\\*kwargs*.
966 :param fallback:
967 If ``True``, if no selection has been marked, use the content
968 currently visible in the viewer.
970 :param marker_selector:
971 If not ``None`` a callback to filter markers.
973 :param mode:
974 Set to ``'inview'`` (default) to only include selections currently
975 shown in the viewer (excluding traces accessible through vertical
976 scrolling), ``'visible'`` to include all traces not currently
977 hidden by hide or quick-select commands (including traces
978 accessible through vertical scrolling), or ``'all'`` to disable any
979 restrictions.
981 :param main_bandpass:
982 If ``True``, apply main control high- and lowpass filters to
983 traces. Note: use with caution. Processing is fixed to use 4th
984 order Butterworth highpass and lowpass and the signal is always
985 demeaned before filtering. FFT filtering, rotation, demean and
986 bandpass settings from the graphical interface are not respected
987 here. Padding is not automatically adjusted so results may include
988 artifacts.
990 :param progress:
991 If given a string a progress bar is shown to the user. The string
992 is used as the label for the progress bar.
994 :param responsive:
995 If set to ``True``, occasionally allow UI events to be processed.
996 If used in combination with ``progress``, this allows the iterator
997 to be aborted by the user.
998 '''
1000 try:
1001 viewer = self.get_viewer()
1002 markers = [
1003 m for m in viewer.selected_markers()
1004 if not isinstance(m, EventMarker)]
1006 if marker_selector is not None:
1007 markers = [
1008 marker for marker in markers if marker_selector(marker)]
1010 pile = self.get_pile()
1012 def rtrue(tr):
1013 return True
1015 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
1016 trace_selector_viewer = self.get_viewer_trace_selector(mode)
1018 style_arg = kwargs.pop('style', None)
1020 if main_bandpass:
1021 def apply_filters(traces):
1022 for tr in traces:
1023 if viewer.highpass is not None:
1024 tr.highpass(4, viewer.highpass)
1025 if viewer.lowpass is not None:
1026 tr.lowpass(4, viewer.lowpass)
1027 return traces
1028 else:
1029 def apply_filters(traces):
1030 return traces
1032 pb = viewer.parent().get_progressbars()
1034 time_last = [time.time()]
1036 def update_progress(label, batch):
1037 time_now = time.time()
1038 if responsive:
1039 # start processing events with one second delay, so that
1040 # e.g. cleanup actions at startup do not cause track number
1041 # changes etc.
1042 if time_last[0] + 1. < time_now:
1043 qw.qApp.processEvents()
1044 else:
1045 # redraw about once a second
1046 if time_last[0] + 1. < time_now:
1047 viewer.repaint()
1049 time_last[0] = time.time() # use time after drawing
1051 abort = pb.set_status(
1052 label, batch.i*100./batch.n, responsive)
1053 abort |= viewer.window().is_closing()
1055 return abort
1057 if markers:
1058 for imarker, marker in enumerate(markers):
1059 try:
1060 if progress:
1061 label = '%s: %i/%i' % (
1062 progress, imarker+1, len(markers))
1064 pb.set_status(label, 0, responsive)
1066 if not marker.nslc_ids:
1067 trace_selector_marker = rtrue
1068 else:
1069 def trace_selector_marker(tr):
1070 return marker.match_nslc(tr.nslc_id)
1072 def trace_selector(tr):
1073 return trace_selector_arg(tr) \
1074 and trace_selector_viewer(tr) \
1075 and trace_selector_marker(tr)
1077 for batch in pile.chopper(
1078 tmin=marker.tmin,
1079 tmax=marker.tmax,
1080 trace_selector=trace_selector,
1081 style='batch',
1082 *args,
1083 **kwargs):
1085 if progress:
1086 abort = update_progress(label, batch)
1087 if abort:
1088 return
1090 batch.traces = apply_filters(batch.traces)
1091 if style_arg == 'batch':
1092 yield batch
1093 else:
1094 yield batch.traces
1096 finally:
1097 if progress:
1098 pb.set_status(label, 100., responsive)
1100 elif fallback:
1101 def trace_selector(tr):
1102 return trace_selector_arg(tr) \
1103 and trace_selector_viewer(tr)
1105 tmin, tmax = viewer.get_time_range()
1107 if not pile.is_empty():
1108 ptmin = pile.get_tmin()
1109 tpad = kwargs.get('tpad', 0.0)
1110 if ptmin > tmin:
1111 tmin = ptmin + tpad
1112 ptmax = pile.get_tmax()
1113 if ptmax < tmax:
1114 tmax = ptmax - tpad
1116 try:
1117 if progress:
1118 label = progress
1119 pb.set_status(label, 0, responsive)
1121 for batch in pile.chopper(
1122 tmin=tmin,
1123 tmax=tmax,
1124 trace_selector=trace_selector,
1125 style='batch',
1126 *args,
1127 **kwargs):
1129 if progress:
1130 abort = update_progress(label, batch)
1132 if abort:
1133 return
1135 batch.traces = apply_filters(batch.traces)
1137 if style_arg == 'batch':
1138 yield batch
1139 else:
1140 yield batch.traces
1142 finally:
1143 if progress:
1144 pb.set_status(label, 100., responsive)
1146 else:
1147 raise NoTracesSelected()
1149 except NoViewerSet:
1150 pile = self.get_pile()
1151 return pile.chopper(*args, **kwargs)
1153 def get_selected_time_range(self, fallback=False):
1154 '''
1155 Get the time range spanning all selected markers.
1157 :param fallback: if ``True`` and no marker is selected return begin and
1158 end of visible time range
1159 '''
1161 viewer = self.get_viewer()
1162 markers = viewer.selected_markers()
1163 mins = [marker.tmin for marker in markers]
1164 maxs = [marker.tmax for marker in markers]
1166 if mins and maxs:
1167 tmin = min(mins)
1168 tmax = max(maxs)
1170 elif fallback:
1171 tmin, tmax = viewer.get_time_range()
1173 else:
1174 raise NoTracesSelected()
1176 return tmin, tmax
1178 def panel_visibility_changed(self, bool):
1179 '''
1180 Called when the snuffling's panel becomes visible or is hidden.
1182 Can be overloaded in subclass, e.g. to perform additional setup actions
1183 when the panel is activated the first time.
1184 '''
1186 pass
1188 def make_pile(self):
1189 '''
1190 Create a pile.
1192 To be overloaded in subclass. The default implementation just calls
1193 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1194 arguments.
1195 '''
1197 cachedirname = config.config().cache_dir
1198 sources = self._cli_params.get('sources', sys.argv[1:])
1199 return pile.make_pile(
1200 sources,
1201 cachedirname=cachedirname,
1202 regex=self._cli_params['regex'],
1203 fileformat=self._cli_params['format'])
1205 def make_panel(self, parent):
1206 '''
1207 Create a widget for the snuffling's control panel.
1209 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1210 if no panel is needed (e.g. if the snuffling has no adjustable
1211 parameters).
1212 '''
1214 params = self.get_parameters()
1215 self._param_controls = {}
1216 if params or self._force_panel:
1217 sarea = MyScrollArea(parent.get_panel_parent_widget())
1218 sarea.setFrameStyle(qw.QFrame.NoFrame)
1219 sarea.setSizePolicy(qw.QSizePolicy(
1220 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1221 frame = MyFrame(sarea)
1222 frame.widgetVisibilityChanged.connect(
1223 self.panel_visibility_changed)
1225 frame.setSizePolicy(qw.QSizePolicy(
1226 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1227 frame.setFrameStyle(qw.QFrame.NoFrame)
1228 sarea.setWidget(frame)
1229 sarea.setWidgetResizable(True)
1230 layout = qw.QGridLayout()
1231 layout.setContentsMargins(0, 0, 0, 0)
1232 layout.setSpacing(0)
1233 frame.setLayout(layout)
1235 parlayout = qw.QGridLayout()
1237 irow = 0
1238 ipar = 0
1239 have_switches = False
1240 have_params = False
1241 for iparam, param in enumerate(params):
1242 if isinstance(param, Param):
1243 if param.minimum <= 0.0:
1244 param_control = LinValControl(
1245 high_is_none=param.high_is_none,
1246 low_is_none=param.low_is_none,
1247 type=param.type)
1248 else:
1249 param_control = ValControl(
1250 high_is_none=param.high_is_none,
1251 low_is_none=param.low_is_none,
1252 low_is_zero=param.low_is_zero,
1253 type=param.type)
1255 param_control.setup(
1256 param.name,
1257 param.minimum,
1258 param.maximum,
1259 param.default,
1260 iparam)
1262 param_control.set_tracking(param.tracking)
1263 param_control.valchange.connect(
1264 self.modified_snuffling_panel)
1266 self._param_controls[param.ident] = param_control
1267 for iw, w in enumerate(param_control.widgets()):
1268 parlayout.addWidget(w, ipar, iw)
1270 ipar += 1
1271 have_params = True
1273 elif isinstance(param, Choice):
1274 param_widget = ChoiceControl(
1275 param.ident, param.default, param.choices, param.name)
1276 param_widget.choosen.connect(
1277 self.choose_on_snuffling_panel)
1279 self._param_controls[param.ident] = param_widget
1280 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1281 ipar += 1
1282 have_params = True
1284 elif isinstance(param, Switch):
1285 have_switches = True
1287 if have_params:
1288 parframe = qw.QFrame(sarea)
1289 parframe.setSizePolicy(qw.QSizePolicy(
1290 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1291 parframe.setLayout(parlayout)
1292 layout.addWidget(parframe, irow, 0)
1293 irow += 1
1295 if have_switches:
1296 swlayout = qw.QGridLayout()
1297 isw = 0
1298 for iparam, param in enumerate(params):
1299 if isinstance(param, Switch):
1300 param_widget = SwitchControl(
1301 param.ident, param.default, param.name)
1302 param_widget.sw_toggled.connect(
1303 self.switch_on_snuffling_panel)
1305 self._param_controls[param.ident] = param_widget
1306 swlayout.addWidget(param_widget, isw//10, isw % 10)
1307 isw += 1
1309 swframe = qw.QFrame(sarea)
1310 swframe.setSizePolicy(qw.QSizePolicy(
1311 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1312 swframe.setLayout(swlayout)
1313 layout.addWidget(swframe, irow, 0)
1314 irow += 1
1316 butframe = qw.QFrame(sarea)
1317 butframe.setSizePolicy(qw.QSizePolicy(
1318 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1319 butlayout = qw.QHBoxLayout()
1320 butframe.setLayout(butlayout)
1322 live_update_checkbox = qw.QCheckBox('Auto-Run')
1323 if self._live_update:
1324 live_update_checkbox.setCheckState(qc.Qt.Checked)
1326 butlayout.addWidget(live_update_checkbox)
1327 live_update_checkbox.toggled.connect(
1328 self.live_update_toggled)
1330 help_button = qw.QPushButton('Help')
1331 butlayout.addWidget(help_button)
1332 help_button.clicked.connect(
1333 self.help_button_triggered)
1335 clear_button = qw.QPushButton('Clear')
1336 butlayout.addWidget(clear_button)
1337 clear_button.clicked.connect(
1338 self.clear_button_triggered)
1340 call_button = qw.QPushButton('Run')
1341 butlayout.addWidget(call_button)
1342 call_button.clicked.connect(
1343 self.call_button_triggered)
1345 for name, method in self._triggers:
1346 but = qw.QPushButton(name)
1348 def call_and_update(method):
1349 def f():
1350 self.check_call(method)
1351 self.get_viewer().update()
1352 return f
1354 but.clicked.connect(
1355 call_and_update(method))
1357 butlayout.addWidget(but)
1359 layout.addWidget(butframe, irow, 0)
1361 irow += 1
1362 spacer = qw.QSpacerItem(
1363 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1365 layout.addItem(spacer, irow, 0)
1367 return sarea
1369 else:
1370 return None
1372 def make_helpmenuitem(self, parent):
1373 '''
1374 Create the help menu item for the snuffling.
1375 '''
1377 item = qw.QAction(self.get_name(), None)
1379 item.triggered.connect(
1380 self.help_button_triggered)
1382 return item
1384 def make_menuitem(self, parent):
1385 '''
1386 Create the menu item for the snuffling.
1388 This method may be overloaded in subclass and return ``None``, if no
1389 menu entry is wanted.
1390 '''
1392 item = qw.QAction(self.get_name(), None)
1393 item.setCheckable(
1394 self._have_pre_process_hook or self._have_post_process_hook)
1396 item.triggered.connect(
1397 self.menuitem_triggered)
1399 return item
1401 def output_filename(
1402 self,
1403 caption='Save File',
1404 dir='',
1405 filter='',
1406 selected_filter=None):
1408 '''
1409 Query user for an output filename.
1411 This is currently a wrapper to ``QFileDialog.getSaveFileName``.
1412 :py:exc:`UserCancelled` exception is raised if the user cancels the
1413 dialog.
1414 '''
1416 if not dir and self._previous_output_filename:
1417 dir = self._previous_output_filename
1419 fn = getSaveFileName(
1420 self.get_viewer(), caption, dir, filter, selected_filter)
1421 if not fn:
1422 raise UserCancelled()
1424 self._previous_output_filename = fn
1425 return str(fn)
1427 def input_directory(self, caption='Open Directory', dir=''):
1428 '''
1429 Query user for an input directory.
1431 This is a wrapper to ``QFileDialog.getExistingDirectory``. A
1432 :py:exc:`UserCancelled` exception is raised if the user cancels the
1433 dialog.
1434 '''
1436 if not dir and self._previous_input_directory:
1437 dir = self._previous_input_directory
1439 dn = qw.QFileDialog.getExistingDirectory(
1440 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1442 if not dn:
1443 raise UserCancelled()
1445 self._previous_input_directory = dn
1446 return str(dn)
1448 def input_filename(self, caption='Open File', dir='', filter='',
1449 selected_filter=None):
1450 '''
1451 Query user for an input filename.
1453 This is currently a wrapper to ``QFileDialog.getOpenFileName``. A
1454 :py:exc:`UserCancelled` exception is raised if the user cancels the
1455 dialog.
1456 '''
1458 if not dir and self._previous_input_filename:
1459 dir = self._previous_input_filename
1461 fn, _ = qw.QFileDialog.getOpenFileName(
1462 self.get_viewer(),
1463 caption,
1464 dir,
1465 filter)
1467 if not fn:
1468 raise UserCancelled()
1470 self._previous_input_filename = fn
1471 return str(fn)
1473 def input_dialog(self, caption='', request='', directory=False):
1474 '''
1475 Query user for a text input.
1477 This is currently a wrapper to ``QInputDialog.getText``.
1478 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1479 dialog.
1480 '''
1482 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1484 if not ok:
1485 raise UserCancelled()
1487 return inp
1489 def modified_snuffling_panel(self, value, iparam):
1490 '''
1491 Called when the user has played with an adjustable parameter.
1493 The default implementation sets the parameter, calls the snuffling's
1494 :py:meth:`call` method and finally triggers an update on the viewer
1495 widget.
1496 '''
1498 param = self.get_parameters()[iparam]
1499 self._set_parameter_value(param.ident, value)
1500 if self._live_update:
1501 self.check_call(self.call)
1502 self.get_viewer().update()
1504 def switch_on_snuffling_panel(self, ident, state):
1505 '''
1506 Called when the user has toggled a switchable parameter.
1507 '''
1509 self._set_parameter_value(ident, state)
1510 if self._live_update:
1511 self.check_call(self.call)
1512 self.get_viewer().update()
1514 def choose_on_snuffling_panel(self, ident, state):
1515 '''
1516 Called when the user has made a choice about a choosable parameter.
1517 '''
1519 self._set_parameter_value(ident, state)
1520 if self._live_update:
1521 self.check_call(self.call)
1522 self.get_viewer().update()
1524 def menuitem_triggered(self, arg):
1525 '''
1526 Called when the user has triggered the snuffling's menu.
1528 The default implementation calls the snuffling's :py:meth:`call` method
1529 and triggers an update on the viewer widget.
1530 '''
1532 self.check_call(self.call)
1534 if self._have_pre_process_hook:
1535 self._pre_process_hook_enabled = arg
1537 if self._have_post_process_hook:
1538 self._post_process_hook_enabled = arg
1540 if self._have_pre_process_hook or self._have_post_process_hook:
1541 self.get_viewer().clean_update()
1542 else:
1543 self.get_viewer().update()
1545 def call_button_triggered(self):
1546 '''
1547 Called when the user has clicked the snuffling's call button.
1549 The default implementation calls the snuffling's :py:meth:`call` method
1550 and triggers an update on the viewer widget.
1551 '''
1553 self.check_call(self.call)
1554 self.get_viewer().update()
1556 def clear_button_triggered(self):
1557 '''
1558 Called when the user has clicked the snuffling's clear button.
1560 This calls the :py:meth:`cleanup` method and triggers an update on the
1561 viewer widget.
1562 '''
1564 self.cleanup()
1565 self.get_viewer().update()
1567 def help(self):
1568 '''
1569 Get help text in html/markdown.
1570 '''
1572 # Older snuffling used to provide this through __doc__, newer code
1573 # should overload .help()
1574 return self.__doc__ or ''
1576 def help_button_triggered(self):
1577 '''
1578 Creates a :py:class:`QLabel` which contains the documentation as
1579 given in the snufflings :py:meth:`help`.
1580 '''
1582 s = self.help().strip()
1584 if s:
1585 if s.startswith('<html>'):
1586 doc = qw.QLabel(s)
1587 else:
1588 try:
1589 import markdown
1590 doc = qw.QLabel(markdown.markdown(s))
1592 except ImportError:
1593 logger.error(
1594 'Install Python module "markdown" for pretty help '
1595 'formatting.')
1597 doc = qw.QLabel(s)
1598 else:
1599 doc = qw.QLabel('This snuffling does not provide any online help.')
1601 labels = [doc]
1603 if self._filename:
1604 from html import escape
1606 code = open(self._filename, 'r').read()
1608 doc_src = qw.QLabel(
1609 '''<html><body>
1610<hr />
1611<center><em>May the source be with you, young Skywalker!</em><br /><br />
1612<a href="file://%s"><code>%s</code></a></center>
1613<br />
1614<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1615<pre style="white-space: pre-wrap"><code>%s
1616</code></pre></p></body></html>'''
1617 % (
1618 quote(self._filename),
1619 escape(self._filename),
1620 escape(code)))
1622 labels.append(doc_src)
1624 for h in labels:
1625 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1626 h.setWordWrap(True)
1628 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1630 def live_update_toggled(self, on):
1631 '''
1632 Called when the checkbox for live-updates has been toggled.
1633 '''
1635 self.set_live_update(on)
1637 def add_traces(self, traces):
1638 '''
1639 Add traces to the viewer.
1641 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1643 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1644 added to the viewer's internal pile for display. Note, that unlike with
1645 the traces from the files given on the command line, these traces are
1646 kept in memory and so may quickly occupy a lot of ram if a lot of
1647 traces are added.
1649 This method should be preferred over modifying the viewer's internal
1650 pile directly, because this way, the snuffling has a chance to
1651 automatically remove its private traces again (see :py:meth:`cleanup`
1652 method).
1653 '''
1655 ticket = self.get_viewer().add_traces(traces)
1656 self._tickets.append(ticket)
1657 return ticket
1659 def add_trace(self, tr):
1660 '''
1661 Add a trace to the viewer.
1663 See :py:meth:`add_traces`.
1664 '''
1666 self.add_traces([tr])
1668 def add_markers(self, markers):
1669 '''
1670 Add some markers to the display.
1672 Takes a list of objects of type
1673 :py:class:`pyrocko.gui.snuffler.marker.Marker` and adds these to the
1674 viewer.
1675 '''
1677 self.get_viewer().add_markers(markers)
1678 self._markers.extend(markers)
1680 def add_marker(self, marker):
1681 '''
1682 Add a marker to the display.
1684 See :py:meth:`add_markers`.
1685 '''
1687 self.add_markers([marker])
1689 def cleanup(self):
1690 '''
1691 Remove all traces and markers which have been added so far by the
1692 snuffling.
1693 '''
1695 try:
1696 viewer = self.get_viewer()
1697 viewer.release_data(self._tickets)
1698 viewer.remove_markers(self._markers)
1700 except NoViewerSet:
1701 pass
1703 self._tickets = []
1704 self._markers = []
1706 def check_call(self, method):
1708 viewer = self.get_viewer()
1709 if viewer:
1710 sb = viewer.window().statusBar()
1711 sb.clearMessage()
1713 if method in self._call_in_progress:
1714 self.show_message('error', 'Previous action still in progress.')
1715 return
1717 try:
1718 self._call_in_progress[method] = True
1719 method()
1720 return 0
1722 except SnufflingError as e:
1723 if not isinstance(e, SnufflingCallFailed):
1724 # those have logged within error()
1725 logger.error('%s: %s' % (self._name, e))
1726 logger.error('%s: Snuffling action failed' % self._name)
1727 return 1
1729 except Exception as e:
1730 message = '%s: Snuffling action raised an exception: %s' % (
1731 self._name, str(e))
1733 logger.exception(message)
1734 self.show_message('error', message)
1736 finally:
1737 del self._call_in_progress[method]
1739 def call(self):
1740 '''
1741 Main work routine of the snuffling.
1743 This method is called when the snuffling's menu item has been triggered
1744 or when the user has played with the panel controls. To be overloaded
1745 in subclass. The default implementation does nothing useful.
1746 '''
1748 pass
1750 def pre_process_hook(self, traces):
1751 return traces
1753 def post_process_hook(self, traces):
1754 return traces
1756 def get_tpad(self):
1757 '''
1758 Return current amount of extra padding needed by live processing hooks.
1759 '''
1761 return 0.0
1763 def pre_destroy(self):
1764 '''
1765 Called when the snuffling instance is about to be deleted.
1767 Can be overloaded to do user-defined cleanup actions. The
1768 default implementation calls :py:meth:`cleanup` and deletes
1769 the snuffling`s tempory directory, if needed.
1770 '''
1772 self.cleanup()
1773 if self._tempdir is not None:
1774 import shutil
1775 shutil.rmtree(self._tempdir)
1778class SnufflingError(Exception):
1779 '''
1780 Base exception for Snuffling errors.
1781 '''
1782 pass
1785class NoViewerSet(SnufflingError):
1786 '''
1787 This exception is raised, when no viewer has been set on a Snuffling.
1788 '''
1790 def __str__(self):
1791 return 'No GUI available. ' \
1792 'Maybe this Snuffling cannot be run in command line mode?'
1795class MissingStationInformation(SnufflingError):
1796 '''
1797 Raised when station information is missing.
1798 '''
1801class NoTracesSelected(SnufflingError):
1802 '''
1803 This exception is raised, when no traces have been selected in the viewer
1804 and we cannot fallback to using the current view.
1805 '''
1807 def __str__(self):
1808 return 'No traces have been selected / are available.'
1811class UserCancelled(SnufflingError):
1812 '''
1813 This exception is raised, when the user has cancelled a snuffling dialog.
1814 '''
1816 def __str__(self):
1817 return 'The user has cancelled a dialog.'
1820class SnufflingCallFailed(SnufflingError):
1821 '''
1822 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1823 :py:meth:`Snuffling.call`.
1824 '''
1827class InvalidSnufflingFilename(Exception):
1828 pass
1831class SnufflingModule(object):
1832 '''
1833 Utility class to load/reload snufflings from a file.
1835 The snufflings are created by user modules which have the special function
1836 ``__snufflings__`` which return the snuffling instances to be
1837 exported. The snuffling module is attached to a handler class, which makes
1838 use of the snufflings (e.g.
1839 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` from
1840 ``pile_viewer.py``). The handler class must implement the methods
1841 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1842 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1843 :py:meth:`remove_snufflings` which may be called from the handler class,
1844 when needed.
1845 '''
1847 mtimes = {}
1849 def __init__(self, path, name, handler):
1850 self._path = path
1851 self._name = name
1852 self._mtime = None
1853 self._module = None
1854 self._snufflings = []
1855 self._handler = handler
1857 def load_if_needed(self):
1858 '''
1859 Called by Snuffler to check whether it has to reload the module.
1860 '''
1861 filename = os.path.join(self._path, self._name+'.py')
1863 try:
1864 mtime = os.stat(filename)[8]
1865 except OSError as e:
1866 if e.errno == 2:
1867 logger.error(e)
1868 raise BrokenSnufflingModule(filename)
1870 if self._module is None:
1871 sys.path[0:0] = [self._path]
1872 try:
1873 logger.debug('Loading snuffling module %s' % filename)
1874 if self._name in sys.modules:
1875 raise InvalidSnufflingFilename(self._name)
1877 self._module = __import__(self._name)
1878 del sys.modules[self._name]
1880 for snuffling in self._module.__snufflings__():
1881 snuffling._filename = filename
1882 self.add_snuffling(snuffling)
1884 except (Exception, SystemExit):
1885 logger.error(traceback.format_exc())
1886 raise BrokenSnufflingModule(filename)
1888 finally:
1889 sys.path[0:1] = []
1891 elif self._mtime != mtime:
1892 logger.warning('Reloading snuffling module %s' % filename)
1893 settings = self.remove_snufflings()
1894 sys.path[0:0] = [self._path]
1895 try:
1897 sys.modules[self._name] = self._module
1899 reload(self._module)
1900 del sys.modules[self._name]
1902 for snuffling in self._module.__snufflings__():
1903 snuffling._filename = filename
1904 self.add_snuffling(snuffling, reloaded=True)
1906 if len(self._snufflings) == len(settings):
1907 for sett, snuf in zip(settings, self._snufflings):
1908 snuf.set_settings(sett)
1910 except (Exception, SystemExit):
1911 logger.error(traceback.format_exc())
1912 raise BrokenSnufflingModule(filename)
1914 finally:
1915 sys.path[0:1] = []
1917 self._mtime = mtime
1919 def add_snuffling(self, snuffling, reloaded=False):
1920 '''
1921 Called by :py:meth:`load_if_needed` to add a snuffling.
1922 '''
1923 snuffling._path = self._path
1924 snuffling.setup()
1925 self._snufflings.append(snuffling)
1926 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1928 def remove_snufflings(self):
1929 '''
1930 Called by :py:meth:`load_if_needed` to remove all snufflings.
1931 '''
1932 settings = []
1933 for snuffling in self._snufflings:
1934 settings.append(snuffling.get_settings())
1935 self._handler.remove_snuffling(snuffling)
1937 self._snufflings = []
1938 return settings
1941class BrokenSnufflingModule(Exception):
1942 pass
1945class MyScrollArea(qw.QScrollArea):
1947 def sizeHint(self):
1948 '''
1949 '''
1951 s = qc.QSize()
1952 s.setWidth(self.widget().sizeHint().width())
1953 s.setHeight(self.widget().sizeHint().height())
1954 return s
1957class SwitchControl(qw.QCheckBox):
1958 sw_toggled = qc.pyqtSignal(object, bool)
1960 def __init__(self, ident, default, *args):
1961 qw.QCheckBox.__init__(self, *args)
1962 self.ident = ident
1963 self.setChecked(default)
1964 self.toggled.connect(self._sw_toggled)
1966 def _sw_toggled(self, state):
1967 self.sw_toggled.emit(self.ident, state)
1969 def set_value(self, state):
1970 self.blockSignals(True)
1971 self.setChecked(state)
1972 self.blockSignals(False)
1975class ChoiceControl(qw.QFrame):
1976 choosen = qc.pyqtSignal(object, object)
1978 def __init__(self, ident, default, choices, name, *args):
1979 qw.QFrame.__init__(self, *args)
1980 self.label = qw.QLabel(name, self)
1981 self.label.setMinimumWidth(120)
1982 self.cbox = qw.QComboBox(self)
1983 self.layout = qw.QHBoxLayout(self)
1984 self.layout.addWidget(self.label)
1985 self.layout.addWidget(self.cbox)
1986 self.layout.setContentsMargins(0, 0, 0, 0)
1987 self.layout.setSpacing(0)
1988 self.ident = ident
1989 self.choices = choices
1990 for ichoice, choice in enumerate(choices):
1991 self.cbox.addItem(choice)
1993 self.set_value(default)
1994 self.cbox.activated.connect(self.emit_choosen)
1996 def set_choices(self, choices):
1997 icur = self.cbox.currentIndex()
1998 if icur != -1:
1999 selected_choice = choices[icur]
2000 else:
2001 selected_choice = None
2003 self.choices = choices
2004 self.cbox.clear()
2005 for ichoice, choice in enumerate(choices):
2006 self.cbox.addItem(qc.QString(choice))
2008 if selected_choice is not None and selected_choice in choices:
2009 self.set_value(selected_choice)
2010 return selected_choice
2011 else:
2012 self.set_value(choices[0])
2013 return choices[0]
2015 def emit_choosen(self, i):
2016 self.choosen.emit(
2017 self.ident,
2018 self.choices[i])
2020 def set_value(self, v):
2021 self.cbox.blockSignals(True)
2022 for i, choice in enumerate(self.choices):
2023 if choice == v:
2024 self.cbox.setCurrentIndex(i)
2025 self.cbox.blockSignals(False)