Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffling.py: 46%
836 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 06:59 +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):
447 '''
448 Show an error message box and raise :py:exc:`SnufflingCallFailed`
449 exception.
451 :param message: specifying the error
452 '''
454 self.error(message)
455 raise SnufflingCallFailed(message)
457 def pylab(self, name=None, get='axes', figure_cls=None):
458 '''
459 Create a :py:class:`pyrocko.gui.util.FigureFrame` and return either the
460 frame, a :py:class:`matplotlib.figure.Figure` instance or a
461 :py:class:`matplotlib.axes.Axes` instance.
463 :param name: labels the figure frame's tab
464 :param get: 'axes'|'figure'|'frame' (optional)
465 '''
467 if name is None:
468 self._iplot += 1
469 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
471 fframe = FigureFrame(figure_cls=figure_cls)
472 self._panel_parent.add_tab(name, fframe)
473 if get == 'axes':
474 return fframe.gca()
475 elif get == 'figure':
476 return fframe.gcf()
477 elif get == 'figure_frame':
478 return fframe
480 def figure(self, name=None):
481 '''
482 Returns a :py:class:`matplotlib.figure.Figure` instance.
484 Force drawing of the figure by calling `fig.canvas.draw()` on the
485 returned object ``fig``.
487 :param name: labels the tab of the figure
488 '''
490 return self.pylab(name=name, get='figure')
492 def axes(self, name=None):
493 '''
494 Returns a :py:class:`matplotlib.axes.Axes` instance.
496 :param name: labels the tab of axes
497 '''
499 return self.pylab(name=name, get='axes')
501 def figure_frame(self, name=None, figure_cls=None):
502 '''
503 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
505 :param name: labels the tab figure frame
506 '''
508 return self.pylab(name=name, get='figure_frame', figure_cls=figure_cls)
510 def smartplot_frame(self, name, *args, plot_cls=None, **kwargs):
511 '''
512 Create a :py:class:`pyrocko.gui.util.SmartplotFrame`.
514 :param name: labels the tab
515 :param \\*args:
516 passed to :py:class:`pyrocko.plot.smartplot.Plot`
517 :param \\*kwargs:
518 passed to :py:class:`pyrocko.plot.smartplot.Plot`
519 :param plot_cls:
520 if given, subclass to be used instead of
521 :py:class:`pyrocko.plot.smartplot.Plot`
522 '''
523 frame = SmartplotFrame(
524 plot_args=args,
525 plot_cls=plot_cls,
526 plot_kwargs=kwargs)
528 self._panel_parent.add_tab(name, frame)
529 return frame
531 def pixmap_frame(self, filename=None, name=None):
532 '''
533 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
535 :param name: labels the tab
536 :param filename: name of file to be displayed
537 '''
539 f = PixmapFrame(filename)
541 scroll_area = qw.QScrollArea()
542 scroll_area.setWidget(f)
543 scroll_area.setWidgetResizable(True)
545 self._panel_parent.add_tab(name or 'Pixmap', scroll_area)
546 return f
548 def web_frame(self, url=None, name=None):
549 '''
550 Creates a :py:class:`~pyrocko.gui.util.WebKitFrame` which can be
551 used as a browser within Snuffler.
553 :param url: url to open
554 :param name: labels the tab
555 '''
557 if name is None:
558 self._iplot += 1
559 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
561 f = WebKitFrame(url)
562 self._panel_parent.add_tab(name, f)
563 return f
565 def vtk_frame(self, name=None, actors=None):
566 '''
567 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
568 graphics.
570 :param actors: list of VTKActors
571 :param name: labels the tab
573 Initialize the interactive rendering by calling the frames'
574 :py:meth`initialize` method after having added all actors to the frames
575 renderer.
577 Requires installation of vtk including python wrapper.
578 '''
579 if name is None:
580 self._iplot += 1
581 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
583 try:
584 f = VTKFrame(actors=actors)
585 except ImportError as e:
586 self.fail(e)
588 self._panel_parent.add_tab(name, f)
589 return f
591 def tempdir(self):
592 '''
593 Create a temporary directory and return its absolute path.
595 The directory and all its contents are removed when the Snuffling
596 instance is deleted.
597 '''
599 if self._tempdir is None:
600 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
602 return self._tempdir
604 def set_live_update(self, live_update):
605 '''
606 Enable/disable live updating.
608 When live updates are enabled, the :py:meth:`call` method is called
609 whenever the user changes a parameter. If it is disabled, the user has
610 to initiate such a call manually by triggering the snuffling's menu
611 item or pressing the call button.
612 '''
614 self._live_update = live_update
615 if self._have_pre_process_hook:
616 self._pre_process_hook_enabled = live_update
617 if self._have_post_process_hook:
618 self._post_process_hook_enabled = live_update
620 try:
621 self.get_viewer().clean_update()
622 except NoViewerSet:
623 pass
625 def add_parameter(self, param):
626 '''
627 Add an adjustable parameter to the snuffling.
629 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
630 :py:class:`Choice`.
632 For each parameter added, controls are added to the snuffling's panel,
633 so that the parameter can be adjusted from the gui.
634 '''
636 self._parameters.append(param)
637 self._set_parameter_value(param.ident, param.default)
639 if self._panel is not None:
640 self.delete_gui()
641 self.setup_gui()
643 def add_trigger(self, name, method):
644 '''
645 Add a button to the snuffling's panel.
647 :param name: string that labels the button
648 :param method: method associated with the button
649 '''
651 self._triggers.append((name, method))
653 if self._panel is not None:
654 self.delete_gui()
655 self.setup_gui()
657 def get_parameters(self):
658 '''
659 Get the snuffling's adjustable parameter definitions.
661 Returns a list of objects of type :py:class:`Param`.
662 '''
664 return self._parameters
666 def get_parameter(self, ident):
667 '''
668 Get one of the snuffling's adjustable parameter definitions.
670 :param ident: identifier of the parameter
672 Returns an object of type :py:class:`Param` or ``None``.
673 '''
675 for param in self._parameters:
676 if param.ident == ident:
677 return param
678 return None
680 def set_parameter(self, ident, value):
681 '''
682 Set one of the snuffling's adjustable parameters.
684 :param ident: identifier of the parameter
685 :param value: new value of the parameter
687 Adjusts the control of a parameter without calling :py:meth:`call`.
688 '''
690 self._set_parameter_value(ident, value)
692 control = self._param_controls.get(ident, None)
693 if control:
694 control.set_value(value)
696 def set_parameter_range(self, ident, vmin, vmax):
697 '''
698 Set the range of one of the snuffling's adjustable parameters.
700 :param ident: identifier of the parameter
701 :param vmin,vmax: new minimum and maximum value for the parameter
703 Adjusts the control of a parameter without calling :py:meth:`call`.
704 '''
706 control = self._param_controls.get(ident, None)
707 if control:
708 control.set_range(vmin, vmax)
710 def set_parameter_choices(self, ident, choices):
711 '''
712 Update the choices of a Choice parameter.
714 :param ident: identifier of the parameter
715 :param choices: list of strings
716 '''
718 control = self._param_controls.get(ident, None)
719 if control:
720 selected_choice = control.set_choices(choices)
721 self._set_parameter_value(ident, selected_choice)
723 def _set_parameter_value(self, ident, value):
724 setattr(self, ident, value)
726 def get_parameter_value(self, ident):
727 '''
728 Get the current value of a parameter.
730 :param ident: identifier of the parameter
731 '''
732 return getattr(self, ident)
734 def get_settings(self):
735 '''
736 Returns a dictionary with identifiers of all parameters as keys and
737 their values as the dictionaries values.
738 '''
740 params = self.get_parameters()
741 settings = {}
742 for param in params:
743 settings[param.ident] = self.get_parameter_value(param.ident)
745 return settings
747 def set_settings(self, settings):
748 params = self.get_parameters()
749 dparams = dict([(param.ident, param) for param in params])
750 for k, v in settings.items():
751 if k in dparams:
752 self._set_parameter_value(k, v)
753 if k in self._param_controls:
754 control = self._param_controls[k]
755 control.set_value(v)
757 def get_viewer(self):
758 '''
759 Get the parent viewer.
761 Returns a reference to an object of type
762 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer`, which is the
763 main viewer widget.
765 If no gui has been initialized for the snuffling, a
766 :py:exc:`NoViewerSet` exception is raised.
767 '''
769 if self._viewer is None:
770 raise NoViewerSet()
771 return self._viewer
773 def get_pile(self):
774 '''
775 Get the pile.
777 If a gui has been initialized, a reference to the viewer's internal
778 pile is returned. If not, the :py:meth:`make_pile` method (which may be
779 overloaded in subclass) is called to create a pile. This can be
780 utilized to make hybrid snufflings, which may work also in a standalone
781 mode.
782 '''
784 try:
785 p = self.get_viewer().get_pile()
786 except NoViewerSet:
787 if self._no_viewer_pile is None:
788 self._no_viewer_pile = self.make_pile()
790 p = self._no_viewer_pile
792 return p
794 def get_active_event_and_stations(
795 self, trange=(-3600., 3600.), missing='warn'):
797 '''
798 Get event and stations with available data for active event.
800 :param trange: (begin, end), time range around event origin time to
801 query for available data
802 :param missing: string, what to do in case of missing station
803 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
805 :returns: ``(event, stations)``
806 '''
808 p = self.get_pile()
809 v = self.get_viewer()
811 event = v.get_active_event()
812 if event is None:
813 self.fail(
814 'No active event set. Select an event and press "e" to make '
815 'it the "active event"')
817 stations = {}
818 for traces in p.chopper(
819 event.time+trange[0],
820 event.time+trange[1],
821 load_data=False,
822 degap=False):
824 for tr in traces:
825 try:
826 for skey in v.station_keys(tr):
827 if skey in stations:
828 continue
830 station = v.get_station(skey)
831 stations[skey] = station
833 except KeyError:
834 s = 'No station information for station key "%s".' \
835 % '.'.join(skey)
837 if missing == 'warn':
838 logger.warning(s)
839 elif missing == 'raise':
840 raise MissingStationInformation(s)
841 elif missing == 'ignore':
842 pass
843 else:
844 assert False, 'invalid argument to "missing"'
846 stations[skey] = None
848 return event, list(set(
849 st for st in stations.values() if st is not None))
851 def get_stations(self):
852 '''
853 Get all stations known to the viewer.
854 '''
856 v = self.get_viewer()
857 stations = list(v.stations.values())
858 return stations
860 def get_markers(self):
861 '''
862 Get all markers from the viewer.
863 '''
865 return self.get_viewer().get_markers()
867 def get_event_markers(self):
868 '''
869 Get all event markers from the viewer.
870 '''
872 return [m for m in self.get_viewer().get_markers()
873 if isinstance(m, EventMarker)]
875 def get_selected_markers(self):
876 '''
877 Get all selected markers from the viewer.
878 '''
880 return self.get_viewer().selected_markers()
882 def get_selected_event_markers(self):
883 '''
884 Get all selected event markers from the viewer.
885 '''
887 return [m for m in self.get_viewer().selected_markers()
888 if isinstance(m, EventMarker)]
890 def get_active_event_and_phase_markers(self):
891 '''
892 Get the marker of the active event and any associated phase markers
893 '''
895 viewer = self.get_viewer()
896 markers = viewer.get_markers()
897 event_marker = viewer.get_active_event_marker()
898 if event_marker is None:
899 self.fail(
900 'No active event set. '
901 'Select an event and press "e" to make it the "active event"')
903 event = event_marker.get_event()
905 selection = []
906 for m in markers:
907 if isinstance(m, PhaseMarker):
908 if m.get_event() is event:
909 selection.append(m)
911 return (
912 event_marker,
913 [m for m in markers if isinstance(m, PhaseMarker) and
914 m.get_event() == event])
916 def get_viewer_trace_selector(self, mode='inview'):
917 '''
918 Get currently active trace selector from viewer.
920 :param mode: set to ``'inview'`` (default) to only include selections
921 currently shown in the viewer, ``'visible' to include all traces
922 not currenly hidden by hide or quick-select commands, or ``'all'``
923 to disable any restrictions.
924 '''
926 viewer = self.get_viewer()
928 def rtrue(tr):
929 return True
931 if mode == 'inview':
932 return viewer.trace_selector or rtrue
933 elif mode == 'visible':
934 return viewer.trace_filter or rtrue
935 elif mode == 'all':
936 return rtrue
937 else:
938 raise Exception('invalid mode argument')
940 def chopper_selected_traces(self, fallback=False, marker_selector=None,
941 mode='inview', main_bandpass=False,
942 progress=None, responsive=False,
943 *args, **kwargs):
944 '''
945 Iterate over selected traces.
947 Shortcut to get all trace data contained in the selected markers in the
948 running snuffler. For each selected marker,
949 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
950 *tmin*, *tmax*, and *trace_selector* set to values according to the
951 marker. Additional arguments to the chopper are handed over from
952 *\\*args* and *\\*\\*kwargs*.
954 :param fallback:
955 If ``True``, if no selection has been marked, use the content
956 currently visible in the viewer.
958 :param marker_selector:
959 If not ``None`` a callback to filter markers.
961 :param mode:
962 Set to ``'inview'`` (default) to only include selections currently
963 shown in the viewer (excluding traces accessible through vertical
964 scrolling), ``'visible'`` to include all traces not currently
965 hidden by hide or quick-select commands (including traces
966 accessible through vertical scrolling), or ``'all'`` to disable any
967 restrictions.
969 :param main_bandpass:
970 If ``True``, apply main control high- and lowpass filters to
971 traces. Note: use with caution. Processing is fixed to use 4th
972 order Butterworth highpass and lowpass and the signal is always
973 demeaned before filtering. FFT filtering, rotation, demean and
974 bandpass settings from the graphical interface are not respected
975 here. Padding is not automatically adjusted so results may include
976 artifacts.
978 :param progress:
979 If given a string a progress bar is shown to the user. The string
980 is used as the label for the progress bar.
982 :param responsive:
983 If set to ``True``, occasionally allow UI events to be processed.
984 If used in combination with ``progress``, this allows the iterator
985 to be aborted by the user.
986 '''
988 try:
989 viewer = self.get_viewer()
990 markers = [
991 m for m in viewer.selected_markers()
992 if not isinstance(m, EventMarker)]
994 if marker_selector is not None:
995 markers = [
996 marker for marker in markers if marker_selector(marker)]
998 pile = self.get_pile()
1000 def rtrue(tr):
1001 return True
1003 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
1004 trace_selector_viewer = self.get_viewer_trace_selector(mode)
1006 style_arg = kwargs.pop('style', None)
1008 if main_bandpass:
1009 def apply_filters(traces):
1010 for tr in traces:
1011 if viewer.highpass is not None:
1012 tr.highpass(4, viewer.highpass)
1013 if viewer.lowpass is not None:
1014 tr.lowpass(4, viewer.lowpass)
1015 return traces
1016 else:
1017 def apply_filters(traces):
1018 return traces
1020 pb = viewer.parent().get_progressbars()
1022 time_last = [time.time()]
1024 def update_progress(label, batch):
1025 time_now = time.time()
1026 if responsive:
1027 # start processing events with one second delay, so that
1028 # e.g. cleanup actions at startup do not cause track number
1029 # changes etc.
1030 if time_last[0] + 1. < time_now:
1031 qw.qApp.processEvents()
1032 else:
1033 # redraw about once a second
1034 if time_last[0] + 1. < time_now:
1035 viewer.repaint()
1037 time_last[0] = time.time() # use time after drawing
1039 abort = pb.set_status(
1040 label, batch.i*100./batch.n, responsive)
1041 abort |= viewer.window().is_closing()
1043 return abort
1045 if markers:
1046 for imarker, marker in enumerate(markers):
1047 try:
1048 if progress:
1049 label = '%s: %i/%i' % (
1050 progress, imarker+1, len(markers))
1052 pb.set_status(label, 0, responsive)
1054 if not marker.nslc_ids:
1055 trace_selector_marker = rtrue
1056 else:
1057 def trace_selector_marker(tr):
1058 return marker.match_nslc(tr.nslc_id)
1060 def trace_selector(tr):
1061 return trace_selector_arg(tr) \
1062 and trace_selector_viewer(tr) \
1063 and trace_selector_marker(tr)
1065 for batch in pile.chopper(
1066 tmin=marker.tmin,
1067 tmax=marker.tmax,
1068 trace_selector=trace_selector,
1069 style='batch',
1070 *args,
1071 **kwargs):
1073 if progress:
1074 abort = update_progress(label, batch)
1075 if abort:
1076 return
1078 batch.traces = apply_filters(batch.traces)
1079 if style_arg == 'batch':
1080 yield batch
1081 else:
1082 yield batch.traces
1084 finally:
1085 if progress:
1086 pb.set_status(label, 100., responsive)
1088 elif fallback:
1089 def trace_selector(tr):
1090 return trace_selector_arg(tr) \
1091 and trace_selector_viewer(tr)
1093 tmin, tmax = viewer.get_time_range()
1095 if not pile.is_empty():
1096 ptmin = pile.get_tmin()
1097 tpad = kwargs.get('tpad', 0.0)
1098 if ptmin > tmin:
1099 tmin = ptmin + tpad
1100 ptmax = pile.get_tmax()
1101 if ptmax < tmax:
1102 tmax = ptmax - tpad
1104 try:
1105 if progress:
1106 label = progress
1107 pb.set_status(label, 0, responsive)
1109 for batch in pile.chopper(
1110 tmin=tmin,
1111 tmax=tmax,
1112 trace_selector=trace_selector,
1113 style='batch',
1114 *args,
1115 **kwargs):
1117 if progress:
1118 abort = update_progress(label, batch)
1120 if abort:
1121 return
1123 batch.traces = apply_filters(batch.traces)
1125 if style_arg == 'batch':
1126 yield batch
1127 else:
1128 yield batch.traces
1130 finally:
1131 if progress:
1132 pb.set_status(label, 100., responsive)
1134 else:
1135 raise NoTracesSelected()
1137 except NoViewerSet:
1138 pile = self.get_pile()
1139 return pile.chopper(*args, **kwargs)
1141 def get_selected_time_range(self, fallback=False):
1142 '''
1143 Get the time range spanning all selected markers.
1145 :param fallback: if ``True`` and no marker is selected return begin and
1146 end of visible time range
1147 '''
1149 viewer = self.get_viewer()
1150 markers = viewer.selected_markers()
1151 mins = [marker.tmin for marker in markers]
1152 maxs = [marker.tmax for marker in markers]
1154 if mins and maxs:
1155 tmin = min(mins)
1156 tmax = max(maxs)
1158 elif fallback:
1159 tmin, tmax = viewer.get_time_range()
1161 else:
1162 raise NoTracesSelected()
1164 return tmin, tmax
1166 def panel_visibility_changed(self, bool):
1167 '''
1168 Called when the snuffling's panel becomes visible or is hidden.
1170 Can be overloaded in subclass, e.g. to perform additional setup actions
1171 when the panel is activated the first time.
1172 '''
1174 pass
1176 def make_pile(self):
1177 '''
1178 Create a pile.
1180 To be overloaded in subclass. The default implementation just calls
1181 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1182 arguments.
1183 '''
1185 cachedirname = config.config().cache_dir
1186 sources = self._cli_params.get('sources', sys.argv[1:])
1187 return pile.make_pile(
1188 sources,
1189 cachedirname=cachedirname,
1190 regex=self._cli_params['regex'],
1191 fileformat=self._cli_params['format'])
1193 def make_panel(self, parent):
1194 '''
1195 Create a widget for the snuffling's control panel.
1197 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1198 if no panel is needed (e.g. if the snuffling has no adjustable
1199 parameters).
1200 '''
1202 params = self.get_parameters()
1203 self._param_controls = {}
1204 if params or self._force_panel:
1205 sarea = MyScrollArea(parent.get_panel_parent_widget())
1206 sarea.setFrameStyle(qw.QFrame.NoFrame)
1207 sarea.setSizePolicy(qw.QSizePolicy(
1208 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1209 frame = MyFrame(sarea)
1210 frame.widgetVisibilityChanged.connect(
1211 self.panel_visibility_changed)
1213 frame.setSizePolicy(qw.QSizePolicy(
1214 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1215 frame.setFrameStyle(qw.QFrame.NoFrame)
1216 sarea.setWidget(frame)
1217 sarea.setWidgetResizable(True)
1218 layout = qw.QGridLayout()
1219 layout.setContentsMargins(0, 0, 0, 0)
1220 layout.setSpacing(0)
1221 frame.setLayout(layout)
1223 parlayout = qw.QGridLayout()
1225 irow = 0
1226 ipar = 0
1227 have_switches = False
1228 have_params = False
1229 for iparam, param in enumerate(params):
1230 if isinstance(param, Param):
1231 if param.minimum <= 0.0:
1232 param_control = LinValControl(
1233 high_is_none=param.high_is_none,
1234 low_is_none=param.low_is_none,
1235 type=param.type)
1236 else:
1237 param_control = ValControl(
1238 high_is_none=param.high_is_none,
1239 low_is_none=param.low_is_none,
1240 low_is_zero=param.low_is_zero,
1241 type=param.type)
1243 param_control.setup(
1244 param.name,
1245 param.minimum,
1246 param.maximum,
1247 param.default,
1248 iparam)
1250 param_control.set_tracking(param.tracking)
1251 param_control.valchange.connect(
1252 self.modified_snuffling_panel)
1254 self._param_controls[param.ident] = param_control
1255 for iw, w in enumerate(param_control.widgets()):
1256 parlayout.addWidget(w, ipar, iw)
1258 ipar += 1
1259 have_params = True
1261 elif isinstance(param, Choice):
1262 param_widget = ChoiceControl(
1263 param.ident, param.default, param.choices, param.name)
1264 param_widget.choosen.connect(
1265 self.choose_on_snuffling_panel)
1267 self._param_controls[param.ident] = param_widget
1268 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1269 ipar += 1
1270 have_params = True
1272 elif isinstance(param, Switch):
1273 have_switches = True
1275 if have_params:
1276 parframe = qw.QFrame(sarea)
1277 parframe.setSizePolicy(qw.QSizePolicy(
1278 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1279 parframe.setLayout(parlayout)
1280 layout.addWidget(parframe, irow, 0)
1281 irow += 1
1283 if have_switches:
1284 swlayout = qw.QGridLayout()
1285 isw = 0
1286 for iparam, param in enumerate(params):
1287 if isinstance(param, Switch):
1288 param_widget = SwitchControl(
1289 param.ident, param.default, param.name)
1290 param_widget.sw_toggled.connect(
1291 self.switch_on_snuffling_panel)
1293 self._param_controls[param.ident] = param_widget
1294 swlayout.addWidget(param_widget, isw//10, isw % 10)
1295 isw += 1
1297 swframe = qw.QFrame(sarea)
1298 swframe.setSizePolicy(qw.QSizePolicy(
1299 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1300 swframe.setLayout(swlayout)
1301 layout.addWidget(swframe, irow, 0)
1302 irow += 1
1304 butframe = qw.QFrame(sarea)
1305 butframe.setSizePolicy(qw.QSizePolicy(
1306 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1307 butlayout = qw.QHBoxLayout()
1308 butframe.setLayout(butlayout)
1310 live_update_checkbox = qw.QCheckBox('Auto-Run')
1311 if self._live_update:
1312 live_update_checkbox.setCheckState(qc.Qt.Checked)
1314 butlayout.addWidget(live_update_checkbox)
1315 live_update_checkbox.toggled.connect(
1316 self.live_update_toggled)
1318 help_button = qw.QPushButton('Help')
1319 butlayout.addWidget(help_button)
1320 help_button.clicked.connect(
1321 self.help_button_triggered)
1323 clear_button = qw.QPushButton('Clear')
1324 butlayout.addWidget(clear_button)
1325 clear_button.clicked.connect(
1326 self.clear_button_triggered)
1328 call_button = qw.QPushButton('Run')
1329 butlayout.addWidget(call_button)
1330 call_button.clicked.connect(
1331 self.call_button_triggered)
1333 for name, method in self._triggers:
1334 but = qw.QPushButton(name)
1336 def call_and_update(method):
1337 def f():
1338 self.check_call(method)
1339 self.get_viewer().update()
1340 return f
1342 but.clicked.connect(
1343 call_and_update(method))
1345 butlayout.addWidget(but)
1347 layout.addWidget(butframe, irow, 0)
1349 irow += 1
1350 spacer = qw.QSpacerItem(
1351 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1353 layout.addItem(spacer, irow, 0)
1355 return sarea
1357 else:
1358 return None
1360 def make_helpmenuitem(self, parent):
1361 '''
1362 Create the help menu item for the snuffling.
1363 '''
1365 item = qw.QAction(self.get_name(), None)
1367 item.triggered.connect(
1368 self.help_button_triggered)
1370 return item
1372 def make_menuitem(self, parent):
1373 '''
1374 Create the menu item for the snuffling.
1376 This method may be overloaded in subclass and return ``None``, if no
1377 menu entry is wanted.
1378 '''
1380 item = qw.QAction(self.get_name(), None)
1381 item.setCheckable(
1382 self._have_pre_process_hook or self._have_post_process_hook)
1384 item.triggered.connect(
1385 self.menuitem_triggered)
1387 return item
1389 def output_filename(
1390 self,
1391 caption='Save File',
1392 dir='',
1393 filter='',
1394 selected_filter=None):
1396 '''
1397 Query user for an output filename.
1399 This is currently a wrapper to ``QFileDialog.getSaveFileName``.
1400 :py:exc:`UserCancelled` exception is raised if the user cancels the
1401 dialog.
1402 '''
1404 if not dir and self._previous_output_filename:
1405 dir = self._previous_output_filename
1407 fn = getSaveFileName(
1408 self.get_viewer(), caption, dir, filter, selected_filter)
1409 if not fn:
1410 raise UserCancelled()
1412 self._previous_output_filename = fn
1413 return str(fn)
1415 def input_directory(self, caption='Open Directory', dir=''):
1416 '''
1417 Query user for an input directory.
1419 This is a wrapper to ``QFileDialog.getExistingDirectory``. A
1420 :py:exc:`UserCancelled` exception is raised if the user cancels the
1421 dialog.
1422 '''
1424 if not dir and self._previous_input_directory:
1425 dir = self._previous_input_directory
1427 dn = qw.QFileDialog.getExistingDirectory(
1428 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1430 if not dn:
1431 raise UserCancelled()
1433 self._previous_input_directory = dn
1434 return str(dn)
1436 def input_filename(self, caption='Open File', dir='', filter='',
1437 selected_filter=None):
1438 '''
1439 Query user for an input filename.
1441 This is currently a wrapper to ``QFileDialog.getOpenFileName``. A
1442 :py:exc:`UserCancelled` exception is raised if the user cancels the
1443 dialog.
1444 '''
1446 if not dir and self._previous_input_filename:
1447 dir = self._previous_input_filename
1449 fn, _ = qw.QFileDialog.getOpenFileName(
1450 self.get_viewer(),
1451 caption,
1452 dir,
1453 filter)
1455 if not fn:
1456 raise UserCancelled()
1458 self._previous_input_filename = fn
1459 return str(fn)
1461 def input_dialog(self, caption='', request='', directory=False):
1462 '''
1463 Query user for a text input.
1465 This is currently a wrapper to ``QInputDialog.getText``.
1466 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1467 dialog.
1468 '''
1470 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1472 if not ok:
1473 raise UserCancelled()
1475 return inp
1477 def modified_snuffling_panel(self, value, iparam):
1478 '''
1479 Called when the user has played with an adjustable parameter.
1481 The default implementation sets the parameter, calls the snuffling's
1482 :py:meth:`call` method and finally triggers an update on the viewer
1483 widget.
1484 '''
1486 param = self.get_parameters()[iparam]
1487 self._set_parameter_value(param.ident, value)
1488 if self._live_update:
1489 self.check_call(self.call)
1490 self.get_viewer().update()
1492 def switch_on_snuffling_panel(self, ident, state):
1493 '''
1494 Called when the user has toggled a switchable parameter.
1495 '''
1497 self._set_parameter_value(ident, state)
1498 if self._live_update:
1499 self.check_call(self.call)
1500 self.get_viewer().update()
1502 def choose_on_snuffling_panel(self, ident, state):
1503 '''
1504 Called when the user has made a choice about a choosable parameter.
1505 '''
1507 self._set_parameter_value(ident, state)
1508 if self._live_update:
1509 self.check_call(self.call)
1510 self.get_viewer().update()
1512 def menuitem_triggered(self, arg):
1513 '''
1514 Called when the user has triggered the snuffling's menu.
1516 The default implementation calls the snuffling's :py:meth:`call` method
1517 and triggers an update on the viewer widget.
1518 '''
1520 self.check_call(self.call)
1522 if self._have_pre_process_hook:
1523 self._pre_process_hook_enabled = arg
1525 if self._have_post_process_hook:
1526 self._post_process_hook_enabled = arg
1528 if self._have_pre_process_hook or self._have_post_process_hook:
1529 self.get_viewer().clean_update()
1530 else:
1531 self.get_viewer().update()
1533 def call_button_triggered(self):
1534 '''
1535 Called when the user has clicked the snuffling's call button.
1537 The default implementation calls the snuffling's :py:meth:`call` method
1538 and triggers an update on the viewer widget.
1539 '''
1541 self.check_call(self.call)
1542 self.get_viewer().update()
1544 def clear_button_triggered(self):
1545 '''
1546 Called when the user has clicked the snuffling's clear button.
1548 This calls the :py:meth:`cleanup` method and triggers an update on the
1549 viewer widget.
1550 '''
1552 self.cleanup()
1553 self.get_viewer().update()
1555 def help(self):
1556 '''
1557 Get help text in html/markdown.
1558 '''
1560 # Older snuffling used to provide this through __doc__, newer code
1561 # should overload .help()
1562 return self.__doc__ or ''
1564 def help_button_triggered(self):
1565 '''
1566 Creates a :py:class:`QLabel` which contains the documentation as
1567 given in the snufflings :py:meth:`help`.
1568 '''
1570 s = self.help().strip()
1572 if s:
1573 if s.startswith('<html>'):
1574 doc = qw.QLabel(s)
1575 else:
1576 try:
1577 import markdown
1578 doc = qw.QLabel(markdown.markdown(s))
1580 except ImportError:
1581 logger.error(
1582 'Install Python module "markdown" for pretty help '
1583 'formatting.')
1585 doc = qw.QLabel(s)
1586 else:
1587 doc = qw.QLabel('This snuffling does not provide any online help.')
1589 labels = [doc]
1591 if self._filename:
1592 from html import escape
1594 code = open(self._filename, 'r').read()
1596 doc_src = qw.QLabel(
1597 '''<html><body>
1598<hr />
1599<center><em>May the source be with you, young Skywalker!</em><br /><br />
1600<a href="file://%s"><code>%s</code></a></center>
1601<br />
1602<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1603<pre style="white-space: pre-wrap"><code>%s
1604</code></pre></p></body></html>'''
1605 % (
1606 quote(self._filename),
1607 escape(self._filename),
1608 escape(code)))
1610 labels.append(doc_src)
1612 for h in labels:
1613 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1614 h.setWordWrap(True)
1616 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1618 def live_update_toggled(self, on):
1619 '''
1620 Called when the checkbox for live-updates has been toggled.
1621 '''
1623 self.set_live_update(on)
1625 def add_traces(self, traces):
1626 '''
1627 Add traces to the viewer.
1629 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1631 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1632 added to the viewer's internal pile for display. Note, that unlike with
1633 the traces from the files given on the command line, these traces are
1634 kept in memory and so may quickly occupy a lot of ram if a lot of
1635 traces are added.
1637 This method should be preferred over modifying the viewer's internal
1638 pile directly, because this way, the snuffling has a chance to
1639 automatically remove its private traces again (see :py:meth:`cleanup`
1640 method).
1641 '''
1643 ticket = self.get_viewer().add_traces(traces)
1644 self._tickets.append(ticket)
1645 return ticket
1647 def add_trace(self, tr):
1648 '''
1649 Add a trace to the viewer.
1651 See :py:meth:`add_traces`.
1652 '''
1654 self.add_traces([tr])
1656 def add_markers(self, markers):
1657 '''
1658 Add some markers to the display.
1660 Takes a list of objects of type
1661 :py:class:`pyrocko.gui.snuffler.marker.Marker` and adds these to the
1662 viewer.
1663 '''
1665 self.get_viewer().add_markers(markers)
1666 self._markers.extend(markers)
1668 def add_marker(self, marker):
1669 '''
1670 Add a marker to the display.
1672 See :py:meth:`add_markers`.
1673 '''
1675 self.add_markers([marker])
1677 def cleanup(self):
1678 '''
1679 Remove all traces and markers which have been added so far by the
1680 snuffling.
1681 '''
1683 try:
1684 viewer = self.get_viewer()
1685 viewer.release_data(self._tickets)
1686 viewer.remove_markers(self._markers)
1688 except NoViewerSet:
1689 pass
1691 self._tickets = []
1692 self._markers = []
1694 def check_call(self, method):
1696 if method in self._call_in_progress:
1697 self.show_message('error', 'Previous action still in progress.')
1698 return
1700 try:
1701 self._call_in_progress[method] = True
1702 method()
1703 return 0
1705 except SnufflingError as e:
1706 if not isinstance(e, SnufflingCallFailed):
1707 # those have logged within error()
1708 logger.error('%s: %s' % (self._name, e))
1709 logger.error('%s: Snuffling action failed' % self._name)
1710 return 1
1712 except Exception as e:
1713 message = '%s: Snuffling action raised an exception: %s' % (
1714 self._name, str(e))
1716 logger.exception(message)
1717 self.show_message('error', message)
1719 finally:
1720 del self._call_in_progress[method]
1722 def call(self):
1723 '''
1724 Main work routine of the snuffling.
1726 This method is called when the snuffling's menu item has been triggered
1727 or when the user has played with the panel controls. To be overloaded
1728 in subclass. The default implementation does nothing useful.
1729 '''
1731 pass
1733 def pre_process_hook(self, traces):
1734 return traces
1736 def post_process_hook(self, traces):
1737 return traces
1739 def get_tpad(self):
1740 '''
1741 Return current amount of extra padding needed by live processing hooks.
1742 '''
1744 return 0.0
1746 def pre_destroy(self):
1747 '''
1748 Called when the snuffling instance is about to be deleted.
1750 Can be overloaded to do user-defined cleanup actions. The
1751 default implementation calls :py:meth:`cleanup` and deletes
1752 the snuffling`s tempory directory, if needed.
1753 '''
1755 self.cleanup()
1756 if self._tempdir is not None:
1757 import shutil
1758 shutil.rmtree(self._tempdir)
1761class SnufflingError(Exception):
1762 '''
1763 Base exception for Snuffling errors.
1764 '''
1765 pass
1768class NoViewerSet(SnufflingError):
1769 '''
1770 This exception is raised, when no viewer has been set on a Snuffling.
1771 '''
1773 def __str__(self):
1774 return 'No GUI available. ' \
1775 'Maybe this Snuffling cannot be run in command line mode?'
1778class MissingStationInformation(SnufflingError):
1779 '''
1780 Raised when station information is missing.
1781 '''
1784class NoTracesSelected(SnufflingError):
1785 '''
1786 This exception is raised, when no traces have been selected in the viewer
1787 and we cannot fallback to using the current view.
1788 '''
1790 def __str__(self):
1791 return 'No traces have been selected / are available.'
1794class UserCancelled(SnufflingError):
1795 '''
1796 This exception is raised, when the user has cancelled a snuffling dialog.
1797 '''
1799 def __str__(self):
1800 return 'The user has cancelled a dialog.'
1803class SnufflingCallFailed(SnufflingError):
1804 '''
1805 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1806 :py:meth:`Snuffling.call`.
1807 '''
1810class InvalidSnufflingFilename(Exception):
1811 pass
1814class SnufflingModule(object):
1815 '''
1816 Utility class to load/reload snufflings from a file.
1818 The snufflings are created by user modules which have the special function
1819 ``__snufflings__`` which return the snuffling instances to be
1820 exported. The snuffling module is attached to a handler class, which makes
1821 use of the snufflings (e.g.
1822 :py:class:`~pyrocko.gui.snuffler.pile_viewer.PileViewer` from
1823 ``pile_viewer.py``). The handler class must implement the methods
1824 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1825 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1826 :py:meth:`remove_snufflings` which may be called from the handler class,
1827 when needed.
1828 '''
1830 mtimes = {}
1832 def __init__(self, path, name, handler):
1833 self._path = path
1834 self._name = name
1835 self._mtime = None
1836 self._module = None
1837 self._snufflings = []
1838 self._handler = handler
1840 def load_if_needed(self):
1841 '''
1842 Called by Snuffler to check whether it has to reload the module.
1843 '''
1844 filename = os.path.join(self._path, self._name+'.py')
1846 try:
1847 mtime = os.stat(filename)[8]
1848 except OSError as e:
1849 if e.errno == 2:
1850 logger.error(e)
1851 raise BrokenSnufflingModule(filename)
1853 if self._module is None:
1854 sys.path[0:0] = [self._path]
1855 try:
1856 logger.debug('Loading snuffling module %s' % filename)
1857 if self._name in sys.modules:
1858 raise InvalidSnufflingFilename(self._name)
1860 self._module = __import__(self._name)
1861 del sys.modules[self._name]
1863 for snuffling in self._module.__snufflings__():
1864 snuffling._filename = filename
1865 self.add_snuffling(snuffling)
1867 except (Exception, SystemExit):
1868 logger.error(traceback.format_exc())
1869 raise BrokenSnufflingModule(filename)
1871 finally:
1872 sys.path[0:1] = []
1874 elif self._mtime != mtime:
1875 logger.warning('Reloading snuffling module %s' % filename)
1876 settings = self.remove_snufflings()
1877 sys.path[0:0] = [self._path]
1878 try:
1880 sys.modules[self._name] = self._module
1882 reload(self._module)
1883 del sys.modules[self._name]
1885 for snuffling in self._module.__snufflings__():
1886 snuffling._filename = filename
1887 self.add_snuffling(snuffling, reloaded=True)
1889 if len(self._snufflings) == len(settings):
1890 for sett, snuf in zip(settings, self._snufflings):
1891 snuf.set_settings(sett)
1893 except (Exception, SystemExit):
1894 logger.error(traceback.format_exc())
1895 raise BrokenSnufflingModule(filename)
1897 finally:
1898 sys.path[0:1] = []
1900 self._mtime = mtime
1902 def add_snuffling(self, snuffling, reloaded=False):
1903 '''
1904 Called by :py:meth:`load_if_needed` to add a snuffling.
1905 '''
1906 snuffling._path = self._path
1907 snuffling.setup()
1908 self._snufflings.append(snuffling)
1909 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1911 def remove_snufflings(self):
1912 '''
1913 Called by :py:meth:`load_if_needed` to remove all snufflings.
1914 '''
1915 settings = []
1916 for snuffling in self._snufflings:
1917 settings.append(snuffling.get_settings())
1918 self._handler.remove_snuffling(snuffling)
1920 self._snufflings = []
1921 return settings
1924class BrokenSnufflingModule(Exception):
1925 pass
1928class MyScrollArea(qw.QScrollArea):
1930 def sizeHint(self):
1931 '''
1932 '''
1934 s = qc.QSize()
1935 s.setWidth(self.widget().sizeHint().width())
1936 s.setHeight(self.widget().sizeHint().height())
1937 return s
1940class SwitchControl(qw.QCheckBox):
1941 sw_toggled = qc.pyqtSignal(object, bool)
1943 def __init__(self, ident, default, *args):
1944 qw.QCheckBox.__init__(self, *args)
1945 self.ident = ident
1946 self.setChecked(default)
1947 self.toggled.connect(self._sw_toggled)
1949 def _sw_toggled(self, state):
1950 self.sw_toggled.emit(self.ident, state)
1952 def set_value(self, state):
1953 self.blockSignals(True)
1954 self.setChecked(state)
1955 self.blockSignals(False)
1958class ChoiceControl(qw.QFrame):
1959 choosen = qc.pyqtSignal(object, object)
1961 def __init__(self, ident, default, choices, name, *args):
1962 qw.QFrame.__init__(self, *args)
1963 self.label = qw.QLabel(name, self)
1964 self.label.setMinimumWidth(120)
1965 self.cbox = qw.QComboBox(self)
1966 self.layout = qw.QHBoxLayout(self)
1967 self.layout.addWidget(self.label)
1968 self.layout.addWidget(self.cbox)
1969 self.layout.setContentsMargins(0, 0, 0, 0)
1970 self.layout.setSpacing(0)
1971 self.ident = ident
1972 self.choices = choices
1973 for ichoice, choice in enumerate(choices):
1974 self.cbox.addItem(choice)
1976 self.set_value(default)
1977 self.cbox.activated.connect(self.emit_choosen)
1979 def set_choices(self, choices):
1980 icur = self.cbox.currentIndex()
1981 if icur != -1:
1982 selected_choice = choices[icur]
1983 else:
1984 selected_choice = None
1986 self.choices = choices
1987 self.cbox.clear()
1988 for ichoice, choice in enumerate(choices):
1989 self.cbox.addItem(qc.QString(choice))
1991 if selected_choice is not None and selected_choice in choices:
1992 self.set_value(selected_choice)
1993 return selected_choice
1994 else:
1995 self.set_value(choices[0])
1996 return choices[0]
1998 def emit_choosen(self, i):
1999 self.choosen.emit(
2000 self.ident,
2001 self.choices[i])
2003 def set_value(self, v):
2004 self.cbox.blockSignals(True)
2005 for i, choice in enumerate(self.choices):
2006 if choice == v:
2007 self.cbox.setCurrentIndex(i)
2008 self.cbox.blockSignals(False)