Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffler_app.py: 53%
506 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 15:01 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-10-06 15:01 +0000
1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Effective seismological trace viewer.
8'''
10import sys
11import logging
12import time
13import re
14import zlib
15import struct
16import pickle
17try:
18 from urlparse import parse_qsl
19except ImportError:
20 from urllib.parse import parse_qsl
22from pyrocko.streaming import serial_hamster
23from pyrocko.streaming import slink
24from pyrocko.streaming import edl
25from pyrocko.streaming import datacube
27from pyrocko import pile # noqa
28from pyrocko import util # noqa
29from pyrocko import model # noqa
30from pyrocko import config # noqa
31from pyrocko import io # noqa
33from . import pile_viewer # noqa
35from ..qt_compat import qc, qg, qw
37logger = logging.getLogger('pyrocko.gui.snuffler.snuffler_app')
40class AcquisitionThread(qc.QThread):
41 def __init__(self, post_process_sleep=0.0):
42 qc.QThread.__init__(self)
43 self.mutex = qc.QMutex()
44 self.queue = []
45 self.post_process_sleep = post_process_sleep
46 self._sun_is_shining = True
48 def get_wanted_poll_interval(self):
49 return 1000.
51 def run(self):
52 while True:
53 try:
54 self.acquisition_start()
55 while self._sun_is_shining:
56 t0 = time.time()
57 self.process()
58 t1 = time.time()
59 if self.post_process_sleep != 0.0:
60 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
62 self.acquisition_stop()
63 break
65 except (
66 edl.ReadError,
67 serial_hamster.SerialHamsterError,
68 slink.SlowSlinkError) as e:
70 logger.error(str(e))
71 logger.error('Acquistion terminated, restart in 5 s')
72 self.acquisition_stop()
73 time.sleep(5)
74 if not self._sun_is_shining:
75 break
77 def stop(self):
78 self._sun_is_shining = False
80 logger.debug('Waiting for thread to terminate...')
81 self.wait()
82 logger.debug('Thread has terminated.')
84 def got_trace(self, tr):
85 self.mutex.lock()
86 self.queue.append(tr)
87 self.mutex.unlock()
89 def poll(self):
90 self.mutex.lock()
91 items = self.queue[:]
92 self.queue[:] = []
93 self.mutex.unlock()
94 return items
97class SlinkAcquisition(
98 slink.SlowSlink, AcquisitionThread):
100 def __init__(self, *args, **kwargs):
101 slink.SlowSlink.__init__(self, *args, **kwargs)
102 AcquisitionThread.__init__(self)
104 def got_trace(self, tr):
105 AcquisitionThread.got_trace(self, tr)
108class CamAcquisition(
109 serial_hamster.CamSerialHamster, AcquisitionThread):
111 def __init__(self, *args, **kwargs):
112 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
113 AcquisitionThread.__init__(self, post_process_sleep=0.1)
115 def got_trace(self, tr):
116 AcquisitionThread.got_trace(self, tr)
119class USBHB628Acquisition(
120 serial_hamster.USBHB628Hamster, AcquisitionThread):
122 def __init__(self, deltat=0.02, *args, **kwargs):
123 serial_hamster.USBHB628Hamster.__init__(
124 self, deltat=deltat, *args, **kwargs)
125 AcquisitionThread.__init__(self)
127 def got_trace(self, tr):
128 AcquisitionThread.got_trace(self, tr)
131class SchoolSeismometerAcquisition(
132 serial_hamster.SerialHamster, AcquisitionThread):
134 def __init__(self, *args, **kwargs):
135 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
136 AcquisitionThread.__init__(self, post_process_sleep=0.0)
138 def got_trace(self, tr):
139 AcquisitionThread.got_trace(self, tr)
141 def get_wanted_poll_interval(self):
142 return 100.
145class EDLAcquisition(
146 edl.EDLHamster, AcquisitionThread):
148 def __init__(self, *args, **kwargs):
149 edl.EDLHamster.__init__(self, *args, **kwargs)
150 AcquisitionThread.__init__(self)
152 def got_trace(self, tr):
153 AcquisitionThread.got_trace(self, tr)
156class CubeAcquisition(
157 datacube.SerialCube, AcquisitionThread):
159 def __init__(self, *args, **kwargs):
160 datacube.SerialCube.__init__(self, *args, **kwargs)
161 AcquisitionThread.__init__(self)
163 def got_trace(self, tr):
164 AcquisitionThread.got_trace(self, tr)
167def setup_acquisition_sources(args):
169 sources = []
170 iarg = 0
171 while iarg < len(args):
172 arg = args[iarg]
174 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
175 mca = re.match(r'cam://([^:]+)', arg)
176 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
177 msc = re.match(r'school://([^:?]+)(\?([^?]+))?', arg)
178 med = re.match(r'edl://([^:]+)', arg)
179 mcu = re.match(r'cube://([^:]+)', arg)
181 if msl:
182 host = msl.group(1)
183 port = msl.group(3)
184 if not port:
185 port = '18000'
187 sl = SlinkAcquisition(host=host, port=port)
188 if msl.group(5):
189 stream_patterns = msl.group(5).split(',')
191 if '_' not in msl.group(5):
192 try:
193 streams = sl.query_streams()
194 except slink.SlowSlinkError as e:
195 logger.fatal(str(e))
196 sys.exit(1)
198 streams = list(set(
199 util.match_nslcs(stream_patterns, streams)))
201 for stream in streams:
202 sl.add_stream(*stream)
203 else:
204 for stream in stream_patterns:
205 sl.add_raw_stream_selector(stream)
207 sources.append(sl)
209 elif mca:
210 port = mca.group(1)
211 cam = CamAcquisition(port=port, deltat=0.0314504)
212 sources.append(cam)
214 elif mus:
215 port = mus.group(1)
216 try:
217 d = {}
218 if mus.group(3):
219 d = dict(parse_qsl(mus.group(3)))
221 deltat = 1.0/float(d.get('rate', '50'))
222 channels = [(int(c), c) for c in d.get('channels', '01234567')]
223 hb628 = USBHB628Acquisition(
224 port=port,
225 deltat=deltat,
226 channels=channels,
227 buffersize=16,
228 lookback=50)
230 sources.append(hb628)
231 except Exception:
232 sys.exit('invalid acquisition source: %s' % arg)
234 elif msc:
235 port = msc.group(1)
237 d = {}
238 if msc.group(3):
239 d = dict(parse_qsl(msc.group(3)))
241 d_rate = {
242 '20': ('a', 20.032),
243 '40': ('b', 39.860),
244 '80': ('c', 79.719)}
246 s_rate = d.get('rate', '80')
247 station = d.get('station', 'TEST')
249 if s_rate not in d_rate:
250 raise Exception(
251 'Unsupported rate: %s (expected "20", "40" or "80")'
252 % s_rate)
254 s_gain = d.get('gain', '4')
256 if s_gain not in ('1', '2', '4'):
257 raise Exception(
258 'Unsupported gain: %s (expected "1", "2" or "4")'
259 % s_gain)
261 start_string = s_gain + d_rate[s_rate][0]
262 deltat = 1.0 / d_rate[s_rate][1]
264 logger.info(
265 'School seismometer: trying to use device %s with gain=%s and '
266 'rate=%g.' % (port, s_gain, 1.0/deltat))
268 sco = SchoolSeismometerAcquisition(
269 port=port,
270 deltat=deltat,
271 start_string=start_string,
272 min_detection_size=50,
273 disallow_uneven_sampling_rates=False,
274 station=station)
276 sources.append(sco)
278 elif med:
279 port = med.group(1)
280 edl = EDLAcquisition(port=port)
281 sources.append(edl)
282 elif mcu:
283 device = mcu.group(1)
284 cube = CubeAcquisition(device=device)
285 sources.append(cube)
287 if msl or mca or mus or msc or med or mcu:
288 args.pop(iarg)
289 else:
290 iarg += 1
292 return sources
295class PollInjector(qc.QObject):
297 def __init__(self, *args, **kwargs):
298 interval = kwargs.pop('interval', 1000.)
299 qc.QObject.__init__(self)
300 self._injector = pile.Injector(*args, **kwargs)
301 self._sources = []
302 self.startTimer(int(interval))
304 def add_source(self, source):
305 self._sources.append(source)
307 def remove_source(self, source):
308 self._sources.remove(source)
310 def timerEvent(self, ev):
311 for source in self._sources:
312 trs = source.poll()
313 for tr in trs:
314 self._injector.inject(tr)
316 # following methods needed because mulitple inheritance does not seem
317 # to work anymore with QObject in Python3 or PyQt5
319 def set_fixation_length(self, length):
320 return self._injector.set_fixation_length(length)
322 def set_save_path(
323 self,
324 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
325 '%(tmin)s_%(tmax)s.mseed'):
327 return self._injector.set_save_path(path)
329 def fixate_all(self):
330 return self._injector.fixate_all()
332 def free(self):
333 return self._injector.free()
336class Connection(qc.QObject):
338 received = qc.pyqtSignal(object, object)
339 disconnected = qc.pyqtSignal(object)
341 def __init__(self, parent, sock):
342 qc.QObject.__init__(self, parent)
343 self.socket = sock
344 self.readyRead.connect(
345 self.handle_read)
346 self.disconnected.connect(
347 self.handle_disconnected)
348 self.nwanted = 8
349 self.reading_size = True
350 self.handler = None
351 self.nbytes_received = 0
352 self.nbytes_sent = 0
353 self.compressor = zlib.compressobj()
354 self.decompressor = zlib.decompressobj()
356 def handle_read(self):
357 while True:
358 navail = self.socket.bytesAvailable()
359 if navail < self.nwanted:
360 return
362 data = self.socket.read(self.nwanted)
363 self.nbytes_received += len(data)
364 if self.reading_size:
365 self.nwanted = struct.unpack('>Q', data)[0]
366 self.reading_size = False
367 else:
368 obj = pickle.loads(self.decompressor.decompress(data))
369 if obj is None:
370 self.socket.disconnectFromHost()
371 else:
372 self.handle_received(obj)
373 self.nwanted = 8
374 self.reading_size = True
376 def handle_received(self, obj):
377 self.received.emit(self, obj)
379 def ship(self, obj):
380 data = self.compressor.compress(pickle.dumps(obj))
381 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
382 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
383 self.socket.write(data)
384 self.socket.write(data_end)
385 self.nbytes_sent += len(data)+len(data_end) + 8
387 def handle_disconnected(self):
388 self.disconnected.emit(self)
390 def close(self):
391 self.socket.close()
394class ConnectionHandler(qc.QObject):
395 def __init__(self, parent):
396 qc.QObject.__init__(self, parent)
397 self.queue = []
398 self.connection = None
400 def connected(self):
401 return self.connection is None
403 def set_connection(self, connection):
404 self.connection = connection
405 connection.received.connect(
406 self._handle_received)
408 connection.connect(
409 self.handle_disconnected)
411 for obj in self.queue:
412 self.connection.ship(obj)
414 self.queue = []
416 def _handle_received(self, conn, obj):
417 self.handle_received(obj)
419 def handle_received(self, obj):
420 pass
422 def handle_disconnected(self):
423 self.connection = None
425 def ship(self, obj):
426 if self.connection:
427 self.connection.ship(obj)
428 else:
429 self.queue.append(obj)
432class SimpleConnectionHandler(ConnectionHandler):
433 def __init__(self, parent, **mapping):
434 ConnectionHandler.__init__(self, parent)
435 self.mapping = mapping
437 def handle_received(self, obj):
438 command = obj[0]
439 args = obj[1:]
440 self.mapping[command](*args)
443class MyMainWindow(qw.QMainWindow):
445 def __init__(self, app, *args):
446 qg.QMainWindow.__init__(self, *args)
447 self.app = app
449 def keyPressEvent(self, ev):
450 self.app.pile_viewer.get_view().keyPressEvent(ev)
453class SnufflerTabs(qw.QTabWidget):
454 def __init__(self, parent):
455 qw.QTabWidget.__init__(self, parent)
456 if hasattr(self, 'setTabsClosable'):
457 self.setTabsClosable(True)
459 self.tabCloseRequested.connect(
460 self.removeTab)
462 if hasattr(self, 'setDocumentMode'):
463 self.setDocumentMode(True)
465 def hide_close_button_on_first_tab(self):
466 tbar = self.tabBar()
467 if hasattr(tbar, 'setTabButton'):
468 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
469 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
471 def append_tab(self, widget, name):
472 widget.setParent(self)
473 self.insertTab(self.count(), widget, name)
474 self.setCurrentIndex(self.count()-1)
476 def remove_tab(self, widget):
477 self.removeTab(self.indexOf(widget))
479 def tabInserted(self, index):
480 if index == 0:
481 self.hide_close_button_on_first_tab()
483 self.tabbar_visibility()
484 self.setFocus()
486 def removeTab(self, index):
487 w = self.widget(index)
488 w.close()
489 qw.QTabWidget.removeTab(self, index)
491 def tabRemoved(self, index):
492 self.tabbar_visibility()
494 def tabbar_visibility(self):
495 if self.count() <= 1:
496 self.tabBar().hide()
497 elif self.count() > 1:
498 self.tabBar().show()
500 def keyPressEvent(self, event):
501 if event.text() == 'd':
502 i = self.currentIndex()
503 if i != 0:
504 self.tabCloseRequested.emit(i)
505 else:
506 self.parent().keyPressEvent(event)
509class SnufflerStartWizard(qw.QWizard):
511 def __init__(self, parent):
512 qw.QWizard.__init__(self, parent)
514 self.setOption(self.NoBackButtonOnStartPage)
515 self.setOption(self.NoBackButtonOnLastPage)
516 self.setOption(self.NoCancelButton)
517 self.addPageSurvey()
518 self.addPageHelp()
519 self.setWindowTitle('Welcome to Pyrocko')
521 def getSystemInfo(self):
522 import numpy
523 import scipy
524 import pyrocko
525 import platform
526 import uuid
527 data = {
528 'node-uuid': uuid.getnode(),
529 'platform.architecture': platform.architecture(),
530 'platform.system': platform.system(),
531 'platform.release': platform.release(),
532 'python': platform.python_version(),
533 'pyrocko': pyrocko.__version__,
534 'numpy': numpy.__version__,
535 'scipy': scipy.__version__,
536 'qt': qc.PYQT_VERSION_STR,
537 }
538 return data
540 def addPageSurvey(self):
541 import pprint
542 webtk = 'DSFGK234ADF4ASDF'
543 sys_info = self.getSystemInfo()
545 p = qw.QWizardPage()
546 p.setCommitPage(True)
547 p.setTitle('Thank you for installing Pyrocko!')
549 lyt = qw.QVBoxLayout()
550 lyt.addWidget(qw.QLabel(
551 '<p>Your feedback is important for'
552 ' the development and improvement of Pyrocko.</p>'
553 '<p>Do you want to send this system information anon'
554 'ymously to <a href="https://pyrocko.org">'
555 'https://pyrocko.org</a>?</p>'))
557 text_data = qw.QLabel(
558 '<code style="font-size: small;">%s</code>' %
559 pprint.pformat(
560 sys_info,
561 indent=1).replace('\n', '<br>')
562 )
563 text_data.setStyleSheet('padding: 10px;')
564 lyt.addWidget(text_data)
566 lyt.addWidget(qw.QLabel(
567 "This message won't be shown again.\n\n"
568 'We appreciate your contribution!\n- The Pyrocko Developers'
569 ))
571 p.setLayout(lyt)
572 p.setButtonText(self.CommitButton, 'No')
574 yes_btn = qw.QPushButton(p)
575 yes_btn.setText('Yes')
577 @qc.pyqtSlot()
578 def send_data():
579 import requests
580 import json
581 try:
582 requests.post('https://pyrocko.org/%s' % webtk,
583 data=json.dumps(sys_info))
584 except Exception as e:
585 print(e)
586 self.button(self.NextButton).clicked.emit(True)
588 self.customButtonClicked.connect(send_data)
590 self.setButton(self.CustomButton1, yes_btn)
591 self.setOption(self.HaveCustomButton1, True)
593 self.addPage(p)
594 return p
596 def addPageHelp(self):
597 p = qw.QWizardPage()
598 p.setTitle('Welcome to Snuffler!')
600 text = qw.QLabel('''<html>
601<h3>- <i>The Seismogram browser and workbench.</i></h3>
602<p>Looks like you are starting the Snuffler for the first time.<br>
603It allows you to browse and process large archives of waveform data.</p>
604<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
605<ul>
606 <li><b>Download seismograms</b> from GEOFON IRIS and others</li>
607 <li><b>Earthquake catalog</b> access to GEOFON, GlobalCMT, USGS...</li>
608 <li><b>Cake</b>, Calculate synthetic arrival times</li>
609 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
610 <li>
611 <b>Map</b>, swiftly inspect stations and events on interactive maps
612 </li>
613</ul>
614<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
615<p><b>NOTE:</b><br>If you installed snufflings from the
616<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
617snufflings repository</a><br>you also have to pull an update from there.
618</p>
619<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
620 align="center">
621 <b>You can always press <code>?</code> for help!</b>
622</p>
623</html>''')
625 lyt = qw.QVBoxLayout()
626 lyt.addWidget(text)
628 def remove_custom_button():
629 self.setOption(self.HaveCustomButton1, False)
631 p.initializePage = remove_custom_button
633 p.setLayout(lyt)
634 self.addPage(p)
635 return p
638class SnufflerWindow(qw.QMainWindow):
640 def __init__(
641 self, pile, stations=None, events=None, markers=None, ntracks=12,
642 marker_editor_sortable=True, follow=None, controls=True,
643 opengl=None, instant_close=False):
645 qw.QMainWindow.__init__(self)
647 self.instant_close = instant_close
649 self.dockwidget_to_toggler = {}
650 self.dockwidgets = []
652 self.setWindowTitle('Snuffler')
654 self.pile_viewer = pile_viewer.PileViewer(
655 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
656 marker_editor_sortable=marker_editor_sortable,
657 panel_parent=self)
659 self.marker_editor = self.pile_viewer.marker_editor()
660 self.add_panel(
661 'Markers', self.marker_editor, visible=False,
662 where=qc.Qt.RightDockWidgetArea)
663 if stations:
664 self.get_view().add_stations(stations)
666 if events:
667 self.get_view().add_events(events)
669 if len(events) == 1:
670 self.get_view().set_active_event(events[0])
672 if markers:
673 self.get_view().add_markers(markers)
674 self.get_view().associate_phases_to_events()
676 self.tabs = SnufflerTabs(self)
677 self.setCentralWidget(self.tabs)
678 self.add_tab('Main', self.pile_viewer)
680 self.pile_viewer.setup_snufflings()
681 self.setMenuBar(self.pile_viewer.menu)
683 self.main_controls = self.pile_viewer.controls()
684 self.add_panel('Main Controls', self.main_controls, visible=controls)
685 self.show()
687 self.get_view().setFocus(qc.Qt.OtherFocusReason)
689 sb = self.statusBar()
690 sb.clearMessage()
691 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
693 snuffler_config = self.pile_viewer.viewer.config
695 if snuffler_config.first_start:
696 wizard = SnufflerStartWizard(self)
698 @qc.pyqtSlot()
699 def wizard_finished(result):
700 if result == wizard.Accepted:
701 snuffler_config.first_start = False
702 config.write_config(snuffler_config, 'snuffler')
704 wizard.finished.connect(wizard_finished)
706 wizard.show()
708 if follow:
709 self.get_view().follow(float(follow))
711 self.closing = False
713 def sizeHint(self):
714 return qc.QSize(1024, 768)
715 # return qc.QSize(800, 600) # used for screen shots in tutorial
717 def keyPressEvent(self, ev):
718 self.get_view().keyPressEvent(ev)
720 def get_view(self):
721 return self.pile_viewer.get_view()
723 def get_panel_parent_widget(self):
724 return self
726 def add_tab(self, name, widget):
727 self.tabs.append_tab(widget, name)
729 def remove_tab(self, widget):
730 self.tabs.remove_tab(widget)
732 def add_panel(self, name, panel, visible=False, volatile=False,
733 where=qc.Qt.BottomDockWidgetArea):
735 if not self.dockwidgets:
736 self.dockwidgets = []
738 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
740 dockwidget = qw.QDockWidget(name, self)
741 self.dockwidgets.append(dockwidget)
742 dockwidget.setWidget(panel)
743 panel.setParent(dockwidget)
744 self.addDockWidget(where, dockwidget)
746 if dws:
747 self.tabifyDockWidget(dws[-1], dockwidget)
749 self.toggle_panel(dockwidget, visible)
751 mitem = qw.QAction(name, None)
753 def toggle_panel(checked):
754 self.toggle_panel(dockwidget, True)
756 mitem.triggered.connect(toggle_panel)
758 if volatile:
759 def visibility(visible):
760 if not visible:
761 self.remove_panel(panel)
763 dockwidget.visibilityChanged.connect(
764 visibility)
766 self.get_view().add_panel_toggler(mitem)
767 self.dockwidget_to_toggler[dockwidget] = mitem
769 if pile_viewer.is_macos:
770 tabbars = self.findChildren(qw.QTabBar)
771 for tabbar in tabbars:
772 tabbar.setShape(qw.QTabBar.TriangularNorth)
773 tabbar.setDocumentMode(True)
775 def toggle_panel(self, dockwidget, visible):
776 if visible is None:
777 visible = not dockwidget.isVisible()
779 dockwidget.setVisible(visible)
780 if visible:
781 w = dockwidget.widget()
782 minsize = w.minimumSize()
783 w.setMinimumHeight(w.sizeHint().height() + 5)
785 def reset_minimum_size():
786 import sip
787 if not sip.isdeleted(w):
788 w.setMinimumSize(minsize)
790 qc.QTimer.singleShot(200, reset_minimum_size)
792 dockwidget.setFocus()
793 dockwidget.raise_()
795 def toggle_marker_editor(self):
796 self.toggle_panel(self.marker_editor.parent(), None)
798 def toggle_main_controls(self):
799 self.toggle_panel(self.main_controls.parent(), None)
801 def remove_panel(self, panel):
802 dockwidget = panel.parent()
803 self.removeDockWidget(dockwidget)
804 dockwidget.setParent(None)
805 mitem = self.dockwidget_to_toggler[dockwidget]
806 self.get_view().remove_panel_toggler(mitem)
808 def return_tag(self):
809 return self.get_view().return_tag
811 def confirm_close(self):
812 ret = qw.QMessageBox.question(
813 self,
814 'Snuffler',
815 'Close Snuffler window?',
816 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
817 qw.QMessageBox.Ok)
819 return ret == qw.QMessageBox.Ok
821 def closeEvent(self, event):
822 if self.instant_close or self.confirm_close():
823 self.closing = True
824 self.pile_viewer.cleanup()
825 event.accept()
826 else:
827 event.ignore()
829 def is_closing(self):
830 return self.closing