Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/gui/snuffler/snuffler_app.py: 53%
507 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'''
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
32from pyrocko.gui import util as gui_util
34from . import pile_viewer # noqa
36from ..qt_compat import qc, qg, qw
38logger = logging.getLogger('pyrocko.gui.snuffler.snuffler_app')
41class AcquisitionThread(qc.QThread):
42 def __init__(self, post_process_sleep=0.0):
43 qc.QThread.__init__(self)
44 self.mutex = qc.QMutex()
45 self.queue = []
46 self.post_process_sleep = post_process_sleep
47 self._sun_is_shining = True
49 def get_wanted_poll_interval(self):
50 return 1000.
52 def run(self):
53 while True:
54 try:
55 self.acquisition_start()
56 while self._sun_is_shining:
57 t0 = time.time()
58 self.process()
59 t1 = time.time()
60 if self.post_process_sleep != 0.0:
61 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
63 self.acquisition_stop()
64 break
66 except (
67 edl.ReadError,
68 serial_hamster.SerialHamsterError,
69 slink.SlowSlinkError) as e:
71 logger.error(str(e))
72 logger.error('Acquistion terminated, restart in 5 s')
73 self.acquisition_stop()
74 time.sleep(5)
75 if not self._sun_is_shining:
76 break
78 def stop(self):
79 self._sun_is_shining = False
81 logger.debug('Waiting for thread to terminate...')
82 self.wait()
83 logger.debug('Thread has terminated.')
85 def got_trace(self, tr):
86 self.mutex.lock()
87 self.queue.append(tr)
88 self.mutex.unlock()
90 def poll(self):
91 self.mutex.lock()
92 items = self.queue[:]
93 self.queue[:] = []
94 self.mutex.unlock()
95 return items
98class SlinkAcquisition(
99 slink.SlowSlink, AcquisitionThread):
101 def __init__(self, *args, **kwargs):
102 slink.SlowSlink.__init__(self, *args, **kwargs)
103 AcquisitionThread.__init__(self)
105 def got_trace(self, tr):
106 AcquisitionThread.got_trace(self, tr)
109class CamAcquisition(
110 serial_hamster.CamSerialHamster, AcquisitionThread):
112 def __init__(self, *args, **kwargs):
113 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
114 AcquisitionThread.__init__(self, post_process_sleep=0.1)
116 def got_trace(self, tr):
117 AcquisitionThread.got_trace(self, tr)
120class USBHB628Acquisition(
121 serial_hamster.USBHB628Hamster, AcquisitionThread):
123 def __init__(self, deltat=0.02, *args, **kwargs):
124 serial_hamster.USBHB628Hamster.__init__(
125 self, deltat=deltat, *args, **kwargs)
126 AcquisitionThread.__init__(self)
128 def got_trace(self, tr):
129 AcquisitionThread.got_trace(self, tr)
132class SchoolSeismometerAcquisition(
133 serial_hamster.SerialHamster, AcquisitionThread):
135 def __init__(self, *args, **kwargs):
136 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
137 AcquisitionThread.__init__(self, post_process_sleep=0.0)
139 def got_trace(self, tr):
140 AcquisitionThread.got_trace(self, tr)
142 def get_wanted_poll_interval(self):
143 return 100.
146class EDLAcquisition(
147 edl.EDLHamster, AcquisitionThread):
149 def __init__(self, *args, **kwargs):
150 edl.EDLHamster.__init__(self, *args, **kwargs)
151 AcquisitionThread.__init__(self)
153 def got_trace(self, tr):
154 AcquisitionThread.got_trace(self, tr)
157class CubeAcquisition(
158 datacube.SerialCube, AcquisitionThread):
160 def __init__(self, *args, **kwargs):
161 datacube.SerialCube.__init__(self, *args, **kwargs)
162 AcquisitionThread.__init__(self)
164 def got_trace(self, tr):
165 AcquisitionThread.got_trace(self, tr)
168def setup_acquisition_sources(args):
170 sources = []
171 iarg = 0
172 while iarg < len(args):
173 arg = args[iarg]
175 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
176 mca = re.match(r'cam://([^:]+)', arg)
177 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
178 msc = re.match(r'school://([^:?]+)(\?([^?]+))?', arg)
179 med = re.match(r'edl://([^:]+)', arg)
180 mcu = re.match(r'cube://([^:]+)', arg)
182 if msl:
183 host = msl.group(1)
184 port = msl.group(3)
185 if not port:
186 port = '18000'
188 sl = SlinkAcquisition(host=host, port=port)
189 if msl.group(5):
190 stream_patterns = msl.group(5).split(',')
192 if '_' not in msl.group(5):
193 try:
194 streams = sl.query_streams()
195 except slink.SlowSlinkError as e:
196 logger.fatal(str(e))
197 sys.exit(1)
199 streams = list(set(
200 util.match_nslcs(stream_patterns, streams)))
202 for stream in streams:
203 sl.add_stream(*stream)
204 else:
205 for stream in stream_patterns:
206 sl.add_raw_stream_selector(stream)
208 sources.append(sl)
210 elif mca:
211 port = mca.group(1)
212 cam = CamAcquisition(port=port, deltat=0.0314504)
213 sources.append(cam)
215 elif mus:
216 port = mus.group(1)
217 try:
218 d = {}
219 if mus.group(3):
220 d = dict(parse_qsl(mus.group(3)))
222 deltat = 1.0/float(d.get('rate', '50'))
223 channels = [(int(c), c) for c in d.get('channels', '01234567')]
224 hb628 = USBHB628Acquisition(
225 port=port,
226 deltat=deltat,
227 channels=channels,
228 buffersize=16,
229 lookback=50)
231 sources.append(hb628)
232 except Exception:
233 sys.exit('invalid acquisition source: %s' % arg)
235 elif msc:
236 port = msc.group(1)
238 d = {}
239 if msc.group(3):
240 d = dict(parse_qsl(msc.group(3)))
242 d_rate = {
243 '20': ('a', 20.032),
244 '40': ('b', 39.860),
245 '80': ('c', 79.719)}
247 s_rate = d.get('rate', '80')
248 station = d.get('station', 'TEST')
250 if s_rate not in d_rate:
251 raise Exception(
252 'Unsupported rate: %s (expected "20", "40" or "80")'
253 % s_rate)
255 s_gain = d.get('gain', '4')
257 if s_gain not in ('1', '2', '4'):
258 raise Exception(
259 'Unsupported gain: %s (expected "1", "2" or "4")'
260 % s_gain)
262 start_string = s_gain + d_rate[s_rate][0]
263 deltat = 1.0 / d_rate[s_rate][1]
265 logger.info(
266 'School seismometer: trying to use device %s with gain=%s and '
267 'rate=%g.' % (port, s_gain, 1.0/deltat))
269 sco = SchoolSeismometerAcquisition(
270 port=port,
271 deltat=deltat,
272 start_string=start_string,
273 min_detection_size=50,
274 disallow_uneven_sampling_rates=False,
275 station=station)
277 sources.append(sco)
279 elif med:
280 port = med.group(1)
281 edl = EDLAcquisition(port=port)
282 sources.append(edl)
283 elif mcu:
284 device = mcu.group(1)
285 cube = CubeAcquisition(device=device)
286 sources.append(cube)
288 if msl or mca or mus or msc or med or mcu:
289 args.pop(iarg)
290 else:
291 iarg += 1
293 return sources
296class PollInjector(qc.QObject):
298 def __init__(self, *args, **kwargs):
299 interval = kwargs.pop('interval', 1000.)
300 qc.QObject.__init__(self)
301 self._injector = pile.Injector(*args, **kwargs)
302 self._sources = []
303 self.startTimer(int(interval))
305 def add_source(self, source):
306 self._sources.append(source)
308 def remove_source(self, source):
309 self._sources.remove(source)
311 def timerEvent(self, ev):
312 for source in self._sources:
313 trs = source.poll()
314 for tr in trs:
315 self._injector.inject(tr)
317 # following methods needed because mulitple inheritance does not seem
318 # to work anymore with QObject in Python3 or PyQt5
320 def set_fixation_length(self, length):
321 return self._injector.set_fixation_length(length)
323 def set_save_path(
324 self,
325 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
326 '%(tmin)s_%(tmax)s.mseed'):
328 return self._injector.set_save_path(path)
330 def fixate_all(self):
331 return self._injector.fixate_all()
333 def free(self):
334 return self._injector.free()
337class Connection(qc.QObject):
339 received = qc.pyqtSignal(object, object)
340 disconnected = qc.pyqtSignal(object)
342 def __init__(self, parent, sock):
343 qc.QObject.__init__(self, parent)
344 self.socket = sock
345 self.readyRead.connect(
346 self.handle_read)
347 self.disconnected.connect(
348 self.handle_disconnected)
349 self.nwanted = 8
350 self.reading_size = True
351 self.handler = None
352 self.nbytes_received = 0
353 self.nbytes_sent = 0
354 self.compressor = zlib.compressobj()
355 self.decompressor = zlib.decompressobj()
357 def handle_read(self):
358 while True:
359 navail = self.socket.bytesAvailable()
360 if navail < self.nwanted:
361 return
363 data = self.socket.read(self.nwanted)
364 self.nbytes_received += len(data)
365 if self.reading_size:
366 self.nwanted = struct.unpack('>Q', data)[0]
367 self.reading_size = False
368 else:
369 obj = pickle.loads(self.decompressor.decompress(data))
370 if obj is None:
371 self.socket.disconnectFromHost()
372 else:
373 self.handle_received(obj)
374 self.nwanted = 8
375 self.reading_size = True
377 def handle_received(self, obj):
378 self.received.emit(self, obj)
380 def ship(self, obj):
381 data = self.compressor.compress(pickle.dumps(obj))
382 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
383 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
384 self.socket.write(data)
385 self.socket.write(data_end)
386 self.nbytes_sent += len(data)+len(data_end) + 8
388 def handle_disconnected(self):
389 self.disconnected.emit(self)
391 def close(self):
392 self.socket.close()
395class ConnectionHandler(qc.QObject):
396 def __init__(self, parent):
397 qc.QObject.__init__(self, parent)
398 self.queue = []
399 self.connection = None
401 def connected(self):
402 return self.connection is None
404 def set_connection(self, connection):
405 self.connection = connection
406 connection.received.connect(
407 self._handle_received)
409 connection.connect(
410 self.handle_disconnected)
412 for obj in self.queue:
413 self.connection.ship(obj)
415 self.queue = []
417 def _handle_received(self, conn, obj):
418 self.handle_received(obj)
420 def handle_received(self, obj):
421 pass
423 def handle_disconnected(self):
424 self.connection = None
426 def ship(self, obj):
427 if self.connection:
428 self.connection.ship(obj)
429 else:
430 self.queue.append(obj)
433class SimpleConnectionHandler(ConnectionHandler):
434 def __init__(self, parent, **mapping):
435 ConnectionHandler.__init__(self, parent)
436 self.mapping = mapping
438 def handle_received(self, obj):
439 command = obj[0]
440 args = obj[1:]
441 self.mapping[command](*args)
444class MyMainWindow(qw.QMainWindow):
446 def __init__(self, app, *args):
447 qg.QMainWindow.__init__(self, *args)
448 self.app = app
450 def keyPressEvent(self, ev):
451 self.app.pile_viewer.get_view().keyPressEvent(ev)
454class SnufflerTabs(qw.QTabWidget):
455 def __init__(self, parent):
456 qw.QTabWidget.__init__(self, parent)
457 if hasattr(self, 'setTabsClosable'):
458 self.setTabsClosable(True)
460 self.tabCloseRequested.connect(
461 self.removeTab)
463 if hasattr(self, 'setDocumentMode'):
464 self.setDocumentMode(True)
466 def hide_close_button_on_first_tab(self):
467 tbar = self.tabBar()
468 if hasattr(tbar, 'setTabButton'):
469 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
470 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
472 def append_tab(self, widget, name):
473 widget.setParent(self)
474 self.insertTab(self.count(), widget, name)
475 self.setCurrentIndex(self.count()-1)
477 def remove_tab(self, widget):
478 self.removeTab(self.indexOf(widget))
480 def tabInserted(self, index):
481 if index == 0:
482 self.hide_close_button_on_first_tab()
484 self.tabbar_visibility()
485 self.setFocus()
487 def removeTab(self, index):
488 w = self.widget(index)
489 w.close()
490 qw.QTabWidget.removeTab(self, index)
492 def tabRemoved(self, index):
493 self.tabbar_visibility()
495 def tabbar_visibility(self):
496 if self.count() <= 1:
497 self.tabBar().hide()
498 elif self.count() > 1:
499 self.tabBar().show()
501 def keyPressEvent(self, event):
502 if event.text() == 'd':
503 i = self.currentIndex()
504 if i != 0:
505 self.tabCloseRequested.emit(i)
506 else:
507 self.parent().keyPressEvent(event)
510class SnufflerStartWizard(qw.QWizard):
512 def __init__(self, parent):
513 qw.QWizard.__init__(self, parent)
515 self.setOption(self.NoBackButtonOnStartPage)
516 self.setOption(self.NoBackButtonOnLastPage)
517 self.setOption(self.NoCancelButton)
518 self.addPageSurvey()
519 self.addPageHelp()
520 self.setWindowTitle('Welcome to Pyrocko')
522 def getSystemInfo(self):
523 import numpy
524 import scipy
525 import pyrocko
526 import platform
527 import uuid
528 data = {
529 'node-uuid': uuid.getnode(),
530 'platform.architecture': platform.architecture(),
531 'platform.system': platform.system(),
532 'platform.release': platform.release(),
533 'python': platform.python_version(),
534 'pyrocko': pyrocko.__version__,
535 'numpy': numpy.__version__,
536 'scipy': scipy.__version__,
537 'qt': qc.PYQT_VERSION_STR,
538 }
539 return data
541 def addPageSurvey(self):
542 import pprint
543 webtk = 'DSFGK234ADF4ASDF'
544 sys_info = self.getSystemInfo()
546 p = qw.QWizardPage()
547 p.setCommitPage(True)
548 p.setTitle('Thank you for installing Pyrocko!')
550 lyt = qw.QVBoxLayout()
551 lyt.addWidget(qw.QLabel(
552 '<p>Your feedback is important for'
553 ' the development and improvement of Pyrocko.</p>'
554 '<p>Do you want to send this system information anon'
555 'ymously to <a href="https://pyrocko.org">'
556 'https://pyrocko.org</a>?</p>'))
558 text_data = qw.QLabel(
559 '<code style="font-size: small;">%s</code>' %
560 pprint.pformat(
561 sys_info,
562 indent=1).replace('\n', '<br>')
563 )
564 text_data.setStyleSheet('padding: 10px;')
565 lyt.addWidget(text_data)
567 lyt.addWidget(qw.QLabel(
568 "This message won't be shown again.\n\n"
569 'We appreciate your contribution!\n- The Pyrocko Developers'
570 ))
572 p.setLayout(lyt)
573 p.setButtonText(self.CommitButton, 'No')
575 yes_btn = qw.QPushButton(p)
576 yes_btn.setText('Yes')
578 @qc.pyqtSlot()
579 def send_data():
580 import requests
581 import json
582 try:
583 requests.post('https://pyrocko.org/%s' % webtk,
584 data=json.dumps(sys_info))
585 except Exception as e:
586 print(e)
587 self.button(self.NextButton).clicked.emit(True)
589 self.customButtonClicked.connect(send_data)
591 self.setButton(self.CustomButton1, yes_btn)
592 self.setOption(self.HaveCustomButton1, True)
594 self.addPage(p)
595 return p
597 def addPageHelp(self):
598 p = qw.QWizardPage()
599 p.setTitle('Welcome to Snuffler!')
601 text = qw.QLabel('''<html>
602<h3>- <i>The Seismogram browser and workbench.</i></h3>
603<p>Looks like you are starting the Snuffler for the first time.<br>
604It allows you to browse and process large archives of waveform data.</p>
605<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
606<ul>
607 <li><b>Download seismograms</b> from GEOFON IRIS and others</li>
608 <li><b>Earthquake catalog</b> access to GEOFON, GlobalCMT, USGS...</li>
609 <li><b>Cake</b>, Calculate synthetic arrival times</li>
610 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
611 <li>
612 <b>Map</b>, swiftly inspect stations and events on interactive maps
613 </li>
614</ul>
615<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
616<p><b>NOTE:</b><br>If you installed snufflings from the
617<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
618snufflings repository</a><br>you also have to pull an update from there.
619</p>
620<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
621 align="center">
622 <b>You can always press <code>?</code> for help!</b>
623</p>
624</html>''')
626 lyt = qw.QVBoxLayout()
627 lyt.addWidget(text)
629 def remove_custom_button():
630 self.setOption(self.HaveCustomButton1, False)
632 p.initializePage = remove_custom_button
634 p.setLayout(lyt)
635 self.addPage(p)
636 return p
639class SnufflerWindow(qw.QMainWindow):
641 def __init__(
642 self, pile, stations=None, events=None, markers=None, ntracks=12,
643 marker_editor_sortable=True, follow=None, controls=True,
644 opengl=None, instant_close=False):
646 qw.QMainWindow.__init__(self)
648 self.instant_close = instant_close
650 self.dockwidget_to_toggler = {}
651 self.dockwidgets = []
653 self.setWindowTitle('Snuffler')
655 self.pile_viewer = pile_viewer.PileViewer(
656 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
657 marker_editor_sortable=marker_editor_sortable,
658 panel_parent=self)
660 self.marker_editor = self.pile_viewer.marker_editor()
661 self.add_panel(
662 'Markers', self.marker_editor, visible=False,
663 where=qc.Qt.RightDockWidgetArea)
664 if stations:
665 self.get_view().add_stations(stations)
667 if events:
668 self.get_view().add_events(events)
670 if len(events) == 1:
671 self.get_view().set_active_event(events[0])
673 if markers:
674 self.get_view().add_markers(markers)
675 self.get_view().associate_phases_to_events()
677 self.tabs = SnufflerTabs(self)
678 self.setCentralWidget(self.tabs)
679 self.add_tab('Main', self.pile_viewer)
681 self.pile_viewer.setup_snufflings()
682 self.setMenuBar(self.pile_viewer.menu)
684 self.main_controls = self.pile_viewer.controls()
685 self.add_panel('Main Controls', self.main_controls, visible=controls)
686 self.show()
688 self.get_view().setFocus(qc.Qt.OtherFocusReason)
690 self.status_messages = gui_util.StatusMessages()
691 self.statusBar().addPermanentWidget(self.status_messages)
693 self.status_messages.set(
694 'welcome', 'Welcome to Snuffler! Press ? for help.')
696 snuffler_config = self.pile_viewer.viewer.config
698 if snuffler_config.first_start:
699 wizard = SnufflerStartWizard(self)
701 @qc.pyqtSlot()
702 def wizard_finished(result):
703 if result == wizard.Accepted:
704 snuffler_config.first_start = False
705 config.write_config(snuffler_config, 'snuffler')
707 wizard.finished.connect(wizard_finished)
709 wizard.show()
711 if follow:
712 self.get_view().follow(float(follow))
714 self.closing = False
716 def sizeHint(self):
717 return qc.QSize(1024, 768)
718 # return qc.QSize(800, 600) # used for screen shots in tutorial
720 def keyPressEvent(self, ev):
721 self.get_view().keyPressEvent(ev)
723 def get_view(self):
724 return self.pile_viewer.get_view()
726 def get_panel_parent_widget(self):
727 return self
729 def add_tab(self, name, widget):
730 self.tabs.append_tab(widget, name)
732 def remove_tab(self, widget):
733 self.tabs.remove_tab(widget)
735 def add_panel(self, name, panel, visible=False, volatile=False,
736 where=qc.Qt.BottomDockWidgetArea):
738 if not self.dockwidgets:
739 self.dockwidgets = []
741 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
743 dockwidget = qw.QDockWidget(name, self)
744 self.dockwidgets.append(dockwidget)
745 dockwidget.setWidget(panel)
746 panel.setParent(dockwidget)
747 self.addDockWidget(where, dockwidget)
749 if dws:
750 self.tabifyDockWidget(dws[-1], dockwidget)
752 self.toggle_panel(dockwidget, visible)
754 mitem = qw.QAction(name, None)
756 def toggle_panel(checked):
757 self.toggle_panel(dockwidget, True)
759 mitem.triggered.connect(toggle_panel)
761 if volatile:
762 def visibility(visible):
763 if not visible:
764 self.remove_panel(panel)
766 dockwidget.visibilityChanged.connect(
767 visibility)
769 self.get_view().add_panel_toggler(mitem)
770 self.dockwidget_to_toggler[dockwidget] = mitem
772 if pile_viewer.is_macos:
773 tabbars = self.findChildren(qw.QTabBar)
774 for tabbar in tabbars:
775 tabbar.setShape(qw.QTabBar.TriangularNorth)
776 tabbar.setDocumentMode(True)
778 def toggle_panel(self, dockwidget, visible):
779 if visible is None:
780 visible = not dockwidget.isVisible()
782 dockwidget.setVisible(visible)
783 if visible:
784 w = dockwidget.widget()
785 minsize = w.minimumSize()
786 w.setMinimumHeight(w.sizeHint().height() + 5)
788 def reset_minimum_size():
789 import sip
790 if not sip.isdeleted(w):
791 w.setMinimumSize(minsize)
793 qc.QTimer.singleShot(200, reset_minimum_size)
795 dockwidget.setFocus()
796 dockwidget.raise_()
798 def toggle_marker_editor(self):
799 self.toggle_panel(self.marker_editor.parent(), None)
801 def toggle_main_controls(self):
802 self.toggle_panel(self.main_controls.parent(), None)
804 def remove_panel(self, panel):
805 dockwidget = panel.parent()
806 self.removeDockWidget(dockwidget)
807 dockwidget.setParent(None)
808 mitem = self.dockwidget_to_toggler[dockwidget]
809 self.get_view().remove_panel_toggler(mitem)
811 def return_tag(self):
812 return self.get_view().return_tag
814 def confirm_close(self):
815 ret = qw.QMessageBox.question(
816 self,
817 'Snuffler',
818 'Close Snuffler window?',
819 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
820 qw.QMessageBox.Ok)
822 return ret == qw.QMessageBox.Ok
824 def closeEvent(self, event):
825 if self.instant_close or self.confirm_close():
826 self.closing = True
827 self.pile_viewer.cleanup()
828 event.accept()
829 else:
830 event.ignore()
832 def is_closing(self):
833 return self.closing